Skip to content

fix: ESC permanently blocked after Ctrl+letter injection corrupts ConPTY VT parser state#329

Closed
LizardLiang wants to merge 1 commit into
psmux:masterfrom
LizardLiang:fix/esc-cannot-pass-to-neovim-after-using-CtrlW
Closed

fix: ESC permanently blocked after Ctrl+letter injection corrupts ConPTY VT parser state#329
LizardLiang wants to merge 1 commit into
psmux:masterfrom
LizardLiang:fix/esc-cannot-pass-to-neovim-after-using-CtrlW

Conversation

@LizardLiang

@LizardLiang LizardLiang commented May 28, 2026

Copy link
Copy Markdown
Contributor

Problem

After pressing Ctrl+W (or any Ctrl+letter) inside a pane running Neovim, ESC stops being delivered to the application entirely. The pane stays alive and accepts other keystrokes, but the user is stuck and cannot leave insert mode or any other ESC-dependent state.

Reproduce

  1. Open Neovim in a psmux pane
  2. Open Floaterm (:FloatermNew)
  3. Type something in the Floaterm terminal
  4. Press <C-w> to switch windows
  5. Press <Esc> — it is no longer delivered to Neovim

Root cause

Ctrl+letter keys were injected by writing Win32 input-mode VT escape sequences (format: ESC [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _) directly into the ConPTY pipe. These sequences start with \x1b. When they reach a pane running Neovim, ESC handling breaks: subsequent bare \x1b bytes from real ESC keypresses are no longer delivered correctly.

The same Win32 VT sequence path was used in both the live-keypress handler and the send-keys C-x command.

Investigation

Tracing the ESC delivery failure led to the Ctrl+letter injection path. Every Ctrl+letter injected a Win32 input-mode VT escape sequence — a string starting with \x1b — directly into the ConPTY pipe. ConPTY's VT parser consumed the sequence, but the parser was left in a state where a bare \x1b arriving immediately after was treated as the beginning of another escape sequence rather than a standalone ESC keypress. The next real ESC the user pressed fed into that buffered state and was swallowed.

Fix

Ctrl+letter injection now uses WriteConsoleInputW via send_modified_key_event, which writes directly into the child's console input buffer without touching the ConPTY VT pipe. This delivers the correct virtual key code and LEFT_CTRL_PRESSED flag to apps that read from CONIN$ (such as PSReadLine), while leaving the VT parser state untouched. Mirroring the existing Alt+key pattern: try send_modified_key_event first; write the raw byte only if injection fails (no pid, or AttachConsole returned an error).

'c' is excluded from the WriteConsoleInputW branch and always falls through to the raw byte path. WriteConsoleInputW puts a KEY_EVENT into the input queue — it does not trigger CTRL_C_EVENT. Process termination for apps like ping depends on CTRL_C_EVENT, which only ConPTY can generate, and only from the raw byte path (when ENABLE_PROCESSED_INPUT is set on the child's console). There is no single API that delivers both a KEY_EVENT and a CTRL_C_EVENT in one call; the two are separate Windows console mechanisms. Using the raw byte for 'c' lets ConPTY decide: CTRL_C_EVENT when ENABLE_PROCESSED_INPUT is on (shells, CLI tools), KEY_EVENT when it is off (Neovim raw mode) — the same adaptive behaviour as a real keyboard.

char_to_vk now explicitly maps '\x1b'0x1B (VK_ESCAPE) and '\r'0x0D (VK_RETURN). VkKeyScanW returns -1 for non-printable characters; without these arms the old code produced VK code 0, generating KEY_EVENTs that applications discard.

send_modified_enter_event was removed. All key injection — Enter, ESC, Ctrl+letter — now flows through the single send_modified_key_event(pid, ch, ctrl, alt, shift) entry point.

GenerateConsoleCtrlEvent is deliberately absent from send_modified_key_event. Firing GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0) while attached to a child's console sends the signal to that process's entire group. When the pane is running Neovim, that group includes Neovim itself — observed to kill Neovim instead of the foreground process inside a nested terminal (e.g. Floaterm). Signal delivery via GenerateConsoleCtrlEvent is the responsibility of send_ctrl_c_event, which is invoked only from the send-keys C-c code path.

Test plan

  • cargo test char_to_vkescape_returns_vk_escape, carriage_return_returns_vk_return, alphabetic_lowercase_maps_to_vk, alphabetic_uppercase_same_as_lowercase
  • Ctrl+W in Neovim triggers exactly once per keypress (no double window switch)
  • ESC works normally after any Ctrl+letter keypress in Neovim
  • ping -t stops on Ctrl+C
  • PSReadLine Ctrl+letter shortcuts work (Ctrl+E, Ctrl+W confirmed)
  • PSReadLine Ctrl+C cancels the current input line

Changes

File Change
src/input.rs Replace Win32 VT sequence injection with send_modified_key_event for Ctrl+letter (live-keypress and send-keys paths); exclude 'c' from WriteConsoleInputW branch so Ctrl+C retains raw byte delivery; replace send_modified_enter_event calls with send_modified_key_event(pid, '\r', ...)
src/platform.rs Add '\x1b'0x1B and '\r'0x0D explicit arms in char_to_vk; remove send_modified_enter_event (merged into send_modified_key_event)
tests-rs/test_char_to_vk.rs 4 unit tests: escape_returns_vk_escape, carriage_return_returns_vk_return, alphabetic_lowercase_maps_to_vk, alphabetic_uppercase_same_as_lowercase

…Enter/ESC/Ctrl

- Replace send_modified_enter_event calls with send_modified_key_event(pid, '\r', ...)
- Switch Ctrl+letter injection from Win32 VT escape sequences to WriteConsoleInputW
  to avoid corrupting ConPTY's VT parser (ESC sequences left parser buffering \x1b)
- Fix char_to_vk to handle '\x1b' and '\r' explicitly since VkKeyScanW returns -1
  for non-printable chars; reuse char_to_vk/vk_to_scan in send_modified_key_event
- Add tests for char_to_vk covering ESC, carriage return, and alphabetic chars
psmux added a commit that referenced this pull request May 28, 2026
…ter injection (#329)

Ctrl+letter keys were injected by writing Win32 input-mode VT escape
sequences (ESC [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _) to the ConPTY pipe.
These sequences start with 0x1B and corrupt ConPTY's VT parser state,
permanently breaking ESC delivery to applications like Neovim.

Replace with a two-channel approach:
1. Write the raw C0 control byte (ch & 0x1F) to the ConPTY pipe
2. Inject a KEY_EVENT via WriteConsoleInputW for PSReadLine compatibility

Also consolidates send_modified_enter_event into send_modified_key_event,
and fixes char_to_vk to handle ESC (0x1B) and Enter (0x0D) explicitly.

Authored-by: LizardLiang <LizardLiang@users.noreply.github.com>
@psmux

psmux commented May 28, 2026

Copy link
Copy Markdown
Owner

Hey @LizardLiang, great catch and a really well-written PR. The root cause analysis is spot on.

The Win32 VT input mode sequences (ESC [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _) were corrupting ConPTY's VT parser state because they start with \x1b, and the parser would buffer that byte, breaking subsequent bare ESC delivery. Your two-channel approach (raw C0 bytes for the pipe + WriteConsoleInputW for PSReadLine) is the correct fix.

I verified this thoroughly:

  • 16 E2E tests covering Ctrl+W, all 26 Ctrl+letter keys, rapid injection, Ctrl+C, C-m (Enter), ESC delivery after injection, and TUI visual verification -- all pass on both master and the PR branch
  • All 2032 unit tests + 82 crate tests pass with zero failures on the PR branch
  • CI checks (Windows x64, x86, ARM64) all green
  • The consolidation of send_modified_enter_event into send_modified_key_event and the char_to_vk fixes for ESC/Enter are clean improvements

Merged via squash to master in commit 0af970f. Thanks for the contribution!

@LizardLiang

Copy link
Copy Markdown
Contributor Author

Hi @psmux , this PR inadvertently introduced a double delivery bug for Ctrl+letter keys on Windows
— both the raw byte and WriteConsoleInputW injection fire, causing applications to see each keypress
twice. Ctrl+C is also affected it no longer could stop ping -t 8.8.8.8 command.

Apologies for the inconvenience! I've opened a follow-up PR that resolves these issues. Would you mind
taking a look when you get a chance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants