Quick Start
Introduction
tui-lipan is an opinionated, component-based TUI framework for Rust, inspired by React and Elm.
Key characteristics:
- Declarative UI - builder API +
ui!macro (with full autocomplete), plus optionalrsx!. - Component model - properties, local state, and message-based updates.
- Flexbox-like layout - sensible defaults, no raw coordinate math.
- Rich widget set - Frames, Tabs, Lists, Inputs, Tables, Modals, and more.
Import Map
// Recommended: start here for typical app-author code
use tui_lipan::prelude::*;The prelude is intentionally curated for app authors. It re-exports the common component/runtime types, styling primitives, macros, and a broad set of user-facing widgets and widget event types. For framework internals or unusual helpers, prefer explicit imports from tui_lipan.
Representative prelude::* re-exports:
| Symbol | Category |
|---|---|
App, AppRunner, ContrastPolicy, TextAreaNewlineBinding | App runner |
Component, Context, Update, Command, Breakpoint, KeyUpdate, TaskPolicy | Component trait |
Element, IntoElement, Key | Tree primitives |
Callback, CommandLink, KeyHandler, Link | Messaging |
KeyCode, KeyEvent, KeyMods, MouseEvent, MouseMoveEvent | Events |
KeyBinding, KeyBindings | Common keybinding types |
Style, Color, Length, Padding, Align, Justify, BorderStyle, CaretShape | Styling |
RichText, Span, Edge, Rect, Size, ScrollbarConfig, ScrollbarVariant | Layout & text types |
Theme, ColorGradient, GradientDirection, GradientRange, VisualEffect, RetroPreset | Themes & effects |
ClipboardConfig, PasteShiftInsertBehavior | Clipboard config |
TextEditor, TextInput, TextEditEvent, TextEditKind | Text editing |
OverlayId, OverlayScope, ToastHandle, ToastPlacement | Overlays |
App, CommandEntry, CommandRegistry | App commands |
child, mockup!, rsx!, ui! | Macros & helpers |
VStack, HStack, ZStack, Frame, Button, Text, Input, List, Tabs, Table, Modal, TextArea, Tree, DocumentView, FileTree, Animated, AsciiCanvas | Common and advanced widgets |
The prelude no longer re-exports broad internal modules like core, utils, or widgets::* wholesale.
Extra imports not in prelude::*:
// Clipboard image support (requires feature "image" or "clipboard-images")
use tui_lipan::{ImageContent, ImageFormat, ClipboardProvider, ClipboardError};
// Lower-level framework or specialized APIs
use tui_lipan::NodeId;Feature Flags
[dependencies]
tui-lipan = { version = "*", features = ["image", "big-text"] }| Feature | Default | What it enables |
|---|---|---|
clipboard | Yes | System clipboard via arboard (X11/Wayland/macOS/Windows) |
devtools | No | In-app DevTools overlay (F12 by default, rebindable) with frame stats and debug log console; controllable from Context and configurable via DevToolsConfig |
clipboard-images | No | Image clipboard read/write (without Image rendering widget) |
big-text | No | Large ASCII/pixel text via FIGlet and pixel fonts - BigText |
diff-view | No | Side-by-side/unified diff viewer - DiffView |
image | No | Protocol-aware image rendering (Kitty, iTerm2, Sixel, halfblocks) - includes clipboard-images |
markdown | No | Markdown formatter for DocumentView + markdown preview example |
profiling-tracing | No | tracing spans/events around render loop and DocumentView formatting/reconcile hot paths |
syntax-syntect | No | Syntax highlighting in TextArea and DiffView via syntect |
terminal | No | Embedded PTY / terminal viewport - Terminal, ManagedTerminal |
theme-reload | No | Runtime theme file reload for development - see Styling |
web | No | Browser/WASM backend - see Web / WASM Backend |
Profiling with tracing
Enable instrumentation:
tui-lipan = { version = "*", features = ["markdown", "profiling-tracing"] }Then install any standard tracing subscriber in your app binary (for example tracing-subscriber, tracing-tracy, or OpenTelemetry exporters). tui-lipan emits spans/events for frame loop, draw, and DocumentView formatting/reconcile hot paths.
To disable clipboard (e.g. for minimal no-system-dep builds):
tui-lipan = { version = "*", default-features = false }Examples requiring specific features:
| Example | Required feature |
|---|---|
big_text, figlet_editor | big-text |
diff_hub | diff-view |
image, image_modes, messenger | image |
markdown_hub | markdown |
markdown_editor_sync | markdown, syntax-syntect |
terminal_filetree_devtools | terminal |
devtools | devtools |
theme_hot_reload | theme-reload |
With devtools enabled, the built-in panel uses fixed default dimensions per tab; use Context (show_devtools, hide_devtools, toggle_devtools) for visibility.
DevTools runtime configuration
When the devtools feature is enabled, you can opt out of individual subsystems at app start time:
use tui_lipan::prelude::*;
App::new()
.devtools_config(DevToolsConfig {
logs: true, // ingest debug_log! lines into the DevTools log panel
metrics: true, // collect per-frame stats (FPS, reconcile/draw times, node count)
})
.mount(MyApp)
.run()logs: falseremoves thedebug_log!→ devtools sink path entirely (the macro still respectsTUI_LIPAN_DEBUG=1env logging).metrics: falseskips frame timing and tree-size collection; the panel will show "No frame metrics yet".- Both default to
truesofeatures = ["devtools"]behaves exactly as before.
Minimal Example
use tui_lipan::prelude::*;
struct Counter;
#[derive(Default)]
struct State {
count: i32,
}
#[derive(Clone)]
enum Msg {
Increment,
Decrement,
}
impl Component for Counter {
type Message = Msg;
type Properties = ();
type State = State;
fn create_state(&self, _props: &Self::Properties) -> Self::State {
State::default()
}
fn view(&self, ctx: &Context<Self>) -> Element {
rsx! {
VStack {
gap: 1,
padding: 2,
Text { content: format!("Count: {}", ctx.state.count) }
HStack {
gap: 1,
Button { label: "-", on_click: ctx.link().callback(|_| Msg::Decrement) }
Button { label: "+", on_click: ctx.link().callback(|_| Msg::Increment) }
}
}
}
}
fn update(&mut self, msg: Msg, ctx: &mut Context<Self>) -> Update {
match msg {
Msg::Increment => ctx.state.count += 1,
Msg::Decrement => ctx.state.count -= 1,
}
Update::full() // (needs_redraw, optional_command)
}
}
fn main() -> tui_lipan::Result<()> {
App::new()
.title("Counter")
.mount(Counter)
.run()
}Fast Prototyping with mockup!
Skip all Component boilerplate for layout previews:
use tui_lipan::prelude::*;
fn main() -> tui_lipan::Result<()> {
mockup!("Dashboard Preview", {
HStack::new()
.gap(1)
.child(
Frame::new()
.title("Sidebar")
.border(true)
.width(Length::Px(30))
.child(List::new().items([
ListItem::new("Dashboard"),
ListItem::new("Settings"),
ListItem::new("Logs"),
]).selected(0)),
)
.child(
Frame::new()
.title("Content")
.border(true)
.padding(1)
.child(Text::new("Hello from mockup!")),
)
})
}Key behaviors:
- Press
Escorqto quit. - The body expression is auto-wrapped in
.into()- return any widget builder directly. - Interactive widgets (List, Tabs, Inputs) still respond to focus and mouse.
- The closure uses
movecapture, so local data is accessible.
Using Mockup adapter directly:
App::new()
.title("My Layout")
.mount(Mockup::new(|| {
Frame::new().title("Panel").border(true)
.child(Text::new("World")).into() // closure must return Element
}))
.run()Mockup → App Workflow
Extract views as plain functions reusable in both mockups and real components:
fn sidebar(items: &[&str], selected: usize) -> Element {
Frame::new().title("Nav").border(true)
.width(Length::Px(28))
.child(List::new().items(items.iter().map(|s| ListItem::new(*s))).selected(selected))
.into()
}
// Step 1: preview with mockup
fn main() -> tui_lipan::Result<()> {
let items = vec!["Home", "Settings", "Logs"];
mockup!("Preview", { sidebar(&items, 0) })
}
// Step 2: reuse in real component - zero rewrite
fn view(&self, ctx: &Context<Self>) -> Element {
sidebar(&ctx.state.nav_items, ctx.state.selected)
}App Configuration
App::new()
.title("My App") // Optional outer chrome frame
.theme(Theme::one_dark()) // Optional theme override
.inline_ephemeral(8) // Optional: inline mode (8 terminal rows)
.mouse(true) // Mouse capture (default: true in fullscreen, false in inline)
.scroll_wheel_multiplier(3) // Optional: lines per wheel tick (default: 1)
.toast_placement(ToastPlacement::BottomEnd)
.keymap_path("/path/to/keymap.conf") // see docs/keybindings.md
.clipboard_config(ClipboardConfig { .. })
.contrast_policy(ContrastPolicy::Wcag)
.terminal_bg(query_host_colors().map(|c| c.bg)) // enables Opacity through Color::Reset
.mount(Root)
.exit_view(|_component, ctx| {
Text::new(format!("Final count: {}", ctx.state.count)).into()
})
.run()
terminal_bg:ColorTransform::Opacityblends foreground colors toward the resolved cell background. When the cell background isColor::Reset(terminal default) there is no RGB to blend toward, so opacity has no effect. Calling.terminal_bg(query_host_colors().map(|c| c.bg))provides the terminal's actual default background color and enables correct opacity blending.query_host_colors()must be called beforerun()(it briefly enters raw mode internally). Omitting it leaves opacity unchanged on reset-background cells.
exit_view: Attach this onAppRunner<C>after.mount(...)when you want a final one-shot element rendered to stdout after the TUI exits. The callback runs before unmount, so component state is still available inctx.state. This is useful for persisting a session summary or logo in terminal scrollback.
Development Workflow
- Define State -
struct State { ... }with#[derive(Default)] - Define Messages -
enum Msg { ... }with#[derive(Clone)] - Implement Component -
create_state,update,view - Run -
App::new().mount(Root).run()
Debugging
Debug logging
Enable debug output with environment variables:
TUI_LIPAN_DEBUG=1 cargo run # Print to stderr
TUI_LIPAN_DEBUG_FILE=/tmp/tui.log cargo run # Also append to fileUse the debug_log! macro in your own code to emit messages through the same channel:
use tui_lipan::debug_log;
debug_log!("Current state: {:?}", ctx.state);Mouse event diagnostics
The tui_lipan::debug module exposes counters for diagnosing mouse event throughput:
use tui_lipan::debug;
let count = debug::mouse_events_processed(); // Total mouse events since start
debug::reset_mouse_events(); // Reset counter to zero