//! Screen template for tool TUI integration //! //! Copy and customize this template for each screen in your tool. //! Replace {{SCREEN_NAME}} with your screen name (e.g., ListScreen, DetailScreen, etc.) use crate::app::AppState; use ratatui::prelude::*; use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; use tools_tui_shared::prelude::*; /// {{SCREEN_NAME}} - display and interact with items /// /// # Design Pattern: R-STATE-SEPARATION /// This screen ONLY reads state via `&AppState`. /// It NEVER modifies state - all mutations happen in app.rs. /// Rendering is completely separated from state management. /// /// # Rendering /// The render() static method receives: /// - state: &AppState - immutable reference to application state /// - frame: &mut ratatui::Frame - terminal frame to draw on /// - area: Rect - available screen area for this screen pub struct ListScreen; impl ListScreen { /// Render the list screen /// /// # Parameters /// - state: Application state (read-only) /// - frame: Terminal frame /// - area: Available screen area pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) { let theme = Theme::dark(); // Handle empty state if state.items.is_empty() { let empty_msg = Paragraph::new("No items 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 .items .iter() .enumerate() .map(|(idx, item)| { // Highlight selected item with ★ let marker = if idx == state.selected_index { "★ " } else { " " }; ListItem::new(format!("{}{}", marker, item)) }) .collect(); // Build list widget let list = List::new(items) .block( Block::default() .title("Items") .borders(Borders::ALL) .border_style(theme.focused_style()), ) .style(theme.normal_style()); // Render to frame frame.render_widget(list, area); } } /// Detail screen template - show item 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(item) = state.selected_item() { // Create detail text with labels let detail_text = vec![ Line::from(vec![ Span::styled("Item: ", theme.selected_style()), Span::raw(item), ]), 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 item selected") .style(theme.dimmed_style()) .alignment(ratatui::layout::Alignment::Center); frame.render_widget(empty, area); } } } /// Create screen template - form for creating items pub struct CreateScreen; impl CreateScreen { /// Render the create screen with form 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("Name: ", theme.focused_style()), Span::raw(if state.form_name.is_empty() { "".to_string() } else { state.form_name.clone() }), ]), Line::from(""), Line::from(vec![ Span::styled("Description: ", theme.focused_style()), Span::raw(if state.form_description.is_empty() { "".to_string() } else { state.form_description.clone() }), ]), ]; let form = Paragraph::new(form_text) .block( Block::default() .title("Create Item") .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]); } } // ============================================================================ // Tests - Minimum 15+ tests per screen module // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_list_screen_empty() { let state = AppState::new(); assert!(state.items.is_empty()); } #[test] fn test_list_screen_with_items() { let mut state = AppState::new(); state.add_item("Item 1".to_string()); state.add_item("Item 2".to_string()); assert_eq!(state.items.len(), 2); } #[test] fn test_detail_screen_no_selection() { let state = AppState::new(); assert!(state.selected_item().is_none()); } #[test] fn test_detail_screen_with_selection() { let mut state = AppState::new(); state.add_item("Test Item".to_string()); assert!(state.selected_item().is_some()); } #[test] fn test_create_screen_empty_form() { let state = AppState::new(); assert!(state.form_name.is_empty()); assert!(state.form_description.is_empty()); } #[test] fn test_create_screen_form_reset() { let mut state = AppState::new(); state.form_name = "Test".to_string(); state.form_description = "Description".to_string(); state.reset_form(); assert!(state.form_name.is_empty()); assert!(state.form_description.is_empty()); } }