aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--init.el1
-rw-r--r--modules/auto-dim-config.el9
-rw-r--r--modules/dashboard-config.el4
-rw-r--r--modules/eat-config.el196
-rw-r--r--modules/face-diagnostic.el2
-rw-r--r--modules/term-config.el369
-rw-r--r--modules/weather-config.el11
-rw-r--r--tests/test-dashboard-config-launchers.el2
-rw-r--r--tests/test-term-config--f8-in-term.el42
-rw-r--r--tests/test-term-tmux-history.el323
10 files changed, 288 insertions, 671 deletions
diff --git a/init.el b/init.el
index dc09b085e..6cdb06c99 100644
--- a/init.el
+++ b/init.el
@@ -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