syntaxis/shared/rust-tui/TEMPLATE_SCREEN.rs
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

230 lines
7.2 KiB
Rust

//! 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<ListItem> = 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() {
"<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() {
"<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());
}
}