From 83f2ac011f1afb085745ea527990eea32019fc00 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 24 Jun 2026 00:06:01 -0400 Subject: feat(term): modified arrows enter copy-mode and carry direction C- and M- in a ghostel buffer now enter copy-mode and move one step in that direction in a single stroke. The tmux path writes the arrow escape sequence into the pty so the copy cursor follows it; without tmux the same keys enter ghostel-copy-mode and move point. All eight keys join ghostel-keymap-exceptions and the semi-char map is rebuilt, so they reach Emacs instead of the terminal program. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ --- tests/test-term-tmux-history.el | 81 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) (limited to 'tests') diff --git a/tests/test-term-tmux-history.el b/tests/test-term-tmux-history.el index 4ad7fb79d..89b4875eb 100644 --- a/tests/test-term-tmux-history.el +++ b/tests/test-term-tmux-history.el @@ -355,5 +355,86 @@ 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))) +;; --------------------------- directional copy mode --------------------------- + +(ert-deftest test-term-copy-mode-step-tmux-sends-arrow () + "Normal: in tmux, each direction writes its arrow escape sequence to the pty." + (dolist (case '((up . "\e[A") (down . "\e[B") (left . "\e[D") (right . "\e[C"))) + (let ((sent nil)) + (cl-letf (((symbol-function 'ghostel-send-string) + (lambda (s) (push s sent)))) + (cj/--term-copy-mode-move-step (car case) t) + (should (equal sent (list (cdr case)))))))) + +(ert-deftest test-term-copy-mode-step-nontmux-moves-point () + "Normal: without tmux, up/down move by line and left/right move by char." + (with-temp-buffer + (insert "abc\ndef\nghi\n") + ;; up/down: land on line 2, step around. + (goto-char (point-min)) + (forward-line 1) + (should (= (line-number-at-pos) 2)) + (cj/--term-copy-mode-move-step 'up nil) + (should (= (line-number-at-pos) 1)) + (cj/--term-copy-mode-move-step 'down nil) + (should (= (line-number-at-pos) 2)) + ;; left/right: move by a single character. + (goto-char 2) + (cj/--term-copy-mode-move-step 'right nil) + (should (= (point) 3)) + (cj/--term-copy-mode-move-step 'left nil) + (should (= (point) 2)))) + +(ert-deftest test-term-copy-mode-step-unknown-direction-no-op () + "Boundary: an unknown direction sends nothing in the tmux branch." + (let ((sent nil)) + (cl-letf (((symbol-function 'ghostel-send-string) + (lambda (s) (push s sent)))) + (cj/--term-copy-mode-move-step 'sideways t) + (should-not sent)))) + +(ert-deftest test-term-copy-mode-up-command-enters-then-steps () + "Normal: the bound command enters tmux copy-mode, then sends the arrow, +so a single modified arrow both enters copy-mode and carries its direction." + (let ((agent (cj/test--make-fake-ghostel-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)))) + (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-up) + (should (equal (reverse sent) '("\C-b[\C-a" "\e[A")))))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +(ert-deftest test-term-copy-mode-arrows-bound-in-ghostel-map () + "Normal: C- and M- both reach the directional copy-mode commands." + (dolist (case '(("C-" . cj/term-copy-mode-up) + ("M-" . cj/term-copy-mode-up) + ("C-" . cj/term-copy-mode-down) + ("M-" . cj/term-copy-mode-down) + ("C-" . cj/term-copy-mode-left) + ("M-" . cj/term-copy-mode-left) + ("C-" . cj/term-copy-mode-right) + ("M-" . cj/term-copy-mode-right))) + (should (eq (keymap-lookup ghostel-mode-map (car case)) (cdr case))))) + +(ert-deftest test-term-copy-mode-arrows-in-keymap-exceptions () + "Regression: C-/M- are in `ghostel-keymap-exceptions' and the rebuilt +semi-char map no longer forwards them to the pty, so they reach Emacs and +trigger copy-mode entry from inside a ghostel buffer." + (dolist (key '("C-" "C-" "C-" "C-" + "M-" "M-" "M-" "M-")) + (should (member key ghostel-keymap-exceptions))) + (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "C-") + 'ghostel--send-event))) + (provide 'test-term-tmux-history) ;;; test-term-tmux-history.el ends here -- cgit v1.2.3