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
103 lines
4.4 KiB
Swift
103 lines
4.4 KiB
Swift
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)
|
|
}
|
|
}
|