textual 1.0.0-dev

A reactive TUI framework inspired by the Python Textual library
Documentation
use rich_rs::Console;
use textual::css::{default_widget_stylesheet, set_style_context};
use textual::prelude::*;
use textual::render::FrameBuffer;

fn options_for(console: &Console, width: usize, height: usize) -> rich_rs::ConsoleOptions {
    let mut options = console.options().clone();
    options.size = (width, height);
    options.max_width = width;
    options.max_height = height;
    options
}

#[test]
fn rich_log_auto_scrolls_to_latest_lines() {
    let console = Console::new();
    let options = options_for(&console, 16, 2);

    let mut log = RichLog::new();
    log.write("line 1");
    log.write("line 2");
    log.write("line 3");
    log.write("line 4");

    let buf = FrameBuffer::from_renderable(&console, &options, &log, None);
    let lines = buf.as_plain_lines();
    assert!(lines[0].starts_with("line 3"));
    assert!(lines[1].starts_with("line 4"));
}

#[test]
fn rich_log_respects_max_lines() {
    let console = Console::new();
    let options = options_for(&console, 16, 4);

    let mut log = RichLog::new().max_lines(2);
    log.write("line 1");
    log.write("line 2");
    log.write("line 3");

    let buf = FrameBuffer::from_renderable(&console, &options, &log, None);
    let lines = buf.as_plain_lines();
    assert!(lines[0].starts_with("line 2"));
    assert!(lines[1].starts_with("line 3"));
}

#[test]
fn rich_log_scrolls_via_actions() {
    let console = Console::new();
    let options = options_for(&console, 16, 2);

    let mut log = RichLog::new().auto_scroll(false);
    log.write("line 1");
    log.write("line 2");
    log.write("line 3");

    let before = FrameBuffer::from_renderable(&console, &options, &log, None);
    assert!(before.as_plain_lines()[0].starts_with("line 1"));

    log.on_event(&Event::Action(Action::ScrollDown), &mut EventCtx::default());

    let after = FrameBuffer::from_renderable(&console, &options, &log, None);
    assert!(after.as_plain_lines()[0].starts_with("line 2"));
}

#[test]
fn rich_log_preserves_view_anchor_when_trimming_max_lines() {
    let console = Console::new();
    let options = options_for(&console, 16, 2);

    let mut log = RichLog::new().max_lines(3).auto_scroll(false);
    log.write("line 1");
    log.write("line 2");
    log.write("line 3");
    let _ = FrameBuffer::from_renderable(&console, &options, &log, None);
    log.on_event(&Event::Action(Action::ScrollDown), &mut EventCtx::default());

    let before = FrameBuffer::from_renderable(&console, &options, &log, None);
    assert!(before.as_plain_lines()[0].starts_with("line 2"));
    assert!(before.as_plain_lines()[1].starts_with("line 3"));

    log.write("line 4");
    let after = FrameBuffer::from_renderable(&console, &options, &log, None);
    assert!(after.as_plain_lines()[0].starts_with("line 2"));
    assert!(after.as_plain_lines()[1].starts_with("line 3"));
}

#[test]
fn rich_log_renders_all_explicit_lines_from_styled_segments() {
    let console = Console::new();
    let options = options_for(&console, 16, 2);

    let mut log = RichLog::new();
    log.write_segments(vec![rich_rs::Segment::new("line 1\nline 2")]);

    let buf = FrameBuffer::from_renderable(&console, &options, &log, None);
    let lines = buf.as_plain_lines();
    assert!(lines[0].starts_with("line 1"));
    assert!(lines[1].starts_with("line 2"));
}

#[test]
fn rich_log_write_markup_renders_without_literal_markup_tags() {
    let console = Console::new();
    let options = options_for(&console, 24, 1);

    let mut log = RichLog::new();
    log.write_markup("[bold]warn[/] [red]error[/]");

    let buf = FrameBuffer::from_renderable(&console, &options, &log, None);
    let lines = buf.as_plain_lines();
    assert!(lines[0].starts_with("warn error"));
    assert!(!lines[0].contains("[bold]"));
    assert!(!lines[0].contains("[red]"));
}

#[test]
fn rich_log_write_respects_default_markup_setting() {
    let console = Console::new();
    let options = options_for(&console, 24, 1);

    let mut plain = RichLog::new();
    plain.write("[bold]warn[/]");
    let plain_buf = FrameBuffer::from_renderable(&console, &options, &plain, None);
    assert!(plain_buf.as_plain_lines()[0].contains("[bold]warn[/]"));

    let mut markup = RichLog::new().markup(true);
    markup.write("[bold]warn[/]");
    let markup_buf = FrameBuffer::from_renderable(&console, &options, &markup, None);
    assert!(markup_buf.as_plain_lines()[0].starts_with("warn"));
    assert!(!markup_buf.as_plain_lines()[0].contains("[bold]"));
}

#[test]
fn rich_log_write_applies_default_highlighter_when_enabled() {
    let console = Console::new();
    let options = options_for(&console, 32, 1);

    let mut plain = RichLog::new();
    plain.write("None 123");
    let plain_buf = FrameBuffer::from_renderable(&console, &options, &plain, None);

    let mut highlighted = RichLog::new().highlight(true);
    highlighted.write("None 123");
    let highlighted_buf = FrameBuffer::from_renderable(&console, &options, &highlighted, None);

    let plain_styled = (0..32).filter_map(|x| plain_buf.get(x, 0).style).count();
    let highlighted_styled = (0..32)
        .filter_map(|x| highlighted_buf.get(x, 0).style)
        .count();

    assert!(highlighted_styled >= plain_styled);
}

#[test]
fn rich_log_renders_multiline_renderable_entries() {
    let console = Console::new();
    let options = options_for(&console, 16, 2);

    let mut log = RichLog::new();
    log.write_renderable(rich_rs::Text::plain("line 1\nline 2"));

    let buf = FrameBuffer::from_renderable(&console, &options, &log, None);
    let lines = buf.as_plain_lines();
    assert!(lines[0].starts_with("line 1"));
    assert!(lines[1].starts_with("line 2"));
}

#[test]
fn rich_log_auto_scroll_tracks_bottom_after_multiline_renderable_write() {
    let console = Console::new();
    let options = options_for(&console, 16, 2);

    let mut log = RichLog::new();
    log.write("line 1");
    log.write("line 2");
    let _ = FrameBuffer::from_renderable(&console, &options, &log, None);

    log.write_renderable(rich_rs::Text::plain("line 3\nline 4"));

    let buf = FrameBuffer::from_renderable(&console, &options, &log, None);
    let lines = buf.as_plain_lines();
    assert!(lines[0].starts_with("line 3"));
    assert!(lines[1].starts_with("line 4"));
}

#[test]
fn rich_log_focus_style_does_not_draw_border_chrome() {
    let _guard = set_style_context(default_widget_stylesheet());
    let console = Console::new();
    let options = options_for(&console, 12, 2);

    let mut log = RichLog::new();
    log.set_focus(true);
    log.write("line 1");
    log.write("line 2");

    let buf = FrameBuffer::from_renderable(&console, &options, &WidgetRenderable::new(&log), None);
    let lines = buf.as_plain_lines();
    assert!(lines.iter().all(|line| !line.contains('')));
    assert!(lines.iter().all(|line| !line.contains('')));
    assert!(lines[0].starts_with("line 1"));
}