use std::io;
pub trait Terminal {
fn start(&mut self) -> io::Result<()>;
fn stop(&mut self) -> io::Result<()>;
fn write(&mut self, data: &str) -> io::Result<()>;
fn size(&self) -> io::Result<(u16, u16)>;
fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()>;
fn hide_cursor(&mut self) -> io::Result<()>;
fn show_cursor(&mut self) -> io::Result<()>;
}
pub struct TestTerminal {
cols: u16,
rows: u16,
buffer: Vec<String>,
cursor_moves: Vec<(u16, u16)>,
cursor_hidden: bool,
}
impl TestTerminal {
pub fn new(cols: u16, rows: u16) -> Self {
Self {
cols,
rows,
buffer: Vec::new(),
cursor_moves: Vec::new(),
cursor_hidden: false,
}
}
pub fn written(&self) -> &Vec<String> {
&self.buffer
}
pub fn cursor_moves(&self) -> &Vec<(u16, u16)> {
&self.cursor_moves
}
pub fn is_cursor_hidden(&self) -> bool {
self.cursor_hidden
}
}
impl Terminal for TestTerminal {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
fn write(&mut self, data: &str) -> io::Result<()> {
self.buffer.push(data.to_string());
Ok(())
}
fn size(&self) -> io::Result<(u16, u16)> {
Ok((self.cols, self.rows))
}
fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()> {
self.cursor_moves.push((row, col));
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.cursor_hidden = true;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
self.cursor_hidden = false;
Ok(())
}
}
pub struct ProcessTerminal {
stdout: Box<dyn std::io::Write>,
is_tty: bool,
}
impl ProcessTerminal {
pub fn new() -> Self {
Self {
stdout: Box::new(std::io::stdout()),
is_tty: true,
}
}
#[cfg(test)]
pub fn new_test() -> Self {
Self {
stdout: Box::new(Vec::new()),
is_tty: false,
}
}
}
impl Terminal for ProcessTerminal {
fn start(&mut self) -> io::Result<()> {
if self.is_tty {
crossterm::terminal::enable_raw_mode()?;
}
crossterm::execute!(
self.stdout,
crossterm::terminal::EnterAlternateScreen,
crossterm::cursor::Hide
)?;
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
crossterm::execute!(
self.stdout,
crossterm::cursor::Show,
crossterm::terminal::LeaveAlternateScreen
)?;
if self.is_tty {
crossterm::terminal::disable_raw_mode()?;
}
Ok(())
}
fn write(&mut self, data: &str) -> io::Result<()> {
use std::io::Write;
self.stdout.write_all(data.as_bytes())?;
self.stdout.flush()?;
Ok(())
}
fn size(&self) -> io::Result<(u16, u16)> {
if self.is_tty {
crossterm::terminal::size()
} else {
Ok((80, 24))
}
}
fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()> {
crossterm::execute!(self.stdout, crossterm::cursor::MoveTo(col, row))?;
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
crossterm::execute!(self.stdout, crossterm::cursor::Hide)?;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
crossterm::execute!(self.stdout, crossterm::cursor::Show)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn process_terminal_all_methods() {
let mut term = ProcessTerminal::new_test();
term.start().unwrap();
term.write("hello").unwrap();
assert_eq!(term.size().unwrap(), (80, 24));
term.move_cursor(5, 10).unwrap();
term.hide_cursor().unwrap();
term.show_cursor().unwrap();
term.stop().unwrap();
}
#[test]
fn process_terminal_new_does_not_panic() {
let _term = ProcessTerminal::new();
}
}