diff --git a/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__page_keys_follow_pager_scroll_bindings.snap b/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__page_keys_follow_pager_scroll_bindings.snap new file mode 100644 index 0000000000..09360038d5 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__page_keys_follow_pager_scroll_bindings.snap @@ -0,0 +1,43 @@ +--- +source: tui/src/transcript_view_tests.rs +expression: "frames.join(\"\\n\\n\")" +--- +default +row 16 +row 17 +row 18 +row 19 +row 20 +row 21 +row 22 +row 23 + +half page +row 19 +row 20 +row 21 +row 22 +row 23 +row 24 +row 25 +row 26 + +single row +row 22 +row 23 +row 24 +row 25 +row 26 +row 27 +row 28 +row 29 + +unbound +row 23 +row 24 +row 25 +row 26 +row 27 +row 28 +row 29 +row 30 diff --git a/codex-rs/tui/src/transcript_view/input.rs b/codex-rs/tui/src/transcript_view/input.rs index e2923e55e2..f969ec17b2 100644 --- a/codex-rs/tui/src/transcript_view/input.rs +++ b/codex-rs/tui/src/transcript_view/input.rs @@ -343,17 +343,24 @@ impl TranscriptView { ) -> Option { let jump = JumpTarget::from_key(key); match (key.code, key.modifiers) { - (KeyCode::PageUp, KeyModifiers::NONE) => { - self.scroll( - cells, - -(self.area.height.saturating_sub(/*rhs*/ 1).max(/*other*/ 1) as isize), - ); - } - (KeyCode::PageDown, KeyModifiers::NONE) => { - self.scroll( - cells, - self.area.height.saturating_sub(/*rhs*/ 1).max(/*other*/ 1) as isize, - ); + (KeyCode::PageUp | KeyCode::PageDown, KeyModifiers::NONE) => { + // Share page-key configuration without taking the pager's typing shortcuts. + // Full pages retain the main view's existing one-row overlap. + let keymap = &self.disclosure.keymap.pager; + let height = self.area.height.max(/*other*/ 1) as isize; + let page = (height - 1).max(/*other*/ 1); + let half = (height + 1) / 2; + let delta = [ + (&keymap.scroll_up, -1), + (&keymap.scroll_down, 1), + (&keymap.page_up, -page), + (&keymap.page_down, page), + (&keymap.half_page_up, -half), + (&keymap.half_page_down, half), + ] + .into_iter() + .find_map(|(bindings, delta)| bindings.is_pressed(key).then_some(delta))?; + self.scroll(cells, delta); } (KeyCode::Esc, KeyModifiers::NONE) if self.can_return_to_latest() => { self.jump_to_latest(); diff --git a/codex-rs/tui/src/transcript_view_tests.rs b/codex-rs/tui/src/transcript_view_tests.rs index e5c7c47fb4..e7641997d8 100644 --- a/codex-rs/tui/src/transcript_view_tests.rs +++ b/codex-rs/tui/src/transcript_view_tests.rs @@ -63,6 +63,77 @@ pub(super) fn text(buffer: &Buffer) -> String { .join("\n") } +#[test] +fn page_keys_follow_pager_scroll_bindings() { + let cells = [cell( + (1..=30) + .map(|row| format!("row {row}\n")) + .collect::(), + )]; + let mut frames = Vec::new(); + for (label, pager, rows) in [ + ("default", serde_json::json!({}), 7), + ( + "half page", + serde_json::json!({ + "page_up": [], "page_down": [], + "half_page_up": "page-up", "half_page_down": "page-down" + }), + 4, + ), + ( + "single row", + serde_json::json!({ + "page_up": [], "page_down": [], + "scroll_up": "page-up", "scroll_down": "page-down" + }), + 1, + ), + ( + "unbound", + serde_json::json!({"page_up": [], "page_down": []}), + 0, + ), + ] { + let config = serde_json::from_value(serde_json::json!({"pager": pager})).unwrap(); + let keymap = crate::keymap::RuntimeKeymap::from_config(&config).unwrap(); + let mut view = TranscriptView::default(); + view.set_keymap_bindings(&keymap); + let pre_scroll = render(&mut view, &cells, /*width*/ 20, /*height*/ 8); + assert_eq!( + view.handle_key(KeyCode::PageUp.into(), &cells).is_some(), + rows != 0 + ); + let scrolled = render(&mut view, &cells, /*width*/ 20, /*height*/ 8); + let mut expected = TranscriptView::default(); + render(&mut expected, &cells, /*width*/ 20, /*height*/ 8); + expected.scroll(&cells, -rows); + assert_eq!( + scrolled, + render(&mut expected, &cells, /*width*/ 20, /*height*/ 8) + ); + frames.push(format!("{label}\n{}", text(&scrolled))); + assert_eq!( + view.handle_key(KeyCode::PageDown.into(), &cells).is_some(), + rows != 0 + ); + assert_eq!( + render(&mut view, &cells, /*width*/ 20, /*height*/ 8), + pre_scroll + ); + for key in [ + KeyCode::Char('j').into(), + KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL), + ] { + assert!(view.handle_key(key, &cells).is_none()); + } + } + insta::assert_snapshot!( + "page_keys_follow_pager_scroll_bindings", + frames.join("\n\n") + ); +} + #[test] fn terminal_output_disclosure_follows_live_history_and_keymap() { use crate::exec_cell::CommandOutput;