Stand up the window architecture — welcome, board, card
Four scenes (welcome, restore bootstrap, board group, card group) with system restoration disabled in favor of the registry's open-now flags: set when a window actually opens, cleared only on user close, so quit — and crash — leave exactly the restoration set behind. AppModel joins windows to sessions (shared store, registry record, card refs, held security scope); CloseFlushCoordinator pins 02's strict close order as a seam-injected machine (card sessions end, windows drain, store flushes, record stamps, teardown) with named slots where m6/m7 flushes land. HostedWindowController proxies — never replaces — SwiftUI's window delegate to intercept windowShouldClose for the flush, report frames, and place saved frames onto live screens. Card windows are (board path, case-folded card id) values: reopen focuses, and a snapshot-pure fate function dismisses on delete, tombstone, tombstoned lane, or cross-board move. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - BoardWindowRef
|
||||
|
||||
/// What a board window *is*, as a value: the board's root path.
|
||||
///
|
||||
/// `WindowGroup(id:for:)` keys its windows on the presented value, so this type is simultaneously
|
||||
/// the window's identity and the argument its host opens with — which is what makes
|
||||
/// "**one board window per root**" (02-architecture.md § Windows) a property of the scene rather
|
||||
/// than bookkeeping somebody has to remember: `openWindow(value:)` with a ref that already has a
|
||||
/// window focuses that window instead of opening a second one.
|
||||
///
|
||||
/// **A path, not a bookmark or a file identity**, even though the app keys boards by identity
|
||||
/// everywhere else (`BoardStoreRegistry`, `BoardRegistry`). Two reasons, both about what a window
|
||||
/// value has to be: it must be `Codable` into a scene-restoration archive, and it must be cheap to
|
||||
/// compare — a `FileIdentity` is neither. The identity keying is not lost, only moved: `AppModel`
|
||||
/// asks `BoardStoreRegistry.liveStore(for:)` — which *is* identity-keyed — before opening anything,
|
||||
/// so a board reached through two spellings of its path still lands on the window it already has.
|
||||
public struct BoardWindowRef: Codable, Hashable, Sendable {
|
||||
|
||||
/// The board root's filesystem path.
|
||||
public let path: String
|
||||
|
||||
public init(path: String) {
|
||||
self.path = path
|
||||
}
|
||||
|
||||
public init(url: URL) {
|
||||
self.init(path: url.path)
|
||||
}
|
||||
|
||||
/// The root as a URL again. Carries **no** security-scoped access — the scope belongs to the
|
||||
/// URL object the bookmark resolved to, which the session holds for its whole life (`AppModel`),
|
||||
/// never to a URL rebuilt from a string.
|
||||
public var url: URL {
|
||||
URL(fileURLWithPath: path, isDirectory: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CardWindowRef
|
||||
|
||||
/// What a card window is: **board root path plus card GUID** (05-card-window.md ▸ Deletion &
|
||||
/// lifecycle).
|
||||
///
|
||||
/// That compound key is the whole lifecycle rule in one value. Within its board the window *follows*
|
||||
/// its card — the key names the card, not the lane, so a move between lanes is invisible to the
|
||||
/// window. Across boards it does not: "a cross-board move dismisses it exactly like a delete, since
|
||||
/// the board half of the key no longer names it once the card has left" — the card's UUID travels
|
||||
/// with the move, but `(oldBoard, uuid)` names nothing afterwards, so the window that was keyed on
|
||||
/// it has no card and dismisses.
|
||||
///
|
||||
/// ### The card id is a string, compared like an `ItemID`
|
||||
///
|
||||
/// `rawValue` is stored, because the folder's exact spelling is what builds URLs and what must
|
||||
/// round-trip byte-perfect (`ItemID`'s doc comment in `BoardModel.swift`). But equality and hashing
|
||||
/// **case-fold** it, because `ItemID` does: two case-spellings of one UUID are one identity
|
||||
/// everywhere in this app, and a window key that disagreed would open a *second* window for a card
|
||||
/// whose folder was spelled `ABC…` where the first was spelled `abc…` — the exact duplicate the
|
||||
/// identity rule exists to prevent. The board half is compared verbatim: it is a path, and paths are
|
||||
/// the filesystem's business, not this type's.
|
||||
public struct CardWindowRef: Codable, Hashable, Sendable {
|
||||
|
||||
/// The owning board root's path — the same string a `BoardWindowRef` carries, which is what lets
|
||||
/// a card window find its board's session.
|
||||
public let boardPath: String
|
||||
|
||||
/// The card's `ItemID.rawValue`: the folder name exactly as it is spelled on disk.
|
||||
public let cardID: String
|
||||
|
||||
public init(boardPath: String, cardID: String) {
|
||||
self.boardPath = boardPath
|
||||
self.cardID = cardID
|
||||
}
|
||||
|
||||
public init(board: BoardWindowRef, cardID: ItemID) {
|
||||
self.init(boardPath: board.path, cardID: cardID.rawValue)
|
||||
}
|
||||
|
||||
/// The board half, as the board window's own key — how a card window reaches its session.
|
||||
public var board: BoardWindowRef {
|
||||
BoardWindowRef(path: boardPath)
|
||||
}
|
||||
|
||||
public var boardURL: URL {
|
||||
board.url
|
||||
}
|
||||
|
||||
/// The card id under `ItemID`'s comparison rule. Computed, so `cardID` stays the single source of
|
||||
/// truth for what is on disk.
|
||||
public var cardIdentity: ItemID {
|
||||
ItemID(rawValue: cardID)
|
||||
}
|
||||
|
||||
public static func == (lhs: CardWindowRef, rhs: CardWindowRef) -> Bool {
|
||||
lhs.boardPath == rhs.boardPath && lhs.cardIdentity == rhs.cardIdentity
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(boardPath)
|
||||
hasher.combine(cardIdentity)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user