aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/custom-buffer-file.el10
-rw-r--r--modules/dashboard-config.el9
-rw-r--r--modules/dirvish-config.el2
-rw-r--r--modules/dwim-shell-config.el23
-rw-r--r--modules/help-config.el49
-rw-r--r--modules/mail-config.el34
-rw-r--r--modules/markdown-config.el10
-rw-r--r--modules/music-config.el5
-rw-r--r--modules/org-capture-config.el123
-rw-r--r--modules/org-drill-config.el9
-rw-r--r--modules/org-roam-config.el10
-rw-r--r--modules/prog-general.el16
-rw-r--r--modules/reconcile-open-repos.el5
-rw-r--r--modules/selection-framework.el10
-rw-r--r--modules/system-commands.el22
-rw-r--r--modules/system-defaults.el5
-rw-r--r--modules/system-lib.el11
-rw-r--r--modules/ui-navigation.el12
18 files changed, 305 insertions, 60 deletions
diff --git a/modules/custom-buffer-file.el b/modules/custom-buffer-file.el
index 6c3e6c6e..84faf01d 100644
--- a/modules/custom-buffer-file.el
+++ b/modules/custom-buffer-file.el
@@ -48,6 +48,7 @@
;; mm-decode for email viewing (mm-handle-type is a macro, needs early require)
(require 'mm-decode)
(require 'external-open) ;; for cj/xdg-open, cj/open-this-file-with
+(require 'system-lib) ;; cj/confirm-strong (overwrite confirms), used below
;; cj/kill-buffer-and-window and cj/kill-other-window-buffer defined in undead-buffers.el
(declare-function cj/kill-buffer-and-window "undead-buffers")
@@ -156,7 +157,7 @@ When called interactively, prompts for confirmation if target file exists."
(condition-case _
(cj/--move-buffer-and-file dir nil)
(file-already-exists
- (if (yes-or-no-p (format "File %s exists; overwrite? " target))
+ (if (cj/confirm-strong (format "File %s exists; overwrite? " target))
(cj/--move-buffer-and-file dir t)
(message "File not moved"))))))
@@ -196,7 +197,7 @@ When called interactively, prompts for confirmation if target file exists."
(condition-case err
(cj/--rename-buffer-and-file new-name nil)
(file-already-exists
- (if (yes-or-no-p (format "File %s exists; overwrite? " new-name))
+ (if (cj/confirm-strong (format "File %s exists; overwrite? " new-name))
(cj/--rename-buffer-and-file new-name t)
(message "File not renamed")))
(error
@@ -338,7 +339,6 @@ Do not save the deleted text in the kill ring."
(kill-new (buffer-name))
(message "Copied: %s" (buffer-name)))
-(require 'system-lib)
(declare-function ansi-color-apply-on-region "ansi-color")
(defun cj/--diff-with-difftastic (file1 file2 buffer)
@@ -512,8 +512,8 @@ Signals an error if:
"m" #'cj/move-buffer-and-file
"r" #'cj/rename-buffer-and-file
"p" #'cj/copy-buffer-source-as-kill
- "d" #'cj/delete-buffer-and-file
- "D" #'cj/diff-buffer-with-file
+ "d" #'cj/diff-buffer-with-file
+ "D" #'cj/delete-buffer-and-file
"c" cj/copy-buffer-content-map
"n" #'cj/copy-buffer-name
"l" #'cj/copy-link-to-buffer-file
diff --git a/modules/dashboard-config.el b/modules/dashboard-config.el
index b4e4545d..3b8a3c5c 100644
--- a/modules/dashboard-config.el
+++ b/modules/dashboard-config.el
@@ -145,6 +145,13 @@ window."
;; --------------------------------- Dashboard ---------------------------------
;; a useful startup screen for Emacs
+(defun cj/--dashboard-exclude-emms-from-recentf ()
+ "Exclude the EMMS history file from recentf.
+Adds to `recentf-exclude' so entries set elsewhere (e.g. in
+system-defaults) are preserved rather than overwritten."
+ (require 'recentf)
+ (add-to-list 'recentf-exclude "/emms/history"))
+
(use-package dashboard
:demand t
:hook (emacs-startup . cj/dashboard-only)
@@ -196,7 +203,7 @@ window."
(setq dashboard-bookmarks-show-path nil) ;; don't show paths in bookmarks
(setq dashboard-recentf-show-base t) ;; show filename, not full path
(setq dashboard-recentf-item-format "%s")
- (setq recentf-exclude '("/emms/history")) ;; exclude EMMS history from recent files
+ (cj/--dashboard-exclude-emms-from-recentf) ;; exclude EMMS history from recent files
(setq dashboard-set-footer nil) ;; don't show footer and quotes
;; == navigation
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
index 29ba2eba..79d6ff41 100644
--- a/modules/dirvish-config.el
+++ b/modules/dirvish-config.el
@@ -27,7 +27,7 @@
;; - p: Copy absolute file path
;; - P: Print the file at point via CUPS
;; - S: Study — start an org-drill session on the .org file at point
-;; - M-S-d (Meta-Shift-d): DWIM shell commands menu
+;; - M-D (Meta-Shift-d): DWIM shell commands menu
;; - TAB: Toggle subtree expansion
;; - F11: Toggle sidebar view
diff --git a/modules/dwim-shell-config.el b/modules/dwim-shell-config.el
index 57eea706..ad17ea91 100644
--- a/modules/dwim-shell-config.el
+++ b/modules/dwim-shell-config.el
@@ -98,6 +98,7 @@
;;; Code:
(require 'cl-lib)
+(require 'system-lib) ;; cj/confirm-strong (permanent file destruction confirm)
;; --------------------------- Password-file helpers ---------------------------
@@ -197,6 +198,18 @@ file list."
(replace-regexp-in-string "'" "'\\\\''" (expand-file-name f))))
files "\n"))
+(defun cj/dwim-shell--zip-single-file-command ()
+ "Return the zip command template for a single marked file.
+The archive is named =<fne>.zip=, not a reconstruction of the input filename
+\(which produced invalid archives, and a `foo.' name for a directory)."
+ "zip -r '<<fne>>.zip' '<<f>>'")
+
+(defun cj/dwim-shell--dated-backup-command ()
+ "Return the cp command template for a timestamped backup of marked file(s).
+The timestamp is interpolated here with `format-time-string' so it can't sit
+dead inside the shell's single quotes the way a literal =$(date ...)= did."
+ (format "cp -p '<<f>>' '<<f>>.%s.bak'" (format-time-string "%Y%m%d_%H%M%S")))
+
;; ----------------------------- Dwim Shell Command ----------------------------
(use-package dwim-shell-command
@@ -336,7 +349,7 @@ Otherwise, unzip it to an appropriately named subdirectory "
(interactive)
(dwim-shell-command-on-marked-files
"Zip" (if (eq 1 (seq-length (dwim-shell-command--files)))
- "zip -r '<<fne>>.<<e>>' '<<f>>'"
+ (cj/dwim-shell--zip-single-file-command)
"zip -r '<<archive.zip(u)>>' '<<*>>'")
:utils "zip"))
@@ -546,8 +559,8 @@ clipboard contents cannot inject shell commands."
(interactive)
(dwim-shell-command-on-marked-files
"Backup with date"
- "cp -p '<<f>>' '<<f>>.$(date +%Y%m%d_%H%M%S).bak'"
- :utils '("cp" "date")))
+ (cj/dwim-shell--dated-backup-command)
+ :utils '("cp")))
(defun cj/dwim-shell-commands-optimize-image-for-web ()
"Optimize image(s) for web (reduce file size)."
@@ -801,7 +814,7 @@ switching off the .7z format to gpg-wrapped tar."
Uses =shred -u= so the file is unlinked after overwriting, matching the
\"delete\" the command name and prompt promise."
(interactive)
- (when (yes-or-no-p "This will permanently destroy files. Continue? ")
+ (when (cj/confirm-strong "This will permanently destroy files. Continue? ")
(dwim-shell-command-on-marked-files
"Secure delete"
"shred -vfzu -n 3 '<<f>>'"
@@ -929,7 +942,7 @@ gpg: decryption failed: No pinentry"
;; Bind menu to keymaps after function is defined
(with-eval-after-load 'dired
- (keymap-set dired-mode-map "M-S-d" #'dwim-shell-commands-menu)) ;; was M-D, overrides kill-word
+ (keymap-set dired-mode-map "M-D" #'dwim-shell-commands-menu)) ;; Meta-Shift-d; matches the dirvish binding below
(with-eval-after-load 'dirvish
(keymap-set dirvish-mode-map "M-D" #'dwim-shell-commands-menu)))
diff --git a/modules/help-config.el b/modules/help-config.el
index ce9fd861..df27cbea 100644
--- a/modules/help-config.el
+++ b/modules/help-config.el
@@ -50,24 +50,34 @@
;; ------------------------------------ Info -----------------------------------
- (defun cj/open-with-info-mode ()
- "Open the current buffer's file in Info mode if it's a valid info file.
+(defun cj/--info-open-plan (modified-p save-confirmed-p)
+ "Decide how to open a buffer in Info given its MODIFIED-P state.
+SAVE-CONFIRMED-P is the answer to the save prompt, meaningful only when
+MODIFIED-P. Returns `open', `save-then-open', or `cancel'."
+ (cond ((not modified-p) 'open)
+ (save-confirmed-p 'save-then-open)
+ (t 'cancel)))
+
+(defun cj/open-with-info-mode ()
+ "Open the current buffer's file in Info mode if it's a valid info file.
Preserves any unsaved changes and checks if the file exists."
- (interactive)
- (let ((file-name (buffer-file-name)))
- (when file-name
- (if (and (file-exists-p file-name)
- (string-match-p "\\.info\\'" file-name))
- (progn
- (when (buffer-modified-p)
- (if (y-or-n-p "Buffer has unsaved changes. Save before opening in Info? ")
- (save-buffer)
- (message "Operation canceled")
- (cl-return-from cj/open-with-info-mode)))
- (kill-buffer (current-buffer))
- (info file-name))
- (message "Not a valid info file: %s" file-name)))))
+ (interactive)
+ (let ((file-name (buffer-file-name)))
+ (when file-name
+ (if (and (file-exists-p file-name)
+ (string-match-p "\\.info\\'" file-name))
+ (let ((modified (buffer-modified-p)))
+ (pcase (cj/--info-open-plan
+ modified
+ (and modified
+ (y-or-n-p "Buffer has unsaved changes. Save before opening in Info? ")))
+ ('cancel (message "Operation canceled"))
+ (plan
+ (when (eq plan 'save-then-open) (save-buffer))
+ (kill-buffer (current-buffer))
+ (info file-name))))
+ (message "Not a valid info file: %s" file-name)))))
(defun cj/browse-info-files ()
"Browse and open .info or .info.gz files from user-emacs-directory."
@@ -96,7 +106,6 @@ Preserves any unsaved changes and checks if the file exists."
(:map Info-mode-map
("m" . bookmark-set) ;; Rebind 'm' from Info-menu to bookmark-set
("M" . Info-menu)) ;; Move Info-menu to 'M' instead
- :preface
:init
;; Add personal info files BEFORE Info mode initializes
;; (let ((personal-info-dir (expand-file-name "assets/info" user-emacs-directory)))
@@ -104,11 +113,7 @@ Preserves any unsaved changes and checks if the file exists."
;; (setq Info-directory-list (list personal-info-dir))))
;; the above makes the directory the info list. the below adds it to the default list
;; (add-to-list 'Info-default-directory-list personal-info-dir)))
- :hook
- (info-mode . info-persist-history-mode)
- :config
- ;; Make .info files open with our custom function
- (add-to-list 'auto-mode-alist '("\\.info\\'" . cj/open-with-info-mode)))
+ )
(provide 'help-config)
;;; help-config.el ends here.
diff --git a/modules/mail-config.el b/modules/mail-config.el
index f71d6eeb..dfc0c4e0 100644
--- a/modules/mail-config.el
+++ b/modules/mail-config.el
@@ -48,6 +48,31 @@
(defvar message-send-mail-function nil)
(defvar message-sendmail-envelope-from nil)
+(declare-function mu4e-message-field "mu4e-message")
+
+;; Refile (archive) target dispatch. A per-context `mu4e-refile-folder' string
+;; is unsafe: mu4e context :vars are sticky, so a value set when one context is
+;; active leaks into a later context that doesn't set its own -- archiving one
+;; account's mail into another's folder. A single function evaluated per
+;; message at refile time avoids that. Only cmail has a real synced Archive
+;; folder; the Gmail-backed accounts (gmail, dmail) sync no archive maildir, so
+;; refiling them would move mail into an unsynced, server-invisible folder
+;; (silent loss) -- signal instead.
+(defun cj/mu4e--refile-folder-for-maildir (maildir)
+ "Return the refile (archive) folder for MAILDIR, or signal when none exists.
+MAILDIR is a mu4e :maildir string such as \"/cmail/INBOX\"."
+ (cond
+ ((not (stringp maildir))
+ (user-error "Cannot refile: message has no maildir"))
+ ((string-prefix-p "/cmail" maildir) "/cmail/Archive")
+ (t
+ (user-error "No archive folder syncs for this account; refile disabled to avoid moving mail into an unsynced folder"))))
+
+(defun cj/mu4e--refile-folder (msg)
+ "Refile-folder function for `mu4e-refile-folder'.
+Dispatch on MSG's maildir via `cj/mu4e--refile-folder-for-maildir'."
+ (cj/mu4e--refile-folder-for-maildir (and msg (mu4e-message-field msg :maildir))))
+
(defcustom cj/smtpmail-debug-enabled nil
"Non-nil means enable verbose SMTP transport debug logging.
@@ -217,7 +242,8 @@ Prompts user for the action when executing."
:vars '((user-mail-address . "c@cjennings.net")
(user-full-name . "Craig Jennings")
(mu4e-drafts-folder . "/cmail/Drafts")
- (mu4e-sent-folder . "/cmail/Sent")))
+ (mu4e-sent-folder . "/cmail/Sent")
+ (mu4e-trash-folder . "/cmail/Trash")))
(make-mu4e-context
:name "deepsat.com"
@@ -232,6 +258,12 @@ Prompts user for the action when executing."
(mu4e-starred-folder . "/dmail/Starred")
(mu4e-trash-folder . "/dmail/Trash")))))
+ ;; Refile target is computed per message (see `cj/mu4e--refile-folder'), not
+ ;; set per context, because mu4e context :vars are sticky and would leak one
+ ;; account's archive folder into another. cmail archives to /cmail/Archive;
+ ;; gmail/dmail signal rather than move mail into an unsynced folder.
+ (setq mu4e-refile-folder #'cj/mu4e--refile-folder)
+
(setq mu4e-maildir-shortcuts
'(("/cmail/Inbox" . ?i)
("/cmail/Sent" . ?s)
diff --git a/modules/markdown-config.el b/modules/markdown-config.el
index 4faa4474..16935425 100644
--- a/modules/markdown-config.el
+++ b/modules/markdown-config.el
@@ -21,7 +21,7 @@
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:bind (:map markdown-mode-map
- ("<f2>" . markdown-preview)) ;; use same key as compile for consistency
+ ("<f2>" . cj/markdown-preview)) ;; use same key as compile for consistency
:init (setq markdown-command "multimarkdown"))
;; Register markdown as a known org-src-block language so `org-lint'
@@ -36,9 +36,7 @@
;; allows for live previews of your html
;; see: https://github.com/skeeto/impatient-mode
(use-package impatient-mode
- :defer t
- :config
- (setq imp-set-user-filter 'markdown-html))
+ :defer t)
;;;; --------------------- WIP: Markdown-Preview ---------------------
@@ -51,14 +49,14 @@ Idempotent: re-running while the server is already up is a no-op."
(message "markdown preview server running on http://localhost:8080/imp"))
;; the filter to apply to markdown before impatient-mode pushes it to the server
-(defun markdown-preview ()
+(defun cj/markdown-preview ()
"Open the current buffer as a live HTML preview at http://localhost:8080/imp.
The simple-httpd listener must already be running -- see
`cj/markdown-preview-server-start'. Starting a network listener as a
side effect of opening a preview is surprising, so the server start
lives in a separate command."
(interactive)
- (unless (and (boundp 'httpd-process) httpd-process)
+ (unless (httpd-running-p)
(user-error "markdown preview server not running; run `M-x cj/markdown-preview-server-start' first"))
(impatient-mode 1)
(setq imp-user-filter #'cj/markdown-html)
diff --git a/modules/music-config.el b/modules/music-config.el
index fd619d8c..799db133 100644
--- a/modules/music-config.el
+++ b/modules/music-config.el
@@ -95,6 +95,7 @@
(require 'user-constants)
(require 'keybindings) ;; provides cj/custom-keymap
(require 'cj-window-toggle-lib) ;; side-window size memory (F10 toggle)
+(require 'system-lib) ;; cj/confirm-strong (overwrite confirms)
;;; Settings (no Customize)
@@ -371,7 +372,7 @@ Offers completion over existing names but allows new names."
(filename (if (string-suffix-p ".m3u" chosen) chosen (concat chosen ".m3u")))
(full (expand-file-name filename cj/music-m3u-root)))
(when (and (file-exists-p full)
- (not (yes-or-no-p (format "Overwrite %s? " filename))))
+ (not (cj/confirm-strong (format "Overwrite %s? " filename))))
(user-error "Aborted saving playlist"))
(with-current-buffer (cj/music--ensure-playlist-buffer)
(let ((emms-source-playlist-ask-before-overwrite nil))
@@ -924,7 +925,7 @@ For URL tracks: decoded URL."
(file (expand-file-name (concat safe "_Radio.m3u") cj/music-m3u-root))
(content (format "#EXTM3U\n#EXTINF:-1,%s\n%s\n" name url)))
(when (and (file-exists-p file)
- (not (yes-or-no-p (format "Overwrite %s? " (file-name-nondirectory file)))))
+ (not (cj/confirm-strong (format "Overwrite %s? " (file-name-nondirectory file)))))
(user-error "Aborted creating radio station"))
(with-temp-file file
(insert content))
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.
diff --git a/modules/org-drill-config.el b/modules/org-drill-config.el
index 296b0550..2c6e400e 100644
--- a/modules/org-drill-config.el
+++ b/modules/org-drill-config.el
@@ -95,9 +95,12 @@ With a prefix arg OTHER-DIR, prompt for the directory instead of `drill-dir'."
(defun cj/drill-refile ()
"Refile to a drill file."
(interactive)
- (setq org-refile-targets '((nil :maxlevel . 1)
- (drill-dir :maxlevel . 1)))
- (call-interactively 'org-refile))
+ (let ((org-refile-targets
+ `((nil :maxlevel . 1)
+ (,(mapcar (lambda (f) (expand-file-name f drill-dir))
+ (cj/--drill-files-or-error drill-dir))
+ :maxlevel . 1))))
+ (call-interactively 'org-refile)))
;; ------------------------------- Drill Keymap --------------------------------
diff --git a/modules/org-roam-config.el b/modules/org-roam-config.el
index fdd9e1fc..218f37d6 100644
--- a/modules/org-roam-config.el
+++ b/modules/org-roam-config.el
@@ -29,6 +29,12 @@
;; ---------------------------------- Org Roam ---------------------------------
+(defconst cj/--org-roam-dailies-head
+ "#+FILETAGS: Journal\n#+TITLE: %<%Y-%m-%d>\n"
+ "Head inserted into a new org-roam daily file.
+FILETAGS and TITLE must sit on separate lines so Org parses the
+#+TITLE keyword (see `org-roam-dailies-capture-templates').")
+
(use-package org-roam
:defer 1
:commands (org-roam-node-find org-roam-node-insert org-roam-db-autosync-mode)
@@ -37,9 +43,9 @@
(org-roam-dailies-directory journals-dir)
(org-roam-completion-everywhere t)
(org-roam-dailies-capture-templates
- '(("d" "default" entry "* %<%I:%M:%S %p %Z> %?"
+ `(("d" "default" entry "* %<%I:%M:%S %p %Z> %?"
:if-new (file+head "%<%Y-%m-%d>.org"
- "#+FILETAGS: Journal #+TITLE: %<%Y-%m-%d>"))))
+ ,cj/--org-roam-dailies-head))))
(org-roam-capture-templates
`(("d" "default" plain "%?"
diff --git a/modules/prog-general.el b/modules/prog-general.el
index a4be7205..8b4dedda 100644
--- a/modules/prog-general.el
+++ b/modules/prog-general.el
@@ -298,6 +298,22 @@ This is what makes universal snippets like =<cj= work in any buffer."
(yas-reload-all)
(yas-global-mode 1))
+;; Most of the snippet keys start with "<" (=<cj=, =<for=, =<main=…), mirroring
+;; org-tempo. But `electric-pair-mode' pairs "<" into "<>" wherever the mode's
+;; syntax table gives "<" paren syntax (org, and the prog modes that enable
+;; pairing), so typing "<cj" lands as "<cj>"; expanding the "<cj" key then
+;; strands the ">" after the snippet — the cj-comment fence comes out as
+;; "#+end_src>", which breaks the cj-scan fence parser. Inhibit pairing for the
+;; open angle bracket globally; defer to the default for every other character.
+(defun cj/--electric-pair-inhibit-angle (char)
+ "Return non-nil to stop `electric-pair-mode' from pairing the angle CHAR.
+Inhibit the open angle bracket so \"<\"-prefixed yasnippet keys expand cleanly;
+defer to `electric-pair-default-inhibit' for any other CHAR."
+ (or (eq char ?<)
+ (electric-pair-default-inhibit char)))
+
+(setq electric-pair-inhibit-predicate #'cj/--electric-pair-inhibit-angle)
+
;; --------------------- Display Color On Color Declaration --------------------
;; display the actual color as highlight to color hex code
diff --git a/modules/reconcile-open-repos.el b/modules/reconcile-open-repos.el
index dd82ef0f..79a895bf 100644
--- a/modules/reconcile-open-repos.el
+++ b/modules/reconcile-open-repos.el
@@ -171,8 +171,11 @@ Prunes generated/heavy directories. Once a repository root is found, do not
descend into it unless INCLUDE-NESTED is non-nil."
(let (repos)
(when (file-directory-p directory)
- (dolist (child (directory-files directory t "^[^.]+$" 'nosort))
+ (dolist (child (directory-files directory t directory-files-no-dot-files-regexp 'nosort))
(when (and (file-directory-p child)
+ ;; Skip hidden dirs (.git, .config) but keep dotted repo
+ ;; names like mcp.el; the old "^[^.]+$" filter dropped both.
+ (not (string-prefix-p "." (file-name-nondirectory child)))
(not (cj/reconcile--pruned-directory-p child)))
(if (file-directory-p (expand-file-name ".git" child))
(progn
diff --git a/modules/selection-framework.el b/modules/selection-framework.el
index 11687337..b136ad15 100644
--- a/modules/selection-framework.el
+++ b/modules/selection-framework.el
@@ -47,6 +47,11 @@
:init
(vertico-mode))
+;; Save each completion session so `vertico-repeat' (the second C-s in
+;; `cj/consult-line-or-repeat') has a session to resume. `vertico-repeat-save'
+;; is autoloaded, so this defers loading vertico-repeat until the first minibuffer.
+(add-hook 'minibuffer-setup-hook #'vertico-repeat-save)
+
(use-package marginalia
:demand t
:custom
@@ -246,6 +251,11 @@
(use-package vertico-prescient
:demand t
+ :custom
+ ;; orderless does the matching; prescient only sorts. Without this,
+ ;; vertico-prescient-mode's default filtering overrides completion-styles to
+ ;; prescient inside vertico sessions, leaving the orderless config above dead.
+ (vertico-prescient-enable-filtering nil)
:config
(vertico-prescient-mode))
diff --git a/modules/system-commands.el b/modules/system-commands.el
index dba4d40e..44ac3ae8 100644
--- a/modules/system-commands.el
+++ b/modules/system-commands.el
@@ -9,7 +9,7 @@
;; Eager reason: registers the C-; ! system-command keymap; high-impact commands
;; that should run only by command (command-loaded target).
;; Top-level side effects: defines a system-command keymap under cj/custom-keymap.
-;; Runtime requires: keybindings, rx.
+;; Runtime requires: keybindings, host-environment, rx.
;; Direct test load: yes (requires keybindings explicitly).
;;
;; System commands for logout, lock, suspend, shutdown, reboot, and Emacs
@@ -17,7 +17,7 @@
;;
;; Commands include:
;; - Logout (terminate user session)
-;; - Lock screen (slock)
+;; - Lock screen (hyprlock on Wayland, slock on X11)
;; - Suspend (systemctl suspend)
;; - Shutdown (systemctl poweroff)
;; - Reboot (systemctl reboot)
@@ -34,6 +34,14 @@
;; the load-time reference void if anything required `system-commands'
;; before `keybindings'. Make the dependency explicit.
(require 'keybindings)
+;; `host-environment' provides `env-wayland-p', referenced at load time by the
+;; `lockscreen-cmd' defvar below to pick the session-appropriate locker. A hard
+;; require keeps the module loadable on its own (tests, byte-compile) rather
+;; than relying on init.el's load order.
+(require 'host-environment)
+;; `system-lib' provides `cj/confirm-strong', used at runtime by the `strong'
+;; confirm branch of `cj/system-cmd' for irreversible actions (shutdown/reboot).
+(require 'system-lib)
(eval-when-compile (require 'subr-x))
(require 'rx)
@@ -71,7 +79,7 @@ If CMD is deemed dangerous, ask for confirmation."
;; Strong confirm for irreversible actions (shutdown, reboot):
;; require an explicit "yes", so a stray RET/space can't trigger them.
((eq confirm 'strong)
- (unless (yes-or-no-p (format "Really run %s (%s)? " label cmdstr))
+ (unless (cj/confirm-strong (format "Really run %s (%s)? " label cmdstr))
(user-error "Aborted")))
;; Quick (Y/n) confirm for recoverable actions (logout, suspend).
(confirm
@@ -102,7 +110,13 @@ actions like shutdown and reboot), nil for no confirmation."
;; Define system commands
(cj/defsystem-command cj/system-cmd-logout logout-cmd "loginctl terminate-user $(whoami)" t)
-(cj/defsystem-command cj/system-cmd-lock lockscreen-cmd "slock")
+;; slock is X11-only and can't grab a Wayland session. On Wayland, lock via
+;; the session manager (`loginctl lock-session') rather than spawning a locker
+;; directly: logind emits the Lock signal, hypridle catches it and runs its
+;; lock_cmd (hyprlock), the same path idle/before-sleep locking already uses.
+;; X11 machines keep slock.
+(cj/defsystem-command cj/system-cmd-lock lockscreen-cmd
+ (if (env-wayland-p) "loginctl lock-session" "slock"))
(cj/defsystem-command cj/system-cmd-suspend suspend-cmd "systemctl suspend" t)
(cj/defsystem-command cj/system-cmd-shutdown shutdown-cmd "systemctl poweroff" strong)
(cj/defsystem-command cj/system-cmd-reboot reboot-cmd "systemctl reboot" strong)
diff --git a/modules/system-defaults.el b/modules/system-defaults.el
index eccc6c35..1703b1bf 100644
--- a/modules/system-defaults.el
+++ b/modules/system-defaults.el
@@ -200,8 +200,9 @@ appears only once per session."
(setq confirm-nonexistent-file-or-buffer nil) ;; don't ask if a file I visit with C-x C-f or C-x b doesn't exist
(setq ad-redefinition-action 'accept) ;; silence warnings about advised functions getting redefined.
(setq large-file-warning-threshold nil) ;; open files regardless of size
-(fset 'yes-or-no-p 'y-or-n-p) ;; require a single letter for binary answers
-(setq use-short-answers t) ;; same as above with Emacs 28+
+(setq use-short-answers t) ;; single-key y/n for ordinary yes-or-no-p prompts
+ ;; (irreversible actions use `cj/confirm-strong', which
+ ;; forces a typed "yes" by binding this nil for that call)
(setq auto-revert-verbose nil) ;; turn off auto revert messages
(setq custom-safe-themes t) ;; treat all themes as safe (stop asking)
(setq server-client-instructions nil) ;; I already know what to do when done with the frame
diff --git a/modules/system-lib.el b/modules/system-lib.el
index 333c15ee..9e25be5b 100644
--- a/modules/system-lib.el
+++ b/modules/system-lib.el
@@ -130,5 +130,16 @@ Callers that must have a secret layer their own error on top."
(secret (plist-get (car (apply #'auth-source-search spec)) :secret)))
(if (functionp secret) (funcall secret) secret)))
+;; ---------------------------- Strong Confirmation ----------------------------
+
+(defun cj/confirm-strong (prompt)
+ "Ask PROMPT, requiring a full typed \"yes\" or \"no\" answer.
+For irreversible actions -- file destruction, overwrites, power-off. The
+global default makes `yes-or-no-p' a single keystroke (`use-short-answers'
+is t); this binds it to nil for the one call so the prompt demands the
+long-form answer, keeping a stray RET or space from confirming."
+ (let ((use-short-answers nil))
+ (yes-or-no-p prompt)))
+
(provide 'system-lib)
;;; system-lib.el ends here
diff --git a/modules/ui-navigation.el b/modules/ui-navigation.el
index f1324c16..f2181d97 100644
--- a/modules/ui-navigation.el
+++ b/modules/ui-navigation.el
@@ -160,7 +160,9 @@ This function won't work with more than one split window."
;; UNDO KILL BUFFER
(defun cj/undo-kill-buffer (arg)
- "Re-open the last buffer killed. With ARG, re-open the nth buffer."
+ "Re-open the last buffer killed.
+With numeric prefix ARG, re-open the ARGth most-recently-killed file
+\(1-based, so no prefix re-opens the most recent)."
(interactive "p")
(require 'recentf)
(unless recentf-mode
@@ -177,9 +179,11 @@ This function won't work with more than one split window."
(delq buf-file recently-killed-list)))
buffer-files-list)
(when recently-killed-list
- (find-file
- (if arg (nth arg recently-killed-list)
- (car recently-killed-list))))))
+ (let ((file (nth (1- arg) recently-killed-list)))
+ (if file
+ (find-file file)
+ (user-error "Only %d killed file(s) to choose from"
+ (length recently-killed-list)))))))
(keymap-global-set "M-S-z" #'cj/undo-kill-buffer) ;; was M-Z, overrides zap-to-char
;; ---------------------------- Undo Layout Changes ----------------------------