diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index 1a2a8cbb..66182681 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -456,6 +456,26 @@ struct TerminalCallbackState { write_pty: Option>, pwd_changes: Vec>, clipboard_writes: Vec>, + size_report: ffi::GhosttySizeReportSize, +} + +unsafe extern "C" fn size_trampoline( + _terminal: ffi::GhosttyTerminal, + userdata: *mut c_void, + out_size: *mut ffi::GhosttySizeReportSize, +) -> bool { + if userdata.is_null() || out_size.is_null() { + return false; + } + let state = unsafe { &*userdata.cast::() }; + let size = state.size_report; + if size.rows == 0 || size.columns == 0 || size.cell_width == 0 || size.cell_height == 0 { + return false; + } + unsafe { + out_size.write(size); + } + true } unsafe extern "C" fn write_pty_trampoline( @@ -734,7 +754,14 @@ impl Terminal { let mut terminal = Self { raw, - callback_state: Box::default(), + callback_state: Box::new(TerminalCallbackState { + size_report: ffi::GhosttySizeReportSize { + rows, + columns: cols, + ..Default::default() + }, + ..Default::default() + }), kitty_fingerprints: Mutex::new(HashMap::new()), kitty_empty_generation: Cell::new(None), }; @@ -747,6 +774,12 @@ impl Terminal { userdata, ) .into_result()?; + ffi::ghostty_terminal_set( + terminal.raw, + ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_SIZE, + (size_trampoline as *const ()).cast(), + ) + .into_result()?; ffi::ghostty_terminal_set( terminal.raw, ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_PWD_CHANGED, @@ -799,13 +832,25 @@ impl Terminal { cell_width_px: u32, cell_height_px: u32, ) -> Result<(), Error> { - let cell_width_px = cell_width_px.max(1); - let cell_height_px = cell_height_px.max(1); + let size_report = ffi::GhosttySizeReportSize { + rows, + columns: cols, + cell_width: cell_width_px, + cell_height: cell_height_px, + }; // SAFETY: self.raw is valid and sizes are plain values. unsafe { - ffi::ghostty_terminal_resize(self.raw, cols, rows, cell_width_px, cell_height_px) - .into_result() + ffi::ghostty_terminal_resize( + self.raw, + cols, + rows, + cell_width_px.max(1), + cell_height_px.max(1), + ) + .into_result()?; } + self.callback_state.size_report = size_report; + Ok(()) } pub fn enable_kitty_graphics(&mut self) -> Result<(), Error> { diff --git a/src/pane.rs b/src/pane.rs index ad9e1f92..ee3aee12 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -1716,6 +1716,9 @@ impl PaneRuntime { let (response_tx, _response_rx) = mpsc::channel::(1); let mut terminal = crate::ghostty::Terminal::new(cols, rows, scrollback_limit_bytes) .map_err(|e| std::io::Error::other(e.to_string()))?; + terminal + .resize(cols, rows, cell_width_px, cell_height_px) + .map_err(|e| std::io::Error::other(e.to_string()))?; if crate::kitty_graphics::is_enabled() { terminal .enable_kitty_graphics() diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 23dcffff..81c57ef9 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -4483,6 +4483,60 @@ mod tests { assert!(pane.recent_text(3).trim().is_empty()); } + #[test] + fn process_pty_bytes_answers_xtwinops_size_queries() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.resize(24, 80, 9, 18); + + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b[14t\x1b[16t\x1b[18t", &tx); + + assert_eq!( + result.terminal_responses, + vec![ + Bytes::from_static(b"\x1b[4;432;720t"), + Bytes::from_static(b"\x1b[6;18;9t"), + Bytes::from_static(b"\x1b[8;24;80t"), + ] + ); + } + + #[test] + fn xtwinops_size_queries_follow_successful_resize() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.resize(24, 80, 9, 18); + pane.resize(30, 100, 10, 20); + + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b[14t\x1b[16t\x1b[18t", &tx); + + assert_eq!( + result.terminal_responses, + vec![ + Bytes::from_static(b"\x1b[4;600;1000t"), + Bytes::from_static(b"\x1b[6;20;10t"), + Bytes::from_static(b"\x1b[8;30;100t"), + ] + ); + } + + #[test] + fn xtwinops_size_queries_stay_silent_without_pixel_geometry() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + for (cell_width_px, cell_height_px) in [(0, 0), (0, 18), (9, 0)] { + pane.resize(24, 80, cell_width_px, cell_height_px); + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b[14t\x1b[16t\x1b[18t", &tx); + assert!(result.terminal_responses.is_empty()); + } + } + #[test] fn resize_returns_in_band_size_report_response() { let (tx, _rx) = mpsc::channel(4);