avt 0.18.0

asciinema virtual terminal
Documentation
use crate::line::Line;
use crate::vt::Vt;
use std::mem;

#[derive(Default)]
pub struct TextUnwrapper {
    wrapped_line: String,
}

impl TextUnwrapper {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn push(&mut self, line: &Line) -> Option<String> {
        if line.wrapped {
            self.wrapped_line.push_str(&line.text());

            None
        } else {
            self.wrapped_line.push_str(line.text().trim_end());

            Some(mem::take(&mut self.wrapped_line))
        }
    }

    pub fn flush(self) -> Option<String> {
        if self.wrapped_line.is_empty() {
            None
        } else {
            Some(self.wrapped_line)
        }
    }
}

pub struct TextCollector {
    vt: Vt,
    unwrapper: TextUnwrapper,
}

impl TextCollector {
    pub fn new(vt: Vt) -> Self {
        Self {
            vt,
            unwrapper: TextUnwrapper::new(),
        }
    }

    pub fn feed_str(&mut self, s: &str) -> impl Iterator<Item = String> + '_ {
        self.vt
            .feed_str(s)
            .scrollback
            .filter_map(|l| self.unwrapper.push(&l))
    }

    pub fn resize(&mut self, cols: u16, rows: u16) -> impl Iterator<Item = String> + '_ {
        self.vt
            .resize(cols.into(), rows.into())
            .scrollback
            .filter_map(|l| self.unwrapper.push(&l))
    }

    pub fn flush(self) -> Vec<String> {
        let mut unwrapper = self.unwrapper;
        let mut lines: Vec<String> = self.vt.lines().filter_map(|l| unwrapper.push(l)).collect();
        lines.extend(unwrapper.flush());

        while !lines.is_empty() && lines[lines.len() - 1].is_empty() {
            lines.truncate(lines.len() - 1);
        }

        lines
    }
}

#[cfg(test)]
mod tests {
    use super::TextUnwrapper;
    use crate::{util::TextCollector, Line, Pen, Vt};

    #[test]
    fn text_unwrapper() {
        let mut tu = TextUnwrapper::new();
        let pen = Pen::default();

        let mut line = Line::blank(5, pen);
        line.print(0, 'a', pen);
        line.print(4, 'b', pen);
        line.wrapped = false;

        let text = tu.push(&line);

        assert!(matches!(text, Some(ref x) if x == "a   b"));

        let mut line = Line::blank(5, pen);
        line.print(0, 'c', pen);
        line.print(4, 'd', pen);
        line.wrapped = true;

        let text = tu.push(&line);

        assert!(text.is_none());

        let mut line = Line::blank(5, pen);
        line.print(0, 'e', pen);
        line.print(4, 'f', pen);
        line.wrapped = true;

        let text = tu.push(&line);

        assert!(text.is_none());

        let mut line = Line::blank(5, pen);
        line.print(0, 'g', pen);
        line.print(1, 'h', pen);
        line.wrapped = false;

        let text = tu.push(&line);

        assert!(matches!(text, Some(ref x) if x == "c   de   fgh"));

        let mut line = Line::blank(5, pen);
        line.print(0, 'i', pen);
        line.wrapped = true;

        let text = tu.push(&line);

        assert!(text.is_none());

        let text = tu.flush();

        assert!(matches!(text, Some(ref x) if x == "i    "));
    }

    #[test]
    fn text_collector_no_scrollback() {
        let vt = Vt::builder().size(10, 2).scrollback_limit(0).build();
        let mut tc = TextCollector::new(vt);

        let lines: Vec<String> = tc.feed_str("a\r\nb\r\nc\r\nd\r\n").collect();

        assert_eq!(lines, ["a", "b", "c"]);

        let lines: Vec<String> = tc.flush();

        assert_eq!(lines, ["d"]);
    }

    #[test]
    fn text_collector_unlimited_scrollback() {
        let vt = Vt::builder().size(10, 2).build();
        let mut tc = TextCollector::new(vt);

        let lines: Vec<String> = tc.feed_str("a\r\nb\r\nc\r\nd\r\n").collect();

        assert!(lines.is_empty());

        let lines: Vec<String> = tc.flush();

        assert_eq!(lines, ["a", "b", "c", "d"]);
    }

    #[test]
    fn text_collector_wrapping() {
        let vt = Vt::builder().size(10, 2).scrollback_limit(0).build();
        let mut tc = TextCollector::new(vt);

        let lines: Vec<String> = tc.feed_str("abcdefghijklmno\r\n").collect();

        assert!(lines.is_empty());

        let lines: Vec<String> = tc.flush();

        assert_eq!(lines, vec!["abcdefghijklmno"]);
    }
}