feat: add ANSI parsing support on Windows (VT input hybrid) - #1030
feat: add ANSI parsing support on Windows (VT input hybrid)#1030eitsupi wants to merge 38 commits into
Conversation
Move the ANSI escape sequence parser to a platform-shared module so it can be reused on Windows. Replace internal is_raw_mode_enabled() calls with the public API (crate::terminal::is_raw_mode_enabled()) which has a uniform Result<bool> signature on all platforms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make CursorPosition, KeyboardEnhancementFlags, and PrimaryDeviceAttributes variants available on all platforms. Unify EventFilter::eval to use the matches! pattern for all platforms. Remove cfg gates from filter structs, impls, and test functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the identical Parser struct from mio.rs and tty.rs into sys/parse.rs. Add push_event() method for Windows hybrid source to enqueue non-ANSI events alongside parsed events. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable ENABLE_VIRTUAL_TERMINAL_INPUT on Windows console input to receive ANSI escape sequences (including bracketed paste). The hybrid approach feeds KEY_EVENT unicode characters through the shared ANSI parser when VT input is available, and falls back to VK code handling for keys without character data or when VT input is unsupported (legacy conhost). Also fix enable_mouse_capture() to OR flags instead of replacing, which would clobber the VT input flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add comment about init_original_console_mode ordering dependency - Add comment about potential stale event count in batch reading - Add comment about surrogate buffer invariant across event types - Clarify VT key-up skip comment re: behavioral consistency - Extract decode_utf16_char to shared free function with unit tests (BMP chars, surrogate pairs, orphaned high/low surrogates) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…arnings - Split surrogate_buffer into vt_surrogate and legacy_surrogate to prevent interference between VT and non-VT code paths within a single batch of input events - Add comment explaining unwrap_or(false) rationale for is_raw_mode_enabled on Windows - Add #[allow(dead_code)] to push_event and decode_utf16_char (only used on Windows, but defined in shared module for testability) - Expand try_enable_vt_input comment explaining why all set_mode errors are treated as "VT not supported" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-add comment explaining that non-key events don't touch vt_surrogate, so interleaved events between surrogate pair halves are harmless. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After removing #[cfg(unix)] from InternalEvent variants (CursorPosition, KeyboardEnhancementFlags, PrimaryDeviceAttributes), the wildcard match arms in event.rs and stream.rs also need their #[cfg(unix)] guards removed to remain exhaustive on Windows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CursorPositionFilter, KeyboardEnhancementFlagsFilter, and PrimaryDeviceAttributesFilter are only used on Unix (for terminal queries). Use #[cfg_attr(windows, allow(dead_code))] to suppress warnings specifically on Windows without hiding them on Unix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I verified this with Forge code -> reedline -> crossterm. // Patches to enable Windows VT input and bracketed paste support It works, THANKS! I just hope the maintainers will merge and release a new version soon 🙏 |
Crossterm's Windows backend uses ReadConsoleInput which delivers structured INPUT_RECORD events. The console strips VT escape sequences (including bracketed paste markers) from these events. This is a known crossterm limitation (crossterm-rs/crossterm#737). Fix by enabling ENABLE_VIRTUAL_TERMINAL_INPUT on the console input handle and reading VT sequences from KEY_EVENT_RECORD.uChar. This is the hybrid approach recommended by Microsoft and used by Cygwin, MSYS2, and OpenSSH (see crossterm-rs/crossterm#1030). Changes: - New win_vt_input module: enables VT input mode, reads INPUT_RECORD events, extracts raw VT bytes from key events, handles resize/focus as structured events, supports UTF-16 surrogate pairs - relay_windows.rs: forwards raw VT bytes to server (matching Unix relay behavior) with crossterm fallback for legacy Windows - main.rs: Windows direct mode uses InputParser to parse VT bytes into crossterm Events, with crossterm fallback if VT input unavailable https://claude.ai/code/session_01NRqCzMiQM41Low1HvSNaLc
Crossterm's Windows backend uses ReadConsoleInput which delivers structured INPUT_RECORD events. The console strips VT escape sequences (including bracketed paste markers) from these events. This is a known crossterm limitation (crossterm-rs/crossterm#737). Fix by enabling ENABLE_VIRTUAL_TERMINAL_INPUT on the console input handle and reading VT sequences from KEY_EVENT_RECORD.uChar. This is the hybrid approach recommended by Microsoft and used by Cygwin, MSYS2, and OpenSSH (see crossterm-rs/crossterm#1030). Changes: - New win_vt_input module: enables VT input mode, reads INPUT_RECORD events, extracts raw VT bytes from key events, handles resize/focus as structured events, supports UTF-16 surrogate pairs - relay_windows.rs: forwards raw VT bytes to server (matching Unix relay behavior) with crossterm fallback for legacy Windows - main.rs: Windows direct mode uses InputParser to parse VT bytes into crossterm Events, with crossterm fallback if VT input unavailable https://claude.ai/code/session_01NRqCzMiQM41Low1HvSNaLc
Crossterm's Windows backend uses ReadConsoleInput which delivers structured INPUT_RECORD events. The console strips VT escape sequences (including bracketed paste markers) from these events. This is a known crossterm limitation (crossterm-rs/crossterm#737). Fix by enabling ENABLE_VIRTUAL_TERMINAL_INPUT on the console input handle and reading VT sequences from KEY_EVENT_RECORD.uChar. This is the hybrid approach recommended by Microsoft and used by Cygwin, MSYS2, and OpenSSH (see crossterm-rs/crossterm#1030). Changes: - New win_vt_input module: enables VT input mode, reads INPUT_RECORD events, extracts raw VT bytes from key events, handles resize/focus as structured events, supports UTF-16 surrogate pairs - relay_windows.rs: forwards raw VT bytes to server (matching Unix relay behavior) with crossterm fallback for legacy Windows - main.rs: Windows direct mode uses InputParser to parse VT bytes into crossterm Events, with crossterm fallback if VT input unavailable https://claude.ai/code/session_01NRqCzMiQM41Low1HvSNaLc
try_enable_vt_input() sets ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200) on the console input handle, but disable_raw_mode() only restores LINE_INPUT, ECHO_INPUT, and PROCESSED_INPUT — leaving VT input active after the application exits. Parent shells that don't handle VT input sequences (e.g. nushell using standard crossterm without this fork) misinterpret keystrokes when VT input is left enabled, because keystrokes are delivered as ANSI escape sequences rather than the legacy key events they expect. Fix by also clearing ENABLE_VIRTUAL_TERMINAL_INPUT when disabling raw mode.
The previous fix unconditionally cleared ENABLE_VIRTUAL_TERMINAL_INPUT in disable_raw_mode(), which could clobber the flag in environments where it was already active before this process entered raw mode. Instead, check ORIGINAL_CONSOLE_MODE (saved by try_enable_vt_input before modifying the mode). If VT input was absent in the original mode, we added it, so we clear it on exit. If it was already set, we leave it untouched. This preserves the fix for nushell (which does not pre-enable VT input) while not regressing environments that rely on VT input being set.
|
@easyinplay Thanks for your detailed review! I'll take a look. |
crossterm 0.29.0 reads console `INPUT_RECORD`s on Windows instead of VT input, so the `ESC[200~` markers a terminal sends around a paste never decode, `Event::Paste` never fires, and a multiline paste submits at its first newline (#336). The fix lives in crossterm-rs/crossterm#1030, which is not released yet, so we pin the patch to that PR head. The rev is taken from `refs/pull/1030/head` on the upstream repo, which lives there forever, so the pin survives the contributor deleting their fork. The PR also rewrites the unix input path, so Linux wants a hand test before this ships, and `cargo update` will not move a rev pin when the PR gets rebased.
crossterm 0.29.0 reads console `INPUT_RECORD`s on Windows instead of VT input, so the `ESC[200~` markers a terminal sends around a paste never decode, `Event::Paste` never fires, and a multiline paste submits at its first newline (#336). The fix lives in crossterm-rs/crossterm#1030, which is not released yet, so we pin the patch to that PR head. The rev is taken from `refs/pull/1030/head` on the upstream repo, which lives there forever, so the pin survives the contributor deleting their fork. The PR also rewrites the unix input path, so Linux wants a hand test before this ships, and `cargo update` will not move a rev pin when the PR gets rebased.
|
@easyinplay Thank you for the detailed review. I have pushed a set of changes addressing the findings at the current HEAD. Findings 1–4, 6, and 7 are fixed in code. Finding 5 remains an intentional Unix-compatible Esc/Alt ambiguity and is now documented. I also added a Windows diagnostic example for the mode lifecycle, key-event kinds, and bracketed paste. |
Port of maki 37ffc739. crossterm 0.29.0 reads console INPUT_RECORDs on Windows instead of VT input, so the ESC[200~ markers a terminal sends around a paste never decode, Event::Paste never fires, and a multiline paste submits at its first newline. Pinned to the head of crossterm-rs/crossterm#1030, which lives in refs/pull/1030/head on the upstream repo. Remove once the PR ships in a release. Co-authored-by: Tony Solomonik <tony.solomonik@gmail.com>
There was a problem hiding this comment.
@eitsupi Thanks for working through all of these. Six findings addressed in one push, finding 5 documented as an intentional trade rather than quietly kept, and a first-party diagnostic example on top. That last one is the part that keeps paying off: it gives the next person a way to check the mode lifecycle and key kinds without building their own probe. This PR has been waiting since February for reasons that have nothing to do with the work in it.
Re-reviewed 11e4912. All six findings you addressed are fixed, and I confirmed each one against the code rather than taking the commit messages for it. Finding 5 reads fine as an intentional, now-documented trade.
Four new things below. One of them will fail CI and is a one-line fix, so it leads. Two of the others are consequences of the finding 1 fix rather than pre-existing problems.
I also have a correction to make to my own finding 1. The behavior I reported was real, but the mechanism I gave for the Alt-code half of it was wrong, and the raw records I captured for this pass show why.
cargo clippy -- -D warnings fails on this HEAD
.github/workflows/ci.yml:65, verbatim:
cargo clippy --locked --all-targets --all-features -- -D warnings
error: this loop could be written as a `for` loop
--> src/event/sys/parse.rs:1833:9
|
1833 | while let Some(event) = self.next() {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `for event in self.by_ref()`
|
= note: `-D clippy::while-let-on-iterator` implied by `-D warnings`
error: could not compile `crossterm` (lib) due to 1 previous error
That is next_event(), from c12062b8. Clean on 3ca5429 with the same toolchain, so it came with this push. The suggested for event in self.by_ref() builds on Windows and takes the command back to clean.
Leading with it because it is one line and the rest of this can wait for a normal round.
Character key-up now works, and two things follow from how it works
Finding 1 is fixed. source/windows.rs:103's else arm now takes every record the VT arm doesn't, and the comment says exactly what it's for. Measured in conhost on this HEAD:
Key Char('a') NONE Press
Key Char('a') NONE Release +778 us
against 4 Press / 0 Release on the previous HEAD. Arrow and function keys are unchanged, as expected. Alt+numpad produces a character again.
Both of the following are new behavior introduced by that fix, not pre-existing. I don't think either is a reason to hold the PR, but the second one is a visible correctness bug and neither is mentioned in the description.
Raw INPUT_RECORDs below come from a read-only dump taken through the same CONIN$ handle crossterm uses, after crossterm's own enable_raw_mode() has run. Nothing is injected and the dump changes no mode. conhost, mode=0x000003f0, VT_INPUT=true, Windows 11 26200. cks is control_key_state in hex.
2. Press and Release disagree for Ctrl+Shift+letter
Physically typed Ctrl+Shift+A:
down=true vk=0x11 scan=0x1D u_char=0x0000 cks=0x28 ctrl down
down=true vk=0x10 scan=0x2A u_char=0x0000 cks=0x38 shift down
down=true vk=0x00 scan=0x00 u_char=0x0001 cks=0x00 VT byte, cks cleared
down=false vk=0x41 scan=0x1E u_char=0x0001 cks=0x38 key-up, verbatim
down=false vk=0x11 scan=0x1D u_char=0x0000 cks=0x30
down=false vk=0x10 scan=0x2A u_char=0x0000 cks=0x20
delivered as
Key Char('a') CONTROL Press
Key Char('A') SHIFT | CONTROL Release
The key code and the modifiers both differ between the two halves of one keypress. The cause is structural rather than a slip in the routing: the VT byte is 0x01, which has no way to encode shift, and conhost clears control_key_state on the record it rewrites. The key-up keeps cks=0x38 (NUMLOCK | SHIFT | LEFT_CTRL) because VT translation passes key-ups through untouched. So the VT arm can only ever report CONTROL here while the VK arm reports SHIFT | CONTROL, and this will hold for every Ctrl+Shift+letter.
An application pairing press to release by (code, modifiers) gets a Release matching no Press, and the Press stays outstanding. That is the same stuck-key symptom finding 1 was about, reached a different way, so it seems worth deciding deliberately rather than inheriting.
This also closes the gap I was explicit about last time. I wrote then that I had not watched a physically typed character key-up under VT and that the conjunction was inference. It isn't any more; the trace above is a real keystroke.
3. Alt+numpad delivers the character twice
Alt+numpad 0233:
down=true vk=0x00 scan=0x00 u_char=0x001B cks=0x00 ESC, VT byte
down=true vk=0x00 scan=0x00 u_char=0x00E9 cks=0x00 e-acute, VT byte
down=false vk=0xE7 scan=0x00 u_char=0x00E9 cks=0x22 key-up, vk=0xE7 (VK_PACKET)
down=true vk=0x00 scan=0x00 u_char=0x00E9 cks=0x00 e-acute, VT byte again
down=false vk=0x12 scan=0x38 u_char=0x0000 cks=0x20 alt up, u_char is zero
delivered as
Key Char('é') ALT Press
Key Char('é') ALT Release
Key Char('é') NONE Press
Three events, and two of them are Press. A text field receiving this inserts the character twice.
Tracing them: the first two records are ESC followed by the character as VT bytes, which the shared parser combines into Alt+é. The VK_PACKET key-up is what the new else arm now routes to handle_key_event, giving the Release with ALT from cks=0x22 (NUMLOCK | LEFT_ALT_PRESSED). The third record is the character arriving as a VT byte a second time, with no modifiers.
The doc comment on examples/windows-vt-input.rs gives the expected observation as "Alt+numpad 0233 producing a character event", singular, so I don't think this is intended.
Correction to my finding 1
My finding 1 claimed that with VT input active, Alt+numpad 0233 loses the character because is_alt_code at sys/windows/parse.rs:202 becomes unreachable. The loss was real on the previous HEAD, but that rationale was wrong, and the last record above is why: the Alt key-up carries u_char=0x0000 under VT input. is_alt_code requires u_char != 0, so it cannot fire under VT regardless of how records are routed. It isn't being bypassed; it has nothing to act on.
What actually happens is that conhost supplies the character itself as VT bytes. On the previous HEAD those still arrived, but the discarded key-up meant nothing completed, and on this HEAD they arrive alongside a third delivery from the VK arm. I based the earlier claim on injected records rather than a typed Alt sequence, and injection doesn't reproduce what conhost does with a real Alt code.
The consequence for the fix is that the two arms can now both produce a character for the same keystroke, and only the record's own fields distinguish "this character already went out as a VT byte" from "this character exists only on the VK record".
4. An unterminated bracketed paste now withholds non-key events too
The ordering fix in a6599bad is right, and test_pending_sequence_at_queue_front_blocks_later_event covers the case it was written for. That case always resolves, because a lone ESC parses to KeyCode::Esc once flush() passes more=false.
The bracketed-paste case can't resolve. parse_csi_bracketed_paste returns Ok(None) regardless of input_available — deliberately, so a queue drain can't split a payload, which I noted approvingly last time — and flush_impl preserves the buffer on Ok(None). With buffer_event_position == 0 the new guard in Iterator::next then holds back everything queued behind it, and nothing can lift it.
Same reproducer against both trees:
let mut p = Parser::default();
p.advance(b"\x1b[200~hello", false);
p.push_event(InternalEvent::Event(Event::Resize(80, 24)));
p.push_event(mouse(7));
p.flush();
let drained: Vec<_> = std::iter::from_fn(|| p.next()).collect();where mouse(7) is any InternalEvent::Event(Event::Mouse(..)).
drained |
|
|---|---|
3ca5429 |
[Resize(80, 24), Mouse(col 7)] |
11e4912 |
[] (buffer.len() == 11, buffer_event_position == 0) |
You already have the unterminated paste itself on the deferred list and I'm not asking to move it. The change is in what it costs when it happens: previously the application stopped receiving keys, and now it stops receiving Event::Resize as well, so it can't even redraw at the right size while wedged. If the eventual parser-hardening change caps the buffer, this goes away with it. It seemed worth recording next to that item rather than discovering it later.
5. Three symbols are dead on Windows now
A non-test Windows build:
cargo build --locked --lib --no-default-features --features windows,events
warning: function `parse_event_with_raw_mode` is never used
--> src\event\sys\parse.rs:40:15
warning: methods `advance` and `flush` are never used
--> src\event\sys\parse.rs:1773:19
Clean on 3ca5429. Checking the three:
Parser::advance(:1773) has noallowat all. Live on Unix (source/unix/tty.rs:150,source/unix/mio.rs:99), dead on Windows, which usesadvance_with_raw_mode.Parser::flush(:1853) has#[cfg_attr(unix, allow(dead_code))]. It has no non-test caller on either platform; Windows usesflush_with_raw_mode.parse_event_with_raw_mode(:40) has#[cfg_attr(not(windows), allow(dead_code))]but is only called from tests, so Windows warns too.
Nothing fails on this: the warnings do show up in the Windows test job, which builds --all-targets and so compiles the non-test lib, but -D warnings only runs in the ubuntu clippy job. It reaches downstream Windows builds, though, which is why I'd rather mention it than not.
This is partly my fault for how I phrased the cfg_attr note last time. push_event is correct as written.
A concern I raised and then withdrew
I thought the new buffer_event_position guard in Iterator::next might stall Unix, since Parser is shared and the Unix sources have no flush() call to lift it. It can't. The Unix sources only ever call advance and next, never push_event, so internal_events is populated exclusively by insert_buffered_event, which runs only when the buffer empties. A non-empty buffer therefore implies nothing was queued after the sequence began, and the guard can only ever refuse a pop that would have returned None. cargo test --locked --lib --all-features on Linux: 146 passed, 0 failed, 7 ignored.
Recording it because I nearly filed it.
Method
Two trees at 3ca5429 and 11e4912 from the same clone, same toolchain, same machine, so every before/after above differs only in HEAD. Windows 11 26200, rustc 1.96.0; the Linux runs are the same source in WSL. Three programs behind it: the JSONL event logger from my earlier comment, the read-only raw record dump quoted above, and the two-tree parser reproducer. Happy to post any of them.
Not covered
Same list as last time, none of it exercised on this HEAD: legacy console (ForceV2=0), the disable_mouse_capture() path, large pastes, a CRLF clipboard, WaveTerm. The re-check of bracketed paste across the terminal set is also not redone here, since nothing in this push touches the paste path I measured before. Say which of these would help and I'll run them.
@joshka For what it's worth, this looks close to done to me. Everything I raised in the adversarial pass has been dealt with, and what's left is one CI line plus two behaviors that fall out of the key-up fix and want a deliberate decision rather than more digging. I hope the review was some use in getting it to a state you can take another look at.
|
I tested this PR through xi-agent on native Windows 11 ( The issue appears to be the interaction described in microsoft/terminal#15296. With I confirmed a local workaround: keep the existing Win32 with the corresponding disable sequences in reverse order during cleanup: With dual capture enabled, all of the following work in Windows Terminal:
I did not observe duplicated mouse events. The Win32 capture should remain enabled for conhost, which may continue delivering Reproduction:
The review currently states that preventing ANSI execution for |
|
Thank you both! |
|
@easyinplay @larsch Thank you for your feedback. The latest version has been validated on both Windows Terminal and the modern Windows Console Host, including raw-mode key releases, bracketed paste, mouse input, Alt+numpad input, and console-mode restoration. Could you please take another look when you have a chance? |
|
Tested the latest PR HEAD ( I removed xi-agent’s local VT mouse workaround and verified the upstream implementation directly in both Windows Terminal and modern Console Host (
This addresses the mouse regression I previously reported. I did not observe missing or duplicated mouse events. Thanks for incorporating the feedback. |
Fix #737
Fix #962
Summary
ENABLE_VIRTUAL_TERMINAL_INPUTinstead of enabling it implicitly.EnableBracketedPaste, allowing modern Windows terminals to deliver bracketed paste without changing keyboard semantics for every raw-mode user.TERMindicates ANSI support but no Win32CONIN$console is available.sys/unix/parsetosys/parse.WindowsEventSourceconstruction side-effect free and select the VT or VK path from the actual console mode for each input batch.?1003;1006representation, so mouse input continues across VT transport changes.ReadConsoleInputVK handling when VT input is inactive or unavailable.Scope on Windows
The source reads the console mode once per batch. Character-bearing key-down records use the shared ANSI parser only while VT input is active; other records continue through the Win32 path when the host supplies them.
Event::Paste)MOUSE_EVENTpathINPUT_RECORDhandlingKeyboard input is still expected to be read in raw mode. Reading events without entering raw mode remains valid for resize, focus, and mouse events and does not change the console mode as a side effect.
Explicitly out of scope: the kitty keyboard protocol.
supports_keyboard_enhancement()still returnsOk(false)on Windows andPushKeyboardEnhancementFlagsstill reportsErrorKind::Unsupported. Sharing the parser removes one decoding obstacle, but negotiating that protocol on Windows is separate work.Console-mode lifecycle
WindowsEventSource::new()opensCONIN$but does not modify its mode.enable_raw_mode()captures the first-touch console mode, changes only the Windows raw-mode bits, and preserves the current VT-input bit.disable_raw_mode()restores the cooked-mode bits while likewise preserving the current VT-input bit.EnableBracketedPasteflushes prior output, saves the current VT-input bit in a bracketed-paste-specific state, enables VT input, writesCSI ? 2004 h, and flushes again before returning.DisableBracketedPastewritesCSI ? 2004 l, flushes it, and only then restores the VT-input bit observed immediately before the paired enable. It preserves every other current console-mode bit and consumes the saved state only after a successful restore.CONIN$console, both commands still emit their ANSI sequences and safely skip the WinAPI mode side effects.ConsoleandConsoleModeshare the sameCONIN$handle, and the real VT and raw-mode state is read once per batch.The ordering is deliberate: VT input must be active before enabling the terminal protocol, while the disable sequence must be flushed before restoring the input transport. Failures short-circuit later steps so the parser transport is not disabled before the terminal has accepted
CSI ? 2004 l.Command execution on Windows
The hidden
Commandimplementation hooks distinguish:This preserves the existing legacy fallback while supporting the coordinated mouse and bracketed-paste operations. Hybrid commands are rejected by the internal
fmt::Writeformatting path because it cannot provide the required I/O flush ordering.Mouse transport
Windows mouse capture now keeps the WinAPI flags and, when ANSI is supported, also emits
?1003;1006h/l.MOUSE_EVENTrecords.Key releases and Alt+numpad
When a host supplies character-bearing key-up records during VT input, releases are normalized through the same control-character and uppercase rules as the shared ANSI parser. This keeps the public Press and Release identities aligned.
Some hosts do not supply those records. In particular, Windows Terminal/ConPTY was observed to expose ordinary characters, function keys, modified characters, and Alt-code input as Press-only while VT input is active. This PR does not synthesize missing releases because doing so would duplicate real releases on other hosts and would invent hold, repeat, and ordering semantics.
For the conhost Alt+numpad trace, suppression is limited to the exact adjacent sequence: an Alt-bearing
VK_PACKETrelease followed immediately by the same character as a key-down record with no virtual-key code. Mismatches, non-key records, and VT mode changes clear the candidate.Input batching and ordering
read_console_input()and only the records actually returned are processed. This removes the additional per-record blocking race introduced by repeatedly callingread_single_input_event(). The pre-existing race where another console reader drains the entire queue between readiness andReadConsoleInputWremains.flush()or completion inserts the parsed event at that position, preserving its order relative to mouse, focus, resize, and VK-translated events.Esc and Alt sequence boundary
With VT input enabled for bracketed paste, Windows delivers keyboard input as the same byte stream used by Unix terminals. This carries the same inherent ambiguity between a standalone
Esckey and the prefix of an Alt or CSI sequence.The parser uses the currently available input batch: a quickly followed key can combine with
Escas an Alt-prefixed sequence, while a prefix split at a queue-empty boundary can be emitted as a standaloneEsc. This PR intentionally keeps that Unix-compatible behavior rather than adding a Windows-only timing threshold.Once the complete bracketed-paste opener (
ESC [ 200 ~) has been recognized, temporary queue drains do not split the paste payload. A split at the openingEscremains subject to the same boundary.Diagnostic example
windows-vt-inputprovides two phases in one run:Event::Paste, SGR mouse, physical-numpad Alt codes, and the documented Press-only host behavior.It drains input already queued when the event source is constructed (typically the command-launch Enter release) before Phase 1 statistics begin. Its key accounting distinguishes additional Press records emitted while a key is held from genuinely unmatched identities, while retaining signed detection for release-first, extra-release, modifier-mismatch, and Press-only cases. It also reports narrowly matched Alt-code duplicate candidates, mouse activity without flooding moved events, mode transitions, partial-setup cleanup, and restoration of VT, mouse, window, and extended console-input flags. When VT input was already active before startup, Phase 1 advances on the transition-key press because ConPTY may not provide its release; otherwise it observes the press and release before enabling paste.
Follow-up work
An unterminated bracketed paste can keep the shared parser waiting indefinitely and grow its buffer without a bound. This behavior already exists on Unix and is now reachable on Windows. Choosing a buffer limit and recovery behavior affects both platforms and is deferred to a separate parser-hardening change.
Tested
cargo fmt --all -- --checkcargo test --locked --all-targets --all-features -- --test-threads 1: 153 library tests passed, 0 failed, 7 ignored, plus integration targets and 4 diagnostic-accounting example testscargo clippy --locked --all-targets --all-features -- -D warningscargo check --locked --all-targets --no-default-features(one pre-existingfile_descriptor::readdead-code warning)cargo test --locked --all-targets --no-default-features: 52 passed, 0 failed, 1 ignored, plus integration and example targets245d5c6was manually validated on both Windows Terminal and modern conhost: raw mode preserved VT input off; Phase 1 retained Win32 Press/Release and mouse behavior; Phase 2 delivered multilineEvent::Paste, SGR click/drag/wheel input, and one Alt+numpad character Press without a duplicate; cleanup restored the original VT, mouse, window, and extended input bits.94ed715changes only diagnostic startup-event and held-key accounting based on those traces. Its four example tests cover the balance invariants, and a modern-conhost run confirmed that the startup Enter release is separated, additional held-key Press records do not inflate imbalance, and a Press-only VT identity still remains visible as one mismatch.