diff --git a/Cargo.toml b/Cargo.toml index 035eaf3..9b3eff2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ description = "Wipe your terminal with a random animation." repository = "https://github.com/ricoriedel/wipe" [dependencies] +anyhow = "1.0" clap = { version = "3.1", features = ["derive"]} crossterm = "0.23" rand = "0.8" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ffbcb97..a5bac09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod char; mod fill; mod vec; mod array; +mod render; #[derive(Parser)] #[clap(author = "Rico Riedel", version = "0.1.0", about = "Wipe your terminal with a random animation.")] diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..c348755 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,208 @@ +use anyhow::Error; +use crossterm::cursor::MoveTo; +use crossterm::{ExecutableCommand, QueueableCommand}; +use crossterm::style::{Color, Print, SetForegroundColor}; +use crossterm::terminal::{Clear, ClearType}; +use std::io::Write; +use crate::array::Array2D; + +pub trait Renderer { + fn width(&self) -> usize; + fn height(&self) -> usize; + fn draw(&mut self, x: usize, y: usize, char: char, color: Color); + fn clear(&mut self, x: usize, y: usize); + fn render(&mut self) -> Result<(), Error>; + fn purge(&mut self) -> Result<(), Error>; +} + +pub struct WriteRenderer { + out: Box, + array: Array2D, +} + + +#[derive(Copy, Clone)] +enum Cell { + Keep, + Draw { char: char, color: Color }, +} + +impl Default for Cell { + fn default() -> Self { Cell::Keep } +} + +impl WriteRenderer { + pub fn new(out: Box, width: usize, height: usize) -> Self { + Self { + out, + array: Array2D::new(width, height) + } + } +} + +impl Renderer for WriteRenderer { + fn width(&self) -> usize { + self.array.width() + } + + fn height(&self) -> usize { + self.array.height() + } + + fn draw(&mut self, x: usize, y: usize, char: char, color: Color) { + self.array[(x, y)] = Cell::Draw { char, color }; + } + + fn clear(&mut self, x: usize, y: usize) { + self.array[(x, y)] = Cell::Draw { char: ' ', color: Color::Reset }; + } + + fn render(&mut self) -> Result<(), Error> { + let mut needs_move; + let mut last_color = None; + + for y in 0..self.array.height() { + needs_move = true; + + for x in 0..self.array.width() { + match self.array[(x, y)] { + Cell::Keep => { + needs_move = true; + } + Cell::Draw { char, color } => { + if needs_move { + needs_move = false; + self.out.queue(MoveTo(x as u16, y as u16))?; + } + if last_color.is_none() || last_color.unwrap() != color { + last_color = Some(color); + self.out.queue(SetForegroundColor(color))?; + } + self.out.queue(Print(char))?; + } + } + } + } + self.out.queue(MoveTo(0, 0))?; + self.out.flush()?; + Ok(()) + } + + fn purge(&mut self) -> Result<(), Error> { + self.out.execute(Clear(ClearType::Purge))?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::cell::RefCell; + use std::rc::Rc; + use super::*; + + #[derive(PartialEq, Debug)] + struct Data { + flushed: Vec>, + buffer: Vec + } + + struct MockWrite { + data: Rc> + } + + impl Data { + fn new() -> Rc> { + Rc::new(RefCell::new(Self { + flushed: Vec::new(), + buffer: Vec::new() + })) + } + } + + impl MockWrite { + fn new(data: Rc>) -> Box { + Box::new(Self { data }) + } + } + + impl Write for MockWrite { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.data.borrow_mut().buffer.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + let data = self.data.borrow_mut().buffer.drain(..).collect(); + + self.data.borrow_mut().flushed.push(data); + Ok(()) + } + } + + #[test] + fn width() { + let data = Data::new(); + let mock = MockWrite::new(data); + let renderer = WriteRenderer::new(mock, 10, 2); + + assert_eq!(10, renderer.width()); + } + + #[test] + fn height() { + let data = Data::new(); + let mock = MockWrite::new(data); + let renderer = WriteRenderer::new(mock, 5, 8); + + assert_eq!(8, renderer.height()); + } + + #[test] + fn purge() { + // Execute + let data = Data::new(); + let mock = MockWrite::new(data.clone()); + let mut renderer = WriteRenderer::new(mock, 4, 4); + + renderer.purge().unwrap(); + + // Recreate expectation + let expected = Data::new(); + let mut stream = MockWrite::new(expected.clone()); + + stream.execute(Clear(ClearType::Purge)).unwrap(); + + // Compare + assert_eq!(expected, data); + } + + #[test] + fn render() { + // Execute + let data = Data::new(); + let mock = MockWrite::new(data.clone()); + let mut renderer = WriteRenderer::new(mock, 3, 2); + + renderer.draw(0, 0, 'A', Color::Green); + renderer.draw(1, 0, 'x', Color::Green); + renderer.clear(1, 1); + renderer.render().unwrap(); + + // Recreate expectation + let expected = Data::new(); + let mut stream = MockWrite::new(expected.clone()); + + stream.queue(MoveTo(0, 0)).unwrap(); + stream.queue(SetForegroundColor(Color::Green)).unwrap(); + stream.queue(Print('A')).unwrap(); + stream.queue(Print('x')).unwrap(); + stream.queue(MoveTo(1, 1)).unwrap(); + stream.queue(SetForegroundColor(Color::Reset)).unwrap(); + stream.queue(Print(' ')).unwrap(); + stream.queue(MoveTo(0, 0)).unwrap(); + stream.flush().unwrap(); + + // Compare + assert_eq!(expected, data); + } +} \ No newline at end of file