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:
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user