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
+