aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-14 22:57:09 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-14 22:57:09 +0300
commitc2e58974f4fd57fed7a8fc992ee20fe42b56e619 (patch)
tree8afdb39aa1ea5a3fed95b991f328aa85ae86382e /src/app/input
parent5c82623dbf6097035332f14164269ecaede6a61d (diff)
fix(input): scroll pane scrollback with pageup/pagedown and parse enhanced tilde sequences
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/terminal.rs152
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);
+ }
}