diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-15 12:02:32 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-15 12:02:32 -0500 |
| commit | d0790d28270594e600fbf100d9e0a4f164ce32f9 (patch) | |
| tree | caa098d94fb0f6d14027e27f7b4fad9cf2e2bd29 | |
| parent | de59e46ee20bae16dadd27209b0a08047692b767 (diff) | |
| download | dotemacs-d0790d28270594e600fbf100d9e0a4f164ce32f9.tar.gz dotemacs-d0790d28270594e600fbf100d9e0a4f164ce32f9.zip | |
refactor(org-capture): single-Task desktop popup into the org-roam inbox
The Hyprland Super+Shift+N popup now goes straight to a single Task capture into the org-roam inbox (file+headline inbox-file "Inbox"), with no template menu. It drops Bug and Event from the popup, removes the now-pointless org-mks Customize-strip advice, and replaces the Task/Bug/Event subset filter with a one-template builder, cj/--quick-capture-template. The full org-capture menu in the daemon is unchanged. todo.org: cancelled the deferred Note/Recipe popup feature and replaced the old manual-verify checklist with one matching the simpler behavior.
| -rw-r--r-- | modules/org-capture-config.el | 74 | ||||
| -rw-r--r-- | tests/test-org-capture-config-popup-window.el | 152 | ||||
| -rw-r--r-- | todo.org | 19 |
3 files changed, 61 insertions, 184 deletions
diff --git a/modules/org-capture-config.el b/modules/org-capture-config.el index 393f1d97b..18e130dc6 100644 --- a/modules/org-capture-config.el +++ b/modules/org-capture-config.el @@ -400,34 +400,21 @@ never split the small floating frame." cj/org-capture--display-sole-window)) ;; The desktop quick-capture popup is launched globally (no browser selection, -;; no mu4e message, no pdf/epub buffer), so most templates make no sense there: -;; the context fields (%:link, %i) come up empty or point at the daemon's last -;; buffer, and the pdf templates error outright. `cj/quick-capture' offers only -;; Task, Bug, and Event; Task and Bug file to the global inbox rather than a -;; project todo.org, since a desktop capture has no meaningful project context. -;; It also closes the popup frame on every exit path (abort, error, finalize) — -;; `org-capture' only runs `org-capture-after-finalize-hook' on a completed -;; capture, so a q/C-g at the template menu or an erroring template would -;; otherwise orphan the frame. The Hyprland script calls this instead of -;; `org-capture'. - -(defun cj/--org-capture-popup-templates (templates inbox) - "Return the desktop-popup subset of TEMPLATES: Task, Bug, Event. -Task (\"t\") and Bug (\"b\") are retargeted to INBOX's \"Inbox\" headline; -Event (\"e\") passes through unchanged. All other templates are dropped. -Template bodies and properties are preserved." - (delq nil - (mapcar - (lambda (entry) - (pcase (car-safe entry) - ((or "t" "b") - ;; (KEY DESC TYPE TARGET TEMPLATE . PROPS) -> retarget TARGET - (append (list (nth 0 entry) (nth 1 entry) (nth 2 entry) - (list 'file+headline inbox "Inbox")) - (nthcdr 4 entry))) - ("e" entry) - (_ nil))) - templates))) +;; no mu4e message, no pdf/epub buffer), so the context-dependent templates make +;; no sense there. `cj/quick-capture' captures a single Task straight into the +;; global inbox -- no template menu -- under its "Inbox" headline, since a +;; desktop capture has no meaningful project context. It closes the popup frame +;; on every exit path (abort, error, finalize): `org-capture' runs +;; `org-capture-after-finalize-hook' only on a completed capture, so a C-g or an +;; erroring template would otherwise orphan the frame. The Hyprland script +;; calls this instead of `org-capture'. + +(defun cj/--quick-capture-template (inbox) + "Return the desktop quick-capture template: a single Task into INBOX's Inbox. +INBOX is the inbox file path; the Task files under its \"Inbox\" headline." + (list (list "t" "Task" 'entry + (list 'file+headline inbox "Inbox") + "* TODO %?" :prepend t))) (defun cj/org-capture--popup-frame () "Return a live frame named \"org-capture\" (the quick-capture popup), or nil." @@ -438,8 +425,8 @@ Template bodies and properties are preserved." (defun cj/quick-capture () "Org-capture entry point for the Hyprland desktop popup (frame \"org-capture\"). -Offers only Task, Bug, and Event; Task and Bug file to the global inbox. -Closes the popup frame on abort or error so a stray selection never orphans it. +Captures a single Task into the global inbox, with no template menu. +Closes the popup frame on abort or error so a stray launch never orphans it. Selects the \"org-capture\" frame by name before capturing rather than trusting the ambient selected frame: the launching =emacsclient -c -e= runs before @@ -450,34 +437,11 @@ daemon's main frame and the capture would otherwise land there." (condition-case err (progn (when frame (select-frame-set-input-focus frame)) - (let ((org-capture-templates - (cj/--org-capture-popup-templates org-capture-templates inbox-file))) - (org-capture))) + (let ((org-capture-templates (cj/--quick-capture-template inbox-file))) + (org-capture nil "t"))) (quit (cj/org-capture--delete-popup-frame)) (error (message "Quick-capture: %s" (error-message-string err)) (cj/org-capture--delete-popup-frame))))) -;; The template menu's "C — Customize org-capture-templates" special makes no -;; sense in the desktop popup (it would open a Customize buffer in the floating -;; frame). Strip it from the menu when the selection runs in the popup frame, -;; keeping "q — Abort". `org-mks' is the menu primitive; advising it (gated on -;; the frame name) catches the capture template selection without touching -;; org-mks's other callers. - -(defun cj/--org-capture-popup-strip-specials (specials) - "Remove the \"C\" Customize entry from org-mks SPECIALS, keeping the rest. -SPECIALS is the org-mks specials alist (e.g. the Customize and Abort entries)." - (delq nil (mapcar (lambda (s) (unless (equal (car-safe s) "C") s)) specials))) - -(defun cj/org-capture--popup-mks-advice (orig table title &optional prompt specials) - "Around-advice for `org-mks': hide the Customize special in the quick-capture popup. -ORIG is the real `org-mks'; TABLE TITLE PROMPT SPECIALS are its arguments." - (funcall orig table title prompt - (if (cj/org-capture--popup-frame-p) - (cj/--org-capture-popup-strip-specials specials) - specials))) - -(advice-add 'org-mks :around #'cj/org-capture--popup-mks-advice) - (provide 'org-capture-config) ;;; org-capture-config.el ends here. diff --git a/tests/test-org-capture-config-popup-window.el b/tests/test-org-capture-config-popup-window.el index 34f67b36e..d308fc2b7 100644 --- a/tests/test-org-capture-config-popup-window.el +++ b/tests/test-org-capture-config-popup-window.el @@ -1,13 +1,12 @@ -;;; test-org-capture-config-popup-window.el --- Quick-capture popup single-window tests -*- lexical-binding: t; -*- +;;; test-org-capture-config-popup-window.el --- Quick-capture popup tests -*- lexical-binding: t; -*- ;;; Commentary: -;; Tests for the pure predicate behind the quick-capture popup single-window -;; fix. The Hyprland Super+Shift+N popup opens an emacsclient frame named -;; "org-capture"; in that frame the *Org Select* template menu and the -;; CAPTURE-* buffer must fill the frame's sole window instead of splitting it. -;; `cj/org-capture--popup-sole-window-p' is the frame+buffer decision; the -;; display-buffer action that acts on it is exercised by hand (window ops), -;; not here. +;; Tests for the Hyprland Super+Shift+N quick-capture popup. The popup opens an +;; emacsclient frame named "org-capture" and runs `cj/quick-capture', which +;; captures a single Task into the global inbox with no template menu. Covered +;; here: the sole-window predicate and display action (the CAPTURE-* buffer +;; fills the frame), the single-Task template builder, frame discovery and focus +;; (the emacsclient focus race), and frame cleanup on every exit path. ;;; Code: @@ -19,18 +18,6 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (require 'org-capture-config) -(defconst test-org-capture-popup--sample-templates - '(("t" "Task" entry (function cj/--org-capture-project-location) - "* TODO %?" :prepend t) - ("b" "Bug" entry (function cj/--org-capture-project-location) - "* TODO [#C] %?" :prepend t) - ("e" "Event" entry (file+headline schedule-file "Scheduled Events") - "* %?" :prepend t :prepare-finalize cj/org-capture-format-event-headline) - ("m" "Mu4e Email" entry (file+headline inbox-file "Inbox") "* TODO %?" :prepend t) - ("L" "Link" entry (file+headline inbox-file "Inbox") "* %?" :immediate-finish t) - ("d" "Drill Question" entry (file ignore) "* Item :drill:\n%?" :prepend t)) - "A representative org-capture-templates list for popup-subset tests.") - ;;; cj/org-capture--popup-sole-window-p (ert-deftest test-org-capture-config-popup-sole-window-p-select-menu () @@ -73,9 +60,6 @@ Components integrated: - display-buffer / display-buffer-alist (real) Validates the popup frame ends with one window showing the CAPTURE buffer." - ;; The batch frame is auto-named (\"F1\"), which cannot be restored by name - ;; (\"F<num> usurped by Emacs\"); reset to nil to return it to auto-naming, - ;; keeping the test independent of execution order. (let ((buf (get-buffer-create "CAPTURE-itest"))) (unwind-protect (progn @@ -87,76 +71,47 @@ Validates the popup frame ends with one window showing the CAPTURE buffer." (set-frame-parameter nil 'name nil) (when (buffer-live-p buf) (kill-buffer buf))))) -;;; cj/--org-capture-popup-templates (pure subset/retarget) - -(ert-deftest test-org-capture-config-popup-templates-keeps-tbe () - "Normal: only Task, Bug, Event survive, preserving order." - (should (equal (mapcar #'car (cj/--org-capture-popup-templates - test-org-capture-popup--sample-templates "/inbox.org")) - '("t" "b" "e")))) +;;; cj/--quick-capture-template (single Task into the inbox) -(ert-deftest test-org-capture-config-popup-templates-retargets-task-bug () - "Normal: Task and Bug retarget to the inbox \"Inbox\" headline; body + props kept." - (let* ((result (cj/--org-capture-popup-templates - test-org-capture-popup--sample-templates "/inbox.org")) - (task (assoc "t" result)) - (bug (assoc "b" result))) +(ert-deftest test-org-capture-config-quick-capture-template () + "Normal: the quick-capture template is a single Task into INBOX's Inbox." + (let* ((tmpl (cj/--quick-capture-template "/inbox.org")) + (task (assoc "t" tmpl))) + (should (equal (mapcar #'car tmpl) '("t"))) + (should (equal (nth 1 task) "Task")) + (should (eq (nth 2 task) 'entry)) (should (equal (nth 3 task) '(file+headline "/inbox.org" "Inbox"))) - (should (equal (nth 3 bug) '(file+headline "/inbox.org" "Inbox"))) (should (equal (nth 4 task) "* TODO %?")) - (should (equal (nth 4 bug) "* TODO [#C] %?")) (should (memq :prepend task)))) -(ert-deftest test-org-capture-config-popup-templates-event-unchanged () - "Boundary: Event passes through untouched, schedule-file target and props intact." - (let ((event (assoc "e" (cj/--org-capture-popup-templates - test-org-capture-popup--sample-templates "/inbox.org")))) - (should (equal (nth 3 event) '(file+headline schedule-file "Scheduled Events"))) - (should (memq :prepare-finalize event)))) - -(ert-deftest test-org-capture-config-popup-templates-drops-context-templates () - "Boundary: context-dependent templates (mu4e, link, drill) are dropped." - (let ((result (cj/--org-capture-popup-templates - test-org-capture-popup--sample-templates "/inbox.org"))) - (should-not (assoc "m" result)) - (should-not (assoc "L" result)) - (should-not (assoc "d" result)))) - -(ert-deftest test-org-capture-config-popup-templates-empty () - "Error/Boundary: empty or all-dropped input yields nil without raising." - (should-not (cj/--org-capture-popup-templates nil "/inbox.org")) - (should-not (cj/--org-capture-popup-templates - '(("L" "Link" entry (file+headline f "Inbox") "* %?")) "/inbox.org"))) - -;;; cj/quick-capture (binds the subset; integration with a stubbed org-capture) - -(ert-deftest test-integration-org-capture-quick-capture-binds-subset () - "Integration: cj/quick-capture runs org-capture with only Task/Bug/Event, -Task and Bug retargeted to the inbox. +;;; cj/quick-capture (single Task; stubbed org-capture) + +(ert-deftest test-integration-org-capture-quick-capture-binds-task-only () + "Integration: cj/quick-capture runs org-capture with a single Task template +targeting the inbox, dispatched by key. Components integrated: - cj/quick-capture (real) -- cj/--org-capture-popup-templates (real) -- org-capture (MOCKED — records the dynamically-bound templates)" - (let ((org-capture-templates test-org-capture-popup--sample-templates) - captured) +- cj/--quick-capture-template (real) +- org-capture (MOCKED — records the bound templates and dispatch key)" + (let (captured key) (cl-letf (((symbol-function 'org-capture) - (lambda (&rest _) (setq captured org-capture-templates)))) + (lambda (&optional _goto k) (setq captured org-capture-templates key k)))) (cj/quick-capture)) - (should (equal (mapcar #'car captured) '("t" "b" "e"))) + (should (equal (mapcar #'car captured) '("t"))) (should (equal (nth 3 (assoc "t" captured)) (list 'file+headline inbox-file "Inbox"))) - (should (equal (nth 3 (assoc "b" captured)) (list 'file+headline inbox-file "Inbox"))))) + (should (equal (nth 4 (assoc "t" captured)) "* TODO %?")) + (should (equal key "t")))) (ert-deftest test-integration-org-capture-quick-capture-closes-frame-on-abort () - "Integration: when selection aborts (org-capture signals), cj/quick-capture + "Integration: when capture aborts (org-capture signals), cj/quick-capture deletes the popup frame instead of leaving it orphaned. Components integrated: - cj/quick-capture (real) - org-capture (MOCKED — signals user-error \"Abort\") - cj/org-capture--delete-popup-frame (MOCKED — records the call)" - (let ((org-capture-templates test-org-capture-popup--sample-templates) - (deleted 0)) + (let ((deleted 0)) (cl-letf (((symbol-function 'org-capture) (lambda (&rest _) (user-error "Abort"))) ((symbol-function 'cj/org-capture--delete-popup-frame) @@ -166,8 +121,7 @@ Components integrated: (ert-deftest test-integration-org-capture-quick-capture-closes-frame-on-quit () "Integration: a C-g (quit) during capture also closes the popup frame." - (let ((org-capture-templates test-org-capture-popup--sample-templates) - (deleted 0)) + (let ((deleted 0)) (cl-letf (((symbol-function 'org-capture) (lambda (&rest _) (signal 'quit nil))) ((symbol-function 'cj/org-capture--delete-popup-frame) @@ -178,31 +132,13 @@ Components integrated: (ert-deftest test-integration-org-capture-quick-capture-keeps-frame-on-success () "Integration: a successful capture (no signal) does NOT delete the frame — the finalize hook owns that." - (let ((org-capture-templates test-org-capture-popup--sample-templates) - (deleted 0)) + (let ((deleted 0)) (cl-letf (((symbol-function 'org-capture) (lambda (&rest _) nil)) ((symbol-function 'cj/org-capture--delete-popup-frame) (lambda () (cl-incf deleted)))) (cj/quick-capture)) (should (= deleted 0)))) -;;; cj/--org-capture-popup-strip-specials (drop the Customize menu entry) - -(ert-deftest test-org-capture-config-popup-strip-specials-removes-customize () - "Normal: the \"C\" Customize entry is removed, \"q\" Abort kept, order intact." - (should (equal (cj/--org-capture-popup-strip-specials - '(("C" "Customize org-capture-templates") ("q" "Abort"))) - '(("q" "Abort"))))) - -(ert-deftest test-org-capture-config-popup-strip-specials-no-customize () - "Boundary: specials without a \"C\" entry pass through unchanged." - (should (equal (cj/--org-capture-popup-strip-specials '(("q" "Abort"))) - '(("q" "Abort"))))) - -(ert-deftest test-org-capture-config-popup-strip-specials-empty () - "Error/Boundary: nil specials yields nil without raising." - (should-not (cj/--org-capture-popup-strip-specials nil))) - ;;; cj/org-capture--popup-frame-p (ert-deftest test-org-capture-config-popup-frame-p () @@ -212,26 +148,6 @@ the finalize hook owns that." (cl-letf (((symbol-function 'frame-parameter) (lambda (&rest _) "emacs"))) (should-not (cj/org-capture--popup-frame-p)))) -;;; cj/org-capture--popup-mks-advice (frame-gated specials stripping) - -(ert-deftest test-org-capture-config-popup-mks-advice-strips-in-popup () - "Integration: in the popup frame, org-mks receives specials without \"C\"." - (let (seen) - (cl-letf (((symbol-function 'cj/org-capture--popup-frame-p) (lambda () t))) - (cj/org-capture--popup-mks-advice - (lambda (_table _title _prompt specials) (setq seen specials)) - nil nil nil '(("C" "Customize org-capture-templates") ("q" "Abort")))) - (should (equal seen '(("q" "Abort")))))) - -(ert-deftest test-org-capture-config-popup-mks-advice-keeps-elsewhere () - "Integration: in a normal frame, org-mks receives the specials untouched." - (let (seen) - (cl-letf (((symbol-function 'cj/org-capture--popup-frame-p) (lambda () nil))) - (cj/org-capture--popup-mks-advice - (lambda (_table _title _prompt specials) (setq seen specials)) - nil nil nil '(("C" "Customize org-capture-templates") ("q" "Abort")))) - (should (equal seen '(("C" "Customize org-capture-templates") ("q" "Abort")))))) - ;;; cj/org-capture--popup-frame (find the popup frame by name) (ert-deftest test-org-capture-config-popup-frame-found () @@ -254,8 +170,7 @@ the finalize hook owns that." (ert-deftest test-integration-org-capture-quick-capture-selects-named-frame () "Integration: cj/quick-capture selects the \"org-capture\" frame found by name, not whatever frame happens to be selected (the emacsclient -c focus race)." - (let ((org-capture-templates test-org-capture-popup--sample-templates) - (focused nil)) + (let ((focused nil)) (cl-letf (((symbol-function 'cj/org-capture--popup-frame) (lambda () 'popup-frame)) ((symbol-function 'select-frame-set-input-focus) (lambda (f) (setq focused f))) @@ -266,8 +181,7 @@ not whatever frame happens to be selected (the emacsclient -c focus race)." (ert-deftest test-integration-org-capture-quick-capture-no-frame-still-captures () "Integration: when no popup frame is found, cj/quick-capture skips the focus call and still runs the capture (no error)." - (let ((org-capture-templates test-org-capture-popup--sample-templates) - (focused 'unset) + (let ((focused 'unset) (captured nil)) (cl-letf (((symbol-function 'cj/org-capture--popup-frame) (lambda () nil)) ((symbol-function 'select-frame-set-input-focus) @@ -108,8 +108,9 @@ The ai-term window should dock from whichever edge conserves more screen space, :END: Spec: [[id:540bf06b-16b8-46c6-b459-c40d1b9c795d][keybinding-console-safety-spec-doing.org]]. Phase 0 (revert 4a1ecf64) is done and pushed. Decisions D1-D5 are open TODOs in the spec; D2/D4/D5 gate the primary work (Phase 1 prune via Appendix D, Phase 2 consolidate + retire the translation block), while D1/D3 (the console-safe prefix) gate only the optional Phase 3 and can stay open indefinitely. Resolve D2/D4/D5, then run Phase 1-2. Appendix D is the keybinding pruning checklist. Add a =#+TODO: TODO | DONE SUPERSEDED CANCELLED= header line to the spec if adopting those decision keywords (rulesets convention update, 2026-06-12). -** TODO [#D] Desktop quick-capture: Note + Recipe types :feature:solo: -Deferred 2026-06-13 — build when the need triggers, not ahead of use. Add generic Note (timestamped datetree) and Recipe (skeleton with Ingredients/Instructions + :SOURCE:) capture types to =cj/quick-capture= in =modules/org-capture-config.el=: one template each with an absolute target plus its key in the desktop subset; reuse the existing frame-cleanup. Full design in the archsetup handoff (2026-06-13 note in the inbox/sessions). +** CANCELLED [#D] Desktop quick-capture: Note + Recipe types :feature:solo: +CLOSED: [2026-06-15 Mon] +Superseded 2026-06-15: the desktop popup was simplified to a single Task into the org-roam inbox (no Bug/Event, no template menu), so adding Note/Recipe types to the popup subset no longer applies. ** TODO [#A] Calibre Open Work :PROPERTIES: @@ -4555,15 +4556,13 @@ What we're verifying: M-P (reconcile open repos) now visits repos whose director - Run M-P (or M-x cj/reconcile-open-repos) - Watch the per-repo progress / final summary Expected: dot-named repos under ~/code (mcp.el, gptel-mcp.el, capture.el, google-contacts.el, …) appear in the reconciliation pass, not just dot-free ones. -*** TODO org-capture quick-capture popup behaves correctly -What we're verifying: the Hyprland Super+Shift+N popup is single-window, offers only the sensible templates, files to the inbox, never orphans its frame, and runs the capture in the popup even when launched from a focused main frame (archsetup request 2026-06-12; fixes in modules/org-capture-config.el incl. the frame-targeting focus-race fix, all pushed and live). archsetup verified the base case on ratio 2026-06-12; the focus-race fix landed after. +*** VERIFY org-capture popup captures a single Task into the inbox +What we're verifying: Super+Shift+N pops straight into a Task capture (no template menu) targeting the org-roam inbox.org "Inbox" headline, fills the popup frame as one window, and closes the frame on every exit path even when launched from a focused main frame (the emacsclient focus race). Simplified 2026-06-15 from the old Task/Bug/Event popup. - Press Super+Shift+N to open the quick-capture popup -- The *Org Select* menu should fill the popup frame as one window (no top sliver of your last-visited buffer, one modeline) and list only Task / Bug / Event — and NOT split your main frame (the focus-race fix) -- It should show no "C — Customize org-capture-templates" row -- Pick Task (t): the CAPTURE buffer also fills the frame as one window; finishing with C-c C-c files it to the global inbox under "Inbox" (not a project's todo.org) -- Re-open and pick Event (e): it prompts for a date and files to the schedule -- Re-open and hit q (or C-g) at the menu: the popup frame closes (no orphan) -Expected: single window at every step; menu limited to Task/Bug/Event; Task/Bug land in the inbox; aborting at the menu closes the frame; the frame still closes on normal finalize and C-c C-k. +- It should go straight to the CAPTURE buffer (no *Org Select* menu), filling the frame as one window (no sliver of your last buffer, one modeline), and NOT split your main frame +- Type a task and finish with C-c C-c: the entry lands as "* TODO ..." under "Inbox" in ~/org/roam/inbox.org +- Re-open and hit C-c C-k (or C-g): the popup frame closes with no orphan +Expected: no menu; single full-frame window; the Task files under Inbox in the org-roam inbox; the frame closes on finalize, C-c C-k, and C-g. *** TODO Lock screen actually locks on Wayland What we're verifying: C-; ! l locks the screen on Wayland. slock (X11-only) never worked here; the locker now runs loginctl lock-session, which logind turns into a Lock signal that hypridle handles by running hyprlock — the same path idle/sleep locking already uses. Fix in modules/system-commands.el, live in the daemon. - Press C-; ! l (or run M-x cj/system-cmd-lock) |
