lv-tui-code-highlight 0.4.0

Syntax highlighting for the lv-tui framework
Documentation

lv-tui

A reactive TUI framework for Rust. Component tree, reactive state, CSS-like styling, event bubbling, focus management — everything you need to build complex terminal applications.

use lv_tui::prelude::*;
use lv_tui::Component;

#[derive(Component)]
struct Counter {
    #[reactive(paint, copy)]
    count: i32,
}

impl Counter {
    fn new() -> Self { Self { count: 0 } }
}

#[lv_tui::event_handlers]
impl Counter {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(format!("count: {}", self.get_count()));
        cx.line("press + to increment, q to quit");
    }

    fn on_key_plus(&mut self, cx: &mut EventCx) {
        self.set_count(self.get_count() + 1, cx);
    }

    fn on_key_q(&mut self, cx: &mut EventCx) {
        cx.quit();
    }
}

fn main() -> lv_tui::Result<()> {
    App::new(Counter::new()).run()
}

Features

  • Component tree — compose UIs with Column, Row, Stack, Block, Scroll, Overlay
  • Reactive state#[reactive(paint)] auto-triggers repaint on change
  • Declarative events#[event_handlers] macro with on_focus, on_blur, on_tick, on_key_* naming convention
  • Event bubbling — Capture → Target → Bubble with stop_propagation()
  • Focus management — Tab/Shift+Tab navigation with Focus/Blur events
  • CSS-like stylesheets — type/class/id selectors with pseudo-classes (:focus, :hover, :disabled, :focus-within) and style inheritance
  • 24-bit colorColor::Rgb(r,g,b), Color::Indexed(i), Color::hex("#ff8800"), plus "text".rgb(255,0,0) via Stylize
  • Unicode — CJK/Emoji wide characters, text wrap, truncation, alignment
  • Timer APIcx.set_timer(ms), cx.set_interval(ms), cx.cancel_timer(id) for one-shot and periodic dispatch
  • Cancellable workerscx.spawn_worker() returns WorkerId for tracking and cancellation
  • Headless testingPilot driver with event injection, buffer inspection, press(), run_until()
  • Debug view — press d to visualize component borders and labels

Widgets

Widget Description
Label Text display with styling
Input Single-line text input with cursor
TextArea Multi-line editor with undo/redo, line numbers, word navigation
Column Vertical layout container
Row Horizontal layout container
Stack Layered/z-order container
Block Border + padding wrapper with optional title
Scroll Scrollable content container
Overlay Modal dialog with background dimming
Table Data table with headers, row/cell selection, fixed rows, sorting
Tabs Tabbed container with keyboard navigation
Select Dropdown with keyboard navigation
Checkbox Toggleable boolean with label
RadioGroup Mutually exclusive radio selection
ProgressBar Unicode 8-segment progress bar
Dialog Border + key bindings (Esc/Enter) wrapper
SplitPane Resizable split panels (Ctrl+arrows)
VirtualList Virtual-scrolling list for large datasets
Spinner Animated loading indicator
DiffView Unified diff display for text comparisons
Tree Hierarchical tree with expand/collapse, guide lines
MarkdownView Markdown renderer (in lv-tui-markdown crate)

Text System

All widgets accept impl Into<Text>, giving you three levels of control:

use lv_tui::prelude::*;

// Simple: plain text
Label::new("hello")

// Styled: use Stylize trait — named colors, 24-bit, or hex
Label::new(Line::from(vec![
    "Error: ".red().bold(),
    Span::new("not found"),
]))
Label::new(Line::from("Success".rgb(0, 255, 0)))

// Multi-line: build Text from Lines
Label::new(Text::from(vec![
    Line::from(Span::new("Title").bold()),
    Line::from(Span::new("Body")),
]))

Declarative Events

Replace manual fn event() with #[event_handlers]:

#[lv_tui::event_handlers]
impl MyWidget {
    fn render(&self, cx: &mut RenderCx) { ... }

    fn on_focus(&mut self, cx: &mut EventCx) { ... }
    fn on_blur(&mut self, cx: &mut EventCx) { ... }
    fn on_key_q(&mut self, cx: &mut EventCx) { cx.quit(); }
    fn on_key_tab(&mut self, cx: &mut EventCx) { ... }
}

Handlers follow naming convention: on_focus, on_blur, on_tick, on_key_<char> (e.g. on_key_q), on_key_<name> (e.g. on_key_tab, on_key_enter, on_key_space).

24-bit Color

use lv_tui::prelude::*;

// Construct colors
let c = Color::Rgb(255, 128, 0);
let c = Color::hex("#ff6600").unwrap();
let c = Color::Indexed(196);  // 256-color palette

// Stylize chainable methods
let span = "hello".rgb(255, 0, 0).bold();
let span = "world".hex("#00ff00").underline();
let span = "text".on_rgb(30, 30, 30).white();

CSS Pseudo-classes

Button:focus { fg: white; bg: blue; }
Button:hover { bg: gray; }
Input:disabled { fg: gray; }
use lv_tui::style_parser::{StyleSheet, WidgetState};

let sheet = StyleSheet::parse("Button:focus { fg: blue; }").unwrap();
let state = WidgetState { focused: true, ..Default::default() };
let style = sheet.resolve("Button", None, None, &state);

Timer API

fn mount(&mut self, cx: &mut EventCx) {
    self.spinner_timer = Some(cx.set_interval(80));  // every 80ms
    self.toast_timer = Some(cx.set_timer(3000));     // one-shot 3s
}

fn event(&mut self, event: &Event, cx: &mut EventCx) {
    if let Event::Timer(id) = event {
        if Some(*id) == self.spinner_timer { self.advance_frame(cx); }
        if Some(*id) == self.toast_timer { self.dismiss_toast(cx); }
    }
}

Testing

use lv_tui::Pilot;

let mut pilot = Pilot::new(MyWidget::new(), 80, 24);
pilot.focus_first();
pilot.press(Key::Char(' ')).unwrap();   // inject key
pilot.press(Key::Tab).unwrap();

// Assert rendered output
assert!(pilot.frame().cells.iter().any(|c| c.symbol == ""));

// Run until condition
let done = pilot.run_until(100, |buf| {
    buf.cells.iter().any(|c| c.symbol == "X")
}).unwrap();

Getting Started

[dependencies]
lv-tui = "0.3"
cargo run --example counter
cargo run --example demo_app

License

MIT