Terminal User Interface architecture
The cerebro TUI, in crates/cerebro-tui, is a Ratatui-based terminal app that
provides interactive browsing of project activity data. It uses the
Component + Action architecture pattern recommended by the Ratatui community.
Architecture pattern
Component trait
All UI components implement the Component trait:
#![allow(unused)]
fn main() {
pub trait Component {
fn init(&mut self, area: Size) -> Result<()>;
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>>;
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>>;
fn handle_mouse_event(&mut self, mouse: MouseEvent) -> Result<Option<Action>>;
fn update(&mut self, action: Action) -> Result<Option<Action>>;
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()>;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn help_entries(&self) -> Vec<HelpEntry>;
}
}
Each component encapsulates its own state, including scroll positions, filter
selections, and text input focus, and communicates with other components through
the Action dispatch system.
Action dispatch
The Action enum is the single communication channel between components and the Home orchestrator:
Component ──Action──> Home ──Action──> Component
Actions fall into these categories:
| Category | Examples |
|---|---|
| Lifecycle | Tick, Render, Resize, Quit, ClearScreen |
| Data loading | DataLoaded, LoadError, TriggerRefresh, RefreshComplete |
| Navigation | SwitchView, SelectProject, SwitchTab |
| UI state | ShowPreview, ClosePreview, SetInputMode |
| Config | ToggleProjectActive, SubmitAddProject, ConfigUpdated |
| Build | TriggerBuild, BuildOutput, BuildComplete, CloseBuildLog |
| Text input | StartJournalEntry, StartManualNote, SaveInput, CancelInput |
Key binding system
A context-based system resolves key bindings to prevent collisions:
| Context | Keys | Purpose |
|---|---|---|
| Global | q, r, b, ?, Space, 1-5 | Always active: quit, refresh, build, help, leader key, view switching |
| Sidebar | j/k, arrows, Tab, l | Navigate sidebar list, switch focus to content |
| Content | Tab, Shift+Tab, h | Switch focus back to sidebar |
The leader key, Space, followed by d/p/j/t/c provides an alternative view switching path.
Component hierarchy
Home (orchestrator)
├── Sidebar (project list, view navigation)
├── Dashboard (summary cards, project table)
├── Projects (tabbed sessions/commits/todos, note editor)
├── Journal (date-grouped entries, entry editor)
├── Todos (filterable table, search)
├── Config (project toggle list, add-project form)
├── Help (keybinding overlay)
└── Widgets (shared)
├── Spinner (loading indicator)
├── Toast (notifications)
├── EmptyState (no-data display)
├── SummaryCard (dashboard stats)
├── BuildLog (build output modal)
└── Modal (generic overlay)
Data flow
The TUI follows a Flux-inspired data flow:
1. Home.do_initial_load_sync() collects data from cortex
2. Action::DataLoaded(contexts) dispatched to all components
3. Each component stores relevant data in its own state
4. User interaction → Component.handle_key_event() → Action
5. Home.update() processes Action, may dispatch follow-up Actions
6. Components receive Actions via Component.update()
7. Frame render → Component.draw() reads from local state
Async operations
Long-running operations spawn tokio tasks and report back via Actions:
- Data refresh:
trigger_refresh()spawnscollect_all()in a tokio task, sendsDataLoadedorLoadErroron completion - Config writes:
spawn_config_toggle()writes TOML viatoml_edit, sendsConfigUpdated - Build:
TriggerBuildspawnscerebro buildas a subprocess, streams output viaBuildOutputevents
Input modes
The TUI supports three input modes tracked by InputMode:
| Mode | Description | Active when |
|---|---|---|
| Navigation | Default: keys control UI | Always, except during text editing |
| JournalEntry | Multi-line text editing with ratatui-textarea | Journal view, editing an entry |
| ManualNote | Multi-line text editing with ratatui-textarea | Projects view, editing status/next sections |
In editing modes, the text input widget intercepts most navigation keys. Escape
cancels input and returns to Navigation mode.
Testing strategy
The TUI uses three testing layers:
| Layer | Tool | Purpose |
|---|---|---|
| Unit tests | #[cfg(test)] | Component state transitions, action handling, key binding parsing |
| Integration tests | ratatui::backend::TestBackend | Render full app to buffer, assert on content |
| BDD tests | Cucumber, defined in tests/cucumber.rs | User-facing scenarios in Gherkin |
| System tests | Ghostty devtools | Terminal lifecycle, resize, responsiveness |
Integration tests construct Home directly, call set_cerebro_config() and
do_initial_load_sync().await, then render and assert against the buffer. BDD
tests assert against real component state, such as home.view_name(), not buffer
text.