aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--init.el3
-rw-r--r--modules/eat-config.el230
-rw-r--r--modules/eshell-config.el100
-rw-r--r--modules/term-config.el199
-rw-r--r--scripts/theme-studio/WIP.json189
-rw-r--r--scripts/theme-studio/previews.js58
-rw-r--r--scripts/theme-studio/theme-studio.html54
-rw-r--r--tests/test-eshell-config--prompt.el75
-rw-r--r--tests/test-term-toggle--buffer-filter.el44
-rw-r--r--tests/test-term-toggle--dispatch.el2
-rw-r--r--tests/test-term-toggle--display.el2
-rw-r--r--tests/testutil-ghostel-buffers.el10
-rw-r--r--themes/WIP-theme.el26
-rw-r--r--todo.org8
14 files changed, 713 insertions, 287 deletions
diff --git a/init.el b/init.el
index 2fa34ab4c..f50c1fb8f 100644
--- a/init.el
+++ b/init.el
@@ -78,7 +78,8 @@
(require 'telega-config) ;; telegram client via telega.el (TDLib in docker)
(require 'signal-config) ;; signal client via forked signel + signal-cli
(require 'eshell-config) ;; emacs shell configuration
-(require 'term-config) ;; ghostel + F12 toggle + tmux history copy
+(require 'eat-config) ;; EAT terminal + the F12 dock-and-remember toggle
+(require 'term-config) ;; ghostel (ai-term backend) + tmux history copy
(require 'ai-term) ;; in-Emacs Claude launcher (vertical-split ghostel)
(require 'help-utils) ;; search: arch-wiki, devdoc, tldr, wikipedia
(require 'help-config) ;; info, man, help config
diff --git a/modules/eat-config.el b/modules/eat-config.el
new file mode 100644
index 000000000..7f3eab69f
--- /dev/null
+++ b/modules/eat-config.el
@@ -0,0 +1,230 @@
+;;; eat-config.el --- EAT terminal emulator and the F12 eshell toggle -*- lexical-binding: t; coding: utf-8; -*-
+
+;;; Commentary:
+;;
+;; EAT (Emulate A Terminal, pure elisp) is the terminal emulator. Because EAT
+;; renders entirely in elisp, its whole palette is real Emacs faces, so it themes
+;; from the theme. This module owns the eat package configuration, the keymap
+;; wiring that lets F12 and C-; reach Emacs from inside a terminal, and the F12
+;; dock-and-remember toggle.
+;;
+;; F12 opens eshell, which runs through EAT (eat-eshell-mode, set up in
+;; eshell-config.el): the shell is eshell -- elisp functions as commands, TRAMP
+;; transparency -- and EAT renders its visual commands. eshell-config.el holds
+;; the shell itself; this module holds the emulator and the toggle.
+;;
+;; The toggle reuses the geometry-preservation pattern from cj-window-toggle-lib:
+;; capture direction + body size at toggle-off, replay them via a custom display
+;; action using frame-edge directions and body-relative sizes, so the docked
+;; terminal returns at the same size and the result is divider-independent.
+
+;;; Code:
+
+(require 'cj-window-geometry-lib)
+(require 'cj-window-toggle-lib)
+
+(declare-function eat "eat" (&optional program arg))
+(declare-function eshell "eshell" (&optional arg))
+(defvar eat-mode-map)
+(defvar eat-semi-char-mode-map)
+(defvar eshell-buffer-name)
+(defvar cj/custom-keymap)
+
+(defun cj/turn-off-chrome-for-term ()
+ "Turn off line numbers and hl-line in a terminal buffer."
+ (hl-line-mode -1)
+ (display-line-numbers-mode -1))
+
+;; ------------------------------- eat package ---------------------------------
+
+(use-package eat
+ :ensure t
+ :commands (eat)
+ :hook (eat-mode . cj/turn-off-chrome-for-term)
+ :custom
+ ;; Close the EAT buffer when its shell exits.
+ (eat-kill-buffer-on-exit t)
+ ;; Shell-integration UX. These are EAT defaults, set explicitly to document
+ ;; intent and survive default changes. They only light up once the shell
+ ;; sources EAT's integration script -- see the EAT block in the zsh rc.
+ (eat-enable-directory-tracking t) ; Emacs follows the terminal's cwd
+ (eat-enable-shell-prompt-annotation t) ; the success/running/failure prompt glyphs
+ (eat-enable-shell-command-history t) ; terminal history into EAT line-mode isearch
+ ;; Interaction.
+ (eat-enable-mouse t) ; mouse clicks + selection in TUIs (default)
+ (eat-enable-kill-from-terminal t) ; terminal selection -> Emacs kill-ring (default)
+ (eat-enable-yank-to-terminal t) ; Emacs kill-ring -> the terminal (off by default)
+ ;; Fidelity.
+ (eat-enable-alternative-display t) ; alt-screen so TUIs restore scrollback on exit (default)
+ (eat-term-scrollback-size (* 10 1024 1024)) ; ~10MB of scrollback, matching the old ghostel
+ ;; Truecolor is already on: eat-term-name auto-selects the compiled eat-truecolor terminfo.
+ ;; Niceties.
+ (eat-sixel-render-formats '(xpm svg half-block background none)) ; inline images (on by default)
+ (eat-query-before-killing-running-terminal 'auto) ; confirm before killing a terminal with a live process
+ :config
+ ;; F12 and C-; must reach Emacs from inside EAT. In semi-char mode (EAT's
+ ;; default) EAT forwards unbound keys to the terminal -- a letter runs
+ ;; `eat-self-input' -- so bind these explicitly or they never reach Emacs:
+ ;; F12 toggles the terminal window, C-; opens the global prefix map.
+ (keymap-set eat-semi-char-mode-map "<f12>" #'cj/term-toggle)
+ (keymap-set eat-semi-char-mode-map "C-;" cj/custom-keymap)
+ (keymap-set eat-mode-map "<f12>" #'cj/term-toggle)
+ (keymap-set eat-mode-map "C-;" cj/custom-keymap))
+
+;; ----------------------- F12 toggle (custom) -----------------------
+;;
+;; Mirrors the geometry-preservation pattern shared with ai-term.el: capture
+;; direction + body size at toggle-off, replay them via a custom display action
+;; using frame-edge directions and body-relative sizes so the result is
+;; divider-independent and layout-stable. Manages the EAT terminal only;
+;; ai-term.el's ghostel agent buffers are separate (M-SPC).
+
+(defcustom cj/term-toggle-window-height 0.7
+ "Default fraction of frame height for the F12 terminal window.
+Used as the size fallback when F12 docks the terminal as a bottom split."
+ :type 'number
+ :group 'term)
+
+(defcustom cj/term-toggle-window-width 0.5
+ "Default fraction of frame width for the F12 terminal window.
+Used as the size fallback when F12 docks the terminal as a right-side
+column (see `cj/--term-toggle-default-direction')."
+ :type 'number
+ :group 'term)
+
+(defun cj/--term-toggle-default-direction ()
+ "Return the default dock direction for the F12 terminal: `right' or `below'.
+Docks as a right-side column only when a side-by-side split would leave
+both panes at least `cj/window-dock-min-columns' wide (the terminal's
+share is `cj/term-toggle-window-width'); otherwise stacks below. See
+`cj/preferred-dock-direction'."
+ (cj/preferred-dock-direction (frame-width) cj/term-toggle-window-width))
+
+(defun cj/--term-toggle-default-size (direction)
+ "Return the default size fraction paired with DIRECTION for the F12 terminal.
+`cj/term-toggle-window-width' for `right', `cj/term-toggle-window-height'
+otherwise."
+ (if (eq direction 'right)
+ cj/term-toggle-window-width
+ cj/term-toggle-window-height))
+
+(defvar cj/--term-toggle-last-direction nil
+ "Last user-chosen direction for the F12 terminal display.
+Symbol: right, left, or below. `above' is never stored. nil means use the
+default `below' for F12's traditional bottom split.")
+
+(defvar cj/--term-toggle-last-size nil
+ "Last user-chosen size for the F12 terminal display.
+Positive integer: body-cols (right/left) or total-lines (below/above) -- see
+`cj/window-replay-size' for why the vertical axis uses total, not body.
+nil means fall back to `cj/term-toggle-window-height' as a fraction.")
+
+(defun cj/--term-toggle-buffer-p (buffer)
+ "Return non-nil when BUFFER is an eshell terminal F12 should manage.
+
+F12 opens eshell, which runs through EAT via eat-eshell-mode. ai-term's ghostel
+agent buffers are managed separately via M-SPC, not F12."
+ (and (bufferp buffer)
+ (buffer-live-p buffer)
+ (with-current-buffer buffer
+ (derived-mode-p 'eshell-mode))))
+
+(defun cj/--term-toggle-buffers ()
+ "Return live F12-managed terminal buffers in `buffer-list' (MRU) order."
+ (seq-filter #'cj/--term-toggle-buffer-p (buffer-list)))
+
+(defun cj/--term-toggle-displayed-window (&optional frame)
+ "Return a window in FRAME currently displaying an F12 terminal buffer, or nil.
+FRAME defaults to the selected frame. Minibuffer is excluded."
+ (seq-find (lambda (w)
+ (cj/--term-toggle-buffer-p (window-buffer w)))
+ (window-list (or frame (selected-frame)) 'never)))
+
+(defun cj/--term-toggle-capture-state (window)
+ "Capture WINDOW's direction + body size into module-level state.
+The default direction (used when WINDOW fills its frame) is the
+column-rule choice from `cj/--term-toggle-default-direction'."
+ (cj/window-toggle-capture-state
+ window (cj/--term-toggle-default-direction)
+ 'cj/--term-toggle-last-direction
+ 'cj/--term-toggle-last-size
+ '(right below left)))
+
+(defun cj/--term-toggle-display-saved (buffer alist)
+ "Display-buffer action: split per saved direction and body size.
+Delegates to `cj/window-toggle-display-saved' against the F12 state vars,
+falling back to the column-rule default direction
+\(`cj/--term-toggle-default-direction') and its paired size."
+ (let ((dir (cj/--term-toggle-default-direction)))
+ (cj/window-toggle-display-saved
+ buffer alist
+ 'cj/--term-toggle-last-direction dir
+ 'cj/--term-toggle-last-size (cj/--term-toggle-default-size dir))))
+
+(defun cj/--term-toggle-display-rule-list ()
+ "Return the `display-buffer-alist' entry list installed by F12.
+Routes any terminal buffer satisfying `cj/--term-toggle-buffer-p' through
+reuse-window then the saved-geometry action. Excludes agent buffers."
+ '(((lambda (buffer-or-name _)
+ (cj/--term-toggle-buffer-p (get-buffer buffer-or-name)))
+ (display-buffer-reuse-window
+ cj/--term-toggle-display-saved)
+ (inhibit-same-window . t))))
+
+(dolist (entry (cj/--term-toggle-display-rule-list))
+ (add-to-list 'display-buffer-alist entry))
+
+(defun cj/--term-toggle-dispatch ()
+ "Compute the F12 (`cj/term-toggle') action without performing it.
+
+Returns one of:
+- (toggle-off . WINDOW) -- terminal displayed in WINDOW; hide it.
+- (show-recent . BUFFER) -- terminal alive but not shown; redisplay.
+- (create-new) -- no terminal buffer alive; create one."
+ (let ((win (cj/--term-toggle-displayed-window)))
+ (cond
+ (win (cons 'toggle-off win))
+ (t
+ (let ((buffers (cj/--term-toggle-buffers)))
+ (cond
+ (buffers (cons 'show-recent (car buffers)))
+ (t '(create-new))))))))
+
+(defun cj/term-toggle ()
+ "Toggle the F12 eshell terminal (the primary `*eshell*', run through EAT).
+
+- If it is displayed in this frame, capture its geometry and delete its window
+ (toggle off). Falls back to burying when it is the only window in the frame.
+- Otherwise, if it is alive, display it via the saved-geometry action.
+- Otherwise, open eshell, displaying it through the same saved-geometry action.
+
+eshell runs through EAT via eat-eshell-mode, so visual commands render in a real
+terminal. ai-term's ghostel agent buffers are managed separately via M-SPC."
+ (interactive)
+ (pcase (cj/--term-toggle-dispatch)
+ (`(toggle-off . ,win)
+ (cj/--term-toggle-capture-state win)
+ (if (one-window-p)
+ (bury-buffer (window-buffer win))
+ (delete-window win))
+ nil)
+ (`(show-recent . ,buf)
+ (display-buffer buf)
+ (let ((w (get-buffer-window buf)))
+ (when w (select-window w)))
+ buf)
+ (`(create-new)
+ ;; Open the primary eshell without stealing the layout, then display it
+ ;; through the saved-geometry dock rule (same path as show-recent).
+ (save-window-excursion (eshell))
+ (let ((buf (get-buffer (or (bound-and-true-p eshell-buffer-name) "*eshell*"))))
+ (when buf
+ (display-buffer buf)
+ (let ((w (get-buffer-window buf)))
+ (when w (select-window w))))
+ buf))))
+
+(keymap-global-set "<f12>" #'cj/term-toggle)
+
+(provide 'eat-config)
+;;; eat-config.el ends here
diff --git a/modules/eshell-config.el b/modules/eshell-config.el
index c2ec6d152..7379795d2 100644
--- a/modules/eshell-config.el
+++ b/modules/eshell-config.el
@@ -51,6 +51,9 @@
(declare-function eshell-send-input "esh-mode")
(declare-function eshell/pwd "em-dirs")
(declare-function eshell/alias "em-alias")
+(declare-function eshell/cd "em-dirs")
+(declare-function eshell-stringify "esh-util")
+(declare-function eat-eshell-mode "eat")
(defgroup cj/eshell nil
"Personal Eshell configuration."
@@ -83,6 +86,59 @@ pairs where COMMAND is the `cd' string `eshell/alias' should run."
(dolist (pair (cj/--eshell-ssh-alias-commands hosts))
(eshell/alias (car pair) (cdr pair))))
+;; ---------------------------- prompt segments --------------------------------
+
+(defun cj/--eshell-git-branch ()
+ "Return the current git branch for `default-directory', or nil.
+Reads .git/HEAD directly so it adds no subprocess per prompt, and skips remote
+directories so a TRAMP prompt stays fast."
+ (unless (file-remote-p default-directory)
+ (when-let* ((root (locate-dominating-file default-directory ".git"))
+ (head (expand-file-name ".git/HEAD" root)))
+ (when (file-readable-p head)
+ (with-temp-buffer
+ (insert-file-contents head)
+ (when (looking-at "ref: refs/heads/\\(.*\\)")
+ (string-trim (match-string 1))))))))
+
+(defun cj/--eshell-prompt-status-segment ()
+ "Return the eshell prompt's exit-status segment, or an empty string.
+Shows the last command's exit code in brackets when it was non-zero, mirroring
+the zsh prompt's failure indicator."
+ (let ((status (bound-and-true-p eshell-last-command-status)))
+ (if (or (null status) (zerop status))
+ ""
+ (format " [%d]" status))))
+
+;; ------------------------------- zoxide --------------------------------------
+;; Share the same frecency database as the zsh shell by calling the zoxide
+;; binary: `z' jumps to a remembered directory, and every eshell directory
+;; change feeds `zoxide add' so eshell visits accrue in the same database.
+
+(defun eshell/z (&rest args)
+ "Jump to a directory via zoxide, sharing the zsh zoxide database.
+With no ARGS, cd home. Otherwise query zoxide for the best match and cd there."
+ (if (null args)
+ (eshell/cd)
+ (let ((dir (string-trim
+ (shell-command-to-string
+ (concat "zoxide query -- "
+ (mapconcat #'shell-quote-argument
+ (mapcar #'eshell-stringify args) " "))))))
+ (if (and (not (string-empty-p dir)) (file-directory-p dir))
+ (eshell/cd dir)
+ (error "zoxide: no match for %s"
+ (string-join (mapcar #'eshell-stringify args) " "))))))
+
+(defun cj/--eshell-zoxide-add ()
+ "Record `default-directory' in the zoxide database (skips remote dirs)."
+ (when (and (not (file-remote-p default-directory))
+ (executable-find "zoxide"))
+ (call-process "zoxide" nil 0 nil "add" "--"
+ (expand-file-name default-directory))))
+
+(add-hook 'eshell-directory-change-hook #'cj/--eshell-zoxide-add)
+
(use-package eshell
:ensure nil ;; built-in
:commands (eshell)
@@ -108,6 +164,9 @@ pairs where COMMAND is the `cd' string `eshell/alias' should run."
(propertize (system-name) 'face 'default)
":"
(propertize (abbreviate-file-name (eshell/pwd)) 'face 'default)
+ (let ((branch (cj/--eshell-git-branch)))
+ (if branch (propertize (concat " (" branch ")") 'face 'default) ""))
+ (propertize (cj/--eshell-prompt-status-segment) 'face 'default)
"\n"
(propertize "%" 'face 'default)
" ")))
@@ -179,35 +238,20 @@ pairs where COMMAND is the `cd' string `eshell/alias' should run."
(delete-window)))
(advice-add 'eshell-life-is-too-much :after 'cj/eshell-delete-window-on-exit)
-(use-package eshell-toggle
- :custom
- (eshell-toggle-size-fraction 2)
- (eshell-toggle-run-command nil)
- (eshell-toggle-init-function #'eshell-toggle-init-eshell)
- :bind
- ("C-<f12>" . eshell-toggle))
+;; Run eshell's external commands through EAT (a real terminal): visual commands
+;; (vim, htop, less) render properly and ANSI output is faithful, while eshell
+;; stays the shell -- elisp functions as commands + TRAMP transparency. EAT
+;; handles color itself, so it supersedes xterm-color for eshell; the
+;; xterm-color block below stays for now and steps aside if colors double up.
+(with-eval-after-load 'esh-mode
+ (require 'eat)
+ (eat-eshell-mode 1))
-(use-package xterm-color
- :after eshell
- ;; Two hooks. eshell-before-prompt is the real hook name; use-package appends
- ;; "-hook", so writing eshell-before-prompt-hook here registered on a
- ;; nonexistent eshell-before-prompt-hook-hook and never ran. The eshell-mode
- ;; hook scopes TERM=xterm-256color to eshell-spawned processes only (a global
- ;; setenv would leak it to every start-process regardless of terminal).
- :hook
- ((eshell-before-prompt . (lambda ()
- (setq xterm-color-preserve-properties t)))
- (eshell-mode . (lambda ()
- (setq-local process-environment
- (cons "TERM=xterm-256color"
- process-environment)))))
- :config
- ;; Wire xterm-color into eshell's output pipeline (per its README): install
- ;; the filter and drop eshell's own ANSI handler. Without this the escapes are
- ;; never interpreted and TERM=xterm-256color only leaks raw codes.
- (add-to-list 'eshell-preoutput-filter-functions 'xterm-color-filter)
- (setq eshell-output-filter-functions
- (remove 'eshell-handle-ansi-color eshell-output-filter-functions)))
+;; eshell-toggle and xterm-color are retired. F12 opens eshell now (the
+;; dock-and-remember toggle in eat-config.el), and eat-eshell-mode renders
+;; eshell's output through EAT, which handles ANSI color natively -- so
+;; xterm-color's filter and its TERM=xterm-256color override are redundant and
+;; would fight EAT's own TERM=eat-truecolor.
(use-package eshell-syntax-highlighting
:after esh-mode
diff --git a/modules/term-config.el b/modules/term-config.el
index 474a85c42..659224198 100644
--- a/modules/term-config.el
+++ b/modules/term-config.el
@@ -60,14 +60,13 @@
(defvar ghostel-keymap-exceptions)
(defvar ghostel-buffer-name)
(defvar ghostel--input-mode)
-
-;; eat backs the F12 toggle (see the eat package + F12 toggle sections below).
-(declare-function eat "eat" (&optional program arg))
-(defvar eat-buffer-name)
-(defvar eat-mode-map)
-(defvar eat-semi-char-mode-map)
(defvar cj/custom-keymap)
+;; The EAT F12 terminal and its dock-and-remember toggle live in eat-config.el.
+;; ghostel (ai-term's backend) reuses cj/term-toggle and cj/turn-off-chrome-for-term
+;; from there: F12 in a ghostel agent buffer toggles the EAT terminal.
+(require 'eat-config)
+
(defvar-keymap cj/term-map
:doc "Personal terminal command map.")
;; Lowercase x picked over T for fewer Shift presses; t is the toggle leaf.
@@ -254,11 +253,6 @@ into the pty; without tmux, moves point up in the `ghostel-copy-mode' buffer."
;; ----------------------------- ghostel package -------------------------------
-(defun cj/turn-off-chrome-for-term ()
- "Turn off line numbers and hl-line in a terminal buffer."
- (hl-line-mode -1)
- (display-line-numbers-mode -1))
-
(defun cj/term-launch-tmux ()
"Auto-launch tmux in a ghostel buffer unless already inside tmux.
@@ -321,189 +315,6 @@ run its own project-named tmux session instead of a bare, auto-named one.
;; Byte analog of the prior 100000-line vterm setting (~100 bytes/line) -- D7.
(ghostel-max-scrollback (* 10 1024 1024)))
-;; ------------------------------- eat package ---------------------------------
-;; EAT (pure-elisp terminal) backs the F12 toggle: its whole palette is real
-;; Emacs faces, so it themes from the theme. ghostel stays for ai-term (M-SPC).
-;; No tmux here -- F12's EAT runs a plain $SHELL (decision 2026-06-25).
-
-(use-package eat
- :ensure t
- :commands (eat)
- :hook (eat-mode . cj/turn-off-chrome-for-term)
- :custom
- ;; Close the EAT buffer when its shell exits (mirrors ghostel-kill-buffer-on-exit).
- (eat-kill-buffer-on-exit t)
- :config
- ;; F12 and C-; must reach Emacs from inside EAT. In semi-char mode (EAT's
- ;; default) EAT forwards unbound keys to the terminal -- a letter runs
- ;; `eat-self-input' -- so bind these explicitly or they never reach Emacs:
- ;; F12 toggles the terminal window, C-; opens the global prefix map.
- (keymap-set eat-semi-char-mode-map "<f12>" #'cj/term-toggle)
- (keymap-set eat-semi-char-mode-map "C-;" cj/custom-keymap)
- (keymap-set eat-mode-map "<f12>" #'cj/term-toggle)
- (keymap-set eat-mode-map "C-;" cj/custom-keymap))
-
-;; ----------------------- F12 toggle (custom) -----------------------
-;;
-;; Mirrors the geometry-preservation pattern shared with ai-term.el: capture
-;; direction + body size at toggle-off, replay them via a custom display action
-;; using frame-edge directions and body-relative sizes so the result is
-;; divider-independent and layout-stable. Manages the EAT terminal only;
-;; ai-term.el's ghostel agent buffers are separate (M-SPC).
-
-(defcustom cj/term-toggle-window-height 0.7
- "Default fraction of frame height for the F12 terminal window.
-Used as the size fallback when F12 docks the terminal as a bottom split."
- :type 'number
- :group 'term)
-
-(defcustom cj/term-toggle-window-width 0.5
- "Default fraction of frame width for the F12 terminal window.
-Used as the size fallback when F12 docks the terminal as a right-side
-column (see `cj/--term-toggle-default-direction')."
- :type 'number
- :group 'term)
-
-(defun cj/--term-toggle-default-direction ()
- "Return the default dock direction for the F12 terminal: `right' or `below'.
-Docks as a right-side column only when a side-by-side split would leave
-both panes at least `cj/window-dock-min-columns' wide (the terminal's
-share is `cj/term-toggle-window-width'); otherwise stacks below. See
-`cj/preferred-dock-direction'."
- (cj/preferred-dock-direction (frame-width) cj/term-toggle-window-width))
-
-(defun cj/--term-toggle-default-size (direction)
- "Return the default size fraction paired with DIRECTION for the F12 terminal.
-`cj/term-toggle-window-width' for `right', `cj/term-toggle-window-height'
-otherwise."
- (if (eq direction 'right)
- cj/term-toggle-window-width
- cj/term-toggle-window-height))
-
-(defvar cj/--term-toggle-last-direction nil
- "Last user-chosen direction for the F12 terminal display.
-Symbol: right, left, or below. `above' is never stored. nil means use the
-default `below' for F12's traditional bottom split.")
-
-(defvar cj/--term-toggle-last-size nil
- "Last user-chosen size for the F12 terminal display.
-Positive integer: body-cols (right/left) or total-lines (below/above) -- see
-`cj/window-replay-size' for why the vertical axis uses total, not body.
-nil means fall back to `cj/term-toggle-window-height' as a fraction.")
-
-(defun cj/--term-toggle-buffer-p (buffer)
- "Return non-nil when BUFFER is the EAT terminal F12 should manage.
-
-Qualifies when BUFFER is alive and has `eat-mode' (or its name starts with the
-EAT buffer-name prefix). ai-term's ghostel agent buffers never match -- they
-are managed separately via M-SPC, not F12."
- (and (bufferp buffer)
- (buffer-live-p buffer)
- (with-current-buffer buffer
- (or (eq major-mode 'eat-mode)
- (string-prefix-p (or (bound-and-true-p eat-buffer-name)
- "*eat*")
- (buffer-name buffer))))))
-
-(defun cj/--term-toggle-buffers ()
- "Return live F12-managed terminal buffers in `buffer-list' (MRU) order."
- (seq-filter #'cj/--term-toggle-buffer-p (buffer-list)))
-
-(defun cj/--term-toggle-displayed-window (&optional frame)
- "Return a window in FRAME currently displaying an F12 terminal buffer, or nil.
-FRAME defaults to the selected frame. Minibuffer is excluded."
- (seq-find (lambda (w)
- (cj/--term-toggle-buffer-p (window-buffer w)))
- (window-list (or frame (selected-frame)) 'never)))
-
-(defun cj/--term-toggle-capture-state (window)
- "Capture WINDOW's direction + body size into module-level state.
-The default direction (used when WINDOW fills its frame) is the
-column-rule choice from `cj/--term-toggle-default-direction'."
- (cj/window-toggle-capture-state
- window (cj/--term-toggle-default-direction)
- 'cj/--term-toggle-last-direction
- 'cj/--term-toggle-last-size
- '(right below left)))
-
-(defun cj/--term-toggle-display-saved (buffer alist)
- "Display-buffer action: split per saved direction and body size.
-Delegates to `cj/window-toggle-display-saved' against the F12 state vars,
-falling back to the column-rule default direction
-\(`cj/--term-toggle-default-direction') and its paired size."
- (let ((dir (cj/--term-toggle-default-direction)))
- (cj/window-toggle-display-saved
- buffer alist
- 'cj/--term-toggle-last-direction dir
- 'cj/--term-toggle-last-size (cj/--term-toggle-default-size dir))))
-
-(defun cj/--term-toggle-display-rule-list ()
- "Return the `display-buffer-alist' entry list installed by F12.
-Routes any terminal buffer satisfying `cj/--term-toggle-buffer-p' through
-reuse-window then the saved-geometry action. Excludes agent buffers."
- '(((lambda (buffer-or-name _)
- (cj/--term-toggle-buffer-p (get-buffer buffer-or-name)))
- (display-buffer-reuse-window
- cj/--term-toggle-display-saved)
- (inhibit-same-window . t))))
-
-(dolist (entry (cj/--term-toggle-display-rule-list))
- (add-to-list 'display-buffer-alist entry))
-
-(defun cj/--term-toggle-dispatch ()
- "Compute the F12 (`cj/term-toggle') action without performing it.
-
-Returns one of:
-- (toggle-off . WINDOW) -- terminal displayed in WINDOW; hide it.
-- (show-recent . BUFFER) -- terminal alive but not shown; redisplay.
-- (create-new) -- no terminal buffer alive; create one."
- (let ((win (cj/--term-toggle-displayed-window)))
- (cond
- (win (cons 'toggle-off win))
- (t
- (let ((buffers (cj/--term-toggle-buffers)))
- (cond
- (buffers (cons 'show-recent (car buffers)))
- (t '(create-new))))))))
-
-(defun cj/term-toggle ()
- "Toggle the EAT terminal buffer.
-
-- If the EAT terminal is displayed in this frame, capture its geometry and
- delete its window (toggle off). Falls back to burying when it is the only
- window in the frame.
-- Otherwise, if the EAT terminal buffer is alive, display it via the
- saved-geometry action.
-- Otherwise, create a new EAT terminal, displaying it through the same
- saved-geometry action.
-
-ai-term's ghostel agent buffers are managed separately via M-SPC, not F12."
- (interactive)
- (pcase (cj/--term-toggle-dispatch)
- (`(toggle-off . ,win)
- (cj/--term-toggle-capture-state win)
- (if (one-window-p)
- (bury-buffer (window-buffer win))
- (delete-window win))
- nil)
- (`(show-recent . ,buf)
- (display-buffer buf)
- (let ((w (get-buffer-window buf)))
- (when w (select-window w)))
- buf)
- (`(create-new)
- ;; Create the EAT buffer without stealing the layout, then display it
- ;; through the saved-geometry dock rule (same path as show-recent).
- (save-window-excursion (eat))
- (let ((buf (get-buffer (or (bound-and-true-p eat-buffer-name) "*eat*"))))
- (when buf
- (display-buffer buf)
- (let ((w (get-buffer-window buf)))
- (when w (select-window w))))
- buf))))
-
-(keymap-global-set "<f12>" #'cj/term-toggle)
-
;; ----------------------------- prefix menu -----------------------------------
(keymap-set cj/term-map "c" #'cj/term-copy-mode-dwim)
diff --git a/scripts/theme-studio/WIP.json b/scripts/theme-studio/WIP.json
index 23a0ff46e..935c7cc88 100644
--- a/scripts/theme-studio/WIP.json
+++ b/scripts/theme-studio/WIP.json
@@ -788,8 +788,8 @@
"height": null
},
"region": {
- "fg": "#100f0f",
- "bg": "#ab8d2e",
+ "fg": null,
+ "bg": "#4a4b4f",
"distant-fg": null,
"family": null,
"weight": null,
@@ -1154,8 +1154,6 @@
"num",
"ui:lazy-highlight",
"ui:isearch",
- "ui:hl-line",
- "ui:highlight",
"str",
"esc",
"re",
@@ -1497,7 +1495,6 @@
"pkg:dired:dired-flagged",
"pkg:dired:dired-ignored",
"pkg:dired:dired-warning",
- "ui:region",
"ui:mode-line-inactive",
"pkg:nerd-icons:nerd-icons-blue",
"pkg:nerd-icons:nerd-icons-blue-alt",
@@ -1532,7 +1529,31 @@
"pkg:nerd-icons:nerd-icons-red-alt",
"pkg:nerd-icons:nerd-icons-silver",
"pkg:nerd-icons:nerd-icons-yellow",
- "pkg:nerd-icons:nerd-icons-lpink"
+ "pkg:nerd-icons:nerd-icons-lpink",
+ "pkg:eat:eat-term-color-white",
+ "pkg:eat:eat-term-color-green",
+ "pkg:eat:eat-shell-prompt-annotation-failure",
+ "pkg:eat:eat-shell-prompt-annotation-success",
+ "pkg:eat:eat-shell-prompt-annotation-running",
+ "pkg:eat:eat-term-bold",
+ "pkg:eat:eat-term-italic",
+ "pkg:eat:eat-term-color-yellow",
+ "pkg:eat:eat-term-color-blue",
+ "pkg:eat:eat-term-color-red",
+ "pkg:eat:eat-term-faint",
+ "pkg:eat:eat-term-color-magenta",
+ "pkg:eat:eat-term-color-cyan",
+ "pkg:eat:eat-term-color-bright-black",
+ "pkg:eat:eat-term-color-black",
+ "pkg:eat:eat-term-color-bright-white",
+ "pkg:eat:eat-term-color-bright-cyan",
+ "pkg:eat:eat-term-color-bright-magenta",
+ "pkg:eat:eat-term-color-bright-blue",
+ "pkg:eat:eat-term-color-bright-yellow",
+ "pkg:eat:eat-term-color-bright-green",
+ "pkg:eat:eat-term-color-bright-red",
+ "pkg:eat:eat-term-slow-blink",
+ "pkg:eat:eat-term-fast-blink"
],
"packages": {
"org-mode": {
@@ -3600,6 +3621,162 @@
"source": "user"
}
},
+ "eat": {
+ "eat-term-color-black": {
+ "fg": "#100f0f",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-red": {
+ "fg": "#cb6b4d",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-green": {
+ "fg": "#74932f",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-yellow": {
+ "fg": "#e6ce88",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-blue": {
+ "fg": "#67809c",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-magenta": {
+ "fg": "#8255b5",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-cyan": {
+ "fg": "#88b2c3",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-white": {
+ "fg": "#bfc4d0",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-bright-black": {
+ "fg": "#8e919a",
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-bright-red": {
+ "fg": "#cb6b4d",
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-bright-green": {
+ "fg": "#74932f",
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-bright-yellow": {
+ "fg": "#e6ce88",
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-bright-blue": {
+ "fg": "#899bb1",
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-bright-magenta": {
+ "fg": "#8255b5",
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-bright-cyan": {
+ "fg": "#6ba9bd",
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-color-bright-white": {
+ "fg": "#a9b2bb",
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-bold": {
+ "fg": null,
+ "bg": null,
+ "weight": "bold",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-faint": {
+ "fg": "#777980",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-italic": {
+ "fg": null,
+ "bg": null,
+ "slant": "italic",
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-slow-blink": {
+ "fg": null,
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-term-fast-blink": {
+ "fg": null,
+ "bg": null,
+ "inherit": null,
+ "source": "default"
+ },
+ "eat-shell-prompt-annotation-success": {
+ "fg": "#74932f",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-shell-prompt-annotation-running": {
+ "fg": "#dab53d",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ },
+ "eat-shell-prompt-annotation-failure": {
+ "fg": "#a85b42",
+ "bg": null,
+ "inherit": null,
+ "source": "user"
+ }
+ },
"auto-dim-other-buffers": {
"auto-dim-other-buffers": {
"fg": "#777980",
diff --git a/scripts/theme-studio/previews.js b/scripts/theme-studio/previews.js
index 8e7c273fc..15e885da1 100644
--- a/scripts/theme-studio/previews.js
+++ b/scripts/theme-studio/previews.js
@@ -296,28 +296,52 @@ function renderGitGutterPreview(){const a='git-gutter',L=[];
function renderEatPreview(){const a='eat',L=[],c=(f,t)=>os(a,'eat-term-color-'+f,t),x=(f,t)=>os(a,'eat-term-'+f,t),an=(g,t)=>os(a,'eat-shell-prompt-annotation-'+g,t);
const p=g=>an(g,g==='success'?'✔':g==='failure'?'✘':'…')+' ~/projects/app $ ';
// 1. directory listing -- the widest palette block (dircolors)
- L.push(p('success')+'eza --color');
- L.push(c('blue','src/')+' '+c('blue','build/')+' '+c('bright-green','run.sh')+' '+c('cyan','latest -> v2.1/')+' '+c('red','backup.tar.gz')+' '+c('magenta','logo.png')+' README.md');
+ L.push(p('success')+'eza -la --color');
+ L.push('drwxr-xr-x - 14:02 '+c('blue','.git/'));
+ L.push('.rw-r--r-- 120 09:11 .gitignore');
+ L.push('drwxr-xr-x - 14:02 '+c('blue','src/'));
+ L.push('drwxr-xr-x - 13:48 '+c('blue','tests/'));
+ L.push('.rwxr-xr-x 2.1k 14:00 '+c('bright-green','run.sh'));
+ L.push('lrwxr-xr-x - 14:01 '+c('cyan','latest')+' -> '+c('blue','v2.1/'));
+ L.push('.rw-r--r-- 4.5M 22:30 '+c('red','backup.tar.gz'));
+ L.push('.rw-r--r-- 88k 18:05 '+c('magenta','logo.png'));
+ L.push('.rw-r--r-- 3.2k 14:02 README.md');
L.push('');
// 2. git status -- staged green, unstaged/untracked red
L.push(p('success')+'git status -sb');
- L.push(c('bright-cyan','## main...origin/main'));
- L.push(c('green','A src/cache.el')+' '+c('green','M README.md'));
- L.push(c('red',' M init.el')+' '+c('red',' D old.el')+' '+c('red','?? scratch.txt'));
- L.push('');
- // 3. git log --decorate -- yellow hashes, colored refs
+ L.push(c('bright-cyan','## main...origin/main [ahead 2]'));
+ L.push(c('green','A src/eat-preview.js'));
+ L.push(c('green','A src/cache.el'));
+ L.push(c('green','M README.md'));
+ L.push(c('red',' M init.el'));
+ L.push(c('red',' M modules/term-config.el'));
+ L.push(c('red',' D modules/old-vterm.el'));
+ L.push(c('red','?? docs/design/eat.org'));
+ L.push(c('red','?? scratch.txt'));
+ L.push('');
+ // 3. git log --decorate -- yellow hashes, colored refs, a merge
L.push(p('success')+'git log --oneline --graph --decorate');
- L.push(c('bright-black','* ')+c('yellow','a1b2c3d')+' '+c('bright-cyan','(HEAD -> ')+c('bright-green','main')+c('bright-cyan',')')+' add eat preview blocks');
- L.push(c('bright-black','* ')+c('yellow','9f8e7d6')+' '+c('bright-yellow','(tag: v2.1)')+' '+c('bright-red','(origin/main)')+' lowercase labels');
- L.push(c('bright-black','* ')+c('yellow','3c4d5e6')+' expose eat faces');
- L.push('');
- // 4. test run -- pass green, skip yellow, fail red, bold summary, faint timing
+ L.push(c('bright-black','* ')+c('yellow','a1b2c3d')+' '+c('bright-cyan','(HEAD -> ')+c('bright-green','main')+c('bright-cyan',')')+' richer eat preview blocks');
+ L.push(c('bright-black','* ')+c('yellow','9f8e7d6')+' '+c('bright-yellow','(tag: v2.1, ')+c('bright-red','origin/main')+c('bright-yellow',')')+' lowercase the labels');
+ L.push(c('bright-black','* ')+c('yellow','3c4d5e6')+' Merge branch '+c('green',"'eat-faces'"));
+ L.push(c('bright-black','|\\ '));
+ L.push(c('bright-black','| * ')+c('yellow','7a8b9c0')+' expose eat faces to studio');
+ L.push(c('bright-black','| * ')+c('yellow','1d2e3f4')+' add eat-term-color docstrings');
+ L.push(c('bright-black','|/ '));
+ L.push(c('bright-black','* ')+c('yellow','5f6a7b8')+' toggle eat instead of ghostel on f12');
+ L.push(c('bright-black','* ')+c('yellow','2c3d4e5')+' calendar-sync robustness fixes');
+ L.push('');
+ // 4. test run -- pass green, skip yellow, fail red, bold summary, faint detail
L.push(p('failure')+'make test');
- L.push(c('green','✔ PASS')+' init-config (12)');
- L.push(c('green','✔ PASS')+' eat-toggle (19)');
- L.push(c('yellow','⚠ SKIP')+' network-sync (2, offline)');
- L.push(c('red','✘ FAIL')+' calendar-parse (1)');
- L.push(x('bold','Ran 34 tests, 33 passed, ')+c('red','1 failed')+' '+x('faint','0.84s'));
+ L.push(c('green','✔ PASS')+' term-toggle '+x('faint','(19 tests)'));
+ L.push(c('green','✔ PASS')+' ai-term '+x('faint','(158 tests)'));
+ L.push(c('green','✔ PASS')+' calendar-sync '+x('faint','(575 tests)'));
+ L.push(c('green','✔ PASS')+' dashboard '+x('faint','(18 tests)'));
+ L.push(c('yellow','⚠ SKIP')+' network-sync '+x('faint','(2 tests, offline)'));
+ L.push(c('green','✔ PASS')+' transcription '+x('faint','(44 tests)'));
+ L.push(c('red','✘ FAIL')+' org-roam-refile '+x('faint','(1 test)'));
+ L.push(' '+x('italic','expected 3 refile targets, got 0'));
+ L.push(x('bold','Ran 817 tests, 815 passed, ')+c('yellow','1 skipped, ')+c('red','1 failed')+' '+x('faint','0.84s'));
L.push('');
// swatch reference key, below the realistic blocks
L.push(x('faint','palette')+' '+c('black','■')+c('red','■')+c('green','■')+c('yellow','■')+c('blue','■')+c('magenta','■')+c('cyan','■')+c('white','■')+' '+c('bright-black','■')+c('bright-red','■')+c('bright-green','■')+c('bright-yellow','■')+c('bright-blue','■')+c('bright-magenta','■')+c('bright-cyan','■')+c('bright-white','■'));
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index b69f06e77..b7414817f 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -2964,28 +2964,52 @@ function renderGitGutterPreview(){const a='git-gutter',L=[];
function renderEatPreview(){const a='eat',L=[],c=(f,t)=>os(a,'eat-term-color-'+f,t),x=(f,t)=>os(a,'eat-term-'+f,t),an=(g,t)=>os(a,'eat-shell-prompt-annotation-'+g,t);
const p=g=>an(g,g==='success'?'✔':g==='failure'?'✘':'…')+' ~/projects/app $ ';
// 1. directory listing -- the widest palette block (dircolors)
- L.push(p('success')+'eza --color');
- L.push(c('blue','src/')+' '+c('blue','build/')+' '+c('bright-green','run.sh')+' '+c('cyan','latest -> v2.1/')+' '+c('red','backup.tar.gz')+' '+c('magenta','logo.png')+' README.md');
+ L.push(p('success')+'eza -la --color');
+ L.push('drwxr-xr-x - 14:02 '+c('blue','.git/'));
+ L.push('.rw-r--r-- 120 09:11 .gitignore');
+ L.push('drwxr-xr-x - 14:02 '+c('blue','src/'));
+ L.push('drwxr-xr-x - 13:48 '+c('blue','tests/'));
+ L.push('.rwxr-xr-x 2.1k 14:00 '+c('bright-green','run.sh'));
+ L.push('lrwxr-xr-x - 14:01 '+c('cyan','latest')+' -> '+c('blue','v2.1/'));
+ L.push('.rw-r--r-- 4.5M 22:30 '+c('red','backup.tar.gz'));
+ L.push('.rw-r--r-- 88k 18:05 '+c('magenta','logo.png'));
+ L.push('.rw-r--r-- 3.2k 14:02 README.md');
L.push('');
// 2. git status -- staged green, unstaged/untracked red
L.push(p('success')+'git status -sb');
- L.push(c('bright-cyan','## main...origin/main'));
- L.push(c('green','A src/cache.el')+' '+c('green','M README.md'));
- L.push(c('red',' M init.el')+' '+c('red',' D old.el')+' '+c('red','?? scratch.txt'));
+ L.push(c('bright-cyan','## main...origin/main [ahead 2]'));
+ L.push(c('green','A src/eat-preview.js'));
+ L.push(c('green','A src/cache.el'));
+ L.push(c('green','M README.md'));
+ L.push(c('red',' M init.el'));
+ L.push(c('red',' M modules/term-config.el'));
+ L.push(c('red',' D modules/old-vterm.el'));
+ L.push(c('red','?? docs/design/eat.org'));
+ L.push(c('red','?? scratch.txt'));
L.push('');
- // 3. git log --decorate -- yellow hashes, colored refs
+ // 3. git log --decorate -- yellow hashes, colored refs, a merge
L.push(p('success')+'git log --oneline --graph --decorate');
- L.push(c('bright-black','* ')+c('yellow','a1b2c3d')+' '+c('bright-cyan','(HEAD -> ')+c('bright-green','main')+c('bright-cyan',')')+' add eat preview blocks');
- L.push(c('bright-black','* ')+c('yellow','9f8e7d6')+' '+c('bright-yellow','(tag: v2.1)')+' '+c('bright-red','(origin/main)')+' lowercase labels');
- L.push(c('bright-black','* ')+c('yellow','3c4d5e6')+' expose eat faces');
+ L.push(c('bright-black','* ')+c('yellow','a1b2c3d')+' '+c('bright-cyan','(HEAD -> ')+c('bright-green','main')+c('bright-cyan',')')+' richer eat preview blocks');
+ L.push(c('bright-black','* ')+c('yellow','9f8e7d6')+' '+c('bright-yellow','(tag: v2.1, ')+c('bright-red','origin/main')+c('bright-yellow',')')+' lowercase the labels');
+ L.push(c('bright-black','* ')+c('yellow','3c4d5e6')+' Merge branch '+c('green',"'eat-faces'"));
+ L.push(c('bright-black','|\\ '));
+ L.push(c('bright-black','| * ')+c('yellow','7a8b9c0')+' expose eat faces to studio');
+ L.push(c('bright-black','| * ')+c('yellow','1d2e3f4')+' add eat-term-color docstrings');
+ L.push(c('bright-black','|/ '));
+ L.push(c('bright-black','* ')+c('yellow','5f6a7b8')+' toggle eat instead of ghostel on f12');
+ L.push(c('bright-black','* ')+c('yellow','2c3d4e5')+' calendar-sync robustness fixes');
L.push('');
- // 4. test run -- pass green, skip yellow, fail red, bold summary, faint timing
+ // 4. test run -- pass green, skip yellow, fail red, bold summary, faint detail
L.push(p('failure')+'make test');
- L.push(c('green','✔ PASS')+' init-config (12)');
- L.push(c('green','✔ PASS')+' eat-toggle (19)');
- L.push(c('yellow','⚠ SKIP')+' network-sync (2, offline)');
- L.push(c('red','✘ FAIL')+' calendar-parse (1)');
- L.push(x('bold','Ran 34 tests, 33 passed, ')+c('red','1 failed')+' '+x('faint','0.84s'));
+ L.push(c('green','✔ PASS')+' term-toggle '+x('faint','(19 tests)'));
+ L.push(c('green','✔ PASS')+' ai-term '+x('faint','(158 tests)'));
+ L.push(c('green','✔ PASS')+' calendar-sync '+x('faint','(575 tests)'));
+ L.push(c('green','✔ PASS')+' dashboard '+x('faint','(18 tests)'));
+ L.push(c('yellow','⚠ SKIP')+' network-sync '+x('faint','(2 tests, offline)'));
+ L.push(c('green','✔ PASS')+' transcription '+x('faint','(44 tests)'));
+ L.push(c('red','✘ FAIL')+' org-roam-refile '+x('faint','(1 test)'));
+ L.push(' '+x('italic','expected 3 refile targets, got 0'));
+ L.push(x('bold','Ran 817 tests, 815 passed, ')+c('yellow','1 skipped, ')+c('red','1 failed')+' '+x('faint','0.84s'));
L.push('');
// swatch reference key, below the realistic blocks
L.push(x('faint','palette')+' '+c('black','■')+c('red','■')+c('green','■')+c('yellow','■')+c('blue','■')+c('magenta','■')+c('cyan','■')+c('white','■')+' '+c('bright-black','■')+c('bright-red','■')+c('bright-green','■')+c('bright-yellow','■')+c('bright-blue','■')+c('bright-magenta','■')+c('bright-cyan','■')+c('bright-white','■'));
diff --git a/tests/test-eshell-config--prompt.el b/tests/test-eshell-config--prompt.el
new file mode 100644
index 000000000..7073c7e0b
--- /dev/null
+++ b/tests/test-eshell-config--prompt.el
@@ -0,0 +1,75 @@
+;;; test-eshell-config--prompt.el --- Tests for eshell prompt helpers -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the pure prompt-segment helpers added for zsh parity: the
+;; .git/HEAD branch reader and the exit-status segment.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'eshell-config)
+
+(defvar eshell-last-command-status) ; declared special for the status tests
+
+;;; cj/--eshell-git-branch
+
+(ert-deftest test-eshell-git-branch-reads-head ()
+ "Normal: a .git/HEAD pointing at a branch returns the branch name."
+ (let ((dir (make-temp-file "esh-git-" t)))
+ (unwind-protect
+ (progn
+ (make-directory (expand-file-name ".git" dir))
+ (with-temp-file (expand-file-name ".git/HEAD" dir)
+ (insert "ref: refs/heads/feature-x\n"))
+ (let ((default-directory (file-name-as-directory dir)))
+ (should (equal (cj/--eshell-git-branch) "feature-x"))))
+ (delete-directory dir t))))
+
+(ert-deftest test-eshell-git-branch-no-repo-nil ()
+ "Boundary: a directory with no .git returns nil."
+ (let ((dir (make-temp-file "esh-nogit-" t)))
+ (unwind-protect
+ (let ((default-directory (file-name-as-directory dir)))
+ (should-not (cj/--eshell-git-branch)))
+ (delete-directory dir t))))
+
+(ert-deftest test-eshell-git-branch-detached-nil ()
+ "Boundary: a detached HEAD (a raw SHA, no ref) returns nil."
+ (let ((dir (make-temp-file "esh-detached-" t)))
+ (unwind-protect
+ (progn
+ (make-directory (expand-file-name ".git" dir))
+ (with-temp-file (expand-file-name ".git/HEAD" dir)
+ (insert "a1b2c3d4e5f6\n"))
+ (let ((default-directory (file-name-as-directory dir)))
+ (should-not (cj/--eshell-git-branch))))
+ (delete-directory dir t))))
+
+(ert-deftest test-eshell-git-branch-remote-skipped ()
+ "Boundary: a remote default-directory is skipped (no TRAMP read)."
+ (let ((default-directory "/ssh:host:/some/path/"))
+ (should-not (cj/--eshell-git-branch))))
+
+;;; cj/--eshell-prompt-status-segment
+
+(ert-deftest test-eshell-prompt-status-zero-empty ()
+ "Normal: a zero exit status yields an empty segment."
+ (let ((eshell-last-command-status 0))
+ (should (equal (cj/--eshell-prompt-status-segment) ""))))
+
+(ert-deftest test-eshell-prompt-status-nonzero-bracketed ()
+ "Normal: a non-zero exit status is shown in brackets."
+ (let ((eshell-last-command-status 1))
+ (should (equal (cj/--eshell-prompt-status-segment) " [1]")))
+ (let ((eshell-last-command-status 130))
+ (should (equal (cj/--eshell-prompt-status-segment) " [130]"))))
+
+(ert-deftest test-eshell-prompt-status-unset-empty ()
+ "Boundary: an unset status yields an empty segment, no error."
+ (let ((eshell-last-command-status nil))
+ (should (equal (cj/--eshell-prompt-status-segment) ""))))
+
+(provide 'test-eshell-config--prompt)
+;;; test-eshell-config--prompt.el ends here
diff --git a/tests/test-term-toggle--buffer-filter.el b/tests/test-term-toggle--buffer-filter.el
index 44f30aad6..6db2ec65c 100644
--- a/tests/test-term-toggle--buffer-filter.el
+++ b/tests/test-term-toggle--buffer-filter.el
@@ -4,9 +4,9 @@
;; Three closely-related helpers determine which terminal buffer F12
;; manages: the predicate `cj/--term-toggle-buffer-p', the list
;; `cj/--term-toggle-buffers', and the per-frame window finder
-;; `cj/--term-toggle-displayed-window'. F12 manages the EAT terminal;
-;; ghostel buffers (including ai-term's agent buffers) are NOT F12-managed --
-;; they live on M-SPC.
+;; `cj/--term-toggle-displayed-window'. F12 opens eshell (run through EAT via
+;; eat-eshell-mode), so it manages eshell-mode buffers. Standalone eat buffers,
+;; ghostel buffers, and ai-term's agent buffers are NOT F12-managed.
;;; Code:
@@ -14,7 +14,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
-(require 'term-config)
+(require 'eat-config)
(require 'testutil-ghostel-buffers)
(defun test-term-toggle--cleanup ()
@@ -22,18 +22,18 @@
(cj/test--kill-agent-buffers)
(cj/test--kill-test-term-buffers))
-(ert-deftest test-term-toggle--buffer-p-accepts-eat-mode ()
- "Normal: an eat-mode buffer qualifies as the F12 terminal."
+(ert-deftest test-term-toggle--buffer-p-accepts-eshell-mode ()
+ "Normal: an eshell-mode buffer qualifies as the F12 terminal."
(test-term-toggle--cleanup)
- (let ((buf (cj/test--make-fake-eat-buffer "*test-term-1*")))
+ (let ((buf (cj/test--make-fake-eshell-buffer "*test-term-1*")))
(unwind-protect
(should (cj/--term-toggle-buffer-p buf))
(kill-buffer buf))))
-(ert-deftest test-term-toggle--buffer-p-rejects-ghostel ()
- "Boundary: a ghostel buffer is NOT F12-managed (ghostel is ai-term's, M-SPC)."
+(ert-deftest test-term-toggle--buffer-p-rejects-eat ()
+ "Boundary: a standalone eat buffer is NOT F12-managed (F12 opens eshell)."
(test-term-toggle--cleanup)
- (let ((buf (cj/test--make-fake-ghostel-buffer "*test-term-ghostel*")))
+ (let ((buf (cj/test--make-fake-eat-buffer "*test-term-eat*")))
(unwind-protect
(should-not (cj/--term-toggle-buffer-p buf))
(kill-buffer buf))))
@@ -47,7 +47,7 @@
(kill-buffer buf))))
(ert-deftest test-term-toggle--buffer-p-rejects-non-terminal ()
- "Boundary: a regular buffer (not eat-mode, no terminal name prefix) -> nil."
+ "Boundary: a regular buffer (not eshell-mode) -> nil."
(test-term-toggle--cleanup)
(let ((buf (get-buffer-create "*test-term-regular*")))
(unwind-protect
@@ -57,35 +57,35 @@
(ert-deftest test-term-toggle--buffer-p-rejects-dead-buffer ()
"Boundary: nil and dead buffers -> nil."
(should-not (cj/--term-toggle-buffer-p nil))
- (let ((buf (cj/test--make-fake-eat-buffer "*test-term-dead*")))
+ (let ((buf (cj/test--make-fake-eshell-buffer "*test-term-dead*")))
(kill-buffer buf)
(should-not (cj/--term-toggle-buffer-p buf))))
-(ert-deftest test-term-toggle--buffers-returns-eat-excludes-others ()
- "Normal: returns the EAT terminal but not ghostel/agent buffers."
+(ert-deftest test-term-toggle--buffers-returns-eshell-excludes-others ()
+ "Normal: returns the eshell terminal but not eat/agent buffers."
(test-term-toggle--cleanup)
- (let ((eat (cj/test--make-fake-eat-buffer "*test-term-eat*"))
+ (let ((esh (cj/test--make-fake-eshell-buffer "*test-term-esh*"))
(agent (cj/test--make-fake-ghostel-buffer "agent [for-test]")))
(unwind-protect
(let ((result (cj/--term-toggle-buffers)))
- (should (memq eat result))
+ (should (memq esh result))
(should-not (memq agent result)))
- (kill-buffer eat)
+ (kill-buffer esh)
(kill-buffer agent))))
(ert-deftest test-term-toggle--displayed-window-finds-terminal ()
- "Normal: the EAT terminal in a window -> returns that window."
+ "Normal: the eshell terminal in a window -> returns that window."
(test-term-toggle--cleanup)
- (let ((eat (cj/test--make-fake-eat-buffer "*test-term-shown*")))
+ (let ((esh (cj/test--make-fake-eshell-buffer "*test-term-shown*")))
(unwind-protect
(save-window-excursion
(delete-other-windows)
(let ((win (split-window-right)))
- (set-window-buffer win eat)
+ (set-window-buffer win esh)
(let ((result (cj/--term-toggle-displayed-window)))
(should (windowp result))
- (should (eq (window-buffer result) eat)))))
- (kill-buffer eat))))
+ (should (eq (window-buffer result) esh)))))
+ (kill-buffer esh))))
(ert-deftest test-term-toggle--displayed-window-skips-agent ()
"Boundary: only an agent terminal is displayed -> nil (agent not F12-managed)."
diff --git a/tests/test-term-toggle--dispatch.el b/tests/test-term-toggle--dispatch.el
index f13c2840b..0d17395cc 100644
--- a/tests/test-term-toggle--dispatch.el
+++ b/tests/test-term-toggle--dispatch.el
@@ -14,7 +14,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
-(require 'term-config)
+(require 'eat-config)
(require 'testutil-ghostel-buffers)
(ert-deftest test-term-toggle--dispatch-window-displayed-returns-toggle-off ()
diff --git a/tests/test-term-toggle--display.el b/tests/test-term-toggle--display.el
index d6dd33da2..d59d23b15 100644
--- a/tests/test-term-toggle--display.el
+++ b/tests/test-term-toggle--display.el
@@ -14,7 +14,7 @@
(require 'cl-lib)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
-(require 'term-config)
+(require 'eat-config)
(ert-deftest test-term-toggle--capture-state-records-direction-and-size ()
"Normal: capture-state writes direction and integer size.
diff --git a/tests/testutil-ghostel-buffers.el b/tests/testutil-ghostel-buffers.el
index 3c8d75d00..8e26efec4 100644
--- a/tests/testutil-ghostel-buffers.el
+++ b/tests/testutil-ghostel-buffers.el
@@ -56,5 +56,15 @@ predicate without the side-effects of `(eat)'."
(setq-local major-mode 'eat-mode))
buf))
+(defun cj/test--make-fake-eshell-buffer (name)
+ "Return a buffer named NAME with `major-mode' set to `eshell-mode'.
+
+Avoids starting a real eshell by setting the mode buffer-locally. Used by the
+F12 toggle tests that need a buffer satisfying the eshell-mode predicate."
+ (let ((buf (get-buffer-create name)))
+ (with-current-buffer buf
+ (setq-local major-mode 'eshell-mode))
+ buf))
+
(provide 'testutil-ghostel-buffers)
;;; testutil-ghostel-buffers.el ends here
diff --git a/themes/WIP-theme.el b/themes/WIP-theme.el
index bac8f5071..018241dfb 100644
--- a/themes/WIP-theme.el
+++ b/themes/WIP-theme.el
@@ -36,7 +36,7 @@
'(font-lock-delimiter-face ((t (:foreground "#dce0e3"))))
'(font-lock-misc-punctuation-face ((t (:foreground "#dce0e3"))))
'(cursor ((t (:foreground "#100f0f" :background "#bac1c8"))))
- '(region ((t (:foreground "#100f0f" :background "#ab8d2e"))))
+ '(region ((t (:background "#4a4b4f"))))
'(hl-line ((t (:inherit highlight :background "#222223"))))
'(highlight ((t (:foreground "#eddba7" :weight bold))))
'(mode-line ((t (:foreground "#cbd0d6" :background "#424f5e" :box (:line-width 1 :color "#a9b2bb")))))
@@ -275,6 +275,28 @@
'(ansi-color-bright-magenta ((t (:inherit ansi-color-bright-magenta :weight bold))))
'(ansi-color-bright-cyan ((t (:inherit ansi-color-cyan :weight bold))))
'(ansi-color-bright-white ((t (:inherit ansi-color-bright-white :weight bold))))
+ '(eat-term-color-black ((t (:foreground "#100f0f"))))
+ '(eat-term-color-red ((t (:foreground "#cb6b4d"))))
+ '(eat-term-color-green ((t (:foreground "#74932f"))))
+ '(eat-term-color-yellow ((t (:foreground "#e6ce88"))))
+ '(eat-term-color-blue ((t (:foreground "#67809c"))))
+ '(eat-term-color-magenta ((t (:foreground "#8255b5"))))
+ '(eat-term-color-cyan ((t (:foreground "#88b2c3"))))
+ '(eat-term-color-white ((t (:foreground "#bfc4d0"))))
+ '(eat-term-color-bright-black ((t (:foreground "#8e919a" :weight bold))))
+ '(eat-term-color-bright-red ((t (:foreground "#cb6b4d" :weight bold))))
+ '(eat-term-color-bright-green ((t (:foreground "#74932f" :weight bold))))
+ '(eat-term-color-bright-yellow ((t (:foreground "#e6ce88" :weight bold))))
+ '(eat-term-color-bright-blue ((t (:foreground "#899bb1" :weight bold))))
+ '(eat-term-color-bright-magenta ((t (:foreground "#8255b5" :weight bold))))
+ '(eat-term-color-bright-cyan ((t (:foreground "#6ba9bd" :weight bold))))
+ '(eat-term-color-bright-white ((t (:foreground "#a9b2bb" :weight bold))))
+ '(eat-term-bold ((t (:weight bold))))
+ '(eat-term-faint ((t (:foreground "#777980"))))
+ '(eat-term-italic ((t (:slant italic))))
+ '(eat-shell-prompt-annotation-success ((t (:foreground "#74932f"))))
+ '(eat-shell-prompt-annotation-running ((t (:foreground "#dab53d"))))
+ '(eat-shell-prompt-annotation-failure ((t (:foreground "#a85b42"))))
'(auto-dim-other-buffers ((t (:foreground "#777980"))))
'(auto-dim-other-buffers-hide ((t (:foreground "#0a0c0d"))))
'(dashboard-banner-logo-title ((t (:inherit default :foreground "#dab53d" :background "#100f0f" :weight bold :slant italic :height 1.25))))
@@ -630,7 +652,7 @@
'(nerd-icons-lgreen ((t (:foreground "#a9be87"))))
'(nerd-icons-lmaroon ((t (:foreground "#a85b42"))))
'(nerd-icons-lorange ((t (:foreground "#cb8b7a"))))
- '(nerd-icons-lpink ((t (:foreground "#ff505b"))))
+ '(nerd-icons-lpink ((t (:foreground "#c99990"))))
'(nerd-icons-lpurple ((t (:foreground "#9f80c9"))))
'(nerd-icons-lred ((t (:foreground "#cb7b64"))))
'(nerd-icons-lsilver ((t (:foreground "#bac1c8"))))
diff --git a/todo.org b/todo.org
index de5563e36..f53320450 100644
--- a/todo.org
+++ b/todo.org
@@ -621,6 +621,14 @@ Couldn't reproduce — neither could Craig (2026-06-25). The toggle code is clea
** DONE [#B] F12 pops EAT instead of ghostel :feature:studio:
CLOSED: [2026-06-25 Thu]
Done 2026-06-25, design doc =docs/design/eat-f12-toggle.org=. Part A (commit fe7aa658): F12 toggles a single EAT terminal instead of ghostel, reusing the dock-and-remember geometry toggle; ghostel stays for ai-term (M-SPC); EAT runs a plain shell with no tmux; F12 and C-; are bound in EAT's keymaps so they reach Emacs from inside the terminal. Part B (commit 687b438f): EAT's faces are exposed in theme-studio (16 named palette + attribute + prompt-annotation faces) with a =renderEatPreview=, no colors set so it stays vanilla. term 223/223, ai-term 158/158, studio gates green; the toggle wiring (F12 reaches Emacs in EAT, =(eat)= creates the buffer, the predicate recognizes it) was verified live in the daemon. Accepted tradeoff: EAT needs a buffer reload to pick up a theme switch (ghostel auto-resyncs), taken for EAT's pure-elisp face control. The visual F12 dock/toggle check is a VERIFY under Manual testing and validation.
+** DOING [#B] Consolidate on EAT, retire ghostel :feature:refactor:
+Make EAT the only terminal and remove ghostel entirely (decision 2026-06-25). Phased; the ai-term port (Phase 3) wants its own focused session with a spike first.
+- Phase 1 DONE (commit 82294404): extracted =modules/eat-config.el= (eat package + F12/C-; keymaps + the F12 dock-and-remember toggle) out of =term-config.el=. term-config keeps ghostel (ai-term's backend) and requires eat-config. Toggle tests retargeted to eat-config; full suite green.
+- Phase 2 DONE (commit 0290b015): EAT experience settings in eat-config.el -- yank-to-terminal on, directory-tracking / prompt-annotations / command-history / mouse / kill-from-terminal / alt-screen affirmed, 10MB scrollback, truecolor already on via the compiled =eat-truecolor= terminfo. zsh shell-integration source line added to =~/.dotfiles/common/.zshrc= (uncommitted -- needs a dotfiles commit + a pull on the other daily driver).
+- Phase 3 (the big one): port ai-term from ghostel to EAT. ~30 ghostel touchpoints; the ~74 tmux touchpoints stay (tmux is the persistence layer). *Spike first*: prove EAT + tmux gives the same detach/reattach behavior. Then port =(ghostel)= -> eat creation with the "agent [project]" buffer name, =ghostel-mode= -> =eat-mode= detection, =ghostel-keymap-exceptions= + rebuild -> direct binds in =eat-semi-char-mode-map=, =ghostel-send-string= -> EAT's send fn, =ghostel-copy-mode= -> EAT line mode. Port the 158 ai-term tests.
+- Phase 4: retire ghostel. dashboard "Launch Terminal" =(ghostel)= -> =(eat)=; drop ghostel refs in =face-diagnostic.el= + =auto-dim-config.el=; migrate the useful term-config bits (tmux-history capture, copy surfaces -- both tmux-level, work under EAT) into eat-config; delete =term-config.el= and its init.el require; remove the pinned ghostel install.
+- Phase 5: cleanup. Remove the theme-studio ghostel app (=GHOSTEL_FACES=) once those faces are dead (ansi-color stays -- EAT inherits it); sweep ghostel mentions in comments/docs.
+
** TODO [#C] ai-term.el commentary names a stale F9 keybinding scheme :quick:
The header commentary (lines ~43-64) still documents an old =F9= / =C-F9= / =s-F9= / =M-F9= scheme for =cj/ai-term= and its family, but those bindings no longer exist — F9 is unbound in the daemon and the only live global binding is =M-SPC= -> =cj/ai-term-next= (=ai-term.el:1059=). The =M-<f9>= mention in the =cj/ai-term-shutdown= docstring (~996) is stale too. Rewrite the commentary and any stale docstrings to match the current keymap. Found 2026-06-25 while scoping the F12 -> EAT work.