diff --git a/DESIGN/11-command-nexus.md b/DESIGN/11-command-nexus.md index 316901b..cf87cb5 100644 --- a/DESIGN/11-command-nexus.md +++ b/DESIGN/11-command-nexus.md @@ -32,7 +32,7 @@ The single source of truth for **every command and action the app can perform** | File | Add Comment | — (no default) | Card window (all tiers — 12); if Show Comments is off, turns it on (persisted, the same user choice) and focuses the composer — 05 ▸ The comments column | | File | Delete | ⌘⌫ | Board window, any card or lane selection — staged by place (resettled 2026-07-28; lanes rejoined 2026-07-29): board cards and lanes move to `.trash/`, trash selections delete permanently (03's recoverability confirm — freight-counting for lanes). Deliberately **not** extended to the card window: an enabled ⌘⌫ key equivalent would steal delete-to-line-start from the window's text surfaces, so there the card's delete is the sidebar Actions button (05). **Delete Immediately (⌥⌘⌫) is deliberately absent** (removed 2026-07-30): permanence is only reachable inside the trash — 03 ▸ Trash | | File | Empty Trash… (confirmed) | ⇧⌘⌫ | Board window, trash shown and non-empty (whole-trash scope, search-independent — 03 ▸ Trash) | -| File | Print… | ⌘P | Board window: prints **the board** as a linear document — lanes in left-to-right order, each lane's cards top-to-bottom, never a graphical snapshot of the strip; the trash is unreachable (it is a sibling container of `lanes`, not a lane — 01 ▸ Deletion). Card window: prints **that card**. Validation is scope and nothing else — a print is a read, so neither the read-only lock nor the focused-editor rule closes it (Reveal in Finder's posture). Options ride in a **print-panel accessory** with the system's live preview: which components (title, icon+labels line, rendered body, comments — off by default, oldest- or newest-first), page breaks (continuous / between lanes / between cards, real sheet boundaries), one base font face and size every other size derives from, and a toggleable header/footer (board title, print date, page numbers, custom line). The option sets persist app-side as **named print profiles** with a reserved "Last Used" pseudo-profile that auto-captures the most recent settings — `UserDefaults`, never board data (02 ▸ Per-board app state), the zoom level's own argument. **Page Setup… is deliberately absent**: the paper questions are answered in the print panel's own page-setup group, so a second dialog would be a second place to set one margin | +| File | Print… | ⌘P | Board window: prints **the board** as a linear document — lanes in left-to-right order, each lane's cards top-to-bottom, never a graphical snapshot of the strip; the trash is unreachable (it is a sibling container of `lanes`, not a lane — 01 ▸ Deletion). Card window: prints **that card**. **The sandbox needs an entitlement to print at all** (owner-reported 2026-08-09, board window frontmost, row enabled): `Kanban.entitlements` carried no `com.apple.security.print`, so a sandboxed `NSPrintOperation` was denied with the system alert "This application does not support printing" regardless of which code path invoked it — the fix is the entitlement key, not app logic. **Also revised the same day, as hardening rather than as the fix**: the row is now **always enabled** — a print is a read, so neither the read-only lock nor the focused-editor rule ever closed it (Reveal in Finder's posture), and the row claims ⌘P unconditionally rather than disabling, because a disabled `Button` still owns its key equivalent — a window with neither scope (welcome, the template chooser, Settings, the restore-bootstrap window, a card window whose board hasn't joined) used to drop the unclaimed chord into AppKit's own nil-target print handling, whose stock failure happens to be the identical alert text by a wholly separate mechanism. Scope now resolves at the moment ⌘P is pressed instead: board, then card, then — with neither — a one-sentence app-drawn refusal ("Nothing to Print" / "Open a board or a card to print it."), the same voice the empty-document refusal already uses. Finder's own `printFiles` Apple Event (File ▸ Print… on a selected board, a drag onto a printer queue, a print-and-open service) is implemented rather than left unhandled — its own separate route to the same stock alert: `AppDelegate` loads the path headless through `BoardLoader` and either prints it or gives the identical one-sentence refusal. Options ride in a **print-panel accessory** with the system's live preview: which components (title, icon+labels line, rendered body, comments — off by default, oldest- or newest-first), page breaks (continuous / between lanes / between cards, real sheet boundaries), one base font face and size every other size derives from, and a toggleable header/footer (board title, print date, page numbers, custom line). The option sets persist app-side as **named print profiles** with a reserved "Last Used" pseudo-profile that auto-captures the most recent settings — `UserDefaults`, never board data (02 ▸ Per-board app state), the zoom level's own argument. **Page Setup… is deliberately absent**: the paper questions are answered in the print panel's own page-setup group, so a second dialog would be a second place to set one margin | | File | Close | ⌘W | Any window; flushes per 02 ▸ Windows | | Edit | Undo / Redo (M−) | ⌘Z / ⇧⌘Z | Focus-routed (06 ▸ Undo routing): text undo in a focused editor, git undo otherwise; git undo disabled on no-git and repo-nested boards, during 06's abnormal-state pause (detached HEAD, in-progress merge/rebase), and under the read-only lock (02) | | Edit | Cut / Copy / Paste | ⌘X / ⌘C / ⌘V | Board window: cards and lanes (cards-XOR-lanes selections; lane paste lands after the anchor lane — 04 ▸ Clipboard; on a zero-lane board only a lane payload pastes — 04 ▸ ⌘N target rule); in the trash, ⌘C copies out and ⌘X/⌘V is the keyboard restore path (resettled 2026-07-28 — 04 ▸ The trash); paste never targets the trash; text editors: standard text clipboard | diff --git a/Kanban/App/AppDelegate.swift b/Kanban/App/AppDelegate.swift index fc2a51a..c101521 100644 --- a/Kanban/App/AppDelegate.swift +++ b/Kanban/App/AppDelegate.swift @@ -71,6 +71,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } + /// Finder's `printFiles` Apple Event — File ▸ Print… on a board selected in Finder, a drag onto a + /// printer queue, or a print-and-open service. Left unimplemented, this delivers to AppKit's stock + /// nil-target print handling and its stock failure: "This application does not support printing. + /// Please contact the application's developer." — the same wording the *reported* bug turned out to + /// have a different cause for (the sandbox's `com.apple.security.print` entitlement, + /// `Kanban.entitlements`, `PrintCommand`'s doc comment tells that story) — but an unhandled Apple + /// Event is its own route to it regardless, which is what this method retires + /// (`PrintCoordinator.printFiles`'s doc comment tells the whole story; this method is only the + /// receiving end of the Apple Event). + /// + /// Every path is resolved and printed independently — `PrintCoordinator.printFiles` does the actual + /// work, headless (no store, no window, `BoardLoader` on the path directly), so this method is a + /// straight handoff of the Apple Event's three arguments and nothing more. `appModel` should always + /// be set by the time an Apple Event can reach here (it exists from the first run-loop turn onward — + /// this type's own note), but a `nil` is answered with `.printingFailure` rather than force-unwrapped, + /// the same defensive posture `application(_:open:)` takes one method up. + func application( + _ application: NSApplication, + printFiles filenames: [String], + withSettings printSettings: [NSPrintInfo.AttributeKey: Any], + showPrintPanels: Bool + ) -> NSApplication.PrintReply { + guard let appModel else { return .printingFailure } + return PrintCoordinator.printFiles( + filenames, + settings: printSettings, + showPrintPanels: showPrintPanels, + profiles: appModel.printProfiles + ) + } + /// 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 diff --git a/Kanban/Kanban.entitlements b/Kanban/Kanban.entitlements index 2eeb6ae..86f75d3 100644 --- a/Kanban/Kanban.entitlements +++ b/Kanban/Kanban.entitlements @@ -8,6 +8,14 @@ com.apple.security.files.bookmarks.app-scope + + com.apple.security.print +