diff --git a/Cargo.lock b/Cargo.lock index cb0fe5770..e2b7ed9d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6341,9 +6341,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vt100" -version = "0.16.2" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9" +checksum = "eb05cdd19ae8707bf6a0a269972c07e6358103a7079faec749dec318a5b3dc23" dependencies = [ "itoa", "unicode-width 0.2.2", diff --git a/Cargo.toml b/Cargo.toml index 9dc4101e8..6f83e2b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ fancy-regex = "0.19" chrono = { version = "0.4", default-features = false, features = ["std", "clock"] } chrono-tz = "0.10" # In-process terminal screen model (terminal feature) -vt100 = "0.16" +vt100 = "0.18" # Compression flate2 = "1" diff --git a/crates/bashkit/src/terminal/mod.rs b/crates/bashkit/src/terminal/mod.rs index 2a70255d0..14fc91126 100644 --- a/crates/bashkit/src/terminal/mod.rs +++ b/crates/bashkit/src/terminal/mod.rs @@ -339,7 +339,9 @@ impl Terminal { pub fn screen_rows(&self) -> Vec { self.tty.with_screen(|s| { let (_, cols) = s.size(); - s.rows(0, cols).map(|r| r.trim_end().to_string()).collect() + s.rows(0, cols.get()) + .map(|r| r.trim_end().to_string()) + .collect() }) } @@ -396,7 +398,10 @@ impl Terminal { Some(self.tty.with_screen(|s| { let (row, col) = s.cursor_position(); let (_, cols) = s.size(); - let text = s.rows(0, cols).nth(usize::from(row)).unwrap_or_default(); + let text = s + .rows(0, cols.get()) + .nth(usize::from(row)) + .unwrap_or_default(); text.chars().take(usize::from(col)).collect::() })) } diff --git a/crates/bashkit/src/terminal/tty.rs b/crates/bashkit/src/terminal/tty.rs index bca990ec9..3eb750aa9 100644 --- a/crates/bashkit/src/terminal/tty.rs +++ b/crates/bashkit/src/terminal/tty.rs @@ -6,6 +6,7 @@ //! holds the lock across an `.await`. use std::collections::VecDeque; +use std::num::NonZero; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; @@ -25,6 +26,15 @@ pub(crate) const MAX_OUTPUT_TAP: usize = 256 * 1024; /// Lines of primary-screen history kept for `history_text`. pub(crate) const SCROLLBACK_LINES: usize = 1000; +/// vt100 0.18 takes screen dimensions as `NonZero`; [`TerminalSize`] keeps +/// plain `u16` because its fields are public API. `TerminalSize::new` already +/// clamps to 2, but the struct can also be built literally, so a zero can still +/// reach here. Clamp it to 1 rather than panic: a degenerate size is the +/// caller's to notice, and it is not worth aborting a session over. +fn nz(v: u16) -> NonZero { + NonZero::new(v).unwrap_or(NonZero::::MIN) +} + /// What a blocking terminal read returned. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TtyEvent { @@ -85,7 +95,7 @@ impl Tty { reading_line: false, resized: false, closed: false, - screen: vt100::Parser::new(size.rows, size.cols, SCROLLBACK_LINES), + screen: vt100::Parser::new(nz(size.rows), nz(size.cols), SCROLLBACK_LINES), output: Vec::new(), tap: Vec::new(), tap_total: 0, @@ -136,7 +146,9 @@ impl Tty { pub(crate) fn resize(&self, size: TerminalSize) { { let mut st = self.lock(); - st.screen.screen_mut().set_size(size.rows, size.cols); + st.screen + .screen_mut() + .set_size(nz(size.rows), nz(size.cols)); st.resized = true; st.waiting = false; } @@ -190,6 +202,7 @@ impl Tty { let mut st = self.lock(); let screen = st.screen.screen_mut(); let (rows, cols) = screen.size(); + let (rows, cols) = (rows.get(), cols.get()); let rows = usize::from(rows); screen.set_scrollback(usize::MAX); let total = screen.scrollback(); @@ -212,7 +225,10 @@ impl Tty { pub(crate) fn size(&self) -> TerminalSize { let (rows, cols) = self.lock().screen.screen().size(); - TerminalSize { rows, cols } + TerminalSize { + rows: rows.get(), + cols: cols.get(), + } } pub(crate) fn set_raw(&self, raw: bool) { diff --git a/knowledge/integrations/in-process-terminal.md b/knowledge/integrations/in-process-terminal.md index 021d8fb44..b66b009f8 100644 --- a/knowledge/integrations/in-process-terminal.md +++ b/knowledge/integrations/in-process-terminal.md @@ -61,6 +61,13 @@ C FFI, which would break the wasm, PyPI and npm builds, for a feature whose screen needs are basic (cells, cursor, alternate screen). Revisit only if fidelity gaps show up that vt100 cannot close. +From vt100 0.18 the screen dimensions are `NonZero`. `TerminalSize` keeps +plain `u16`, because its fields are public API, and converts at the vt100 +boundary only. `TerminalSize::new` clamps to 2, but the struct can also be +built literally, so a zero can still reach that boundary; it clamps to 1 rather +than panicking, since a degenerate size is the caller's to notice and not worth +aborting a session over. + ## Decision: device handle via execution extensions The shell loop passes the `Tty` handle on every `exec_with_options` call diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 5d5e3a6f9..1937c57e0 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -2114,9 +2114,9 @@ version = "0.9.5" criteria = "safe-to-deploy" [[exemptions.vt100]] -version = "0.16.2" +version = "0.18.1" criteria = "safe-to-deploy" -notes = "Terminal screen model for the optional `terminal` feature. Pure parsing/state; reviewed: no unsafe, no host I/O, process, or network access." +notes = "Terminal screen model for the optional `terminal` feature. Pure parsing/state. The exemption moves from 0.16.2 rather than gaining a delta audit: that delta is ~3100 lines across a dozen files plus a new capture.rs, so a delta audit would claim a review that diff does not get. Checked instead for what the exemption asserts, and all of it still holds: zero `unsafe` in src (unchanged from 0.16.2), zero use of std::fs, std::net, std::process or std::env, and no new runtime dependency -- 0.18.1 adds only an optional `embedded-io`, which stays unenabled and is absent from vt100's resolved deps in Cargo.lock (itoa, unicode-width, vte); vte stays at 0.15.0." [[exemptions.vte]] version = "0.15.0"