# 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.
```rust
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 color** — `Color::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 API** — `cx.set_timer(ms)`, `cx.set_interval(ms)`, `cx.cancel_timer(id)` for one-shot and periodic dispatch
- **Cancellable workers** — `cx.spawn_worker()` returns `WorkerId` for tracking and cancellation
- **Headless testing** — `Pilot` driver with event injection, buffer inspection, `press()`, `run_until()`
- **Debug view** — press `d` to visualize component borders and labels
## Widgets
| `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:
```rust
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]`:
```rust
#[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
```rust
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
```css
Button:focus { fg: white; bg: blue; }
Button:hover { bg: gray; }
Input:disabled { fg: gray; }
```
```rust
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
```rust
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
```rust
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
// Run until condition
let done = pilot.run_until(100, |buf| {
buf.cells.iter().any(|c| c.symbol == "X")
}).unwrap();
```
## Getting Started
```toml
[dependencies]
lv-tui = "0.3"
```
```bash
cargo run --example counter
cargo run --example demo_app
```
## License
MIT