aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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