diff --git a/src/main.rs b/src/main.rs index 931ed69..9695962 100644 --- a/src/main.rs +++ b/src/main.rs @@ -401,6 +401,10 @@ fn choose_surface_alpha_mode(modes: &[wgpu::CompositeAlphaMode]) -> wgpu::Compos .unwrap_or(wgpu::CompositeAlphaMode::Auto) } +fn should_bootstrap_resize(current: (usize, usize), desired: (usize, usize)) -> bool { + (desired.0 > current.0 || desired.1 > current.1) && desired.0 > 1 && desired.1 > 1 +} + // ── TerminalWindow ──────────────────────────────────────────────────────────── struct TerminalWindow { @@ -648,7 +652,33 @@ impl TerminalWindow { } } + fn maybe_bootstrap_resize(&mut self) { + let size = self.window.inner_size(); + if size.width <= self.surface_config.width && size.height <= self.surface_config.height { + return; + } + + let rects = self.pane_rects(); + let cw = self.renderer.cell_width.max(1); + let ch = self.renderer.cell_height.max(1); + let should_resize = self.panes.iter().enumerate().any(|(i, pane)| { + let (_, _, pw, ph) = + rects + .get(i) + .copied() + .unwrap_or((0.0, 0.0, size.width as f32, size.height as f32)); + let desired = ((pw as usize / cw).max(1), (ph as usize / ch).max(1)); + let current = (pane.terminal.state.cols, pane.terminal.state.rows); + should_bootstrap_resize(current, desired) + }); + + if should_resize { + self.on_resize(size.width, size.height); + } + } + fn redraw(&mut self) { + self.maybe_bootstrap_resize(); let window = self.window.clone(); let sz = window.inner_size(); let (w, h) = (sz.width, sz.height); @@ -2941,4 +2971,14 @@ mod tests { let chosen = choose_surface_alpha_mode(&[wgpu::CompositeAlphaMode::Opaque]); assert_eq!(chosen, wgpu::CompositeAlphaMode::Opaque); } + + #[test] + fn bootstrap_resize_triggers_for_real_growth() { + assert!(should_bootstrap_resize((80, 24), (120, 32))); + } + + #[test] + fn bootstrap_resize_ignores_suspicious_one_cell_target() { + assert!(!should_bootstrap_resize((80, 24), (1, 1))); + } } diff --git a/src/terminal.rs b/src/terminal.rs index 49cc9e7..5120153 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -145,6 +145,8 @@ pub struct TerminalState { /// Whether synchronized-output mode is active (?2026h). When true, /// redraws are suppressed until the mode is cleared. pub sync_output: bool, + /// Whether insert-replace mode (IRM, CSI 4 h/l) is active. + insert_mode: bool, /// OSC 8 hyperlink URL table. Index 0 is unused; `link_id` in Cell is /// 1-based into this vec. pub links: Vec, @@ -152,6 +154,7 @@ pub struct TerminalState { current_link_id: u16, saved_cursor: (usize, usize), saved_attrs: Attrs, + saved_wrap_next: bool, wrap_next: bool, /// Grid coordinates of the most recently printed cell (for combining-char attachment). last_placed: (usize, usize), @@ -193,6 +196,7 @@ impl TerminalState { current_dir: std::env::var("HOME").unwrap_or_default(), saved_cursor: (0, 0), saved_attrs: Attrs::default(), + saved_wrap_next: false, wrap_next: false, last_placed: (0, 0), last_was_regional_indicator: false, @@ -207,6 +211,7 @@ impl TerminalState { mouse_tracking: false, mouse_sgr: false, sync_output: false, + insert_mode: false, links: Vec::new(), current_link_id: 0, } @@ -363,6 +368,9 @@ impl TerminalState { self.do_newline(); } if self.cursor_row < self.rows && self.cursor_col < self.cols { + if self.insert_mode { + self.insert_blank_cells_at_cursor(width); + } let blank = self.blank_cell(); self.clear_cell_span(self.cursor_row, self.cursor_col, blank); if width == 2 && self.cursor_col + 1 < self.cols { @@ -456,6 +464,23 @@ impl TerminalState { self.grid[row][col] = blank; } + fn insert_blank_cells_at_cursor(&mut self, n: usize) { + if self.cursor_row >= self.rows || self.cursor_col >= self.cols || n == 0 { + return; + } + let blank = self.blank_cell(); + let row = &mut self.grid[self.cursor_row]; + row[self.cursor_col..].rotate_right(n); + for cell in row + .iter_mut() + .take((self.cursor_col + n).min(self.cols)) + .skip(self.cursor_col) + { + *cell = blank; + } + Self::normalize_row_cells(row); + } + fn normalize_row_cells(row: &mut [Cell]) { let mut col = 0usize; while col < row.len() { @@ -887,13 +912,29 @@ impl Perform for TerminalState { (0, 's') => { self.saved_cursor = (self.cursor_row, self.cursor_col); self.saved_attrs = self.attrs; + self.saved_wrap_next = self.wrap_next; } (0, 'u') => { (self.cursor_row, self.cursor_col) = self.saved_cursor; self.attrs = self.saved_attrs; + self.wrap_next = self.saved_wrap_next; self.cursor_row = self.cursor_row.min(self.rows.saturating_sub(1)); self.cursor_col = self.cursor_col.min(self.cols.saturating_sub(1)); } + (0, 'h') => { + for ¶m in &p { + if param == 4 { + self.insert_mode = true; + } + } + } + (0, 'l') => { + for ¶m in &p { + if param == 4 { + self.insert_mode = false; + } + } + } // Device attributes (0, 'c') => { self.pending_responses.push(b"\x1b[?1;2c".to_vec()); @@ -944,10 +985,12 @@ impl Perform for TerminalState { b'7' => { self.saved_cursor = (self.cursor_row, self.cursor_col); self.saved_attrs = self.attrs; + self.saved_wrap_next = self.wrap_next; } b'8' => { (self.cursor_row, self.cursor_col) = self.saved_cursor; self.attrs = self.saved_attrs; + self.wrap_next = self.saved_wrap_next; self.cursor_row = self.cursor_row.min(self.rows.saturating_sub(1)); self.cursor_col = self.cursor_col.min(self.cols.saturating_sub(1)); } @@ -1061,6 +1104,7 @@ impl TerminalState { self.cursor_shape = 0; self.mouse_tracking = false; self.mouse_sgr = false; + self.insert_mode = false; self.wrap_next = false; } } @@ -1115,10 +1159,12 @@ mod tests { mouse_tracking: bool, mouse_sgr: bool, sync_output: bool, + insert_mode: bool, links: Vec, current_link_id: u16, saved_cursor: (usize, usize), saved_attrs: Attrs, + saved_wrap_next: bool, wrap_next: bool, last_placed: (usize, usize), last_was_regional_indicator: bool, @@ -1173,10 +1219,12 @@ mod tests { mouse_tracking: state.mouse_tracking, mouse_sgr: state.mouse_sgr, sync_output: state.sync_output, + insert_mode: state.insert_mode, links: state.links.clone(), current_link_id: state.current_link_id, saved_cursor: state.saved_cursor, saved_attrs: state.saved_attrs, + saved_wrap_next: state.saved_wrap_next, wrap_next: state.wrap_next, last_placed: state.last_placed, last_was_regional_indicator: state.last_was_regional_indicator, @@ -2813,6 +2861,27 @@ mod tests { assert_eq!(ch(&t, 0, 3), 'B'); } + #[test] + fn irm_toggle_tracks_insert_mode() { + let mut t = t(10, 5); + assert!(!t.state.insert_mode); + t.process(b"\x1b[4h"); + assert!(t.state.insert_mode); + t.process(b"\x1b[4l"); + assert!(!t.state.insert_mode); + } + + #[test] + fn irm_print_inserts_at_cursor() { + let mut t = t(6, 2); + t.process(b"ABCD\x1b[1;2H\x1b[4hX"); + assert_eq!(ch(&t, 0, 0), 'A'); + assert_eq!(ch(&t, 0, 1), 'X'); + assert_eq!(ch(&t, 0, 2), 'B'); + assert_eq!(ch(&t, 0, 3), 'C'); + assert_eq!(ch(&t, 0, 4), 'D'); + } + // ── Resize ──────────────────────────────────────────────────────────────── #[test] @@ -3112,6 +3181,30 @@ mod tests { assert!(t.state.wrap_next, "CUU must not affect pending wrap"); } + #[test] + fn esc_save_restore_preserves_wrap_next() { + let mut t = t(5, 3); + t.process(b"AB"); + t.process(b"\x1b7\x1b[3;1fXXXXX\x1b8C"); + assert_eq!(ch(&t, 0, 0), 'A'); + assert_eq!(ch(&t, 0, 1), 'B'); + assert_eq!(ch(&t, 0, 2), 'C'); + assert_eq!(t.state.cursor_row, 0); + assert_eq!(t.state.cursor_col, 3); + } + + #[test] + fn csi_save_restore_preserves_wrap_next() { + let mut t = t(5, 3); + t.process(b"AB"); + t.process(b"\x1b[s\x1b[3;1fXXXXX\x1b[uC"); + assert_eq!(ch(&t, 0, 0), 'A'); + assert_eq!(ch(&t, 0, 1), 'B'); + assert_eq!(ch(&t, 0, 2), 'C'); + assert_eq!(t.state.cursor_row, 0); + assert_eq!(t.state.cursor_col, 3); + } + // ── Cursor clamping ─────────────────────────────────────────────────────── #[test]