Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mutex<Option<(usize, usize)>>>,
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.
Expand Down
11 changes: 8 additions & 3 deletions src/drain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mutex<Option<(usize, usize, u32, u32)>>>;

// ── ParseEffect ───────────────────────────────────────────────────────────────

/// Side-effects produced by a parser thread batch and consumed on the main thread.
Expand Down Expand Up @@ -104,7 +107,7 @@ pub struct ParserThreadArgs {
pub wakeup_pending: Arc<AtomicBool>,
/// 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<Mutex<Option<(usize, usize)>>>,
pub pending_resize: PendingResize,
pub wakeup: Box<dyn Fn() + Send + 'static>,
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/drain_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
9 changes: 7 additions & 2 deletions src/pane_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ impl App {
None
};
let log_file = Arc::new(Mutex::new(log_file_opt));
let pending_resize: Arc<Mutex<Option<(usize, usize)>>> = Arc::new(Mutex::new(None));
let pending_resize: drain::PendingResize = Arc::new(Mutex::new(None));
let (effects_tx, effects_rx) = unbounded::<drain::ParseEffect>();
let parser_thread = drain::spawn_parser_thread(drain::ParserThreadArgs {
rx: pty_rx,
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/pane_ops_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions src/terminal/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ pub struct Grid {
pub last_exit_code: Option<i32>,
// 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 {
Expand Down Expand Up @@ -299,6 +301,7 @@ impl Grid {
shell_state: ShellState::Unknown,
last_exit_code: None,
pending_notification: None,
cell_px: (0, 0),
}
}

Expand Down
47 changes: 29 additions & 18 deletions src/terminal/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
_ => {}
}
}
Expand Down Expand Up @@ -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<'_> {
Expand Down Expand Up @@ -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),
_ => {}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/terminal/parser_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}