From 34b686a35a3e6ea0dca9bc0347772684fe37b016 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Tue, 28 Jul 2026 08:51:38 +0200 Subject: [PATCH 1/2] migrate test terminal emulator from vt100 to rio-vt --- Cargo.toml | 2 +- src/multiselect.rs | 14 ++++++------ src/select.rs | 6 ++--- src/test.rs | 57 +++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aad6fe4..61deda2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,10 @@ libc = "0.2" ctor = "1" indoc = "2" insta = "1" -vt100 = "0.16" [target.'cfg(unix)'.dev-dependencies] portable-pty = "0.9" +rio-vt = "0.5.1" [profile.dev] debug = 1 diff --git a/src/multiselect.rs b/src/multiselect.rs index 2420c7e..c4d1552 100644 --- a/src/multiselect.rs +++ b/src/multiselect.rs @@ -711,7 +711,7 @@ impl<'a, T> MultiSelect<'a, T> { mod tests { use crate::test::without_ansi; #[cfg(unix)] - use crate::test::{capture_term, replay, snapshot}; + use crate::test::{capture_term, replay, snapshot, Parser}; use super::*; use indoc::indoc; @@ -799,7 +799,7 @@ mod tests { /// code (jdx/demand#129), which made the menu drift up on every keypress. /// /// Drives the redraw path through a `Term::read_write_pair` whose writer is - /// a `SharedBuf`, replays the captured bytes through a vt100 emulator, and + /// a `SharedBuf`, replays the captured bytes through a terminal emulator, and /// asserts that the cursor row is stable across iterations (i.e. the render /// origin doesn't drift). #[cfg(unix)] @@ -813,10 +813,10 @@ mod tests { .option(DemandOption::new("Nutella")); ms.term = term; - // Start the vt100 cursor well below the top of the screen, so that an + // Start the emulator cursor well below the top of the screen, so that an // upward off-by-one would actually shift the rendered area visibly // rather than getting silently clamped at row 0. - let mut parser = vt100::Parser::new(40, 120, 0); + let mut parser = Parser::new(40, 120, 0); parser.process(&b"\n".repeat(20)); let mut cursor_rows = Vec::new(); @@ -833,7 +833,7 @@ mod tests { ms.handle_down().unwrap(); } - // After iter 1 the vt100 cursor settles at the end of the frame. + // After iter 1 the emulator cursor settles at the end of the frame. // With the bug, every subsequent iter pulls it up by another row; with // the fix it stays put. let first = cursor_rows[0]; @@ -857,7 +857,7 @@ mod tests { .option(DemandOption::new("c")); ms.term = term; - let mut parser = vt100::Parser::new(40, 120, 0); + let mut parser = Parser::new(40, 120, 0); parser.process(&b"\n".repeat(20)); // First render with all 3 options. @@ -901,7 +901,7 @@ mod tests { let width = ms.term.size().1 as usize; assert!(width < 140, "test needs a label wider than the terminal"); - let mut parser = vt100::Parser::new(40, width as u16, 0); + let mut parser = Parser::new(40, width as u16, 0); parser.process(&b"\n".repeat(20)); for _ in 0..3 { diff --git a/src/select.rs b/src/select.rs index 1441132..7279341 100644 --- a/src/select.rs +++ b/src/select.rs @@ -543,7 +543,7 @@ impl<'a, T> Select<'a, T> { mod tests { use crate::test::without_ansi; #[cfg(unix)] - use crate::test::{capture_term, replay, snapshot}; + use crate::test::{capture_term, replay, snapshot, Parser}; use super::*; use indoc::indoc; @@ -623,7 +623,7 @@ mod tests { /// leftovers, so the prompt visibly duplicated itself. /// /// Drives three redraw cycles through a captured `Term`, replays the - /// bytes through a vt100 emulator, and asserts the screen holds one + /// bytes through a terminal emulator, and asserts the screen holds one /// copy of the prompt. #[cfg(unix)] #[test] @@ -646,7 +646,7 @@ mod tests { select.cursor_y = i % select.options.len(); } - let mut parser = vt100::Parser::new(24, width as u16, 0); + let mut parser = Parser::new(24, width as u16, 0); replay(&mut parser, &snapshot(&buf)); let screen = parser.screen().contents(); assert_eq!( diff --git a/src/test.rs b/src/test.rs index 1223913..401bc13 100644 --- a/src/test.rs +++ b/src/test.rs @@ -11,23 +11,74 @@ pub fn without_ansi(s: &str) -> Cow<'_, str> { } /// A `Term` whose output is captured in memory, for driving a widget's -/// redraw path and replaying the bytes through a vt100 emulator. +/// redraw path and replaying the bytes through a terminal emulator. /// /// `Term::read_write_pair` is `#[cfg(unix)]` in the console crate. CI also /// runs on Windows, where there is simply no test seam — the redraw bugs /// these cover were reported on Linux/macOS and the fixes are /// platform-agnostic, so unix-only coverage is sufficient. #[cfg(unix)] -pub use capture::{capture_term, replay, snapshot}; +pub use capture::{capture_term, replay, snapshot, Parser}; #[cfg(unix)] mod capture { use console::Term; + use rio_vt::ansi::CursorShape; + use rio_vt::crosswords::formatter::FormatOptions; + use rio_vt::crosswords::{Crosswords, CrosswordsSize}; + use rio_vt::event::{VoidListener, WindowId}; + use rio_vt::performer::handler::Processor; use std::fs::{File, OpenOptions}; use std::io::Write; use std::os::fd::{AsRawFd, RawFd}; use std::sync::{Arc, Mutex}; + /// Minimal vt100-shaped adapter over rio-vt: feed bytes with `process` + /// and read the screen back through `screen()`. + pub struct Parser { + term: Crosswords, + processor: Processor, + } + + impl Parser { + pub fn new(rows: u16, cols: u16, _scrollback: usize) -> Self { + Self { + term: Crosswords::new( + CrosswordsSize::new(cols as usize, rows as usize), + CursorShape::Block, + VoidListener, + WindowId::from(0), + 0, + 0, + ), + processor: Processor::default(), + } + } + + pub fn process(&mut self, bytes: &[u8]) { + self.processor.advance(&mut self.term, bytes); + } + + pub fn screen(&self) -> Screen<'_> { + Screen { term: &self.term } + } + } + + pub struct Screen<'a> { + term: &'a Crosswords, + } + + impl Screen<'_> { + pub fn contents(&self) -> String { + self.term.format(FormatOptions::plain()) + } + + pub fn cursor_position(&self) -> (u16, u16) { + let pos = self.term.cursor().pos; + (pos.row.0 as u16, pos.col.0 as u16) + } + } + /// `Term::read_write_pair` is bounded by `Write + Debug + AsRawFd + Send /// + 'static`. It only uses the fd for its own `AsRawFd` impl — the /// actual I/O goes through `Write::write_all` — so we can satisfy the @@ -73,7 +124,7 @@ mod capture { /// Unix TTY performs. The emulator does not apply ONLCR itself, so feeding /// raw `\n` bytes would leave the cursor column unchanged and make redraw /// assertions model a pipe rather than a terminal. - pub fn replay(parser: &mut vt100::Parser, output: &[u8]) { + pub fn replay(parser: &mut Parser, output: &[u8]) { let mut tty_output = Vec::with_capacity(output.len()); for &byte in output { if byte == b'\n' { From e1c9441ecffa693161a892d081a286a207831390 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Tue, 28 Jul 2026 14:01:30 +0200 Subject: [PATCH 2/2] sort test imports for rustfmt --- src/multiselect.rs | 2 +- src/select.rs | 2 +- src/test.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/multiselect.rs b/src/multiselect.rs index c4d1552..4b24c00 100644 --- a/src/multiselect.rs +++ b/src/multiselect.rs @@ -711,7 +711,7 @@ impl<'a, T> MultiSelect<'a, T> { mod tests { use crate::test::without_ansi; #[cfg(unix)] - use crate::test::{capture_term, replay, snapshot, Parser}; + use crate::test::{Parser, capture_term, replay, snapshot}; use super::*; use indoc::indoc; diff --git a/src/select.rs b/src/select.rs index 7279341..2542cee 100644 --- a/src/select.rs +++ b/src/select.rs @@ -543,7 +543,7 @@ impl<'a, T> Select<'a, T> { mod tests { use crate::test::without_ansi; #[cfg(unix)] - use crate::test::{capture_term, replay, snapshot, Parser}; + use crate::test::{Parser, capture_term, replay, snapshot}; use super::*; use indoc::indoc; diff --git a/src/test.rs b/src/test.rs index 401bc13..479c57c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -18,7 +18,7 @@ pub fn without_ansi(s: &str) -> Cow<'_, str> { /// these cover were reported on Linux/macOS and the fixes are /// platform-agnostic, so unix-only coverage is sufficient. #[cfg(unix)] -pub use capture::{capture_term, replay, snapshot, Parser}; +pub use capture::{Parser, capture_term, replay, snapshot}; #[cfg(unix)] mod capture {