498 lines
12 KiB
Markdown
498 lines
12 KiB
Markdown
# TUI Orchestrator: Framework-Based Design
|
|
|
|
## Philosophy Shift
|
|
|
|
### From Building Blocks to Framework
|
|
|
|
**Old approach:** Provide individual primitives (keys, bindings, focus) that users wire together manually.
|
|
|
|
**New approach:** Provide complete TUI framework where users define components and library handles everything else.
|
|
|
|
This is a **plugin-play model**:
|
|
- Library is the runtime
|
|
- Components are plugins
|
|
- Extension points allow customization
|
|
- Everything else is optional with sensible defaults
|
|
|
|
---
|
|
|
|
## The "Ready to Use" Vision
|
|
|
|
### What Users Should Do
|
|
|
|
```rust
|
|
// 1. Define component
|
|
#[derive(Debug, Clone)]
|
|
enum LoginPage {
|
|
Username,
|
|
Password,
|
|
LoginBtn,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum LoginEvent {
|
|
AttemptLogin { username: String, password: String },
|
|
Cancel,
|
|
}
|
|
|
|
impl Component for LoginPage {
|
|
type Focus = LoginPage;
|
|
type Action = ComponentAction;
|
|
type Event = LoginEvent;
|
|
|
|
fn targets(&self) -> &[Self::Focus] { ... }
|
|
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> { ... }
|
|
}
|
|
|
|
// 2. Register and run
|
|
fn main() -> Result<()> {
|
|
let mut orch = Orchestrator::builder()
|
|
.with_page("login", LoginPage::new())
|
|
.with_default_bindings()
|
|
.build()?;
|
|
|
|
orch.run()?;
|
|
}
|
|
```
|
|
|
|
### What Library Does
|
|
|
|
**Automatically:**
|
|
- Input processing (read keys, route to actions)
|
|
- Focus management (next, prev, set, clear overlay)
|
|
- Page navigation (call on_exit, swap, call on_enter)
|
|
- Lifecycle hooks (on_focus, on_blur called at right time)
|
|
- Default bindings (Tab=Next, Enter=Select, etc.)
|
|
- Event collection and routing
|
|
|
|
**Never:**
|
|
- Forces user to write glue code
|
|
- Requires manual lifecycle management
|
|
- Makes assumptions about app structure
|
|
- Requires complex configuration
|
|
|
|
---
|
|
|
|
## Extension Model
|
|
|
|
### Three-Layer Architecture
|
|
|
|
```
|
|
Layer 1: Core Framework (Library)
|
|
├── Component trait
|
|
├── Orchestrator runtime
|
|
├── Default bindings
|
|
└── Router + lifecycle
|
|
|
|
Layer 2: Extension Points (For komp_ac)
|
|
├── ModeResolver - dynamic mode resolution
|
|
├── OverlayManager - custom overlay types
|
|
├── EventHandler - custom event routing
|
|
└── FocusNavigation - boundary detection
|
|
|
|
Layer 3: App Logic (User)
|
|
├── Page definitions
|
|
├── Business logic (gRPC, authentication)
|
|
└── Rendering
|
|
```
|
|
|
|
### Layer 1: What Library Provides
|
|
|
|
**Component trait** - The abstraction:
|
|
- `targets()` - What's focusable
|
|
- `handle()` - What happens on action
|
|
- `on_enter/on_exit` - Lifecycle hooks
|
|
- `on_focus/on_blur` - Focus lifecycle
|
|
- `handle_text()` - Optional text input
|
|
- `can_navigate_*()` - Optional boundary detection
|
|
|
|
**Orchestrator** - The runtime:
|
|
- `register_page()` - Add pages
|
|
- `navigate_to()` - Page navigation
|
|
- `process_frame()` - Process one input frame
|
|
- `run()` - Complete main loop
|
|
|
|
**Standard actions** - Common patterns:
|
|
- `Next`, `Prev`, `First`, `Last` - Navigation
|
|
- `Select`, `Cancel` - Selection
|
|
- `TypeChar`, `Backspace`, `Delete` - Text input
|
|
- `Custom(usize)` - User extension
|
|
|
|
### Layer 2: Extension Points
|
|
|
|
Each extension has a **default implementation** that works for simple apps, and a **trait** that komp_ac implements for custom behavior.
|
|
|
|
#### ModeResolver
|
|
|
|
**Default:** Static mode stack
|
|
|
|
```rust
|
|
pub struct DefaultModeResolver;
|
|
impl ModeResolver for DefaultModeResolver {
|
|
fn resolve(&self, _focus: &dyn Any) -> Vec<ModeName> {
|
|
vec![ModeName::General]
|
|
}
|
|
}
|
|
```
|
|
|
|
**komp_ac extension:** Dynamic Canvas-style mode resolution
|
|
|
|
```rust
|
|
pub struct CanvasModeResolver {
|
|
app_state: AppState,
|
|
}
|
|
|
|
impl ModeResolver for CanvasModeResolver {
|
|
fn resolve(&self, focus: &dyn Any) -> Vec<ModeName> {
|
|
// Check if focus is canvas field
|
|
// Get editor mode (Edit/ReadOnly)
|
|
// Return mode stack: [EditorMode, Common, Global]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use case:** Simple app doesn't care about modes. komp_ac needs dynamic resolution based on editor state.
|
|
|
|
#### OverlayManager
|
|
|
|
**Default:** Simple dialog/input overlay
|
|
|
|
```rust
|
|
pub struct DefaultOverlayManager {
|
|
stack: Vec<Overlay>,
|
|
}
|
|
|
|
impl OverlayManager for DefaultOverlayManager {
|
|
fn handle_input(&mut self, key: Key) -> Option<OverlayResult> { ... }
|
|
}
|
|
```
|
|
|
|
**komp_ac extension:** Complex overlay types (command palette, find file, search palette)
|
|
|
|
```rust
|
|
pub struct KompAcOverlayManager {
|
|
command_bar: CommandBar,
|
|
find_file: FindFilePalette,
|
|
search: SearchPalette,
|
|
}
|
|
|
|
impl OverlayManager for KompAcOverlayManager {
|
|
fn handle_input(&mut self, key: Key) -> Option<OverlayResult> {
|
|
// Route to appropriate overlay
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use case:** Simple app uses built-in dialogs. komp_ac needs custom overlays that integrate with editor, gRPC, etc.
|
|
|
|
#### EventHandler
|
|
|
|
**Default:** Return events to user
|
|
|
|
```rust
|
|
pub struct DefaultEventHandler<E>;
|
|
|
|
impl<E> EventHandler for DefaultEventHandler<E> {
|
|
fn handle(&mut self, event: E) -> Result<HandleResult> {
|
|
// Just pass events back to user
|
|
Ok(HandleResult::Consumed)
|
|
}
|
|
}
|
|
```
|
|
|
|
**komp_ac extension:** Route to page/global/canvas handlers
|
|
|
|
```rust
|
|
pub struct KompAcEventHandler {
|
|
router: Router,
|
|
focus: FocusManager,
|
|
canvas_handlers: HashMap<Page, Box<dyn CanvasHandler>>,
|
|
}
|
|
|
|
impl EventHandler for KompAcEventHandler {
|
|
fn handle(&mut self, event: AppEvent) -> Result<HandleResult> {
|
|
match self.focus.current() {
|
|
Some(FocusTarget::CanvasField(_)) => self.canvas_handler.handle(event),
|
|
_ => self.page_handler.handle(event),
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use case:** Simple app just processes events. komp_ac needs complex routing based on focus type and context.
|
|
|
|
### Layer 3: App Logic
|
|
|
|
**This is entirely user-defined:**
|
|
- Page structs/enums
|
|
- Business logic
|
|
- API calls (gRPC, HTTP)
|
|
- State management
|
|
- Rendering
|
|
|
|
The library never touches this.
|
|
|
|
---
|
|
|
|
## Key Design Decisions
|
|
|
|
### 1. Associated Types vs Generics
|
|
|
|
**Choice:** Component trait uses associated types
|
|
|
|
```rust
|
|
pub trait Component {
|
|
type Focus: FocusId;
|
|
type Action: Action;
|
|
type Event: Clone + Debug;
|
|
}
|
|
```
|
|
|
|
**Why:**
|
|
- One component = one configuration
|
|
- Type system ensures consistency
|
|
- Cleaner trait signature
|
|
|
|
**Alternative:** Generics `Component<F, A, E>`
|
|
|
|
**Why not:**
|
|
- More verbose
|
|
- Type inference harder
|
|
- Less "component feels like a thing"
|
|
|
|
### 2. Automatic vs Explicit Navigation
|
|
|
|
**Choice:** Library automatically moves focus on Next/Prev actions
|
|
|
|
**Why:**
|
|
- Reduces boilerplate
|
|
- Consistent behavior across apps
|
|
- Component only needs to know "button was pressed"
|
|
|
|
**Alternative:** Library passes Next/Prev action, component decides what to do
|
|
|
|
**Why not:**
|
|
- Every component implements same logic
|
|
- Easy to miss patterns
|
|
- Library already has FocusManager—use it
|
|
|
|
**Escape hatch:** Components can override with `can_navigate_forward/backward()`
|
|
|
|
### 3. Event Model
|
|
|
|
**Choice:** Components return `Option<Event>`, library collects and returns
|
|
|
|
**Why:**
|
|
- Library can handle internal events (focus changes, page nav)
|
|
- Users get clean list of events to process
|
|
- Decouples component from application
|
|
|
|
**Alternative:** Components emit events directly to channel/bus
|
|
|
|
**Why not:**
|
|
- Requires async or channels
|
|
- More complex setup
|
|
- Library can't orchestrate internal events
|
|
|
|
### 4. Page vs Component
|
|
|
|
**Choice:** Library doesn't distinguish—everything is a Component
|
|
|
|
**Why:**
|
|
- Simpler API
|
|
- User can nest components if needed
|
|
- Flat hierarchy, easy to understand
|
|
|
|
**Alternative:** Library has `Page` and `Component` concepts
|
|
|
|
**Why not:**
|
|
- Forces app structure
|
|
- Some apps don't have pages
|
|
- More concepts to learn
|
|
|
|
### 5. Extension Points
|
|
|
|
**Choice:** Extension points are trait objects (`Box<dyn Trait> + 'static`)
|
|
|
|
**Why:**
|
|
- Allows komp_ac to pass stateful resolvers
|
|
- Flexible at runtime
|
|
- Can be swapped dynamically
|
|
|
|
**Alternative:** Generic with bounds (`<R: ModeResolver + Sized>`)
|
|
|
|
**Why not:**
|
|
- Monomorphization bloat
|
|
- Can't store different implementations
|
|
- Less flexible
|
|
|
|
---
|
|
|
|
## Comparison: Building Blocks vs Framework
|
|
|
|
### Building Blocks (Old Design)
|
|
|
|
**What user writes:**
|
|
|
|
```rust
|
|
// Setup
|
|
let mut focus = FocusManager::new();
|
|
let mut bindings = Bindings::new();
|
|
let mut router = Router::new();
|
|
|
|
// Configuration
|
|
bindings.bind(Key::tab(), MyAction::Next);
|
|
bindings.bind(Key::enter(), MyAction::Select);
|
|
focus.set_targets(page.targets());
|
|
router.navigate(Page::Login);
|
|
|
|
// Main 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()?;
|
|
let result = page.handle_button(focused)?;
|
|
// Handle result...
|
|
}
|
|
}
|
|
}
|
|
|
|
render(&focus, &router)?;
|
|
}
|
|
```
|
|
|
|
**Problems:**
|
|
- Tons of boilerplate
|
|
- User must understand all systems
|
|
- Easy to miss lifecycle (forgot to call on_exit?)
|
|
- Manual wiring everywhere
|
|
- Every app reinvents same code
|
|
|
|
### Framework (New Design)
|
|
|
|
**What user writes:**
|
|
|
|
```rust
|
|
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::builder()
|
|
.with_page("login", LoginPage::new())
|
|
.build()?;
|
|
|
|
orch.run()?;
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Zero boilerplate
|
|
- Library handles everything
|
|
- Lifecycle automatic
|
|
- Consistent behavior
|
|
- Easy to reason about
|
|
|
|
---
|
|
|
|
## Extension Strategy for komp_ac
|
|
|
|
### What komp_ac Keeps
|
|
|
|
komp_ac continues to own:
|
|
- All page state and logic
|
|
- gRPC client and authentication
|
|
- Rendering with ratatui
|
|
- Canvas editor integration
|
|
- Command palette logic
|
|
- Find file palette logic
|
|
- Business rules
|
|
|
|
### What komp_ac Replaces
|
|
|
|
komp_ac removes:
|
|
- `InputOrchestrator` - Uses library's `Orchestrator`
|
|
- `ActionDecider` routing logic - Uses library's event handler
|
|
- Manual lifecycle calls - Uses library's automatic hooks
|
|
- Mode stack assembly - Uses library's `ModeResolver` extension
|
|
- Overlay management - Uses library's `OverlayManager` extension
|
|
|
|
### Integration Pattern
|
|
|
|
komp_ac implements `Component` trait for each page:
|
|
|
|
```rust
|
|
impl Component for LoginPage {
|
|
type Focus = FocusTarget;
|
|
type Action = ResolvedAction;
|
|
type Event = AppEvent;
|
|
|
|
fn targets(&self) -> &[Self::Focus] {
|
|
// Return existing focus targets
|
|
&[FocusTarget::CanvasField(0), FocusTarget::Button(0), ...]
|
|
}
|
|
|
|
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> {
|
|
// Return existing app events
|
|
match (focus, action) {
|
|
(FocusTarget::Button(0), ResolvedAction::Keybind(KeybindAction::Save)) => {
|
|
Ok(Some(AppEvent::FormSave { path: self.path.clone() }))
|
|
}
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
komp_ac uses extension points:
|
|
|
|
```rust
|
|
let mut orch = Orchestrator::new()
|
|
.with_mode_resolver(CanvasModeResolver::new(app_state))
|
|
.with_overlay_manager(KompAcOverlayManager::new())
|
|
.with_event_handler(KompAcEventHandler::new(router, focus));
|
|
```
|
|
|
|
**Result:** komp_ac uses library's core while keeping all custom behavior.
|
|
|
|
---
|
|
|
|
## Future-Proofing
|
|
|
|
### What Can Be Added Without Breaking Changes
|
|
|
|
1. **Additional lifecycle hooks:** Add new methods to `Component` trait with default impls
|
|
2. **More actions:** Add variants to `ComponentAction` enum
|
|
3. **New overlay types:** Implement `OverlayManager` trait
|
|
4. **Custom input sources:** Implement `InputSource` trait
|
|
5. **Animation support:** Add hooks for frame updates
|
|
6. **Accessibility:** Add hooks for screen readers
|
|
|
|
### What Requires Breaking Changes
|
|
|
|
1. **Component trait signature:** Changing associated types
|
|
2. **Orchestrator API:** Major method signature changes
|
|
3. **Extension point contracts:** Changing trait methods
|
|
|
|
**Strategy:** Mark APIs as `#[doc(hidden)]` or `#[deprecated]` before removing.
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
The redesigned TUI Orchestrator is:
|
|
|
|
1. **Complete framework** - Not just building blocks
|
|
2. **Zero boilerplate** - Users define components, library runs show
|
|
3. **Sensible defaults** - Works without configuration
|
|
4. **Fully extendable** - Trait-based extension points
|
|
5. **komp_ac compatible** - Can replace existing orchestration
|
|
6. **User-focused** - "register page" not "bind chord to registry"
|
|
|
|
The library becomes a **TUI runtime** where users write application logic and library handles everything else.
|