16 KiB
TUI Orchestrator - Complete TUI Framework
Overview
tui_orchestrator is a ready-to-use TUI framework that provides a complete runtime for building terminal user interfaces. Users define their pages, buttons, and logic—library handles everything else: input routing, focus management, page navigation, lifecycle hooks, and event orchestration.
Key Philosophy
"Register pages with buttons and logic—it just works."
The library is a complete application framework where:
- User defines components (pages with focusable elements)
- Library orchestrates all runtime concerns
- Everything is optional—define what you need
- Fully extendable for complex apps like komp_ac
Zero Boilerplate
Users write:
impl Component for LoginPage {
fn targets(&self) -> &[Focus];
fn handle(&mut self, focus: &Focus, action: Action) -> Result<Option<Event>> {
// What happens when button pressed
}
}
Library handles:
- Input processing
- Focus management
- Page navigation
- Lifecycle hooks
- Event routing
- Default keybindings
Extension Model
komp_ac can use the library for 90% of functionality while extending:
- Mode resolution (dynamic Canvas-style modes)
- Overlay management (command palette, find file, search)
- Event routing (global vs page vs canvas actions)
- Custom behaviors (boundary detection, navigation rules)
Defaults work, override what's different.
Architecture
┌─────────────────────────────────────────────────┐
│ User Code (What You Define) │
│ │
│ Component trait │
│ - Page structs/enums │
│ - Focus targets (buttons, fields) │
│ - Button logic (what happens on press) │
│ - Lifecycle hooks (optional) │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Orchestrator (Library Runtime) │
│ │
│ - ComponentRegistry │
│ - FocusManager │
│ - Bindings (default + custom) │
│ - Router + history │
│ - ModeStack │
│ - OverlayStack │
│ - EventBus │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Extension Points (For komp_ac) │
│ │
│ - ModeResolver (dynamic mode resolution) │
│ - OverlayManager (custom overlay types) │
│ - EventHandler (custom event routing) │
│ - FocusNavigation (boundary detection) │
└─────────────────────────────────────────────────┘
Implementation Phases
Phase 1: Core Foundation ✅ COMPLETE
Completed:
src/input/- Key types, bindings, sequence handlingsrc/focus/- Focus manager, queries, traits
What this provides:
- Backend-agnostic key representation
- Key-to-action mappings
- Focus tracking with navigation
- Generic focus IDs (user-defined enums,
usize,String, etc.)
Phase 2: Component System (CURRENT)
Goal: Unified abstraction for pages/components.
Files to create:
src/component/mod.rs- Component traitsrc/component/action.rs- Standard component actionssrc/component/error.rs- Component-specific errors
Component Trait:
pub trait Component {
type Focus: FocusId + Clone;
type Action: Action + Clone;
type Event: Clone + core::fmt::Debug;
fn targets(&self) -> &[Self::Focus];
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>>;
fn on_enter(&mut self) -> Result<()> { Ok(()) }
fn on_exit(&mut self) -> Result<()> { Ok(()) }
fn on_focus(&mut self, focus: &Self::Focus) -> Result<()> { Ok(()) }
fn on_blur(&mut self, focus: &Self::Focus) -> Result<()> { Ok(()) }
fn handle_text(&mut self, focus: &Self::Focus, ch: char) -> Result<Option<Self::Event>> { Ok(None) }
fn can_navigate_forward(&self, focus: &Self::Focus) -> bool { true }
fn can_navigate_backward(&self, focus: &Self::Focus) -> bool { true }
}
Standard Component Actions:
pub enum ComponentAction {
Next, // Tab by default
Prev, // Shift+Tab by default
First,
Last,
Select, // Enter by default
Cancel, // Esc by default
TypeChar(char),
Backspace,
Delete,
Custom(usize), // User extension
}
Phase 3: Router & Lifecycle
Goal: Page navigation with automatic lifecycle hooks.
Files to create:
src/router/mod.rs- Router trait and implementationsrc/router/history.rs- Navigation history
Router API:
pub struct Router<C: Component> {
pages: alloc::collections::HashMap<String, C>,
current: Option<String>,
history: alloc::vec::Vec<String>,
}
impl<C: Component> Router<C> {
pub fn new() -> Self;
pub fn navigate(&mut self, id: &str) -> Result<()>;
pub fn back(&mut self) -> Result<Option<()>>;
pub fn forward(&mut self) -> Result<Option<()>>;
pub fn current(&self) -> Option<&C>;
}
Automatic behavior:
navigate()callsold_page.on_exit()→ swaps page → callsnew_page.on_enter()back()/forward()manage history stack- Focus targets auto-updated from
targets()
Phase 4: Orchestrator (The Core Runtime)
Goal: Wire everything together into complete TUI runtime.
Files to create:
src/orchestrator/mod.rs- Orchestrator structsrc/orchestrator/modes.rs- Mode stack and resolutionsrc/orchestrator/overlays.rs- Overlay stacksrc/orchestrator/events.rs- Event bussrc/orchestrator/bindings.rs- Default + custom bindings
Orchestrator API:
pub struct Orchestrator<C: Component> {
registry: ComponentRegistry<C>,
focus: FocusManager<C::Focus>,
bindings: Bindings<ComponentAction>,
router: Router<C>,
modes: ModeStack<ModeName>,
overlays: OverlayStack<C::Event>,
events: EventBus<C::Event>,
}
impl<C: Component> Orchestrator<C> {
pub fn new() -> Self;
pub fn register_page(&mut self, id: &str, page: C) -> Result<()>;
pub fn navigate_to(&mut self, id: &str) -> Result<()>;
pub fn process_frame(&mut self, key: Key) -> Result<alloc::vec::Vec<C::Event>>;
pub fn run<I: InputSource>(&mut self, input: I) -> Result<()>;
// Extension points
pub fn set_mode_resolver<R: ModeResolver + 'static>(&mut self, resolver: R);
pub fn set_overlay_manager<O: OverlayManager + 'static>(&mut self, manager: O);
pub fn set_event_handler<H: EventHandler<C::Event> + 'static>(&mut self, handler: H);
}
Process flow:
- Check overlay active → route to overlay
- Get current mode + focus
- Lookup binding → get action
- Get current component
- Call
component.handle(action, focus) - Collect events returned
- Handle internal events (focus changes, page nav)
- Return external events to user
Phase 5: Extension Traits
Goal: Provide extension points for komp_ac's custom behavior.
Files to create:
src/extension/mod.rs- Extension traitssrc/extension/mode.rs- Mode resolversrc/extension/overlay.rs- Overlay managersrc/extension/event.rs- Event handler
ModeResolver (for dynamic mode resolution):
pub trait ModeResolver {
fn resolve(&self, focus: &dyn core::any::Any) -> alloc::vec::Vec<ModeName>;
}
pub struct DefaultModeResolver;
impl ModeResolver for DefaultModeResolver {
fn resolve(&self, _focus: &dyn core::any::Any) -> alloc::vec::Vec<ModeName> {
alloc::vec![ModeName::General]
}
}
OverlayManager (for custom overlays):
pub trait OverlayManager {
fn is_active(&self) -> bool;
fn handle_input(&mut self, key: Key) -> Option<OverlayResult>;
}
pub enum OverlayResult {
Dismissed,
Selected(OverlayData),
Continue,
}
EventHandler (for custom event routing):
pub trait EventHandler<E> {
fn handle(&mut self, event: E) -> Result<HandleResult>;
}
pub enum HandleResult {
Consumed,
Forward,
Navigate(String),
}
Phase 6: Builder & Defaults
Goal: Easy setup with sensible defaults.
Files to create:
src/builder/mod.rs- Builder patternsrc/defaults/bindings.rs- Preset keybindings
Builder API:
impl<C: Component> Orchestrator<C> {
pub fn builder() -> Builder<C> { Builder::new() }
}
pub struct Builder<C: Component> {
orchestrator: Orchestrator<C>,
}
impl<C: Component> Builder<C> {
pub fn with_page(mut self, id: &str, page: C) -> Result<Self>;
pub fn with_default_bindings(mut self) -> Self;
pub fn with_mode_resolver<R: ModeResolver + 'static>(mut self, resolver: R) -> Self;
pub fn with_overlay_manager<O: OverlayManager + 'static>(mut self, manager: O) -> Self;
pub fn build(self) -> Result<Orchestrator<C>>;
}
Default bindings:
pub fn default_bindings<A: Action>() -> Bindings<ComponentAction> {
let mut bindings = Bindings::new();
bindings.bind(Key::tab(), ComponentAction::Next);
bindings.bind(Key::shift_tab(), ComponentAction::Prev);
bindings.bind(Key::enter(), ComponentAction::Select);
bindings.bind(Key::esc(), ComponentAction::Cancel);
bindings.bind(Key::ctrl('c'), ComponentAction::Custom(0)); // Quit
}
Phase 7: Integration (Optional)
Goal: Seamless integration with komp_ac.
Files to create:
src/integration/mod.rs- Integration helperssrc/integration/komp_ac.rs- komp_ac-specific adapters
Adapter pattern:
impl Component for komp_ac::LoginPage {
type Focus = komp_ac::FocusTarget;
type Action = komp_ac::ResolvedAction;
type Event = komp_ac::AppEvent;
fn targets(&self) -> &[Self::Focus] { ... }
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> { ... }
}
File Structure
src/
├── lib.rs # Routing only, re-exports
├── prelude.rs # Common imports
│
├── input/ # Phase 1 ✅
│ ├── mod.rs
│ ├── key.rs # KeyCode, KeyModifiers
│ ├── bindings.rs # Bindings
│ ├── handler.rs # InputHandler
│ ├── result.rs # MatchResult
│ └── action.rs # Action trait
│
├── focus/ # Phase 1 ✅
│ ├── mod.rs
│ ├── id.rs # FocusId trait
│ ├── manager.rs # FocusManager
│ ├── query.rs # FocusQuery
│ ├── error.rs # FocusError
│ └── traits.rs # Focusable
│
├── component/ # Phase 2
│ ├── mod.rs
│ ├── trait.rs # Component trait
│ ├── action.rs # ComponentAction
│ └── error.rs # ComponentError
│
├── router/ # Phase 3
│ ├── mod.rs
│ ├── router.rs # Router
│ └── history.rs # HistoryStack
│
├── orchestrator/ # Phase 4
│ ├── mod.rs
│ ├── core.rs # Orchestrator
│ ├── modes.rs # ModeStack, ModeResolver
│ ├── overlays.rs # OverlayStack
│ ├── bindings.rs # Component bindings
│ └── events.rs # EventBus
│
├── extension/ # Phase 5
│ ├── mod.rs
│ ├── mode.rs # ModeResolver trait
│ ├── overlay.rs # OverlayManager trait
│ └── event.rs # EventHandler trait
│
├── builder/ # Phase 6
│ ├── mod.rs
│ ├── builder.rs # Builder pattern
│ └── defaults.rs # Default bindings
│
└── integration/ # Phase 7
├── mod.rs
└── komp_ac.rs # komp_ac adapters
tests/ # Mirror src/ structure
├── input/
├── focus/
├── component/
├── router/
├── orchestrator/
└── integration/
Design Principles
From AGENTS.md
- Feature-based tree structure—group by domain
- Each feature is self-contained—handler, logic, types, tests
- Functional programming style—pure functions, stateless where possible
- Use structs, traits, enums, impl, match over if
- No Arc/Mutex/RefCell
- Result<T, E> everywhere
- mod.rs is for routing only
- No comments unless necessary
Additional for Framework
- Batteries included—not just building blocks
- Sensible defaults—zero configuration works
- Optional everything—define only what you need
- Extension points—override defaults when needed
- no_std compatible—works on embedded, WASM
- Backend-agnostic—no crossterm/ratatui dependencies
- User-focused—"register page" not "register_chord"
Dependencies
Core (no_std)
alloc- For dynamic collections (Vec, HashMap)
Optional Features
std- Enable std library supportsequences- Enable multi-key sequences
User Experience
Before (Building Blocks)
Users must manually wire everything:
- Create focus manager
- Create bindings
- Create router
- Set up components
- Write main loop
- Handle lifecycle manually
Result: Lots of boilerplate, easy to get wrong.
After (Framework)
Users define components and run:
#[derive(Debug, Clone)]
struct LoginPage;
impl Component for LoginPage {
fn targets(&self) -> &[Focus] { ... }
fn handle(&mut self, focus: &Focus, action: Action) -> Result<Option<Event>> { ... }
}
fn main() -> Result<()> {
let mut orch = Orchestrator::new();
orch.register_page("login", LoginPage::new())?;
orch.run()?;
}
Result: Zero boilerplate, everything just works.
Migration for komp_ac
Before Integration
komp_ac has:
- Custom orchestrator
- Custom mode resolution (Canvas-style)
- Custom overlays (command bar, find file, search)
- Custom action routing (page vs canvas vs global)
After Integration
komp_ac:
- Implements
Componenttrait for all pages - Uses library's
Orchestratoras runtime - Extends with custom
ModeResolver - Extends with custom
OverlayManager - Extends with custom
EventHandler
Result:
- Library handles 90% of runtime
- komp_ac keeps all custom behavior
- No code duplication
- Cleaner, more maintainable codebase
Feature Checklist
- Phase 1: Input handling (keys, bindings, focus)
- Phase 2: Component trait and actions
- Phase 3: Router with lifecycle
- Phase 4: Orchestrator runtime
- Phase 5: Extension traits
- Phase 6: Builder and defaults
- Phase 7: Integration with komp_ac
- Documentation updates
- Example applications
- Full test coverage