Skip to content

Comments

fix(emulator): Fix for keyboard repeat#1021

Merged
gdamore merged 1 commit intomainfrom
key-repeat
Jan 27, 2026
Merged

fix(emulator): Fix for keyboard repeat#1021
gdamore merged 1 commit intomainfrom
key-repeat

Conversation

@gdamore
Copy link
Owner

@gdamore gdamore commented Jan 27, 2026

Summary by CodeRabbit

  • New Features
    • Configurable keyboard repeat (delay/interval) and improved repeat handling across layouts, modifiers, cursor keys, keypad and UTF/ASCII input; ALT/CTRL-modified keys now suppress repeats where appropriate.
  • Tests
    • Added comprehensive key-repeat tests covering basic repeats, modifiers (Shift/CapsLock/Alt), cursor keys, and release behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Adds key-repeat management: new emulator helpers (repeatRaw, noRepeatRaw), a public KeyboardState with repeat configuration and timing, MockTerm SetRepeat, and tests exercising repeat behavior across modifiers and cursor keys.

Changes

Cohort / File(s) Summary
Emulator helpers & key handling
vt/emulate.go
Adds repeatRaw(ev KeyEvent, data []byte) and noRepeatRaw(ev KeyEvent, data []byte); replaces direct SendRaw calls inside legacy key handling to selectively allow or suppress repeats and adds explicit returns after those calls.
Keyboard state & repeat API
vt/layout.go
Introduces public KeyboardState type with repeatTime, lazy initialization, SetRepeat(delay, interval time.Duration), reset/clearRepeat semantics, and updated Pressed/Released/Repeat logic using configured timings.
Mock terminal API
vt/mock.go
Adds SetRepeat(delay, interval time.Duration) to vt.MockTerm and implements it on *mockTerm, delegating to ks.SetRepeat.
Tests for repeat behavior
vt/tests/key_test.go
Adds multiple tests: TestKeyRepeat, TestKeyRepeatCapsLock, TestKeyRepeatNoAlt, TestKeyRepeatShift, TestKeyRepeatShiftRelease, TestKeyRepeatCursor; configures repeats via SetRepeat and verifies emitted sequences.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant KeyboardState as "KeyboardState\n(vt/layout.go)"
  participant Emulator as "emulator\n(vt/emulate.go)"
  participant MockTerm as "mockTerm\n(vt/mock.go)"
  participant Output as "Terminal Output"

  Client->>MockTerm: Inject KeyPress(KeyEvent)
  MockTerm->>KeyboardState: Pressed(ev)
  alt first press or repeating enabled
    KeyboardState->>MockTerm: schedule repeat ticks (delay/interval)
  end
  MockTerm->>Emulator: handle keyLegacy(ev)
  alt ALT/CTRL or non-repeatable
    Emulator->>Emulator: noRepeatRaw(ev, data)
    Emulator->>Output: SendRaw(data) [single]
  else repeatable
    Emulator->>Emulator: repeatRaw(ev, data)
    loop up to configured repeats
      Emulator->>Output: SendRaw(data)
    end
  end
  Note over KeyboardState,Emulator: On repeat tick -> MockTerm invokes Pressed/Repeat -> Emulator re-emits via repeatRaw/noRepeatRaw
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop, I press, then gently peep,
Repeat or hush — I guard the heap,
repeatRaw dances, noRepeatRaw still,
Keys sing again on timing's will,
A tiny rabbit cheers the new key sweep.

🚥 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 accurately describes the main change: implementing keyboard repeat functionality fixes in the emulator, which is reflected across multiple files (emulate.go, layout.go, mock.go, and tests).
Docstring Coverage ✅ Passed Docstring coverage is 87.50% 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 docstrings

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.

@codecov
Copy link

codecov bot commented Jan 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.95%. Comparing base (d8faf89) to head (2f78a3c).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1021      +/-   ##
==========================================
+ Coverage   85.77%   85.95%   +0.17%     
==========================================
  Files          46       46              
  Lines        4817     4834      +17     
==========================================
+ Hits         4132     4155      +23     
+ Misses        534      528       -6     
  Partials      151      151              

☔ 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@vt/layout.go`:
- Around line 132-136: The timing uses ks.repeatInterval for the initial delay
when a new key is pressed; change the logic so that when lastKey != k and
ks.repeatInterval != 0 you call ks.clearRepeat(), then set ks.repeatStart =
time.Now().Add(ks.repeatDelay) (not repeatInterval) and set ks.repeatTime =
ks.repeatStart so the first repeat honors the configured initial delay; update
the block around lastKey, k, ks.clearRepeat(), ks.repeatStart and ks.repeatTime
accordingly.

In `@vt/tests/key_test.go`:
- Around line 339-355: TestKeyRepeatCursor contains a duplicate consecutive call
to term.KeyRelease(vt.KeyRight); remove the extra KeyRelease invocation so there
is only a single KeyRelease(vt.KeyRight) where the key should be released;
locate this in the TestKeyRepeatCursor function and delete the redundant
term.KeyRelease(vt.KeyRight) call to clean up the test.
🧹 Nitpick comments (1)
vt/tests/key_test.go (1)

240-257: Consider increasing timing margins for CI stability.

Time-based tests with tight tolerances (50ms delay, 25ms interval, 100ms sleep expecting exactly 4 outputs) may be flaky in CI environments where scheduling delays can cause timing variations.

The expected output "xxxx" assumes repeats at 0ms, 50ms, 75ms, 100ms. However, if the system is under load, timing could drift causing fewer or more repeats.

Consider either:

  1. Using more generous timing margins (e.g., 200ms delay, 100ms interval, 500ms sleep)
  2. Accepting a range of valid outputs rather than exact counts
  3. Adding a comment acknowledging potential CI flakiness
💡 Example with more generous timing
-	// these are unreasonable repeat rates, but its somewhere to start
-	term.SetRepeat(time.Millisecond*50, time.Millisecond*25)
+	// Use generous timing to reduce CI flakiness
+	term.SetRepeat(time.Millisecond*100, time.Millisecond*50)

 	term.KeyPress(vt.KeyX)
-	time.Sleep(100 * time.Millisecond)
+	time.Sleep(250 * time.Millisecond)
 	term.KeyPress(vt.KeyX)
 	term.KeyRelease(vt.KeyX)
 	term.KeyRelease(vt.KeyCapsLock)

-	CheckRead(t, term, "xxxx") // 0 ms, 50 ms, 75 ms, 100 ms
+	CheckRead(t, term, "xxxx") // 0 ms, 100 ms, 150 ms, 200 ms

Comment on lines +339 to +355
func TestKeyRepeatCursor(t *testing.T) {
term := vt.NewMockTerm()
defer MustClose(t, term)

MustStart(t, term)

// these are unreasonable repeat rates, but its somewhere to start
term.SetRepeat(time.Millisecond*50, time.Millisecond*25)

term.KeyPress(vt.KeyRight)
time.Sleep(100 * time.Millisecond)
term.KeyPress(vt.KeyRight)
term.KeyRelease(vt.KeyRight)
term.KeyRelease(vt.KeyRight)

CheckRead(t, term, "\x1b[C\x1b[C\x1b[C\x1b[C") // 0 ms, 50 ms, 75 ms, 100 ms
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Duplicate KeyRelease call.

Line 352 calls term.KeyRelease(vt.KeyRight) twice in succession. While harmless (second release on an already-released key), this appears to be a copy-paste artifact.

🧹 Suggested fix
 	term.KeyPress(vt.KeyRight)
 	time.Sleep(100 * time.Millisecond)
 	term.KeyPress(vt.KeyRight)
 	term.KeyRelease(vt.KeyRight)
-	term.KeyRelease(vt.KeyRight)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestKeyRepeatCursor(t *testing.T) {
term := vt.NewMockTerm()
defer MustClose(t, term)
MustStart(t, term)
// these are unreasonable repeat rates, but its somewhere to start
term.SetRepeat(time.Millisecond*50, time.Millisecond*25)
term.KeyPress(vt.KeyRight)
time.Sleep(100 * time.Millisecond)
term.KeyPress(vt.KeyRight)
term.KeyRelease(vt.KeyRight)
term.KeyRelease(vt.KeyRight)
CheckRead(t, term, "\x1b[C\x1b[C\x1b[C\x1b[C") // 0 ms, 50 ms, 75 ms, 100 ms
}
func TestKeyRepeatCursor(t *testing.T) {
term := vt.NewMockTerm()
defer MustClose(t, term)
MustStart(t, term)
// these are unreasonable repeat rates, but its somewhere to start
term.SetRepeat(time.Millisecond*50, time.Millisecond*25)
term.KeyPress(vt.KeyRight)
time.Sleep(100 * time.Millisecond)
term.KeyPress(vt.KeyRight)
term.KeyRelease(vt.KeyRight)
CheckRead(t, term, "\x1b[C\x1b[C\x1b[C\x1b[C") // 0 ms, 50 ms, 75 ms, 100 ms
}
🤖 Prompt for AI Agents
In `@vt/tests/key_test.go` around lines 339 - 355, TestKeyRepeatCursor contains a
duplicate consecutive call to term.KeyRelease(vt.KeyRight); remove the extra
KeyRelease invocation so there is only a single KeyRelease(vt.KeyRight) where
the key should be released; locate this in the TestKeyRepeatCursor function and
delete the redundant term.KeyRelease(vt.KeyRight) call to clean up the test.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, but its benign, and it actually has a benefit of testing a code path that was not previously tested (release without matching press), so I am inclined to leave it there for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gdamore, that makes perfect sense! Testing the release-without-matching-press code path is valuable, and I appreciate you clarifying the intent. I'll leave it as-is.


✏️ Learnings added
Learnt from: gdamore
Repo: gdamore/tcell PR: 1021
File: vt/tests/key_test.go:339-355
Timestamp: 2026-01-27T17:18:21.368Z
Learning: In vt/tests/key_test.go, the duplicate `term.KeyRelease(vt.KeyRight)` call in TestKeyRepeatCursor (line 352) is intentional to test the code path for releasing a key without a matching press, not a copy-paste error.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: gdamore
Repo: gdamore/tcell PR: 959
File: vt/emulate.go:0-0
Timestamp: 2025-12-31T22:01:49.486Z
Learning: In vt/emulate.go, the autoWrap (pending wrap) flag behavior is directional: backward/leftward cursor movements always clear autoWrap because they move away from the right column, while forward movements may or may not clear it depending on the specific action. CHT (cursor forward tab) intentionally does not clear autoWrap, aligning with HT (horizontal tab) behavior.

@gdamore gdamore merged commit 720eeab into main Jan 27, 2026
15 checks passed
@gdamore gdamore deleted the key-repeat branch January 27, 2026 21:44
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.

1 participant