Recreate repository due to Git object corruption (all files preserved)

This commit is contained in:
filipriec_vm
2026-01-11 09:53:37 +01:00
commit 35b9e8e5a8
54 changed files with 4803 additions and 0 deletions

582
INTEGRATION_GUIDE.md Normal file
View 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.