diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-13 00:51:13 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-13 00:51:13 -0500 |
| commit | fab07b7519d6eadf86d8dae0b92bf490fc6b67cc (patch) | |
| tree | 2cbef9468d1666b0d9c11a211a412409590340b5 /modules | |
| parent | b64dadbe7d2fdfadcd0cbdcd1e12f8cd2b03eb91 (diff) | |
| download | dotemacs-fab07b7519d6eadf86d8dae0b92bf490fc6b67cc.tar.gz dotemacs-fab07b7519d6eadf86d8dae0b92bf490fc6b67cc.zip | |
fix(org-capture): make the desktop quick-capture popup behave
The Hyprland Super+Shift+N popup opens a floating emacsclient frame named "org-capture" and runs a capture in it. Three things were wrong. The *Org Select* menu and the CAPTURE buffer split the small frame instead of filling it, so the daemon's last buffer leaked in beside two tmux-like modelines. The menu offered every template, including ones that can't work off the desktop: the pdf templates error outright, and the link and mu4e templates pull a link to whatever file the daemon last had open. And the frame only closed on a completed capture, so aborting at the menu or hitting an erroring template orphaned it.
I added cj/quick-capture as the launcher's entry point instead of org-capture. It offers only Task, Bug, and Event, with Task and Bug retargeted to the global inbox since a desktop capture has no real project context, and it closes the popup frame on every exit path: finalize, abort, or error. A frame-scoped display-buffer-alist entry forces the menu and capture buffer into the frame's sole window, and an org-mks advice drops the Customize entry. Both gate on the "org-capture" frame name, so in-Emacs captures keep their windows.
cj/quick-capture selects the "org-capture" frame by name before capturing rather than trusting the selected frame. The launcher runs before Hyprland settles focus on the new float, so the selected frame is still the main one and the capture would otherwise land there.
Raised from the archsetup project.
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/org-capture-config.el | 123 |
1 files changed, 122 insertions, 1 deletions
diff --git a/modules/org-capture-config.el b/modules/org-capture-config.el index b4030479..393f1d97 100644 --- a/modules/org-capture-config.el +++ b/modules/org-capture-config.el @@ -351,12 +351,133 @@ Captured On: %U" :prepend t) ;; aborts, so the popup never lingers. Frames not named "org-capture" are ;; untouched — normal in-Emacs captures keep their windows. +(defun cj/org-capture--popup-frame-p () + "Return non-nil when the selected frame is the quick-capture popup." + (equal (frame-parameter nil 'name) "org-capture")) + (defun cj/org-capture--delete-popup-frame () "Delete the current frame when it is the quick-capture popup." - (when (equal (frame-parameter nil 'name) "org-capture") + (when (cj/org-capture--popup-frame-p) (delete-frame))) (add-hook 'org-capture-after-finalize-hook #'cj/org-capture--delete-popup-frame) +;; The popup opens a fresh emacsclient frame still showing the daemon's last +;; buffer. `org-mks' shows the *Org Select* menu via +;; `switch-to-buffer-other-window', and `org-capture-place-template' shows the +;; CAPTURE-* buffer via `pop-to-buffer' with a split action — both split the +;; small floating frame, so two reverse-video modelines read like tmux bars and +;; the working buffer leaks into a popup that should only show capture UI. A +;; frame-scoped `display-buffer-alist' entry forces both into the frame's sole +;; window. Gated on the "org-capture" frame name, so normal in-Emacs captures +;; keep their windows. + +(defun cj/org-capture--popup-sole-window-p (frame-name buffer-name) + "Return non-nil when BUFFER-NAME in a frame named FRAME-NAME is capture popup UI. +Capture popup UI is the *Org Select* template menu or a CAPTURE-* buffer +shown in the quick-capture frame (FRAME-NAME equal to \"org-capture\")." + (and (equal frame-name "org-capture") + (stringp buffer-name) + (or (equal buffer-name "*Org Select*") + (string-prefix-p "CAPTURE-" buffer-name)))) + +(defun cj/org-capture--popup-display-condition (buffer-name &optional _action) + "`display-buffer' CONDITION matching capture UI in the quick-capture popup. +BUFFER-NAME is the buffer's name; the selected frame supplies the frame name." + (cj/org-capture--popup-sole-window-p (frame-parameter nil 'name) buffer-name)) + +(defun cj/org-capture--display-sole-window (buffer _alist) + "`display-buffer' ACTION showing BUFFER as the only window of the frame. +Used for the quick-capture popup so the template menu and capture buffer +never split the small floating frame." + (let ((window (frame-root-window))) + (delete-other-windows window) + (set-window-buffer window buffer) + window)) + +(add-to-list 'display-buffer-alist + '(cj/org-capture--popup-display-condition + 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))) + +(defun cj/org-capture--popup-frame () + "Return a live frame named \"org-capture\" (the quick-capture popup), or nil." + (seq-find (lambda (f) + (and (frame-live-p f) + (equal (frame-parameter f 'name) "org-capture"))) + (frame-list))) + +(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. + +Selects the \"org-capture\" frame by name before capturing rather than trusting +the ambient selected frame: the launching =emacsclient -c -e= runs before +Hyprland settles focus on the new float, so =(selected-frame)= is still the +daemon's main frame and the capture would otherwise land there." + (interactive) + (let ((frame (cj/org-capture--popup-frame))) + (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))) + (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. |
