aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-02 00:00:41 -0400
committerCraig Jennings <c@cjennings.net>2026-07-02 00:00:41 -0400
commit37d92510afbaea8609e8aa3612c6e9d27edba12d (patch)
tree78820bdb59e4f8334528dcc490b63151e619d0ac
parent60560d1eb346c76de355a524e78ae389e8e07807 (diff)
downloaddotemacs-37d92510afbaea8609e8aa3612c6e9d27edba12d.tar.gz
dotemacs-37d92510afbaea8609e8aa3612c6e9d27edba12d.zip
feat(modeline): mode icons, status segments, and a repair command
I rebuilt the custom modeline as pure segment helpers with thin :eval wiring: - The nerd-icons mode icon replaces the mode-name text (cached per buffer, plain name on terminal frames), with the full mode name in the help-echo. - New left-side segments: modified dot / read-only lock (file buffers only), remote @host tag, Narrow tag that widens on click, point-based percentage, region selection info, and a MACRO tag while a keyboard macro records. - New right-side segments: mode-line-process (eat and compilation state was invisible) and flycheck per-severity counts with click-through to the error list, replacing the stock status text. Glyphs are nerd-icons private-use codepoints so emojify can't rewrite them, with text fallbacks when icons are unavailable. - cj/modeline-reset kills a hijacked buffer-local mode-line-format (two-column mode, ediff, calc). - Optional taller bar via cj/modeline-height-factor, a display height property on the padding space so the theme's mode-line faces stay untouched. - Housekeeping: the dead user-constants require and stale commentary are gone, cj/modeline-vc-faces left the risky-local-variable list, and the cache-key defun is cj/--modeline-vc-cache-key so it no longer shadows the same-named defvar. Percent signs from :eval strings go through the mode-line %-construct pass, so the position segment emits %% for a literal percent.
-rw-r--r--modules/modeline-config.el258
-rw-r--r--tests/test-modeline-config-buffer-status.el73
-rw-r--r--tests/test-modeline-config-flycheck-render.el51
-rw-r--r--tests/test-modeline-config-flycheck-segment.el6
-rw-r--r--tests/test-modeline-config-reset.el37
-rw-r--r--tests/test-modeline-config-segments.el122
-rw-r--r--tests/test-modeline-config-vc-cache-key.el10
7 files changed, 508 insertions, 49 deletions
diff --git a/modules/modeline-config.el b/modules/modeline-config.el
index 2793cfae..4cc7fb29 100644
--- a/modules/modeline-config.el
+++ b/modules/modeline-config.el
@@ -8,24 +8,40 @@
;; Load shape: eager.
;; Eager reason: the modeline is visible in the first frame.
;; Top-level side effects: two add-hook (VC cache lifecycle).
-;; Runtime requires: user-constants.
+;; Runtime requires: none (nerd-icons and flycheck are used opportunistically
+;; behind fboundp guards, so the modeline renders with plain-text fallbacks
+;; when either is absent).
;; Direct test load: yes.
;;
-;; Simple, minimal modeline using only built-in Emacs functionality.
-;; No external packages = no buffer issues, no native-comp errors.
-
-;; Features:
-;; - Buffer name
-;; - Major mode
-;; - Version control status
-;; - Line and column position
-;; - Buffer percentage
+;; Simple, minimal modeline built on Emacs 30's own right-alignment.
+;; Segments are pure helpers wired in with thin :eval forms.
+;;
+;; Left side:
+;; - Padding space (optional taller bar via `cj/modeline-height-factor')
+;; - Major-mode icon (nerd-icons; falls back to the mode name in
+;; terminal frames or when nerd-icons is absent)
+;; - Modified dot / read-only lock
+;; - Buffer name (click to cycle buffers)
+;; - Remote @host tag for TRAMP buffers
+;; - Narrow tag when the buffer is narrowed (click to widen)
+;; - Line/column and percentage; selection info while the region is active
+;; - MACRO tag while a keyboard macro is recording
+;;
+;; Right side:
+;; - Recording indicator (video-audio-recording capture)
+;; - Flycheck error/warning counts (click to list errors)
+;; - Process state (eat/comint/compilation via `mode-line-process')
+;; - VC branch colored by state (click for diffs)
+;; - Misc info (chime notifications, weather, etc.)
+;;
+;; Glyphs are nerd-icons private-use codepoints or plain unicode shapes
+;; (U+25CF dot), never emoji codepoints — emojify rewrites those.
+;;
+;; `cj/modeline-reset' repairs a buffer whose mode-line-format was
+;; hijacked buffer-locally (two-column mode, ediff, calc).
;;; Code:
-;; Use buffer status colors from user-constants
-(require 'user-constants)
-
;; -------------------------- Modeline Configuration --------------------------
;; Use Emacs 30's built-in right-alignment
@@ -49,6 +65,13 @@
:type 'boolean
:group 'modeline)
+(defcustom cj/modeline-height-factor 1.15
+ "Height multiplier for the modeline's padding space.
+Values above 1.0 make the modeline slightly taller than the text it
+holds. 1.0 (or nil) renders a plain space with no height change."
+ :type '(choice (const :tag "No extra height" nil) number)
+ :group 'modeline)
+
;; -------------------------- Helper Functions ---------------------------------
(defun cj/modeline-window-narrow-p ()
@@ -81,8 +104,76 @@ runs it too. Shared builder for the clickable modeline segments."
(define-key map [mode-line mouse-3] mouse-3))
map))
+(defun cj/modeline-reset ()
+ "Restore the default modeline in the current buffer.
+Some packages (two-column mode, ediff, calc) replace `mode-line-format'
+buffer-locally with their own layout and can leave it behind. This
+kills the buffer-local value so the default format returns."
+ (interactive)
+ (kill-local-variable 'mode-line-format)
+ (force-mode-line-update)
+ (message "Modeline restored to default in %s" (buffer-name)))
+
;; -------------------------- Modeline Segments --------------------------------
+(defun cj/--modeline-padding ()
+ "Return the leading modeline space, taller per `cj/modeline-height-factor'.
+A display height property on a single space pads the whole modeline
+vertically without touching the mode-line faces the theme owns."
+ (if (and cj/modeline-height-factor
+ (/= cj/modeline-height-factor 1.0))
+ (propertize " " 'display `(height ,cj/modeline-height-factor))
+ " "))
+
+(defvar-local cj/--modeline-mode-icon-cache nil
+ "Cons of (MAJOR-MODE . GRAPHIC-P) paired with the rendered mode segment.
+Avoids a nerd-icons lookup on every redisplay.")
+
+(defun cj/--modeline-mode-icon-compute ()
+ "Build the major-mode segment: a nerd-icons glyph, or the mode name.
+Graphical frames with nerd-icons available get the mode's colored icon;
+terminal frames and icon-less setups get the plain mode name. Either
+way the segment carries the full mode name in its help-echo and clicks
+through to `describe-mode'."
+ (let* ((name (format-mode-line mode-name))
+ (icon (and (display-graphic-p)
+ (fboundp 'nerd-icons-icon-for-mode)
+ (let ((i (ignore-errors (nerd-icons-icon-for-mode major-mode))))
+ (and (stringp i) i))))
+ (help (if-let* ((parent (get major-mode 'derived-mode-parent)))
+ (format "Major mode: %s (%s)\nDerived from: %s\nmouse-1: describe-mode"
+ name major-mode parent)
+ (format "Major mode: %s (%s)\nmouse-1: describe-mode"
+ name major-mode))))
+ (propertize (or icon name)
+ 'mouse-face 'mode-line-highlight
+ 'help-echo help
+ 'local-map (cj/--modeline-click-map 'describe-mode))))
+
+(defun cj/--modeline-mode-icon ()
+ "Return the cached major-mode segment for the current buffer."
+ (let ((key (cons major-mode (display-graphic-p))))
+ (unless (equal (car-safe cj/--modeline-mode-icon-cache) key)
+ (setq cj/--modeline-mode-icon-cache
+ (cons key (cj/--modeline-mode-icon-compute))))
+ (cdr cj/--modeline-mode-icon-cache)))
+
+(defun cj/--modeline-buffer-status ()
+ "Return the modified/read-only indicator, or nil when neither applies.
+Read-only shows a lock and wins over modified. The modified dot shows
+only for file-visiting buffers -- special buffers are perpetually
+modified and would be noise. Clean file buffers show nothing."
+ (cond
+ (buffer-read-only
+ (concat (or (and (fboundp 'nerd-icons-faicon)
+ (ignore-errors (nerd-icons-faicon "nf-fa-lock" :face 'shadow)))
+ (propertize "RO" 'face 'shadow))
+ " "))
+ ((and (buffer-modified-p) buffer-file-name)
+ (concat (propertize "●" 'face 'warning
+ 'help-echo "Buffer has unsaved changes")
+ " "))))
+
(defvar-local cj/modeline-buffer-name
'(:eval (let* ((name (buffer-name))
(truncated-name (cj/modeline-string-cut-middle name)))
@@ -96,10 +187,104 @@ runs it too. Shared builder for the clickable modeline segments."
"Buffer name in the mode line.
Truncates in narrow windows. Click to switch buffers.")
-(defvar-local cj/modeline-position
- '("L:" (:eval (format-mode-line "%l")) " C:" (:eval (format-mode-line "%c")))
- "Line and column position as L:line C:col.
-Uses built-in cached values for performance.")
+(defun cj/--modeline-remote-host ()
+ "Return an @host tag when the buffer's directory is remote, else nil."
+ (when-let* ((host (file-remote-p default-directory 'host)))
+ (concat " "
+ (propertize (concat "@" host)
+ 'face 'warning
+ 'help-echo (format "Remote: %s" default-directory)))))
+
+(defun cj/--modeline-narrow-indicator ()
+ "Return the Narrow tag when the buffer is narrowed, else nil.
+Click to widen."
+ (when (buffer-narrowed-p)
+ (concat " "
+ (propertize "Narrow"
+ 'face 'warning
+ 'mouse-face 'mode-line-highlight
+ 'help-echo "Buffer is narrowed\nmouse-1: widen"
+ 'local-map (cj/--modeline-click-map 'widen)))))
+
+(defun cj/--modeline-position-info ()
+ "Return position info: L:line C:col and percentage through the buffer.
+While the region is active, return selection info instead (lines and
+characters selected)."
+ (if (use-region-p)
+ (let* ((lines (count-lines (region-beginning) (region-end)))
+ (chars (- (region-end) (region-beginning))))
+ (format "%d line%s, %d char%s"
+ lines (if (= lines 1) "" "s")
+ chars (if (= chars 1) "" "s")))
+ ;; Percent is computed from point rather than %p: it answers "how far
+ ;; is point" instead of "where is the window", and it stays correct in
+ ;; batch/undisplayed buffers. %% survives the mode-line's %-construct
+ ;; pass over :eval results as a literal percent sign.
+ (format "L:%s C:%s %d%%%%"
+ (format-mode-line "%l")
+ (format-mode-line "%c")
+ (floor (* 100.0 (- (point) (point-min)))
+ (max 1 (- (point-max) (point-min)))))))
+
+(defun cj/--modeline-macro-indicator ()
+ "Return the MACRO tag while a keyboard macro is recording, else nil."
+ (when defining-kbd-macro
+ (concat " "
+ (propertize "MACRO"
+ 'face 'error
+ 'help-echo "Recording keyboard macro\nF4 or C-x ) to stop"))))
+
+;; ------------------------------ Flycheck Segment ------------------------------
+
+(defvar cj/--modeline-flycheck-glyphs nil
+ "Cached (ERROR-GLYPH . WARNING-GLYPH) for the flycheck segment.
+nerd-icons private-use glyphs (emojify never rewrites those). Only a
+successful lookup is cached, so a load before nerd-icons doesn't poison
+the cache with nils.")
+
+(defun cj/--modeline-flycheck-glyphs ()
+ "Return the flycheck glyph pair, or nil when icons aren't usable.
+Text fallbacks apply on non-graphic frames (PUA glyphs don't render in
+a terminal) and whenever nerd-icons is absent."
+ (when (display-graphic-p)
+ (or cj/--modeline-flycheck-glyphs
+ (when (fboundp 'nerd-icons-faicon)
+ (let ((pair (cons (ignore-errors (nerd-icons-faicon "nf-fa-times_circle" :face 'error))
+ (ignore-errors (nerd-icons-faicon "nf-fa-warning" :face 'warning)))))
+ (when (and (car pair) (cdr pair))
+ (setq cj/--modeline-flycheck-glyphs pair)))))))
+
+(defun cj/--modeline-flycheck-render (counts)
+ "Render flycheck COUNTS alist ((error . N) (warning . M) ...) or nil.
+Errors carry the error face, warnings the warning face; zero-count
+severities are omitted; all-clean renders nothing. The segment clicks
+through to `flycheck-list-errors'."
+ (let* ((errors (or (alist-get 'error counts) 0))
+ (warnings (or (alist-get 'warning counts) 0))
+ (glyphs (cj/--modeline-flycheck-glyphs))
+ (parts nil))
+ (when (> warnings 0)
+ (push (concat (or (cdr glyphs) (propertize "W" 'face 'warning)) " "
+ (propertize (number-to-string warnings) 'face 'warning))
+ parts))
+ (when (> errors 0)
+ (push (concat (or (car glyphs) (propertize "E" 'face 'error)) " "
+ (propertize (number-to-string errors) 'face 'error))
+ parts))
+ (when parts
+ (propertize (mapconcat #'identity parts " ")
+ 'mouse-face 'mode-line-highlight
+ 'help-echo "Flycheck\nmouse-1: list errors"
+ 'local-map (cj/--modeline-click-map 'flycheck-list-errors)))))
+
+(defun cj/--modeline-flycheck-status ()
+ "Return the rendered flycheck counts for the current buffer, or nil."
+ (when (and (fboundp 'flycheck-count-errors)
+ (boundp 'flycheck-current-errors))
+ (cj/--modeline-flycheck-render
+ (flycheck-count-errors flycheck-current-errors))))
+
+;; -------------------------------- VC Segment ----------------------------------
(defvar cj/modeline-vc-faces
'((added . vc-locally-added-state)
@@ -134,7 +319,7 @@ Uses built-in cached values for performance.")
cj/modeline-vc-cache-value nil
cj/modeline-vc-cache-set-p nil))
-(defun cj/modeline-vc-cache-key (file)
+(defun cj/--modeline-vc-cache-key (file)
"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.
@@ -180,7 +365,7 @@ break it. Caching nil degrades to \"no VC info\" instead."
(when-let* ((file (cj/modeline-vc-file)))
(unless (and (file-remote-p file) (not cj/modeline-vc-show-remote))
(let* ((now (float-time))
- (key (cj/modeline-vc-cache-key file)))
+ (key (cj/--modeline-vc-cache-key file)))
(if (cj/modeline-vc-cache-valid-p key now)
cj/modeline-vc-cache-value
(setq cj/modeline-vc-cache-key key
@@ -211,18 +396,6 @@ break it. Caching nil degrades to \"no VC info\" instead."
Shows only in active window. Truncates in narrow windows.
Click to show diffs with `vc-diff' or `vc-root-diff'.")
-(defvar-local cj/modeline-major-mode
- '(:eval (let ((mode-str (format-mode-line mode-name)) ; Convert to string
- (mode-sym major-mode))
- (propertize mode-str
- 'mouse-face 'mode-line-highlight
- 'help-echo (if-let* ((parent (get mode-sym 'derived-mode-parent)))
- (format "Major mode: %s\nDerived from: %s\nmouse-1: describe-mode" mode-sym parent)
- (format "Major mode: %s\nmouse-1: describe-mode" mode-sym))
- 'local-map (cj/--modeline-click-map 'describe-mode))))
- "Major mode name only (no minor modes).
-Click to show help with `describe-mode'.")
-
(defvar-local cj/modeline-misc-info
'(:eval (when (mode-line-window-selected-p)
mode-line-misc-info))
@@ -237,24 +410,30 @@ Shows only in active window.")
(setq-default mode-line-format
'("%e" ; Error message if out of memory
;; LEFT SIDE
- " "
- cj/modeline-major-mode
+ (:eval (cj/--modeline-padding))
+ (:eval (cj/--modeline-mode-icon))
" "
+ (:eval (cj/--modeline-buffer-status))
cj/modeline-buffer-name
+ (:eval (cj/--modeline-remote-host))
+ (:eval (cj/--modeline-narrow-indicator))
" "
- cj/modeline-position
+ (:eval (cj/--modeline-position-info))
+ (:eval (cj/--modeline-macro-indicator))
;; RIGHT SIDE (using Emacs 30 built-in right-align)
;; Order: leftmost to rightmost as they appear in the list
mode-line-format-right-align
(:eval (when (fboundp 'cj/recording-modeline-indicator)
(cj/recording-modeline-indicator)))
- ;; Flycheck status: prefix + counts (or success indicator). Gated
- ;; to the active window, and to buffers where flycheck has loaded
- ;; and turned on, so the call is safe even before flycheck loads.
+ ;; Flycheck status: error/warning counts. Gated to the active
+ ;; window, and to buffers where flycheck has loaded and turned on,
+ ;; so the call is safe even before flycheck loads.
(:eval (when (and (mode-line-window-selected-p)
(bound-and-true-p flycheck-mode))
- (flycheck-mode-line-status-text)))
+ (cj/--modeline-flycheck-status)))
" "
+ mode-line-process
+ " "
cj/modeline-vc-branch
" "
cj/modeline-misc-info
@@ -262,10 +441,7 @@ Shows only in active window.")
;; Mark all segments as risky-local-variable (required for :eval forms)
(dolist (construct '(cj/modeline-buffer-name
- cj/modeline-position
cj/modeline-vc-branch
- cj/modeline-vc-faces
- cj/modeline-major-mode
cj/modeline-misc-info))
(put construct 'risky-local-variable t))
diff --git a/tests/test-modeline-config-buffer-status.el b/tests/test-modeline-config-buffer-status.el
new file mode 100644
index 00000000..123f1062
--- /dev/null
+++ b/tests/test-modeline-config-buffer-status.el
@@ -0,0 +1,73 @@
+;;; test-modeline-config-buffer-status.el --- buffer-status segment -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for `cj/--modeline-buffer-status': the modified / read-only
+;; indicator. Read-only wins over modified; the modified dot shows only
+;; for file-visiting buffers (special buffers are perpetually modified
+;; and would be noise); clean file buffers show nothing.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+(require 'modeline-config)
+
+(ert-deftest test-modeline-config-buffer-status-modified-file-shows-dot ()
+ "Normal: a modified file-visiting buffer returns the warning-faced dot."
+ (let ((file (make-temp-file "modeline-status-" nil ".txt")))
+ (unwind-protect
+ (with-current-buffer (find-file-noselect file)
+ (insert "x")
+ (let ((status (cj/--modeline-buffer-status)))
+ (should (stringp status))
+ (should (string-match-p "●" status))
+ (should (eq (get-text-property
+ (string-match "●" status) 'face status)
+ 'warning)))
+ (set-buffer-modified-p nil)
+ (kill-buffer))
+ (delete-file file))))
+
+(ert-deftest test-modeline-config-buffer-status-clean-file-nil ()
+ "Normal: an unmodified file-visiting buffer returns nil."
+ (let ((file (make-temp-file "modeline-status-" nil ".txt")))
+ (unwind-protect
+ (with-current-buffer (find-file-noselect file)
+ (should-not (cj/--modeline-buffer-status))
+ (kill-buffer))
+ (delete-file file))))
+
+(ert-deftest test-modeline-config-buffer-status-read-only-shows-lock ()
+ "Normal: a read-only buffer returns the read-only indicator."
+ (with-temp-buffer
+ (setq buffer-read-only t)
+ (let ((status (cj/--modeline-buffer-status)))
+ (should (stringp status))
+ (should (> (length status) 0)))))
+
+(ert-deftest test-modeline-config-buffer-status-read-only-wins-over-modified ()
+ "Boundary: read-only + modified shows the read-only indicator, not the dot."
+ (let ((file (make-temp-file "modeline-status-" nil ".txt")))
+ (unwind-protect
+ (with-current-buffer (find-file-noselect file)
+ (insert "x")
+ (setq buffer-read-only t)
+ (let ((status (cj/--modeline-buffer-status)))
+ (should (stringp status))
+ (should-not (string-match-p "●" status)))
+ (setq buffer-read-only nil)
+ (set-buffer-modified-p nil)
+ (kill-buffer))
+ (delete-file file))))
+
+(ert-deftest test-modeline-config-buffer-status-modified-non-file-nil ()
+ "Boundary: a modified non-file buffer (scratch-like) returns nil."
+ (with-temp-buffer
+ (insert "x")
+ (should (buffer-modified-p))
+ (should-not (cj/--modeline-buffer-status))))
+
+(provide 'test-modeline-config-buffer-status)
+;;; test-modeline-config-buffer-status.el ends here
diff --git a/tests/test-modeline-config-flycheck-render.el b/tests/test-modeline-config-flycheck-render.el
new file mode 100644
index 00000000..46d0cdef
--- /dev/null
+++ b/tests/test-modeline-config-flycheck-render.el
@@ -0,0 +1,51 @@
+;;; test-modeline-config-flycheck-render.el --- flycheck counts rendering -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for `cj/--modeline-flycheck-render', the pure formatter that
+;; turns a flycheck counts alist ((error . N) (warning . M) ...) into a
+;; propertized modeline string. Errors carry the error face, warnings
+;; the warning face; zero-count severities are omitted; an all-clean
+;; alist (or nil) renders nothing. Glyphs come from nerd-icons when
+;; available (private-use codepoints, safe from emojify) with plain-text
+;; fallbacks in batch mode.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+(require 'modeline-config)
+
+(ert-deftest test-modeline-config-flycheck-render-errors-and-warnings ()
+ "Normal: both counts render, error face on errors, warning on warnings."
+ (let ((s (cj/--modeline-flycheck-render '((error . 2) (warning . 5)))))
+ (should (stringp s))
+ (should (string-match-p "2" s))
+ (should (string-match-p "5" s))
+ (let ((epos (string-match "2" s))
+ (wpos (string-match "5" s)))
+ (should (eq (get-text-property epos 'face s) 'error))
+ (should (eq (get-text-property wpos 'face s) 'warning)))))
+
+(ert-deftest test-modeline-config-flycheck-render-errors-only ()
+ "Normal: warnings absent when their count is zero or missing."
+ (let ((s (cj/--modeline-flycheck-render '((error . 3)))))
+ (should (stringp s))
+ (should (string-match-p "3" s))
+ (should-not (text-property-any 0 (length s) 'face 'warning s))))
+
+(ert-deftest test-modeline-config-flycheck-render-clean-nil ()
+ "Boundary: zero counts render nothing."
+ (should-not (cj/--modeline-flycheck-render '((error . 0) (warning . 0)))))
+
+(ert-deftest test-modeline-config-flycheck-render-nil-input ()
+ "Boundary: nil counts alist renders nothing."
+ (should-not (cj/--modeline-flycheck-render nil)))
+
+(ert-deftest test-modeline-config-flycheck-render-info-ignored ()
+ "Boundary: info-level counts alone render nothing (errors/warnings only)."
+ (should-not (cj/--modeline-flycheck-render '((info . 4)))))
+
+(provide 'test-modeline-config-flycheck-render)
+;;; test-modeline-config-flycheck-render.el ends here
diff --git a/tests/test-modeline-config-flycheck-segment.el b/tests/test-modeline-config-flycheck-segment.el
index 2ae2f5de..ed4d0601 100644
--- a/tests/test-modeline-config-flycheck-segment.el
+++ b/tests/test-modeline-config-flycheck-segment.el
@@ -2,7 +2,7 @@
;;; Commentary:
;; Smoke test that the custom modeline's `mode-line-format' includes
-;; a guarded reference to `flycheck-mode-line-status-text', and that
+;; a guarded reference to `cj/--modeline-flycheck-status', and that
;; the guard requires both `mode-line-window-selected-p' and
;; `bound-and-true-p flycheck-mode'. See
;; docs/specs/flycheck-modeline-customization-spec-implemented.org for the design.
@@ -16,9 +16,9 @@
(require 'modeline-config)
(ert-deftest test-modeline-config-flycheck-segment-present ()
- "`mode-line-format' contains an :eval form invoking flycheck-mode-line-status-text."
+ "`mode-line-format' contains an :eval form invoking cj/--modeline-flycheck-status."
(let ((printed (format "%S" (default-value 'mode-line-format))))
- (should (string-match-p "flycheck-mode-line-status-text" printed))))
+ (should (string-match-p "cj/--modeline-flycheck-status" printed))))
(ert-deftest test-modeline-config-flycheck-segment-guarded-by-active-window ()
"Flycheck segment gates on `mode-line-window-selected-p'."
diff --git a/tests/test-modeline-config-reset.el b/tests/test-modeline-config-reset.el
new file mode 100644
index 00000000..54b7bb2f
--- /dev/null
+++ b/tests/test-modeline-config-reset.el
@@ -0,0 +1,37 @@
+;;; test-modeline-config-reset.el --- cj/modeline-reset -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for `cj/modeline-reset', the repair command for buffers whose
+;; `mode-line-format' was hijacked buffer-locally (two-column mode, ediff,
+;; calc). It kills the buffer-local value so the default format returns.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+(require 'modeline-config)
+
+(ert-deftest test-modeline-config-reset-kills-local-format ()
+ "Normal: a hijacked buffer-local mode-line-format is removed."
+ (with-temp-buffer
+ (setq-local mode-line-format '("hijacked"))
+ (should (local-variable-p 'mode-line-format))
+ (cj/modeline-reset)
+ (should-not (local-variable-p 'mode-line-format))
+ (should (eq mode-line-format (default-value 'mode-line-format)))))
+
+(ert-deftest test-modeline-config-reset-noop-without-local ()
+ "Boundary: harmless when the buffer has no local mode-line-format."
+ (with-temp-buffer
+ (should-not (local-variable-p 'mode-line-format))
+ (cj/modeline-reset)
+ (should-not (local-variable-p 'mode-line-format))))
+
+(ert-deftest test-modeline-config-reset-is-a-command ()
+ "Normal: cj/modeline-reset is interactive."
+ (should (commandp #'cj/modeline-reset)))
+
+(provide 'test-modeline-config-reset)
+;;; test-modeline-config-reset.el ends here
diff --git a/tests/test-modeline-config-segments.el b/tests/test-modeline-config-segments.el
new file mode 100644
index 00000000..580a7711
--- /dev/null
+++ b/tests/test-modeline-config-segments.el
@@ -0,0 +1,122 @@
+;;; test-modeline-config-segments.el --- small modeline segments -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the small pure segment helpers added in the 2026-07-01
+;; modeline overhaul: macro indicator, remote-host tag, narrowing
+;; indicator, position/region info, and the padding space.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+(require 'modeline-config)
+
+;; ---------------------------- Macro Indicator --------------------------------
+
+(ert-deftest test-modeline-config-macro-indicator-shows-when-defining ()
+ "Normal: MACRO text with the error face while a macro is recording."
+ (let ((defining-kbd-macro t))
+ (let ((s (cj/--modeline-macro-indicator)))
+ (should (stringp s))
+ (should (string-match-p "MACRO" s))
+ (should (eq (get-text-property (string-match "MACRO" s) 'face s)
+ 'error)))))
+
+(ert-deftest test-modeline-config-macro-indicator-nil-when-idle ()
+ "Boundary: nil when no macro is being defined."
+ (let ((defining-kbd-macro nil))
+ (should-not (cj/--modeline-macro-indicator))))
+
+;; ---------------------------- Remote Host Tag --------------------------------
+
+(ert-deftest test-modeline-config-remote-host-shows-host ()
+ "Normal: remote default-directory yields an @host tag."
+ (with-temp-buffer
+ (setq default-directory "/ssh:velox:/home/cjennings/")
+ (let ((s (cj/--modeline-remote-host)))
+ (should (stringp s))
+ (should (string-match-p "@velox" s)))))
+
+(ert-deftest test-modeline-config-remote-host-nil-when-local ()
+ "Boundary: nil for a local directory."
+ (with-temp-buffer
+ (setq default-directory "/tmp/")
+ (should-not (cj/--modeline-remote-host))))
+
+;; --------------------------- Narrowing Indicator -----------------------------
+
+(ert-deftest test-modeline-config-narrow-indicator-shows-when-narrowed ()
+ "Normal: narrowed buffer yields the Narrow tag."
+ (with-temp-buffer
+ (insert "line one\nline two\nline three\n")
+ (narrow-to-region 1 9)
+ (let ((s (cj/--modeline-narrow-indicator)))
+ (should (stringp s))
+ (should (string-match-p "Narrow" s)))))
+
+(ert-deftest test-modeline-config-narrow-indicator-nil-when-widened ()
+ "Boundary: nil when the buffer is not narrowed."
+ (with-temp-buffer
+ (insert "text")
+ (should-not (cj/--modeline-narrow-indicator))))
+
+;; --------------------------- Position / Region Info ---------------------------
+
+(ert-deftest test-modeline-config-position-info-line-column-percent ()
+ "Normal: no region yields L: C: plus a percentage-through-buffer."
+ (with-temp-buffer
+ (insert (make-string 200 ?x))
+ (goto-char (point-min))
+ (deactivate-mark)
+ (let ((s (cj/--modeline-position-info)))
+ (should (stringp s))
+ (should (string-match-p "L:" s))
+ (should (string-match-p "C:" s))
+ ;; point-based percent, %%-escaped for the mode-line construct pass
+ (should (string-match-p "%" s)))))
+
+(ert-deftest test-modeline-config-position-info-region-lines-chars ()
+ "Normal: an active region yields selection info instead of position."
+ (with-temp-buffer
+ (insert "one\ntwo\nthree\n")
+ (goto-char (point-min))
+ (push-mark (point) t t)
+ (goto-char 9) ; through "one\ntwo\n"
+ (let ((transient-mark-mode t))
+ (let ((s (cj/--modeline-position-info)))
+ (should (stringp s))
+ (should (string-match-p "2 lines" s))
+ (should (string-match-p "8 chars" s))))))
+
+(ert-deftest test-modeline-config-position-info-single-char-region ()
+ "Boundary: a one-char region reports 1 line, 1 char."
+ (with-temp-buffer
+ (insert "abc")
+ (goto-char 1)
+ (push-mark (point) t t)
+ (goto-char 2)
+ (let ((transient-mark-mode t))
+ (let ((s (cj/--modeline-position-info)))
+ (should (string-match-p "1 line" s))
+ (should (string-match-p "1 char" s))))))
+
+;; ------------------------------- Padding --------------------------------------
+
+(ert-deftest test-modeline-config-padding-carries-height-display ()
+ "Normal: padding space carries a display height property."
+ (let ((cj/modeline-height-factor 1.2))
+ (let ((s (cj/--modeline-padding)))
+ (should (stringp s))
+ (should (get-text-property 0 'display s)))))
+
+(ert-deftest test-modeline-config-padding-plain-at-factor-one ()
+ "Boundary: factor 1.0 (or nil) yields a plain space, no display prop."
+ (let ((cj/modeline-height-factor 1.0))
+ (let ((s (cj/--modeline-padding)))
+ (should (stringp s))
+ (should-not (get-text-property 0 'display s)))))
+
+(provide 'test-modeline-config-segments)
+;;; test-modeline-config-segments.el ends here
diff --git a/tests/test-modeline-config-vc-cache-key.el b/tests/test-modeline-config-vc-cache-key.el
index 6ba7985c..38052949 100644
--- a/tests/test-modeline-config-vc-cache-key.el
+++ b/tests/test-modeline-config-vc-cache-key.el
@@ -17,20 +17,20 @@
(ert-deftest test-modeline-vc-cache-key-is-file-and-show-remote ()
"Normal: the key is (FILE SHOW-REMOTE), with no per-render file-truename stat."
(let ((cj/modeline-vc-show-remote nil))
- (should (equal (cj/modeline-vc-cache-key "/x/y.el") '("/x/y.el" nil)))))
+ (should (equal (cj/--modeline-vc-cache-key "/x/y.el") '("/x/y.el" nil)))))
(ert-deftest test-modeline-vc-cache-key-tracks-show-remote ()
"Boundary: toggling show-remote yields a different key (separate cache entry)."
(should-not (equal (let ((cj/modeline-vc-show-remote nil))
- (cj/modeline-vc-cache-key "/x/y.el"))
+ (cj/--modeline-vc-cache-key "/x/y.el"))
(let ((cj/modeline-vc-show-remote t))
- (cj/modeline-vc-cache-key "/x/y.el")))))
+ (cj/--modeline-vc-cache-key "/x/y.el")))))
(ert-deftest test-modeline-vc-cache-key-stable-for-same-file ()
"Boundary: the key is stable across calls for an unchanged file + show-remote."
(let ((cj/modeline-vc-show-remote nil))
- (should (equal (cj/modeline-vc-cache-key "/x/y.el")
- (cj/modeline-vc-cache-key "/x/y.el")))))
+ (should (equal (cj/--modeline-vc-cache-key "/x/y.el")
+ (cj/--modeline-vc-cache-key "/x/y.el")))))
(provide 'test-modeline-config-vc-cache-key)
;;; test-modeline-config-vc-cache-key.el ends here