tui: support Ghostty Ctrl-b/Ctrl-f fallback (#2427)

Ensure Emacs-style Ctrl-b/Ctrl-f work when terminals send bare control
chars.

- Map ^B (U+0002) to move left when no CONTROL modifier is reported.
- Map ^F (U+0006) to move right when no CONTROL modifier is reported.
- Preserve existing Ctrl-b/Ctrl-f and Alt-b/Alt-f behavior.
- Add unit test covering the fallback path.

Background: Ghostty (and some tmux/terminal configs) can emit bare
control characters for Ctrl-b/Ctrl-f. Previously these could be treated
as literal input; with this change both styles behave identically.
This commit is contained in:
Evan Verma
2025-08-19 10:27:50 -07:00
committed by GitHub
parent 7b4313bf31
commit 9d3124c6b7

View File

@@ -204,6 +204,16 @@ impl TextArea {
pub fn input(&mut self, event: KeyEvent) {
match event {
// Some terminals (or configurations) send Control key chords as
// C0 control characters without reporting the CONTROL modifier.
// Handle common fallbacks for Ctrl-B/Ctrl-F here so they don't get
// inserted as literal control bytes.
KeyEvent { code: KeyCode::Char('\u{0002}'), modifiers: KeyModifiers::NONE, .. } /* ^B */ => {
self.move_cursor_left();
}
KeyEvent { code: KeyCode::Char('\u{0006}'), modifiers: KeyModifiers::NONE, .. } /* ^F */ => {
self.move_cursor_right();
}
KeyEvent {
code: KeyCode::Char(c),
// Insert plain characters (and Shift-modified). Do NOT insert when ALT is held,
@@ -1142,6 +1152,25 @@ mod tests {
assert_eq!(t.cursor(), 1);
}
#[test]
fn control_b_f_fallback_control_chars_move_cursor() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
let mut t = ta_with("abcd");
t.set_cursor(2);
// Simulate terminals that send C0 control chars without CONTROL modifier.
// ^B (U+0002) should move left
t.input(KeyEvent::new(KeyCode::Char('\u{0002}'), KeyModifiers::NONE));
assert_eq!(t.cursor(), 1);
// ^F (U+0006) should move right
t.input(KeyEvent::new(KeyCode::Char('\u{0006}'), KeyModifiers::NONE));
assert_eq!(t.cursor(), 2);
}
#[test]
fn cursor_vertical_movement_across_lines_and_bounds() {
let mut t = ta_with("short\nloooooooooong\nmid");