diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/custom-buffer-file.el | 10 | ||||
| -rw-r--r-- | modules/dashboard-config.el | 9 | ||||
| -rw-r--r-- | modules/dirvish-config.el | 2 | ||||
| -rw-r--r-- | modules/dwim-shell-config.el | 23 | ||||
| -rw-r--r-- | modules/help-config.el | 49 | ||||
| -rw-r--r-- | modules/mail-config.el | 34 | ||||
| -rw-r--r-- | modules/markdown-config.el | 10 | ||||
| -rw-r--r-- | modules/music-config.el | 5 | ||||
| -rw-r--r-- | modules/org-capture-config.el | 123 | ||||
| -rw-r--r-- | modules/org-drill-config.el | 9 | ||||
| -rw-r--r-- | modules/org-roam-config.el | 10 | ||||
| -rw-r--r-- | modules/prog-general.el | 16 | ||||
| -rw-r--r-- | modules/reconcile-open-repos.el | 5 | ||||
| -rw-r--r-- | modules/selection-framework.el | 10 | ||||
| -rw-r--r-- | modules/system-commands.el | 22 | ||||
| -rw-r--r-- | modules/system-defaults.el | 5 | ||||
| -rw-r--r-- | modules/system-lib.el | 11 | ||||
| -rw-r--r-- | modules/ui-navigation.el | 12 |
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 ---------------------------- |
