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)
230 lines
7.2 KiB
Rust
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());
|
|
}
|
|
}
|