diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index 36d60e41..1ba2789c 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -1971,4 +1971,42 @@ mod tests { .expect("scroll metrics after PageUp"); assert_eq!(end_metrics.offset_from_bottom, 0); } + + #[tokio::test] + async fn page_up_scrolls_shell_like_decckm_with_bracketed_paste() { + 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(); + // zsh enables DECCKM via smkx and bracketed paste together; bash/fish do not. + let mut bytes = b"\x1b[?1h\x1b[?2004h".to_vec(); + bytes.extend_from_slice(&numbered_lines_bytes(64)); + let (runtime, mut input_rx) = + crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes( + info.inner_rect.width, + info.inner_rect.height, + 16 * 1024, + &bytes, + 4, + ); + ws.tabs[0].runtimes.insert(pane_id, runtime); + + 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())); + + assert!( + input_rx.try_recv().is_err(), + "PageUp should not reach the shell" + ); + assert_eq!( + pane_scroll_offset(&app, pane_id), + info.inner_rect.height as usize + ); + } } diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 438c3037..b90c63f1 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -130,7 +130,11 @@ impl InputState { } pub fn plain_page_keys_use_host_scrollback(self) -> bool { - !self.alternate_screen && !self.mouse_reporting_enabled() && !self.application_cursor + !self.alternate_screen + && !self.mouse_reporting_enabled() + // Bracketed paste distinguishes zsh's line editor (where it's on) + // from e.g. less -X (where it's off). + && (!self.application_cursor || self.bracketed_paste) } } @@ -3073,6 +3077,22 @@ mod tests { use ratatui::{layout::Rect, style::Color}; use tokio::sync::mpsc; + #[test] + fn plain_page_keys_host_scroll_for_shell_like_decckm_with_bracketed_paste() { + assert!(InputState { + alternate_screen: false, + application_cursor: true, + bracketed_paste: true, + focus_reporting: false, + mouse_protocol_mode: crate::input::MouseProtocolMode::None, + mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Default, + mouse_alternate_scroll: false, + modify_other_keys: false, + color_scheme_reporting: false, + } + .plain_page_keys_use_host_scrollback()); + } + fn text_cell(text: &str) -> crate::ghostty::ScreenTextCell { crate::ghostty::ScreenTextCell { wide: crate::ghostty::CellWide::Narrow, diff --git a/src/server/headless.rs b/src/server/headless.rs index ff85f253..f72d1a94 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -6468,6 +6468,22 @@ next_tab = "" }); } + #[test] + fn terminal_attach_page_key_host_scrolls_shell_like_decckm_with_bracketed_paste() { + with_terminal_attach_page_key_runtime(b"\x1b[?1h\x1b[?2004h", 0, |runtime, input_rx| { + apply_terminal_attach_page_up(runtime); + + assert_eq!( + runtime + .scroll_metrics() + .expect("scroll metrics") + .offset_from_bottom, + 4 + ); + assert!(input_rx.try_recv().is_err()); + }); + } + #[test] fn terminal_attach_page_key_forwards_in_alternate_screen_without_mouse_reporting() { with_terminal_attach_page_key_runtime(b"\x1b[?1049h", 3, |runtime, input_rx| {