fix: answer XTWINOPS size queries in panes

refs #835
This commit is contained in:
Ogulcan Celik 2026-07-24 02:39:42 +03:00
parent 1bcc55e47d
commit 169a4fd90c
3 changed files with 107 additions and 5 deletions

View File

@ -456,6 +456,26 @@ struct TerminalCallbackState {
write_pty: Option<Box<WritePtyCallback>>,
pwd_changes: Vec<Vec<u8>>,
clipboard_writes: Vec<Vec<u8>>,
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::<TerminalCallbackState>() };
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> {

View File

@ -1716,6 +1716,9 @@ impl PaneRuntime {
let (response_tx, _response_rx) = mpsc::channel::<Bytes>(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()

View File

@ -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);