aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-24 02:39:42 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-24 02:39:51 +0300
commit169a4fd90c20a00aec77d684ecec98b9f0f996cc (patch)
treeaa92f013e5380ff5d3253eeb6393f91b85f1c26b /src
parent1bcc55e47d94a10b154a180e01ee8c068b953337 (diff)
fix: answer XTWINOPS size queries in panes
refs #835
Diffstat (limited to 'src')
-rw-r--r--src/ghostty/mod.rs55
-rw-r--r--src/pane.rs3
-rw-r--r--src/pane/terminal.rs54
3 files changed, 107 insertions, 5 deletions
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<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),
};
@@ -749,6 +776,12 @@ impl Terminal {
.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,
(pwd_changed_trampoline as *const ()).cast(),
)
@@ -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::<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()
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
@@ -4484,6 +4484,60 @@ mod tests {
}
#[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);
let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();