diff --git a/CHANGELOG.md b/CHANGELOG.md index 80024e3..600772f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added +- XTWINOPS text-area size reports: `CSI 14 t` (pixels) and `CSI 18 t` (cells) + ### Changed - input mode (normal/insert/visual/search) is now tracked per tab; switching tabs restores each tab's own mode diff --git a/src/app_state.rs b/src/app_state.rs index 84caecf..8a607f5 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -26,7 +26,7 @@ pub struct PaneEntry { /// Non-blocking resize request: main thread writes Some((cols, rows)); /// parser thread applies grid.resize() within its existing write lock and /// clears the Option. Avoids blocking the event loop on grid.write(). - pub pending_resize: Arc>>, + pub pending_resize: crate::drain::PendingResize, pub(crate) _parser_thread: std::thread::JoinHandle<()>, /// Per-pane density-independent font size. Physical px = scale.px(logical_font_size). /// Mutated by Ctrl±/reset; re-derived (not persisted) on ScaleFactorChanged. diff --git a/src/drain.rs b/src/drain.rs index b5c70d9..381181e 100644 --- a/src/drain.rs +++ b/src/drain.rs @@ -51,6 +51,9 @@ use super::App; #[path = "drain_test.rs"] mod tests; +/// Non-blocking resize signal (main → parser): `(cols, rows, cell_width_px, cell_height_px)` in px. +pub type PendingResize = Arc>>; + // ── ParseEffect ─────────────────────────────────────────────────────────────── /// Side-effects produced by a parser thread batch and consumed on the main thread. @@ -104,7 +107,7 @@ pub struct ParserThreadArgs { pub wakeup_pending: Arc, /// Non-blocking resize request set by the main thread; parser applies it /// within its existing write lock so the event loop never blocks on grid.write(). - pub pending_resize: Arc>>, + pub pending_resize: PendingResize, pub wakeup: Box, } @@ -141,8 +144,9 @@ pub fn spawn_parser_thread(args: ParserThreadArgs) -> thread::JoinHandle<()> { Err(RecvTimeoutError::Timeout) => { // Check for a pending resize while the channel is quiet. let pending = pending_resize.lock().unwrap().take(); - if let Some((new_cols, new_rows)) = pending { + if let Some((new_cols, new_rows, cw, ch)) = pending { let mut g = grid.write().unwrap(); + g.cell_px = (cw, ch); let delta = g.resize(new_cols, new_rows); let new_sb = g.scrollback_len(); drop(g); @@ -194,7 +198,8 @@ pub fn spawn_parser_thread(args: ParserThreadArgs) -> thread::JoinHandle<()> { parser.process(&batch, &mut g); g.scan_urls(); let new = g.scrollback_len(); // parse-only scrollback delta (before any resize) - let resize_effect = pending.map(|(new_cols, new_rows)| { + let resize_effect = pending.map(|(new_cols, new_rows, cw, ch)| { + g.cell_px = (cw, ch); let delta = g.resize(new_cols, new_rows); ParseEffect::Resized { delta, diff --git a/src/drain_test.rs b/src/drain_test.rs index fcc70ed..70f4cb3 100644 --- a/src/drain_test.rs +++ b/src/drain_test.rs @@ -199,7 +199,7 @@ fn pending_resize_applied_during_parse() { for _ in 0..20 { tx.send(b"AAAAA\r\n".to_vec()).unwrap(); } - *entry.pending_resize.lock().unwrap() = Some((100, 40)); + *entry.pending_resize.lock().unwrap() = Some((100, 40, 8, 16)); std::thread::sleep(std::time::Duration::from_millis(150)); diff --git a/src/pane_ops.rs b/src/pane_ops.rs index 7f57df5..43f8371 100644 --- a/src/pane_ops.rs +++ b/src/pane_ops.rs @@ -85,7 +85,7 @@ impl App { None }; let log_file = Arc::new(Mutex::new(log_file_opt)); - let pending_resize: Arc>> = Arc::new(Mutex::new(None)); + let pending_resize: drain::PendingResize = Arc::new(Mutex::new(None)); let (effects_tx, effects_rx) = unbounded::(); let parser_thread = drain::spawn_parser_thread(drain::ParserThreadArgs { rx: pty_rx, @@ -278,7 +278,12 @@ impl App { // grid.write() while the parser holds it (up to ~36 ms), keeping // resize fluid even during heavy output. if let Ok(mut pr) = entry.pending_resize.lock() { - *pr = Some((cols, rows)); + *pr = Some(( + cols, + rows, + entry.metrics.cell_width, + entry.metrics.cell_height, + )); } let _ = entry.pty.resize(cols as u16, rows as u16); } diff --git a/src/pane_ops_test.rs b/src/pane_ops_test.rs index d3cee26..5974620 100644 --- a/src/pane_ops_test.rs +++ b/src/pane_ops_test.rs @@ -109,12 +109,12 @@ fn sync_uses_per_pane_metrics() { // sync_pane_sizes_tab writes target dimensions to pending_resize; the parser // thread applies them asynchronously. Test the contract that sync_pane_sizes_tab // keeps: it must compute the correct (cols, rows) for each pane's metrics. - let (c1, r1) = tab.panes[&1] + let (c1, r1, _, _) = tab.panes[&1] .pending_resize .lock() .unwrap() .expect("pane 1 should have a pending resize"); - let (c2, r2) = tab.panes[&2] + let (c2, r2, _, _) = tab.panes[&2] .pending_resize .lock() .unwrap() diff --git a/src/terminal/grid.rs b/src/terminal/grid.rs index a669ce4..3502e65 100644 --- a/src/terminal/grid.rs +++ b/src/terminal/grid.rs @@ -233,6 +233,8 @@ pub struct Grid { pub last_exit_code: Option, // OSC 777 pending desktop notification (title, body); drained once by the parser thread pub pending_notification: Option<(String, String)>, + // Cell (width, height) in pixels, mirrored from FontMetrics on resize; used by XTWINOPS (CSI 14 t) + pub cell_px: (u32, u32), } impl Grid { @@ -299,6 +301,7 @@ impl Grid { shell_state: ShellState::Unknown, last_exit_code: None, pending_notification: None, + cell_px: (0, 0), } } diff --git a/src/terminal/parser.rs b/src/terminal/parser.rs index 2046d95..d969bd8 100644 --- a/src/terminal/parser.rs +++ b/src/terminal/parser.rs @@ -66,27 +66,19 @@ struct Performer<'a> { impl Performer<'_> { fn handle_dec_private_modes(&mut self, action: char, p0: u16) { + // Only DECSET (h) / DECRST (l) toggle modes; the `('h' | 'l', _)` patterns + // keep other actions out of these arms, so `on` is only used when relevant. + let on = action == 'h'; match (action, p0) { - ('h', 1) => self.grid.application_cursor_keys = true, - ('l', 1) => self.grid.application_cursor_keys = false, - ('h', 7) => self.grid.autowrap = true, - ('l', 7) => self.grid.autowrap = false, - ('h', 25) => self.grid.cursor_visible = true, - ('l', 25) => self.grid.cursor_visible = false, - ('h', 1000) => self.grid.mouse_mode = 1000, - ('l', 1000) => self.grid.mouse_mode = 0, - ('h', 1002) => self.grid.mouse_mode = 1002, - ('l', 1002) => self.grid.mouse_mode = 0, - ('h', 1003) => self.grid.mouse_mode = 1003, - ('l', 1003) => self.grid.mouse_mode = 0, - ('h', 1004) => self.grid.focus_report = true, - ('l', 1004) => self.grid.focus_report = false, - ('h', 1006) => self.grid.mouse_sgr = true, - ('l', 1006) => self.grid.mouse_sgr = false, + ('h' | 'l', 1) => self.grid.application_cursor_keys = on, + ('h' | 'l', 7) => self.grid.autowrap = on, + ('h' | 'l', 25) => self.grid.cursor_visible = on, + ('h' | 'l', 1000 | 1002 | 1003) => self.grid.mouse_mode = if on { p0 } else { 0 }, + ('h' | 'l', 1004) => self.grid.focus_report = on, + ('h' | 'l', 1006) => self.grid.mouse_sgr = on, + ('h' | 'l', 2004) => self.grid.bracketed_paste = on, ('h', 1049) => self.grid.enter_alternate_screen(), ('l', 1049) => self.grid.exit_alternate_screen(), - ('h', 2004) => self.grid.bracketed_paste = true, - ('l', 2004) => self.grid.bracketed_paste = false, _ => {} } } @@ -230,6 +222,23 @@ impl Performer<'_> { self.grid.cursor_row = top; self.grid.cursor_col = 0; } + + // XTWINOPS (CSI Ps t): report text-area size. + // 14 → pixels: CSI 4 ; height ; width t 18 → cells: CSI 8 ; rows ; cols t + fn handle_xtwinops(&mut self, p0: u16) { + let g = &self.grid; + let (cw, ch) = g.cell_px; + let px_h = g.rows as u32 * ch; + let px_w = g.cols as u32 * cw; + let resp = match p0 { + 14 => format!("\x1b[4;{px_h};{px_w}t"), + 18 => format!("\x1b[8;{};{}t", g.rows, g.cols), + _ => return, + }; + self.grid + .pending_responses + .extend_from_slice(resp.as_bytes()); + } } impl Perform for Performer<'_> { @@ -313,6 +322,8 @@ impl Perform for Performer<'_> { } // Set scroll region 'r' => self.handle_scroll_region(p0, p1), + // XTWINOPS: report text-area size (CSI 14 t pixels, CSI 18 t cells) + 't' => self.handle_xtwinops(p0), _ => {} } } diff --git a/src/terminal/parser_test.rs b/src/terminal/parser_test.rs index 42251b9..eaeedcf 100644 --- a/src/terminal/parser_test.rs +++ b/src/terminal/parser_test.rs @@ -1287,3 +1287,12 @@ fn osc777_without_notify_keyword_is_ignored() { p.process(b"\x1b]777;other;Title;Body\x07"); assert!(p.grid.pending_notification.is_none()); } + +#[test] +fn xtwinops_reports_text_area_size() { + let mut p = make_parser(80, 24); + p.grid.cell_px = (8, 16); + // 18 → cells (CSI 8;rows;cols t), 14 → pixels (CSI 4;h;w t), 999 → unsupported (no reply) + p.process(b"\x1b[18t\x1b[14t\x1b[999t"); + assert_eq!(p.grid.pending_responses, b"\x1b[8;24;80t\x1b[4;384;640t"); +}