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 four parts: a TerminalSession (raw mode guard), a Renderer (output), an event source, and a loop that wires them together.

use std::io;
use tui::{
    Component, CrosstermEvent, Event, Frame, KeyCode, Line,
    MouseCapture, Renderer, TerminalSession, Theme, ViewContext,
    spawn_terminal_event_task, 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 terminal, renderer, and event source
#[tokio::main]
async fn main() -> io::Result<()> {
    let size = terminal_size().unwrap_or((80, 24));
    let mut renderer = Renderer::new(io::stdout(), Theme::default(), size);
    let _session = TerminalSession::new(true, MouseCapture::Disabled)?;
    let mut events = spawn_terminal_event_task();

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

    // 3. Event loop
    loop {
        let Some(raw) = events.recv().await else { break };
        if let CrosstermEvent::Resize(c, r) = &raw {
            renderer.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(()),
                    }
                }
            }
            renderer.render_frame(|ctx| app.render(ctx))?;
        }
    }
    Ok(())
}

Dropping _session 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. spawn_terminal_event_task() reads raw crossterm events in a blocking tokio 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. Renderer::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, Layout, 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 {
        // Stack child frames vertically
        let mut layout = Layout::new();
        layout.section(self.name.render(ctx).into_lines());
        layout.section(self.path.render(ctx).into_lines());
        layout.into_frame()
    }
}

Use FocusRing to track which child receives events and Layout 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§

Checkbox
Boolean toggle rendered as [x] / [ ].
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.
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.
FuzzyMatcher
Gallery
KeyEvent
Represents a key event.
KeyEventState
Represents extra state about the key event.
KeyModifiers
Represents key modifiers (shift, control, alt, etc.).
Layout
Stacks content sections vertically with automatic cursor offset tracking.
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.
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
Style
Text styling: foreground/background colors and attributes (bold, italic, underline, dim, strikethrough).
SyntaxHighlighter
Unified syntax-highlighting facade.
TerminalSession
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
Environment passed to Component::render: terminal size and theme.

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.
PickerKey
PickerMessage
Generic message type for picker components.
RendererCommand
SelectListMessage
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
spawn_terminal_event_task
split_blank_panel
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.