Skip to main content

Crate tui

Crate tui 

Source
Expand description

§tui

A lightweight, composable terminal UI library for building full-screen CLI apps in Rust.

Your app owns its event loop and state machine. The library provides composable building blocks: a Component trait for widgets, a diff-based Renderer, and RAII terminal management.

§Table of Contents

§Minimal app

A complete TUI app has three parts: a TerminalRuntime (terminal lifecycle), a Component (your UI), and a loop that wires them together.

use std::io;
use tui::{
    Component, CrosstermEvent, Event, Frame, KeyCode, Line,
    MouseCapture, TerminalConfig, TerminalRuntime, Theme, ViewContext,
    terminal_size,
};

// 1. Define your root component
struct Counter { count: i32 }

impl Component for Counter {
    type Message = CounterMsg;
    async fn on_event(&mut self, event: &Event) -> Option<Vec<CounterMsg>> {
        if let Event::Key(key) = event {
            match key.code {
                KeyCode::Up    => self.count += 1,
                KeyCode::Down  => self.count -= 1,
                KeyCode::Char('q') => return Some(vec![CounterMsg::Quit]),
                _ => return None,
            }
            return Some(vec![]);
        }
        None
    }
    fn render(&mut self, ctx: &ViewContext) -> Frame {
        Frame::new(vec![
            Line::styled("Counter (↑/↓, q to quit)", ctx.theme.muted()),
            Line::new(format!("  {}", self.count)),
        ])
    }
}

enum CounterMsg { Quit }

// 2. Set up the terminal runtime
#[tokio::main]
async fn main() -> io::Result<()> {
    let size = terminal_size().unwrap_or((80, 24));
    let mut terminal = TerminalRuntime::new(
        io::stdout(),
        Theme::default(),
        size,
        TerminalConfig { bracketed_paste: true, mouse_capture: MouseCapture::Disabled },
    )?;

    let mut app = Counter { count: 0 };
    terminal.render_frame(|ctx| app.render(ctx))?; // initial paint

    // 3. Event loop
    loop {
        let Some(raw) = terminal.next_event().await else { break };
        if let CrosstermEvent::Resize(c, r) = &raw {
            terminal.on_resize((*c, *r));
        }
        if let Ok(event) = Event::try_from(raw) {
            if let Some(msgs) = app.on_event(&event).await {
                for msg in msgs {
                    match msg {
                        CounterMsg::Quit => return Ok(()),
                    }
                }
            }
            terminal.render_frame(|ctx| app.render(ctx))?;
        }
    }
    Ok(())
}

Dropping the TerminalRuntime automatically restores the terminal (disables raw mode, bracketed paste, and mouse capture).

§How it works

crossterm::Event ──→ Event::try_from ──→ Component::on_event ──→ Vec<Message>
                                                │                       │
                                                ▼                       ▼
                                         Component::render     parent handles messages
                                                │
                                                ▼
                                    Renderer::render_frame (diff → ANSI)
  1. TerminalRuntime::next_event() reads raw crossterm events from an internal blocking task.
  2. Event::try_from filters key releases and normalizes resize events.
  3. Component::on_event returns None (ignored), Some(vec![]) (consumed), or Some(vec![msg]) (messages for the parent).
  4. Component::render returns a Frame (lines + cursor) given a ViewContext (size + theme).
  5. TerminalRuntime::render_frame diffs against the previous frame and emits only changed ANSI sequences.

§Composing components

Nest components by owning them in your parent and delegating events:

use tui::{Component, Event, Frame, ViewContext, TextField, merge};

struct MyApp {
    name: TextField,
    path: TextField,
    // ...
}

impl Component for MyApp {
    type Message = ();
    async fn on_event(&mut self, event: &Event) -> Option<Vec<()>> {
        // Delegate to the focused child; merge results if needed
        merge(
            self.name.on_event(event).await,
            self.path.on_event(event).await,
        )
    }
    fn render(&mut self, ctx: &ViewContext) -> Frame {
        Frame::vstack([
            self.name.render(ctx),
            self.path.render(ctx),
        ])
    }
}

Use FocusRing to track which child receives events and Frame::vstack to stack frames vertically.

§Built-in widgets

WidgetDescription
TextFieldSingle-line text input
NumberFieldNumeric input (integer or float)
CheckboxBoolean toggle [x] / [ ]
RadioSelectSingle-select radio buttons
MultiSelectMulti-select checkboxes
SelectListScrollable list with selection
FormMulti-field tabbed form
PanelBordered container
SpinnerAnimated progress indicator
ComboboxFuzzy-searchable picker (feature: picker)

§Feature flags

FeatureDescriptionDefault
syntaxSyntax highlighting, markdown rendering, diff previews via syntectyes
pickerFuzzy combobox picker via nucleoyes
testingTest utilities (TestTerminal, render_component, assert_buffer_eq)no

Disable defaults for a smaller dependency tree:

[dependencies]
tui = { version = "0.1", default-features = false }

§License

MIT

Modules§

test_picker
testing
Test utilities for components built with this crate.

Structs§

BorderedTextField
Single-line text input rendered inside a box with the label intersecting the top border.
Checkbox
Boolean toggle rendered as [x] / [ ], optionally with an inline label: [x] Label.
Combobox
A fuzzy-searchable picker that filters items as the user types.
Cursor
Logical cursor position within a component’s rendered output.
DiffLine
A single line in a diff, tagged with its change type.
DiffPreview
A preview of changed lines for an edit operation.
FitOptions
Configuration for Frame::fit.
FocusRing
Tracks which child in a list of focusable items is currently focused.
Form
A multi-field form rendered as a tabbed pane with a virtual “Submit” tab.
FormField
A single field within a Form.
Frame
Logical output from a Component::render call: a vector of Lines plus a Cursor position.
FramePart
A horizontally-stacked slot in Frame::hstack.
FuzzyMatcher
Gallery
Insets
Edge insets used to shrink a ViewContext to a smaller size.
KeyEvent
Represents a key event.
KeyEventState
Represents extra state about the key event.
KeyModifiers
Represents key modifiers (shift, control, alt, etc.).
Line
A single line of styled terminal output, composed of Spans.
MouseEvent
Represents a mouse event.
MultiSelect
Multi-select from a list of options, rendered as checkboxes with a cursor.
NumberField
Numeric input field supporting integers or floats.
Panel
A bordered panel for wrapping content blocks with title/footer chrome.
RadioSelect
Single-select from a list of options, rendered as radio buttons.
Renderer
Diff-based terminal renderer that efficiently updates only changed content.
SelectList
A generic scrollable list with keyboard and mouse navigation.
SelectOption
A single option in a selection list.
Size
The size, in columns and rows, a component is permitted to draw into.
Span
A contiguous run of text sharing a single Style.
Spinner
SplitDiffCell
One side of a split diff row.
SplitDiffRow
A row in a side-by-side diff, pairing an old (left) line with a new (right) line.
SplitLayout
SplitPanel
SplitWidths
Stepper
StepperItem
Style
Text styling: foreground/background colors and attributes (bold, italic, underline, dim, strikethrough).
SyntaxHighlighter
Unified syntax-highlighting facade.
TerminalConfig
TerminalRuntime
TerminalSession
Low-level RAII guard for terminal raw mode, bracketed paste, and mouse capture.
TextField
Single-line text input with cursor tracking and navigation.
Theme
Semantic color palette for TUI rendering.
ViewContext
The size this render call may draw into, plus the Theme in effect.

Enums§

Color
Represents a color.
CrosstermEvent
Represents an event.
DiffTag
Tag indicating the kind of change a diff line represents.
Either
Event
Unified input events for Component::on_event.
FocusOutcome
The result of FocusRing::handle_key.
FormFieldKind
The widget type backing a FormField.
FormMessage
Messages emitted by Form input handling.
GalleryMessage
KeyCode
Represents a key.
KeyEventKind
Represents a keyboard event kind.
MouseCapture
MouseEventKind
A mouse event kind.
Overflow
Overflow policy used by Frame::fit when content exceeds the target width.
PickerKey
PickerMessage
Generic message type for picker components.
RendererCommand
SelectListMessage
StepVisualState
ThemeBuildError

Constants§

BORDER_H_PAD
Width consumed by left (“│ “) and right (” │“) borders.
BRAILLE_FRAMES
GUTTER_WIDTH
SEPARATOR
SEPARATOR_WIDTH

Traits§

Component
The core abstraction for all interactive widgets. Every built-in widget implements this trait, and parent components compose children through it.
Searchable
SelectItem

Functions§

classify_key
display_width_text
highlight_diff
Render a diff preview with syntax-highlighted context/removed/added lines.
merge
Merge two event outcomes. None (ignored) yields to the other. Messages are concatenated in order.
pad_text_to_width
Pads text with trailing spaces to reach target_width display columns. Returns the original text unchanged if it already meets or exceeds the target.
render_diff
Renders a diff preview, choosing split or unified based on terminal width and whether the diff has removals.
render_markdown
soft_wrap_line
split_render_cell
terminal_size
truncate_line
Truncates a styled line to fit within max_width display columns.
truncate_text
Truncates text to fit within max_width display columns, appending “…” if truncated. Returns the original string borrowed when no truncation is needed.
wrap_selection
Wrapping navigation helper for selection indices. delta of -1 moves up, +1 moves down, wrapping at boundaries.