mural 0.1.0

Conversational terminal rendering for command-line applications.
Documentation
use mural::{
    Color, Line, Modifiers, Size, Span, Style, Terminal, Text, TextError,
    backend::fake::{FakeBackend, Operation},
};

#[test]
fn style_supports_colors_and_text_modifiers() {
    let style = Style::new()
        .fg(Color::Red)
        .bg(Color::Rgb(1, 2, 3))
        .bold()
        .dim()
        .italic()
        .underline()
        .reversed();

    assert_eq!(style.foreground(), Some(Color::Red));
    assert_eq!(style.background(), Some(Color::Rgb(1, 2, 3)));
    assert!(style.modifiers().contains(Modifiers::BOLD));
    assert!(style.modifiers().contains(Modifiers::DIM));
    assert!(style.modifiers().contains(Modifiers::ITALIC));
    assert!(style.modifiers().contains(Modifiers::UNDERLINE));
    assert!(style.modifiers().contains(Modifiers::REVERSED));

    let overlaid = Style::new()
        .fg(Color::Blue)
        .modifier(Modifiers::ITALIC)
        .overlay(Style::new().bg(Color::Yellow).reversed());
    assert_eq!(overlaid.foreground(), Some(Color::Blue));
    assert_eq!(overlaid.background(), Some(Color::Yellow));
    assert!(overlaid.modifiers().contains(Modifiers::ITALIC));
    assert!(overlaid.modifiers().contains(Modifiers::REVERSED));
}

#[test]
fn line_and_span_constructors_reject_structural_content() {
    assert!(Span::new("hello", Style::new()).is_ok());
    assert!(Line::from_plain("hello").is_ok());

    assert_eq!(
        Span::new("hello\nworld", Style::new()).unwrap_err(),
        TextError::StructuralContent
    );
    assert_eq!(
        Span::new("hello\tworld", Style::new()).unwrap_err(),
        TextError::StructuralContent
    );
    assert_eq!(
        Line::from_plain("hello\nworld").unwrap_err(),
        TextError::StructuralContent
    );
}

#[test]
fn text_represents_multiple_and_empty_lines_and_is_renderable() {
    let text = Text::from_plain("hello\n\nworld").unwrap();

    assert_eq!(text.lines().len(), 3);
    assert_eq!(text.lines()[0].plain_content(), "hello");
    assert!(text.lines()[1].spans().is_empty());
    assert_eq!(text.lines()[2].plain_content(), "world");
    assert!(Text::empty().lines().is_empty());

    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal.push_live(text).unwrap();

    terminal.render().unwrap();

    assert_eq!(
        terminal.backend().operations(),
        &[
            Operation::QuerySize,
            Operation::HideCursor,
            Operation::Print(Line::from_plain("hello").unwrap()),
            Operation::Newline,
            Operation::Print(Line::from_plain("").unwrap()),
            Operation::Newline,
            Operation::Print(Line::from_plain("world").unwrap()),
            Operation::Flush,
        ]
    );
}

#[test]
fn backend_observes_style_only_differences() {
    let plain_line = Line::from_spans(vec![Span::new("same", Style::new()).unwrap()]);
    let styled_line = Line::from_spans(vec![
        Span::new("same", Style::new().fg(Color::Green).underline()).unwrap(),
    ]);
    let text = Text::from_lines(vec![plain_line.clone(), styled_line.clone()]);

    let mut terminal = Terminal::new(FakeBackend::new(Size::new(80, 24))).unwrap();
    terminal.push_live(text).unwrap();

    terminal.render().unwrap();

    assert_ne!(plain_line, styled_line);
    assert_eq!(
        terminal.backend().operations(),
        &[
            Operation::QuerySize,
            Operation::HideCursor,
            Operation::Print(plain_line),
            Operation::Newline,
            Operation::Print(styled_line),
            Operation::Flush,
        ]
    );
}

#[test]
fn textwrap_wrapping_preserves_span_styles() {
    let red = Style::new().fg(Color::Red);
    let blue = Style::new().fg(Color::Blue);
    let text = Text::from_lines(vec![Line::from_spans(vec![
        Span::new("hello", red).unwrap(),
        Span::new("world", blue).unwrap(),
    ])]);

    let mut terminal = Terminal::new(FakeBackend::new(Size::new(6, 24))).unwrap();
    terminal.push_live(text).unwrap();

    terminal.render().unwrap();

    assert_eq!(
        terminal.backend().operations(),
        &[
            Operation::QuerySize,
            Operation::HideCursor,
            Operation::Print(Line::from_spans(vec![Span::new("hello", red).unwrap()])),
            Operation::Newline,
            Operation::Print(Line::from_spans(vec![Span::new("world", blue).unwrap()])),
            Operation::Flush,
        ]
    );
}

#[test]
fn textwrap_wrapping_splits_a_span_without_losing_its_style() {
    let red = Style::new().fg(Color::Red).bold();
    let text = Text::from_lines(vec![Line::from_spans(vec![
        Span::new("helloworld", red).unwrap(),
    ])]);

    let mut terminal = Terminal::new(FakeBackend::new(Size::new(6, 24))).unwrap();
    terminal.push_live(text).unwrap();

    terminal.render().unwrap();

    assert_eq!(
        terminal.backend().operations(),
        &[
            Operation::QuerySize,
            Operation::HideCursor,
            Operation::Print(Line::from_spans(vec![Span::new("hello", red).unwrap()])),
            Operation::Newline,
            Operation::Print(Line::from_spans(vec![Span::new("world", red).unwrap()])),
            Operation::Flush,
        ]
    );
}