use std::sync::{Arc, Mutex};
mod ansi;
mod buffer;
mod cursor;
mod errors;
mod state;
use ansi::{parse_escape_sequence, AnsiCommand, AnsiParser, ClearMode, ControlChar, Token};
use state::TtyState;
pub struct VirtualTty {
state: Arc<Mutex<TtyState>>,
width: usize,
height: usize,
}
impl VirtualTty {
pub fn new(width: usize, height: usize) -> Self {
let state = TtyState::new(width, height);
Self {
state: Arc::new(Mutex::new(state)),
width,
height,
}
}
pub fn get_width(&self) -> usize {
self.width
}
pub fn get_height(&self) -> usize {
self.height
}
pub fn get_size(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn stdout_write(&mut self, data: &str) {
self.write_internal(data);
}
pub fn stderr_write(&mut self, data: &str) {
self.write_internal(data);
}
pub fn send_input(&mut self, input: &str) {
self.write_internal(input);
}
fn write_internal(&mut self, data: &str) {
match AnsiParser::parse(data) {
Ok(tokens) => {
let mut state = self.state.lock().unwrap();
for token in tokens {
self.process_token(token, &mut state);
}
}
Err(_) => {
self.write_internal_legacy(data);
}
}
}
fn process_token(&self, token: Token, state: &mut TtyState) {
match token {
Token::Text(text) => {
for ch in text.chars() {
let cursor_row = state.cursor.row;
let cursor_col = state.cursor.col;
if cursor_row < self.height && cursor_col < self.width {
state.buffer.set_char(cursor_row, cursor_col, ch);
if state.cursor.advance(self.width, self.height) {
state.buffer.scroll_up();
}
}
}
}
Token::Command(command) => {
if command.validate().is_ok() {
self.execute_ansi_command(&command, state);
}
}
Token::ControlChar(ctrl_char) => {
match ctrl_char {
ControlChar::LineFeed => {
if state.cursor.newline(self.height) {
state.buffer.scroll_up();
}
}
ControlChar::CarriageReturn => {
state.cursor.carriage_return();
}
ControlChar::Backspace => {
state.cursor.backspace();
}
ControlChar::Tab => {
let tab_width = 8;
let cursor_col = state.cursor.col;
let next_tab_stop = ((cursor_col / tab_width) + 1) * tab_width;
let spaces_to_add = next_tab_stop - cursor_col;
for _ in 0..spaces_to_add {
let cursor_row = state.cursor.row;
let cursor_col = state.cursor.col;
if cursor_row < self.height && cursor_col < self.width {
state.buffer.set_char(cursor_row, cursor_col, ' ');
if state.cursor.advance(self.width, self.height) {
state.buffer.scroll_up();
}
}
}
}
ControlChar::Bell => {
}
ControlChar::VerticalTab => {
if state.cursor.newline(self.height) {
state.buffer.scroll_up();
}
}
ControlChar::FormFeed => {
state.buffer.clear();
state.cursor.set_position(0, 0, self.height, self.width);
}
}
}
Token::Invalid(_) => {
}
}
}
fn write_internal_legacy(&mut self, data: &str) {
let mut state = self.state.lock().unwrap();
let mut chars = data.chars();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.next() == Some('[') {
if let Some(command) = parse_escape_sequence(&mut chars) {
self.execute_ansi_command(&command, &mut state);
}
}
} else if ch == '\r' {
state.cursor.carriage_return();
} else if ch == '\n' {
if state.cursor.newline(self.height) {
state.buffer.scroll_up();
}
} else if ch == '\x08' {
state.cursor.backspace();
} else {
let cursor_row = state.cursor.row;
let cursor_col = state.cursor.col;
if cursor_row < self.height && cursor_col < self.width {
state.buffer.set_char(cursor_row, cursor_col, ch);
if state.cursor.advance(self.width, self.height) {
state.buffer.scroll_up();
}
}
}
}
}
fn execute_ansi_command(&self, command: &AnsiCommand, state: &mut TtyState) {
match command {
AnsiCommand::CursorUp(n) => {
state.cursor.move_up(*n);
}
AnsiCommand::CursorDown(n) => {
state.cursor.move_down(*n, self.height);
}
AnsiCommand::CursorForward(n) => {
state.cursor.move_forward(*n, self.width);
}
AnsiCommand::CursorBack(n) => {
state.cursor.move_back(*n);
}
AnsiCommand::CursorPosition { row, col } => {
state
.cursor
.set_position(*row, *col, self.height, self.width);
}
AnsiCommand::ClearScreen(clear_mode) => match clear_mode {
ClearMode::Entire => {
state.buffer.clear();
state.cursor.set_position(0, 0, self.height, self.width);
}
ClearMode::ToBeginning => {
let cursor_row = state.cursor.row;
let cursor_col = state.cursor.col;
state
.buffer
.clear_from_beginning_to_cursor(cursor_row, cursor_col);
}
ClearMode::ToEnd => {
let cursor_row = state.cursor.row;
let cursor_col = state.cursor.col;
state
.buffer
.clear_from_cursor_to_end(cursor_row, cursor_col);
}
},
AnsiCommand::ClearLine(clear_mode) => match clear_mode {
ClearMode::Entire => {
let cursor_row = state.cursor.row;
state.buffer.clear_entire_line(cursor_row);
}
ClearMode::ToBeginning => {
let cursor_row = state.cursor.row;
let cursor_col = state.cursor.col;
state
.buffer
.clear_line_from_beginning_to_cursor(cursor_row, cursor_col);
}
ClearMode::ToEnd => {
let cursor_row = state.cursor.row;
let cursor_col = state.cursor.col;
state
.buffer
.clear_line_from_cursor_to_end(cursor_row, cursor_col);
}
},
AnsiCommand::SetGraphicsRendition => {
}
}
}
pub fn get_snapshot(&self) -> String {
let state = self.state.lock().unwrap();
state.get_snapshot()
}
pub fn clear(&mut self) {
let mut state = self.state.lock().unwrap();
state.clear(self.width, self.height);
}
pub fn get_cursor_position(&self) -> (usize, usize) {
let state = self.state.lock().unwrap();
state.get_cursor_position()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let tty = VirtualTty::new(80, 24);
assert_eq!(tty.get_width(), 80);
assert_eq!(tty.get_height(), 24);
assert_eq!(tty.get_size(), (80, 24));
}
#[test]
fn test_basic_write() {
let mut tty = VirtualTty::new(10, 3);
tty.stdout_write("Hello");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Hello \n
\n
\n
");
}
#[test]
fn test_newline() {
let mut tty = VirtualTty::new(10, 3);
tty.stdout_write("Line1\nLine2");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Line1 \n
Line2 \n
\n
");
}
#[test]
fn test_line_wrap() {
let mut tty = VirtualTty::new(5, 3);
tty.stdout_write("HelloWorld");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Hello\n
World\n
\n
");
}
#[test]
fn test_clear_screen() {
let mut tty = VirtualTty::new(10, 3);
tty.stdout_write("Hello\nWorld");
tty.stdout_write("\x1b[2J");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
\n
\n
\n
");
}
#[test]
fn test_stderr() {
let mut tty = VirtualTty::new(10, 3);
tty.stderr_write("Error!");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Error! \n
\n
\n
");
}
#[test]
fn test_scroll() {
let mut tty = VirtualTty::new(10, 2);
tty.stdout_write("Line1\nLine2\nLine3");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Line2 \n
Line3 \n
");
}
#[test]
fn test_clear() {
let mut tty = VirtualTty::new(10, 3);
tty.stdout_write("Hello\nWorld");
tty.clear();
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
\n
\n
\n
");
}
#[test]
fn test_stderr_basic_write() {
let mut tty = VirtualTty::new(10, 3);
tty.stderr_write("Hello");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Hello \n
\n
\n
");
}
#[test]
fn test_stderr_newline() {
let mut tty = VirtualTty::new(10, 3);
tty.stderr_write("Line1\nLine2");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Line1 \n
Line2 \n
\n
");
}
#[test]
fn test_stderr_line_wrap() {
let mut tty = VirtualTty::new(5, 3);
tty.stderr_write("HelloWorld");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Hello\n
World\n
\n
");
}
#[test]
fn test_stderr_clear_screen() {
let mut tty = VirtualTty::new(10, 3);
tty.stderr_write("Hello\nWorld");
tty.stderr_write("\x1b[2J");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
\n
\n
\n
");
}
#[test]
fn test_stderr_scroll() {
let mut tty = VirtualTty::new(10, 2);
tty.stderr_write("Line1\nLine2\nLine3");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Line2 \n
Line3 \n
");
}
#[test]
fn test_mixed_stdout_stderr() {
let mut tty = VirtualTty::new(15, 3);
tty.stdout_write("Hello");
tty.stderr_write(" World");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
Hello World \n
\n
\n
");
}
#[test]
fn test_stderr_with_ansi_escape() {
let mut tty = VirtualTty::new(10, 3);
tty.stderr_write("Hello");
tty.stderr_write("\x1b[1A"); tty.stderr_write("X");
let snapshot = tty.get_snapshot();
insta::assert_snapshot!(snapshot, @r"
HelloX \n
\n
\n
");
}
}