diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-14 22:57:09 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-14 22:57:09 +0300 |
| commit | c2e58974f4fd57fed7a8fc992ee20fe42b56e619 (patch) | |
| tree | 8afdb39aa1ea5a3fed95b991f328aa85ae86382e /src/app | |
| parent | 5c82623dbf6097035332f14164269ecaede6a61d (diff) | |
fix(input): scroll pane scrollback with pageup/pagedown and parse enhanced tilde sequences
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/input/terminal.rs | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index bbfcd323..37174273 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -65,6 +65,35 @@ impl App { let ws = self.state.workspaces.get(ws_idx)?; let pane_id = ws.focused_pane_id()?; let rt = ws.runtimes.get(&pane_id)?; + + // Intercept PageUp/PageDown for pane scrollback when the focused pane + // doesn't handle its own scrolling (e.g., a plain shell with mouse off). + // Only intercept when we know the pane state; if input_state is unknown, + // fail-open and forward the key to the pane. + if matches!(key_event.code, KeyCode::PageUp | KeyCode::PageDown) { + if let Some(input_state) = rt.input_state() { + if !input_state.alternate_screen && !input_state.mouse_reporting_enabled() { + let lines = self + .state + .pane_info_by_id(pane_id) + .map(|info| info.inner_rect.height as usize) + .unwrap_or(10) + .max(1); + if key_event.code == KeyCode::PageUp { + self.state.scroll_pane_up(pane_id, lines); + } else { + self.state.scroll_pane_down(pane_id, lines); + } + debug!( + code = ?key_event.code, + lines, + "intercepted page key for pane scrollback" + ); + return None; + } + } + } + rt.scroll_reset(); let protocol = rt.keyboard_protocol(); let bytes = rt.encode_terminal_key(key); @@ -438,4 +467,127 @@ mod tests { assert_ne!(app.state.workspaces[0].layout.focused(), focused_before); assert_eq!(app.state.mode, Mode::Terminal); } + + #[tokio::test] + async fn page_up_scrolls_plain_shell_pane() { + let mut app = app_for_mouse_test(); + let mut ws = Workspace::test_new("test"); + let pane_id = ws.tabs[0].root_pane; + let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18)); + let info = pane_infos[0].clone(); + ws.tabs[0].runtimes.insert( + pane_id, + crate::pane::PaneRuntime::test_with_scrollback_bytes( + info.inner_rect.width, + info.inner_rect.height, + 16 * 1024, + &numbered_lines_bytes(64), + ), + ); + + app.state.workspaces = vec![ws]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.view.pane_infos = pane_infos; + + let start_metrics = app.state.workspaces[0] + .runtime(pane_id) + .and_then(crate::pane::PaneRuntime::scroll_metrics) + .expect("initial scroll metrics"); + assert_eq!(start_metrics.offset_from_bottom, 0); + + app.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty())); + + let end_metrics = app.state.workspaces[0] + .runtime(pane_id) + .and_then(crate::pane::PaneRuntime::scroll_metrics) + .expect("scroll metrics after PageUp"); + assert_eq!( + end_metrics.offset_from_bottom, + info.inner_rect.height as usize + ); + } + + #[tokio::test] + async fn page_down_returns_to_bottom_after_page_up() { + let mut app = app_for_mouse_test(); + let mut ws = Workspace::test_new("test"); + let pane_id = ws.tabs[0].root_pane; + let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18)); + let info = pane_infos[0].clone(); + ws.tabs[0].runtimes.insert( + pane_id, + crate::pane::PaneRuntime::test_with_scrollback_bytes( + info.inner_rect.width, + info.inner_rect.height, + 16 * 1024, + &numbered_lines_bytes(64), + ), + ); + + app.state.workspaces = vec![ws]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.view.pane_infos = pane_infos; + + app.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty())); + let after_up = app.state.workspaces[0] + .runtime(pane_id) + .and_then(crate::pane::PaneRuntime::scroll_metrics) + .expect("scroll metrics after PageUp"); + assert!(after_up.offset_from_bottom > 0); + + app.handle_terminal_key_headless(TerminalKey::new( + KeyCode::PageDown, + KeyModifiers::empty(), + )); + let after_down = app.state.workspaces[0] + .runtime(pane_id) + .and_then(crate::pane::PaneRuntime::scroll_metrics) + .expect("scroll metrics after PageDown"); + assert_eq!(after_down.offset_from_bottom, 0); + } + + #[tokio::test] + async fn page_up_forwarded_to_mouse_reporting_pane() { + let mut app = app_for_mouse_test(); + let mut ws = Workspace::test_new("test"); + let pane_id = ws.tabs[0].root_pane; + let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18)); + let info = pane_infos[0].clone(); + let mut bytes = b"\x1b[?1002h".to_vec(); + bytes.extend_from_slice(&numbered_lines_bytes(64)); + ws.tabs[0].runtimes.insert( + pane_id, + crate::pane::PaneRuntime::test_with_scrollback_bytes( + info.inner_rect.width, + info.inner_rect.height, + 16 * 1024, + &bytes, + ), + ); + + app.state.workspaces = vec![ws]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.view.pane_infos = pane_infos; + + let start_metrics = app.state.workspaces[0] + .runtime(pane_id) + .and_then(crate::pane::PaneRuntime::scroll_metrics) + .expect("initial scroll metrics"); + assert_eq!(start_metrics.offset_from_bottom, 0); + + app.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty())); + + let end_metrics = app.state.workspaces[0] + .runtime(pane_id) + .and_then(crate::pane::PaneRuntime::scroll_metrics) + .expect("scroll metrics after PageUp"); + // Forwarded to pane, so test runtime doesn't process it — scroll stays at bottom. + assert_eq!(end_metrics.offset_from_bottom, 0); + } } |
