Introduction
ratcn is a component library for Ratatui apps: beautifully designed terminal UI components that you can copy, paste, theme, and own in your application code.
This site documents the preview release. The crate is not yet published to crates.io — that is coming — and not all components have been implemented yet. If there are specific components, patterns, or features you would like to see included, please open an issue.
Two layers, use either one
The library has two layers, and each works on its own:
- Paint-only widgets —
ButtonWidget,BarChartWidget, and friends. Ordinary Ratatui widgets that just draw. They drop into any Ratatui app withframe.render_widget(...): no runtime, no message type, no change to how your app already works. - Interactive components —
Button,List,Tabs,Dialog, and more. These add focus, keyboard and mouse handling, and messages on top, and are declared through theRatcnruntime.
Just want the look?
Give a paint widget a theme and some bools for how it should render, and that is the whole integration:
frame.render_widget(
ButtonWidget::new("Save").themed(&theme).focused(is_focused),
area,
);If you already have focus and event handling you like, keep it. The interactive components paint through these same widgets, so nothing you build this way looks second-class — and you can adopt the runtime later, one component at a time.
Your app stays in charge
ratcn does not own your app loop or your state. Your app owns state, events, and updates; the library reads state while rendering and returns messages when something happens. It enters your app at exactly two call sites — remove them and the rest of the loop is untouched:
Ratcn::render(frame, state, theme, declare)— paint one frame and declare which components are on screen.Ratcn::handle_event(event, state)— route one input event and maybe get a message back.
A typical app has three pieces:
| Piece | Role |
|---|---|
AppState | Your state: domain data, form values, selected rows, FocusState, theme, open dialogs. |
Msg | Your message enum. Components emit these; your update function applies them. |
Ratcn | The runtime: remembers what was on screen last frame and routes events to it. |
Each frame, the closure you pass to Ratcn::render declares the UI: build components from current state, split areas with ordinary Ratatui layouts, and place each interactive component where it is painted. Decorative widgets are painted directly and need no ID or registration.
Components never write your state. A Button emits a message when pressed. A List reads its selection from your state and emits the chosen item for you to store. Focus works the same way: a FocusState lives in your AppState, and focus changes come back as a message. Your update function is the only place state changes.
When an event arrives, hand it to handle_event. The result tells you what to do:
| Result | Meaning |
|---|---|
Emit(msg) | A component handled the event and produced an app message — apply it. |
Consumed | A component handled the event; nothing for you to do. |
Ignored | No component wanted it; your own shortcuts can have it. |
Minimal example
The smallest useful shape: app state, messages, a runtime, one function that draws, and one that handles events.
use ratcn::{Button, Theme};
use ratcn::runtime::{EventResult, FocusState, Ratcn, TabWrap};
struct AppState {
focus: FocusState,
theme: Theme,
saved: bool,
}
#[derive(Clone)]
enum Msg {
FocusChanged(FocusState),
Save,
}
impl AppState {
/// The only place app state changes. A plain function of state and
/// message: testable without a terminal, an event loop, or the runtime.
fn update(&mut self, msg: Msg) {
match msg {
Msg::FocusChanged(focus) => self.focus = focus,
Msg::Save => self.saved = true,
}
}
}
struct App {
state: AppState,
ratcn: Ratcn<AppState, Msg>,
}
impl App {
fn new(state: AppState) -> Self {
let ratcn = Ratcn::new()
.focus(|state: &AppState| &state.focus, Msg::FocusChanged)
.tab_wrap(TabWrap::Wrap);
Self { state, ratcn }
}
/// Route one event; apply whatever it produced.
fn handle_event(&mut self, event: impl TryInto<ratcn::runtime::Event>) {
if let EventResult::Emit(msg) = self.ratcn.handle_event(event, &self.state) {
self.state.update(msg);
}
}
fn draw(&mut self, frame: &mut ratatui::Frame) {
let saved = self.state.saved;
let area = frame.area();
self.ratcn.render(
frame,
&self.state,
&self.state.theme,
|ctx| {
let save = Button::new("Save")
.disabled(saved)
.on_press(|| Msg::Save);
ctx.render_component(
"save",
save,
area,
);
},
);
}
}handle_event routes; update applies. Keeping the applying half in its own function makes every state transition a plain call you can test without a terminal, and it gives messages from other sources — a background task, a timer — the same single path into state.
Styling comes from the theme passed to Ratcn::render — either app-owned or fixed — and that is the only styling most apps touch. See Themes for presets and authored palettes.
The concepts
The concept pages each cover one idea in depth. Roughly in reading order:
- State and Messages — the ownership rules: your app owns state, components read it and emit messages,
updateis the only writer. - Rendering and Event Routing — how a frame is declared, how the runtime remembers it, and how events find the right component.
- Focus, Hover, and Identity — how components get stable identities, how Tab traversal works, and how focus and hover are stored in your state.
- Layers and Modals — dialogs, overlays, and paint ordering.
- Themes — built-in presets and authoring your own palette.
- Host Integration — wiring the runtime into a native crossterm loop or a browser app with ratzilla.
- Mouse Input and Dragging — enabling mouse support, and how clicks, hover, and drags reach components.
- Structuring a larger app — splitting state, messages, and rendering per screen once one module is not enough.
- Custom Components — writing your own components with the same powers as the built-ins.
- Design Decisions — why declaration mistakes panic, and other deliberate choices, for readers evaluating the library.
Component pages under Components cover each built-in component's features with live demos.