Stop terminal reports from typing themselves into the input box - #844
Merged
Conversation
A user watched text pour into the compose field on its own — "nonstop input, as if I had been holding something down". crossterm 0.29 has no parser for OSC / DCS / APC sequences. `parse_event` handles `ESC O`, `ESC [` and `ESC ESC`; every other byte after ESC falls through to a recursive call that reports the introducer as an Alt-modified character, and `Parser::advance` then clears its buffer — so the payload is parsed byte-by-byte as ordinary text. A background-colour report, `ESC ] 11 ; rgb:2e2e/3434/3636 BEL`, therefore arrived as Alt+`]` plus twenty-one characters, every one of which the compose editor inserted. Terminals emit these unprompted (colour reports on a theme change or an alt-screen transition, an OSC 52 clipboard reply, DCS XTGETTCAP replies, kitty's OSC 99 notification reports), and in a multiplexer one pane can receive the reply to another pane's query. dirge already drained that chatter at startup, at teardown and around a suspended subprocess; nothing stood between it and the editor mid-session. Four changes, all in the input path: - The input reader recognises an OSC / DCS / APC / SOS / PM introducer and swallows the run through its terminator, including an ST split across two parses. A run it cannot close — no terminator, a human-scale gap, or more than 4096 events — is released as ordinary keystrokes rather than dropped, so a deliberate Alt+`]` is delayed by 25ms and never lost, and an AltGr-composed `]` (Ctrl+Alt on the Windows layouts) is still typing. (dirge-v4xf) - Readers carry a generation. The suspend path proceeds after 150ms even when the reader has not exited and the resume path then clears the shutdown flag, so a stale reader woke to a `false` flag and kept reading fd 0 next to its replacement — and next to the stdin drains, which splits escape sequences and lands the same junk in the compose box by a different route. Claiming a generation retires every older reader at its next tick, and only the live generation may report the reader gone. (dirge-xxo9) - Poll/read errors are logged and retried a few times instead of ending the thread for the session, which left a healthy-looking UI that accepted no keys. The dead-tty probe still owns the unrecoverable case. (dirge-sp1x) - `?1003h` (any-event mouse tracking) is no longer enabled. Nothing consumed it — the reader maps the wheel and left button down/drag/up and drops the rest, `MouseEventKind::Moved` appears nowhere in the tree, and `?1002h` already covers the wheel and the drag-selection. All it bought was the only continuous input byte stream in the program, which is what turns a one-off desync on fd 0 into a sustained flood rather than a single burst. `?1015h` (urxvt encoding, which crossterm cannot parse) goes with it; the teardown strings still clear both. (dirge-hn6e) Tests: unit tests for the filter (report swallowed whole, both terminators, split ST, cap released not dropped, AltGr `]` untouched, ordinary typing untouched) and PTY-level tests that push the real bytes through fd 0 and the production reader — which is what pins the crossterm behaviour the filter assumes — plus one that a retired reader stops consuming input.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The report
Root cause
crossterm 0.29 has no parser for OSC / DCS / APC sequences. In
src/event/sys/unix/parse.rs,parse_eventhandles onlyESC O,ESC [andESC ESC; every other byte after ESC falls through toso
ESC ]yieldsKey(Alt+']')— andParser::advance(
src/event/source/unix/tty.rs:245) then clears its buffer. The rest ofthe report is fed to the parser byte-by-byte with no
ESCcontext, so everypayload byte parses as a standalone plain
Char. BEL becomesCtrl+G; STbecomes
Alt+\.A background-colour report:
therefore reached dirge as
Alt+]followed by the literal characters11;rgb:2e2e/3434/3636, andui::inputinserts every unmodifiedCharintothe compose buffer — so the user watches text type itself into the input box.
Terminals emit these unprompted: colour reports on a theme change or an
alt-screen transition (kitty, ghostty, foot, iTerm2), an OSC 52 clipboard
reply (kilobytes of base64 — the shape that looks like a held-down key), DCS
XTGETTCAP / DECRQSS replies, kitty's OSC 99 notification reports; and in a
multiplexer one pane can be handed the reply to another pane's query. dirge
already knew about this chatter —
sync_and_drain_via_sentinelanddrain_stdin_nonblockingexist to swallow "alt-screen-exit chatter fromiTerm2 / kitty / foot — OSC 11 bg-color, primary DA, spontaneous CPR" — but
those drains only run at startup, at teardown, and around a suspended
subprocess. Once the reader is live, nothing stood between a report and the
editor.
What changed
The input reader filters reports (dirge-v4xf). It recognises an OSC / DCS
/ APC / SOS / PM introducer and swallows the run through its terminator (BEL
or ST, including an ST split across two parses). Deliberately conservative in
the other direction: a run it cannot close — no terminator, a human-scale gap
(25ms), or more than 4096 events — is released as ordinary keystrokes rather
than dropped, so a real Alt+
]is delayed and never lost. An AltGr-composed](Ctrl+Alt on the Italian/German/Spanish layouts, GH #659) is still typing.Readers carry a generation (dirge-xxo9).
suspend_tui_for_subprocessgives the reader 150ms and then proceeds anyway (it says so on stderr);
resume_tui_after_subprocessclears the shutdown flag and starts areplacement. The loop never latched the flag, so a reader that hadn't exited
woke to a
falseflag and kept reading fd 0 next to its replacement — andnext to the stdin drains, and
EVENT_READER_EXITEDwas then set by whicheverexited first, so the next suspend's barrier could pass with a reader still
consuming. Two consumers on one descriptor split escape sequences, and the
tail of a split sequence parses as plain text: the same symptom by a different
route. Claiming a generation now retires every older reader at its next tick,
and only the live generation may report the reader gone.
Poll/read errors are retried (dirge-sp1x). Both mapped
Errtobreak,which ended the thread for the session with nothing outside suspend/resume to
restart it — a painted UI that accepts no keys, indistinguishable from a hang,
with the dead-tty watchdog quiet because the terminal was alive. Errors are
logged and retried a few times; the dead-tty probe still owns the case that
cannot recover.
?1003his no longer enabled (dirge-hn6e). Any-event mouse tracking madethe terminal report every cell of pointer motion with no button held, and
nothing consumed it: the reader maps the wheel and left button down/drag/up
and drops the rest, and
MouseEventKind::Movedappears nowhere in the tree.?1002halready covers the wheel and the drag-selection. All the mode boughtwas the only continuous input byte stream in the program — parsed and thrown
away whenever the pointer crossed the window, and the fuel that turns a
one-off desync on fd 0 into a sustained flood rather than a single burst.
?1015h(urxvt encoding, which crossterm cannot parse) goes with it. Theteardown and panic-reset strings still clear both, since another program — or
an older dirge — may have set them.
Tests
ui::input_readerunit tests for the filter: report swallowed whole, bothterminators, ST split across two parses, the cap releasing rather than
swallowing forever, AltGr
]untouched, ordinary typing untouched, the idletick releasing a lone introducer.
ui::relay_tests::terminal_reportspushes the real byte sequences through aPTY on fd 0 and the production reader — this is what pins the crossterm
behaviour the filter assumes. Before the filter the first of them collected
22 events; it now expects only the trailing keystroke. One more test asserts
a retired reader stops consuming input.
renderer_testsnow assert?1003his absent from both re-assert payloadsand
?1002hpresent.Verified locally:
cargo fmt --all --check,cargo clippy --all-targets --no-default-features --features no-plugin,sandbox-microvm -- -D warnings,and the tests above. The
pluginfeature needs libclang, which this boxdoesn't have, so the default-feature and all-features builds are CI's to
confirm.