aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-18 20:28:27 -0400
committerCraig Jennings <c@cjennings.net>2026-05-18 20:28:27 -0400
commit016068349e0ebbd470e337440a1ef378248e0edb (patch)
treec4476dce902986a505d89aea6a73be6637435ffc
parent55fb9102c70dc272d4267aec30eed4860f3abdf5 (diff)
downloaddotemacs-016068349e0ebbd470e337440a1ef378248e0edb.tar.gz
dotemacs-016068349e0ebbd470e337440a1ef378248e0edb.zip
fix(vterm): stop wheel/escape forwarders from blocking Emacs
vterm-send-string ends with (accept-process-output ... vterm-timer-delay ...). The global vterm-timer-delay is nil in this config, so the call blocks forever when the pty's program consumes the event without producing output -- a common pattern for TUIs like Claude Code reacting to mouse wheel or Escape. The symptom is a spinning cursor until C-g. cj/vterm--send-mouse-wheel and cj/vterm-send-escape now wrap the send in a let-binding that pins vterm-timer-delay to 0, so accept-process-output returns immediately. A top-level (defvar vterm-timer-delay) declaration goes alongside so the let is dynamic. Without it, lexical-binding-t in this file makes the binding lexical, invisible to vterm-send-string across files. The backtrace from the failing case confirmed the lookup was still receiving nil before the declaration.
-rw-r--r--modules/vterm-config.el27
1 files changed, 23 insertions, 4 deletions
diff --git a/modules/vterm-config.el b/modules/vterm-config.el
index 70f5d60a..20c85468 100644
--- a/modules/vterm-config.el
+++ b/modules/vterm-config.el
@@ -35,6 +35,12 @@
(require 'cj-window-geometry-lib)
(require 'cj-window-toggle-lib)
+;; Declare so `let'-bindings in this file are dynamic (special) rather than
+;; lexical. Without this, `(let ((vterm-timer-delay 0)) (vterm-send-string
+;; ...))' creates a lexical binding that `vterm-send-string' (in vterm.el)
+;; cannot see, so its `accept-process-output' still blocks on the global nil.
+(defvar vterm-timer-delay)
+
(defvar-keymap cj/vterm-map
:doc "Personal vterm command map.")
;; Lowercase x picked over V for fewer Shift presses; v is the VC menu.
@@ -202,8 +208,17 @@ vterm's keymap binds only `mouse-1' and `mouse-yank-primary' --
wheel events fall through to Emacs's default scroll behavior, which
moves the window over vterm's scrollback instead of reaching the
pty. Without this forwarding, tmux's `set -g mouse on' never fires
-because tmux never sees the events."
- (vterm-send-string (format "\e[<%d;1;1M" button)))
+because tmux never sees the events.
+
+`vterm-timer-delay' is locally pinned to 0 so
+`vterm-send-string''s `accept-process-output' returns immediately.
+With the buffer-local nil (`vterm-config' sets it for refresh
+batching), `accept-process-output' blocks forever when the program
+in the pty consumes the event without producing visible output --
+common for TUIs like Claude Code. Result before the pin: spinning
+cursor until C-g, no actual scroll."
+ (let ((vterm-timer-delay 0))
+ (vterm-send-string (format "\e[<%d;1;1M" button))))
(defun cj/vterm-mouse-wheel-up ()
"Forward a wheel-up event to the program running in this vterm."
@@ -222,9 +237,13 @@ because tmux never sees the events."
`modules/keybindings.el'), so without this override Emacs swallows
the key before it can reach the pty. Forwarding it here lets tmux
copy-mode cancel, vi-mode exits, and any other in-terminal program
-that relies on Escape see the key."
+that relies on Escape see the key.
+
+`vterm-timer-delay' is locally pinned to 0; see
+`cj/vterm--send-mouse-wheel' for the hang scenario this avoids."
(interactive)
- (vterm-send-string "\e"))
+ (let ((vterm-timer-delay 0))
+ (vterm-send-string "\e")))
(use-package vterm
:defer .5