Files
pages-tui/REDESIGN.md

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.