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)
326 lines
8.3 KiB
Rust
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());
|
|
}
|
|
}
|