Files
pages-tui/INTEGRATION_GUIDE.md

14 KiB

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:

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

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

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).

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.

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

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

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

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

// 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

#[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:

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):

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):

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

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:

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:

#[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:

#[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:

// 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:

// 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:

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.