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, CaretShapeStyling
RichText, Span, Edge, Rect, Size, ScrollbarConfig, ScrollbarVariantLayout & text types
Theme, ColorGradient, GradientDirection, GradientRange, VisualEffect, 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, 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
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) - includes clipboard-images
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 }

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)
    })
    .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".
  • Both default to true so features = ["devtools"] behaves exactly as before.

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
    .mount(Root)
    .exit_view(|_component, ctx| {
        Text::new(format!("Final count: {}", ctx.state.count)).into()
    })
    .run()

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)) provides the terminal's actual default background color and enables correct opacity blending. query_host_colors() must be called before run() (it briefly enters raw mode internally). Omitting it 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);

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