diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-26 00:04:15 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-26 00:04:15 -0400 |
| commit | 6a9ec62ec621e982a7122425b92b874c9fea2587 (patch) | |
| tree | 6ced45113b2aad1f8683675dbb49c6122d219e8e | |
| parent | 0d5787978579980f214e4fc822c2e73fd56fa92f (diff) | |
| download | dotemacs-6a9ec62ec621e982a7122425b92b874c9fea2587.tar.gz dotemacs-6a9ec62ec621e982a7122425b92b874c9fea2587.zip | |
refactor(term): retire ghostel, migrate copy-mode and tmux-history to eat-config
Complete the EAT consolidation by removing ghostel. ai-term and F12 already run on EAT, so ghostel's only remaining users were the dashboard launcher and term-config itself. Migrate the terminal-generic pieces into eat-config: the tmux copy-mode (C-<up> enters it, the same UX and keybinding as before, since agents run EAT over tmux) and the tmux-history capture, swapping ghostel-send-string for a pty write and the mode checks to eat-mode. Repoint the dashboard "Launch Terminal" to the eshell/EAT toggle, swap the face-diagnostic terminal-mode check to eat-mode, and refresh auto-dim's comment. Delete term-config.el and its init require. EAT's default semi-char non-bound-keys already lets windmove, buffer-move, and the Emacs essentials reach the terminal. Tests retargeted; the obsolete ghostel-keymap-exceptions tests are dropped.
| -rw-r--r-- | init.el | 1 | ||||
| -rw-r--r-- | modules/auto-dim-config.el | 9 | ||||
| -rw-r--r-- | modules/dashboard-config.el | 4 | ||||
| -rw-r--r-- | modules/eat-config.el | 196 | ||||
| -rw-r--r-- | modules/face-diagnostic.el | 2 | ||||
| -rw-r--r-- | modules/term-config.el | 369 | ||||
| -rw-r--r-- | modules/weather-config.el | 11 | ||||
| -rw-r--r-- | tests/test-dashboard-config-launchers.el | 2 | ||||
| -rw-r--r-- | tests/test-term-config--f8-in-term.el | 42 | ||||
| -rw-r--r-- | tests/test-term-tmux-history.el | 323 |
10 files changed, 288 insertions, 671 deletions
@@ -79,7 +79,6 @@ (require 'signal-config) ;; signal client via forked signel + signal-cli (require 'eshell-config) ;; emacs shell configuration (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 EAT terminal) (require 'help-utils) ;; search: arch-wiki, devdoc, tldr, wikipedia (require 'help-config) ;; info, man, help config diff --git a/modules/auto-dim-config.el b/modules/auto-dim-config.el index a143f8fe0..efae5341b 100644 --- a/modules/auto-dim-config.el +++ b/modules/auto-dim-config.el @@ -19,11 +19,10 @@ ;; auto-dim-other-buffers-hide) live in the active theme (the generated ;; theme-studio theme) so they track theme switches. ;; -;; Terminal buffers (ghostel) do not participate in window dimming: ghostel -;; bakes its color palette into the native module per-terminal, not per-window, -;; so there is no per-window color hook to dim through (the vterm engine had -;; one via `vterm--get-color', which this module used to advise). See the -;; terminal-migration follow-up task in todo.org for revisiting this. +;; EAT terminals render in real Emacs faces and use the `default' face for the +;; terminal background, so -- unlike the old ghostel/vterm engines, which baked +;; color per-terminal with no per-window hook -- they follow the per-window +;; dimmed background like any other buffer. ;;; Code: diff --git a/modules/dashboard-config.el b/modules/dashboard-config.el index 96aaaf6a1..17a0e2c4a 100644 --- a/modules/dashboard-config.el +++ b/modules/dashboard-config.el @@ -21,7 +21,7 @@ (eval-when-compile (require 'undead-buffers)) (declare-function cj/make-buffer-undead "undead-buffers" (string)) (autoload 'cj/make-buffer-undead "undead-buffers" nil t) -(declare-function ghostel "ghostel" (&optional arg)) +(declare-function cj/term-toggle "eat-config") ;; ------------------------------ Declarations ------------------------------- ;; These functions and variables belong to lazily-loaded packages or to other @@ -137,7 +137,7 @@ Adjust this if the title doesn't appear centered under the banner image.") (list (list "c" #'nerd-icons-faicon "nf-fa-code" "Code" "Switch Project" (lambda () (projectile-switch-project))) (list "d" #'nerd-icons-faicon "nf-fa-folder_o" "Files" "Dirvish File Manager" (lambda () (dirvish user-home-dir))) - (list "t" #'nerd-icons-devicon "nf-dev-terminal" "Terminal" "Launch Terminal" (lambda () (ghostel))) + (list "t" #'nerd-icons-devicon "nf-dev-terminal" "Terminal" "Launch Terminal" (lambda () (cj/term-toggle))) (list "a" #'nerd-icons-mdicon "nf-md-calendar" "Agenda" "Main Org Agenda" (lambda () (cj/main-agenda-display))) (list "r" #'nerd-icons-faicon "nf-fa-rss_square" "Feeds" "Elfeed Feed Reader" (lambda () (cj/elfeed-open))) (list "b" #'nerd-icons-codicon "nf-cod-library" "Books" "Calibre Ebook Reader" (lambda () (calibredb))) diff --git a/modules/eat-config.el b/modules/eat-config.el index 7f3eab69f..d08fb91e6 100644 --- a/modules/eat-config.el +++ b/modules/eat-config.el @@ -20,6 +20,7 @@ ;;; Code: +(require 'keybindings) (require 'cj-window-geometry-lib) (require 'cj-window-toggle-lib) @@ -226,5 +227,200 @@ terminal. ai-term's ghostel agent buffers are managed separately via M-SPC." (keymap-global-set "<f12>" #'cj/term-toggle) +;; ------------------- terminal copy mode + tmux history ----------------------- +;; Carried over from the ghostel era for the EAT agent terminals (ai-term). +;; Agents run EAT over tmux, so copy-mode is tmux's own copy-mode -- the same UX +;; ghostel-over-tmux had. C-<up> enters it and scrolls up in one stroke; C-; x c +;; enters it via the menu, and C-; x h grabs the whole pane history into a buffer. + +(declare-function cj/register-prefix-map "keybindings") +(declare-function eat-emacs-mode "eat") +(defvar eat--semi-char-mode) +(defvar eat--char-mode) +(defvar eat--line-mode) + +(defun cj/--term-send-string (string) + "Send STRING to the current terminal buffer's process (the pty)." + (let ((proc (get-buffer-process (current-buffer)))) + (when (process-live-p proc) + (process-send-string proc string)))) + +(defun cj/term--tmux-output (&rest args) + "Run tmux with ARGS and return its stdout. +Signal `user-error' when tmux exits with a non-zero status." + (with-temp-buffer + (let ((exit-code (apply #'process-file "tmux" nil t nil args))) + (unless (zerop exit-code) + (user-error "tmux failed: %s" (string-trim (buffer-string)))) + (buffer-string)))) + +(defun cj/term--tmux-pane-id-for-tty (tty) + "Return the tmux pane id for client TTY." + (let* ((output (cj/term--tmux-output + "list-clients" "-F" "#{client_tty}\t#{pane_id}")) + (lines (split-string output "\n" t)) + (match (seq-find + (lambda (line) + (let ((fields (split-string line "\t"))) + (equal (car fields) tty))) + lines))) + (unless match + (user-error "No tmux client found for terminal tty %s" tty)) + (cadr (split-string match "\t")))) + +(defun cj/term--tmux-capture-pane (pane-id) + "Return full joined tmux history for PANE-ID." + (cj/term--tmux-output + "capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" pane-id)) + +(defun cj/term--current-tmux-pane-id () + "Return the tmux pane id for the current EAT terminal buffer." + (unless (derived-mode-p 'eat-mode) + (user-error "Current buffer is not an EAT terminal")) + (let* ((proc (get-buffer-process (current-buffer))) + (tty (and proc (process-tty-name proc)))) + (unless (and tty (not (string-empty-p tty))) + (user-error "Could not determine terminal tty")) + (cj/term--tmux-pane-id-for-tty tty))) + +(defvar-local cj/term-tmux-history--origin-buffer nil + "Buffer active before opening the tmux history buffer.") +(defvar-local cj/term-tmux-history--origin-window nil + "Window active before opening the tmux history buffer.") +(defvar-local cj/term-tmux-history--origin-point nil + "Point in the origin buffer before opening the tmux history buffer.") + +(defun cj/term-tmux-history-quit () + "Quit tmux history and return to its origin buffer." + (interactive) + (let ((history-buffer (current-buffer)) + (origin-buffer cj/term-tmux-history--origin-buffer) + (origin-window cj/term-tmux-history--origin-window) + (origin-point cj/term-tmux-history--origin-point)) + (when (buffer-live-p origin-buffer) + (if (window-live-p origin-window) + (progn + (set-window-buffer origin-window origin-buffer) + (select-window origin-window)) + (pop-to-buffer origin-buffer)) + (with-current-buffer origin-buffer + (when (integer-or-marker-p origin-point) + (goto-char origin-point)))) + (when (buffer-live-p history-buffer) + (kill-buffer history-buffer)))) + +(defvar-keymap cj/term-tmux-history-mode-map + :doc "Keymap for `cj/term-tmux-history-mode'. +M-w copies the active region without leaving the buffer; C-g, <escape>, or q +returns to the terminal without copying. RET is left unbound." + "M-w" #'kill-ring-save + "C-g" #'cj/term-tmux-history-quit + "<escape>" #'cj/term-tmux-history-quit + "q" #'cj/term-tmux-history-quit) + +(define-derived-mode cj/term-tmux-history-mode special-mode "Tmux History" + "Mode for copying captured tmux pane history with normal Emacs keys." + (setq-local truncate-lines t) + (goto-address-mode 1)) + +(defun cj/term-tmux-history () + "Open full tmux pane history in a temporary Emacs buffer. + +The history buffer uses normal Emacs navigation and selection. `M-w' copies +the active region and stays open, so several pieces can be copied in a row; +`q', `<escape>', or `C-g' returns point to the terminal buffer that launched +it. The history view replaces the origin terminal buffer in the same window." + (interactive) + (let* ((origin-buffer (current-buffer)) + (origin-window (selected-window)) + (origin-point (point)) + (pane-id (cj/term--current-tmux-pane-id)) + (history (cj/term--tmux-capture-pane pane-id)) + (buffer (get-buffer-create + (format "*terminal tmux history: %s*" (buffer-name origin-buffer))))) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (erase-buffer) + (insert history)) + (cj/term-tmux-history-mode) + (setq-local cj/term-tmux-history--origin-buffer origin-buffer) + (setq-local cj/term-tmux-history--origin-window origin-window) + (setq-local cj/term-tmux-history--origin-point origin-point) + (goto-char (point-max))) + (switch-to-buffer buffer))) + +(defun cj/term--in-tmux-p () + "Return non-nil when the current EAT buffer has a tmux client attached. +Lookup errors (not eat-mode, no tty, no client, tmux absent) are treated as +nil so callers can use this as a cheap boolean predicate." + (and (derived-mode-p 'eat-mode) + (condition-case _ + (and (cj/term--current-tmux-pane-id) t) + (error nil)))) + +(defun cj/--term-in-emacs-mode-p () + "Return non-nil when the current EAT buffer is in emacs (navigation) mode. +EAT has no dedicated emacs-mode flag; emacs mode is the absence of the +semi-char, char, and line input modes." + (and (derived-mode-p 'eat-mode) + (not (or (bound-and-true-p eat--semi-char-mode) + (bound-and-true-p eat--char-mode) + (bound-and-true-p eat--line-mode))))) + +(defun cj/term-copy-mode-dwim () + "Enter copy-mode using the engine appropriate to this terminal. + +When tmux is attached (an agent terminal), write tmux's prefix sequence (C-b [) +into the pty so the user lands in tmux's copy-mode with the full pane history, +then C-a to land the cursor at column 0 so scrolling up runs up the left edge. +Without tmux, falls through to EAT's emacs mode (a navigable view of the +scrollback) and moves point to the start of the line." + (interactive) + (if (cj/term--in-tmux-p) + (cj/--term-send-string "\C-b[\C-a") + (eat-emacs-mode) + (beginning-of-line))) + +(defun cj/term--tmux-pane-in-copy-mode-p (pane-id) + "Return non-nil when tmux PANE-ID is currently displaying a mode. +tmux's `pane_in_mode' is 1 while a pane is in any mode; copy-mode is the only +mode this config enters. tmux failures are treated as nil." + (condition-case nil + (equal "1" (string-trim + (cj/term--tmux-output + "display-message" "-p" "-t" pane-id "#{pane_in_mode}"))) + (error nil))) + +(defun cj/term-copy-mode-up () + "Enter copy-mode if needed, then scroll up one line. +A single C-<up> lands in the terminal's copy-mode already moving up. Pressed +again while already in copy-mode it just moves up another line, so it never +re-enters and resets the cursor. In tmux, writes the up-arrow escape into the +pty; without tmux, moves point up in EAT's emacs-mode buffer." + (interactive) + (let ((pane (ignore-errors (cj/term--current-tmux-pane-id)))) + (cond + (pane + (unless (cj/term--tmux-pane-in-copy-mode-p pane) + (cj/term-copy-mode-dwim)) + (cj/--term-send-string "\e[A")) + (t + (unless (cj/--term-in-emacs-mode-p) + (cj/term-copy-mode-dwim)) + (forward-line -1))))) + +;; The C-; x terminal prefix (copy-mode, tmux history, the F12 toggle). C-<up> +;; enters copy-mode + scrolls in one stroke; bound in EAT's semi-char map so it +;; reaches Emacs from inside an agent terminal. +(defvar-keymap cj/term-map + :doc "Personal terminal command map.") +(cj/register-prefix-map "x" cj/term-map) +(keymap-set cj/term-map "c" #'cj/term-copy-mode-dwim) +(keymap-set cj/term-map "h" #'cj/term-tmux-history) +(keymap-set cj/term-map "t" #'cj/term-toggle) + +(with-eval-after-load 'eat + (keymap-set eat-semi-char-mode-map "C-<up>" #'cj/term-copy-mode-up)) + (provide 'eat-config) ;;; eat-config.el ends here diff --git a/modules/face-diagnostic.el b/modules/face-diagnostic.el index a2bfe2483..6f0722099 100644 --- a/modules/face-diagnostic.el +++ b/modules/face-diagnostic.el @@ -36,7 +36,7 @@ Return one of `theme-faced', `terminal-ansi', `document-shr', or best-effort dump rather than a full provenance trace." (with-current-buffer (or buffer (current-buffer)) (cond - ((derived-mode-p 'term-mode 'comint-mode 'eshell-mode 'ghostel-mode) + ((derived-mode-p 'term-mode 'comint-mode 'eshell-mode 'eat-mode) 'terminal-ansi) ((derived-mode-p 'eww-mode 'nov-mode 'elfeed-show-mode 'mu4e-view-mode) 'document-shr) diff --git a/modules/term-config.el b/modules/term-config.el deleted file mode 100644 index 659224198..000000000 --- a/modules/term-config.el +++ /dev/null @@ -1,369 +0,0 @@ -;;; term-config.el --- Settings for ghostel and the F12 toggle -*- lexical-binding: t; coding: utf-8; -*- -;; author Craig Jennings <c@cjennings.net> - -;;; Commentary: -;; -;; Layer: 3 (Domain Workflow). -;; Category: D/P. -;; Load shape: eager. -;; Eager reason: registers terminal keymaps and the F12 toggle. -;; Top-level side effects: defines two keymaps (one under cj/custom-keymap), one -;; global key, two add-hook, package config. -;; Runtime requires: keybindings, seq, subr-x, cj-window-geometry-lib, -;; cj-window-toggle-lib. -;; Direct test load: yes (requires keybindings explicitly). -;; -;; GHOSTEL -;; ghostel is a native Emacs terminal emulator over libghostty-vt (the Ghostty -;; engine). Like a real terminal, in its default semi-char mode most keys are -;; sent to the running program; `ghostel-keymap-exceptions' lists the keys that -;; reach Emacs instead. We add C-; so the personal prefix keymap works inside -;; ghostel buffers. -;; -;; The module degrades gracefully when ghostel is unavailable (D6 of the -;; migration spec): the package installs via use-package, the native module -;; auto-downloads on first use, and ghostel emits its own warning if the module -;; cannot load. A machine without a prebuilt binary needs Zig to build it; the -;; terminal commands stay defined either way. -;; -;; Two ways to lift text out of a terminal, both with the same key story: -;; - C-; x c enters copy-mode via `cj/term-copy-mode-dwim'. When a tmux -;; client is attached (typical -- `cj/term-launch-tmux' auto-starts tmux), -;; sends tmux's prefix C-b [ then C-a, so the user lands in tmux's own -;; copy-mode with the full pane history and the cursor at column 0 (so -;; scrolling up runs up the left, not the right). Without tmux, falls back to -;; `ghostel-copy-mode' (read-only standard-Emacs navigation over the -;; scrollback; M-w copies and stays, q / C-g exit) and moves point to the -;; start of the line for the same column-0 reason. -;; - C-; x h captures the current tmux pane's full history into a temporary -;; Emacs buffer. -;; In both copy surfaces, M-w copies the active region and stays open so several -;; pieces can be grabbed in a row; C-g / q leave without copying. - -;;; Code: - -(require 'keybindings) -(require 'seq) -(require 'subr-x) -(require 'cj-window-geometry-lib) -(require 'cj-window-toggle-lib) - -(declare-function ghostel "ghostel" (&optional directory)) -(declare-function ghostel-send-string "ghostel" (string)) -(declare-function ghostel-copy-mode "ghostel" ()) -(declare-function ghostel-clear-scrollback "ghostel" ()) -(declare-function ghostel-next-prompt "ghostel" (&optional n)) -(declare-function ghostel-previous-prompt "ghostel" (&optional n)) -(declare-function ghostel-send-next-key "ghostel" ()) -(declare-function ghostel--rebuild-semi-char-keymap "ghostel" ()) -(defvar ghostel-mode-map) -(defvar ghostel-keymap-exceptions) -(defvar ghostel-buffer-name) -(defvar ghostel--input-mode) -(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. -(cj/register-prefix-map "x" cj/term-map) - -;; ----------------------------- tmux history ---------------------------------- - -(defvar-local cj/term-tmux-history--origin-buffer nil - "Buffer active before opening the tmux history buffer.") - -(defvar-local cj/term-tmux-history--origin-window nil - "Window active before opening the tmux history buffer.") - -(defvar-local cj/term-tmux-history--origin-point nil - "Point in the origin buffer before opening the tmux history buffer.") - -(defun cj/term--tmux-output (&rest args) - "Run tmux with ARGS and return its stdout. -Signal `user-error' when tmux exits with a non-zero status." - (with-temp-buffer - (let ((exit-code (apply #'process-file "tmux" nil t nil args))) - (unless (zerop exit-code) - (user-error "tmux failed: %s" (string-trim (buffer-string)))) - (buffer-string)))) - -(defun cj/term--tmux-pane-id-for-tty (tty) - "Return the tmux pane id for client TTY." - (let* ((output (cj/term--tmux-output - "list-clients" "-F" "#{client_tty}\t#{pane_id}")) - (lines (split-string output "\n" t)) - (match (seq-find - (lambda (line) - (let ((fields (split-string line "\t"))) - (equal (car fields) tty))) - lines))) - (unless match - (user-error "No tmux client found for terminal tty %s" tty)) - (cadr (split-string match "\t")))) - -(defun cj/term--tmux-capture-pane (pane-id) - "Return full joined tmux history for PANE-ID." - (cj/term--tmux-output - "capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" pane-id)) - -(defun cj/term--current-tmux-pane-id () - "Return the tmux pane id for the current ghostel buffer." - (unless (eq major-mode 'ghostel-mode) - (user-error "Current buffer is not a ghostel buffer")) - (let* ((proc (get-buffer-process (current-buffer))) - (tty (and proc (process-tty-name proc)))) - (unless (and tty (not (string-empty-p tty))) - (user-error "Could not determine terminal tty")) - (cj/term--tmux-pane-id-for-tty tty))) - -(defvar-keymap cj/term-tmux-history-mode-map - :doc "Keymap for `cj/term-tmux-history-mode'. -M-w copies the active region without leaving the buffer; C-g, <escape>, or q -returns to the terminal without copying. RET is left unbound." - "M-w" #'kill-ring-save - "C-g" #'cj/term-tmux-history-quit - "<escape>" #'cj/term-tmux-history-quit - "q" #'cj/term-tmux-history-quit) - -(define-derived-mode cj/term-tmux-history-mode special-mode "Tmux History" - "Mode for copying captured tmux pane history with normal Emacs keys." - (setq-local truncate-lines t) - (goto-address-mode 1)) - -(defun cj/term-tmux-history-quit () - "Quit tmux history and return to its origin buffer." - (interactive) - (let ((history-buffer (current-buffer)) - (origin-buffer cj/term-tmux-history--origin-buffer) - (origin-window cj/term-tmux-history--origin-window) - (origin-point cj/term-tmux-history--origin-point)) - (when (buffer-live-p origin-buffer) - (if (window-live-p origin-window) - (progn - (set-window-buffer origin-window origin-buffer) - (select-window origin-window)) - (pop-to-buffer origin-buffer)) - (with-current-buffer origin-buffer - (when (integer-or-marker-p origin-point) - (goto-char origin-point)))) - (when (buffer-live-p history-buffer) - (kill-buffer history-buffer)))) - -(defun cj/term-tmux-history () - "Open full tmux pane history in a temporary Emacs buffer. - -The history buffer uses normal Emacs navigation and selection. `M-w' -copies the active region and stays open, so several pieces can be -copied in a row; `q', `<escape>', or `C-g' returns point to the -terminal buffer that launched it. - -The history view replaces the origin terminal buffer in the same window -\(via `switch-to-buffer'), not a split or a popped-up window." - (interactive) - (let* ((origin-buffer (current-buffer)) - (origin-window (selected-window)) - (origin-point (point)) - (pane-id (cj/term--current-tmux-pane-id)) - (history (cj/term--tmux-capture-pane pane-id)) - (buffer (get-buffer-create - (format "*terminal tmux history: %s*" (buffer-name origin-buffer))))) - (with-current-buffer buffer - (let ((inhibit-read-only t)) - (erase-buffer) - (insert history)) - (cj/term-tmux-history-mode) - (setq-local cj/term-tmux-history--origin-buffer origin-buffer) - (setq-local cj/term-tmux-history--origin-window origin-window) - (setq-local cj/term-tmux-history--origin-point origin-point) - (goto-char (point-max))) - (switch-to-buffer buffer))) - -;; ----------------------------- copy mode ------------------------------------- - -(defun cj/term--in-tmux-p () - "Return non-nil when the current ghostel buffer has a tmux client attached. -Errors from the pane-id lookup (not in ghostel-mode, no tty, no matching -client, tmux not installed) are treated as nil so callers can use this as a -cheap boolean predicate." - (and (eq major-mode 'ghostel-mode) - (condition-case _ - (and (cj/term--current-tmux-pane-id) t) - (error nil)))) - -(defun cj/term-copy-mode-dwim () - "Enter copy-mode using the engine appropriate to this terminal. - -When tmux is attached, write tmux's default prefix sequence (C-b [) into the -pty so the user lands in tmux's copy-mode with the full pane history, then -C-a to land the cursor at the start of the line. Without the trailing C-a -the copy cursor inherits the live column (far right after a prompt) and -scrolling up runs up the right edge; tmux's emacs copy-mode binds C-a to -start-of-line, so column 0 makes it run up the left. Without tmux, falls -through to `ghostel-copy-mode' (a read-only standard-Emacs view of the -scrollback; M-w copies and stays, q / C-g exit), then moves point to the -start of the line for the same column-0 reason." - (interactive) - (if (cj/term--in-tmux-p) - (ghostel-send-string "\C-b[\C-a") - (ghostel-copy-mode) - (beginning-of-line))) - -;; ----------------------------- copy-mode scroll ------------------------------ -;; -;; C-<up> both enters copy-mode and scrolls up one line, so a single stroke -;; lands in the scrollback already moving the right way. It joins -;; `ghostel-keymap-exceptions' so it reaches Emacs instead of the pty. Only the -;; up gesture is bound: C-<left>/<right> are readline word-motion at the shell -;; prompt and must pass through, and the other directions have no copy-mode use. -;; Pressed again while already in copy-mode it just moves up -- re-entering would -;; reset the cursor (tmux's prefix-[ + C-a, or ghostel's toggle exiting). - -(defun cj/term--tmux-pane-in-copy-mode-p (pane-id) - "Return non-nil when tmux PANE-ID is currently displaying a mode. -tmux's `pane_in_mode' is 1 while a pane is in any mode; copy-mode is the only -mode this config enters. tmux failures are treated as nil." - (condition-case nil - (equal "1" (string-trim - (cj/term--tmux-output - "display-message" "-p" "-t" pane-id "#{pane_in_mode}"))) - (error nil))) - -(defun cj/term-copy-mode-up () - "Enter copy-mode if needed, then scroll up one line. -A single C-<up> lands in the terminal's copy-mode already moving up. Pressed -again while already in copy-mode it just moves up another line, so it never -re-enters and resets the cursor. In tmux, writes the up-arrow escape sequence -into the pty; without tmux, moves point up in the `ghostel-copy-mode' buffer." - (interactive) - (let ((pane (ignore-errors (cj/term--current-tmux-pane-id)))) - (cond - (pane - (unless (cj/term--tmux-pane-in-copy-mode-p pane) - (cj/term-copy-mode-dwim)) - (ghostel-send-string "\e[A")) - (t - (unless (eq (bound-and-true-p ghostel--input-mode) 'copy) - (cj/term-copy-mode-dwim)) - (forward-line -1))))) - -;; ----------------------------- ghostel package ------------------------------- - -(defun cj/term-launch-tmux () - "Auto-launch tmux in a ghostel buffer unless already inside tmux. - -Skipped when `cj/--ai-term-suppress-tmux' is non-nil so the AI-agent flow can -run its own project-named tmux session instead of a bare, auto-named one. -`bound-and-true-p' keeps this safe whether or not ai-term.el is loaded." - (let ((proc (get-buffer-process (current-buffer)))) - (when (and proc - (not (getenv "TMUX")) - (not (bound-and-true-p cj/--ai-term-suppress-tmux))) - (ghostel-send-string "tmux\n")))) - -(use-package ghostel - ;; PINNED at module 0.33.0 (ghostel-20260604.2049, the last pre-rework June-4 - ;; build), installed directly into elpa/ rather than from MELPA. The 0.35.0-0.35.2 - ;; native-PTY rework (worker threads + mutex-outside-read-loop) hard-crashes the - ;; whole Emacs process when a ghostel buffer is displayed: on Linux/glibc a - ;; SIGSETXID handler calls malloc while the main thread holds the arena lock - ;; (ghostel upstream #422); on macOS a recursive os_unfair_lock via - ;; run_window_change_functions (#423). `:ensure t' is satisfied by the present - ;; 0.33.0 dir and will NOT upgrade it -- do NOT `package-upgrade' ghostel until - ;; #422/#423 are fixed upstream, or it returns to the crashing 0.35.x. - :ensure t - :commands (ghostel) - :init - ;; These keys must reach Emacs (not the terminal program) inside ghostel - ;; buffers. In semi-char mode ghostel forwards every key NOT in - ;; `ghostel-keymap-exceptions' to the pty, and `ghostel-semi-char-mode-map' - ;; is rebuilt from that list by `ghostel--rebuild-semi-char-keymap' -- - ;; `add-to-list' alone updates the list but not the already-built map, so the - ;; rebuild is what actually lets the key through to `ghostel-mode-map' / the - ;; global map. C-; and F12 are the prefix + toggle; the modified arrows are - ;; windmove (S-arrows, focus), buffer-move (C-M-arrows, swap), and copy-mode - ;; entry (C-<up> only, via `cj/term-copy-mode-up'), which the ai-term workflow - ;; expects to work from inside an agent buffer. C-<left>/<right> deliberately - ;; stay forwarding so readline word-motion works at the shell prompt. F8 and - ;; F10 are global bindings (org agenda, music-playlist toggle) that reach - ;; Emacs by falling through to the global map once the semi-char map stops - ;; forwarding them. (Server shutdown moved off C-F10 to C-x C, which is - ;; deliberately left forwarding to the terminal program inside an agent - ;; buffer.) - (with-eval-after-load 'ghostel - (dolist (key '("C-;" "<f8>" "<f12>" "<f10>" - "S-<up>" "S-<down>" "S-<left>" "S-<right>" - "C-M-<up>" "C-M-<down>" "C-M-<left>" "C-M-<right>" - "C-<up>")) - (add-to-list 'ghostel-keymap-exceptions key)) - (ghostel--rebuild-semi-char-keymap)) - :hook - ((ghostel-mode . cj/turn-off-chrome-for-term) - (ghostel-mode . cj/term-launch-tmux)) - :custom - (ghostel-kill-buffer-on-exit t) - ;; Auto-download the prebuilt native module on first launch instead of the - ;; default `ask' prompt -- it fetches the platform release asset from GitHub - ;; (for the pinned 0.33.0 source this resolves to the matching v0.33.0 module). - ;; The compile-from-source fallback also works here: zig 0.15.2 is installed at - ;; /usr/local/bin/zig (see M-x ghostel-module-compile). - (ghostel-module-auto-install 'download) - ;; Byte analog of the prior 100000-line vterm setting (~100 bytes/line) -- D7. - (ghostel-max-scrollback (* 10 1024 1024))) - -;; ----------------------------- prefix menu ----------------------------------- - -(keymap-set cj/term-map "c" #'cj/term-copy-mode-dwim) -(keymap-set cj/term-map "h" #'cj/term-tmux-history) -(keymap-set cj/term-map "l" #'ghostel-clear-scrollback) -(keymap-set cj/term-map "N" #'ghostel) -(keymap-set cj/term-map "n" #'ghostel-next-prompt) -(keymap-set cj/term-map "p" #'ghostel-previous-prompt) -(keymap-set cj/term-map "q" #'ghostel-send-next-key) -(keymap-set cj/term-map "t" #'cj/term-toggle) - -(defun cj/term-send-C-SPC () - "Forward C-SPC (NUL) to the terminal instead of setting an Emacs mark. - -ghostel forwards the `C-@' event but not the distinct `C-SPC' event GUI -Emacs produces, so a bare C-SPC in a ghostel buffer falls through to the -global `set-mark-command'. That sets an Emacs region in the terminal buffer -that follows point as output streams (a stuck \"selection\" C-g / Escape -can't clear) and, worse, never reaches tmux -- so tmux copy-mode's -begin-selection (C-Space) never starts and M-w then copies nothing. -Forwarding NUL makes C-Space behave like a terminal key." - (interactive) - (ghostel-send-string "\C-@")) - -(defun cj/term-install-keys () - "Make `C-;' resolve as the personal keymap inside ghostel buffers, bind the -F12 toggle, forward C-SPC so it reaches the terminal (see -`cj/term-send-C-SPC'), and bind C-<up> to enter copy-mode and scroll up." - (when (boundp 'ghostel-mode-map) - (keymap-set ghostel-mode-map "C-;" cj/custom-keymap) - (keymap-set ghostel-mode-map "<f12>" #'cj/term-toggle) - (keymap-set ghostel-mode-map "C-SPC" #'cj/term-send-C-SPC) - (keymap-set ghostel-mode-map "C-<up>" #'cj/term-copy-mode-up))) - -(cj/term-install-keys) -(with-eval-after-load 'ghostel - (cj/term-install-keys)) - -(with-eval-after-load 'which-key - (which-key-add-key-based-replacements - "C-; x" "terminal menu" - "C-; x c" "copy mode (tmux/ghostel)" - "C-; x h" "tmux scrollback history" - "C-; x l" "clear scrollback" - "C-; x N" "new terminal" - "C-; x n" "next prompt" - "C-; x p" "previous prompt" - "C-; x q" "send next key to terminal" - "C-; x t" "toggle terminal")) - -(provide 'term-config) -;;; term-config.el ends here. diff --git a/modules/weather-config.el b/modules/weather-config.el index 93b0a6148..04531350f 100644 --- a/modules/weather-config.el +++ b/modules/weather-config.el @@ -32,7 +32,18 @@ ("M-S-w" . wttrin) ;; was M-W, overrides kill-ring-save :config (setopt wttrin-unit-system "u") + ;; Drop the "Follow @igor_chubin for wttr.in updates" footer. "F" is the + ;; wttr.in flag for "no Follow line"; everything else (forecast, header, + ;; colors) is unchanged. + (setopt wttrin-display-options "F") (setopt wttrin-favorite-location "New Orleans, LA") + ;; Higher-accuracy geolocation via the whereami WiFi-scan script (Google-backed), + ;; far better than IP behind a VPN or cellular hotspot. Used by the picker's + ;; "Current location (detect)" entry; wttrin falls back to its IP provider if the + ;; command is missing or fails. setq (not setopt): wttrin-geolocation-command is + ;; defined in the lazily-loaded wttrin-geolocation sub-module, so it may be unbound + ;; at :config time; the later defcustom won't clobber an already-set value. + (setq wttrin-geolocation-command "/home/cjennings/.local/bin/whereami --json") (setopt wttrin-mode-line-refresh-interval (* 30 60)) ;; thirty minutes (setq wttrin-default-locations '( "New Orleans, LA" diff --git a/tests/test-dashboard-config-launchers.el b/tests/test-dashboard-config-launchers.el index a9a871979..e7e5dcd53 100644 --- a/tests/test-dashboard-config-launchers.el +++ b/tests/test-dashboard-config-launchers.el @@ -86,7 +86,7 @@ Slack, Linear, and Signal sharing the last row." (let ((map (make-sparse-keymap)) (calls nil)) (cl-letf (((symbol-function 'projectile-switch-project) (lambda (&rest _) (push 'code calls))) ((symbol-function 'dirvish) (lambda (&rest _) (push 'files calls))) - ((symbol-function 'ghostel) (lambda (&rest _) (push 'term calls))) + ((symbol-function 'cj/term-toggle) (lambda (&rest _) (push 'term calls))) ((symbol-function 'cj/main-agenda-display) (lambda (&rest _) (push 'agenda calls))) ((symbol-function 'cj/elfeed-open) (lambda (&rest _) (push 'feeds calls))) ((symbol-function 'calibredb) (lambda (&rest _) (push 'books calls))) diff --git a/tests/test-term-config--f8-in-term.el b/tests/test-term-config--f8-in-term.el deleted file mode 100644 index 6cee4ff46..000000000 --- a/tests/test-term-config--f8-in-term.el +++ /dev/null @@ -1,42 +0,0 @@ -;;; test-term-config--f8-in-term.el --- F8 reaches Emacs from inside a ghostel buffer -*- lexical-binding: t; -*- - -;;; Commentary: -;; <f8> is a global binding (`cj/main-agenda-display', set in org-agenda-config). -;; ghostel's semi-char mode forwards every key NOT in `ghostel-keymap-exceptions' -;; to the terminal program, so a plain <f8> typed while point is in a ghostel -;; buffer would be sent to the program instead of opening the agenda. Unlike the -;; F9 family, F8 is NOT re-bound in `ghostel-mode-map' -- it simply falls through -;; to the global map once the semi-char map stops forwarding it, so the only -;; wiring term-config.el adds is the keymap-exceptions entry plus the rebuild. -;; These tests require ghostel (so term-config's `with-eval-after-load' fires) -;; BEFORE term-config, then confirm the exception landed and the rebuilt -;; semi-char map no longer forwards <f8>. `(require 'ghostel)' does not load the -;; native module, so this stays light. - -;;; Code: - -(require 'ert) -(require 'package) - -(setq package-user-dir (expand-file-name "elpa" user-emacs-directory)) -(package-initialize) -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ghostel) -(require 'term-config) - -(ert-deftest test-term-config-f8-in-keymap-exceptions () - "Regression: <f8> is in `ghostel-keymap-exceptions' so semi-char mode lets it -reach Emacs instead of forwarding it to the terminal program. This is what lets -the global agenda binding work from inside a ghostel buffer." - (should (member "<f8>" ghostel-keymap-exceptions))) - -(ert-deftest test-term-config-f8-not-forwarded-by-semi-char-map () - "Regression: the rebuilt semi-char map must no longer forward <f8> to the pty. -`add-to-list' updates the exceptions list but not the already-built map -- only -`ghostel--rebuild-semi-char-keymap' (run in term-config's :init) drops the -forwarding binding so <f8> falls through to the global agenda command." - (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f8>") - 'ghostel--send-event))) - -(provide 'test-term-config--f8-in-term) -;;; test-term-config--f8-in-term.el ends here diff --git a/tests/test-term-tmux-history.el b/tests/test-term-tmux-history.el index 0ea7cf37d..08d39e5bf 100644 --- a/tests/test-term-tmux-history.el +++ b/tests/test-term-tmux-history.el @@ -1,14 +1,13 @@ -;;; test-term-tmux-history.el --- Tests for term-config tmux history + menu UX -*- lexical-binding: t; -*- +;;; test-term-tmux-history.el --- Tests for the EAT terminal copy-mode + tmux history -*- lexical-binding: t; -*- ;;; Commentary: -;; Exercises the term-config (ghostel) terminal UX: the Emacs-owned tmux -;; history buffer, the copy-mode-dwim engine pick, the tmux pane-id / -;; attached-client predicates, and the C-; x menu bindings. +;; Exercises the terminal UX carried into eat-config for the EAT agent +;; terminals: the Emacs-owned tmux history buffer, the copy-mode-dwim engine +;; pick, the tmux pane-id / attached-client predicates, and the C-; x menu +;; bindings. Agents run EAT over tmux, so copy-mode is tmux's own copy-mode. ;; -;; ghostel is required (which defines `ghostel-mode-map' / -;; `ghostel-keymap-exceptions' and lets term-config's `with-eval-after-load' -;; fire) before term-config. `(require 'ghostel)' does not load the native -;; module; tmux is mocked via `process-file', so nothing spawns. +;; eat is required (so eat-config's `with-eval-after-load' fires for the C-<up> +;; bind) before eat-config; tmux is mocked via `process-file', so nothing spawns. ;;; Code: @@ -21,8 +20,8 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) (setq load-prefer-newer t) -(require 'ghostel) -(require 'term-config) +(require 'eat) +(require 'eat-config) (require 'testutil-ghostel-buffers) (defmacro test-term-tmux-history--with-tmux-mock (responses &rest body) @@ -51,6 +50,8 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." exit-code)))) ,@body))) +;;; tmux helpers + (ert-deftest test-term-tmux-history--pane-id-for-tty-matches-client () "Normal: current terminal pty maps to the active pane for that tmux client." (test-term-tmux-history--with-tmux-mock @@ -66,9 +67,32 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." (should (equal (cj/term--tmux-capture-pane "%8") "first line\nsecond line\n")))) +(ert-deftest test-term-current-tmux-pane-id-rejects-non-eat-buffer () + "Error: pane-id lookup refuses a buffer that is not in `eat-mode'." + (with-temp-buffer + (should-error (cj/term--current-tmux-pane-id) :type 'user-error))) + +(ert-deftest test-term-current-tmux-pane-id-accepts-agent-named-buffer () + "Normal: an agent-named eat buffer resolves by process TTY, not buffer name." + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process &rest _) "/dev/pts/8"))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/1\t%1\n/dev/pts/8\t%8\n")) + (should (equal (cj/term--current-tmux-pane-id) "%8"))))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +;;; tmux history buffer + (ert-deftest test-term-tmux-history-open-renders-read-only-history-buffer () - "Normal: command renders tmux history in a normal Emacs buffer." - (let ((origin (cj/test--make-fake-ghostel-buffer "*test-term-history-origin*"))) + "Normal: the command renders tmux history in a normal Emacs buffer." + (let ((origin (cj/test--make-fake-eat-buffer "*test-term-history-origin*"))) (unwind-protect (save-window-excursion (switch-to-buffer origin) @@ -90,41 +114,8 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." (when (buffer-live-p origin) (kill-buffer origin))))) -(ert-deftest test-term-tmux-history-replaces-origin-buffer-in-same-window () - "Normal: the history view replaces the origin in the selected window. - -`cj/term-tmux-history' uses `switch-to-buffer' so reading scrollback keeps -the terminal's frame slot rather than splitting or popping a new window." - (let ((origin (cj/test--make-fake-ghostel-buffer "*test-term-history-inplace*"))) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (switch-to-buffer origin) - (let ((win (selected-window))) - (should (eq (window-buffer win) origin)) - (should (one-window-p)) - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process &rest _) "/dev/pts/8"))) - (test-term-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/8\t%8\n") - (("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0 - "scrollback line\n")) - (cj/term-tmux-history))) - (should (one-window-p)) - (should (eq (selected-window) win)) - (should (string-prefix-p - "*terminal tmux history:" - (buffer-name (window-buffer win)))))) - (cj/test--kill-buffers-matching-prefix "*terminal tmux history") - (when (buffer-live-p origin) - (kill-buffer origin))))) - (ert-deftest test-term-tmux-history-quit-returns-to-origin () - "Normal: q / <escape> / C-g (cj/term-tmux-history-quit) kills the history -buffer and restores the origin buffer, window, and point." + "Normal: quit kills the history buffer and restores origin buffer/window/point." (let ((origin (get-buffer-create "*test-term-history-return*"))) (unwind-protect (let ((history (get-buffer-create "*terminal tmux history: test*"))) @@ -149,10 +140,8 @@ buffer and restores the origin buffer, window, and point." (kill-buffer origin))))) (ert-deftest test-term-tmux-history-mode-keymap () - "Normal: in the history buffer M-w copies without quitting; q, <escape>, -and C-g quit back to the terminal; RET is left unbound (no special exit)." - (should (eq (keymap-lookup cj/term-tmux-history-mode-map "M-w") - #'kill-ring-save)) + "Normal: M-w copies; q/<escape>/C-g quit; RET is left unbound." + (should (eq (keymap-lookup cj/term-tmux-history-mode-map "M-w") #'kill-ring-save)) (should (eq (keymap-lookup cj/term-tmux-history-mode-map "q") #'cj/term-tmux-history-quit)) (should (eq (keymap-lookup cj/term-tmux-history-mode-map "<escape>") @@ -161,50 +150,11 @@ and C-g quit back to the terminal; RET is left unbound (no special exit)." #'cj/term-tmux-history-quit)) (should-not (keymap-lookup cj/term-tmux-history-mode-map "RET"))) -(ert-deftest test-term-keymap-includes-history-and-copy-bindings () - "Normal: the personal terminal map owns the high-level UX commands, and C-; -reaches Emacs inside ghostel buffers so the prefix works there." - (should (member "C-;" ghostel-keymap-exceptions)) - (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/term-tmux-history)) - (should (eq (keymap-lookup cj/custom-keymap "x c") #'cj/term-copy-mode-dwim)) - (should (equal (keymap-lookup ghostel-mode-map "C-;") cj/custom-keymap)) - (should (eq (keymap-lookup ghostel-mode-map "C-; x h") #'cj/term-tmux-history)) - (should (eq (keymap-lookup ghostel-mode-map "C-; x c") #'cj/term-copy-mode-dwim))) - -(ert-deftest test-term-keymap-prompt-navigation () - "Normal: n/p navigate prompts, capital N creates a new terminal buffer." - (should (eq (keymap-lookup cj/custom-keymap "x n") #'ghostel-next-prompt)) - (should (eq (keymap-lookup cj/custom-keymap "x p") #'ghostel-previous-prompt)) - (should (eq (keymap-lookup cj/custom-keymap "x N") #'ghostel))) - -(ert-deftest test-term-current-tmux-pane-id-rejects-non-ghostel-buffer () - "Error: pane-id lookup refuses a buffer that is not in `ghostel-mode'." - (with-temp-buffer - (should-error (cj/term--current-tmux-pane-id) :type 'user-error))) - -(ert-deftest test-term-current-tmux-pane-id-accepts-agent-named-buffer () - "Normal: an agent-named ghostel buffer resolves by process TTY. - -The pane lookup keys off the live process TTY, never the buffer name, so a -buffer named `agent [repo]' (ai-term.el's naming) resolves like any other -ghostel-mode terminal." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process &rest _) "/dev/pts/8"))) - (test-term-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/1\t%1\n/dev/pts/8\t%8\n")) - (should (equal (cj/term--current-tmux-pane-id) "%8"))))) - (when (buffer-live-p agent) - (kill-buffer agent))))) +;;; in-tmux-p predicate (ert-deftest test-term-in-tmux-p-true-when-client-attached () "Normal: predicate returns t when tmux reports a client for our tty." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))) (unwind-protect (with-current-buffer agent (cl-letf (((symbol-function 'get-buffer-process) @@ -218,25 +168,18 @@ ghostel-mode terminal." (when (buffer-live-p agent) (kill-buffer agent))))) -(ert-deftest test-term-in-tmux-p-nil-when-no-matching-client () - "Boundary: predicate returns nil when tmux runs but our tty has no client." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process &rest _) "/dev/pts/8"))) - (test-term-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/1\t%1\n")) - (should-not (cj/term--in-tmux-p))))) - (when (buffer-live-p agent) - (kill-buffer agent))))) +(ert-deftest test-term-in-tmux-p-nil-when-not-eat-mode () + "Boundary: predicate refuses non-eat buffers without calling tmux." + (with-temp-buffer + (let ((tmux-called nil)) + (cl-letf (((symbol-function 'process-file) + (lambda (&rest _) (setq tmux-called t) 0))) + (should-not (cj/term--in-tmux-p)) + (should-not tmux-called))))) (ert-deftest test-term-in-tmux-p-nil-when-tmux-fails () "Error: predicate swallows tmux failures and returns nil." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))) (unwind-protect (with-current-buffer agent (cl-letf (((symbol-function 'get-buffer-process) @@ -250,117 +193,33 @@ ghostel-mode terminal." (when (buffer-live-p agent) (kill-buffer agent))))) -(ert-deftest test-term-in-tmux-p-nil-when-not-ghostel-mode () - "Boundary: predicate refuses non-ghostel buffers without calling tmux." - (with-temp-buffer - (let ((tmux-called nil)) - (cl-letf (((symbol-function 'process-file) - (lambda (&rest _) (setq tmux-called t) 0))) - (should-not (cj/term--in-tmux-p)) - (should-not tmux-called))))) +;;; copy-mode (tmux path -- the agent terminal case) (ert-deftest test-term-copy-mode-dwim-sends-tmux-prefix-when-attached () - "Normal: with tmux attached, dwim writes C-b [ then C-a into the pty so -tmux enters its own copy-mode and lands the cursor at the start of the -line. Without the trailing C-a the cursor inherits the live column (far -right after a prompt) and scrolling up runs up the right edge; start-of-line -puts it at column 0 so it runs up the left." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")) - (sent nil) - (copy-mode-called nil)) + "Normal: with tmux attached, dwim writes C-b [ then C-a into the pty so tmux +enters copy-mode with the cursor at column 0." + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")) + (sent nil)) (unwind-protect (with-current-buffer agent (cl-letf (((symbol-function 'get-buffer-process) (lambda (_buffer) 'fake-process)) ((symbol-function 'process-tty-name) (lambda (_process &rest _) "/dev/pts/8")) - ((symbol-function 'ghostel-send-string) - (lambda (s) (push s sent))) - ((symbol-function 'ghostel-copy-mode) - (lambda () (setq copy-mode-called t)))) + ((symbol-function 'cj/--term-send-string) + (lambda (s) (push s sent)))) (test-term-tmux-history--with-tmux-mock '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 "/dev/pts/8\t%8\n")) (cj/term-copy-mode-dwim) - (should (equal sent '("\C-b[\C-a"))) - (should-not copy-mode-called)))) - (when (buffer-live-p agent) - (kill-buffer agent))))) - -(ert-deftest test-term-copy-mode-dwim-falls-back-without-tmux () - "Boundary: without tmux, dwim calls `ghostel-copy-mode' then moves point -to the start of the line and sends nothing to the pty. The -`beginning-of-line' must run after `ghostel-copy-mode' so it repositions -inside the copy view; column 0 keeps the cursor on the left edge while -scrolling, parity with the tmux branch's trailing C-a." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")) - (sent nil) - (dwim-order nil)) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process &rest _) "/dev/pts/8")) - ((symbol-function 'ghostel-send-string) - (lambda (s) (push s sent))) - ((symbol-function 'ghostel-copy-mode) - (lambda () (push 'copy-mode dwim-order))) - ((symbol-function 'beginning-of-line) - (lambda (&optional _n) (push 'beginning-of-line dwim-order)))) - (test-term-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 1 - "no server running")) - (cj/term-copy-mode-dwim) - (should-not sent) - (should (equal (reverse dwim-order) '(copy-mode beginning-of-line)))))) + (should (equal sent '("\C-b[\C-a")))))) (when (buffer-live-p agent) (kill-buffer agent))))) -(ert-deftest test-term-prefix-and-f12-in-keymap-exceptions () - "Regression: C-; and F12 are in `ghostel-keymap-exceptions' and the rebuilt -semi-char map no longer forwards them to the pty, so the prefix keymap and the -F12 toggle reach Emacs inside ghostel buffers." - (dolist (key '("C-;" "<f12>")) - (should (member key ghostel-keymap-exceptions))) - (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f12>") - 'ghostel--send-event))) - -(ert-deftest test-term-window-nav-keys-in-keymap-exceptions () - "Regression: windmove (S-arrows) and buffer-move (C-M-arrows) are in -`ghostel-keymap-exceptions' so they reach Emacs from inside a ghostel buffer -instead of being forwarded to the terminal program." - (dolist (key '("S-<up>" "S-<down>" "S-<left>" "S-<right>" - "C-M-<up>" "C-M-<down>" "C-M-<left>" "C-M-<right>")) - (should (member key ghostel-keymap-exceptions))) - (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "C-M-<left>") - 'ghostel--send-event))) - -(ert-deftest test-term-f10-music-in-keymap-exceptions () - "Regression: F10 (music playlist toggle) is in `ghostel-keymap-exceptions' -so it reaches Emacs from inside a ghostel buffer instead of being forwarded -to the terminal program. It is a global binding, so dropping it from the -semi-char map lets the lookup fall through to the global map. Server -shutdown moved off C-F10 to C-x C, which is deliberately NOT an exception -(C-x C stays forwarding to the terminal program inside an agent buffer)." - (should (member "<f10>" ghostel-keymap-exceptions)) - (should-not (member "C-<f10>" ghostel-keymap-exceptions)) - (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f10>") - 'ghostel--send-event))) - -(ert-deftest test-term-c-spc-forwarded-not-set-mark () - "Regression: C-SPC is forwarded to the terminal, not bound to the global -`set-mark-command'. ghostel only forwards the `C-@' event, so without this an -Emacs region gets stuck in the ghostel buffer and tmux copy-mode's -begin-selection never starts." - (should (eq (keymap-lookup ghostel-mode-map "C-SPC") #'cj/term-send-C-SPC))) - -;; ----------------------------- copy-mode scroll ------------------------------ - (ert-deftest test-term-copy-mode-up-tmux-enters-then-scrolls-up () "Normal: from a live (non-copy) tmux pane, C-<up> enters copy-mode then sends the up-arrow, so one stroke both enters copy-mode and scrolls up." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")) + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")) (sent nil)) (unwind-protect (with-current-buffer agent @@ -368,7 +227,7 @@ the up-arrow, so one stroke both enters copy-mode and scrolls up." (lambda (_buffer) 'fake-process)) ((symbol-function 'process-tty-name) (lambda (_process &rest _) "/dev/pts/8")) - ((symbol-function 'ghostel-send-string) + ((symbol-function 'cj/--term-send-string) (lambda (s) (push s sent)))) (test-term-tmux-history--with-tmux-mock '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 @@ -381,8 +240,8 @@ the up-arrow, so one stroke both enters copy-mode and scrolls up." (ert-deftest test-term-copy-mode-up-tmux-already-in-mode-just-scrolls () "Normal: when the tmux pane is already in copy-mode, C-<up> only sends the -up-arrow -- it does not re-enter (which would reset the cursor)." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")) +up-arrow -- it does not re-enter and reset the cursor." + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")) (sent nil)) (unwind-protect (with-current-buffer agent @@ -390,7 +249,7 @@ up-arrow -- it does not re-enter (which would reset the cursor)." (lambda (_buffer) 'fake-process)) ((symbol-function 'process-tty-name) (lambda (_process &rest _) "/dev/pts/8")) - ((symbol-function 'ghostel-send-string) + ((symbol-function 'cj/--term-send-string) (lambda (s) (push s sent)))) (test-term-tmux-history--with-tmux-mock '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 @@ -401,54 +260,18 @@ up-arrow -- it does not re-enter (which would reset the cursor)." (when (buffer-live-p agent) (kill-buffer agent))))) -(ert-deftest test-term-copy-mode-up-nontmux-enters-then-moves-up () - "Boundary: without tmux and not yet in copy-mode, C-<up> enters -ghostel-copy-mode then moves point up a line, sending nothing to the pty." - (with-temp-buffer - (insert "abc\ndef\nghi\n") - (goto-char (point-min)) - (forward-line 2) ; land on line 3 - (let ((sent nil) (entered nil)) - (cl-letf (((symbol-function 'ghostel-send-string) (lambda (s) (push s sent))) - ((symbol-function 'ghostel-copy-mode) (lambda () (setq entered t)))) - (cj/term-copy-mode-up) - (should entered) - (should-not sent) - (should (= (line-number-at-pos) 2)))))) - -(ert-deftest test-term-copy-mode-up-nontmux-already-in-copy-just-moves () - "Normal: when ghostel is already in copy-mode, C-<up> just moves point up -- -it does not call `ghostel-copy-mode' again (which would toggle copy-mode off)." - (with-temp-buffer - (insert "abc\ndef\nghi\n") - (goto-char (point-min)) - (forward-line 2) ; land on line 3 - (setq-local ghostel--input-mode 'copy) - (let ((sent nil) (entered nil)) - (cl-letf (((symbol-function 'ghostel-send-string) (lambda (s) (push s sent))) - ((symbol-function 'ghostel-copy-mode) (lambda () (setq entered t)))) - (cj/term-copy-mode-up) - (should-not entered) - (should-not sent) - (should (= (line-number-at-pos) 2)))))) +;;; bindings -(ert-deftest test-term-copy-mode-only-c-up-bound () - "Normal/Regression: only C-<up> enters copy-mode in ghostel-mode-map; the -other arrows are not bound to it, so they pass through to the terminal." - (should (eq (keymap-lookup ghostel-mode-map "C-<up>") #'cj/term-copy-mode-up)) - (dolist (key '("C-<down>" "C-<left>" "C-<right>" - "M-<up>" "M-<down>" "M-<left>" "M-<right>")) - (should-not (eq (keymap-lookup ghostel-mode-map key) #'cj/term-copy-mode-up)))) +(ert-deftest test-term-keymap-history-and-copy-bindings () + "Normal: the C-; x terminal map owns the tmux-history and copy-mode commands." + (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/term-tmux-history)) + (should (eq (keymap-lookup cj/custom-keymap "x c") #'cj/term-copy-mode-dwim)) + (should (eq (keymap-lookup cj/custom-keymap "x t") #'cj/term-toggle))) -(ert-deftest test-term-copy-mode-only-c-up-in-keymap-exceptions () - "Regression (C-arrow copy-mode bug): only C-<up> is in -`ghostel-keymap-exceptions'. C-<left>/<right>/<down> are readline word-motion -at the shell prompt and the M-arrows have no copy-mode role, so none are -exceptions -- they reach the terminal program instead of Emacs." - (should (member "C-<up>" ghostel-keymap-exceptions)) - (dolist (key '("C-<down>" "C-<left>" "C-<right>" - "M-<up>" "M-<down>" "M-<left>" "M-<right>")) - (should-not (member key ghostel-keymap-exceptions)))) +(ert-deftest test-term-copy-mode-up-bound-in-eat-semi-char-map () + "Normal: C-<up> enters copy-mode + scrolls up from inside an EAT terminal." + (should (eq (keymap-lookup eat-semi-char-mode-map "C-<up>") + #'cj/term-copy-mode-up))) (provide 'test-term-tmux-history) ;;; test-term-tmux-history.el ends here |
