Skip to content

Commit 51c20c3

Browse files
committed
feat(tui): add Ctrl+n/Ctrl+p navigation to selection popups
Extends keyboard navigation support to approval mode, model selection, and rate limit switch popups, matching the behavior added in #1994 for slash commands and file selector.
1 parent b1c918d commit 51c20c3

File tree

1 file changed

+86
-0
lines changed

1 file changed

+86
-0
lines changed

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,20 @@ impl BottomPaneView for ListSelectionView {
266266
match key_event {
267267
KeyEvent {
268268
code: KeyCode::Up, ..
269+
}
270+
| KeyEvent {
271+
code: KeyCode::Char('p'),
272+
modifiers: KeyModifiers::CONTROL,
273+
..
269274
} => self.move_up(),
270275
KeyEvent {
271276
code: KeyCode::Down,
272277
..
278+
}
279+
| KeyEvent {
280+
code: KeyCode::Char('n'),
281+
modifiers: KeyModifiers::CONTROL,
282+
..
273283
} => self.move_down(),
274284
KeyEvent {
275285
code: KeyCode::Backspace,
@@ -713,4 +723,80 @@ mod tests {
713723
render_lines_with_width(&view, 24)
714724
);
715725
}
726+
727+
#[test]
728+
fn ctrl_n_moves_selection_down() {
729+
let mut view = make_selection_view(None);
730+
// Initial selection is on first item (Read Only which is_current)
731+
let initial = render_lines(&view);
732+
assert!(
733+
initial.contains("› 1. Read Only"),
734+
"expected first item selected initially"
735+
);
736+
737+
// Press Ctrl+n to move down
738+
view.handle_key_event(KeyEvent::new(
739+
KeyCode::Char('n'),
740+
KeyModifiers::CONTROL,
741+
));
742+
let after_ctrl_n = render_lines(&view);
743+
assert!(
744+
after_ctrl_n.contains("› 2. Full Access"),
745+
"expected second item selected after Ctrl+n"
746+
);
747+
}
748+
749+
#[test]
750+
fn ctrl_p_moves_selection_up() {
751+
let mut view = make_selection_view(None);
752+
// Move down first so we can move up
753+
view.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
754+
let after_down = render_lines(&view);
755+
assert!(
756+
after_down.contains("› 2. Full Access"),
757+
"expected second item selected after Down"
758+
);
759+
760+
// Press Ctrl+p to move up
761+
view.handle_key_event(KeyEvent::new(
762+
KeyCode::Char('p'),
763+
KeyModifiers::CONTROL,
764+
));
765+
let after_ctrl_p = render_lines(&view);
766+
assert!(
767+
after_ctrl_p.contains("› 1. Read Only"),
768+
"expected first item selected after Ctrl+p"
769+
);
770+
}
771+
772+
#[test]
773+
fn ctrl_n_and_ctrl_p_wrap_around() {
774+
let mut view = make_selection_view(None);
775+
// Move to second item
776+
view.handle_key_event(KeyEvent::new(
777+
KeyCode::Char('n'),
778+
KeyModifiers::CONTROL,
779+
));
780+
// Move past last item - should wrap to first
781+
view.handle_key_event(KeyEvent::new(
782+
KeyCode::Char('n'),
783+
KeyModifiers::CONTROL,
784+
));
785+
let wrapped_forward = render_lines(&view);
786+
assert!(
787+
wrapped_forward.contains("› 1. Read Only"),
788+
"expected selection to wrap to first item"
789+
);
790+
791+
// Move up from first item - should wrap to last
792+
view.handle_key_event(KeyEvent::new(
793+
KeyCode::Char('p'),
794+
KeyModifiers::CONTROL,
795+
));
796+
let wrapped_back = render_lines(&view);
797+
assert!(
798+
wrapped_back.contains("› 2. Full Access"),
799+
"expected selection to wrap to last item"
800+
);
801+
}
716802
}

0 commit comments

Comments
 (0)