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) } }