# TUI Integration Example Walkthrough Complete step-by-step guide for creating a new tool TUI from scratch using templates. ## Overview This walkthrough shows how to create a TUI for an existing tool. We'll use a hypothetical `note-manager` tool as the example, but the same steps apply to any tool. ## Step 1: Project Structure Setup Create the following directory structure within your tool's workspace: ``` note-manager/ ├── crates/ │ ├── note-manager-core/ # Your existing core library │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── lib.rs │ │ ├── types.rs │ │ └── handlers/ │ │ │ └── note-manager-tui/ # NEW: TUI crate (create this) │ ├── Cargo.toml │ ├── src/ │ │ ├── lib.rs │ │ ├── app.rs │ │ ├── screens/ │ │ │ ├── mod.rs │ │ │ ├── list.rs │ │ │ ├── create.rs │ │ │ └── detail.rs │ │ └── integration.rs │ └── tests/ │ └── integration_tests.rs │ ├── Cargo.toml (workspace root) └── Cargo.lock ``` ## Step 2: Create Cargo.toml for TUI Crate Create `crates/note-manager-tui/Cargo.toml`: ```toml [package] name = "note-manager-tui" version = "0.1.0" edition = "2021" [dependencies] note-manager-core = { path = "../note-manager-core" } tools-tui-shared = { path = "../../../shared/rust-tui" } ratatui = { version = "0.30.0-beta.0", features = ["all-widgets"] } crossterm = "0.29" serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.40", features = ["full"] } [lib] path = "src/lib.rs" [dev-dependencies] tempfile = "3.12" ``` Update workspace root `Cargo.toml` to include the new crate: ```toml [workspace] members = [ "crates/note-manager-core", "crates/note-manager-tui", # Add this line ] ``` ## Step 3: Implement Application State (app.rs) Create `crates/note-manager-tui/src/app.rs` using the template: ```rust //! Application state for Note Manager TUI use serde::{Deserialize, Serialize}; use std::fmt; /// Screen types - customize based on your tool's screens #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum ScreenType { /// List screen - show all notes List, /// Create screen - create new note Create, /// Detail screen - show note details Detail, } impl fmt::Display for ScreenType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ScreenType::List => write!(f, "List"), ScreenType::Create => write!(f, "Create"), ScreenType::Detail => write!(f, "Detail"), } } } /// Application state - manages all mutable state /// /// # Design Pattern: R-STATE-SEPARATION /// All state mutations go through explicit methods. Rendering functions /// only read state (take `&AppState`, never `&mut AppState`). #[derive(Clone, Debug)] pub struct AppState { /// Current screen (navigation state) pub current_screen: ScreenType, /// Notes collection pub notes: Vec, /// Currently selected note index pub selected_index: usize, /// Whether state changed (needs redraw) pub dirty: bool, /// Whether application should quit pub should_quit: bool, /// Form field: note title pub form_title: String, /// Form field: note content pub form_content: String, } impl AppState { /// Create new application state pub fn new() -> Self { AppState { current_screen: ScreenType::List, notes: Vec::new(), selected_index: 0, dirty: true, should_quit: false, form_title: String::new(), form_content: String::new(), } } // === Note Management === /// Add a new note pub fn add_note(&mut self, title: String) { self.notes.push(title); self.dirty = true; } /// Get currently selected note pub fn selected_note(&self) -> Option<&String> { self.notes.get(self.selected_index) } // === Navigation === /// Select next note in list pub fn select_next(&mut self) { if !self.notes.is_empty() { self.selected_index = (self.selected_index + 1) % self.notes.len(); self.dirty = true; } } /// Select previous note in list pub fn select_previous(&mut self) { if !self.notes.is_empty() { self.selected_index = if self.selected_index == 0 { self.notes.len() - 1 } else { self.selected_index - 1 }; self.dirty = true; } } /// Switch to a different screen pub fn set_screen(&mut self, screen: ScreenType) { if self.current_screen != screen { self.current_screen = screen; self.dirty = true; } } // === Form Management === /// Reset form fields to empty pub fn reset_form(&mut self) { self.form_title.clear(); self.form_content.clear(); self.dirty = true; } // === Lifecycle === /// Request application quit pub fn request_quit(&mut self) { self.should_quit = true; } /// Check if state has changed since last render pub fn is_dirty(&self) -> bool { self.dirty } /// Mark state as rendered (clear dirty flag) pub fn reset_dirty(&mut self) { self.dirty = false; } /// Check if should quit pub fn should_quit(&self) -> bool { self.should_quit } } impl Default for AppState { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_app_state_new() { let state = AppState::new(); assert_eq!(state.current_screen, ScreenType::List); assert!(state.notes.is_empty()); assert!(state.dirty); assert!(!state.should_quit); } #[test] fn test_add_note() { let mut state = AppState::new(); state.add_note("Test Note".to_string()); assert_eq!(state.notes.len(), 1); assert!(state.dirty); } #[test] fn test_select_next() { let mut state = AppState::new(); state.add_note("Note 1".to_string()); state.add_note("Note 2".to_string()); state.reset_dirty(); state.select_next(); assert_eq!(state.selected_index, 1); assert!(state.dirty); } #[test] fn test_set_screen() { let mut state = AppState::new(); state.reset_dirty(); state.set_screen(ScreenType::Create); assert_eq!(state.current_screen, ScreenType::Create); assert!(state.dirty); } #[test] fn test_reset_form() { let mut state = AppState::new(); state.form_title = "Test".to_string(); state.form_content = "Content".to_string(); state.reset_dirty(); state.reset_form(); assert!(state.form_title.is_empty()); assert!(state.form_content.is_empty()); assert!(state.dirty); } } ``` ## Step 4: Implement Screens Create `crates/note-manager-tui/src/screens/list.rs`: ```rust //! List screen - shows all notes use crate::app::AppState; use ratatui::prelude::*; use ratatui::widgets::{Block, Borders, List, ListItem}; use tools_tui_shared::prelude::*; /// List screen - display and navigate notes pub struct ListScreen; impl ListScreen { /// Render the list screen pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) { let theme = Theme::dark(); // Handle empty state if state.notes.is_empty() { let empty_msg = ratatui::widgets::Paragraph::new("No notes yet. Create one to get started.") .style(theme.dimmed_style()) .alignment(ratatui::layout::Alignment::Center); frame.render_widget(empty_msg, area); return; } // Create list items from state let items: Vec = state .notes .iter() .enumerate() .map(|(idx, title)| { let marker = if idx == state.selected_index { "★ " } else { " " }; ListItem::new(format!("{}{}", marker, title)) }) .collect(); // Build list widget let list = List::new(items) .block( Block::default() .title("Notes") .borders(Borders::ALL) .border_style(theme.focused_style()), ) .style(theme.normal_style()) .row_highlight_style(theme.selected_style()); // Render to frame frame.render_widget(list, area); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_list_screen_empty() { let state = AppState::new(); assert!(state.notes.is_empty()); } #[test] fn test_list_screen_with_notes() { let mut state = AppState::new(); state.add_note("Note 1".to_string()); state.add_note("Note 2".to_string()); assert_eq!(state.notes.len(), 2); } } ``` Create `crates/note-manager-tui/src/screens/create.rs`: ```rust //! Create screen - form for creating new notes use crate::app::AppState; use ratatui::prelude::*; use ratatui::widgets::{Block, Borders, Paragraph}; use tools_tui_shared::prelude::*; /// Create screen - form for creating new notes pub struct CreateScreen; impl CreateScreen { /// Render the create screen pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) { let theme = Theme::dark(); // Split area into form and help sections let chunks = Layout::default() .direction(ratatui::layout::Direction::Vertical) .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) .split(area); // Form section let form_text = vec![ Line::from(vec![ Span::styled("Title: ", theme.focused_style()), Span::raw(if state.form_title.is_empty() { "".to_string() } else { state.form_title.clone() }), ]), Line::from(""), Line::from(vec![ Span::styled("Content: ", theme.focused_style()), Span::raw(if state.form_content.is_empty() { "".to_string() } else { state.form_content.clone() }), ]), ]; let form = Paragraph::new(form_text) .block( Block::default() .title("Create Note") .borders(Borders::ALL) .border_style(theme.focused_style()), ) .style(theme.normal_style()); frame.render_widget(form, chunks[0]); // Help section let help_text = vec![Line::from(vec![ Span::styled("Tab", theme.selected_style()), Span::raw(" - Next field | "), Span::styled("Enter", theme.selected_style()), Span::raw(" - Create | "), Span::styled("Esc", theme.selected_style()), Span::raw(" - Cancel"), ])]; let help = Paragraph::new(help_text) .block(Block::default().title("Help").borders(Borders::ALL)) .style(theme.dimmed_style()); frame.render_widget(help, chunks[1]); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_create_screen_empty_form() { let state = AppState::new(); assert!(state.form_title.is_empty()); assert!(state.form_content.is_empty()); } } ``` Create `crates/note-manager-tui/src/screens/detail.rs`: ```rust //! Detail screen - shows note details use crate::app::AppState; use ratatui::prelude::*; use ratatui::widgets::{Block, Borders, Paragraph}; use tools_tui_shared::prelude::*; /// Detail screen - show note details pub struct DetailScreen; impl DetailScreen { /// Render the detail screen pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) { let theme = Theme::dark(); if let Some(note) = state.selected_note() { let detail_text = vec![ Line::from(vec![ Span::styled("Note: ", theme.selected_style()), Span::raw(note), ]), Line::from(""), Line::from(vec![ Span::styled("Status: ", theme.selected_style()), Span::raw("Active"), ]), ]; let detail = Paragraph::new(detail_text) .block( Block::default() .title("Details") .borders(Borders::ALL) .border_style(theme.focused_style()), ) .style(theme.normal_style()); frame.render_widget(detail, area); } else { let empty = Paragraph::new("No note selected") .style(theme.dimmed_style()) .alignment(ratatui::layout::Alignment::Center); frame.render_widget(empty, area); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_detail_screen_no_selection() { let state = AppState::new(); assert!(state.selected_note().is_none()); } } ``` Create `crates/note-manager-tui/src/screens/mod.rs`: ```rust //! Screen implementations pub mod create; pub mod detail; pub mod list; pub use create::CreateScreen; pub use detail::DetailScreen; pub use list::ListScreen; ``` ## Step 5: Create Library Entry Point (lib.rs) Create `crates/note-manager-tui/src/lib.rs`: ```rust #![forbid(unsafe_code)] #![warn(missing_docs)] //! Note Manager TUI - Terminal user interface for note management pub mod app; pub mod screens; pub use app::{AppState, ScreenType}; /// Prelude module - convenient imports for TUI usage pub mod prelude { pub use crate::{AppState, ScreenType}; pub use crate::screens::{CreateScreen, DetailScreen, ListScreen}; } ``` ## Step 6: Create Integration Module (integration.rs) Create `crates/note-manager-tui/src/integration.rs` to register with multi-tool dashboard: ```rust //! Integration with tools-dashboard use crate::app::AppState; use std::sync::{Arc, Mutex}; use tools_tui_shared::integration::{ToolMetrics, ToolSummary, ToolSummaryProvider}; /// Note Manager TUI integration for tools-dashboard pub struct NoteManagerTui { /// Shared state state: Arc>, } impl NoteManagerTui { /// Create new note manager TUI integration pub fn new() -> Self { NoteManagerTui { state: Arc::new(Mutex::new(AppState::new())), } } /// Get note count pub fn note_count(&self) -> usize { self.state.lock().unwrap().notes.len() } } impl Default for NoteManagerTui { fn default() -> Self { Self::new() } } impl ToolSummaryProvider for NoteManagerTui { fn name(&self) -> &str { "note-manager" } fn summary(&self) -> Result { let state = self.state.lock().unwrap(); let metrics = ToolMetrics::new( state.notes.len() as u32, state.notes.len() as u32, 0, chrono::Utc::now().to_rfc2822(), ); Ok(ToolSummary::new( "note-manager".to_string(), "Note Manager".to_string(), "Manage notes with TUI interface".to_string(), metrics, ) .with_commands(vec![ "list".to_string(), "create".to_string(), "delete".to_string(), ])) } fn execute_command(&self, command: &str, _args: &[String]) -> Result { match command { "list" => Ok(format!("Listed {} notes", self.note_count())), "create" => Ok("Creating new note...".to_string()), "delete" => Ok("Note deleted".to_string()), _ => Err(format!("Unknown command: {}", command)), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_note_manager_tui_creation() { let tui = NoteManagerTui::new(); assert_eq!(tui.name(), "note-manager"); } #[test] fn test_note_manager_empty_summary() { let tui = NoteManagerTui::new(); let summary = tui.summary().unwrap(); assert_eq!(summary.label, "Note Manager"); } } ``` ## Step 7: Create Tests Create `crates/note-manager-tui/tests/integration_tests.rs`: ```rust //! Integration tests for Note Manager TUI use note_manager_tui::{AppState, ScreenType}; #[test] fn test_full_workflow() { let mut state = AppState::new(); // Start on list screen assert_eq!(state.current_screen, ScreenType::List); // Create a note state.add_note("My First Note".to_string()); assert_eq!(state.notes.len(), 1); // Navigate to create screen state.set_screen(ScreenType::Create); assert_eq!(state.current_screen, ScreenType::Create); // Fill form state.form_title = "New Note".to_string(); state.form_content = "Note content".to_string(); // Reset form state.reset_form(); assert!(state.form_title.is_empty()); assert!(state.form_content.is_empty()); // Navigate back to list state.set_screen(ScreenType::List); assert_eq!(state.current_screen, ScreenType::List); // Request quit state.request_quit(); assert!(state.should_quit()); } #[test] fn test_dirty_flag_optimization() { let mut state = AppState::new(); assert!(state.is_dirty()); state.reset_dirty(); assert!(!state.is_dirty()); // Any change should mark dirty state.add_note("Test".to_string()); assert!(state.is_dirty()); } #[test] fn test_selection_navigation() { let mut state = AppState::new(); state.add_note("Note 1".to_string()); state.add_note("Note 2".to_string()); state.add_note("Note 3".to_string()); assert_eq!(state.selected_index, 0); state.select_next(); assert_eq!(state.selected_index, 1); state.select_next(); assert_eq!(state.selected_index, 2); // Wraps around state.select_next(); assert_eq!(state.selected_index, 0); state.select_previous(); assert_eq!(state.selected_index, 2); } ``` ## Step 8: Verify Everything Compiles and Tests Pass ```bash cd /Users/Akasha/Tools/note-manager cargo check --workspace cargo test --workspace cargo clippy --all-targets cargo fmt --check ``` Expected output: ``` test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ## Step 9: Run Your TUI If you created a binary entry point in `src/main.rs`, you can run: ```bash cargo run -p note-manager-tui --release ``` ## Summary This walkthrough demonstrates: 1. **Directory structure** - How to organize TUI as separate crate 2. **Cargo.toml** - Proper dependency setup with shared library 3. **AppState** - Implementation of R-STATE-SEPARATION pattern 4. **Screens** - Multiple screen implementations following templates 5. **Integration** - Dashboard registration via ToolSummaryProvider 6. **Testing** - Comprehensive test coverage (15+ tests minimum) 7. **Quality** - Zero unsafe code, proper error handling, documentation You can adapt this structure and code for any tool in the Tools ecosystem. The key is following the patterns established in the templates and tools-tui-shared library.