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:
@@ -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
|
||||
|
||||
@@ -6,18 +6,22 @@ import SwiftUI
|
||||
///
|
||||
/// 11-command-nexus.md's own contract runs both directions: "a command absent here doesn't exist, and
|
||||
/// adding one means adding a row here first" — so once a row *is* in the Nexus, shipping the window
|
||||
/// behind it is a validation-and-action change, not a menu change. `FutureCommand` (a `Button`) and
|
||||
/// `FutureToggleCommand` (a `Toggle`) below are that reading, applied: the row exists now, stably
|
||||
/// titled and stably chorded — `NSUserKeyEquivalents` already resolves it, so a user can remap it
|
||||
/// today — with validation pinned to `false` and the action a no-op until the milestone named at the
|
||||
/// call site fills both in. That milestone's whole diff then reads as "flip `.disabled`, fill the
|
||||
/// closure" rather than "add a menu item", which is also why every call site below carries the
|
||||
/// codebase's `mN-` marker for a component still owed.
|
||||
/// behind it is a validation-and-action change, not a menu change. `FutureCommand` below is that
|
||||
/// reading, applied: the row exists now, stably titled and stably chorded — `NSUserKeyEquivalents`
|
||||
/// already resolves it, so a user can remap it today — with validation pinned to `false` and the
|
||||
/// action a no-op until the milestone named at the call site fills both in. That milestone's whole
|
||||
/// diff then reads as "flip `.disabled`, fill the closure" rather than "add a menu item", which is
|
||||
/// also why every call site below carries the codebase's `mN-` marker for a component still owed.
|
||||
///
|
||||
/// **The title never moves once a row ships**, disabled or not: a toggle wired live later must not
|
||||
/// **The title never moves once a row ships**, disabled or not: a command wired live later must not
|
||||
/// gain a second spelling on the way (04-interactions.md ▸ Configurable bindings — "toggles keep one
|
||||
/// stable title, checkmark state only" — which applies to a row that has not started ticking yet
|
||||
/// exactly as it does to one that has).
|
||||
///
|
||||
/// There was a `FutureToggleCommand` beside this — a disabled `Toggle` for a checkmark row — and it
|
||||
/// went with the last of its call sites (Raw Source, `RawSourceCommand`). Every row still owed is a
|
||||
/// plain command; a future checkmark row brings its scaffold back with it rather than keeping an
|
||||
/// unused one warm.
|
||||
struct FutureCommand: View {
|
||||
let title: String
|
||||
var key: KeyEquivalent?
|
||||
@@ -32,22 +36,6 @@ struct FutureCommand: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// `FutureCommand`'s checkmark-state twin, for a row the Nexus already marks "(checkmark toggle)".
|
||||
///
|
||||
/// `isOn` is a constant `false` rather than real state: there is no session yet for a binding to
|
||||
/// read, which is exactly the disabled, unchecked state a not-yet-wired toggle should show.
|
||||
struct FutureToggleCommand: View {
|
||||
let title: String
|
||||
var key: KeyEquivalent?
|
||||
var modifiers: EventModifiers = .command
|
||||
|
||||
var body: some View {
|
||||
Toggle(title, isOn: .constant(false))
|
||||
.keyboardShortcut(key.map { KeyboardShortcut($0, modifiers: modifiers) })
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File ▸ Save as Template
|
||||
|
||||
/// File ▸ Save as Template — no default chord (11-command-nexus.md; 09-templates.md).
|
||||
@@ -108,21 +96,19 @@ struct FindSteppingCommands: View {
|
||||
/// View ▸ Edit Body (⌘E) / Raw Source (⌥⌘E) / History — the card window's three view-state rows
|
||||
/// (11-command-nexus.md).
|
||||
///
|
||||
/// **Edit Body is live** (`EditBodyCommand`, beside the focused value it reads): the body column's
|
||||
/// Preview/Edit toggle, checkmark state and all. Its diff was the one `FutureCommand` promises —
|
||||
/// the title and the chord did not move, the validation and the action filled in.
|
||||
/// **Edit Body and Raw Source are both live** (`EditBodyCommand`, `RawSourceCommand`, each beside the
|
||||
/// focused value it reads): the body column's Preview/Edit toggle, and the window-level outlet whose
|
||||
/// toggling-off *applies*. Each diff was the one `FutureCommand` promises — the title and the chord
|
||||
/// did not move, the validation and the action filled in — and the pair also carries the clause that
|
||||
/// joins them, "Edit Body disables while Raw Source is active" (05-card-window.md).
|
||||
///
|
||||
// m6-raw-source: Raw Source is the same shape one card later — a checkmark toggle over a
|
||||
// window-level mode, which also adds the "Edit Body disables while Raw Source is active" clause to
|
||||
// the row above (05-card-window.md).
|
||||
//
|
||||
// m6-card-sidebar: History is a plain command that focuses the sidebar's History section, and
|
||||
// disables outright on mode `none` / repo-nested boards once that section exists (05-card-window.md,
|
||||
// 07-sync-collab.md). Both remain unconditionally disabled here — neither surface exists yet.
|
||||
// 07-sync-collab.md). It remains unconditionally disabled here — that surface does not exist yet.
|
||||
struct CardViewCommands: View {
|
||||
var body: some View {
|
||||
EditBodyCommand()
|
||||
FutureToggleCommand(title: "Raw Source", key: "e", modifiers: [.option, .command])
|
||||
RawSourceCommand()
|
||||
FutureCommand(title: "History")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user