From 016068349e0ebbd470e337440a1ef378248e0edb Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 18 May 2026 20:28:27 -0400 Subject: 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. --- modules/vterm-config.el | 27 +++++++++++++++++++++++---- 1 file 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 -- cgit v1.2.3