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

326 lines
8.3 KiB
Rust

//! Application state template for tool TUI integration
//!
//! Copy and customize this template for your tool's app state.
//! Replace {{TOOL_NAME}} with your tool name (e.g., MyTool, TrackingManager, etc.)
//! Replace {{ITEM_TYPE}} with your primary data type
use serde::{Deserialize, Serialize};
use std::fmt;
/// Screen types - customize based on your tool's screens
///
/// # Example
/// ```ignore
/// pub enum ScreenType {
/// List, // Show all items
/// Detail, // Show item details
/// Create, // Create new item
/// Settings, // Tool settings
/// }
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScreenType {
/// List screen - show all items
List,
/// Detail screen - show item details
Detail,
/// Create screen - create new items
Create,
}
impl fmt::Display for ScreenType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScreenType::List => write!(f, "List"),
ScreenType::Detail => write!(f, "Detail"),
ScreenType::Create => write!(f, "Create"),
}
}
}
/// 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`).
///
/// # Dirty Flag
/// The `dirty` flag tracks whether state changed since last render.
/// Only redraw when `dirty` is true, then set to false.
#[derive(Clone, Debug)]
pub struct AppState {
/// Current screen (navigation state)
pub current_screen: ScreenType,
/// Primary items collection - customize type as needed
pub items: Vec<String>,
/// Currently selected item index
pub selected_index: usize,
/// Whether state changed (needs redraw)
pub dirty: bool,
/// Whether application should quit
pub should_quit: bool,
// === Form fields for create/edit ===
/// Form input: item name
pub form_name: String,
/// Form input: item description
pub form_description: String,
}
impl AppState {
/// Create new application state
pub fn new() -> Self {
AppState {
current_screen: ScreenType::List,
items: Vec::new(),
selected_index: 0,
dirty: true,
should_quit: false,
form_name: String::new(),
form_description: String::new(),
}
}
// === Item Management ===
/// Add a new item to the collection
pub fn add_item(&mut self, item: String) {
self.items.push(item);
self.dirty = true;
}
/// Get currently selected item
pub fn selected_item(&self) -> Option<&String> {
self.items.get(self.selected_index)
}
// === Navigation ===
/// Select next item in list
pub fn select_next(&mut self) {
if !self.items.is_empty() {
self.selected_index = (self.selected_index + 1) % self.items.len();
self.dirty = true;
}
}
/// Select previous item in list
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;
}
}
/// 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_name.clear();
self.form_description.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)
///
/// Call this after successfully rendering to the terminal
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()
}
}
// ============================================================================
// Tests - Minimum 15+ tests per module
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
// === Initialization Tests ===
#[test]
fn test_app_state_new() {
let state = AppState::new();
assert_eq!(state.current_screen, ScreenType::List);
assert!(state.items.is_empty());
assert_eq!(state.selected_index, 0);
assert!(state.dirty);
assert!(!state.should_quit);
assert!(state.form_name.is_empty());
}
#[test]
fn test_app_state_default() {
let state = AppState::default();
assert_eq!(state.current_screen, ScreenType::List);
}
// === Item Management Tests ===
#[test]
fn test_add_item() {
let mut state = AppState::new();
state.add_item("Item 1".to_string());
assert_eq!(state.items.len(), 1);
assert!(state.dirty);
}
#[test]
fn test_selected_item() {
let mut state = AppState::new();
state.add_item("Item 1".to_string());
assert_eq!(state.selected_item(), Some(&"Item 1".to_string()));
}
#[test]
fn test_selected_item_none() {
let state = AppState::new();
assert!(state.selected_item().is_none());
}
// === Navigation Tests ===
#[test]
fn test_select_next() {
let mut state = AppState::new();
state.add_item("Item 1".to_string());
state.add_item("Item 2".to_string());
state.reset_dirty();
state.select_next();
assert_eq!(state.selected_index, 1);
assert!(state.dirty);
}
#[test]
fn test_select_next_wraps() {
let mut state = AppState::new();
state.add_item("Item 1".to_string());
state.add_item("Item 2".to_string());
state.selected_index = 1;
state.reset_dirty();
state.select_next();
assert_eq!(state.selected_index, 0);
}
#[test]
fn test_select_previous() {
let mut state = AppState::new();
state.add_item("Item 1".to_string());
state.add_item("Item 2".to_string());
state.selected_index = 1;
state.reset_dirty();
state.select_previous();
assert_eq!(state.selected_index, 0);
}
#[test]
fn test_select_previous_wraps() {
let mut state = AppState::new();
state.add_item("Item 1".to_string());
state.add_item("Item 2".to_string());
state.reset_dirty();
state.select_previous();
assert_eq!(state.selected_index, 1);
}
// === Screen Navigation Tests ===
#[test]
fn test_set_screen() {
let mut state = AppState::new();
state.reset_dirty();
state.set_screen(ScreenType::Detail);
assert_eq!(state.current_screen, ScreenType::Detail);
assert!(state.dirty);
}
#[test]
fn test_set_screen_same_not_dirty() {
let mut state = AppState::new();
state.reset_dirty();
state.set_screen(ScreenType::List);
assert!(!state.dirty);
}
// === Form Management Tests ===
#[test]
fn test_reset_form() {
let mut state = AppState::new();
state.form_name = "Test".to_string();
state.form_description = "Description".to_string();
state.reset_dirty();
state.reset_form();
assert!(state.form_name.is_empty());
assert!(state.form_description.is_empty());
assert!(state.dirty);
}
// === Lifecycle Tests ===
#[test]
fn test_request_quit() {
let mut state = AppState::new();
assert!(!state.should_quit());
state.request_quit();
assert!(state.should_quit());
}
#[test]
fn test_dirty_flag() {
let mut state = AppState::new();
assert!(state.is_dirty());
state.reset_dirty();
assert!(!state.is_dirty());
}
}