generic library now instead of string based

This commit is contained in:
Priec
2026-01-19 15:58:44 +01:00
parent feb22d270c
commit f30a2a2758
16 changed files with 1068 additions and 670 deletions

397
README.md
View File

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