Wire Finder open as a standard document open
application(_:open:) forwards every Finder-delivered URL to AppModel.openBoard(at:) — the exact path welcome and File > Open use, so a Finder open gets the same registry record-before-load, recents stamp, already-open-focuses-its-window dedup, and row-level failure surfacing on welcome (DESIGN/02 > Launch). A cold Finder launch can arrive before any scene has captured the window opener; openBoard now buffers such URLs and captureWindowActions replays them once opening is possible. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -34,6 +34,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Double-clicking a `.kanban` folder in Finder, or `open -a` — "opening a board from Finder is a
|
||||||
|
/// standard document open" (02-architecture.md § Launch and window lifecycle). Every URL is
|
||||||
|
/// forwarded to `AppModel.openBoard(at:)`, the exact entry point welcome and File ▸ Open… already
|
||||||
|
/// call (`AppModel.presentOpenPanel()`), so a Finder open gets the identical registry record,
|
||||||
|
/// board window, and recents stamp, and a board that is already open focuses its window rather
|
||||||
|
/// than opening a second one — `openBoard` starts its own security-scoped access on the URL, the
|
||||||
|
/// same as the open panel's, so nothing needs stashing here first.
|
||||||
|
///
|
||||||
|
/// **Not pre-validated.** Info.plist's `CFBundleDocumentTypes` declares the UTI, so macOS should
|
||||||
|
/// never route anything but a `.kanban` folder here — but if it did, or the folder has since gone
|
||||||
|
/// missing, `openBoard`'s fail-fast load surfaces the failure row-level on welcome, uniform with
|
||||||
|
/// every other open failure. A second vocabulary for "not a board" here would just be a worse copy
|
||||||
|
/// of the one that already exists.
|
||||||
|
///
|
||||||
|
/// **Can fire before any scene has appeared** — a cold launch (the app was not already running)
|
||||||
|
/// delivers this ahead of the first window's `onAppear`, which is where `windowOpener` is normally
|
||||||
|
/// captured (`CaptureOpenWindow`). `openBoard` buffers a URL that arrives that early and replays
|
||||||
|
/// it once the action exists, so this method does not have to reason about launch ordering itself.
|
||||||
|
func application(_ application: NSApplication, open urls: [URL]) {
|
||||||
|
guard let appModel else { return }
|
||||||
|
for url in urls {
|
||||||
|
appModel.openBoard(at: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Quit runs the close flush for **every** open board before the app goes away.
|
/// Quit runs the close flush for **every** open board before the app goes away.
|
||||||
///
|
///
|
||||||
/// The same `CloseFlushCoordinator` sequence as a user close, once per board, in the same fixed
|
/// The same `CloseFlushCoordinator` sequence as a user close, once per board, in the same fixed
|
||||||
|
|||||||
@@ -251,11 +251,25 @@ public final class AppModel {
|
|||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
public var windowDismisser: DismissWindowAction?
|
public var windowDismisser: DismissWindowAction?
|
||||||
|
|
||||||
|
/// URLs handed to `openBoard(at:)` before `windowOpener` existed to open them — a cold launch's
|
||||||
|
/// Finder-open (`AppDelegate.application(_:open:)`) can arrive ahead of the first scene's
|
||||||
|
/// `onAppear`. Held in order, replayed the moment `captureWindowActions` gives the app somewhere
|
||||||
|
/// to open them, then discarded — the buffer is a doorway, not a second registry of intent.
|
||||||
|
@ObservationIgnored
|
||||||
|
private var pendingOpenURLs: [URL] = []
|
||||||
|
|
||||||
/// What `CaptureOpenWindow` calls. A method rather than two assignments so the launch flow, which
|
/// What `CaptureOpenWindow` calls. A method rather than two assignments so the launch flow, which
|
||||||
/// needs the actions before any `onAppear` has run, has one thing to call.
|
/// needs the actions before any `onAppear` has run, has one thing to call.
|
||||||
func captureWindowActions(open: OpenWindowAction, dismiss: DismissWindowAction) {
|
func captureWindowActions(open: OpenWindowAction, dismiss: DismissWindowAction) {
|
||||||
windowOpener = open
|
windowOpener = open
|
||||||
windowDismisser = dismiss
|
windowDismisser = dismiss
|
||||||
|
|
||||||
|
guard !pendingOpenURLs.isEmpty else { return }
|
||||||
|
let urls = pendingOpenURLs
|
||||||
|
pendingOpenURLs.removeAll()
|
||||||
|
for url in urls {
|
||||||
|
openBoard(at: url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Recents
|
// MARK: Recents
|
||||||
@@ -403,9 +417,13 @@ public final class AppModel {
|
|||||||
///
|
///
|
||||||
/// Security-scoped access starts here, *before* the window exists, because the host's very first
|
/// Security-scoped access starts here, *before* the window exists, because the host's very first
|
||||||
/// act is a tree walk: a scope started after the load would be too late.
|
/// act is a tree walk: a scope started after the load would be too late.
|
||||||
|
///
|
||||||
|
/// **Called before any scene has appeared, and that's fine.** A cold launch's Finder-open can
|
||||||
|
/// reach here before `windowOpener` is captured; the URL joins `pendingOpenURLs` and this same
|
||||||
|
/// method runs again for it once `captureWindowActions` has something to open it with.
|
||||||
public func openBoard(at url: URL) {
|
public func openBoard(at url: URL) {
|
||||||
guard let windowOpener else {
|
guard let windowOpener else {
|
||||||
Self.logger.error("openBoard with no window opener captured yet — ignored")
|
pendingOpenURLs.append(url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user