Build the template chooser and Save as Template

The chooser completes its three tiers: bundled by template order, then
keyed user templates, then keyless boards by display name — and a
malformed user template still lists, by folder name with the loader's
own sentence on the row, never failing its neighbours. The store is
re-scanned on every presentation and on app activation, the Reveal
round trip made honest without watching a folder 09 deliberately
leaves unwatched; Reveal lives in the chooser's header and mints the
store on first press. Save as Template repeats Duplicate's sequence —
progress row with Cancel, flush, detached cancellable copy — through
the engine: mint the store, read the next user order before the copy
can count itself, Finder-ladder the name, copy excluding .git and
.trash/, then stamp the whole template: mapping on the landed copy
through updateIndex, with no bracket because the copy lives outside
every watched board. Folder attributes deliberately don't carry — the
one lock the command stays live under is the read-only-DMG one, and
carrying its mode bits would mint a read-only template in the user's
own store; the command gates instead on the real hazard, unsaved card
content. A signpost names the template only when the ladder renamed
it. One name ladder now serves Duplicate and the store.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 19:39:18 -04:00
parent e7b48d2d53
commit 3aa80db2a4
17 changed files with 1358 additions and 90 deletions
+21
View File
@@ -54,6 +54,25 @@ public final class CardRawSourceSession {
/// neither blocks on the open buffer nor invalidates it Apply stays last-writer-wins" (05).
public var text = ""
/// The file as `enter()` read it the baseline behind `holdsUnsavedText`, and never shown.
///
/// `CardBodyEditSession.disk`'s counterpart, with the same job and none of its reconciliation:
/// this one is set once, when the outlet opens, and is not followed by a watcher reload (the
/// buffer above is deliberately never reconciled while it is open), so it answers exactly one
/// question has the user typed anything since the file was read.
private var loadedText = ""
/// **Whether an open outlet is holding bytes the file does not** the raw-source half of
/// 09-templates.md Save as Template's carve-out, which stays live under the
/// unwritable-location lock "unless an open Edit or raw-source session holds unsaved content the
/// suspended saves can't flush" (02-architecture.md Live-reload resilience).
///
/// A closed outlet holds nothing (the buffer is emptied on the way out), and an open one that
/// has not been typed in holds nothing either it is showing the file. Apply is the only thing
/// that writes it, and under the lock Apply is refused, which is precisely why a template taken
/// past this text would silently miss it.
public var holdsUnsavedText: Bool { isActive && text != loadedText }
/// The alert waiting to be shown, if any a validation refusal on Apply, or a file that could
/// not be opened as source. Cleared by the OK that dismisses it, which returns the user to the
/// text they were editing (05: "a failed validation keeps source mode open (toggle stays
@@ -109,6 +128,7 @@ public final class CardRawSourceSession {
switch read() {
case let .read(source):
text = source
loadedText = source
alert = nil
isActive = true
return true
@@ -175,6 +195,7 @@ public final class CardRawSourceSession {
isActive = false
alert = nil
text = ""
loadedText = ""
}
}