From 2d1571114a5f54d416dddcdf098047b361bf7825 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 2 Jul 2026 00:32:07 -0400 Subject: feat(modeline): eat state icons and info-left, systray-right layout The modeline now follows one layout rule: the left side is Emacs information (mode icon, eat state, modified/read-only, buffer name, @host, Narrow, VC branch, position, MACRO, process) and the right side is a systray for package indicators (recording, flycheck counts, misc-info). The VC branch and mode-line-process moved left to fit the rule. eat buffers trade the [semi-char]:run text for two icons beside the mode icon: a keyboard glyph for the input mode (quiet when semi-char, warning otherwise, hover text explains where keys go, mouse-1/2/3 switch modes mirroring eat's own bindings) and a green play / red power-off for the process state. eat-config clears eat's buffer-local mode-line-process so nothing renders twice. --- modules/eat-config.el | 10 +++- modules/modeline-config.el | 98 +++++++++++++++++++++++++++++---- tests/test-modeline-config-eat-state.el | 94 +++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 tests/test-modeline-config-eat-state.el diff --git a/modules/eat-config.el b/modules/eat-config.el index d66507c4..e059fe36 100644 --- a/modules/eat-config.el +++ b/modules/eat-config.el @@ -112,11 +112,19 @@ not recognize (which would later trip (cl-assert charset) on write)." ;; ------------------------------- eat package --------------------------------- +(defun cj/--eat-clear-mode-line-process () + "Drop eat's buffer-local [mode]:status `mode-line-process'. +The modeline's own eat segment (cj/--modeline-eat-state in +modeline-config.el) renders the input mode and process state as icons +on the left, so eat's text form would be a duplicate." + (setq mode-line-process nil)) + (use-package eat :ensure t :commands (eat) :hook ((eat-mode . cj/turn-off-chrome-for-term) - (eat-mode . cj/--eat-tame-scroll)) + (eat-mode . cj/--eat-tame-scroll) + (eat-mode . cj/--eat-clear-mode-line-process)) :custom ;; Close the EAT buffer when its shell exits. (eat-kill-buffer-on-exit t) diff --git a/modules/modeline-config.el b/modules/modeline-config.el index 4cc7fb29..be7f72e5 100644 --- a/modules/modeline-config.el +++ b/modules/modeline-config.el @@ -16,22 +16,30 @@ ;; Simple, minimal modeline built on Emacs 30's own right-alignment. ;; Segments are pure helpers wired in with thin :eval forms. ;; -;; Left side: +;; Layout principle: the LEFT side is Emacs information -- everything +;; about this buffer and this Emacs (identity, state, position, VC, +;; process). The RIGHT side is a systray for package indicators +;; (recording, flycheck counts, the misc-info icons). +;; +;; Left side, in order: ;; - 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) +;; - eat terminal state: input-mode + process icons with hover text +;; (replaces eat's own [semi-char]:run mode-line-process) ;; - 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) +;; - VC branch colored by state (click for diffs) ;; - Line/column and percentage; selection info while the region is active ;; - MACRO tag while a keyboard macro is recording +;; - Process state for non-eat buffers (comint/compilation via +;; `mode-line-process'; eat buffers get theirs cleared in eat-config) ;; -;; Right side: +;; Right side (the systray): ;; - 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 @@ -234,6 +242,75 @@ characters selected)." 'face 'error 'help-echo "Recording keyboard macro\nF4 or C-x ) to stop")))) +;; ----------------------------- Eat State Segment ------------------------------ + +(defconst cj/--modeline-eat-mode-help + '((semi-char . "most keys go to the terminal; Emacs keeps C-x, M-x, and the F-keys") + (char . "raw: nearly every key goes to the terminal") + (line . "line editing in Emacs; RET sends the line") + (emacs . "all keys are Emacs; terminal output is read-only")) + "Hover-text description per eat input mode.") + +(defun cj/--modeline-eat-input-mode () + "Return the current eat input mode as a symbol." + (cond ((bound-and-true-p eat--semi-char-mode) 'semi-char) + ((bound-and-true-p eat--char-mode) 'char) + ((bound-and-true-p eat--line-mode) 'line) + (t 'emacs))) + +(defun cj/--modeline-eat-switch-map (mode) + "Return a mode-line keymap switching away from eat input MODE. +Mirrors eat's own mouse-1/2/3 assignments for each mode." + (let ((map (make-sparse-keymap)) + (targets (pcase mode + ('semi-char '(eat-char-mode eat-line-mode eat-emacs-mode)) + ('char '(eat-semi-char-mode eat-line-mode eat-emacs-mode)) + ('line '(eat-semi-char-mode eat-emacs-mode eat-char-mode)) + (_ '(eat-semi-char-mode eat-line-mode eat-char-mode))))) + (define-key map [mode-line down-mouse-1] (nth 0 targets)) + (define-key map [mode-line down-mouse-2] (nth 1 targets)) + (define-key map [mode-line down-mouse-3] (nth 2 targets)) + map)) + +(defun cj/--modeline-eat-state () + "Input-mode + process icons for eat terminal buffers, or nil elsewhere. +Replaces eat's own [semi-char]:run `mode-line-process' text (cleared in +eat-config): a keyboard glyph colored quiet for semi-char and warning +for the other modes, then a running/exited indicator. Hover text +explains each; the keyboard glyph clicks through eat's mode switches." + (when (derived-mode-p 'eat-mode) + (let* ((mode (cj/--modeline-eat-input-mode)) + (mode-face (if (eq mode 'semi-char) 'shadow 'warning)) + (icons-p (and (display-graphic-p) (fboundp 'nerd-icons-faicon))) + (mode-glyph (or (and icons-p + (ignore-errors + (nerd-icons-faicon "nf-fa-keyboard_o" :face mode-face))) + (propertize (upcase (substring (symbol-name mode) 0 1)) + 'face mode-face))) + (proc (get-buffer-process (current-buffer))) + (live (and proc (process-live-p proc))) + (proc-glyph (if live + (or (and icons-p + (ignore-errors + (nerd-icons-faicon "nf-fa-play" :face 'success))) + (propertize "run" 'face 'success)) + (or (and icons-p + (ignore-errors + (nerd-icons-faicon "nf-fa-power_off" :face 'error))) + (propertize "exit" 'face 'error))))) + (concat " " + (propertize mode-glyph + 'mouse-face 'mode-line-highlight + 'help-echo (format "Input mode: %s -- %s\nmouse-1/2/3: switch input modes" + mode + (alist-get mode cj/--modeline-eat-mode-help)) + 'local-map (cj/--modeline-eat-switch-map mode)) + " " + (propertize proc-glyph + 'help-echo (if live + "Terminal process: running" + "Terminal process exited")))))) + ;; ------------------------------ Flycheck Segment ------------------------------ (defvar cj/--modeline-flycheck-glyphs nil @@ -409,18 +486,23 @@ Shows only in active window.") (setq-default mode-line-format '("%e" ; Error message if out of memory - ;; LEFT SIDE + ;; LEFT SIDE -- Emacs information: identity, state, position, VC, process (:eval (cj/--modeline-padding)) (:eval (cj/--modeline-mode-icon)) + (:eval (cj/--modeline-eat-state)) " " (:eval (cj/--modeline-buffer-status)) cj/modeline-buffer-name (:eval (cj/--modeline-remote-host)) (:eval (cj/--modeline-narrow-indicator)) " " + cj/modeline-vc-branch + " " (:eval (cj/--modeline-position-info)) (:eval (cj/--modeline-macro-indicator)) - ;; RIGHT SIDE (using Emacs 30 built-in right-align) + " " + mode-line-process + ;; RIGHT SIDE -- the package systray (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) @@ -432,10 +514,6 @@ Shows only in active window.") (bound-and-true-p flycheck-mode)) (cj/--modeline-flycheck-status))) " " - mode-line-process - " " - cj/modeline-vc-branch - " " cj/modeline-misc-info " ")) diff --git a/tests/test-modeline-config-eat-state.el b/tests/test-modeline-config-eat-state.el new file mode 100644 index 00000000..7d20c681 --- /dev/null +++ b/tests/test-modeline-config-eat-state.el @@ -0,0 +1,94 @@ +;;; test-modeline-config-eat-state.el --- eat input-mode/process segment -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for `cj/--modeline-eat-state', the left-side segment that shows +;; an eat terminal's input mode and process state as icons with +;; explanatory hover text (replacing eat's own [semi-char]:run +;; mode-line-process). eat isn't installed under `make test', so the +;; tests fake an eat buffer: `major-mode' set to `eat-mode' and the +;; input-mode flags defvar'd here. Batch has no graphic display, so the +;; glyphs exercise their letter/text fallbacks. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +(require 'modeline-config) + +(defvar eat--semi-char-mode nil) +(defvar eat--char-mode nil) +(defvar eat--line-mode nil) + +(defmacro test-eat-state--with-fake-eat-buffer (&rest body) + "Run BODY in a temp buffer masquerading as an eat-mode buffer." + `(with-temp-buffer + (setq major-mode 'eat-mode) + ,@body)) + +(ert-deftest test-modeline-eat-state-nil-outside-eat () + "Boundary: non-eat buffers get no segment." + (with-temp-buffer + (should-not (cj/--modeline-eat-state)))) + +(ert-deftest test-modeline-eat-state-semi-char-quiet-face () + "Normal: semi-char (the default mode) renders in the quiet shadow face." + (test-eat-state--with-fake-eat-buffer + (setq-local eat--semi-char-mode t) + (let ((s (cj/--modeline-eat-state))) + (should (stringp s)) + (should (text-property-any 0 (length s) 'face 'shadow s))))) + +(ert-deftest test-modeline-eat-state-char-mode-warning-face () + "Normal: char mode (raw keys) renders in the warning face." + (test-eat-state--with-fake-eat-buffer + (setq-local eat--char-mode t) + (let ((s (cj/--modeline-eat-state))) + (should (stringp s)) + (should (text-property-any 0 (length s) 'face 'warning s))))) + +(ert-deftest test-modeline-eat-state-input-mode-detection () + "Normal: the four input-mode flags map to the right symbols." + (test-eat-state--with-fake-eat-buffer + (setq-local eat--semi-char-mode t) + (should (eq (cj/--modeline-eat-input-mode) 'semi-char))) + (test-eat-state--with-fake-eat-buffer + (setq-local eat--char-mode t) + (should (eq (cj/--modeline-eat-input-mode) 'char))) + (test-eat-state--with-fake-eat-buffer + (setq-local eat--line-mode t) + (should (eq (cj/--modeline-eat-input-mode) 'line))) + (test-eat-state--with-fake-eat-buffer + (should (eq (cj/--modeline-eat-input-mode) 'emacs)))) + +(ert-deftest test-modeline-eat-state-dead-process-error-face () + "Error: no live process renders the exited indicator in the error face." + (test-eat-state--with-fake-eat-buffer + (setq-local eat--semi-char-mode t) + (let ((s (cj/--modeline-eat-state))) + (should (text-property-any 0 (length s) 'face 'error s))))) + +(ert-deftest test-modeline-eat-state-live-process-success-face () + "Normal: a live buffer process renders the running indicator, no error face." + (test-eat-state--with-fake-eat-buffer + (setq-local eat--semi-char-mode t) + (let ((proc (start-process "test-eat-state" (current-buffer) "sleep" "10"))) + (unwind-protect + (let ((s (cj/--modeline-eat-state))) + (should (text-property-any 0 (length s) 'face 'success s)) + (should-not (text-property-any 0 (length s) 'face 'error s))) + (set-process-query-on-exit-flag proc nil) + (kill-process proc))))) + +(ert-deftest test-modeline-eat-state-hover-text-explains () + "Normal: the input-mode glyph carries explanatory help-echo." + (test-eat-state--with-fake-eat-buffer + (setq-local eat--semi-char-mode t) + (let* ((s (cj/--modeline-eat-state)) + (pos (text-property-not-all 0 (length s) 'help-echo nil s))) + (should pos) + (should (string-match-p "semi-char" (get-text-property pos 'help-echo s)))))) + +(provide 'test-modeline-config-eat-state) +;;; test-modeline-config-eat-state.el ends here -- cgit v1.2.3