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/dirvish-config.el14
-rw-r--r--modules/dwim-shell-config.el23
-rw-r--r--modules/erc-config.el9
-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/modeline-config.el50
-rw-r--r--modules/music-config.el53
-rw-r--r--modules/org-config.el40
-rw-r--r--modules/prog-general.el7
-rw-r--r--modules/prog-lisp.el12
-rw-r--r--modules/reconcile-open-repos.el5
-rw-r--r--modules/selection-framework.el10
-rw-r--r--modules/slack-config.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-config.el57
-rw-r--r--modules/user-constants.el45
20 files changed, 240 insertions, 236 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/dirvish-config.el b/modules/dirvish-config.el
index 29ba2eba..e2fc19f1 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
@@ -513,15 +513,9 @@ Uses feh on X11, swww on Wayland."
;;; ----------------------------- Dired Text Greying ----------------------------
-;; The right-column file-size attribute uses `shadow' (#969385). Match the
-;; visible text faces to it so the column reads as one tone, with icon color
-;; supplying the only accent. `default' is remapped buffer-locally inside
-;; dired/dirvish so plain files match too — no global side effects.
-
-(with-eval-after-load 'dired
- (set-face-attribute 'dired-directory nil :foreground 'unspecified :inherit 'shadow)
- (set-face-attribute 'dired-symlink nil :foreground 'unspecified :inherit 'shadow)
- (set-face-attribute 'dired-header nil :foreground 'unspecified :inherit 'shadow))
+;; `default' is remapped buffer-locally to `shadow' inside dired/dirvish (see
+;; `cj/--dired-text-greyout' below) so plain files read grey, with icon color
+;; the only accent. The dired text faces themselves are left to the theme.
(defun cj/--dired-text-greyout ()
"Buffer-local: render `default' in `shadow' so plain files read grey."
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/erc-config.el b/modules/erc-config.el
index 22ba7f53..067b1e57 100644
--- a/modules/erc-config.el
+++ b/modules/erc-config.el
@@ -28,8 +28,10 @@
;; Load cl-lib at compile time and runtime (lightweight, already loaded in most configs)
(require 'cl-lib)
(require 'keybindings) ;; provides cj/custom-keymap
-(eval-when-compile (require 'erc)
- (require 'user-constants))
+(eval-when-compile (require 'erc))
+;; user-constants is required at runtime, not just compile time: `user-whole-name'
+;; is read at load time below (erc-user-full-name), so a standalone .elc needs it.
+(require 'user-constants)
;; ------------------------------------ ERC ------------------------------------
;; Server definitions and connection settings
@@ -97,7 +99,7 @@ Change this value to use a different nickname.")
(let ((server-buffers '()))
(dolist (buf (erc-buffer-list))
(with-current-buffer buf
- (when (eq (buffer-local-value 'erc-server-process buf) erc-server-process)
+ (when (and (erc-server-buffer-p) (erc-server-process-alive))
(unless (member (buffer-name) server-buffers)
(push (buffer-name) server-buffers)))))
@@ -222,7 +224,6 @@ Auto-adds # prefix if missing. Offers completion from configured channels."
match
move-to-prompt
noncommands
- notifications
readonly
services
stamp
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/modeline-config.el b/modules/modeline-config.el
index 0e6e5d0f..f6b8ef4e 100644
--- a/modules/modeline-config.el
+++ b/modules/modeline-config.el
@@ -75,12 +75,7 @@ Example: `my-very-long-name.el' → `my-ver...me.el'"
;; -------------------------- Modeline Segments --------------------------------
(defvar-local cj/modeline-buffer-name
- '(:eval (let* ((state (cond
- (buffer-read-only 'read-only)
- (overwrite-mode 'overwrite)
- ((buffer-modified-p) 'modified)
- (t 'unmodified)))
- (color (alist-get state cj/buffer-status-colors))
+ '(:eval (let* ((color (cj/buffer-status-color (cj/buffer-status-state)))
(name (buffer-name))
(truncated-name (cj/modeline-string-cut-middle name)))
(propertize truncated-name
@@ -137,12 +132,12 @@ Uses built-in cached values for performance.")
cj/modeline-vc-cache-set-p nil))
(defun cj/modeline-vc-cache-key (file)
- "Return the cache key for FILE.
-Includes the resolved `file-truename' so that if FILE is a symlink whose
-target moves to a different VC tree, the key changes and the cache is not
-served a stale backend. The extra `file-truename' is one stat per refresh,
-cheap next to the VC calls the cache avoids."
- (list file (file-truename file) cj/modeline-vc-show-remote))
+ "Return the cache key for FILE: the file path and `cj/modeline-vc-show-remote'.
+`file-truename' is deliberately omitted -- the mode-line rebuilds this key on
+every render to check cache validity, so a stat here would run per redisplay.
+A symlink whose target moves to a different VC tree is picked up at the next
+TTL refresh, when `vc-backend' resolves the link fresh."
+ (list file cj/modeline-vc-show-remote))
(defun cj/modeline-vc-cache-valid-p (key now)
"Return non-nil when cached VC data is valid for KEY at NOW."
@@ -157,18 +152,25 @@ Return a plist with `:branch' and `:state', or nil when FILE has no VC data.
Uses `vc-git--symbolic-ref' for branch names when available (it returns the
symbolic ref like \"main\" instead of a SHA when HEAD is on a branch), but
falls back to `vc-working-revision' if the internal accessor is missing --
-the symbol is internal and can be renamed or removed between Emacs versions."
- (unless (and (file-remote-p file) (not cj/modeline-vc-show-remote))
- (when-let* ((backend (vc-backend file))
- (branch (vc-working-revision file backend)))
- (when (eq backend 'Git)
- (unless (fboundp 'vc-git--symbolic-ref)
- (require 'vc-git nil 'noerror))
- (when (fboundp 'vc-git--symbolic-ref)
- (when-let* ((symbolic (ignore-errors (vc-git--symbolic-ref file))))
- (setq branch symbolic))))
- (list :branch branch
- :state (vc-state file backend)))))
+the symbol is internal and can be renamed or removed between Emacs versions.
+
+The whole VC probe is wrapped in `condition-case' returning nil. These are
+synchronous git calls that, on TTL expiry, run while the mode-line is built;
+on a slow or unmounted filesystem a signal here would land in redisplay and
+break it. Caching nil degrades to \"no VC info\" instead."
+ (condition-case nil
+ (unless (and (file-remote-p file) (not cj/modeline-vc-show-remote))
+ (when-let* ((backend (vc-backend file))
+ (branch (vc-working-revision file backend)))
+ (when (eq backend 'Git)
+ (unless (fboundp 'vc-git--symbolic-ref)
+ (require 'vc-git nil 'noerror))
+ (when (fboundp 'vc-git--symbolic-ref)
+ (when-let* ((symbolic (ignore-errors (vc-git--symbolic-ref file))))
+ (setq branch symbolic))))
+ (list :branch branch
+ :state (vc-state file backend))))
+ (error nil)))
(defun cj/modeline-vc-info ()
"Return cached modeline VC data for the current buffer."
diff --git a/modules/music-config.el b/modules/music-config.el
index fd619d8c..be836429 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))
@@ -721,54 +722,6 @@ For URL tracks: decoded URL."
(setq emms-track-description-function #'cj/music--track-description)
- ;; Playlist faces
- (defface cj/music-header-face
- '((((class color) (background dark))
- (:foreground "#969385"))
- (((class color) (background light))
- (:foreground "gray50")))
- "Face for playlist header labels.")
-
- (defface cj/music-header-value-face
- '((((class color) (background dark))
- (:foreground "#d0cbc0"))
- (((class color) (background light))
- (:foreground "gray30")))
- "Face for playlist header values.")
-
- (defface cj/music-mode-on-face
- '((((class color) (background dark))
- (:foreground "#d7af5f"))
- (((class color) (background light))
- (:foreground "DarkGoldenrod")))
- "Face for active mode indicators in the playlist header.")
-
- (defface cj/music-mode-off-face
- '((((class color) (background dark))
- (:foreground "#58574e"))
- (((class color) (background light))
- (:foreground "gray70")))
- "Face for inactive mode indicators in the playlist header.")
-
- (defface cj/music-keyhint-face
- '((((class color) (background dark))
- (:foreground "#8a9496"))
- (((class color) (background light))
- (:foreground "gray50")))
- "Face for keybinding hints in the playlist header.")
-
- (custom-set-faces
- '(emms-playlist-track-face
- ((((class color) (background dark))
- (:foreground "#8a9496"))
- (((class color) (background light))
- (:foreground "gray50"))))
- '(emms-playlist-selected-face
- ((((class color) (background dark))
- (:foreground "#d7af5f" :weight bold))
- (((class color) (background light))
- (:foreground "DarkGoldenrod" :weight bold)))))
-
;; Multi-line header overlay
(defvar-local cj/music--header-overlay nil
"Overlay displaying the playlist header.")
@@ -924,7 +877,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-config.el b/modules/org-config.el
index d2a0be34..26b5f0aa 100644
--- a/modules/org-config.el
+++ b/modules/org-config.el
@@ -63,23 +63,8 @@
;; -------------------------- Org Appearance Settings --------------------------
(defun cj/org-appearance-settings()
- "Set foreground, background, and font styles for org mode."
+ "Set org-mode appearance options (org faces are left to the theme)."
(interactive)
- ;; org-hide should use fix-pitch to align indents for proportional fonts
- (set-face-attribute 'org-hide nil :inherit 'fixed-pitch)
- (set-face-attribute 'org-meta-line nil :inherit 'shadow)
-
- ;; Remove foreground and background from block faces
- (set-face-attribute 'org-block nil :foreground 'unspecified :background 'unspecified)
- (set-face-attribute 'org-block-begin-line nil :foreground 'unspecified :background 'unspecified)
- (set-face-attribute 'org-block-end-line nil :foreground 'unspecified :background 'unspecified)
-
- ;; Get rid of the background on column views
- (set-face-attribute 'org-column nil :background 'unspecified)
- (set-face-attribute 'org-column-title nil :background 'unspecified)
-
- ;; make sure org-links are underlined
- (set-face-attribute 'org-link nil :underline t)
(setq org-ellipsis " ▾") ;; change ellipses to down arrow
(setq org-hide-emphasis-markers t) ;; hide emphasis markers (org-appear shows them when editing)
@@ -158,29 +143,12 @@ edge, less the tag width.")
"DELEGATED(x)" "|"
"FAILED(f!)" "DONE(d!)" "CANCELLED(c!)")))
- ;; Keyword and priority colors come from the active theme's dupre-org-*
- ;; faces (themes/dupre-faces.el) rather than hard-coded color names, so they
- ;; match the palette and dim with the rest of an unfocused window
- ;; (auto-dim-config.el remaps each to its -dim variant).
- (setq org-todo-keyword-faces
- '(("TODO" . dupre-org-todo)
- ("PROJECT" . dupre-org-project)
- ("DOING" . dupre-org-doing)
- ("WAITING" . dupre-org-waiting)
- ("VERIFY" . dupre-org-verify)
- ("STALLED" . dupre-org-stalled)
- ("DELEGATED" . dupre-org-todo)
- ("FAILED" . dupre-org-failed)
- ("DONE" . dupre-org-done)
- ("CANCELLED" . dupre-org-done)))
-
+ ;; Keyword and priority colors are left to the active theme's standard org
+ ;; faces (org-todo / org-done / org-priority) so they follow whatever theme is
+ ;; loaded rather than hard-wiring the dupre-org-* faces.
(setq org-highest-priority ?A)
(setq org-lowest-priority ?D)
(setq org-default-priority ?D)
- (setq org-priority-faces '((?A . dupre-org-priority-a)
- (?B . dupre-org-priority-b)
- (?C . dupre-org-priority-c)
- (?D . dupre-org-priority-d)))
(setq org-enforce-todo-dependencies t)
(setq org-enforce-todo-checkbox-dependencies t)
diff --git a/modules/prog-general.el b/modules/prog-general.el
index 8b4dedda..cb46ce6b 100644
--- a/modules/prog-general.el
+++ b/modules/prog-general.el
@@ -336,14 +336,9 @@ defer to `electric-pair-default-inhibit' for any other CHAR."
(use-package highlight-indent-guides
:hook (prog-mode . cj/highlight-indent-guides-enable)
:config
- ;; Disable auto face coloring to use explicit faces for better visibility across themes
+ ;; Disable auto face coloring; the guide faces are left to the theme
(setq highlight-indent-guides-auto-enabled nil)
- ;; Set explicit face backgrounds and foreground for the indentation guides
- (set-face-background 'highlight-indent-guides-odd-face "darkgray")
- (set-face-background 'highlight-indent-guides-even-face "darkgray")
- (set-face-foreground 'highlight-indent-guides-character-face "dimgray")
-
(defun cj/highlight-indent-guides-enable ()
"Enable highlight-indent-guides with preferred settings for programming modes."
(setq-local highlight-indent-guides-method 'bitmap)
diff --git a/modules/prog-lisp.el b/modules/prog-lisp.el
index a5111669..30c04ad7 100644
--- a/modules/prog-lisp.el
+++ b/modules/prog-lisp.el
@@ -131,17 +131,7 @@
(use-package rainbow-delimiters
:hook
- ((emacs-lisp-mode lisp-mode scheme-mode) . rainbow-delimiters-mode)
- :config
- (set-face-foreground 'rainbow-delimiters-depth-1-face "#c66") ;; red
- (set-face-foreground 'rainbow-delimiters-depth-2-face "#6c6") ;; green
- (set-face-foreground 'rainbow-delimiters-depth-3-face "#69f") ;; blue
- (set-face-foreground 'rainbow-delimiters-depth-4-face "#cc6") ;; yellow
- (set-face-foreground 'rainbow-delimiters-depth-5-face "#6cc") ;; cyan
- (set-face-foreground 'rainbow-delimiters-depth-6-face "#c6c") ;; magenta
- (set-face-foreground 'rainbow-delimiters-depth-7-face "#ccc") ;; light gray
- (set-face-foreground 'rainbow-delimiters-depth-8-face "#999") ;; medium gray
- (set-face-foreground 'rainbow-delimiters-depth-9-face "#666")) ;; dark gray
+ ((emacs-lisp-mode lisp-mode scheme-mode) . rainbow-delimiters-mode))
;; ----------------------------------- SLIME -----------------------------------
;; Superior Lisp Interaction Mode for Emacs (Common Lisp REPL/debugger)
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/slack-config.el b/modules/slack-config.el
index 0902ef35..adf38804 100644
--- a/modules/slack-config.el
+++ b/modules/slack-config.el
@@ -45,6 +45,7 @@
(require 'system-lib) ;; provides cj/auth-source-secret-value
(require 'cl-lib)
+(require 'keybindings) ;; provides cj/register-prefix-map
(defvar slack-current-buffer)
(defvar slack-message-compose-buffer-mode-map)
@@ -120,7 +121,9 @@ or more panes; this pins the choice to any non-selected window."
:defer t
:commands (slack-start slack-select-rooms slack-select-unread-rooms
slack-im-select slack-thread-show-or-create
- slack-insert-emoji slack-register-team)
+ slack-insert-emoji slack-register-team
+ slack-message-write-another-buffer
+ slack-message-embed-mention slack-message-embed-channel)
:custom
;; Disabled: emojify-mode in lui buffers causes (wrong-type-argument listp)
;; errors on emoji characters during lui-scroll-post-command's recenter call.
@@ -243,7 +246,8 @@ swallows exceptions via `websocket-try-callback'."
(interactive)
(let ((count 0))
(dolist (buf (buffer-list))
- (when (buffer-local-value 'slack-current-buffer buf)
+ (when (and (buffer-local-boundp 'slack-current-buffer buf)
+ (buffer-local-value 'slack-current-buffer buf))
(let ((win (get-buffer-window buf t)))
(when (and win (not (window-dedicated-p win)))
(delete-window win)))
@@ -256,7 +260,7 @@ swallows exceptions via `websocket-try-callback'."
(defvar cj/slack-keymap (make-sparse-keymap)
"Keymap for Slack commands under C-; S.")
-(global-set-key (kbd "C-; S") cj/slack-keymap)
+(cj/register-prefix-map "S" cj/slack-keymap "slack")
(define-key cj/slack-keymap (kbd "s") #'cj/slack-start)
(define-key cj/slack-keymap (kbd "c") #'slack-select-unread-rooms)
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-config.el b/modules/ui-config.el
index 7afe528b..86670b29 100644
--- a/modules/ui-config.el
+++ b/modules/ui-config.el
@@ -94,53 +94,32 @@ When `cj/enable-transparency' is nil, reset alpha to fully opaque."
(if cj/enable-transparency "enabled" "disabled")))
;; ----------------------------------- Cursor ----------------------------------
-;; set cursor color according to mode
-;;
-;; #f06a3f indicates a read-only document
-;; #c48702 indicates overwrite mode
-;; #64aa0f indicates insert and read/write mode
+;; Set the cursor color from the active theme's faces according to buffer state.
+;; The state classifier and the state->face map live in user-constants.el
+;; (cj/buffer-status-state / cj/buffer-status-faces, colored via the theme's
+;; error / warning / success faces) and are shared with the modeline buffer-name
+;; indicator, so the cursor and the modeline stay in sync.
(defvar cj/-cursor-last-color nil
"Last color applied by `cj/set-cursor-color-according-to-mode'.")
(defvar cj/-cursor-last-buffer nil
"Last buffer name where cursor color was applied.")
-(defun cj/--buffer-cursor-state ()
- "Return the buffer-state symbol used to choose the cursor color.
-
-One of `read-only', `overwrite', `modified', or `unmodified' — keys
-of `cj/buffer-status-colors'.
-
-A live ghostel terminal (in `ghostel-mode' and an input mode that
-forwards keys — semi-char / char / line) reports `unmodified' even
-though the buffer is read-only: keystrokes go to the terminal process,
-so from the user's side the buffer is writeable and the read-only
-(orange) cursor would be misleading. ghostel's `copy' and `emacs'
-input modes are the exception — there the buffer really is a read-only
-Emacs buffer the user navigates, so it falls through to `read-only'
-and keeps the orange cursor."
- (cond
- ((and (eq major-mode 'ghostel-mode)
- (not (memq (bound-and-true-p ghostel--input-mode) '(copy emacs))))
- 'unmodified)
- (buffer-read-only 'read-only)
- (overwrite-mode 'overwrite)
- ((buffer-modified-p) 'modified)
- (t 'unmodified)))
-
(defun cj/set-cursor-color-according-to-mode ()
- "Change cursor color according to buffer state (modified, read-only, overwrite).
-Only updates for real user buffers, not internal/temporary buffers.
-A no-op on non-graphical frames -- TTY/batch sessions have no cursor color
-to set."
+ "Set the cursor color from the active theme according to buffer state.
+The state and its theme face come from `cj/buffer-status-state' and
+`cj/buffer-status-color' (shared with the modeline), so the color follows the
+loaded theme. Only updates real user buffers, not internal/temporary ones; a
+no-op on non-graphical frames -- TTY/batch sessions have no cursor color to set."
(when (display-graphic-p)
- ;; Only update cursor for real buffers (not internal ones like *temp*, *Echo Area*, etc.)
- (unless (string-prefix-p " " (buffer-name)) ; Internal buffers start with space
- (let ((color (alist-get (cj/--buffer-cursor-state) cj/buffer-status-colors)))
- ;; Only skip if BOTH color AND buffer are the same (optimization)
- ;; This allows color to update when buffer state changes
- (unless (and (string= color cj/-cursor-last-color)
- (string= (buffer-name) cj/-cursor-last-buffer))
+ ;; Only update cursor for real buffers (not internal ones like *temp*, *Echo Area*).
+ (unless (string-prefix-p " " (buffer-name)) ; internal buffers start with a space
+ (let ((color (cj/buffer-status-color (cj/buffer-status-state))))
+ ;; Skip only when BOTH color and buffer are unchanged (so the color still
+ ;; updates when the buffer state changes).
+ (when (and color
+ (not (and (equal color cj/-cursor-last-color)
+ (equal (buffer-name) cj/-cursor-last-buffer))))
(set-cursor-color color)
(setq cj/-cursor-last-color color
cj/-cursor-last-buffer (buffer-name)))))))
diff --git a/modules/user-constants.el b/modules/user-constants.el
index 2e64b355..1ee8ecda 100644
--- a/modules/user-constants.el
+++ b/modules/user-constants.el
@@ -55,13 +55,44 @@ mail, chime, etc."
;; ---------------------------- Buffer Status Colors ---------------------------
-(defconst cj/buffer-status-colors
- '((read-only . "#f06a3f") ; red – buffer is read-only
- (overwrite . "#c48702") ; gold – overwrite mode
- (modified . "#64aa0f") ; green – modified & writeable
- (unmodified . "#ffffff")) ; white – unmodified & writeable
- "Alist mapping buffer states to their colors.
-Used by cursor color, modeline, and other UI elements.")
+(defconst cj/buffer-status-faces
+ '((read-only . error) ; can't edit
+ (overwrite . warning) ; overwrite mode
+ (modified . warning) ; writeable, with unsaved changes
+ (unmodified . success)) ; clean and writeable
+ "Alist mapping a buffer state to the theme face whose foreground colors it.
+Shared by the cursor color (ui-config.el) and the modeline buffer-status
+indicator (modeline-config.el) so the two stay in sync and follow the active
+theme, rather than hard-coding hex colors.")
+
+(defun cj/buffer-status-state ()
+ "Return the buffer-state symbol for the current buffer.
+One of `read-only', `overwrite', `modified', or `unmodified' -- the keys of
+`cj/buffer-status-faces'.
+
+A live ghostel terminal (in `ghostel-mode' and an input mode that forwards keys
+-- semi-char / char / line) reports `unmodified' even though the buffer is
+read-only: keystrokes go to the terminal process, so from the user's side it is
+writeable and the read-only state would be misleading. ghostel's `copy' and
+`emacs' input modes are the exception -- there the buffer really is a read-only
+Emacs buffer the user navigates, so it falls through to `read-only'."
+ (cond
+ ((and (eq major-mode 'ghostel-mode)
+ (not (memq (bound-and-true-p ghostel--input-mode) '(copy emacs))))
+ 'unmodified)
+ (buffer-read-only 'read-only)
+ (overwrite-mode 'overwrite)
+ ((buffer-modified-p) 'modified)
+ (t 'unmodified)))
+
+(defun cj/buffer-status-color (state)
+ "Return the foreground color of the theme face mapped to buffer STATE.
+Resolves STATE through `cj/buffer-status-faces' against the active theme. Nil
+when the state is unknown or its face has no concrete foreground (face-attribute
+returns the symbol `unspecified' there), so callers can skip cleanly."
+ (when-let* ((face (alist-get state cj/buffer-status-faces))
+ (fg (face-attribute face :foreground nil t)))
+ (and (stringp fg) fg)))
;; --------------------------- Media File Extensions ---------------------------