Skip to content

Commit a154d28

Browse files
committed
fix(tui): handle C0 control chars for Ctrl+n/Ctrl+p in selection popups
Some terminals send Control key chords as C0 control characters without reporting the CONTROL modifier. Add fallback handling for ^N (U+000E) and ^P (U+0010) to ensure consistent navigation across all terminal environments. Follows the same pattern used in textarea.rs (see #7530).
1 parent 51c20c3 commit a154d28

File tree

1 file changed

+43
-2
lines changed

1 file changed

+43
-2
lines changed

codex-rs/tui/src/bottom_pane/list_selection_view.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,22 @@ impl ListSelectionView {
264264
impl BottomPaneView for ListSelectionView {
265265
fn handle_key_event(&mut self, key_event: KeyEvent) {
266266
match key_event {
267+
// Some terminals (or configurations) send Control key chords as
268+
// C0 control characters without reporting the CONTROL modifier.
269+
// Handle fallbacks for Ctrl-P/N here so navigation works everywhere.
267270
KeyEvent {
268271
code: KeyCode::Up, ..
269272
}
270273
| KeyEvent {
271274
code: KeyCode::Char('p'),
272275
modifiers: KeyModifiers::CONTROL,
273276
..
274-
} => self.move_up(),
277+
}
278+
| KeyEvent {
279+
code: KeyCode::Char('\u{0010}'),
280+
modifiers: KeyModifiers::NONE,
281+
..
282+
} /* ^P */ => self.move_up(),
275283
KeyEvent {
276284
code: KeyCode::Down,
277285
..
@@ -280,7 +288,12 @@ impl BottomPaneView for ListSelectionView {
280288
code: KeyCode::Char('n'),
281289
modifiers: KeyModifiers::CONTROL,
282290
..
283-
} => self.move_down(),
291+
}
292+
| KeyEvent {
293+
code: KeyCode::Char('\u{000e}'),
294+
modifiers: KeyModifiers::NONE,
295+
..
296+
} /* ^N */ => self.move_down(),
284297
KeyEvent {
285298
code: KeyCode::Backspace,
286299
..
@@ -799,4 +812,32 @@ mod tests {
799812
"expected selection to wrap to last item"
800813
);
801814
}
815+
816+
#[test]
817+
fn c0_control_chars_navigate_selection() {
818+
let mut view = make_selection_view(None);
819+
// Initial selection is on first item
820+
let initial = render_lines(&view);
821+
assert!(
822+
initial.contains("› 1. Read Only"),
823+
"expected first item selected initially"
824+
);
825+
826+
// Simulate terminals that send C0 control chars without CONTROL modifier.
827+
// ^N (U+000E) should move down
828+
view.handle_key_event(KeyEvent::new(KeyCode::Char('\u{000e}'), KeyModifiers::NONE));
829+
let after_c0_n = render_lines(&view);
830+
assert!(
831+
after_c0_n.contains("› 2. Full Access"),
832+
"expected second item selected after ^N (C0)"
833+
);
834+
835+
// ^P (U+0010) should move up
836+
view.handle_key_event(KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE));
837+
let after_c0_p = render_lines(&view);
838+
assert!(
839+
after_c0_p.contains("› 1. Read Only"),
840+
"expected first item selected after ^P (C0)"
841+
);
842+
}
802843
}

0 commit comments

Comments
 (0)