Build the Raw Source outlet

The escape hatch: View > Raw Source (opt-cmd-E) unmounts the whole
content area for the literal on-disk index.md in a plain monospaced
editor with Cancel/Apply. Raw source is window-level state, not a third
body mode — entry rides setMode(.preview), which flushes the Edit
session by construction, then reads the file fresh; exit reveals
Preview, and an empty body after Apply does not reopen Edit (openIfNeeded
already ran). Apply validates the proposed bytes through the loader's
own card checks — parseDocument's strict UTF-8/BOM rejection, schema,
order — deliberately skipping the uneditable-shape refusal, since a
flow-mapping card is exactly what the hatch repairs; invalid bytes
alert in place with the loader's own error and no bracket opens. The
write is byte-for-byte with no modified stamp and no modified-by clear,
per 01's explicit carve-out — the verbatim contract outranks stamping —
and identical bytes write nothing. Escape cancels, cmd-Return applies,
toggle-off applies too, and cmd-E disables while raw is active via a
testable predicate. Tombstoned targets refuse as vanished: a foreign
delete is never reverted by a stale buffer.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 11:25:25 -04:00
parent e989c1f26e
commit 40c0a75c24
12 changed files with 1733 additions and 65 deletions
+82 -2
View File
@@ -117,8 +117,11 @@ final class CardWindowSession: CardSessionFlushing {
///
/// This file owns identity, lifecycle, the title and subtitle, and where the window opens. The
/// two-column composition inside it is `CardWindowView`'s, and what fills those columns the title
/// field, Preview/Edit, the raw-source outlet, the sidebar's five sections arrives card by card
/// underneath a composition that does not move.
/// field, Preview/Edit, the sidebar's five sections arrives card by card underneath a composition
/// that does not move. The *window-scoped* state those surfaces need lives here, because a window is
/// what it is scoped to: the body column's mode (`CardBodyPresentation`) and the raw-source outlet
/// (`CardRawSourceSession`), both published through the focus system so the View menu's rows can
/// reach the frontmost card window.
struct CardWindowHost: View {
let ref: CardWindowRef
@@ -132,6 +135,10 @@ struct CardWindowHost: View {
/// This window's body column the Preview/Edit mode, and the F hook a menu item reaches
/// through the focus system (`CardBodyPresentation`).
@State private var bodyPresentation = CardBodyPresentation()
/// This window's raw-source outlet the whole-content-area swap View Raw Source (E) drives
/// (`CardRawSourceSession`). Beside the body handle rather than inside it: the two are different
/// scopes, and the Edit Body row reads both.
@State private var rawSource = CardRawSourceSession()
/// Whether a close is waiting on the dirty-buffer modal. Set when `windowShouldClose` could not
/// flush; cleared by the resolution that lets the close resume.
@State private var isClosePending = false
@@ -199,6 +206,31 @@ struct CardWindowHost: View {
// item reaches the frontmost one's body surface through this, exactly as board-window
// items reach their window's store (`FocusedBoardStoreKey`).
.focusedSceneValue(\.cardBody, bodyPresentation)
// View Raw Source (E) reaches the frontmost card window the same way, and Edit Body
// reads it too "View Edit Body (E) disables while source mode is active"
// (05-card-window.md Raw source outlet).
.focusedSceneValue(\.cardRawSource, rawSource)
// The raw-source outlet's detailed alert, presented over this window a validation
// refusal on Apply, or a file that could not be opened as source. It hangs *here* rather
// than inside the editor because the second of those fires while source mode is still
// closed, when there is no editor on screen to present it from.
.alert(
rawSource.alert?.title ?? "",
isPresented: Binding(
get: { rawSource.alert != nil },
set: { presented in
guard !presented else { return }
rawSource.dismissAlert()
}
),
presenting: rawSource.alert
) { _ in
// One button, because there is one thing to do: OK returns to the text, which is
// exactly where it was. Nothing was written, so there is nothing to retry or discard.
Button("OK") { rawSource.dismissAlert() }
} message: { alert in
Text(alert.message)
}
// The one modal moment (02-architecture.md § Write-failure surfacing), presented over
// the window whose close it is holding up which is why it hangs here and not on the
// board: the text being saved is this window's.
@@ -231,6 +263,7 @@ struct CardWindowHost: View {
cardFolder: Self.cardFolder(root: store.rootURL, placement: placement),
bodyPresentation: bodyPresentation,
bodySession: session.body,
rawSource: rawSource,
// "Under the read-only lock the controls disable in place an in-content mutation
// menu validation can't reach" (05 Preview). The checkbox is that control, and
// the store's own lock is the whole predicate.
@@ -346,6 +379,53 @@ struct CardWindowHost: View {
bodyPresentation.flushEdits = { [session] in
session.body.endEditSession()
}
Self.configureRawSource(
rawSource,
body: session.body,
presentation: bodyPresentation,
store: store,
cardID: cardID
)
}
/// Points the raw-source outlet at its card the outlet's three seams (05-card-window.md Raw
/// source outlet), wired in the one place that knows both a buffer and a board.
///
/// **The flush is the Preview flip**, not a second mechanism: "Entering source mode flushes any
/// pending title/body edits first" and "Leaving Edit flushes the debounce (mode flip, raw-source
/// entry, window close)" are the same sentence read from two directions, so putting the entry
/// through `setMode(.preview)` makes the flush structural and settles the exit state at the same
/// time, because a window that genuinely left Edit on the way in has Preview waiting for it on the
/// way out (`CardRawSourceSession`). The unconditional `flush()` behind it costs nothing on a
/// clean buffer and covers the case where the mode was already Preview with a save still owed (a
/// tick suspended under the read-only lock, say).
///
/// The store is captured **weakly**, `configureSession`'s rule: an outlet still holding a closure
/// after the board window has gone should write nothing rather than resurrect a released store.
///
/// `static`, and taking every collaborator as a parameter, for the reason the fate and subtitle
/// rules are: the ordering above is the whole of "flush, *then* read fresh", it is invisible in a
/// running window until it is wrong, and this shape is what lets a test drive the real wiring
/// rather than a re-typed copy of it.
static func configureRawSource(
_ rawSource: CardRawSourceSession,
body: CardBodyEditSession,
presentation: CardBodyPresentation,
store: BoardStore,
cardID: ItemID
) {
rawSource.flushPendingEdits = { [body, presentation] in
presentation.setMode(.preview)
body.flush()
}
rawSource.read = { [weak store] in
guard let store else { return .vanished }
return store.readCardSource(inCard: cardID)
}
rawSource.apply = { [weak store] text in
guard let store else { return .vanished }
return store.applyCardSource(inCard: cardID, text: text)
}
}
/// Size and placement **the remembered frame first, the cascade second** (05-card-window.md