fix(rust): keep an in-progress edit across list updates; app-local Zig cache

A list fragment arriving while an item is being edited now patches the
list around that li instead of replacing it, so the draft and focus stay.
make sets ZIG_GLOBAL_CACHE_DIR to .cache/zig, and clean-deps removes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6d3ad8a577364b702286c800618617e741203e18 parent 862ef0f
2 files changed, +39 −8
  • apps/rust/Makefile +7 −4
  • apps/rust/static/app.js +32 −4
diff --git a/apps/rust/Makefile b/apps/rust/Makefile
index 1425ee6..20ab24e 100644
--- a/apps/rust/Makefile
+++ b/apps/rust/Makefile
@@ -1,10 +1,13 @@
-# todo-rust. Cargo's build output lives in .cache/target (git-ignored);
-# downloaded crate sources stay in ~/.cargo/registry.
+# todo-rust. Cargo's build output lives in .cache/target and Zig's compiled
+# libraries (musl, compiler-rt) in .cache/zig, both git-ignored; downloaded
+# crate sources stay in ~/.cargo/registry.
SHELL := /bin/bash
TARGET_DIR := .cache/target
+ZIG_CACHE := $(CURDIR)/.cache/zig
ARM64 := aarch64-unknown-linux-musl
VERSION := $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
-CARGO := source ../../scripts/env.sh && APP_VERSION=$(VERSION) CARGO_TARGET_DIR=$(TARGET_DIR) cargo
+CARGO := source ../../scripts/env.sh && APP_VERSION=$(VERSION) CARGO_TARGET_DIR=$(TARGET_DIR) \
+ ZIG_GLOBAL_CACHE_DIR=$(ZIG_CACHE) ZIG_LOCAL_CACHE_DIR=$(ZIG_CACHE) cargo
APP := todo-rust
HOST := todo-rust.bfj.oncarlos.com
MESSAGE ?= Deploy $(VERSION)
@@ -30,7 +33,7 @@ clean:
# Every compiled artifact, dependencies included. Sources stay downloaded.
clean-deps: clean
- rm -rf $(TARGET_DIR)
+ rm -rf $(TARGET_DIR) $(ZIG_CACHE)
bump:
sed -i 's/^const BUMP: &str = "unbumped";/const BUMP: \&str = "bumped";/' src/app.rs
diff --git a/apps/rust/static/app.js b/apps/rust/static/app.js
index 994fcdd..1bff4c6 100644
--- a/apps/rust/static/app.js
+++ b/apps/rust/static/app.js
@@ -23,13 +23,41 @@ function send(action, body) {
}
// Replaces section.main and footer.footer with the server's fresh copies.
+// An edit still in progress (not yet sent) survives: its li stays in the
+// page, focus and draft included, and the fresh items go around it.
function swap(html) {
const tpl = document.createElement('template');
tpl.innerHTML = html;
- for (const sel of ['section.main', 'footer.footer']) {
- const fresh = tpl.content.querySelector(sel);
- const old = document.querySelector(`.todoapp ${sel}`);
- if (fresh && old) old.replaceWith(fresh);
+ const freshMain = tpl.content.querySelector('section.main');
+ const freshFooter = tpl.content.querySelector('footer.footer');
+ const main = document.querySelector('.todoapp section.main');
+ const footer = document.querySelector('.todoapp footer.footer');
+ if (!freshMain || !freshFooter || !main || !footer) return;
+ footer.replaceWith(freshFooter);
+
+ const editing = [...main.querySelectorAll('.todo-list li.editing')]
+ .find((li) => !li.querySelector('.edit').dataset.done);
+ const keep = editing && freshMain.querySelector(`.todo-list li[data-id="${editing.dataset.id}"]`);
+ if (!keep) {
+ // Gone from this view: drop the edit without the blur saving it.
+ if (editing) editing.querySelector('.edit').dataset.done = '1';
+ main.replaceWith(freshMain);
+ return;
+ }
+
+ // Patch main in place so the editing li is never detached.
+ main.hidden = freshMain.hidden;
+ const list = main.querySelector('.todo-list');
+ for (const el of [...main.children]) if (el !== list) el.remove();
+ for (const el of [...freshMain.children]) if (!el.matches('.todo-list')) list.before(el);
+ for (const li of [...list.children]) if (li !== editing) li.remove();
+ editing.querySelector('.view').replaceWith(keep.querySelector('.view'));
+ editing.classList.toggle('completed', keep.classList.contains('completed'));
+ let before = true;
+ for (const li of [...freshMain.querySelector('.todo-list').children]) {
+ if (li === keep) before = false;
+ else if (before) editing.before(li);
+ else list.append(li);
}
}