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

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

  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