aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-11 10:02:17 -0500
committerCraig Jennings <c@cjennings.net>2026-05-11 10:02:17 -0500
commit949bdeb3ac4d7b027cdd88efd54cc8c121f8c5d3 (patch)
tree64e1302958d949533e43e8a17e86b5c55d8e2ab0
parent071fb5e972a08e4072d9177d493928ceb26763f4 (diff)
downloaddotemacs-949bdeb3ac4d7b027cdd88efd54cc8c121f8c5d3.tar.gz
dotemacs-949bdeb3ac4d7b027cdd88efd54cc8c121f8c5d3.zip
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', `<escape>', 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 `<pause>'.
-rw-r--r--modules/vterm-config.el57
-rw-r--r--tests/test-vterm-copy-mode-cursor.el7
-rw-r--r--tests/test-vterm-tmux-history.el45
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 <pause>, 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, <escape>, 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, <escape>, or q
+returns to the vterm without copying. RET is left unbound."
+ "M-w" #'kill-ring-save
+ "C-g" #'cj/vterm-tmux-history-quit
+ "<escape>" #'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', `<escape>', 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 "<f12>" #'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', `<escape>', and `q' all leave
+copy-mode without copying. vterm's default `RET' / `<return>' ->
+`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 "<escape>" #'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 "<return>" 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 / <escape> / 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, <escape>,
+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 "<escape>")
+ #'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 "<pause>")))
(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, <escape>, 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 "<escape>")
#'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 "<return>")))
(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."