mural 0.1.0

Conversational terminal rendering for command-line applications.
Documentation
use mural::{
    Backend, Line, Size, Terminal, TerminalError, Text,
    backend::fake::{FakeBackend, Operation},
};
use std::{cell::RefCell, io, rc::Rc};

#[derive(Clone, Debug)]
struct SharedLogBackend {
    size: Size,
    operations: Rc<RefCell<Vec<Operation>>>,
}

impl SharedLogBackend {
    fn new(size: Size, operations: Rc<RefCell<Vec<Operation>>>) -> Self {
        Self { size, operations }
    }

    fn record(&mut self, operation: Operation) {
        self.operations.borrow_mut().push(operation);
    }
}

impl Backend for SharedLogBackend {
    fn size(&mut self) -> io::Result<Size> {
        self.record(Operation::QuerySize);
        Ok(self.size)
    }

    fn hide_cursor(&mut self) -> io::Result<()> {
        self.record(Operation::HideCursor);
        Ok(())
    }

    fn show_cursor(&mut self) -> io::Result<()> {
        self.record(Operation::ShowCursor);
        Ok(())
    }

    fn move_to_origin(&mut self) -> io::Result<()> {
        self.record(Operation::MoveToOrigin);
        Ok(())
    }

    fn move_up(&mut self, lines: u16) -> io::Result<()> {
        self.record(Operation::MoveUp(lines));
        Ok(())
    }

    fn move_to_column(&mut self, column: u16) -> io::Result<()> {
        self.record(Operation::MoveToColumn(column));
        Ok(())
    }

    fn print(&mut self, line: &Line) -> io::Result<()> {
        self.record(Operation::Print(line.clone()));
        Ok(())
    }

    fn newline(&mut self) -> io::Result<()> {
        self.record(Operation::Newline);
        Ok(())
    }

    fn clear(&mut self) -> io::Result<()> {
        self.record(Operation::Clear);
        Ok(())
    }

    fn purge_scrollback(&mut self) -> io::Result<()> {
        self.record(Operation::PurgeScrollback);
        Ok(())
    }

    fn clear_from_cursor_down(&mut self) -> io::Result<()> {
        self.record(Operation::ClearFromCursorDown);
        Ok(())
    }

    fn flush(&mut self) -> io::Result<()> {
        self.record(Operation::Flush);
        Ok(())
    }
}

#[test]
fn backend_mut_exposes_escape_hatch_and_forced_redraw_recovers_renderer_state() {
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .push_live(Text::from_plain("managed").unwrap())
        .unwrap();

    terminal
        .backend_mut()
        .print(&Line::from_plain("outside renderer").unwrap())
        .unwrap();
    terminal.force_full_redraw().unwrap();
    terminal.render().unwrap();

    assert_eq!(
        terminal.backend().operations(),
        &[
            Operation::QuerySize,
            Operation::HideCursor,
            Operation::Print(Line::from_plain("outside renderer").unwrap()),
            Operation::MoveToOrigin,
            Operation::Clear,
            Operation::PurgeScrollback,
            Operation::Print(Line::from_plain("managed").unwrap()),
            Operation::Flush,
        ]
    );
}

#[test]
fn render_and_normal_mutations_report_lifecycle_errors_after_finish() {
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .insert_live("live", Text::from_plain("live").unwrap())
        .unwrap();
    terminal
        .insert_pinned("pinned", Text::from_plain("pinned").unwrap())
        .unwrap();

    terminal.finish().unwrap();

    assert_eq!(
        terminal.render().unwrap_err().kind(),
        io::ErrorKind::BrokenPipe
    );
    assert_eq!(
        terminal.force_full_redraw().unwrap_err().kind(),
        io::ErrorKind::BrokenPipe
    );
    assert_eq!(
        terminal.resize(Size::new(40, 10)).unwrap_err().kind(),
        io::ErrorKind::BrokenPipe
    );
    assert_eq!(
        terminal
            .push_live(Text::from_plain("late live").unwrap())
            .unwrap_err()
            .kind(),
        io::ErrorKind::BrokenPipe
    );
    assert_eq!(
        terminal
            .push_pinned(Text::from_plain("late pinned").unwrap())
            .unwrap_err()
            .kind(),
        io::ErrorKind::BrokenPipe
    );
    assert_eq!(
        terminal.insert_live("late", Text::from_plain("late").unwrap()),
        Err(TerminalError::Finished)
    );
    assert_eq!(
        terminal.insert_pinned("late", Text::from_plain("late").unwrap()),
        Err(TerminalError::Finished)
    );
    assert_eq!(
        terminal.live_block_mut::<Text>("live"),
        Err(TerminalError::Finished)
    );
    assert_eq!(
        terminal.pinned_block_mut::<Text>("pinned"),
        Err(TerminalError::Finished)
    );
    assert_eq!(terminal.remove_live("live"), Err(TerminalError::Finished));
    assert_eq!(
        terminal.remove_pinned("pinned"),
        Err(TerminalError::Finished)
    );
}

#[test]
fn drop_restores_cursor_and_flushes_without_rendering_blocks() {
    let operations = Rc::new(RefCell::new(Vec::new()));

    {
        let mut terminal = Terminal::new(SharedLogBackend::new(
            Size::new(80, 24),
            Rc::clone(&operations),
        ))
        .unwrap();
        terminal
            .push_live(Text::from_plain("unrendered").unwrap())
            .unwrap();
    }

    assert_eq!(
        operations.borrow().as_slice(),
        &[
            Operation::QuerySize,
            Operation::HideCursor,
            Operation::ShowCursor,
            Operation::Flush
        ]
    );
}