diff options
| -rw-r--r-- | init.el | 3 | ||||
| -rw-r--r-- | modules/eat-config.el | 230 | ||||
| -rw-r--r-- | modules/eshell-config.el | 100 | ||||
| -rw-r--r-- | modules/term-config.el | 199 | ||||
| -rw-r--r-- | scripts/theme-studio/WIP.json | 189 | ||||
| -rw-r--r-- | scripts/theme-studio/previews.js | 58 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 54 | ||||
| -rw-r--r-- | tests/test-eshell-config--prompt.el | 75 | ||||
| -rw-r--r-- | tests/test-term-toggle--buffer-filter.el | 44 | ||||
| -rw-r--r-- | tests/test-term-toggle--dispatch.el | 2 | ||||
| -rw-r--r-- | tests/test-term-toggle--display.el | 2 | ||||
| -rw-r--r-- | tests/testutil-ghostel-buffers.el | 10 | ||||
| -rw-r--r-- | themes/WIP-theme.el | 26 | ||||
| -rw-r--r-- | todo.org | 8 |
14 files changed, 713 insertions, 287 deletions
@@ -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")))) @@ -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. |
