aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-15 12:02:32 -0500
committerCraig Jennings <c@cjennings.net>2026-06-15 12:02:32 -0500
commitd0790d28270594e600fbf100d9e0a4f164ce32f9 (patch)
treecaa098d94fb0f6d14027e27f7b4fad9cf2e2bd29
parentde59e46ee20bae16dadd27209b0a08047692b767 (diff)
downloaddotemacs-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.el74
-rw-r--r--tests/test-org-capture-config-popup-window.el152
-rw-r--r--todo.org19
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)
diff --git a/todo.org b/todo.org
index a96550cf0..84d26ea8c 100644
--- a/todo.org
+++ b/todo.org
@@ -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)