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 log_write_auto_scrolls_to_latest_lines() {
    let console = Console::new();
    let options = options_for(&console, 16, 2);

    let mut log = Log::new();
    log.write("line 1\n");
    log.write("line 2\n");
    log.write("line 3\n");

    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 log_respects_max_lines() {
    let console = Console::new();
    let options = options_for(&console, 16, 2);

    let mut log = Log::new().max_lines(2);
    log.write_line("line 1");
    log.write_line("line 2");
    log.write_line("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 log_write_appends_until_newline() {
    let mut log = Log::new();
    log.write("hel");
    log.write("lo");
    assert_eq!(log.line_count(), 1);
    assert_eq!(log.lines(), ["hello"]);

    log.write("\nnext");
    assert_eq!(log.line_count(), 2);
    assert_eq!(log.lines(), ["hello", "next"]);
}

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

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

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

    let mut ctx = EventCtx::default();
    log.on_event(&Event::Action(Action::ScrollDown), &mut ctx);
    assert!(ctx.handled());

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

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

    let mut log = Log::new().auto_scroll(false).max_lines(3);
    log.write_lines(["line 1", "line 2", "line 3"]);
    let _ = FrameBuffer::from_renderable(&console, &options, &log, None);

    let mut scroll_ctx = EventCtx::default();
    log.on_event(&Event::Action(Action::ScrollDown), &mut scroll_ctx);
    assert!(scroll_ctx.handled());

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

    log.write_line("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 log_default_css_sets_surface_background() {
    let _guard = set_style_context(default_widget_stylesheet());
    let console = Console::new();
    let options = options_for(&console, 12, 2);

    let mut log = Log::new();
    log.write_line("line");

    let buf = FrameBuffer::from_renderable(&console, &options, &WidgetRenderable::new(&log), None);
    let style = buf
        .get(0, 0)
        .style
        .expect("expected style metadata on rendered log cell");
    assert!(style.bgcolor.is_some());
}