use std::collections::VecDeque;
use std::time::Instant;
use crate::base::{Result, Size};
use crate::term::{EnterOptions, TermRead, Terminal};
use super::vt::VtScreen;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ScriptedRead {
Input(Vec<u8>),
Resize(Size),
Wake,
Idle,
}
const IDLE_STORM_LIMIT: u32 = 10_000;
pub struct CaptureTerm {
size: Size,
entered: Option<EnterOptions>,
written: Vec<u8>,
flushed_len: usize,
flush_count: u64,
script: VecDeque<ScriptedRead>,
screen: VtScreen,
last_input: Vec<u8>,
idle_streak: u32,
fail_next_write: Option<std::io::ErrorKind>,
suspend_count: u64,
}
impl CaptureTerm {
pub fn new(size: Size) -> CaptureTerm {
CaptureTerm {
size,
entered: None,
written: Vec::new(),
flushed_len: 0,
flush_count: 0,
script: VecDeque::new(),
screen: VtScreen::new(size),
last_input: Vec::new(),
idle_streak: 0,
fail_next_write: None,
suspend_count: 0,
}
}
pub fn push_input(&mut self, bytes: &[u8]) {
self.script.push_back(ScriptedRead::Input(bytes.to_vec()));
}
pub fn push_resize(&mut self, size: Size) {
self.script.push_back(ScriptedRead::Resize(size));
}
pub fn push_wake(&mut self) {
self.script.push_back(ScriptedRead::Wake);
}
pub fn push_idle(&mut self) {
self.script.push_back(ScriptedRead::Idle);
}
pub fn script_len(&self) -> usize {
self.script.len()
}
pub fn fail_next_write(&mut self, kind: std::io::ErrorKind) {
self.fail_next_write = Some(kind);
}
pub fn take_bytes(&mut self) -> Vec<u8> {
self.flushed_len = 0;
std::mem::take(&mut self.written)
}
pub fn bytes(&self) -> &[u8] {
&self.written
}
pub fn unflushed_bytes(&self) -> &[u8] {
&self.written[self.flushed_len..]
}
pub fn flush_count(&self) -> u64 {
self.flush_count
}
pub fn is_entered(&self) -> bool {
self.entered.is_some()
}
pub fn suspend_count(&self) -> u64 {
self.suspend_count
}
pub fn enter_options(&self) -> Option<&EnterOptions> {
self.entered.as_ref()
}
pub fn screen(&self) -> &VtScreen {
&self.screen
}
pub fn as_terminal(&mut self) -> &mut dyn Terminal {
self
}
}
impl Terminal for CaptureTerm {
fn enter(&mut self, opts: &EnterOptions) -> Result<()> {
if self.entered.is_some() {
return Ok(()); }
self.entered = Some(*opts);
let bytes = opts.enter_bytes();
Terminal::write(self, &bytes)?;
self.flush()
}
fn leave(&mut self) -> Result<()> {
let Some(opts) = self.entered.take() else {
return Ok(()); };
let bytes = opts.leave_bytes();
Terminal::write(self, &bytes)?;
self.flush()
}
fn size(&mut self) -> Result<Size> {
Ok(self.size)
}
fn read(&mut self, _deadline: Option<Instant>) -> Result<TermRead<'_>> {
match self.script.pop_front() {
Some(ScriptedRead::Input(bytes)) => {
self.idle_streak = 0;
self.last_input = bytes;
Ok(TermRead::Input(&self.last_input))
}
Some(ScriptedRead::Resize(size)) => {
self.idle_streak = 0;
self.size = size;
Ok(TermRead::Resize(size))
}
Some(ScriptedRead::Wake) => {
self.idle_streak = 0;
Ok(TermRead::Wake)
}
Some(ScriptedRead::Idle) => {
self.idle_streak = 0;
Ok(TermRead::Idle)
}
None => {
self.idle_streak += 1;
assert!(
self.idle_streak < IDLE_STORM_LIMIT,
"CaptureTerm: {IDLE_STORM_LIMIT} consecutive reads on an exhausted \
script — the caller is busy-polling. Use an already-elapsed \
deadline (or script an explicit Idle) in tests."
);
Ok(TermRead::Idle)
}
}
}
fn write(&mut self, bytes: &[u8]) -> Result<()> {
if let Some(kind) = self.fail_next_write.take() {
return Err(std::io::Error::from(kind).into());
}
self.written.extend_from_slice(bytes);
self.screen.feed(bytes);
Ok(())
}
fn flush(&mut self) -> Result<()> {
if let Some(kind) = self.fail_next_write.take() {
return Err(std::io::Error::from(kind).into());
}
self.flushed_len = self.written.len();
self.flush_count += 1;
Ok(())
}
fn set_mouse_reporting(&mut self, on: bool) -> Result<()> {
let Some(opts) = self.entered else {
return Err(crate::base::Error::Term(
"set_mouse_reporting outside a session — enter() first".into(),
));
};
let mode = opts.mouse;
Terminal::write(
self,
if on {
mode.arm_bytes()
} else {
mode.disarm_bytes()
},
)
}
fn suspend(&mut self) -> Result<()> {
let Some(opts) = self.entered else {
return Err(crate::base::Error::Term(
"suspend outside a session — enter() first".into(),
));
};
Terminal::write(self, &opts.leave_bytes())?;
Terminal::write(self, &opts.enter_bytes())?;
self.flush()?;
self.suspend_count += 1;
Ok(())
}
fn set_kitty_keyboard(&mut self, flags: crate::term::KittyFlags) -> Result<()> {
let Some(opts) = &mut self.entered else {
return Err(crate::base::Error::Term(
"set_kitty_keyboard outside a session — enter() first".into(),
));
};
let prev = opts.kitty_keyboard;
if prev == flags {
return Ok(());
}
opts.kitty_keyboard = flags;
let mut bytes = Vec::with_capacity(16);
if !prev.is_empty() {
bytes.extend_from_slice(crate::term::KittyFlags::POP_BYTES);
}
if !flags.is_empty() {
bytes.extend_from_slice(&flags.push_bytes());
}
Terminal::write(self, &bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_reaches_screen_and_byte_log() {
let mut t = CaptureTerm::new(Size::new(10, 2));
Terminal::write(&mut t, b"\x1b[1;3Hhi").unwrap();
assert_eq!(t.bytes(), b"\x1b[1;3Hhi");
assert_eq!(t.screen().cell(2, 0).unwrap().ch(), 'h');
assert_eq!(t.take_bytes(), b"\x1b[1;3Hhi".to_vec());
assert!(t.bytes().is_empty());
assert_eq!(t.screen().cell(2, 0).unwrap().ch(), 'h');
}
#[test]
fn enter_and_leave_balance_modes_via_screen() {
let mut t = CaptureTerm::new(Size::new(8, 2));
let opts = EnterOptions::default();
t.enter(&opts).unwrap();
assert!(t.screen().modes().alt_screen());
assert!(!t.screen().modes().cursor_visible());
t.enter(&opts).unwrap(); t.leave().unwrap();
assert!(!t.screen().modes().alt_screen());
assert!(t.screen().modes().cursor_visible());
t.leave().unwrap(); assert_eq!(t.screen().unknown_seq_count(), 0);
}
#[test]
fn scripted_reads_fifo_through_the_trait() {
let mut t = CaptureTerm::new(Size::new(4, 1));
t.push_input(b"\x1b[A");
t.push_resize(Size::new(8, 2));
t.push_idle();
let term: &mut dyn Terminal = &mut t;
assert!(matches!(
term.read(None).unwrap(),
TermRead::Input(b"\x1b[A")
));
assert!(matches!(term.read(None).unwrap(), TermRead::Resize(s) if s == Size::new(8, 2)));
assert!(matches!(term.read(None).unwrap(), TermRead::Idle));
assert!(matches!(term.read(None).unwrap(), TermRead::Idle)); assert_eq!(term.size().unwrap(), Size::new(8, 2)); }
#[test]
#[should_panic(expected = "exhausted")]
fn idle_storm_guard_fires() {
let mut t = CaptureTerm::new(Size::new(4, 1));
for _ in 0..20_000 {
let _ = Terminal::read(&mut t, None);
}
}
#[test]
fn write_failure_injection() {
let mut t = CaptureTerm::new(Size::new(4, 1));
t.fail_next_write(std::io::ErrorKind::BrokenPipe);
assert!(Terminal::write(&mut t, b"x").is_err());
Terminal::write(&mut t, b"y").unwrap(); }
}