Print: entitle the sandbox, and stop the ⌘P chord from ever falling through

Root cause of the owner's repro (board window frontmost, File ▸ Print…
enabled, chosen from the menu, alert appears anyway): Kanban.entitlements
carried no com.apple.security.print key. The app is sandboxed, and a
sandboxed NSPrintOperation is denied by the sandbox with exactly this
wording — "This application does not support printing. Please contact
the application's developer." — regardless of which code path invokes
it. Fix: add the entitlement.

Alongside it, hardening for a separate, narrower failure mode that
happens to produce the identical alert text by a different mechanism:
PrintCommand used to disable itself over a window that published
neither a board nor a printable card (welcome, the template chooser,
Settings, the restore-bootstrap window, a card window whose board
hasn't joined). A disabled SwiftUI Button still owns its
.keyboardShortcut, so the unclaimed ⌘P chord fell through to AppKit's
own nil-target printDocument: action, whose stock failure is the same
system alert. The row now claims ⌘P unconditionally in every window;
scope resolves at the moment of the action instead (board, then card,
then a polite "Nothing to Print" / "Open a board or a card to print
it." refusal in the app's own voice). The boolean isEnabled(hasBoard:
hasPrintableCard:) becomes a three-way PrintCommand.resolveScope(...)
-> Scope pure function.

Also implements AppDelegate's application(_:printFiles:withSettings:
showPrintPanels:) — Finder's own File ▸ Print… / drag-to-printer /
print-and-open path was previously unhandled, its own separate route
to the same stock alert. PrintCoordinator.printFiles loads each path
headless through BoardLoader (no store, no window) and either prints
it or gives the same one-sentence refusal; the operation-building code
shared with the in-app path is factored out of run(_:) into
makeOperation(for:showsPrintPanel:) and runOperation(_:session:).

Docs: 11-command-nexus.md's Print row, PrintCommand's and
PrintCoordinator's doc comments, KanbanApp.swift's CommandGroup
comment, and project.yml's entitlements comment all narrate the
entitlement as the actual fix and the scope work as hardening beside
it.

Tests: PrintCommandValidationTests now exercises resolveScope's three
arms in place of the old boolean. A new PrintFinderResolutionTests
suite covers PrintCoordinator.resolveFinderPrint(atPath:) — the one
piece of the Finder half a test can drive without handing AppKit a
real print job — against a real board, an empty non-board folder, a
plain file, and an unsupported schema.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-08 23:46:16 -04:00
parent ce92c24190
commit 9e6f4567df
7 changed files with 432 additions and 76 deletions
+31
View File
@@ -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