diff --git a/crates/beer/src/pty.rs b/crates/beer/src/pty.rs index 7bb559e..5524b47 100644 --- a/crates/beer/src/pty.rs +++ b/crates/beer/src/pty.rs @@ -19,9 +19,10 @@ pub struct Pty { } impl Pty { - /// Open a PTY, size it to `cols`x`rows`, and exec the user's login shell on - /// the slave end with `TERM=term`. - pub fn spawn(cols: u16, rows: u16, term: &str) -> anyhow::Result { + /// Open a PTY, size it to `cols`x`rows` (with `cell` giving the cell size in + /// pixels, so the kernel reports a pixel geometry for graphics clients), and + /// exec the user's login shell on the slave end with `TERM=term`. + pub fn spawn(cols: u16, rows: u16, cell: (u16, u16), term: &str) -> anyhow::Result { let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC) .context("open pty master")?; grantpt(&master).context("grantpt")?; @@ -29,7 +30,7 @@ impl Pty { let slave = ioctl_tiocgptpeer(&master, OpenptFlags::RDWR | OpenptFlags::NOCTTY) .context("open pty slave")?; - set_winsize(&master, cols, rows)?; + set_winsize(&master, cols, rows, cell)?; let shell = std::env::var_os("SHELL").unwrap_or_else(|| "/bin/sh".into()); let argv0 = login_argv0(&shell); @@ -73,9 +74,10 @@ impl Pty { &self.master } - /// Inform the kernel (and thus the child) of a new terminal size. - pub fn resize(&self, cols: u16, rows: u16) -> anyhow::Result<()> { - set_winsize(&self.master, cols, rows) + /// Inform the kernel (and thus the child) of a new terminal size; `cell` is + /// the cell size in pixels, carried into the winsize pixel fields. + pub fn resize(&self, cols: u16, rows: u16, cell: (u16, u16)) -> anyhow::Result<()> { + set_winsize(&self.master, cols, rows, cell) } /// Reap the child if it has exited. @@ -84,12 +86,14 @@ impl Pty { } } -fn set_winsize(master: &OwnedFd, cols: u16, rows: u16) -> anyhow::Result<()> { +fn set_winsize(master: &OwnedFd, cols: u16, rows: u16, cell: (u16, u16)) -> anyhow::Result<()> { let ws = Winsize { ws_row: rows, ws_col: cols, - ws_xpixel: 0, - ws_ypixel: 0, + // The pixel geometry lets graphics-protocol clients size images; it is + // the grid size times the cell size. + ws_xpixel: cols.saturating_mul(cell.0), + ws_ypixel: rows.saturating_mul(cell.1), }; tcsetwinsize(master.as_fd(), ws).context("set pty winsize") } diff --git a/crates/beer/src/vt/mod.rs b/crates/beer/src/vt/mod.rs index a64563c..9656472 100644 --- a/crates/beer/src/vt/mod.rs +++ b/crates/beer/src/vt/mod.rs @@ -290,6 +290,20 @@ impl Term { self.graphics.is_animating() } + /// Answer a `CSI 14/16/18 t` geometry query: `14` reports the text area in + /// pixels (`CSI 4 ; h ; w t`), `16` the cell size in pixels (`CSI 6 ; …`), + /// `18` the text area in characters (`CSI 8 ; …`). Graphics clients read + /// these to size and place images. + fn report_geometry(&mut self, kind: u16) { + let (cw, ch) = self.cell_px; + let (cols, rows) = (self.grid.cols() as u32, self.grid.rows() as u32); + let _ = match kind { + 14 => write!(self.response, "\x1b[4;{};{}t", rows * ch, cols * cw), + 16 => write!(self.response, "\x1b[6;{ch};{cw}t"), + _ => write!(self.response, "\x1b[8;{rows};{cols}t"), + }; + } + /// The working directory last reported by the shell (OSC 7), if any. pub fn cwd(&self) -> Option<&str> { self.cwd.as_deref() @@ -682,6 +696,19 @@ mod tests { assert!(resp.windows(2).any(|w| w == b"OK"), "expected OK response"); } + #[test] + fn reports_pixel_geometry_for_graphics_clients() { + // The test harness feeds with an 8x16 cell. A 20x4 grid is then 160x64 + // pixels. These answers are what an image client needs to size images. + let mut t = Term::new(20, 4); + feed(&mut t, b"\x1b[16t"); // cell size in pixels + assert_eq!(t.take_response(), b"\x1b[6;16;8t"); + feed(&mut t, b"\x1b[14t"); // text area in pixels + assert_eq!(t.take_response(), b"\x1b[4;64;160t"); + feed(&mut t, b"\x1b[18t"); // text area in cells + assert_eq!(t.take_response(), b"\x1b[8;4;20t"); + } + #[test] fn kitty_unicode_placeholder_virtual_placement() { // Transmit + a virtual placement (U=1): no cells are stamped, but the diff --git a/crates/beer/src/vt/perform.rs b/crates/beer/src/vt/perform.rs index dd681da..effe0c0 100644 --- a/crates/beer/src/vt/perform.rs +++ b/crates/beer/src/vt/perform.rs @@ -93,7 +93,12 @@ impl Perform for Term { .kitty_set(n(params, 0, 0) as u8, n(params, 1, 1) as u8), _ => self.grid.restore_cursor(), }, - 't' => self.title_stack_op(params), + // `CSI 14/16/18 t` report pixel/character geometry (used by graphics + // clients to size images); other `t` operations are title-stack ops. + 't' => match raw(params, 0) { + 14 | 16 | 18 => self.report_geometry(raw(params, 0)), + _ => self.title_stack_op(params), + }, 'g' => match raw(params, 0) { 3 => self.grid.clear_all_tabs(), _ => self.grid.clear_tab(), diff --git a/crates/beer/src/wayland/mod.rs b/crates/beer/src/wayland/mod.rs index 8ffe255..b426db1 100644 --- a/crates/beer/src/wayland/mod.rs +++ b/crates/beer/src/wayland/mod.rs @@ -575,7 +575,9 @@ impl App { /// Spawn the shell at the current window size and start reading its output. fn spawn_session(&mut self) { let (cols, rows) = self.grid_dims(); - let pty = match Pty::spawn(cols, rows, &self.config.main.term) { + let m = self.renderer.metrics(); + let cell = (m.width as u16, m.height as u16); + let pty = match Pty::spawn(cols, rows, cell, &self.config.main.term) { Ok(pty) => pty, Err(err) => { tracing::error!("spawn shell: {err:#}"); @@ -1857,6 +1859,8 @@ impl App { /// PTY about it if it changed. fn resize_grid(&mut self) { let (cols, rows) = self.grid_dims(); + let m = self.renderer.metrics(); + let cell = (m.width as u16, m.height as u16); let Some(session) = self.session.as_mut() else { return; }; @@ -1866,7 +1870,7 @@ impl App { return; } session.term.resize(cols as usize, rows as usize); - if let Err(err) = session.pty.resize(cols, rows) { + if let Err(err) = session.pty.resize(cols, rows, cell) { tracing::warn!("resize pty: {err}"); } }