tty-overwriter 0.1.0

A lib to ease overwriting text in stdout. Make a body, then overwrite it as much as needed.
Documentation
use crate::prelude::AnsiSeq;
use std::io::Write;

#[derive(Debug, Default)]
/// A struct to call `overwrite` on, to avoid flicker in terminal.
pub struct Body {
    buffer: Vec<usize>,
}

const CLEAR_TIL_EOL: AnsiSeq = AnsiSeq::ClearCursorToEndOfLine;
const CLEAR_TIL_EOF: AnsiSeq = AnsiSeq::ClearCursorToEndOfScreen;
const JUMP_AT_BEGINNING: AnsiSeq = AnsiSeq::AbsoluteMove { horizontal: 0 };

impl Body {
    /// overwrite:
    /// write `new_text` to `write`
    /// and erase the previous text you overwrote with AnsiSeq codes,
    /// making educated guesses about the height to clear
    /// from `available_width` variable and `self.buffer` previous text line lengths.
    /// The goal of `Body` and overwrite is to provide easy text writing to terminal without flicker.
    pub fn overwrite<T, Writer>(
        &mut self,
        new_text: &T,
        mut write: Writer,
        available_width: usize,
    ) -> std::io::Result<()>
    where
        Writer: Write,
        T: ToString,
    {
        let new_text = new_text.to_string();
        let mut next_buffer = if self.buffer.is_empty() {
            vec![]
        } else {
            Vec::with_capacity(self.buffer.capacity())
        };
        let mut symbols = vec![];

        if new_text.is_empty() {
            write!(symbols, "{CLEAR_TIL_EOL}")?;
            next_buffer.push(0);
        } else {
            for (line_no, line) in new_text.lines().enumerate() {
                next_buffer.push(line.len());
                if line_no > 0 {
                    symbols.push(b'\n');
                }
                symbols.write_all(line.as_bytes())?;
                write!(symbols, "{CLEAR_TIL_EOL}")?;
            }
        }

        match self.guess_previous_body_height(available_width) {
            1 => {
                write!(write, "{JUMP_AT_BEGINNING}")?;
            }
            lines if lines > 1 => {
                let movement = AnsiSeq::MoveLines {
                    down: 0,
                    up: (lines - 1) as u16,
                };
                write!(write, "{JUMP_AT_BEGINNING}{movement}")?;
            }
            _ => {}
        }

        write!(symbols, "{CLEAR_TIL_EOF}")?;
        write.write_all(&symbols)?;
        self.buffer = next_buffer;
        Ok(())
    }

    /// create a new Body
    pub fn new() -> Self {
        Default::default()
    }

    fn guess_previous_body_height(&self, available_width: usize) -> usize {
        let mut lines = 0;
        for line in &self.buffer {
            lines += line / available_width + 1;
        }
        lines
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error;
    use std::fmt::Write;

    #[test]
    fn initial() -> Result<(), Box<dyn Error>> {
        let mut body = Body::default();
        let mut buf = Vec::new();
        body.overwrite(&"content", &mut buf, 80)?;

        let string = String::from_utf8(buf.clone())?;
        assert_eq!("content\\e[0K\\e[0J", string);

        body.overwrite(&"content + content", &mut buf, 80)?;
        let string = String::from_utf8(buf.clone())?;
        assert_eq!(
            "content\\e[0K\\e[0J\\e[0Gcontent + content\\e[0K\\e[0J",
            string
        );

        body.overwrite(&"none", &mut buf, 80)?;
        let string = String::from_utf8(buf.clone())?;

        let mut expected = String::new();
        expected.write_str("content")?; // first content
        expected.write_str("\\e[0K")?; // clear til EOL
        expected.write_str("\\e[0J")?; // clear til EOF
        expected.write_str("\\e[0G")?; // go far left
        expected.write_str("content + content")?; // second content
        expected.write_str("\\e[0K")?; // clear til EOL
        expected.write_str("\\e[0J")?; // clear til EOF
        expected.write_str("\\e[0G")?; // go far left
        expected.write_str("none")?; // third content
        expected.write_str("\\e[0K")?; // clear til EOL
        expected.write_str("\\e[0J")?; // clear til EOF

        assert_eq!(expected, string);

        body.overwrite(&"", &mut buf, 80)?;

        expected.write_str("\\e[0G")?; // go far left
        expected.write_str("\\e[0K")?; // clear til EOL
        expected.write_str("\\e[0J")?; // clear til EOF

        let string = String::from_utf8(buf.clone())?;

        assert_eq!(expected, string,);

        Ok(())
    }
}