9.7 KiB
Global quick-capture window (agy gap 11, roadmap 38)
Part of specs/plugins-parity/. Reuse note.capture, native eframe viewport and desktop host. No new crate. Native shortcut dependency is an explicit owner gate.
Research (2026-10-21; agy claims checked)
- Apple documents Fn-Q for Quick Note: Notes keyboard shortcuts. Confirms global-capture pattern; does not prove Reflect/Raycast parity, which is dropped.
- Candidate safe facade global-hotkey documentation supports Windows/macOS/Linux X11, requires platform event-loop integration, and does not support Wayland. Do not copy its unwrap examples or claim every desktop works.
- Wayland alternative is user-authorized XDG GlobalShortcuts portal, a later adapter with its own binding/approval gate. No bundled privileged helper.
Problem / goal
While another application has focus, explicitly registered shortcut opens compact NotesCraft capture viewport. Submit to chosen live vault's Daily note or Inbox through note.capture; preserve unsent text on dismissal or save failure. App must already run; no background daemon or launch-at-login in v1.
Verified baseline / files
apps/notescraft/src/main.rs: each opened vault is separate process and NotesCraftWindow wraps NotesApp; launcher_window.rs spawns vault processes. crates/engine/src/capture/{mod,rules}.rs already handles daily/Inbox/folder, 1 MiB text, source/title and attachment. crates/ui-egui/src/shell/hotkeys/{defaults,dispatch,commands}.rs implements focused-app hotkeys only; not global registration. crates/ui-egui/src/shell/app.rs holds app state and shell/app/window/{commands,frame}.rs dispatches actions. apps/notescraft/Cargo.toml has no direct global-hotkey dependency. crates/engine/src/prefs/{app_settings,settings,mod}.rs owns app preferences; prefs/catalog/list.rs provides remembered vaults.
Non-goals
Auto-start after quit; screenshot/clipboard monitoring; frontmost-app text extraction; cross-vault writes without live session; silent capture when locked/read-only; mobile global shortcut. No changes to shared craft-libs API.
Design decisions
D1 Pure draft state
New crates/ui-egui/src/capture/model.rs, declared by new capture/mod.rs and existing src/lib.rs. Draft { text:String, title:Option<String>, target:Target }, Target Daily or Inbox only; retain in memory, never auto-persist plaintext. CaptureState models Hidden, Editing, Saving, Failed with a monotonically increasing submission generation. Validate <=1 MiB UTF-8, title <=200 characters, whitespace-only invalid; no truncation. submit() produces one request only from Editing/Failed, rejects repeats during Saving. Complete matching generation clears draft on success; stale result cannot clear newer draft. Esc/close hides preserving draft. User's explicit Discard clears. Quit with unsent draft asks Discard/Cancel through existing close flow; crash loses draft, documented.
D2 Host/window lifecycle
Single secondary viewport per owning process, about 520x280 points, theme tokens, title/target, multiline input, Save/Discard; optional frameless styling must retain accessible Close and drag affordance. Open requests coalesce, repeat shortcut focuses existing window; never toggles it closed. Set focus/visibility via eframe viewport commands, no busy-loop or IO in paint. Request repaint on native hotkey event so minimized main window still processes it. Main editor does not receive capture keystrokes. Save Ctrl/Cmd+Enter, Escape hide; Tab navigation/focus restored on reopening. Submit failure keeps draft/window, success hides. Closing main process follows normal unsaved-data checks, unregisters hotkey, closes child.
D3 Shared command surface
Existing note.capture is sole persistence command. New engine capture.window.request {} -> {requested:true} queues one ephemeral UI event on Session; drained once by live shell. enabled requires writable live Session; no text/draft/token returned. Headless sessions return unsupported instead of claiming a window opened. capture.window.status {} -> {supported,available} reports host capability only. Host registers capability with Session at startup; registry never depends on egui. New capture/window.rs + registration in capture/mod.rs, add IDs to core_commands and assistant_gate (status READ, request WRITE; request may focus UI and must respect assistant gate). Native event and palette invoke same request command. CLI/MCP/control can request live window; no new public IPC server.
D4 Multi-process ownership
Global preference quick_capture.owner_vault is explicit canonical existing vault path selected by user, not active/recent vault chosen implicitly. Only process whose live Session matches may register chord; other vault windows show owner name and enabled/disabled status. Launcher alone cannot capture. Two processes for same vault: first successful OS registration wins, second shows conflict and never steals. Owner closes -> feature unavailable until owner vault reopened; no implicit rerouting or process spawning. Changing owner unregisters current registration before saving/applying replacement; never operate two registrations. Chord change: attempt replacement safely, on failure preserve prior effective registration and display configured/effective values rather than silent mismatch.
D5 OS registration / dependency gate
Default quick_capture.enabled=false; configurable chord, proposed Mod+Alt+Shift+N (validate against app table; OS conflicts checked at registration), selected owner path. Persist via existing settings; local machine preferences, never vault sync. Use candidate global-hotkey only after owner approval, version/pinning/license audited then; root workspace and target-specific app manifest changes belong to task 3. Register on macOS main event-loop thread; Windows manager/event-loop same thread. Poll channel without blocking and unregister on disable/rebind/exit. Repo forbids unsafe: no handwritten FFI, no exception to lints. No eframe/winit platform callbacks assumed available without compile probe against pinned versions. If adapter cannot integrate supported event loop, stop/report, do not install low-level keyboard hooks.
macOS/Windows/X11 candidates; OS denial/chord conflict -> visible actionable error and ordinary palette request still works. Wayland reports unsupported in v1, with instructions to use focused-app capture; do not label focused-app binding global. Native Wayland portal postponed until owner approves binding and permission UX. global-hotkey approval is not approval for portal/objc/windows dependency additions.
D6 UI and privacy
Strings [ui.quick_capture] appended to crates/i18n/locales/en.toml, table module through text/features/mod.rs. Reuse craft-ui inputs/buttons/modal and theme tokens; no logging draft text. Settings enable/chord/owner/status and unsaved-draft warning. Memory draft belongs only to selected owner process. If selected vault becomes read-only/locked, refuse submit; preserve draft and explain. No clipboard reads, screenshot capture, network or automatic focus tracking.
Acceptance criteria
- Pure model tests: whitespace/oversize rejected; repeated Save emits one request; failed save preserves text; success clears only matching generation; Escape retains, Discard clears.
capture.window.requestvia control and native event shows same viewport once; headless returns unsupported. No UI imports below L5.- Actual OS smoke test from another app while owner main window minimized: shortcut shows/focuses capture, input lands there, Cmd/Ctrl+Enter saves to selected owner Daily/Inbox exactly once. Main editor unchanged except intended daily-buffer update.
- Dirty daily note receives capture through existing Session buffer, undo remains correct. Read-only or write error leaves draft editable and retryable.
- Two vault processes do not compete; non-owner cannot register. Second identical owner reports conflict; disable/quit releases chord. No foreground-app shortcut mistaken for global.
- Chord change failure preserves previous effective shortcut; OS unsupported/denied status explicit. Wayland does not claim support.
- Close/reopen child preserves draft; process quit asks before discard. No capture text in settings, disk recovery, logs or control status.
- Kittest covers keyboard/focus/Save/Discard, localized strings; visual snapshots read at Mocha/Latte and compact width. Main-window close retains existing safety flow.
Open owner questions
- Q1: approve new
global-hotkeydependency for desktop host (reason: safe Rust global registration under unsafe-forbidden policy), macOS/Windows/X11 first? Version/lock changes require approval before task 3. - Q2: accept Wayland unsupported v1 or approve separate portal dependency/permission design? No implicit native bindings added.
- Q3: confirm one explicit owner vault, app-running-only lifecycle and no automatic transfer/launch-on-login.
- Q4: confirm memory-only draft with quit confirmation, rather than disk plaintext recovery; proposed chord Mod+Alt+Shift+N remains user-configurable.
Feature ideas (researched, ranked)
- Wayland GlobalShortcuts portal, permission-driven binding after owner approval.
- Quick Note resume pattern from Apple Notes shortcuts: one retained draft; later multiple draft tabs only on demand.
- Platform-native shortcut preferences using global-hotkey supported backends, with real conflict diagnostics.