Alt TUI Project
A Rust framework for building rich terminal user interfaces and dashboards, built on top of altui-core (fork of tui-rs).
The initial goal was to let developers focus on the logic of their TUI application, rather than building an architecture from scratch.
The result, I hope, is a framework with a clean, simple architecture that makes application development easier than what's currently possible with Ratatui.
Get Started
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 into [ViewFactory] - the
areasclosure assigns [Rect] values to those views through [CtxStore]
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_AREA] or [CtxStore::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 with [CtxStore::split] andLayout::flexflex_buttons: button views that update shared layout statesimple_buttons_with_vscroll: two-dimensional keyboard navigation withset_vscrollsimple_pages: conditional visibility and [CtxStore::skip_areas]popup: layered UI, visibility toggling, and focus transferscroll_layout: external scrolling with [CtxStore::split_ext]wrapandwrap_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 ;
use ;
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:
- [
AppHandler] owns global application state and controls process lifetime - [
ViewLoop] orchestrates events, logic, area assignment, and rendering - [
ViewFactory] stores views and runtime contexts - [
View] defines 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 views
Event 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
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 = new;
factory.insert_view;
AnyView::new is useful when the widget still needs a small amount of state-driven logic:
let paragraph = new;
factory.insert_view;
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