From 949bdeb3ac4d7b027cdd88efd54cc8c121f8c5d3 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 11 May 2026 10:02:17 -0500 Subject: feat(vterm): unify the keys in vterm copy-mode and tmux history `vterm-copy-mode' and the `C-; x h' tmux-history buffer now share one key story. `M-w' copies the active region and stays put, so I can copy several things in a row. `C-g', `', or `q' leaves (resuming the live terminal, or closing the history buffer) without copying. `RET' is unbound (no special "copy and exit"). In copy-mode that meant removing vterm's default `RET' -> `vterm-copy-mode-done' binding. Before, `M-w' exited and copied as it went, which made grabbing more than one selection awkward. The history buffer's `cj/vterm-tmux-history-copy-and-quit' was the copy-and-exit one-shot. It's gone. `M-w' then `q' is the equivalent. I also moved `cj/vterm-tmux-history' from `C-; x C' to `C-; x h' (unshifted, and it frees `C') and refreshed the file's stale commentary header, which still referenced the old `C-; V' prefix and `'. --- modules/vterm-config.el | 57 ++++++++++++++++++++---------------- tests/test-vterm-copy-mode-cursor.el | 7 +++-- tests/test-vterm-tmux-history.el | 45 ++++++++++++++++++---------- 3 files changed, 64 insertions(+), 45 deletions(-) diff --git a/modules/vterm-config.el b/modules/vterm-config.el index 191d74e8..764c9dcd 100644 --- a/modules/vterm-config.el +++ b/modules/vterm-config.el @@ -8,12 +8,15 @@ ;; just send them to the process that is currently running. So, C-a may be ;; beginning-of-the-line in a shell, or the prefix key in a screen session. -;; If you enter vterm-copy-mode with C-; V c or , the buffer will become -;; a normal Emacs buffer. You can then use your navigation keys, select -;; rectangles, etc. When you press RET or M-w, the region will be copied and -;; you'll be back in a working terminal session. C-; V C captures the current -;; tmux pane history into a temporary Emacs buffer where M-w copies the selected -;; region and returns to the vterm. +;; Two ways to lift text out of a vterm, both with the same key story: +;; - C-; x c enters vterm-copy-mode (the buffer becomes a normal Emacs +;; buffer: navigation keys, rectangles, etc.). +;; - C-; x h captures the current tmux pane's full history into a temporary +;; Emacs buffer. +;; In both, M-w copies the active region and stays open, so several pieces can +;; be grabbed in a row; C-g, , or q leaves (closing the history buffer +;; / resuming the live terminal) without copying. RET is left unbound -- no +;; special "copy and exit" shortcut. ;; ANSI-TERM & TERM ;; I haven't yet found a need for term or ansi-term in my workflows, so I leave @@ -80,8 +83,12 @@ Signal `user-error' when tmux exits with a non-zero status." (cj/vterm--tmux-pane-id-for-tty tty))) (defvar-keymap cj/vterm-tmux-history-mode-map - :doc "Keymap for `cj/vterm-tmux-history-mode'." - "M-w" #'cj/vterm-tmux-history-copy-and-quit + :doc "Keymap for `cj/vterm-tmux-history-mode'. +M-w copies the active region without leaving the buffer; C-g, , or q +returns to the vterm without copying. RET is left unbound." + "M-w" #'kill-ring-save + "C-g" #'cj/vterm-tmux-history-quit + "" #'cj/vterm-tmux-history-quit "q" #'cj/vterm-tmux-history-quit) (define-derived-mode cj/vterm-tmux-history-mode special-mode "Tmux History" @@ -108,24 +115,13 @@ Signal `user-error' when tmux exits with a non-zero status." (when (buffer-live-p history-buffer) (kill-buffer history-buffer)))) -(defun cj/vterm-tmux-history-copy-and-quit () - "Copy active region from tmux history, then quit back to the origin." - (interactive) - (unless (use-region-p) - (user-error "No active region")) - (let ((text (buffer-substring-no-properties - (region-beginning) - (region-end)))) - (kill-new text) - (deactivate-mark) - (cj/vterm-tmux-history-quit))) - (defun cj/vterm-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, closes the history buffer, and returns point -to the vterm buffer that launched it." +copies the active region and stays open, so several pieces can be +copied in a row; `q', `', or `C-g' returns point to the vterm +buffer that launched it." (interactive) (let* ((origin-buffer (current-buffer)) (origin-window (selected-window)) @@ -353,8 +349,8 @@ C-F9 / M-F9 dispatch via `cj/ai-vterm'." (keymap-global-set "" #'cj/vterm-toggle) -(keymap-set cj/vterm-map "C" #'cj/vterm-tmux-history) (keymap-set cj/vterm-map "c" #'vterm-copy-mode) +(keymap-set cj/vterm-map "h" #'cj/vterm-tmux-history) (keymap-set cj/vterm-map "l" #'vterm-clear-scrollback) (keymap-set cj/vterm-map "N" #'vterm) (keymap-set cj/vterm-map "n" #'vterm-next-prompt) @@ -370,11 +366,20 @@ C-F9 / M-F9 dispatch via `cj/ai-vterm'." (keymap-set vterm-mode-map "C-;" cj/custom-keymap))) (defun cj/vterm-install-copy-mode-cancel-keys () - "Install copy and cancel keys in `vterm-copy-mode-map'." + "Install copy and exit keys in `vterm-copy-mode-map'. + +`M-w' copies the active region without leaving copy-mode, so several +pieces can be copied in a row. `C-g', `', and `q' all leave +copy-mode without copying. vterm's default `RET' / `' -> +`vterm-copy-mode-done' bindings are removed so RET isn't a special +\"copy and exit\" -- matching the tmux history buffer." (when (boundp 'vterm-copy-mode-map) + (keymap-set vterm-copy-mode-map "M-w" #'kill-ring-save) (keymap-set vterm-copy-mode-map "C-g" #'cj/vterm-copy-mode-cancel) (keymap-set vterm-copy-mode-map "" #'cj/vterm-copy-mode-cancel) - (keymap-set vterm-copy-mode-map "M-w" #'vterm-copy-mode-done))) + (keymap-set vterm-copy-mode-map "q" #'cj/vterm-copy-mode-cancel) + (keymap-unset vterm-copy-mode-map "RET" t) + (keymap-unset vterm-copy-mode-map "" t))) (cj/vterm-install-prefix-key) (cj/vterm-install-copy-mode-cancel-keys) @@ -404,8 +409,8 @@ cursor-visibility tracking resumes." (with-eval-after-load 'which-key (which-key-add-key-based-replacements "C-; x" "vterm menu" - "C-; x C" "tmux scrollback copy" "C-; x c" "vterm copy mode" + "C-; x h" "tmux scrollback history" "C-; x l" "clear vterm scrollback" "C-; x N" "new vterm" "C-; x n" "next prompt" diff --git a/tests/test-vterm-copy-mode-cursor.el b/tests/test-vterm-copy-mode-cursor.el index 49625405..c549a44f 100644 --- a/tests/test-vterm-copy-mode-cursor.el +++ b/tests/test-vterm-copy-mode-cursor.el @@ -81,9 +81,10 @@ restore function -- not just the helper in isolation." (should-not (local-variable-p 'cursor-type)))) (ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-copy-done () - "Normal: `vterm-copy-mode-done' (M-w / RET binding) toggles copy-mode -off and triggers cursor restoration. This is the path the user takes -most often -- copy and exit in one keystroke." + "Normal: `vterm-copy-mode-done' toggles copy-mode off and triggers cursor +restoration. No key is bound to it in this config (M-w copies and stays; +RET is unbound), but it stays reachable via \\[execute-extended-command] +and its exit path must still restore the cursor." (test-vterm-copy-mode-cursor--in-fake-vterm-buffer (setq-local cursor-type nil) (vterm-copy-mode 1) diff --git a/tests/test-vterm-tmux-history.el b/tests/test-vterm-tmux-history.el index 901d96c9..eec6c622 100644 --- a/tests/test-vterm-tmux-history.el +++ b/tests/test-vterm-tmux-history.el @@ -86,10 +86,10 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." (when (buffer-live-p origin) (kill-buffer origin)))) -(ert-deftest test-vterm-tmux-history-copy-copies-region-and-returns () - "Normal: M-w copies the region, kills history buffer, and restores origin." - (let ((origin (get-buffer-create "*test-vterm-history-return*")) - (kill-ring nil)) +(ert-deftest test-vterm-tmux-history-quit-returns-to-origin () + "Normal: q / / C-g (cj/vterm-tmux-history-quit) kills the history +buffer and restores the origin buffer, window, and point." + (let ((origin (get-buffer-create "*test-vterm-history-return*"))) (unwind-protect (let ((history (get-buffer-create "*vterm tmux history: test*"))) (with-current-buffer origin @@ -105,26 +105,34 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." (setq-local cj/vterm-tmux-history--origin-buffer origin) (setq-local cj/vterm-tmux-history--origin-window origin-window) (setq-local cj/vterm-tmux-history--origin-point (point-min)) - (goto-char (point-min)) - (set-mark (point)) - (goto-char (point-at-eol 2)) - (activate-mark) - (cj/vterm-tmux-history-copy-and-quit)) - (should (equal (car kill-ring) "alpha\nbeta")) + (cj/vterm-tmux-history-quit)) (should-not (buffer-live-p history)) (should (eq (current-buffer) origin)) (should (= (point) (point-min))))) (when (buffer-live-p origin) (kill-buffer origin))))) +(ert-deftest test-vterm-tmux-history-mode-keymap () + "Normal: in the history buffer M-w copies without quitting; q, , +and C-g quit back to the vterm; RET is left unbound (no special exit)." + (should (eq (keymap-lookup cj/vterm-tmux-history-mode-map "M-w") + #'kill-ring-save)) + (should (eq (keymap-lookup cj/vterm-tmux-history-mode-map "q") + #'cj/vterm-tmux-history-quit)) + (should (eq (keymap-lookup cj/vterm-tmux-history-mode-map "") + #'cj/vterm-tmux-history-quit)) + (should (eq (keymap-lookup cj/vterm-tmux-history-mode-map "C-g") + #'cj/vterm-tmux-history-quit)) + (should-not (keymap-lookup cj/vterm-tmux-history-mode-map "RET"))) + (ert-deftest test-vterm-keymap-includes-history-and-copy-bindings () "Normal: personal vterm map owns the high-level vterm UX commands." (should (member "C-;" vterm-keymap-exceptions)) (should-not (eq (keymap-lookup cj/custom-keymap "X c") #'vterm-copy-mode)) - (should (eq (keymap-lookup cj/custom-keymap "x C") #'cj/vterm-tmux-history)) + (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/vterm-tmux-history)) (should (eq (keymap-lookup cj/custom-keymap "x c") #'vterm-copy-mode)) (should (equal (keymap-lookup vterm-mode-map "C-;") cj/custom-keymap)) - (should (eq (keymap-lookup vterm-mode-map "C-; x C") #'cj/vterm-tmux-history)) + (should (eq (keymap-lookup vterm-mode-map "C-; x h") #'cj/vterm-tmux-history)) (should (eq (keymap-lookup vterm-mode-map "C-; x c") #'vterm-copy-mode)) (should-not (keymap-lookup vterm-mode-map "C-c C-t"))) @@ -141,14 +149,19 @@ modern keyboards and was redundant." (let ((binding (keymap-lookup vterm-mode-map ""))) (should-not (eq binding #'vterm-copy-mode)))) -(ert-deftest test-vterm-copy-mode-cancel-keys () - "Normal: copy mode has explicit copy and no-copy exits." +(ert-deftest test-vterm-copy-mode-keys () + "Normal: copy mode mirrors the history buffer -- M-w copies without +leaving; C-g, , and q leave without copying; RET is unbound." + (should (eq (keymap-lookup vterm-copy-mode-map "M-w") + #'kill-ring-save)) (should (eq (keymap-lookup vterm-copy-mode-map "C-g") #'cj/vterm-copy-mode-cancel)) (should (eq (keymap-lookup vterm-copy-mode-map "") #'cj/vterm-copy-mode-cancel)) - (should (eq (keymap-lookup vterm-copy-mode-map "M-w") - #'vterm-copy-mode-done))) + (should (eq (keymap-lookup vterm-copy-mode-map "q") + #'cj/vterm-copy-mode-cancel)) + (should-not (keymap-lookup vterm-copy-mode-map "RET")) + (should-not (keymap-lookup vterm-copy-mode-map ""))) (ert-deftest test-vterm-copy-mode-cancel-errors-outside-copy-mode () "Error: `cj/vterm-copy-mode-cancel' refuses to run when not in copy mode." -- cgit v1.2.3