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)
10 KiB
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
[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
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
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
#![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
cargo test --lib
Architecture Patterns
R-STATE-SEPARATION
Principle: Complete separation of state from rendering logic.
// ✅ 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.
// 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.
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.
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.
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:
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:
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:
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:
#[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 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:
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:
ratatui = { version = "0.30.0-beta.0", features = ["all-widgets", "underline-color"] }
Issue: Terminal not being cleaned up properly
Solution: Use TerminalGuard:
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:
// 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
- Use dirty flag to avoid unnecessary redraws
- Limit event polling with reasonable tick rates (100-200ms)
- Cache theme instead of creating new Theme each render
- Use Constraint::Percentage for responsive layouts instead of hardcoded sizes
- 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
- Check existing tool implementations in
/Users/Akasha/Tools/*/crates/*-tui/ - Review
tools-tui-sharedsource code for patterns - Run tests to validate your implementation
- Check compilation with
cargo check - Lint with
cargo clippy --all-targets --all-features