# 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:
1. the `views` closure inserts views into [`ViewFactory`]
2. the `areas` closure 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 `views` closure defines the order of views
- the `areas` closure 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](https://altlinux.space/writers/altui/src/branch/main/examples/simple_pages)
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 `active` before moving to another interactive view
This distinction shows up clearly in the examples:
- [`scroll_paragraph`](https://altlinux.space/writers/altui/src/branch/main/examples/scroll_paragraph) uses a selectable view to add local scrolling behavior
- [`popup`](https://altlinux.space/writers/altui/src/branch/main/examples/popup) uses an interactive view that keeps focus while the popup is open
- [`flex_buttons`](https://altlinux.space/writers/altui/src/branch/main/examples/flex_buttons) and [`simple_buttons_with_vscroll`](https://altlinux.space/writers/altui/src/branch/main/examples/simple_buttons_with_vscroll) use 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`](https://altlinux.space/writers/altui/src/branch/main/examples/scroll_paragraph):
one custom view, one local scroll state, two widgets rendered by one view
- [`flex`](https://altlinux.space/writers/altui/src/branch/main/examples/flex):
basic area assignment with [`CtxStore::split`] and `Layout::flex`
- [`flex_buttons`](https://altlinux.space/writers/altui/src/branch/main/examples/flex_buttons):
button views that update shared layout state
- [`simple_buttons_with_vscroll`](https://altlinux.space/writers/altui/src/branch/main/examples/simple_buttons_with_vscroll):
two-dimensional keyboard navigation with `set_vscroll`
- [`simple_pages`](https://altlinux.space/writers/altui/src/branch/main/examples/simple_pages):
conditional visibility and [`CtxStore::skip_areas`]
- [`popup`](https://altlinux.space/writers/altui/src/branch/main/examples/popup):
layered UI, visibility toggling, and focus transfer
- [`scroll_layout`](https://altlinux.space/writers/altui/src/branch/main/examples/scroll_layout):
external scrolling with [`CtxStore::split_ext`]
- [`wrap`](https://altlinux.space/writers/altui/src/branch/main/examples/wrap) and
[`wrap_with_scroll`](https://altlinux.space/writers/altui/src/branch/main/examples/wrap_with_scroll):
wrapped layouts with and without scrolling
- [`mouse_drag_border`](https://altlinux.space/writers/altui/src/branch/main/examples/mouse_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
```rust
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:
```text
┌──────────────────────────────┐
│ 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:
```text
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:
1. active view
2. hovered view
3. 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`](https://altlinux.space/writers/altui/src/branch/main/examples/scroll_paragraph)
- the popup from [`popup`](https://altlinux.space/writers/altui/src/branch/main/examples/popup)
- custom mouse-driven components like [`mouse_drag_border`](https://altlinux.space/writers/altui/src/branch/main/examples/mouse_drag_border)
```rust
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
```rust
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:
```rust
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:
- [`flex`](https://altlinux.space/writers/altui/src/branch/main/examples/flex)
- [`flex_buttons`](https://altlinux.space/writers/altui/src/branch/main/examples/flex_buttons)
- [`simple_buttons_with_vscroll`](https://altlinux.space/writers/altui/src/branch/main/examples/simple_buttons_with_vscroll)
- [`simple_pages`](https://altlinux.space/writers/altui/src/branch/main/examples/simple_pages)
#### 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:
- <https://altlinux.space/writers/altui/src/branch/main/widgets/tui-textarea>
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