;;; 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 "" #'cj/term-toggle) (keymap-set eat-semi-char-mode-map "C-;" cj/custom-keymap) (keymap-set eat-mode-map "" #'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 "" #'cj/term-toggle) (provide 'eat-config) ;;; eat-config.el ends here