Implement the hybrid clipboard with deferred cut
⌘X/⌘C/⌘V for cards and lanes per 04-interactions.md § Clipboard: - ClipboardStore stages full folder snapshots eagerly at the gesture into Application Support (at most the current copy; sweep at launch and on each copy purges what the pasteboard no longer references; a copy made before quitting pastes whole after restart) and writes the pasteboard a JSON manifest — every entry embedding its index.md, lane entries their cards' too — plus plain-text titles. - Cut is Finder-style deferred: items dim in place off pendingCut, void on pasteboard takeover (changeCount, no timers), source-board close, or per-item external tombstoning; the first armed paste moves the surviving originals whole (tombstoned interior cards land in the destination's trash), a second paste materializes copies from staging. - Paste anchors by the shared flatten-order rule (NewCardTarget's anchor, extracted); a tombstoned selection never anchors; lane paste reaches the right end and stays enabled on a zero-lane board; paste into the source board is the within-board lane duplicate; copies keep created, take fresh GUIDs, and strip tombstoned cards; trash-sourced copies strip deleted: at materialization; ⌘X is disabled on the trash side. - A degraded paste is loud, never silent: staging gone → the embedded index.md fallback lands content-intact, attachments absent, and a BannerCenter-phrased row names what was lost. - The standard Edit items validate through conditionally-attached onCommand handlers, so AppKit's enablement mirrors the availability predicates; text fields keep their own clipboard while focused. 879 unit tests (68 new). Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -176,6 +176,19 @@ public final class AppModel {
|
||||
/// concern, and nothing outside this module has any business reaching into a gesture in flight.
|
||||
let dragSession = DragSession()
|
||||
|
||||
/// The app's one clipboard (04-interactions.md ▸ Clipboard).
|
||||
///
|
||||
/// App-wide for the drag session's reason turned up a level: a cut/copy **outlives the board it
|
||||
/// came from** — the pasteboard and the staged snapshot survive the source window closing, and
|
||||
/// survive the app quitting — so nothing board-scoped could own it. It lives here rather than as
|
||||
/// a singleton for `styleRecents`' reason (a test holds its own rather than colliding with the
|
||||
/// app's, which for this one also means staying off the machine's real pasteboard), and every
|
||||
/// board window reaches it through the environment.
|
||||
///
|
||||
/// Building it here is also the **launch sweep** (04: "a sweep at launch"): the store's `init`
|
||||
/// reads the pasteboard once and collects every staged tree it no longer names.
|
||||
public let clipboard = ClipboardStore()
|
||||
|
||||
// MARK: Sessions
|
||||
|
||||
/// One open board window and everything hanging off it.
|
||||
|
||||
Reference in New Issue
Block a user