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 WidthEcho;

impl Render for WidthEcho {
    fn render(&self, width: u16) -> Text {
        Text::from_plain(format!("width={width}")).unwrap()
    }
}

#[derive(Debug)]
struct CountingBlock {
    content: String,
    render_count: Rc<Cell<usize>>,
}

impl CountingBlock {
    fn new(content: impl Into<String>, render_count: Rc<Cell<usize>>) -> Self {
        Self {
            content: content.into(),
            render_count,
        }
    }
}

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

struct WidthRecorder {
    seen_width: Rc<Cell<u16>>,
}

impl Render for WidthRecorder {
    fn render(&self, width: u16) -> Text {
        self.seen_width.set(width);
        Text::empty()
    }
}

#[test]
fn resize_defers_terminal_io_and_uses_new_safe_printable_width_on_render() {
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal.push_live(WidthEcho).unwrap();

    terminal.resize(Size::new(9, 24)).unwrap();

    assert_eq!(
        terminal.backend().operations(),
        &[Operation::QuerySize, Operation::HideCursor]
    );

    terminal.render().unwrap();

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

#[test]
fn width_change_rerenders_clean_blocks_and_forces_full_rewrite() {
    let render_count = Rc::new(Cell::new(0));
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .push_live(CountingBlock::new("unchanged", Rc::clone(&render_count)))
        .unwrap();
    terminal.render().unwrap();
    let after_first_render = terminal.backend().operations().len();

    terminal.resize(Size::new(10, 24)).unwrap();
    terminal.render().unwrap();

    assert_eq!(render_count.get(), 2);
    assert_eq!(
        &terminal.backend().operations()[after_first_render..],
        &[
            Operation::MoveToOrigin,
            Operation::Clear,
            Operation::PurgeScrollback,
            Operation::Print(Line::from_plain("unchanged").unwrap()),
            Operation::Flush,
        ]
    );
}

#[test]
fn height_only_change_full_rewrites_without_rerendering_clean_blocks() {
    let render_count = Rc::new(Cell::new(0));
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .push_live(CountingBlock::new("cached", Rc::clone(&render_count)))
        .unwrap();
    terminal.render().unwrap();
    let after_first_render = terminal.backend().operations().len();

    terminal.resize(Size::new(80, 10)).unwrap();
    terminal.render().unwrap();

    assert_eq!(render_count.get(), 1);
    assert_eq!(
        &terminal.backend().operations()[after_first_render..],
        &[
            Operation::MoveToOrigin,
            Operation::Clear,
            Operation::PurgeScrollback,
            Operation::Print(Line::from_plain("cached").unwrap()),
            Operation::Flush,
        ]
    );
}

#[test]
fn zero_width_resize_uses_zero_safe_printable_width() {
    let seen_width = Rc::new(Cell::new(u16::MAX));
    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal
        .push_live(WidthRecorder {
            seen_width: Rc::clone(&seen_width),
        })
        .unwrap();

    terminal.resize(Size::new(0, 24)).unwrap();
    terminal.render().unwrap();

    assert_eq!(seen_width.get(), 0);
    assert_eq!(
        terminal.backend().operations(),
        &[
            Operation::QuerySize,
            Operation::HideCursor,
            Operation::MoveToOrigin,
            Operation::Clear,
            Operation::PurgeScrollback,
            Operation::Flush,
        ]
    );
}