From cd8bd851ab58556124b306abcd9e55cfb9d698a1 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Sat, 17 Jan 2026 21:16:08 -0700 Subject: [PATCH] created rust task manager --- .gitignore | 3 + README.md | 9 + rustfmt.toml | 45 ++ task_manager_v2.rs | 1209 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1266 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 rustfmt.toml create mode 100644 task_manager_v2.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84c2149 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +task_manager_v2 +task_manager_v2_test +.env.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a9a9df --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +Rust Task Manager CLI +============================================= + +this is a simple task manager written in rust + +to compile: `rustc task_manager_v2.rs` +to run: `./task_manager_v2` + +thats it, thats the killer app. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..595e3a2 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,45 @@ +# Rust formatting configuration +# Following universal coding standards (Section 8.1) + +# Maximum line width (Section 1.2: 80-120 chars) +max_width = 100 + +# Use spaces for Rust (ecosystem convention) +hard_tabs = false +tab_spaces = 4 + +# Edition +edition = "2021" + +# Imports +imports_granularity = "Module" +group_imports = "StdExternalCrate" +reorder_imports = true + +# Comments +wrap_comments = true +comment_width = 80 +normalize_comments = true + +# Formatting +newline_style = "Unix" +use_small_heuristics = "Default" + +# Function formatting +fn_params_layout = "Tall" +fn_single_line = false + +# Struct/enum formatting +struct_lit_single_line = true +enum_discrim_align_threshold = 20 + +# Match formatting +match_block_trailing_comma = true +match_arm_blocks = true + +# Control flow +control_brace_style = "AlwaysSameLine" + +# Misc +use_field_init_shorthand = true +use_try_shorthand = true diff --git a/task_manager_v2.rs b/task_manager_v2.rs new file mode 100644 index 0000000..2372ffa --- /dev/null +++ b/task_manager_v2.rs @@ -0,0 +1,1209 @@ +//! Task Manager - A CLI application for managing tasks with priorities and subtasks. +//! +//! This module provides a complete task management system demonstrating +//! idiomatic Rust patterns including error handling, traits, and collections. + +use std::collections::HashMap; +use std::fmt; +use std::io::{self, Write}; + +// ============================================================================ +// Constants +// ============================================================================ + +/// Maximum tasks before warning about performance. +/// Chosen because HashMap operations remain O(1) up to this scale, +/// and terminal output remains readable. +const MAX_RECOMMENDED_TASKS: usize = 1000; + +// ============================================================================ +// Enums +// ============================================================================ + +/// Task priority levels, ordered from lowest to highest urgency. +/// +/// Priority affects the urgency score calculation and sort order +/// when listing tasks. Critical tasks are flagged in statistics. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Priority { + Low, + Medium, + High, + Critical, +} + +impl Priority { + /// Parses a priority from user input string. + /// + /// Accepts both full names and single-letter abbreviations, + /// case-insensitive. Returns `None` for unrecognized input. + /// + /// # Examples + /// ``` + /// assert_eq!(Priority::from_str("high"), Some(Priority::High)); + /// assert_eq!(Priority::from_str("H"), Some(Priority::High)); + /// assert_eq!(Priority::from_str("invalid"), None); + /// ``` + pub fn from_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "low" | "l" => Some(Priority::Low), + "medium" | "m" => Some(Priority::Medium), + "high" | "h" => Some(Priority::High), + "critical" | "c" => Some(Priority::Critical), + _ => None, + } + } + + /// Returns a numeric score for sorting and urgency calculation. + /// + /// Higher scores indicate higher priority. Scale is 1-4 to allow + /// multiplication in urgency formulas without overflow concerns. + fn score(&self) -> u8 { + match self { + Priority::Low => 1, + Priority::Medium => 2, + Priority::High => 3, + Priority::Critical => 4, + } + } +} + +impl fmt::Display for Priority { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let symbol = match self { + Priority::Low => "[ ]", + Priority::Medium => "[=]", + Priority::High => "[!]", + Priority::Critical => "[*]", + }; + write!(f, "{:?} {}", self, symbol) + } +} + +/// Task workflow status. +/// +/// Status transitions follow a typical kanban flow: +/// Todo -> InProgress -> Done, with Blocked as a side state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + Todo, + InProgress, + Blocked, + Done, +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (name, symbol) = match self { + Status::Todo => ("Todo", "[ ]"), + Status::InProgress => ("In Progress", "[~]"), + Status::Blocked => ("Blocked", "[x]"), + Status::Done => ("Done", "[v]"), + }; + write!(f, "{} {}", name, symbol) + } +} + +// ============================================================================ +// Error Types +// ============================================================================ + +/// Errors that can occur during task operations. +/// +/// Each variant provides context for user-friendly error messages. +/// Implements std::error::Error for compatibility with error handling crates. +#[derive(Debug)] +pub enum TaskError { + /// Task with the specified ID does not exist. + NotFound(u32), + /// User provided invalid input (with explanation). + InvalidInput(String), + /// Attempted to add a duplicate tag to a task. + DuplicateTag(String), +} + +impl fmt::Display for TaskError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TaskError::NotFound(id) => write!(f, "Task with ID {} not found", id), + TaskError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), + TaskError::DuplicateTag(tag) => write!(f, "Tag '{}' already exists", tag), + } + } +} + +impl std::error::Error for TaskError {} + +/// Convenience type alias for Results with TaskError. +pub type Result = std::result::Result; + +// ============================================================================ +// Traits +// ============================================================================ + +/// Provides a one-line summary of an item for list views. +pub trait Summarize { + fn summary(&self) -> String; +} + +/// Calculates an urgency/priority score for sorting. +pub trait Scoreable { + fn calculate_score(&self) -> u32; +} + +// ============================================================================ +// Core Data Structures +// ============================================================================ + +/// A task with metadata, subtasks, and tags. +/// +/// Tasks are the core entity in the system. Each task has a unique ID +/// assigned by the TaskManager, along with user-defined attributes. +/// +/// # Invariants +/// - `id` is unique within a TaskManager instance +/// - `tags` contains no duplicates (enforced by `add_tag`) +#[derive(Debug, Clone)] +pub struct Task { + id: u32, + title: String, + description: Option, + priority: Priority, + status: Status, + tags: Vec, + subtasks: Vec, +} + +/// A subtask within a parent task. +/// +/// Subtasks are simpler than tasks - they only have a title and +/// completion status. They cannot be nested further. +#[derive(Debug, Clone)] +pub struct Subtask { + title: String, + completed: bool, +} + +impl Task { + /// Creates a new task with the given title and priority. + /// + /// The task starts in `Todo` status with no description, tags, or subtasks. + /// + /// # Arguments + /// * `id` - Unique identifier (assigned by TaskManager) + /// * `title` - Brief description of the task + /// * `priority` - Initial priority level + fn new(id: u32, title: impl Into, priority: Priority) -> Self { + Task { + id, + title: title.into(), + description: None, + priority, + status: Status::Todo, + tags: Vec::new(), + subtasks: Vec::new(), + } + } + + /// Adds a tag to this task. + /// + /// Tags are case-sensitive and must be unique within a task. + /// Returns an error if the tag already exists. + pub fn add_tag(&mut self, tag: impl Into) -> Result<()> { + let tag = tag.into(); + if self.tags.contains(&tag) { + return Err(TaskError::DuplicateTag(tag)); + } + self.tags.push(tag); + Ok(()) + } + + /// Adds a new subtask to this task. + /// + /// Subtasks start as incomplete. The order of subtasks is preserved. + pub fn add_subtask(&mut self, title: impl Into) { + self.subtasks.push(Subtask { + title: title.into(), + completed: false, + }); + } + + /// Marks a subtask as completed by its index. + /// + /// # Arguments + /// * `index` - Zero-based index of the subtask + /// + /// # Errors + /// Returns `InvalidInput` if the index is out of bounds. + pub fn complete_subtask(&mut self, index: usize) -> Result<()> { + self.subtasks + .get_mut(index) + .ok_or_else(|| { + TaskError::InvalidInput(format!("Subtask index {} out of bounds", index)) + })? + .completed = true; + Ok(()) + } + + /// Calculates the percentage of completed subtasks. + /// + /// If there are no subtasks, returns 100% for Done tasks, 0% otherwise. + /// This provides meaningful progress even for tasks without subtasks. + pub fn progress_percentage(&self) -> f64 { + if self.subtasks.is_empty() { + return if self.status == Status::Done { + 100.0 + } else { + 0.0 + }; + } + let completed = self.subtasks.iter().filter(|s| s.completed).count(); + (completed as f64 / self.subtasks.len() as f64) * 100.0 + } + + /// Returns true if this task is critical and not yet done. + /// + /// Used to flag high-priority incomplete work in statistics. + fn is_critical_incomplete(&self) -> bool { + self.priority == Priority::Critical && self.status != Status::Done + } +} + +impl Summarize for Task { + fn summary(&self) -> String { + let tags_str = if self.tags.is_empty() { + String::from("none") + } else { + self.tags.join(", ") + }; + format!( + "[#{}] {} | Priority: {} | Status: {} | Tags: {} | Progress: {:.0}%", + self.id, + self.title, + self.priority, + self.status, + tags_str, + self.progress_percentage() + ) + } +} + +impl Scoreable for Task { + /// Calculates urgency score for task prioritization. + /// + /// Formula: (priority * 10) + status_penalty + incomplete_subtasks + /// + /// This weights priority highest, then penalizes blocked/todo status, + /// then adds urgency for incomplete subtasks. Higher score = more urgent. + fn calculate_score(&self) -> u32 { + let priority_score = self.priority.score() as u32 * 10; + let status_penalty = match self.status { + Status::Done => 0, + Status::InProgress => 5, + Status::Todo => 10, + Status::Blocked => 15, // Blocked items need attention + }; + let subtask_bonus = self.subtasks.iter().filter(|s| !s.completed).count() as u32; + priority_score + status_penalty + subtask_bonus + } +} + +impl fmt::Display for Task { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "+----------------------------------------------------------------+")?; + writeln!(f, "| Task #{}: {}", self.id, self.title)?; + writeln!(f, "+----------------------------------------------------------------+")?; + writeln!(f, "| Priority: {}", self.priority)?; + writeln!(f, "| Status: {}", self.status)?; + + if let Some(ref desc) = self.description { + writeln!(f, "| Description: {}", desc)?; + } + + if !self.tags.is_empty() { + writeln!(f, "| Tags: [{}]", self.tags.join(", "))?; + } + + if !self.subtasks.is_empty() { + writeln!(f, "| Subtasks:")?; + for (i, subtask) in self.subtasks.iter().enumerate() { + let check = if subtask.completed { "v" } else { " " }; + writeln!(f, "| {}. [{}] {}", i + 1, check, subtask.title)?; + } + } + + writeln!(f, "| Progress: {:.1}%", self.progress_percentage())?; + writeln!(f, "| Urgency Score: {}", self.calculate_score())?; + write!(f, "+----------------------------------------------------------------+") + } +} + +// ============================================================================ +// Task Manager +// ============================================================================ + +/// Manages a collection of tasks with CRUD operations and queries. +/// +/// TaskManager owns all tasks and assigns unique IDs. It provides +/// filtering, searching, and statistical aggregation. +/// +/// # Example +/// ``` +/// let mut manager = TaskManager::new(); +/// let id = manager.create_task("My task", Priority::High); +/// manager.update_status(id, Status::InProgress)?; +/// ``` +pub struct TaskManager { + tasks: HashMap, + next_id: u32, +} + +impl TaskManager { + /// Creates an empty TaskManager. + pub fn new() -> Self { + TaskManager { + tasks: HashMap::new(), + next_id: 1, + } + } + + /// Creates a new task and returns its assigned ID. + /// + /// IDs are assigned sequentially starting from 1. + /// Warns if approaching the recommended task limit. + pub fn create_task(&mut self, title: impl Into, priority: Priority) -> u32 { + let id = self.next_id; + self.next_id += 1; + + if self.tasks.len() >= MAX_RECOMMENDED_TASKS { + eprintln!( + "Warning: {} tasks exceeds recommended limit of {}", + self.tasks.len(), + MAX_RECOMMENDED_TASKS + ); + } + + let task = Task::new(id, title, priority); + self.tasks.insert(id, task); + id + } + + /// Returns an immutable reference to a task by ID. + pub fn get_task(&self, id: u32) -> Result<&Task> { + self.tasks.get(&id).ok_or(TaskError::NotFound(id)) + } + + /// Returns a mutable reference to a task by ID. + pub fn get_task_mut(&mut self, id: u32) -> Result<&mut Task> { + self.tasks.get_mut(&id).ok_or(TaskError::NotFound(id)) + } + + /// Deletes a task and returns it. + pub fn delete_task(&mut self, id: u32) -> Result { + self.tasks.remove(&id).ok_or(TaskError::NotFound(id)) + } + + /// Updates a task's status. + pub fn update_status(&mut self, id: u32, status: Status) -> Result<()> { + self.get_task_mut(id)?.status = status; + Ok(()) + } + + /// Returns all tasks sorted by urgency score (highest first). + pub fn list_all(&self) -> Vec<&Task> { + let mut tasks: Vec<&Task> = self.tasks.values().collect(); + tasks.sort_by(|a, b| b.calculate_score().cmp(&a.calculate_score())); + tasks + } + + /// Returns tasks matching the given status. + pub fn filter_by_status(&self, status: Status) -> Vec<&Task> { + self.tasks + .values() + .filter(|t| t.status == status) + .collect() + } + + /// Returns tasks containing the given tag (case-insensitive). + pub fn filter_by_tag(&self, tag: &str) -> Vec<&Task> { + self.tasks + .values() + .filter(|t| t.tags.iter().any(|t| t.eq_ignore_ascii_case(tag))) + .collect() + } + + /// Searches tasks by title and description (case-insensitive substring). + pub fn search(&self, query: &str) -> Vec<&Task> { + let query_lower = query.to_lowercase(); + self.tasks + .values() + .filter(|t| { + t.title.to_lowercase().contains(&query_lower) + || t.description + .as_ref() + .map(|d| d.to_lowercase().contains(&query_lower)) + .unwrap_or(false) + }) + .collect() + } + + /// Computes aggregate statistics across all tasks. + pub fn statistics(&self) -> Statistics { + let total = self.tasks.len(); + let by_status = |s: Status| self.tasks.values().filter(|t| t.status == s).count(); + let by_priority = |p: Priority| self.tasks.values().filter(|t| t.priority == p).count(); + + let avg_progress = if total > 0 { + self.tasks + .values() + .map(|t| t.progress_percentage()) + .sum::() + / total as f64 + } else { + 0.0 + }; + + let critical_incomplete = self + .tasks + .values() + .filter(|t| t.is_critical_incomplete()) + .count(); + + Statistics { + total, + todo: by_status(Status::Todo), + in_progress: by_status(Status::InProgress), + blocked: by_status(Status::Blocked), + done: by_status(Status::Done), + low_priority: by_priority(Priority::Low), + medium_priority: by_priority(Priority::Medium), + high_priority: by_priority(Priority::High), + critical_priority: by_priority(Priority::Critical), + average_progress: avg_progress, + critical_incomplete, + } + } +} + +impl Default for TaskManager { + fn default() -> Self { + Self::new() + } +} + +// ============================================================================ +// Statistics +// ============================================================================ + +/// Aggregated statistics across all tasks. +pub struct Statistics { + pub total: usize, + pub todo: usize, + pub in_progress: usize, + pub blocked: usize, + pub done: usize, + pub low_priority: usize, + pub medium_priority: usize, + pub high_priority: usize, + pub critical_priority: usize, + pub average_progress: f64, + pub critical_incomplete: usize, +} + +impl fmt::Display for Statistics { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "\n## Task Statistics")?; + writeln!(f, "========================================")?; + writeln!(f, "Total Tasks: {}", self.total)?; + writeln!(f)?; + writeln!(f, "By Status:")?; + writeln!(f, " [ ] Todo: {}", self.todo)?; + writeln!(f, " [~] In Progress: {}", self.in_progress)?; + writeln!(f, " [x] Blocked: {}", self.blocked)?; + writeln!(f, " [v] Done: {}", self.done)?; + writeln!(f)?; + writeln!(f, "By Priority:")?; + writeln!(f, " Low: {}", self.low_priority)?; + writeln!(f, " Medium: {}", self.medium_priority)?; + writeln!(f, " High: {}", self.high_priority)?; + writeln!(f, " Critical: {}", self.critical_priority)?; + writeln!(f)?; + writeln!(f, "Average Progress: {:.1}%", self.average_progress)?; + if self.critical_incomplete > 0 { + writeln!( + f, + "!! Critical tasks incomplete: {}", + self.critical_incomplete + )?; + } + write!(f, "========================================") + } +} + +// ============================================================================ +// CLI Command Handlers +// ============================================================================ +// Each handler is a small function (<50 lines) following the single +// responsibility principle. This makes testing individual commands easy. + +/// Result of executing a command. +enum CommandResult { + Continue, + Exit, +} + +/// Parses a task ID from a string slice. +fn parse_id(s: Option<&&str>, usage: &str) -> Result { + s.and_then(|s| s.parse().ok()) + .ok_or_else(|| TaskError::InvalidInput(usage.into())) +} + +/// Handles the 'add' command. +fn cmd_add(manager: &mut TaskManager, parts: &[&str]) -> Result { + if parts.len() < 2 { + return Err(TaskError::InvalidInput("Usage: add [priority]".into())); + } + + let priority = parts + .get(2) + .and_then(|p| Priority::from_str(p)) + .unwrap_or(Priority::Medium); + + let id = manager.create_task(parts[1], priority); + println!("[OK] Created task #{}", id); + Ok(CommandResult::Continue) +} + +/// Handles the 'list' command. +fn cmd_list(manager: &TaskManager) -> Result<CommandResult> { + let tasks = manager.list_all(); + if tasks.is_empty() { + println!("No tasks found. Use 'demo' to load sample data."); + } else { + println!("\n## All Tasks (sorted by urgency):\n"); + for task in tasks { + println!(" {}", task.summary()); + } + } + Ok(CommandResult::Continue) +} + +/// Handles the 'show' command. +fn cmd_show(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> { + let id = parse_id(parts.get(1), "Usage: show <id>")?; + let task = manager.get_task(id)?; + println!("{}", task); + Ok(CommandResult::Continue) +} + +/// Handles status update commands (done, progress, block). +fn cmd_update_status( + manager: &mut TaskManager, + parts: &[&str], + status: Status, + label: &str, +) -> Result<CommandResult> { + let usage = format!("Usage: {} <id>", label); + let id = parse_id(parts.get(1), &usage)?; + manager.update_status(id, status)?; + println!("[OK] Task #{} marked as {}", id, label); + Ok(CommandResult::Continue) +} + +/// Handles the 'delete' command. +fn cmd_delete(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { + let id = parse_id(parts.get(1), "Usage: delete <id>")?; + let task = manager.delete_task(id)?; + println!("[OK] Deleted task: {}", task.title); + Ok(CommandResult::Continue) +} + +/// Handles the 'tag' command. +fn cmd_tag(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { + if parts.len() < 3 { + return Err(TaskError::InvalidInput("Usage: tag <id> <tag>".into())); + } + + let id: u32 = parts[1] + .parse() + .map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?; + + manager.get_task_mut(id)?.add_tag(parts[2])?; + println!("[OK] Added tag '{}' to task #{}", parts[2], id); + Ok(CommandResult::Continue) +} + +/// Handles the 'subtask' command. +fn cmd_subtask(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { + if parts.len() < 3 { + return Err(TaskError::InvalidInput("Usage: subtask <id> <title>".into())); + } + + let id: u32 = parts[1] + .parse() + .map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?; + + manager.get_task_mut(id)?.add_subtask(parts[2]); + println!("[OK] Added subtask to task #{}", id); + Ok(CommandResult::Continue) +} + +/// Handles the 'check' command. +fn cmd_check(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { + if parts.len() < 3 { + return Err(TaskError::InvalidInput("Usage: check <id> <subtask#>".into())); + } + + let id: u32 = parts[1] + .parse() + .map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?; + + let subtask_idx: usize = parts[2] + .parse::<usize>() + .map_err(|_| TaskError::InvalidInput("Invalid subtask number".into()))? + .saturating_sub(1); // Convert 1-based to 0-based index + + manager.get_task_mut(id)?.complete_subtask(subtask_idx)?; + println!("[OK] Completed subtask"); + Ok(CommandResult::Continue) +} + +/// Handles the 'desc' command. +fn cmd_desc(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { + if parts.len() < 3 { + return Err(TaskError::InvalidInput( + "Usage: desc <id> <description>".into(), + )); + } + + let id: u32 = parts[1] + .parse() + .map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?; + + manager.get_task_mut(id)?.description = Some(parts[2].into()); + println!("[OK] Updated description for task #{}", id); + Ok(CommandResult::Continue) +} + +/// Handles the 'filter' command. +fn cmd_filter(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> { + let status_str = parts.get(1).unwrap_or(&""); + let tasks = match status_str.to_lowercase().as_str() { + "todo" => manager.filter_by_status(Status::Todo), + "progress" | "inprogress" => manager.filter_by_status(Status::InProgress), + "blocked" => manager.filter_by_status(Status::Blocked), + "done" => manager.filter_by_status(Status::Done), + _ => { + return Err(TaskError::InvalidInput( + "Usage: filter <todo|progress|blocked|done>".into(), + )); + } + }; + + println!("\nFiltered tasks:"); + if tasks.is_empty() { + println!(" No tasks match the filter."); + } else { + for task in tasks { + println!(" {}", task.summary()); + } + } + Ok(CommandResult::Continue) +} + +/// Handles the 'search' command. +fn cmd_search(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> { + if parts.len() < 2 { + return Err(TaskError::InvalidInput("Usage: search <query>".into())); + } + + let query = parts[1..].join(" "); + let tasks = manager.search(&query); + + println!("\nSearch results for '{}':", query); + if tasks.is_empty() { + println!(" No tasks found."); + } else { + for task in tasks { + println!(" {}", task.summary()); + } + } + Ok(CommandResult::Continue) +} + +// ============================================================================ +// CLI Infrastructure +// ============================================================================ + +fn print_help() { + println!("\n## Task Manager Commands:"); + println!("=========================================================="); + println!(" add <title> [priority] - Add a new task (priority: l/m/h/c)"); + println!(" list - List all tasks"); + println!(" show <id> - Show task details"); + println!(" done <id> - Mark task as done"); + println!(" progress <id> - Mark task as in progress"); + println!(" block <id> - Mark task as blocked"); + println!(" delete <id> - Delete a task"); + println!(" tag <id> <tag> - Add tag to task"); + println!(" subtask <id> <title> - Add subtask"); + println!(" check <id> <subtask#> - Complete subtask"); + println!(" desc <id> <description> - Add description"); + println!(" filter <status> - Filter by status"); + println!(" search <query> - Search tasks"); + println!(" stats - Show statistics"); + println!(" demo - Load demo data"); + println!(" help - Show this help"); + println!(" quit - Exit"); + println!("==========================================================\n"); +} + +/// Loads sample tasks for demonstration and testing. +fn load_demo_data(manager: &mut TaskManager) { + let id1 = manager.create_task("Deploy production server", Priority::Critical); + if let Ok(task) = manager.get_task_mut(id1) { + task.description = + Some("Deploy the new version to production with zero downtime".into()); + let _ = task.add_tag("devops"); + let _ = task.add_tag("urgent"); + task.add_subtask("Backup database"); + task.add_subtask("Run migrations"); + task.add_subtask("Deploy new containers"); + task.add_subtask("Verify health checks"); + let _ = task.complete_subtask(0); + task.status = Status::InProgress; + } + + let id2 = manager.create_task("Fix authentication bug", Priority::High); + if let Ok(task) = manager.get_task_mut(id2) { + task.description = Some("Users getting logged out unexpectedly".into()); + let _ = task.add_tag("bug"); + let _ = task.add_tag("security"); + task.status = Status::Blocked; + } + + let id3 = manager.create_task("Implement user dashboard", Priority::Medium); + if let Ok(task) = manager.get_task_mut(id3) { + let _ = task.add_tag("feature"); + let _ = task.add_tag("frontend"); + task.add_subtask("Design mockups"); + task.add_subtask("Create React components"); + task.add_subtask("Add API integration"); + task.add_subtask("Write tests"); + let _ = task.complete_subtask(0); + let _ = task.complete_subtask(1); + } + + let id4 = manager.create_task("Update documentation", Priority::Low); + if let Ok(task) = manager.get_task_mut(id4) { + let _ = task.add_tag("docs"); + } + + let id5 = manager.create_task("Setup CI/CD pipeline", Priority::High); + if let Ok(task) = manager.get_task_mut(id5) { + let _ = task.add_tag("devops"); + task.add_subtask("Configure GitHub Actions"); + task.add_subtask("Add test stage"); + task.add_subtask("Add deploy stage"); + let _ = task.complete_subtask(0); + let _ = task.complete_subtask(1); + let _ = task.complete_subtask(2); + task.status = Status::Done; + } + + println!("[OK] Loaded 5 demo tasks!"); +} + +/// Routes a command to the appropriate handler. +/// +/// This dispatcher keeps each command handler small and testable. +/// Adding new commands requires only adding a new match arm and handler. +fn execute_command(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { + let command = parts[0].to_lowercase(); + + match command.as_str() { + "quit" | "q" | "exit" => { + println!("Goodbye!"); + Ok(CommandResult::Exit) + } + "help" | "h" | "?" => { + print_help(); + Ok(CommandResult::Continue) + } + "demo" => { + load_demo_data(manager); + Ok(CommandResult::Continue) + } + "add" => cmd_add(manager, parts), + "list" | "ls" => cmd_list(manager), + "show" => cmd_show(manager, parts), + "done" => cmd_update_status(manager, parts, Status::Done, "done"), + "progress" => cmd_update_status(manager, parts, Status::InProgress, "in progress"), + "block" => cmd_update_status(manager, parts, Status::Blocked, "blocked"), + "delete" | "rm" => cmd_delete(manager, parts), + "tag" => cmd_tag(manager, parts), + "subtask" => cmd_subtask(manager, parts), + "check" => cmd_check(manager, parts), + "desc" => cmd_desc(manager, parts), + "filter" => cmd_filter(manager, parts), + "search" => cmd_search(manager, parts), + "stats" => { + println!("{}", manager.statistics()); + Ok(CommandResult::Continue) + } + _ => Err(TaskError::InvalidInput(format!( + "Unknown command: '{}'. Type 'help' for commands.", + command + ))), + } +} + +/// Main REPL loop for the CLI. +fn run_interactive(manager: &mut TaskManager) { + println!("\n## Welcome to Rust Task Manager!\n"); + print_help(); + + loop { + print!("> "); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_err() { + println!("Error reading input"); + continue; + } + + let parts: Vec<&str> = input.trim().splitn(3, ' ').collect(); + if parts.is_empty() || parts[0].is_empty() { + continue; + } + + match execute_command(manager, &parts) { + Ok(CommandResult::Exit) => break, + Ok(CommandResult::Continue) => {} + Err(e) => println!("[ERROR] {}", e), + } + } +} + +// ============================================================================ +// Entry Point +// ============================================================================ + +fn main() { + let mut manager = TaskManager::new(); + run_interactive(&mut manager); +} + +// ============================================================================ +// Unit Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + // ------------------------------------------------------------------------- + // Priority Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_priority_from_str_full_names() { + assert_eq!(Priority::from_str("low"), Some(Priority::Low)); + assert_eq!(Priority::from_str("medium"), Some(Priority::Medium)); + assert_eq!(Priority::from_str("high"), Some(Priority::High)); + assert_eq!(Priority::from_str("critical"), Some(Priority::Critical)); + } + + #[test] + fn test_priority_from_str_abbreviations() { + assert_eq!(Priority::from_str("l"), Some(Priority::Low)); + assert_eq!(Priority::from_str("m"), Some(Priority::Medium)); + assert_eq!(Priority::from_str("h"), Some(Priority::High)); + assert_eq!(Priority::from_str("c"), Some(Priority::Critical)); + } + + #[test] + fn test_priority_from_str_case_insensitive() { + assert_eq!(Priority::from_str("HIGH"), Some(Priority::High)); + assert_eq!(Priority::from_str("High"), Some(Priority::High)); + assert_eq!(Priority::from_str("H"), Some(Priority::High)); + } + + #[test] + fn test_priority_from_str_invalid() { + assert_eq!(Priority::from_str("invalid"), None); + assert_eq!(Priority::from_str(""), None); + assert_eq!(Priority::from_str("x"), None); + } + + #[test] + fn test_priority_score_ordering() { + assert!(Priority::Low.score() < Priority::Medium.score()); + assert!(Priority::Medium.score() < Priority::High.score()); + assert!(Priority::High.score() < Priority::Critical.score()); + } + + // ------------------------------------------------------------------------- + // Task Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_task_new_defaults() { + let task = Task::new(1, "Test task", Priority::Medium); + + assert_eq!(task.id, 1); + assert_eq!(task.title, "Test task"); + assert_eq!(task.priority, Priority::Medium); + assert_eq!(task.status, Status::Todo); + assert!(task.description.is_none()); + assert!(task.tags.is_empty()); + assert!(task.subtasks.is_empty()); + } + + #[test] + fn test_task_add_tag_success() { + let mut task = Task::new(1, "Test", Priority::Low); + + assert!(task.add_tag("rust").is_ok()); + assert_eq!(task.tags, vec!["rust"]); + } + + #[test] + fn test_task_add_tag_duplicate_error() { + let mut task = Task::new(1, "Test", Priority::Low); + + task.add_tag("rust").unwrap(); + let result = task.add_tag("rust"); + + assert!(result.is_err()); + match result { + Err(TaskError::DuplicateTag(tag)) => assert_eq!(tag, "rust"), + _ => panic!("Expected DuplicateTag error"), + } + } + + #[test] + fn test_task_add_subtask() { + let mut task = Task::new(1, "Test", Priority::Low); + + task.add_subtask("Subtask 1"); + task.add_subtask("Subtask 2"); + + assert_eq!(task.subtasks.len(), 2); + assert_eq!(task.subtasks[0].title, "Subtask 1"); + assert!(!task.subtasks[0].completed); + } + + #[test] + fn test_task_complete_subtask_success() { + let mut task = Task::new(1, "Test", Priority::Low); + task.add_subtask("Subtask 1"); + + assert!(task.complete_subtask(0).is_ok()); + assert!(task.subtasks[0].completed); + } + + #[test] + fn test_task_complete_subtask_out_of_bounds() { + let mut task = Task::new(1, "Test", Priority::Low); + task.add_subtask("Subtask 1"); + + let result = task.complete_subtask(5); + + assert!(result.is_err()); + } + + #[test] + fn test_task_progress_no_subtasks_todo() { + let task = Task::new(1, "Test", Priority::Low); + assert_eq!(task.progress_percentage(), 0.0); + } + + #[test] + fn test_task_progress_no_subtasks_done() { + let mut task = Task::new(1, "Test", Priority::Low); + task.status = Status::Done; + assert_eq!(task.progress_percentage(), 100.0); + } + + #[test] + fn test_task_progress_with_subtasks() { + let mut task = Task::new(1, "Test", Priority::Low); + task.add_subtask("Sub 1"); + task.add_subtask("Sub 2"); + task.add_subtask("Sub 3"); + task.add_subtask("Sub 4"); + + task.complete_subtask(0).unwrap(); + task.complete_subtask(1).unwrap(); + + assert_eq!(task.progress_percentage(), 50.0); + } + + #[test] + fn test_task_urgency_score() { + let mut task = Task::new(1, "Test", Priority::High); + task.status = Status::Todo; + task.add_subtask("Incomplete subtask"); + + // priority(3) * 10 + status_todo(10) + 1 incomplete subtask = 41 + assert_eq!(task.calculate_score(), 41); + } + + // ------------------------------------------------------------------------- + // TaskManager Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_manager_create_task_assigns_sequential_ids() { + let mut manager = TaskManager::new(); + + let id1 = manager.create_task("Task 1", Priority::Low); + let id2 = manager.create_task("Task 2", Priority::Low); + let id3 = manager.create_task("Task 3", Priority::Low); + + assert_eq!(id1, 1); + assert_eq!(id2, 2); + assert_eq!(id3, 3); + } + + #[test] + fn test_manager_get_task_success() { + let mut manager = TaskManager::new(); + let id = manager.create_task("Test", Priority::High); + + let task = manager.get_task(id).unwrap(); + + assert_eq!(task.title, "Test"); + assert_eq!(task.priority, Priority::High); + } + + #[test] + fn test_manager_get_task_not_found() { + let manager = TaskManager::new(); + let result = manager.get_task(999); + + assert!(result.is_err()); + match result { + Err(TaskError::NotFound(id)) => assert_eq!(id, 999), + _ => panic!("Expected NotFound error"), + } + } + + #[test] + fn test_manager_delete_task() { + let mut manager = TaskManager::new(); + let id = manager.create_task("To delete", Priority::Low); + + let deleted = manager.delete_task(id).unwrap(); + + assert_eq!(deleted.title, "To delete"); + assert!(manager.get_task(id).is_err()); + } + + #[test] + fn test_manager_update_status() { + let mut manager = TaskManager::new(); + let id = manager.create_task("Test", Priority::Low); + + manager.update_status(id, Status::InProgress).unwrap(); + + assert_eq!(manager.get_task(id).unwrap().status, Status::InProgress); + } + + #[test] + fn test_manager_list_all_sorted_by_urgency() { + let mut manager = TaskManager::new(); + manager.create_task("Low priority", Priority::Low); + manager.create_task("Critical priority", Priority::Critical); + manager.create_task("Medium priority", Priority::Medium); + + let tasks = manager.list_all(); + + assert_eq!(tasks[0].priority, Priority::Critical); + assert_eq!(tasks[2].priority, Priority::Low); + } + + #[test] + fn test_manager_filter_by_status() { + let mut manager = TaskManager::new(); + let id1 = manager.create_task("Task 1", Priority::Low); + let id2 = manager.create_task("Task 2", Priority::Low); + manager.create_task("Task 3", Priority::Low); + + manager.update_status(id1, Status::Done).unwrap(); + manager.update_status(id2, Status::Done).unwrap(); + + let done_tasks = manager.filter_by_status(Status::Done); + + assert_eq!(done_tasks.len(), 2); + } + + #[test] + fn test_manager_search_by_title() { + let mut manager = TaskManager::new(); + manager.create_task("Fix authentication bug", Priority::High); + manager.create_task("Add new feature", Priority::Medium); + manager.create_task("Fix database bug", Priority::High); + + let results = manager.search("fix"); + + assert_eq!(results.len(), 2); + } + + #[test] + fn test_manager_search_case_insensitive() { + let mut manager = TaskManager::new(); + manager.create_task("Authentication Module", Priority::High); + + let results = manager.search("authentication"); + + assert_eq!(results.len(), 1); + } + + #[test] + fn test_manager_statistics() { + let mut manager = TaskManager::new(); + let id1 = manager.create_task("Task 1", Priority::Critical); + let id2 = manager.create_task("Task 2", Priority::High); + manager.create_task("Task 3", Priority::Low); + + manager.update_status(id1, Status::Done).unwrap(); + manager.update_status(id2, Status::InProgress).unwrap(); + + let stats = manager.statistics(); + + assert_eq!(stats.total, 3); + assert_eq!(stats.done, 1); + assert_eq!(stats.in_progress, 1); + assert_eq!(stats.todo, 1); + assert_eq!(stats.critical_priority, 1); + assert_eq!(stats.critical_incomplete, 0); // Critical task is done + } + + // ------------------------------------------------------------------------- + // Command Handler Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_parse_id_success() { + let parts = vec!["show", "42"]; + let id = parse_id(parts.get(1), "Usage: show <id>").unwrap(); + assert_eq!(id, 42); + } + + #[test] + fn test_parse_id_missing() { + let parts: Vec<&str> = vec!["show"]; + let result = parse_id(parts.get(1), "Usage: show <id>"); + assert!(result.is_err()); + } + + #[test] + fn test_parse_id_invalid() { + let parts = vec!["show", "abc"]; + let result = parse_id(parts.get(1), "Usage: show <id>"); + assert!(result.is_err()); + } +}