aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-07 09:05:54 -0500
committerCraig Jennings <c@cjennings.net>2026-05-07 09:05:54 -0500
commit4ece1ebb4487d3e565642e45d586f97172fe97ce (patch)
tree5af6afbb683bcae92622b4c70922e5d6d27c6735
parentb3ef232a486382601b6788f0c1a1edeb3982075d (diff)
downloaddotemacs-4ece1ebb4487d3e565642e45d586f97172fe97ce.tar.gz
dotemacs-4ece1ebb4487d3e565642e45d586f97172fe97ce.zip
fix: restore daemon icons and consolidate nerd-icons setup
I replaced the load-time icon-stub block in keyboard-compat with per-call :around advice that checks display-graphic-p against the rendering frame. The old block ran at module-load. Under daemon startup no frame exists yet, so display-graphic-p returned nil and the empty-string stubs installed permanently. Every GUI client connecting to that daemon then saw blanks. The new shape lets one daemon serve real icons to GUI clients and blanks to terminal clients. I also pulled the nerd-icons-completion and nerd-icons-ibuffer integrations, the package install, and a new tint helper into modules/nerd-icons-config.el. Per-feature use stays in the consuming module (dashboard, dirvish, keyboard-compat). The malformed cons-cell on the marginalia hook in selection-framework.el got fixed in the move. Added a default darkgoldenrod tint, a :filter-return advice on nerd-icons-icon-for-dir so dir icons pick up a color face, and a buffer-local face-remap in dired-mode-hook so plain files in dired render in shadow grey. 13 tests across 3 new files cover the per-call gate, the dir-color helper (idempotent under nerd-icons' memoized return strings), and the bulk-tint helper.
-rw-r--r--init.el1
-rw-r--r--modules/dirvish-config.el20
-rw-r--r--modules/font-config.el6
-rw-r--r--modules/keyboard-compat.el45
-rw-r--r--modules/nerd-icons-config.el94
-rw-r--r--modules/selection-framework.el7
-rw-r--r--modules/system-utils.el14
-rw-r--r--tests/test-keyboard-compat--icon-blank-in-terminal.el54
-rw-r--r--tests/test-nerd-icons-config--apply-tint.el63
-rw-r--r--tests/test-nerd-icons-config--color-dir.el57
10 files changed, 316 insertions, 45 deletions
diff --git a/init.el b/init.el
index d66bea34..4825a330 100644
--- a/init.el
+++ b/init.el
@@ -59,6 +59,7 @@
(cj/load-theme-from-file)
(require 'ui-navigation) ;; the movement and navigation of windows
(require 'font-config) ;; font and emoji configuration
+(require 'nerd-icons-config) ;; nerd-icons + completion/ibuffer integration + tint
(require 'selection-framework) ;; menu config
(require 'modeline-config) ;; modeline (status-bar) config
(require 'mousetrap-mode) ;; prevent accidental mouse/trackpad modifications
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
index 0ea056bd..5f30ba10 100644
--- a/modules/dirvish-config.el
+++ b/modules/dirvish-config.el
@@ -259,7 +259,7 @@ Uses feh on X11, swww on Wayland."
("pdx" "~/projects/documents/" "project documents")
("pdl" "~/projects/danneel/" "project danneel")
("pcl" "~/projects/clipper/" "project clipper")
- ("pcr" "~/projects/career/" "project career")
+ ("pwk" "~/projects/work/" "project work")
("pl" "~/projects/elibrary/" "project elibrary")
("pf" "~/projects/finances/" "project finances")
("pjr" "~/projects/jr-estate/" "project jr-estate")
@@ -363,6 +363,24 @@ Uses feh on X11, swww on Wayland."
("v" . dirvish-vc-menu)
("y" . dirvish-yank-menu)))
+;;; ----------------------------- 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))
+
+(defun cj/--dired-text-greyout ()
+ "Buffer-local: render `default' in `shadow' so plain files read grey."
+ (face-remap-add-relative 'default 'shadow))
+
+(add-hook 'dired-mode-hook #'cj/--dired-text-greyout)
+
;;; ---------------------------- Dired Hide Dotfiles ----------------------------
(use-package dired-hide-dotfiles
diff --git a/modules/font-config.el b/modules/font-config.el
index 3310580c..f7c3af5f 100644
--- a/modules/font-config.el
+++ b/modules/font-config.el
@@ -209,12 +209,6 @@ If FRAME is nil, uses the selected frame."
:config
(all-the-icons-nerd-fonts-prefer))
-;; -------------------------------- Nerd Icons ---------------------------------
-;; Modern icon fonts for UI elements
-
-(use-package nerd-icons
- :defer .5)
-
;; ----------------------------- Emoji Fonts Per OS ----------------------------
;; Set emoji fonts in priority order (first found wins)
diff --git a/modules/keyboard-compat.el b/modules/keyboard-compat.el
index 735839eb..b914978f 100644
--- a/modules/keyboard-compat.el
+++ b/modules/keyboard-compat.el
@@ -111,26 +111,31 @@ This runs after init to override any package settings."
;; Run after init completes to override any package settings
(add-hook 'emacs-startup-hook #'cj/keyboard-compat-terminal-setup)
-;; Icon disabling only in terminal mode (prevents unicode artifacts)
-(when (env-terminal-p)
- ;; Disable nerd-icons display (shows as \uXXXX artifacts)
- (with-eval-after-load 'nerd-icons
- (defun nerd-icons-icon-for-file (&rest _) "")
- (defun nerd-icons-icon-for-dir (&rest _) "")
- (defun nerd-icons-icon-for-mode (&rest _) "")
- (defun nerd-icons-icon-for-buffer (&rest _) ""))
-
- ;; Disable dashboard icons
- (with-eval-after-load 'dashboard
- (setq dashboard-display-icons-p nil)
- (setq dashboard-set-file-icons nil)
- (setq dashboard-set-heading-icons nil))
-
- ;; Disable all-the-icons
- (with-eval-after-load 'all-the-icons
- (defun all-the-icons-icon-for-file (&rest _) "")
- (defun all-the-icons-icon-for-dir (&rest _) "")
- (defun all-the-icons-icon-for-mode (&rest _) "")))
+;; Icon-rendering functions return blank on terminal frames so unicode
+;; artifacts don't show up. The check runs per call against the selected
+;; frame, so the same daemon serves real icons to GUI clients and blanks to
+;; terminal clients. Earlier this lived in a top-level (when (env-terminal-p)
+;; ...) block that redefined the icon functions at module-load time, which
+;; broke under daemon startup: no frame exists yet, display-graphic-p returns
+;; nil, env-terminal-p returns t, and the stubs install permanently. GUI
+;; clients connecting later saw empty icons everywhere.
+
+(defun cj/--icon-blank-in-terminal (orig &rest args)
+ "Return empty string on a terminal frame, otherwise call ORIG with ARGS."
+ (if (display-graphic-p) (apply orig args) ""))
+
+(with-eval-after-load 'nerd-icons
+ (dolist (fn '(nerd-icons-icon-for-file
+ nerd-icons-icon-for-dir
+ nerd-icons-icon-for-mode
+ nerd-icons-icon-for-buffer))
+ (advice-add fn :around #'cj/--icon-blank-in-terminal)))
+
+(with-eval-after-load 'all-the-icons
+ (dolist (fn '(all-the-icons-icon-for-file
+ all-the-icons-icon-for-dir
+ all-the-icons-icon-for-mode))
+ (advice-add fn :around #'cj/--icon-blank-in-terminal)))
;; =============================================================================
;; GUI-specific fixes
diff --git a/modules/nerd-icons-config.el b/modules/nerd-icons-config.el
new file mode 100644
index 00000000..4a8ce194
--- /dev/null
+++ b/modules/nerd-icons-config.el
@@ -0,0 +1,94 @@
+;;; nerd-icons-config.el --- Nerd-icons setup, integrations, and tinting -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;;
+;; Single home for nerd-icons:
+;; - the package itself
+;; - completion integration (`nerd-icons-completion')
+;; - ibuffer integration (`nerd-icons-ibuffer')
+;; - bulk color tinting of every `nerd-icons-*' color face
+;; - dir-icon color advice (so directory glyphs carry a color face like
+;; file glyphs do, instead of falling through to the buffer default
+;; face)
+;;
+;; Per-feature USE of nerd-icons stays in the feature module that consumes
+;; it: `dashboard-icon-type', `dirvish-attributes', and the keyboard-compat
+;; terminal-frame icon-blanking advice are not centralized here.
+
+;;; Code:
+
+;; ----------------------------- Customization ---------------------------------
+
+(defcustom cj/nerd-icons-tint-color "darkgoldenrod"
+ "Single foreground color applied to every `nerd-icons-*' color face.
+Set via Customize or by `setq' before this module loads, then call
+`cj/nerd-icons-apply-tint' to re-apply on demand."
+ :type 'string
+ :group 'cj)
+
+(defconst cj/--nerd-icons-color-faces
+ '(nerd-icons-red nerd-icons-lred nerd-icons-dred nerd-icons-red-alt
+ nerd-icons-green nerd-icons-lgreen nerd-icons-dgreen
+ nerd-icons-yellow nerd-icons-lyellow nerd-icons-dyellow
+ nerd-icons-orange nerd-icons-lorange nerd-icons-dorange
+ nerd-icons-blue nerd-icons-blue-alt nerd-icons-lblue nerd-icons-dblue
+ nerd-icons-cyan nerd-icons-cyan-alt nerd-icons-lcyan nerd-icons-dcyan
+ nerd-icons-purple nerd-icons-purple-alt nerd-icons-lpurple nerd-icons-dpurple
+ nerd-icons-pink nerd-icons-lpink nerd-icons-dpink
+ nerd-icons-maroon nerd-icons-lmaroon nerd-icons-dmaroon
+ nerd-icons-silver nerd-icons-lsilver nerd-icons-dsilver)
+ "Every color face nerd-icons attaches to glyphs via `:inherit'.")
+
+;; ------------------------------- Helpers -------------------------------------
+
+(defun cj/nerd-icons-apply-tint (&optional color)
+ "Set every face in `cj/--nerd-icons-color-faces' to foreground COLOR.
+COLOR defaults to `cj/nerd-icons-tint-color'. Faces that are not yet
+defined (nerd-icons not loaded) are silently skipped."
+ (interactive)
+ (let ((c (or color cj/nerd-icons-tint-color)))
+ (dolist (f cj/--nerd-icons-color-faces)
+ (when (facep f)
+ (set-face-foreground f c)))))
+
+(defun cj/--nerd-icons-color-dir (icon)
+ "Layer `nerd-icons-yellow' onto ICON's face stack and return ICON.
+ICON is the propertized string returned by `nerd-icons-icon-for-dir'.
+Without this, directory icons render in the buffer default face — so a
+buffer-local face-remap of `default' catches them unintentionally.
+
+Idempotent: nerd-icons memoizes its return strings, and this advice modifies
+in place, so a naive `add-face-text-property' would stack the face symbol on
+every call. The `memq' check skips when the face is already present."
+ (when (and (stringp icon) (> (length icon) 0))
+ (let ((faces (ensure-list (get-text-property 0 'face icon))))
+ (unless (memq 'nerd-icons-yellow faces)
+ (add-face-text-property 0 (length icon) 'nerd-icons-yellow nil icon))))
+ icon)
+
+;; ------------------------------- Packages ------------------------------------
+
+(use-package nerd-icons
+ :demand t
+ :config
+ (advice-add 'nerd-icons-icon-for-dir :filter-return #'cj/--nerd-icons-color-dir)
+ (cj/nerd-icons-apply-tint))
+
+(use-package nerd-icons-completion
+ :demand t
+ :after (nerd-icons marginalia)
+ :hook (marginalia-mode . nerd-icons-completion-marginalia-setup)
+ :config
+ (nerd-icons-completion-mode))
+
+(use-package nerd-icons-ibuffer
+ :after nerd-icons
+ :hook (ibuffer-mode . nerd-icons-ibuffer-mode)
+ :custom
+ (nerd-icons-ibuffer-icon t)
+ (nerd-icons-ibuffer-color-icon t)
+ (nerd-icons-ibuffer-human-readable-size t))
+
+(provide 'nerd-icons-config)
+;;; nerd-icons-config.el ends here
diff --git a/modules/selection-framework.el b/modules/selection-framework.el
index 1129ac05..2a714866 100644
--- a/modules/selection-framework.el
+++ b/modules/selection-framework.el
@@ -45,13 +45,6 @@
:init
(marginalia-mode))
-(use-package nerd-icons-completion
- :demand t
- :hook (marginalia-mode nerd-icons-completion-marginalia-setup)
- :after marginalia
- :init
- (nerd-icons-completion-mode))
-
;; ---------------------------------- Consult ----------------------------------
;; Practical commands based on completing-read
diff --git a/modules/system-utils.el b/modules/system-utils.el
index 86c2ae16..9a81c402 100644
--- a/modules/system-utils.el
+++ b/modules/system-utils.el
@@ -176,20 +176,12 @@ Logs output and exit code to buffer *external-open.log*."
(savehist-mode))
)
-;;; ------------------------ List Buffers With Nerd Icons -----------------------
+;;; ----------------------------- List Buffers Remap ----------------------------
-;; Remap list-buffers to ibuffer (built-in). Keybinding is separate from
-;; nerd-icons-ibuffer package, which only adds icons to ibuffer.
+;; Remap list-buffers to ibuffer (built-in). Icons come from nerd-icons-ibuffer
+;; in `nerd-icons-config'.
(keymap-global-set "<remap> <list-buffers>" #'ibuffer)
-(use-package nerd-icons-ibuffer
- :after nerd-icons
- :hook (ibuffer-mode . nerd-icons-ibuffer-mode)
- :config
- (setq nerd-icons-ibuffer-icon t
- nerd-icons-ibuffer-color-icon t
- nerd-icons-ibuffer-human-readable-size t))
-
;;; -------------------------- Scratch Buffer Happiness -------------------------
(defvar scratch-emacs-version-and-system
diff --git a/tests/test-keyboard-compat--icon-blank-in-terminal.el b/tests/test-keyboard-compat--icon-blank-in-terminal.el
new file mode 100644
index 00000000..db2bb6b7
--- /dev/null
+++ b/tests/test-keyboard-compat--icon-blank-in-terminal.el
@@ -0,0 +1,54 @@
+;;; test-keyboard-compat--icon-blank-in-terminal.el --- Tests for cj/--icon-blank-in-terminal -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the per-call icon-rendering gate that replaced the load-time
+;; defun-redefinition. The helper must dispatch on the current frame's
+;; `display-graphic-p' so the same daemon can serve real icons to GUI frames
+;; and blanks to terminal frames.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'keyboard-compat)
+
+(defmacro test-keyboard-compat--with-graphic-p (graphic-p &rest body)
+ "Run BODY with `display-graphic-p' stubbed to GRAPHIC-P."
+ (declare (indent 1) (debug t))
+ `(cl-letf (((symbol-function 'display-graphic-p)
+ (lambda (&optional _) ,graphic-p)))
+ ,@body))
+
+(ert-deftest test-keyboard-compat--icon-blank-in-terminal-passes-through-on-gui ()
+ "Normal: returns ORIG's value when display-graphic-p is non-nil."
+ (test-keyboard-compat--with-graphic-p t
+ (let ((orig (lambda (&rest _) "ICON")))
+ (should (equal (cj/--icon-blank-in-terminal orig "foo.txt") "ICON")))))
+
+(ert-deftest test-keyboard-compat--icon-blank-in-terminal-blank-on-terminal ()
+ "Boundary: returns empty string when display-graphic-p is nil."
+ (test-keyboard-compat--with-graphic-p nil
+ (let ((orig (lambda (&rest _) "ICON")))
+ (should (equal (cj/--icon-blank-in-terminal orig "foo.txt") "")))))
+
+(ert-deftest test-keyboard-compat--icon-blank-in-terminal-forwards-args ()
+ "Normal: forwards all ARGS to ORIG when graphical."
+ (test-keyboard-compat--with-graphic-p t
+ (let ((received nil))
+ (cj/--icon-blank-in-terminal
+ (lambda (&rest args) (setq received args) "ok")
+ 'a 'b 'c)
+ (should (equal received '(a b c))))))
+
+(ert-deftest test-keyboard-compat--icon-blank-in-terminal-does-not-call-orig-on-terminal ()
+ "Error: ORIG must not be invoked when display-graphic-p is nil."
+ (test-keyboard-compat--with-graphic-p nil
+ (let ((called nil))
+ (cj/--icon-blank-in-terminal
+ (lambda (&rest _) (setq called t) "ICON"))
+ (should-not called))))
+
+(provide 'test-keyboard-compat--icon-blank-in-terminal)
+;;; test-keyboard-compat--icon-blank-in-terminal.el ends here
diff --git a/tests/test-nerd-icons-config--apply-tint.el b/tests/test-nerd-icons-config--apply-tint.el
new file mode 100644
index 00000000..ef723352
--- /dev/null
+++ b/tests/test-nerd-icons-config--apply-tint.el
@@ -0,0 +1,63 @@
+;;; test-nerd-icons-config--apply-tint.el --- Tests for cj/nerd-icons-apply-tint -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the bulk-tint helper. Mocks `set-face-foreground' and `facep'
+;; at the framework boundary so the tests don't depend on nerd-icons being
+;; loaded — only on the symbol list and the dispatch logic.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'nerd-icons-config)
+
+(defmacro test-nerd-icons-config--capture-set-face-foreground (calls-var &rest body)
+ "Run BODY with `set-face-foreground' and `facep' stubbed.
+Each (face color) pair gets pushed onto CALLS-VAR. `facep' returns t
+for every symbol so all faces in the list count as defined."
+ (declare (indent 1) (debug t))
+ `(cl-letf (((symbol-function 'set-face-foreground)
+ (lambda (face color &rest _) (push (cons face color) ,calls-var)))
+ ((symbol-function 'facep)
+ (lambda (_) t)))
+ ,@body))
+
+(ert-deftest test-nerd-icons-config--apply-tint-covers-every-face ()
+ "Normal: apply-tint calls set-face-foreground once per face in the list."
+ (let ((calls nil))
+ (test-nerd-icons-config--capture-set-face-foreground calls
+ (cj/nerd-icons-apply-tint "test-color"))
+ (should (= (length calls) (length cj/--nerd-icons-color-faces)))
+ (dolist (face cj/--nerd-icons-color-faces)
+ (should (assq face calls)))))
+
+(ert-deftest test-nerd-icons-config--apply-tint-passes-color-arg ()
+ "Normal: apply-tint forwards COLOR to every set-face-foreground call."
+ (let ((calls nil))
+ (test-nerd-icons-config--capture-set-face-foreground calls
+ (cj/nerd-icons-apply-tint "rebeccapurple"))
+ (dolist (call calls)
+ (should (equal (cdr call) "rebeccapurple")))))
+
+(ert-deftest test-nerd-icons-config--apply-tint-defaults-to-customvar ()
+ "Normal: with no COLOR arg, uses `cj/nerd-icons-tint-color'."
+ (let ((calls nil))
+ (test-nerd-icons-config--capture-set-face-foreground calls
+ (let ((cj/nerd-icons-tint-color "default-test-color"))
+ (cj/nerd-icons-apply-tint)))
+ (should (cl-every (lambda (call) (equal (cdr call) "default-test-color")) calls))))
+
+(ert-deftest test-nerd-icons-config--apply-tint-skips-undefined-faces ()
+ "Boundary: faces that fail `facep' are silently skipped, not errored."
+ (let ((calls nil))
+ (cl-letf (((symbol-function 'set-face-foreground)
+ (lambda (face color &rest _) (push (cons face color) calls)))
+ ((symbol-function 'facep)
+ (lambda (_) nil)))
+ (cj/nerd-icons-apply-tint "any"))
+ (should (null calls))))
+
+(provide 'test-nerd-icons-config--apply-tint)
+;;; test-nerd-icons-config--apply-tint.el ends here
diff --git a/tests/test-nerd-icons-config--color-dir.el b/tests/test-nerd-icons-config--color-dir.el
new file mode 100644
index 00000000..808c0dc3
--- /dev/null
+++ b/tests/test-nerd-icons-config--color-dir.el
@@ -0,0 +1,57 @@
+;;; test-nerd-icons-config--color-dir.el --- Tests for cj/--nerd-icons-color-dir -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the :filter-return advice that attaches a color face to the
+;; output of `nerd-icons-icon-for-dir'. Without this, directory icons render
+;; in the buffer default face — so a face-remap of `default' (buffer-local
+;; or global) catches the icons unintentionally.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'nerd-icons-config)
+
+(ert-deftest test-nerd-icons-config--color-dir-attaches-face ()
+ "Normal: adds nerd-icons-yellow to the face stack of a propertized icon."
+ (let ((icon (propertize "X" 'face '(:family "Symbols Nerd Font Mono" :height 1.0))))
+ (let ((result (cj/--nerd-icons-color-dir icon)))
+ (should (memq 'nerd-icons-yellow
+ (ensure-list (get-text-property 0 'face result)))))))
+
+(ert-deftest test-nerd-icons-config--color-dir-preserves-family ()
+ "Normal: retains the original :family / :height attributes."
+ (let ((icon (propertize "X" 'face '(:family "Symbols Nerd Font Mono" :height 1.0))))
+ (let* ((result (cj/--nerd-icons-color-dir icon))
+ (face (get-text-property 0 'face result))
+ (specs (ensure-list face))
+ (plist (seq-find (lambda (x) (and (listp x) (plist-member x :family))) specs)))
+ (should (equal (plist-get plist :family) "Symbols Nerd Font Mono"))
+ (should (equal (plist-get plist :height) 1.0)))))
+
+(ert-deftest test-nerd-icons-config--color-dir-empty-string ()
+ "Boundary: an empty icon string returns unchanged (no range to propertize)."
+ (let ((icon ""))
+ (should (equal (cj/--nerd-icons-color-dir icon) ""))))
+
+(ert-deftest test-nerd-icons-config--color-dir-non-string-passthrough ()
+ "Error: a nil input returns nil rather than erroring."
+ (should-not (cj/--nerd-icons-color-dir nil)))
+
+(ert-deftest test-nerd-icons-config--color-dir-idempotent ()
+ "Boundary: calling twice on the same icon adds the face only once.
+nerd-icons memoizes its return strings — without this guard, repeated
+renders would stack `nerd-icons-yellow' over and over on the cached string."
+ (let ((icon (propertize "X" 'face '(:family "Symbols Nerd Font Mono" :height 1.0))))
+ (cj/--nerd-icons-color-dir icon)
+ (cj/--nerd-icons-color-dir icon)
+ (cj/--nerd-icons-color-dir icon)
+ (let* ((face (get-text-property 0 'face icon))
+ (specs (ensure-list face))
+ (yellows (cl-count 'nerd-icons-yellow specs)))
+ (should (= yellows 1)))))
+
+(provide 'test-nerd-icons-config--color-dir)
+;;; test-nerd-icons-config--color-dir.el ends here