From 37d92510afbaea8609e8aa3612c6e9d27edba12d Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 2 Jul 2026 00:00:41 -0400 Subject: 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. --- modules/modeline-config.el | 258 +++++++++++++++++++++---- tests/test-modeline-config-buffer-status.el | 73 +++++++ tests/test-modeline-config-flycheck-render.el | 51 +++++ tests/test-modeline-config-flycheck-segment.el | 6 +- tests/test-modeline-config-reset.el | 37 ++++ tests/test-modeline-config-segments.el | 122 ++++++++++++ tests/test-modeline-config-vc-cache-key.el | 10 +- 7 files changed, 508 insertions(+), 49 deletions(-) create mode 100644 tests/test-modeline-config-buffer-status.el create mode 100644 tests/test-modeline-config-flycheck-render.el create mode 100644 tests/test-modeline-config-reset.el create mode 100644 tests/test-modeline-config-segments.el 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 -- cgit v1.2.3