Expand description
§altui
altui is a state-driven TUI runtime built around View.
It separates a terminal application into explicit parts:
- global application state via
AppHandler - a stable collection of views via
ViewFactory - per-frame area assignment via
CtxStore - the event/render runtime via
ViewLoop
The central idea is simple: every visible behavior in the UI should be explainable in terms of view state, layout assignment, and one shared app state.
§Mental Model
A View is a state-aware UI unit that can:
- react to events through
View::on_event - update itself through
View::logic - render into a buffer through
View::render
A frame in altui is built from two coordinated closures:
- the
viewsclosure inserts views intoViewFactory - the
areasclosure assigns [Rect] values to those views throughCtxStore
These two sequences must stay in sync.
§Core Rule: View Count Must Match Area Count
The runtime relies on one invariant:
- the number of inserted views must always match the number of assigned areas
In practice this means:
- the
viewsclosure defines the order of views - the
areasclosure runs every frame and must assign areas in the same order - even hidden views still need an area, often via
EMPTY_AREAorCtxStore::skip_areas
This is why the simple_pages example
can hide whole pages while still keeping the runtime stable.
§View States
A view can be in one of three runtime states:
- inactive
- hovered
- active
A view context can also be configured as:
- visible or hidden
- selectable
- interactive
- button-like
These flags influence navigation and event routing.
The most useful mental categories are:
- regular view: rendered only, not part of navigation
- selectable view: participates in hover navigation and may perform an extra action when it briefly becomes active
- interactive view: remains active and keeps receiving input until it exits the active state
- button view: becomes active for one iteration and then returns to hover automatically
A subtle but important detail:
- both selectable and interactive views can receive
View::on_event - a selectable view does not take over the whole input flow; it can add behavior on top of normal navigation
- an interactive view captures input while active, so you usually need to exit
activebefore moving to another interactive view
This distinction shows up clearly in the examples:
scroll_paragraphuses a selectable view to add local scrolling behaviorpopupuses an interactive view that keeps focus while the popup is openflex_buttonsandsimple_buttons_with_vscrolluse button views
§Read the Examples First
The root examples are the main source of truth for how altui is intended to be used.
Recommended reading order:
scroll_paragraph: one custom view, one local scroll state, two widgets rendered by one viewflex: basic area assignment withCtxStore::splitandLayout::flexflex_buttons: button views that update shared layout statesimple_buttons_with_vscroll: two-dimensional keyboard navigation withset_vscrollsimple_pages: conditional visibility andCtxStore::skip_areaspopup: layered UI, visibility toggling, and focus transferscroll_layout: external scrolling withCtxStore::split_extwrapandwrap_with_scroll: wrapped layouts with and without scrollingmouse_drag_border: mouse-driven layout updates
The README files in those example directories summarize the main behavior, but the source files are the most important reference.
§Minimal Example
use altui::{
backend::Backend,
buffer::Buffer,
layout::{Alignment, Constraint, Flex, Layout, Rect},
widgets::{Paragraph, Widget},
AltuiInit, AppHandler, AreaCache, CtxStore, Terminal, View, ViewFactory, ViewLoop,
};
use crossterm::event::{read, Event, KeyCode};
struct HelloView<'a> {
text: Paragraph<'a>,
}
impl<'a> HelloView<'a> {
fn new() -> Self {
let mut text = Paragraph::new("Hello world!");
text.alignment(Alignment::Center);
Self { text }
}
}
impl View for HelloView<'_> {
type State = MainState;
fn render(&mut self, area: Rect, _: &mut AreaCache, buf: &mut Buffer) {
self.text.render(area, buf);
}
}
struct MainState {}
impl AppHandler for MainState {}
impl MainState {
fn new() -> Self {
Self {}
}
fn run<'a, B: Backend>(mut self, terminal: &mut Terminal<B>) {
ViewLoop::new(terminal).unwrap().basic_navigation(true).run(
&mut self,
|factory: &mut ViewFactory<MainState>, _: &mut MainState| {
factory.insert_view(HelloView::new());
},
|area: Rect, ctx: &mut CtxStore, _: &mut MainState| {
let layout = ctx.split(
Layout::vertical(&[Constraint::Length(1)]).flex(Flex::Center),
area,
);
ctx.next_areas(&layout);
},
read,
|state, factory| {
if let Some(Event::Key(key)) = factory.event() {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => state.stop(),
_ => {}
}
}
},
);
}
}
fn main() -> std::io::Result<()> {
AltuiInit::new(true)?.set_panic_hook().run(|terminal| {
MainState::new().run(terminal);
Ok(())
})
}This is the smallest useful altui program:
- one app state
- one custom view
- one area assignment
- one global event handler
§Architecture Overview
altui follows a state-driven view architecture:
┌──────────────────────────────┐
│ Application State │
│ (AppHandler) │
└──────────────┬───────────────┘
│
│ shared mutable state
▼
┌──────────────────────────────┐
│ ViewLoop │
│ (runtime event/render loop) │
└──────────────┬───────────────┘
│
manages │ views + contexts
▼
┌──────────────────────────────┐
│ ViewFactory │
│ stores views and contexts │
└──────────────┬───────────────┘
│
│ executes
▼
┌───────────────┐
│ View │
│ (UI element) │
└───────────────┘
│
│ renders using
▼
┌───────────────┐
│ altui_core │
│ widgets/buffer│
└───────────────┘In this architecture:
AppHandlerowns global application state and controls process lifetimeViewLooporchestrates events, logic, area assignment, and renderingViewFactorystores views and runtime contextsViewdefines local UI behavior
Views do not own the app state directly. Instead, they receive mutable access to it during logic and event processing.
§Event Lifecycle
A frame processed by ViewLoop follows this sequence:
1. Read input event
2. Run built-in navigation, if enabled
3. Dispatch the event
4. Run view logic
5. Assign areas
6. Render visible viewsEvent dispatch priority is:
- active view
- hovered view
- global event handler
This priority is why popups, editors, and mouse-driven widgets can temporarily take over input handling without replacing the whole runtime.
§View vs AnyView
There are two common ways to insert UI elements into altui.
§Implement View
Implement View when you need:
- internal state inside the view
- custom event handling
- multiple child widgets rendered from one view
- explicit hover/active behavior
Typical cases:
- the scrollable panel from
scroll_paragraph - the popup from
popup - custom mouse-driven components like
mouse_drag_border
struct MyView { /* internal state */ }
impl View for MyView {
type State = AppState;
fn logic(&mut self, ctx: &mut ViewCtx, state: &mut AppState) {
// update local state from app state
}
fn on_event(&mut self, event: Event, ctx: &mut ViewCtx, state: &mut AppState) {
// optional custom input handling
}
fn render(&mut self, area: Rect, cache: &mut AreaCache, buf: &mut Buffer) {
// render the view
}
}§Use AnyView
AnyView is the lightest way to insert a widget into the runtime.
It is a good fit when:
- the widget is mostly presentational
- state already lives in the application
- a small logic closure is enough
let paragraph = Paragraph::new("Hello world!");
factory.insert_view(AnyView::simple(paragraph));AnyView::new is useful when the widget still needs a small amount of state-driven logic:
enum Pages {
PageOne,
PageTwo,
}
let paragraph = Paragraph::new("Hello world!");
factory.insert_view(AnyView::new(
paragraph,
|_widget, ctx, state: &mut MainState| match state.pages {
Pages::PageOne => ctx.set_visible(false),
Pages::PageTwo => ctx.set_visible(true),
},
));This pattern is used heavily in:
§Layout Guidance
In area-building code, prefer CtxStore::split and CtxStore::split_ext
over calling Layout::split directly.
CtxStore owns the frame-level layout cache, and the examples in this repository
are written around that rule.
In render code, use AreaCache when a single view needs its own cached internal layout.
§Reexports
altui reexports everything from altui_core.
It also reexports altui-textarea, an adapted fork of tui-textarea, from:
The fork keeps the tui-textarea API shape, but is published separately so
it is clear that it is not the upstream crate.
That fork was adjusted specifically to work better with altui. It may
later be proposed upstream, but for now it should be treated as part of this
repository’s current architecture.
§Recommendations
Use ratatui if:
- you want a mature widget ecosystem
- you need a more stable public API
- you prefer ecosystem familiarity over experimentation
Use altui if:
- you want explicit control over view state and event routing
- you want cached layout assignment built into the runtime
- you are comfortable with an experimental API that is still being shaped by examples
Modules§
- backend
- buffer
- layout
- style
stylecontains the primitives used to control how your user interface will look.- symbols
- terminal
- text
- Primitives for styled text.
- widgets
widgetsis a collection of types that implementWidget.
Macros§
- assert_
buffer_ eq - Assert that two buffers are equal by comparing their areas and content.
Structs§
- Altui
Init - Crossterm terminal initialization helper which restores the original terminal state on drop.
- AnyView
- A generic adapter that turns any
Widgetinto aView. - Area
Cache - Per-view layout cache used during rendering.
- CtxStore
- Layout context manager used by the
areasclosure. - Frame
- Represents a consistent terminal interface for rendering.
- Input
- Backend-agnostic key input type.
- Terminal
- Interface to the terminal backed by Termion
- Terminal
Options - Options to pass to
Terminal::with_options - Text
Area - A type to manage state of textarea. These are some important methods:
- ViewCtx
- Per-view runtime state owned by
altui. - View
Factory - Frame-scoped view manager used by
ViewLoop. - View
Loop - The main runtime coordinator of
altui. - Viewport
- UNSTABLE
- Widget
Id - Stable identifier for a view inside the runtime.
Enums§
- Cursor
Move - Specify how to move the cursor.
- Key
- Backend-agnostic key input kind.
- Scrolling
- Specify how to scroll the textarea.
Constants§
- EMPTY_
AREA - A zero‑sized rectangle used to efficiently skip areas in the layout pipeline.
Traits§
- AppHandler
- Trait implemented by the global application state.
- View
- A state-aware UI component.