mural 0.1.0

Conversational terminal rendering for command-line applications.
Documentation
use std::{cell::Cell, rc::Rc};

use mural::{
    Line, Render, Size, Terminal, Text,
    backend::fake::{FakeBackend, Operation},
};

struct AlwaysRenderBlock {
    content: &'static str,
    render_count: Rc<Cell<usize>>,
}

impl AlwaysRenderBlock {
    fn new(content: &'static str, render_count: Rc<Cell<usize>>) -> Self {
        Self {
            content,
            render_count,
        }
    }
}

impl Render for AlwaysRenderBlock {
    fn render(&self, _width: u16) -> Text {
        self.render_count.set(self.render_count.get() + 1);
        Text::from_plain(self.content).unwrap()
    }

    fn render_every_frame(&self) -> bool {
        true
    }
}

#[test]
fn always_render_blocks_rerender_on_clean_frames_without_unnecessary_terminal_output() {
    let render_count = Rc::new(Cell::new(0));
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .push_live(AlwaysRenderBlock::new("same", Rc::clone(&render_count)))
        .unwrap();

    terminal.render().unwrap();
    let after_first_render = terminal.backend().operations().len();
    terminal.render().unwrap();

    assert_eq!(render_count.get(), 2);
    assert_eq!(
        &terminal.backend().operations()[after_first_render..],
        &[Operation::Flush]
    );
    assert_eq!(
        terminal.backend().operations(),
        &[
            Operation::QuerySize,
            Operation::HideCursor,
            Operation::Print(Line::from_plain("same").unwrap()),
            Operation::Flush,
            Operation::Flush,
        ]
    );
}

#[test]
fn render_every_frame_is_a_dynamic_hint() {
    struct DynamicRenderBlock {
        render_count: Rc<Cell<usize>>,
        render_every_frame: Rc<Cell<bool>>,
    }

    impl Render for DynamicRenderBlock {
        fn render(&self, _width: u16) -> Text {
            self.render_count.set(self.render_count.get() + 1);
            Text::from_plain("dynamic").unwrap()
        }

        fn render_every_frame(&self) -> bool {
            self.render_every_frame.get()
        }
    }

    let render_count = Rc::new(Cell::new(0));
    let render_every_frame = Rc::new(Cell::new(true));
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .push_live(DynamicRenderBlock {
            render_count: Rc::clone(&render_count),
            render_every_frame: Rc::clone(&render_every_frame),
        })
        .unwrap();

    terminal.render().unwrap();
    terminal.render().unwrap();
    render_every_frame.set(false);
    terminal.render().unwrap();

    assert_eq!(render_count.get(), 2);
}

#[test]
fn always_render_blocks_report_effective_dirtiness_after_successful_render() {
    let render_count = Rc::new(Cell::new(0));
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .insert_live(
            "clock",
            AlwaysRenderBlock::new("tick", Rc::clone(&render_count)),
        )
        .unwrap();

    assert_eq!(terminal.is_block_dirty("clock"), Ok(true));
    terminal.render().unwrap();

    assert_eq!(terminal.is_block_dirty("clock"), Ok(true));
}

#[test]
fn finish_rerenders_live_always_render_blocks_and_skips_pinned_blocks() {
    let live_render_count = Rc::new(Cell::new(0));
    let pinned_render_count = Rc::new(Cell::new(0));
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .push_live(AlwaysRenderBlock::new(
            "transcript",
            Rc::clone(&live_render_count),
        ))
        .unwrap();
    terminal
        .push_pinned(AlwaysRenderBlock::new(
            "status",
            Rc::clone(&pinned_render_count),
        ))
        .unwrap();

    terminal.render().unwrap();
    terminal.finish().unwrap();

    assert_eq!(live_render_count.get(), 2);
    assert_eq!(pinned_render_count.get(), 1);
}