generic library now instead of string based
This commit is contained in:
397
README.md
397
README.md
@@ -1,321 +1,142 @@
|
||||
# TUI Orchestrator
|
||||
WARNING this library is purely GLM4.7/Opus4.5 generated.
|
||||
Its based on a real production code that was not yet decoupled into a library.
|
||||
This library is core concept extracted for no_std usage.
|
||||
For more info visit: https://gitlab.com/filipriec/komp_ac_client
|
||||
# pages-tui
|
||||
|
||||
A complete, **ready-to-use TUI framework** that handles input routing, focus management, page navigation, and lifecycle hooks—so you can define your pages, buttons, and logic, and it just works.
|
||||
Type-safe TUI page routing with single-generic orchestration.
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero boilerplate** - Define components, library handles everything else
|
||||
- **Ready to use** - Register pages and run, no manual wiring needed
|
||||
- **Sensible defaults** - Works without configuration
|
||||
- **Fully extendable** - Customize via traits when needed
|
||||
- **no_std compatible** - Works on embedded systems and WebAssembly
|
||||
- **Backend-agnostic** - No crossterm/ratatui dependencies
|
||||
- **Zero unsafe** - Pure Rust, no unsafe code
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| (none) | Pure `no_std` + heapless. No allocator required. |
|
||||
| `alloc` | Enables dynamic allocation (Vec, Box, HashMap). |
|
||||
| `std` | Full std support (implies `alloc`). |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Define Your Component
|
||||
|
||||
```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),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (read keys, route to actions)
|
||||
- Focus management (next/prev navigation)
|
||||
- Page navigation (on_exit, swap, on_enter)
|
||||
- Default keybindings (Tab=Next, Enter=Select)
|
||||
- Event collection and routing
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Component
|
||||
|
||||
The main abstraction in tui_orchestrator. A component represents a page or UI section with focusable elements.
|
||||
|
||||
```rust
|
||||
pub trait Component {
|
||||
type Focus: FocusId; // What can receive focus
|
||||
type Action: Action; // What actions this handles
|
||||
type Event: Clone + Debug; // Events this component emits
|
||||
|
||||
fn targets(&self) -> &[Self::Focus];
|
||||
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>>;
|
||||
}
|
||||
```
|
||||
|
||||
**Optional methods** (all have defaults):
|
||||
- `on_enter()` - Called when component becomes active
|
||||
- `on_exit()` - Called when component becomes inactive
|
||||
- `on_focus()` - Called when a focus target gains focus
|
||||
- `on_blur()` - Called when a focus target loses focus
|
||||
- `handle_text()` - Called when character is typed
|
||||
- `can_navigate_forward/backward()` - Control focus movement
|
||||
|
||||
### Component Actions
|
||||
|
||||
Standard actions the library provides:
|
||||
|
||||
```rust
|
||||
pub enum ComponentAction {
|
||||
Next, // Tab
|
||||
Prev, // Shift+Tab
|
||||
First, // Home
|
||||
Last, // End
|
||||
Select, // Enter
|
||||
Cancel, // Esc
|
||||
TypeChar(char), // Any character
|
||||
Backspace, // Backspace
|
||||
Delete, // Delete
|
||||
Custom(usize), // User-defined
|
||||
}
|
||||
```
|
||||
|
||||
### Focus Management
|
||||
|
||||
Focus tracks which element is currently active. The library provides:
|
||||
|
||||
- `FocusManager<F>` - Generic focus tracking
|
||||
- `FocusQuery` - Read-only focus state for rendering
|
||||
- Automatic navigation (next, prev, first, last)
|
||||
|
||||
### Orchestrator
|
||||
|
||||
The complete TUI runtime that wires everything together:
|
||||
|
||||
- `Orchestrator<C>` - Main framework struct
|
||||
- `process_frame()` - Process one input frame
|
||||
- `run()` - Complete main loop
|
||||
- Extension points for custom behavior
|
||||
|
||||
---
|
||||
|
||||
## Extension Points
|
||||
|
||||
For complex applications (like komp_ac), the library provides extension points to customize behavior:
|
||||
|
||||
### ModeResolver
|
||||
|
||||
Customize how modes are resolved (dynamic vs static).
|
||||
|
||||
```rust
|
||||
impl ModeResolver for CustomResolver {
|
||||
fn resolve(&self, focus: &dyn Any) -> Vec<ModeName> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### OverlayManager
|
||||
|
||||
Customize overlay types (dialogs, command palettes, search).
|
||||
|
||||
```rust
|
||||
impl OverlayManager for CustomOverlayManager {
|
||||
fn is_active(&self) -> bool { ... }
|
||||
fn handle_input(&mut self, key: Key) -> Option<OverlayResult> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### EventHandler
|
||||
|
||||
Customize how events are routed to handlers.
|
||||
|
||||
```rust
|
||||
impl EventHandler<AppEvent> for CustomHandler {
|
||||
fn handle(&mut self, event: AppEvent) -> Result<HandleResult> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example: Multi-Page App
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
enum MyPage {
|
||||
Login(LoginPage),
|
||||
Home(HomePage),
|
||||
Settings(SettingsPage),
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut orch = Orchestrator::builder()
|
||||
.with_page("login", LoginPage::new())
|
||||
.with_page("home", HomePage::new())
|
||||
.with_page("settings", SettingsPage::new())
|
||||
.with_default_bindings()
|
||||
.build()?;
|
||||
|
||||
orch.navigate_to("login")?;
|
||||
|
||||
orch.run()?;
|
||||
}
|
||||
```
|
||||
|
||||
Navigation with history:
|
||||
```rust
|
||||
orch.navigate_to("home")?;
|
||||
orch.navigate_to("settings")?;
|
||||
orch.back()? // Return to home
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature Flags
|
||||
### Default: Pure `no_std` Heapless
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tui_orchestrator = { version = "0.1", features = ["std"] }
|
||||
|
||||
# Optional features
|
||||
sequences = ["alloc"] # Enable multi-key sequences
|
||||
pages-tui = "0.2"
|
||||
```
|
||||
|
||||
- `default` - No features (pure no_std)
|
||||
- `std` - Enable std library support
|
||||
- `alloc` - Enable alloc support (needed for collections)
|
||||
No allocator needed! Uses `heapless` collections with const generic capacities.
|
||||
|
||||
---
|
||||
### With Allocation
|
||||
|
||||
## Design Philosophy
|
||||
```toml
|
||||
[dependencies]
|
||||
pages-tui = { version = "0.2", features = ["alloc"] }
|
||||
```
|
||||
|
||||
1. **Plugin-play model** - Library is runtime, components are plugins
|
||||
2. **Sensible defaults** - Zero configuration works
|
||||
3. **Optional everything** - Define only what you need
|
||||
4. **Extension points** - Override defaults when needed
|
||||
5. **User-focused** - "register page" not "bind chord to registry"
|
||||
6. **no_std first** - Works on embedded, opt-in std
|
||||
Uses `Vec`, `Box`, `HashMap` for dynamic sizing and trait objects.
|
||||
|
||||
---
|
||||
### With Full Std
|
||||
|
||||
## For komp_ac Integration
|
||||
```toml
|
||||
[dependencies]
|
||||
pages-tui = { version = "0.2", features = ["std"] }
|
||||
```
|
||||
|
||||
komp_ac can:
|
||||
1. Implement `Component` trait for all pages
|
||||
2. Use library's `Orchestrator` as runtime
|
||||
3. Extend with custom `ModeResolver` for dynamic Canvas-style modes
|
||||
4. Extend with custom `OverlayManager` for command palette, find file, search
|
||||
5. Extend with custom `EventHandler` for page/global/canvas routing
|
||||
## Usage
|
||||
|
||||
**Result:** komp_ac uses library's core while keeping all custom behavior.
|
||||
Define your page as an enum that implements the `Page` trait:
|
||||
|
||||
See [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md) for details.
|
||||
```rust
|
||||
use pages_tui::prelude::*;
|
||||
|
||||
---
|
||||
#[derive(Debug, Clone)]
|
||||
enum MyPage {
|
||||
Home { counter: i32 },
|
||||
Settings { dark_mode: bool },
|
||||
}
|
||||
|
||||
## Migration Guide
|
||||
impl Page for MyPage {
|
||||
type Focus = MyFocus;
|
||||
type Action = MyAction;
|
||||
type Event = MyEvent;
|
||||
|
||||
If you're migrating from a TUI built with manual wiring:
|
||||
fn targets(&self) -> &[Self::Focus] {
|
||||
match self {
|
||||
MyPage::Home { .. } => &[MyFocus::Button(0)],
|
||||
MyPage::Settings { .. } => &[MyFocus::Toggle],
|
||||
}
|
||||
}
|
||||
|
||||
1. **Identify components** - What are your pages/sections?
|
||||
2. **Implement Component trait** - `targets()`, `handle()`, optional hooks
|
||||
3. **Remove manual orchestration** - Delete manual focus/binding/router setup
|
||||
4. **Use Orchestrator** - Register pages and run
|
||||
5. **Add extensions if needed** - ModeResolver, OverlayManager, EventHandler
|
||||
fn handle(&mut self, focus: &Self::Focus, action: Self::Action)
|
||||
-> Result<Option<Self::Event>, ComponentError>
|
||||
{
|
||||
// Handle actions...
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
The library handles everything else.
|
||||
// Create orchestrator with single generic
|
||||
let mut app: Orchestrator<MyPage> = Orchestrator::new();
|
||||
|
||||
---
|
||||
// Register pages
|
||||
app.register(MyPage::Home { counter: 0 });
|
||||
app.register(MyPage::Settings { dark_mode: false });
|
||||
|
||||
## Examples
|
||||
// Navigate (associated data is ignored for lookup)
|
||||
app.navigate_to(MyPage::Home { counter: 999 }).unwrap();
|
||||
```
|
||||
|
||||
See `examples/` directory for complete working applications:
|
||||
- `simple_app.rs` - Basic multi-page TUI
|
||||
- `form_app.rs` - Form with text input
|
||||
- `extended_app.rs` - Using extension points
|
||||
## Capacity Configuration (no_std)
|
||||
|
||||
---
|
||||
In `no_std` mode without `alloc`, configure capacities via const generics:
|
||||
|
||||
## Documentation
|
||||
```rust
|
||||
// Orchestrator<Page, PAGES, HISTORY, FOCUS, BINDINGS, MODES, EVENTS>
|
||||
let mut app: Orchestrator<MyPage, 8, 16, 16, 32, 8, 8> = Orchestrator::new();
|
||||
```
|
||||
|
||||
- [PLAN.md](PLAN.md) - Complete implementation plan
|
||||
- [REDESIGN.md](REDESIGN.md) - Framework architecture deep dive
|
||||
- [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md) - Integration examples and patterns
|
||||
| Generic | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `PAGES` | 8 | Maximum registered pages |
|
||||
| `HISTORY` | 16 | Navigation history depth |
|
||||
| `FOCUS` | 16 | Focus targets per page |
|
||||
| `BINDINGS` | 32 | Key bindings |
|
||||
| `MODES` | 8 | Mode stack depth |
|
||||
| `EVENTS` | 8 | Pending event buffer |
|
||||
|
||||
---
|
||||
With `alloc`, these limits don't apply.
|
||||
|
||||
## API Differences
|
||||
|
||||
### `process_frame` Return Type
|
||||
|
||||
```rust
|
||||
// With alloc: returns Vec<Event>
|
||||
let events: Vec<MyEvent> = app.process_frame(key)?;
|
||||
|
||||
// Without alloc: returns Option<Event>
|
||||
let event: Option<MyEvent> = app.process_frame(key)?;
|
||||
```
|
||||
|
||||
### EventBus
|
||||
|
||||
```rust
|
||||
// With alloc: register handlers via Box<dyn EventHandler>
|
||||
app.event_bus_mut().register(Box::new(my_handler));
|
||||
|
||||
// Without alloc: poll pending events
|
||||
for event in app.event_bus_mut().drain() {
|
||||
// handle event
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Handlers (alloc only)
|
||||
|
||||
```rust
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
app.set_action_resolver(MyResolver);
|
||||
app.set_command_handler(MyHandler);
|
||||
app.set_state_coordinator(MyCoordinator);
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Reference in New Issue
Block a user