syntaxis/shared/rust-tui/INTEGRATION_GUIDE.md
Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
Merge _configs/ into config/ for single configuration directory.
Update all path references.

Changes:
- Move _configs/* to config/
- Update .gitignore for new patterns
- No code references to _configs/ found

Impact: -1 root directory (layout_conventions.md compliance)
2025-12-26 18:36:23 +00:00

429 lines
10 KiB
Markdown

# TUI Integration Guide
Complete guide for integrating RataTui-based TUI into any Tools ecosystem project.
## Overview
The `tools-tui-shared` library provides a production-ready foundation for building terminal user interfaces (TUIs) across the Tools ecosystem. All TUI implementations follow strict architectural patterns and code quality standards.
## Quick Start: 5 Steps
### Step 1: Add Dependency
```toml
[dependencies]
tools-tui-shared = { path = "../../../shared/rust-tui" }
ratatui = { version = "0.30.0-beta.0", features = ["all-widgets"] }
crossterm = "0.29"
tui-textarea = "0.7"
```
### Step 2: Create App State
```rust
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScreenType {
List,
Detail,
}
#[derive(Clone, Debug)]
pub struct AppState {
pub current_screen: ScreenType,
pub items: Vec<YourType>,
pub selected_index: usize,
pub dirty: bool,
pub should_quit: bool,
}
impl AppState {
pub fn new() -> Self {
AppState {
current_screen: ScreenType::List,
items: Vec::new(),
selected_index: 0,
dirty: true,
should_quit: false,
}
}
}
```
### Step 3: Implement Screens
```rust
use crate::app::AppState;
use tools_tui_shared::prelude::*;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, List, ListItem};
pub struct ListScreen;
impl ListScreen {
pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) {
let theme = Theme::dark();
let items: Vec<ListItem> = state.items.iter()
.enumerate()
.map(|(idx, item)| {
let marker = if idx == state.selected_index { "★ " } else { " " };
ListItem::new(format!("{}{}", marker, item.name))
})
.collect();
let list = List::new(items)
.block(Block::default()
.title("Items")
.borders(Borders::ALL)
.border_style(theme.focused_style()))
.style(theme.normal_style());
frame.render_widget(list, area);
}
}
```
### Step 4: Create lib.rs
```rust
#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod app;
pub mod screens;
pub use app::{AppState, ScreenType};
pub mod prelude {
pub use crate::{AppState, ScreenType};
pub use crate::screens::{ListScreen, DetailScreen};
}
```
### Step 5: Run Tests
```bash
cargo test --lib
```
## Architecture Patterns
### R-STATE-SEPARATION
**Principle**: Complete separation of state from rendering logic.
```rust
// ✅ CORRECT: State methods don't render
impl AppState {
pub fn select_next(&mut self) {
if !self.items.is_empty() {
self.selected_index = (self.selected_index + 1) % self.items.len();
self.dirty = true;
}
}
}
// ✅ CORRECT: Render methods only read state
impl ListScreen {
pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) {
// Never call state.select_next() or other mutation methods
// Only read: state.selected_index, state.items, etc.
}
}
// ❌ WRONG: Don't put rendering logic in state
impl AppState {
pub fn render(&self) { } // DON'T DO THIS
}
```
### R-WIDGET-COMPOSITION
**Principle**: Build complex UIs from simple, reusable widgets.
```rust
// Use shared widgets from tools-tui-shared
use tools_tui_shared::widgets::{Menu, Form, Table, StatusBar};
// Compose widgets for your screens
let menu = Menu::new(vec![
MenuItem::new("Save".to_string()).with_key_binding("Ctrl+S"),
MenuItem::new("Exit".to_string()).with_key_binding("Ctrl+Q"),
]);
let form = Form::new()
.add_field(FormField::new("name", FieldType::Text))
.add_field(FormField::new("email", FieldType::Text));
```
### R-CONSISTENT-THEMING
**Principle**: Single theme source for all colors and styles.
```rust
let theme = Theme::dark();
// All UI elements use the same theme
let primary_style = theme.selected_style(); // Header, emphasis
let success_style = theme.success_style(); // Success messages
let warning_style = theme.warning_style(); // Warnings
let error_style = theme.error_style(); // Errors
let normal_style = theme.normal_style(); // Default text
let dimmed_style = theme.dimmed_style(); // Disabled/background
```
### R-EVENT-LOOP
**Principle**: Non-blocking event polling with timeout.
```rust
use tools_tui_shared::events::EventHandler;
let event_handler = EventHandler::new(100)?; // 100ms tick rate
loop {
if app_state.dirty {
terminal.draw(|f| render(f, &app_state))?;
app_state.reset_dirty();
}
// Non-blocking poll with timeout
match event_handler.poll()? {
AppEvent::Tick => { /* Update */},
AppEvent::Key(key) => { /* Handle key */},
AppEvent::Mouse(evt) => { /* Handle mouse */},
AppEvent::Resize(w, h) => { /* Handle resize */},
}
}
```
### R-GRACEFUL-SHUTDOWN
**Principle**: RAII pattern ensures terminal cleanup on exit.
```rust
use tools_tui_shared::terminal::TerminalGuard;
// Terminal is automatically set up and cleaned up
let _guard = TerminalGuard::setup()?;
let mut terminal = _guard.terminal()?;
// Even if panic occurs, terminal is restored
// Drop trait automatically handles cleanup
```
## Common Patterns
### Dirty Flag Optimization
Only redraw when state changes:
```rust
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn reset_dirty(&mut self) {
self.dirty = false;
}
pub fn mark_dirty(&mut self) {
self.dirty = true;
}
// In main event loop:
if app_state.is_dirty() {
terminal.draw(|f| render(f, &app_state))?;
app_state.reset_dirty();
}
```
### Selection Navigation
Implement consistent navigation:
```rust
pub fn select_next(&mut self) {
if !self.items.is_empty() {
self.selected_index = (self.selected_index + 1) % self.items.len();
self.dirty = true;
}
}
pub fn select_previous(&mut self) {
if !self.items.is_empty() {
self.selected_index = if self.selected_index == 0 {
self.items.len() - 1
} else {
self.selected_index - 1
};
self.dirty = true;
}
}
```
### Screen Navigation
Manage multiple screens:
```rust
pub fn set_screen(&mut self, screen: ScreenType) {
if self.current_screen != screen {
self.current_screen = screen;
self.dirty = true;
}
}
// In render loop:
match state.current_screen {
ScreenType::List => ListScreen::render(state, frame, area),
ScreenType::Detail => DetailScreen::render(state, frame, area),
}
```
## Testing Guidelines
Each module should have comprehensive tests:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_state_new() {
let state = AppState::new();
assert!(state.dirty);
assert!(!state.should_quit);
}
#[test]
fn test_select_next() {
let mut state = AppState::new();
state.items.push(/* ... */);
state.select_next();
assert_eq!(state.selected_index, 1);
}
#[test]
fn test_screen_navigation() {
let mut state = AppState::new();
state.set_screen(ScreenType::Detail);
assert_eq!(state.current_screen, ScreenType::Detail);
}
}
```
**Minimum test coverage**: 15+ tests per module
## Code Quality Checklist
- [ ] `#![forbid(unsafe_code)]` at crate root
- [ ] `#![warn(missing_docs)]` for public items
- [ ] All public types implement Debug
- [ ] All public functions documented with examples
- [ ] Result<T> for all fallible operations
- [ ] No unwrap() in production code
- [ ] 15+ tests per module
- [ ] cargo fmt applied
- [ ] cargo clippy passes
- [ ] cargo test --workspace passes
## Integration with Dashboard
To register your tool in the multi-tool dashboard:
```rust
use tools_tui_shared::integration::ToolSummaryProvider;
pub struct MyToolTui;
impl ToolSummaryProvider for MyToolTui {
fn name(&self) -> &str {
"my-tool"
}
fn summary(&self) -> Result<ToolSummary, String> {
Ok(ToolSummary::new(
"my-tool".to_string(),
"My Tool".to_string(),
"Description of my tool".to_string(),
ToolMetrics::new(10, 5, 3, chrono::Utc::now().to_rfc2822()),
).with_commands(vec!["list".to_string(), "create".to_string()]))
}
fn execute_command(&self, command: &str, _args: &[String]) -> Result<String, String> {
match command {
"list" => Ok("Listing items...".to_string()),
"create" => Ok("Creating item...".to_string()),
_ => Err(format!("Unknown command: {}", command)),
}
}
}
```
## Common Issues & Solutions
### Issue: Compilation errors with ratatui widgets
**Solution**: Ensure you're using ratatui 0.30.0-beta.0 or later:
```toml
ratatui = { version = "0.30.0-beta.0", features = ["all-widgets", "underline-color"] }
```
### Issue: Terminal not being cleaned up properly
**Solution**: Use TerminalGuard:
```rust
use tools_tui_shared::terminal::TerminalGuard;
let _guard = TerminalGuard::setup()?;
let mut terminal = _guard.terminal()?;
// Terminal is auto-cleaned up when _guard is dropped
```
### Issue: UI not updating
**Solution**: Check dirty flag logic:
```rust
// Always mark dirty when state changes
pub fn some_action(&mut self) {
self.items.push(new_item);
self.dirty = true; // Don't forget this!
}
// Always reset dirty after rendering
if app_state.is_dirty() {
terminal.draw(|f| render(f, &app_state))?;
app_state.reset_dirty(); // Don't forget this!
}
```
## Performance Tips
1. **Use dirty flag** to avoid unnecessary redraws
2. **Limit event polling** with reasonable tick rates (100-200ms)
3. **Cache theme** instead of creating new Theme each render
4. **Use Constraint::Percentage** for responsive layouts instead of hardcoded sizes
5. **Avoid cloning** large data structures in render methods
## References
- **RataTui Documentation**: https://ratatui.rs
- **Crossterm Documentation**: https://docs.rs/crossterm
- **Tools Shared Library**: `/Users/Akasha/Tools/shared/rust-tui/`
- **Example Implementation**: `/Users/Akasha/Tools/hello-tool/crates/hello-tui/`
## Getting Help
1. Check existing tool implementations in `/Users/Akasha/Tools/*/crates/*-tui/`
2. Review `tools-tui-shared` source code for patterns
3. Run tests to validate your implementation
4. Check compilation with `cargo check`
5. Lint with `cargo clippy --all-targets --all-features`