583 lines
14 KiB
Markdown
583 lines
14 KiB
Markdown
# TUI Orchestrator Integration Guide
|
|
|
|
This guide shows how to use the TUI Orchestrator framework to build terminal user interfaces with minimal boilerplate.
|
|
|
|
---
|
|
|
|
## Quick Start: Your First TUI App
|
|
|
|
### Step 1: Define Your Component
|
|
|
|
A component represents a page or UI section with focusable elements:
|
|
|
|
```rust
|
|
extern crate alloc;
|
|
|
|
use tui_orchestrator::prelude::*;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
enum LoginFocus {
|
|
Username,
|
|
Password,
|
|
LoginButton,
|
|
CancelButton,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum LoginEvent {
|
|
AttemptLogin { username: String, password: String },
|
|
Cancel,
|
|
}
|
|
|
|
struct LoginPage {
|
|
username: alloc::string::String,
|
|
password: alloc::string::String,
|
|
}
|
|
|
|
impl Component for LoginPage {
|
|
type Focus = LoginFocus;
|
|
type Action = ComponentAction;
|
|
type Event = LoginEvent;
|
|
|
|
fn targets(&self) -> &[Self::Focus] {
|
|
&[
|
|
LoginFocus::Username,
|
|
LoginFocus::Password,
|
|
LoginFocus::LoginButton,
|
|
LoginFocus::CancelButton,
|
|
]
|
|
}
|
|
|
|
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> {
|
|
match (focus, action) {
|
|
(LoginFocus::LoginButton, ComponentAction::Select) => {
|
|
Ok(Some(LoginEvent::AttemptLogin {
|
|
username: self.username.clone(),
|
|
password: self.password.clone(),
|
|
}))
|
|
}
|
|
(LoginFocus::CancelButton, ComponentAction::Select) => {
|
|
Ok(Some(LoginEvent::Cancel))
|
|
}
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
|
|
fn handle_text(&mut self, focus: &Self::Focus, ch: char) -> Result<Option<Self::Event>> {
|
|
match focus {
|
|
LoginFocus::Username => {
|
|
self.username.push(ch);
|
|
Ok(None)
|
|
}
|
|
LoginFocus::Password => {
|
|
self.password.push(ch);
|
|
Ok(None)
|
|
}
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
|
|
fn on_enter(&mut self) -> Result<()> {
|
|
self.username.clear();
|
|
self.password.clear();
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 2: Register and Run
|
|
|
|
```rust
|
|
use tui_orchestrator::prelude::*;
|
|
|
|
fn main() -> Result<()> {
|
|
let mut orch = Orchestrator::builder()
|
|
.with_page("login", LoginPage::new())
|
|
.with_default_bindings()
|
|
.build()?;
|
|
|
|
orch.navigate_to("login")?;
|
|
|
|
orch.run(&mut MyInputSource)?;
|
|
}
|
|
```
|
|
|
|
**That's it.** The library handles:
|
|
- Input processing
|
|
- Focus management (Tab/Shift+Tab navigation)
|
|
- Button activation (Enter key)
|
|
- Text input typing
|
|
- Page lifecycle (on_enter/on_exit)
|
|
|
|
---
|
|
|
|
## Component Trait Deep Dive
|
|
|
|
### Associated Types
|
|
|
|
```rust
|
|
pub trait Component {
|
|
type Focus: FocusId + Clone; // What can be focused in this component
|
|
type Action: Action + Clone; // What actions this component handles
|
|
type Event: Clone + Debug; // Events this component emits
|
|
}
|
|
```
|
|
|
|
### Required Methods
|
|
|
|
**`targets(&self) -> &[Self::Focus]`**
|
|
|
|
Returns all focusable elements. Order determines navigation sequence (next/prev).
|
|
|
|
```rust
|
|
fn targets(&self) -> &[Self::Focus] {
|
|
&[
|
|
Focus::Username,
|
|
Focus::Password,
|
|
Focus::LoginButton,
|
|
Focus::CancelButton,
|
|
]
|
|
}
|
|
```
|
|
|
|
**`handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>>`**
|
|
|
|
Called when a bound action occurs. Returns optional event for application to handle.
|
|
|
|
```rust
|
|
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> {
|
|
match (focus, action) {
|
|
(Focus::Submit, ComponentAction::Select) => Ok(Some(Event::Submit)),
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
```
|
|
|
|
### Optional Methods
|
|
|
|
All have default implementations—only override what you need.
|
|
|
|
**`on_enter(&mut self) -> Result<()>`**
|
|
|
|
Called when component becomes active (page is navigated to). Good for resetting state.
|
|
|
|
**`on_exit(&mut self) -> Result<()>`**
|
|
|
|
Called when component becomes inactive (page is navigated away). Good for cleanup.
|
|
|
|
**`on_focus(&mut self, focus: &Self::Focus) -> Result<()>`**
|
|
|
|
Called when a specific focus target gains focus.
|
|
|
|
**`on_blur(&mut self, focus: &Self::Focus) -> Result<()>`**
|
|
|
|
Called when a specific focus target loses focus.
|
|
|
|
**`handle_text(&mut self, focus: &Self::Focus, ch: char) -> Result<Option<Self::Event>>`**
|
|
|
|
Called when character is typed (not a bound action). Only called for text-friendly focus targets.
|
|
|
|
**`can_navigate_forward(&self, focus: &Self::Focus) -> bool`**
|
|
|
|
Return false to prevent Next action from moving focus (useful for boundary detection).
|
|
|
|
**`can_navigate_backward(&self, focus: &Self::Focus) -> bool`**
|
|
|
|
Return false to prevent Prev action from moving focus.
|
|
|
|
---
|
|
|
|
## Standard Component Actions
|
|
|
|
The library provides these actions automatically bound to keys:
|
|
|
|
| Action | Default Key | Description |
|
|
|--------|--------------|-------------|
|
|
| `ComponentAction::Next` | Tab | Move focus to next target |
|
|
| `ComponentAction::Prev` | Shift+Tab | Move focus to previous target |
|
|
| `ComponentAction::First` | Home | Move focus to first target |
|
|
| `ComponentAction::Last` | End | Move focus to last target |
|
|
| `ComponentAction::Select` | Enter | Activate current focus target |
|
|
| `ComponentAction::Cancel` | Esc | Cancel or close |
|
|
| `ComponentAction::TypeChar(c)` | Any character | Type character |
|
|
| `ComponentAction::Backspace` | Backspace | Delete character before cursor |
|
|
| `ComponentAction::Delete` | Delete | Delete character at cursor |
|
|
| `ComponentAction::Custom(n)` | None | User-defined action |
|
|
|
|
### Customizing Bindings
|
|
|
|
```rust
|
|
let mut orch = Orchestrator::new();
|
|
|
|
// Override default bindings
|
|
orch.bindings().bind(Key::ctrl('s'), ComponentAction::Custom(0)); // Custom save action
|
|
orch.bindings().bind(Key::char(':'), ComponentAction::Custom(1)); // Enter command mode
|
|
```
|
|
|
|
---
|
|
|
|
## Orchestrator API
|
|
|
|
### Basic Setup
|
|
|
|
```rust
|
|
let mut orch = Orchestrator::new();
|
|
|
|
// Register pages
|
|
orch.register_page("login", LoginPage::new())?;
|
|
orch.register_page("home", HomePage::new())?;
|
|
|
|
// Navigation
|
|
orch.navigate_to("login")?;
|
|
```
|
|
|
|
### Processing Input
|
|
|
|
```rust
|
|
loop {
|
|
let key = read_key()?;
|
|
let events = orch.process_frame(key)?;
|
|
|
|
for event in events {
|
|
match event {
|
|
LoginEvent::AttemptLogin => do_login(username, password),
|
|
LoginEvent::Cancel => return Ok(()),
|
|
}
|
|
}
|
|
|
|
render(&orch)?;
|
|
}
|
|
```
|
|
|
|
### Reading State
|
|
|
|
```rust
|
|
// Get current page
|
|
if let Some(page) = orch.current_page() {
|
|
// Access page...
|
|
}
|
|
|
|
// Get current focus
|
|
if let Some(focus) = orch.focus().current() {
|
|
// Check what's focused...
|
|
}
|
|
|
|
// Create query snapshot
|
|
let query = orch.focus().query();
|
|
if query.is_focused(&LoginFocus::Username) {
|
|
// Username field is focused...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Multiple Pages Example
|
|
|
|
```rust
|
|
#[derive(Debug, Clone)]
|
|
enum MyPage {
|
|
Login(LoginPage),
|
|
Home(HomePage),
|
|
Settings(SettingsPage),
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let mut orch = Orchestrator::new();
|
|
|
|
orch.register_page("login", LoginPage::new())?;
|
|
orch.register_page("home", HomePage::new())?;
|
|
orch.register_page("settings", SettingsPage::new())?;
|
|
|
|
orch.navigate_to("login")?;
|
|
|
|
orch.run()?;
|
|
}
|
|
```
|
|
|
|
Navigation with history:
|
|
```rust
|
|
orch.navigate_to("home")?;
|
|
orch.navigate_to("settings")?;
|
|
orch.back()? // Return to home
|
|
```
|
|
|
|
---
|
|
|
|
## Extension: Custom Mode Resolution
|
|
|
|
For apps with complex mode systems (like komp_ac):
|
|
|
|
```rust
|
|
pub struct CustomModeResolver {
|
|
state: AppState,
|
|
}
|
|
|
|
impl ModeResolver for CustomModeResolver {
|
|
fn resolve(&self, focus: &dyn Any) -> alloc::vec::Vec<ModeName> {
|
|
match focus.downcast_ref::<FocusTarget>() {
|
|
Some(FocusTarget::CanvasField(_)) => {
|
|
// Dynamic mode based on editor state
|
|
vec![self.state.editor_mode(), ModeName::Common, ModeName::Global]
|
|
}
|
|
_ => vec![ModeName::General, ModeName::Global],
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut orch = Orchestrator::new();
|
|
orch.set_mode_resolver(CustomModeResolver::new(state));
|
|
```
|
|
|
|
---
|
|
|
|
## Extension: Custom Overlays
|
|
|
|
For apps with complex overlay types (command palette, dialogs):
|
|
|
|
```rust
|
|
pub struct CustomOverlayManager {
|
|
command_palette: CommandPalette,
|
|
dialogs: Vec<Dialog>,
|
|
}
|
|
|
|
impl OverlayManager for CustomOverlayManager {
|
|
fn is_active(&self) -> bool {
|
|
self.command_palette.is_active() || !self.dialogs.is_empty()
|
|
}
|
|
|
|
fn handle_input(&mut self, key: Key) -> Option<OverlayResult> {
|
|
if let Some(result) = self.command_palette.handle_input(key) {
|
|
return Some(result);
|
|
}
|
|
// Handle dialogs...
|
|
None
|
|
}
|
|
}
|
|
|
|
let mut orch = Orchestrator::new();
|
|
orch.set_overlay_manager(CustomOverlayManager::new());
|
|
```
|
|
|
|
---
|
|
|
|
## Integration with External Libraries
|
|
|
|
### Reading Input from crossterm
|
|
|
|
```rust
|
|
use crossterm::event;
|
|
use tui_orchestrator::input::Key;
|
|
|
|
impl InputSource for CrosstermInput {
|
|
fn read_key(&mut self) -> Result<Key> {
|
|
match event::read()? {
|
|
event::Event::Key(key_event) => {
|
|
let code = match key_event.code {
|
|
event::KeyCode::Char(c) => KeyCode::Char(c),
|
|
event::KeyCode::Enter => KeyCode::Enter,
|
|
event::KeyCode::Tab => KeyCode::Tab,
|
|
event::KeyCode::Esc => KeyCode::Esc,
|
|
// ... map all codes ...
|
|
};
|
|
|
|
let modifiers = KeyModifiers {
|
|
control: key_event.modifiers.contains(event::KeyModifiers::CONTROL),
|
|
alt: key_event.modifiers.contains(event::KeyModifiers::ALT),
|
|
shift: key_event.modifiers.contains(event::KeyModifiers::SHIFT),
|
|
};
|
|
|
|
Ok(Key::new(code, modifiers))
|
|
}
|
|
_ => Err(Error::NotAKeyEvent),
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using with ratatui for Rendering
|
|
|
|
The library is backend-agnostic—you can render with any framework:
|
|
|
|
```rust
|
|
use ratatui::backend::CrosstermBackend;
|
|
use ratatui::Terminal;
|
|
use tui_orchestrator::prelude::*;
|
|
|
|
struct MyApp {
|
|
orch: Orchestrator<...>,
|
|
terminal: Terminal<CrosstermBackend<std::io::Stdout>>,
|
|
}
|
|
|
|
impl MyApp {
|
|
fn render(&mut self) -> Result<()> {
|
|
self.terminal.draw(|f| {
|
|
let focus = self.orch.focus().query();
|
|
let page = self.orch.current_page().unwrap();
|
|
|
|
// Render page with focus context
|
|
page.render(f, &focus);
|
|
})?;
|
|
}
|
|
|
|
fn run(&mut self) -> Result<()> {
|
|
loop {
|
|
let key = self.orch.read_key()?;
|
|
let events = self.orch.process_frame(key)?;
|
|
|
|
for event in events {
|
|
self.handle_event(event)?;
|
|
}
|
|
|
|
self.render()?;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Components
|
|
|
|
### Unit Tests
|
|
|
|
Test component logic in isolation:
|
|
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_login_button_action() {
|
|
let mut page = LoginPage::new();
|
|
let focus = LoginFocus::LoginButton;
|
|
let action = ComponentAction::Select;
|
|
|
|
let event = page.handle(&focus, action).unwrap();
|
|
assert!(matches!(event, Some(LoginEvent::AttemptLogin { .. })));
|
|
}
|
|
}
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
Test with orchestrator:
|
|
|
|
```rust
|
|
#[test]
|
|
fn test_full_login_flow() {
|
|
let mut orch = Orchestrator::new();
|
|
orch.register_page("login", LoginPage::new()).unwrap();
|
|
|
|
// Simulate tab navigation
|
|
let _ = orch.process_frame(Key::tab()).unwrap();
|
|
assert_eq!(orch.focus().current(), Some(&LoginFocus::Password));
|
|
|
|
// Simulate typing
|
|
let _ = orch.process_frame(Key::char('p')).unwrap();
|
|
let _ = orch.process_frame(Key::char('a')).unwrap();
|
|
let _ = orch.process_frame(Key::char('s')).unwrap();
|
|
let _ = orch.process_frame(Key::char('s')).unwrap();
|
|
|
|
// Simulate enter
|
|
let events = orch.process_frame(Key::enter()).unwrap();
|
|
assert_eq!(events.len(), 1);
|
|
assert!(matches!(events[0], LoginEvent::AttemptLogin { .. }));
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Migration from Existing Code
|
|
|
|
### Migrating from Manual Wiring
|
|
|
|
**Before:**
|
|
```rust
|
|
// Manual setup
|
|
let mut focus = FocusManager::new();
|
|
let mut bindings = Bindings::new();
|
|
let mut router = Router::new();
|
|
let mut page = LoginPage::new();
|
|
|
|
focus.set_targets(page.targets());
|
|
bindings.bind(Key::tab(), MyAction::Next);
|
|
|
|
// Manual loop
|
|
loop {
|
|
let key = read_key()?;
|
|
if let Some(action) = bindings.handle(key) {
|
|
match action {
|
|
MyAction::Next => focus.next(),
|
|
MyAction::Select => {
|
|
let focused = focus.current()?;
|
|
page.handle_button(focused)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**After:**
|
|
```rust
|
|
// Framework setup
|
|
let mut orch = Orchestrator::builder()
|
|
.with_page("login", LoginPage::new())
|
|
.build()?;
|
|
|
|
orch.run()?;
|
|
```
|
|
|
|
### Keeping Custom Behavior
|
|
|
|
If your existing code has custom behavior (like komp_ac's mode resolution), use extension points:
|
|
|
|
```rust
|
|
let mut orch = Orchestrator::new()
|
|
.with_mode_resolver(CustomModeResolver::new(state))
|
|
.with_overlay_manager(CustomOverlayManager::new())
|
|
.with_event_handler(CustomEventHandler::new(router));
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### 1. Keep Components Focused
|
|
|
|
Components should handle their own logic only. Don't directly manipulate other components.
|
|
|
|
### 2. Use Events for Communication
|
|
|
|
Components should emit events, not directly call methods on other components.
|
|
|
|
### 3. Respect Optional Methods
|
|
|
|
Only override lifecycle hooks when you need them. Default implementations are fine for most cases.
|
|
|
|
### 4. Test Component Isolation
|
|
|
|
Test components without orchestrator to ensure logic is correct.
|
|
|
|
### 5. Leverage Default Bindings
|
|
|
|
Use `with_default_bindings()` unless you have specific keybinding requirements.
|
|
|
|
### 6. Use Extension Points Wisely
|
|
|
|
Only implement custom resolvers/handlers when default behavior doesn't meet your needs.
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
The TUI Orchestrator framework provides:
|
|
|
|
1. **Zero boilerplate** - Define components, library handles the rest
|
|
2. **Sensible defaults** - Works without configuration
|
|
3. **Full extension** - Customize via traits when needed
|
|
4. **Backend-agnostic** - Works with any rendering library
|
|
5. **no_std compatible** - Runs on embedded systems and WASM
|
|
|
|
Your job: define components with buttons and logic. Our job: make it just work.
|