Skip to content

fix(input): handle ESC during CSI and SS3 parse states per ECMA-48#1053

Merged
gdamore merged 2 commits into
gdamore:mainfrom
ModeEngage:handle-esc-during-cs3-and-ss3
Apr 11, 2026
Merged

fix(input): handle ESC during CSI and SS3 parse states per ECMA-48#1053
gdamore merged 2 commits into
gdamore:mainfrom
ModeEngage:handle-esc-during-cs3-and-ss3

Conversation

@ModeEngage

@ModeEngage ModeEngage commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Disclosure: I stumbled across this while working on a personal project. AI tools were used to diagnose and resolve the issue as well as generate this potential upstream fix. Systems programming at this level is not my expertise, but the fix seemed small, contained, and testable so I thought it worthwhile to open a PR. AFAIK, the same issue is present in tcell v2.

The input parser's CSI and SS3 state handlers do not check for ESC (0x1B). Per ECMA-48 §5.3.1, ESC is "always effective" and should restart the escape sequence machine from any intermediate state. Instead, ESC falls into the "bad parse" branch in istCsi and is silently discarded, and in istSs3 it fails the key lookup and is similarly lost.

This causes the byte(s) following the discarded ESC to be misinterpreted. In practice, the user's first keystroke after Suspend/Resume can be swallowed: engage() sends escape sequences that may provoke terminal responses, and if those responses leave the parser mid-CSI, the next ESC-prefixed key (arrow keys, function keys) is consumed as the tail of the stale sequence.

The tty input flush added in #1048 reduces the likelihood of stale bytes reaching the parser, but does not eliminate it — terminal responses can arrive after the flush and before inputLoop starts, or bytes can be split across reads leaving a partial sequence in the parser's state machine.

This change adds an ESC check to both istCsi and istSs3 that transitions to istEsc and clears escChar, matching the behavior of istInit. Tests verify that a partial CSI or SS3 followed by a new escape sequence (e.g., arrow key) produces the correct key event.

Related: #194, #480, #1048

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling when an ESC byte arrives mid-way through an active escape sequence: the parser now correctly resets and emits the intended key event, preventing lost or misclassified input.
  • Tests

    • Added tests covering escape bytes arriving during active parsing and a test helper to ensure the first resulting key event is observed.

Properly check for ESC (`0x1B`) to prevent erroneously falling into the
'bad parse' branch of `intCsi` and `intSs3`.
@ModeEngage ModeEngage requested a review from gdamore as a code owner April 11, 2026 14:24
@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6ba8012-1259-4a6e-bf96-4c22004f8d4b

📥 Commits

Reviewing files that changed from the base of the PR and between 1de8363 and df47500.

📒 Files selected for processing (1)
  • input_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • input_test.go

📝 Walkthrough

Walkthrough

The parser now treats an incoming ESC (0x1b) byte received while accumulating CSI or SS3 parameters as a reset: it transitions to the escape state (istEsc) and clears escChar. Tests were added to verify CSI/SS3 mid-sequence ESC handling and a test helper was introduced.

Changes

Cohort / File(s) Summary
Escape sequence state handling
input.go
Handle ESC (0x1b) when seen during CSI (istCsi) and SS3 (istSs3) parameter/intermediate collection by transitioning to istEsc and clearing escChar, restarting the escape-sequence parser per ECMA‑48.
Parser tests & helper
input_test.go
Added firstKey test helper and two tests TestEscDuringCsiResetsParser and TestEscDuringSs3ResetsParser that send back-to-back escape sequences where an ESC arrives mid-sequence and assert correct event emission.

Sequence Diagram(s)

(Skipped — change is localized to parser state transitions without multi-component sequential flow requiring visualization.)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 I hopped through bytes both brave and small,

ESC sprang in mid-CSI, I heard the call.
I cleared the path and let sequences start anew,
Tests watch my feet — they know what I do.
A rabbit's cheer for parsers, tidy and true.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: handling ESC during CSI and SS3 parse states per ECMA-48 standard, which directly aligns with the core fix in input.go.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

The drain loop in the ESC-during-CSI/SS3 tests was keeping the last
EventKey, which could be overwritten by the escTimeout timer firing
during the 100ms drain window. Capture the first EventKey instead.
@gdamore

gdamore commented Apr 11, 2026

Copy link
Copy Markdown
Owner

I believe that this fix is good from a correctness... although unlikely to actually have any real world impact. I've never seen an aborted escape sequence in the wild, and I can think of no reason why a sender would ever start a sequence then abort it. (I also suspect that very few implementations actually would handle this case.)

That said, the change is small and low risk, and I'm happy to include it. Thank you.

@codecov

codecov Bot commented Apr 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.16%. Comparing base (c517008) to head (df47500).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1053      +/-   ##
==========================================
- Coverage   86.18%   86.16%   -0.03%     
==========================================
  Files          46       46              
  Lines        5060     5066       +6     
==========================================
+ Hits         4361     4365       +4     
- Misses        541      542       +1     
- Partials      158      159       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gdamore gdamore merged commit 73733d6 into gdamore:main Apr 11, 2026
10 of 11 checks passed
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