Skip to content

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 optional rsx!.
  • 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

rust
// 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:

SymbolCategory
App, AppRunner, ContrastPolicy, TextAreaNewlineBindingApp runner
Component, Context, Update, Command, Breakpoint, KeyUpdate, TaskPolicyComponent trait
Element, IntoElement, KeyTree primitives
Callback, CommandLink, KeyHandler, LinkMessaging
KeyCode, KeyEvent, KeyMods, MouseEvent, MouseMoveEventEvents
KeyBinding, KeyBindingsCommon keybinding types
Style, Color, Length, Padding, Align, Justify, BorderStyle, BorderEdges, CaretShapeStyling
RichText, Span, Edge, Rect, Size, ScrollbarConfig, ScrollbarVariantLayout & text types
Theme, ColorGradient, GradientDirection, GradientRange, VisualEffect, RippleRadius, RetroPresetThemes & effects
ClipboardConfig, PasteShiftInsertBehaviorClipboard config
TextEditor, TextInput, TextEditEvent, TextEditKindText editing
OverlayId, OverlayScope, ToastHandle, ToastPlacementOverlays
App, CommandEntry, CommandRegistryApp commands
child, mockup!, rsx!, ui!Macros & helpers
VStack, HStack, ZStack, Canvas, Frame, Button, Text, Input, List, Tabs, Table, Modal, TextArea, Tree, DocumentView, FileTree, Animated, AsciiCanvasCommon and advanced widgets

The prelude no longer re-exports broad internal modules like core, utils, or widgets::* wholesale.

Extra imports not in prelude::*:

rust
// 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

toml
[dependencies]
tui-lipan = { version = "*", features = ["image", "big-text"] }
FeatureDefaultWhat it enables
clipboardYesSystem clipboard via arboard (X11/Wayland/macOS/Windows)
devtoolsNoIn-app DevTools overlay (F12 by default, rebindable) with frame stats and debug log console; controllable from Context and configurable via DevToolsConfig
ui-snapshot-jsonNoJSON export for UiSnapshot::to_json() (markdown export is always available)
ui-snapshot-pngNoFont-backed PNG export for UiSnapshot::to_png() / to_png_default() and CapturedFrame::to_png()
clipboard-imagesNoImage clipboard read/write (without Image rendering widget)
big-textNoLarge ASCII/pixel text via FIGlet and pixel fonts - BigText
diff-viewNoSide-by-side/unified diff viewer - DiffView
imageNoProtocol-aware image rendering (Kitty, iTerm2, Sixel, halfblocks) with PNG/JPEG/GIF/WebP codecs - includes clipboard-images
image-full-formatsNoRestores the broad image crate default codec set for image, clipboard-images, or ui-snapshot-png builds
markdownNoMarkdown formatter for DocumentView + markdown preview example
profiling-tracingNotracing spans/events around render loop and DocumentView formatting/reconcile hot paths
syntax-syntectNoSyntax highlighting in TextArea and DiffView via syntect
terminalNoEmbedded PTY / terminal viewport - Terminal, ManagedTerminal
theme-reloadNoRuntime theme file reload for development - see Styling
webNoBrowser/WASM backend - see Web / WASM Backend

Profiling with tracing

Enable instrumentation:

toml
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):

toml
tui-lipan = { version = "*", default-features = false }

For smaller shipping binaries, build app artifacts with the size-optimized profile:

bash
cargo build --profile release-size --no-default-features

Use the normal release profile when runtime throughput is more important than artifact size.

Examples requiring specific features:

ExampleRequired feature
big_text, figlet_editorbig-text
diff_hubdiff-view
image, image_modes, messengerimage
markdown_hubmarkdown
markdown_editor_syncmarkdown, syntax-syntect
terminal_filetree_devtoolsterminal
devtoolsdevtools
theme_hot_reloadtheme-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:

rust
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)
        show_framework_logs: false, // hide tui-lipan's own internal log lines by default
    })
    .mount(MyApp)
    .run()
  • logs: false removes the debug_log! → devtools sink path entirely (the macro still respects TUI_LIPAN_DEBUG=1 env logging).
  • metrics: false skips frame timing and tree-size collection; the panel will show "No frame metrics yet".
  • show_framework_logs: false starts the Logs tab with tui-lipan's own framework noise (key events, dirty tracking, etc.) hidden, leaving only your app's debug_log! lines. Toggle it live with the tui-lipan button in the Logs tab.
  • logs, metrics, and show_framework_logs all default to true, so features = ["devtools"] behaves exactly as before.

In the Logs tab you can also copy the selected row to the clipboard with Ctrl+C, or by activating a row (double-click / Enter).

Subsystem cost — what each toggle controls:

SubsystemWhen true (default)When to turn off
logsEvery debug_log! allocates a String and pushes a DevLogEntry onto a bounded ring buffer (small).Hot loops calling debug_log! thousands of times per frame, or to drop the formatting cost when the panel is never opened.
metricsPer-frame timing samples + node-tree size snapshot collected on every render; small fixed-size ring buffer.Profiling renders against a release build where you don't want sampling overhead, or shipping a build with the feature compiled in but the panel unused.

Note: debug_log! works in --release builds when the devtools feature is enabled — there is no debug_assertions gate. Lines are emitted to the panel regardless of profile, so guard hot paths yourself if you need zero overhead in release.

Minimal Example

rust
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:

rust
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 Esc or q to 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 move capture, so local data is accessible.

Using Mockup adapter directly:

rust
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:

rust
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

rust
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
    .live_host_terminal_colors(true)  // opt-in runner-managed live host palette refresh
    .mount(Root)
    .exit_view(|_component, ctx| {
        Text::new(format!("Final count: {}", ctx.state.count)).into()
    })
    .run()

ScrollView::scroll_wheel_multiplier(...), TextArea::scroll_wheel_multiplier(...), and DocumentView::scroll_wheel_multiplier(...) override the app-wide wheel multiplier for a specific widget.

terminal_bg: ColorTransform::Opacity blends foreground colors toward the resolved cell background. When the cell background is Color::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)) before run() provides the terminal's actual default background color and enables correct opacity blending for static apps. For live system themes, use .live_host_terminal_colors(true): the runner probes once at startup, refreshes on terminal focus gained, and also services ctx.request_host_terminal_color_refresh(). It never polls continuously. Refreshed host backgrounds update terminal_bg automatically. Omitting both leaves opacity unchanged on reset-background cells.

exit_view: Attach this on AppRunner<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 in ctx.state. This is useful for persisting a session summary or logo in terminal scrollback.

Development Workflow

  1. Define State - struct State { ... } with #[derive(Default)]
  2. Define Messages - enum Msg { ... } with #[derive(Clone)]
  3. Implement Component - create_state, update, view
  4. Run - App::new().mount(Root).run()

Debugging

Debug logging

Enable debug output with environment variables:

sh
TUI_LIPAN_DEBUG=1 cargo run                         # Print to stderr
TUI_LIPAN_DEBUG_FILE=/tmp/tui.log cargo run          # Also append to file

Use the debug_log! macro in your own code to emit messages through the same channel:

rust
use tui_lipan::debug_log;

debug_log!("Current state: {:?}", ctx.state);

Layout snapshot diagnostics

When content vanishes in tests or mockups, first check the viewport and sizing:

  • TestBackend starts at an 80x24 viewport unless you call set_viewport(...); use a fixed viewport for reproducible snapshots.
  • Mockup renders at the live terminal size, so a layout can change when the terminal is narrow or short.
  • VStack, HStack, and Frame default to Length::Flex(1) on both axes; fixed headers, footers, and side bars usually need Length::Px(...).
  • Capture with UiSnapshotOptions::diagnostic() to include zero-area nodes, spacers, and dividers; markdown snapshots flag zero-size widgets as zero-area.

Mouse event diagnostics

The tui_lipan::debug module exposes counters for diagnosing mouse event throughput:

rust
use tui_lipan::debug;

let count = debug::mouse_events_processed();  // Total mouse events since start
debug::reset_mouse_events();                  // Reset counter to zero

MIT OR Apache-2.0