diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/custom-buffer-file.el | 10 | ||||
| -rw-r--r-- | modules/dirvish-config.el | 14 | ||||
| -rw-r--r-- | modules/dwim-shell-config.el | 23 | ||||
| -rw-r--r-- | modules/erc-config.el | 9 | ||||
| -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/modeline-config.el | 50 | ||||
| -rw-r--r-- | modules/music-config.el | 53 | ||||
| -rw-r--r-- | modules/org-config.el | 40 | ||||
| -rw-r--r-- | modules/prog-general.el | 7 | ||||
| -rw-r--r-- | modules/prog-lisp.el | 12 | ||||
| -rw-r--r-- | modules/reconcile-open-repos.el | 5 | ||||
| -rw-r--r-- | modules/selection-framework.el | 10 | ||||
| -rw-r--r-- | modules/slack-config.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-config.el | 57 | ||||
| -rw-r--r-- | modules/user-constants.el | 45 |
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 --------------------------- |
