Recreate repository due to Git object corruption (all files preserved)
This commit is contained in:
582
INTEGRATION_GUIDE.md
Normal file
582
INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,582 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user