iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Rust

30天Rust從零到全端系列 第 14

Day 14: 週末專案 - CLI 待辦事項應用

  • 分享至 

  • xImage
  •  

前言

今天我們要整合本週學到的所有概念:結構體、列舉、模式匹配和錯誤處理,建立一個功能完整的命令列待辦事項管理工具。

專案概述

我們將建立一個名為 todo-cli 的命令列工具,以下是 小J PM 提出的相關需求:

  • 新增、完成、刪除待辦事項
  • 列出所有或特定狀態的任務
  • 設定任務優先順序和到期日
  • 資料持久化到檔案
  • 友好的命令列介面
  • 完整的錯誤處理

功能示例

# 新增任務
./todo-cli add "學習 Rust" --priority high --due "2024-01-15"

# 列出所有任務
./todo-cli list

# 完成任務
./todo-cli complete 1

# 刪除任務
./todo-cli delete 2

# 列出高優先順序的任務
./todo-cli list --status pending --priority high

專案結構

先建立專案結構:

day14/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── lib.rs
    ├── task.rs
    ├── storage.rs
    ├── cli.rs
    └── error.rs

步驟 1:設定專案

Cargo.toml

[package]
name = "todo-cli"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "todo-cli"
path = "src/main.rs"

[dependencies]
clap = { version = "4.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
colored = "2.0"
anyhow = "1.0"
thiserror = "1.0"

步驟 2:定義錯誤型別

src/error.rs

use thiserror::Error;

#[derive(Error, Debug)]
pub enum TodoError {
    #[error("任務 ID {id} 不存在")]
    TaskNotFound { id: u32 },
    
    #[error("任務標題不能為空")]
    EmptyTitle,
    
    #[error("無效的優先順序: {priority}")]
    InvalidPriority { priority: String },
    
    #[error("無效的日期格式: {date}")]
    InvalidDate { date: String },
    
    #[error("任務 '{title}' 已經存在")]
    DuplicateTask { title: String },
    
    #[error("任務已經是 {status} 狀態")]
    TaskAlreadyInStatus { status: String },
    
    #[error("IO 錯誤: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("JSON 序列化錯誤: {0}")]
    Json(#[from] serde_json::Error),
    
    #[error("日期解析錯誤: {0}")]
    DateParse(#[from] chrono::ParseError),
}

pub type Result<T> = std::result::Result<T, TodoError>;

步驟 3:定義任務結構體和相關型別

src/task.rs

use crate::error::{Result, TodoError};
use chrono::{DateTime, Local, NaiveDate};
use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TaskStatus {
    Pending,
    InProgress,
    Completed,
    Cancelled,
}

impl fmt::Display for TaskStatus {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            TaskStatus::Pending => write!(f, "待辦"),
            TaskStatus::InProgress => write!(f, "進行中"),
            TaskStatus::Completed => write!(f, "已完成"),
            TaskStatus::Cancelled => write!(f, "已取消"),
        }
    }
}

impl TaskStatus {
    pub fn from_str(s: &str) -> Result<Self> {
        match s.to_lowercase().as_str() {
            "pending" | "待辦" | "p" => Ok(TaskStatus::Pending),
            "progress" | "進行中" | "i" => Ok(TaskStatus::InProgress),
            "completed" | "已完成" | "done" | "c" => Ok(TaskStatus::Completed),
            "cancelled" | "已取消" | "cancel" | "x" => Ok(TaskStatus::Cancelled),
            _ => Err(TodoError::InvalidPriority { 
                priority: s.to_string() 
            }),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Priority {
    Low = 1,
    Medium = 2,
    High = 3,
    Urgent = 4,
}

impl fmt::Display for Priority {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Priority::Low => write!(f, "低"),
            Priority::Medium => write!(f, "中"),
            Priority::High => write!(f, "高"),
            Priority::Urgent => write!(f, "緊急"),
        }
    }
}

impl Priority {
    pub fn from_str(s: &str) -> Result<Self> {
        match s.to_lowercase().as_str() {
            "low" | "l" | "1" | "低" => Ok(Priority::Low),
            "medium" | "med" | "m" | "2" | "中" => Ok(Priority::Medium),
            "high" | "h" | "3" | "高" => Ok(Priority::High),
            "urgent" | "u" | "4" | "緊急" => Ok(Priority::Urgent),
            _ => Err(TodoError::InvalidPriority { 
                priority: s.to_string() 
            }),
        }
    }
    
    pub fn color(&self) -> &'static str {
        match self {
            Priority::Low => "green",
            Priority::Medium => "yellow", 
            Priority::High => "red",
            Priority::Urgent => "magenta",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    pub id: u32,
    pub title: String,
    pub description: Option<String>,
    pub status: TaskStatus,
    pub priority: Priority,
    pub created_at: DateTime<Local>,
    pub updated_at: DateTime<Local>,
    pub due_date: Option<NaiveDate>,
    pub tags: Vec<String>,
}

impl Task {
    pub fn new(id: u32, title: String) -> Result<Self> {
        if title.trim().is_empty() {
            return Err(TodoError::EmptyTitle);
        }

        let now = Local::now();
        Ok(Task {
            id,
            title: title.trim().to_string(),
            description: None,
            status: TaskStatus::Pending,
            priority: Priority::Medium,
            created_at: now,
            updated_at: now,
            due_date: None,
            tags: Vec::new(),
        })
    }
    
    pub fn with_description(mut self, description: String) -> Self {
        self.description = Some(description);
        self.updated_at = Local::now();
        self
    }
    
    pub fn with_priority(mut self, priority: Priority) -> Self {
        self.priority = priority;
        self.updated_at = Local::now();
        self
    }
    
    pub fn with_due_date(mut self, due_date: NaiveDate) -> Self {
        self.due_date = Some(due_date);
        self.updated_at = Local::now();
        self
    }
    
    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
        self.tags = tags;
        self.updated_at = Local::now();
        self
    }
    
    pub fn set_status(&mut self, status: TaskStatus) -> Result<()> {
        if self.status == status {
            return Err(TodoError::TaskAlreadyInStatus {
                status: status.to_string(),
            });
        }
        
        self.status = status;
        self.updated_at = Local::now();
        Ok(())
    }
    
    pub fn set_priority(&mut self, priority: Priority) {
        self.priority = priority;
        self.updated_at = Local::now();
    }
    
    pub fn add_tag(&mut self, tag: String) {
        if !self.tags.contains(&tag) {
            self.tags.push(tag);
            self.updated_at = Local::now();
        }
    }
    
    pub fn remove_tag(&mut self, tag: &str) -> bool {
        if let Some(pos) = self.tags.iter().position(|t| t == tag) {
            self.tags.remove(pos);
            self.updated_at = Local::now();
            true
        } else {
            false
        }
    }
    
    pub fn is_overdue(&self) -> bool {
        if let Some(due_date) = self.due_date {
            Local::now().date_naive() > due_date && self.status != TaskStatus::Completed
        } else {
            false
        }
    }
    
    pub fn days_until_due(&self) -> Option<i64> {
        self.due_date.map(|due_date| {
            (due_date - Local::now().date_naive()).num_days()
        })
    }
    
    pub fn matches_filter(&self, status: Option<&TaskStatus>, priority: Option<&Priority>, tag: Option<&str>) -> bool {
        let status_matches = status.map_or(true, |s| &self.status == s);
        let priority_matches = priority.map_or(true, |p| &self.priority == p);
        let tag_matches = tag.map_or(true, |t| self.tags.iter().any(|tag| tag.contains(t)));
        
        status_matches && priority_matches && tag_matches
    }
}

impl fmt::Display for Task {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let status_symbol = match self.status {
            TaskStatus::Pending => "○",
            TaskStatus::InProgress => "◐",
            TaskStatus::Completed => "●",
            TaskStatus::Cancelled => "✗",
        };
        
        write!(f, "{} [{}] {}", status_symbol, self.id, self.title)?;
        
        if let Some(due_date) = self.due_date {
            let days_until = self.days_until_due().unwrap_or(0);
            if days_until < 0 {
                write!(f, " (逾期 {} 天)", -days_until)?;
            } else if days_until <= 3 {
                write!(f, " ({}天內到期)", days_until)?;
            }
        }
        
        if !self.tags.is_empty() {
            write!(f, " [{}]", self.tags.join(", "))?;
        }
        
        Ok(())
    }
}

步驟 4:實作資料存儲

src/storage.rs

use crate::error::{Result, TodoError};
use crate::task::Task;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Serialize, Deserialize)]
pub struct TodoData {
    pub tasks: HashMap<u32, Task>,
    pub next_id: u32,
}

impl TodoData {
    pub fn new() -> Self {
        TodoData {
            tasks: HashMap::new(),
            next_id: 1,
        }
    }
}

pub struct Storage {
    file_path: PathBuf,
}

impl Storage {
    pub fn new<P: AsRef<Path>>(file_path: P) -> Self {
        Storage {
            file_path: file_path.as_ref().to_path_buf(),
        }
    }
    
    pub fn load(&self) -> Result<TodoData> {
        if !self.file_path.exists() {
            return Ok(TodoData::new());
        }
        
        let content = fs::read_to_string(&self.file_path)?;
        if content.trim().is_empty() {
            return Ok(TodoData::new());
        }
        
        let data: TodoData = serde_json::from_str(&content)?;
        Ok(data)
    }
    
    pub fn save(&self, data: &TodoData) -> Result<()> {
        // 確保父目錄存在
        if let Some(parent) = self.file_path.parent() {
            fs::create_dir_all(parent)?;
        }
        
        let content = serde_json::to_string_pretty(data)?;
        fs::write(&self.file_path, content)?;
        Ok(())
    }
    
    pub fn get_default_path() -> PathBuf {
        dirs::home_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join(".todo-cli")
            .join("tasks.json")
    }
}

pub struct TodoStore {
    storage: Storage,
    data: TodoData,
}

impl TodoStore {
    pub fn new(storage_path: Option<PathBuf>) -> Result<Self> {
        let storage = Storage::new(
            storage_path.unwrap_or_else(Storage::get_default_path)
        );
        let data = storage.load()?;
        
        Ok(TodoStore { storage, data })
    }
    
    pub fn add_task(&mut self, title: String) -> Result<u32> {
        // 檢查是否有重複標題
        for task in self.data.tasks.values() {
            if task.title == title {
                return Err(TodoError::DuplicateTask { title });
            }
        }
        
        let task = Task::new(self.data.next_id, title)?;
        let id = task.id;
        
        self.data.tasks.insert(id, task);
        self.data.next_id += 1;
        
        self.save()?;
        Ok(id)
    }
    
    pub fn get_task(&self, id: u32) -> Result<&Task> {
        self.data.tasks.get(&id).ok_or(TodoError::TaskNotFound { id })
    }
    
    pub fn get_task_mut(&mut self, id: u32) -> Result<&mut Task> {
        self.data.tasks.get_mut(&id).ok_or(TodoError::TaskNotFound { id })
    }
    
    pub fn update_task<F>(&mut self, id: u32, update_fn: F) -> Result<()>
    where
        F: FnOnce(&mut Task) -> Result<()>,
    {
        let task = self.get_task_mut(id)?;
        update_fn(task)?;
        self.save()?;
        Ok(())
    }
    
    pub fn delete_task(&mut self, id: u32) -> Result<Task> {
        let task = self.data.tasks.remove(&id).ok_or(TodoError::TaskNotFound { id })?;
        self.save()?;
        Ok(task)
    }
    
    pub fn list_tasks(&self) -> Vec<&Task> {
        let mut tasks: Vec<_> = self.data.tasks.values().collect();
        tasks.sort_by(|a, b| {
            // 先按狀態排序(未完成的在前)
            match (&a.status, &b.status) {
                (crate::task::TaskStatus::Completed, crate::task::TaskStatus::Completed) |
                (crate::task::TaskStatus::Cancelled, crate::task::TaskStatus::Cancelled) => {},
                (crate::task::TaskStatus::Completed | crate::task::TaskStatus::Cancelled, _) => return std::cmp::Ordering::Greater,
                (_, crate::task::TaskStatus::Completed | crate::task::TaskStatus::Cancelled) => return std::cmp::Ordering::Less,
                _ => {},
            }
            
            // 再按優先順序排序(高優先順序在前)
            b.priority.cmp(&a.priority)
                .then_with(|| a.created_at.cmp(&b.created_at))
        });
        tasks
    }
    
    pub fn list_filtered_tasks(
        &self,
        status: Option<crate::task::TaskStatus>,
        priority: Option<crate::task::Priority>,
        tag: Option<String>,
    ) -> Vec<&Task> {
        self.data.tasks.values()
            .filter(|task| {
                task.matches_filter(
                    status.as_ref(),
                    priority.as_ref(),
                    tag.as_deref(),
                )
            })
            .collect()
    }
    
    pub fn get_statistics(&self) -> TaskStatistics {
        let mut stats = TaskStatistics::default();
        
        for task in self.data.tasks.values() {
            stats.total += 1;
            
            match task.status {
                crate::task::TaskStatus::Pending => stats.pending += 1,
                crate::task::TaskStatus::InProgress => stats.in_progress += 1,
                crate::task::TaskStatus::Completed => stats.completed += 1,
                crate::task::TaskStatus::Cancelled => stats.cancelled += 1,
            }
            
            if task.is_overdue() {
                stats.overdue += 1;
            }
        }
        
        stats
    }
    
    fn save(&self) -> Result<()> {
        self.storage.save(&self.data)
    }
}

#[derive(Debug, Default)]
pub struct TaskStatistics {
    pub total: usize,
    pub pending: usize,
    pub in_progress: usize,
    pub completed: usize,
    pub cancelled: usize,
    pub overdue: usize,
}

impl std::fmt::Display for TaskStatistics {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        writeln!(f, "任務統計:")?;
        writeln!(f, "  總計: {}", self.total)?;
        writeln!(f, "  待辦: {}", self.pending)?;
        writeln!(f, "  進行中: {}", self.in_progress)?;
        writeln!(f, "  已完成: {}", self.completed)?;
        writeln!(f, "  已取消: {}", self.cancelled)?;
        if self.overdue > 0 {
            writeln!(f, "  ⚠️ 逾期: {}", self.overdue)?;
        }
        Ok(())
    }
}

// 添加 dirs crate 到 Cargo.toml
// [dependencies]
// dirs = "5.0"

步驟 5:命令列介面

src/cli.rs

use crate::task::{Priority, TaskStatus};
use chrono::NaiveDate;
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "todo-cli")]
#[command(about = "一個簡單而強大的待辦事項管理工具")]
#[command(version)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,
    
    /// 指定資料檔案路徑
    #[arg(long, global = true)]
    pub file: Option<std::path::PathBuf>,
}

#[derive(Subcommand)]
pub enum Commands {
    /// 新增任務
    Add {
        /// 任務標題
        title: String,
        
        /// 任務描述
        #[arg(short, long)]
        description: Option<String>,
        
        /// 優先順序 (low, medium, high, urgent)
        #[arg(short, long)]
        priority: Option<String>,
        
        /// 到期日 (YYYY-MM-DD)
        #[arg(short = 'd', long)]
        due: Option<String>,
        
        /// 標籤 (可多次使用)
        #[arg(short, long)]
        tag: Vec<String>,
    },
    
    /// 列出任務
    List {
        /// 按狀態篩選
        #[arg(short, long)]
        status: Option<String>,
        
        /// 按優先順序篩選
        #[arg(short, long)]
        priority: Option<String>,
        
        /// 按標籤篩選
        #[arg(short, long)]
        tag: Option<String>,
        
        /// 只顯示逾期任務
        #[arg(long)]
        overdue: bool,
    },
    
    /// 完成任務
    Complete {
        /// 任務 ID
        id: u32,
    },
    
    /// 開始任務
    Start {
        /// 任務 ID
        id: u32,
    },
    
    /// 取消任務
    Cancel {
        /// 任務 ID
        id: u32,
    },
    
    /// 刪除任務
    Delete {
        /// 任務 ID
        id: u32,
        
        /// 強制刪除(不需確認)
        #[arg(short, long)]
        force: bool,
    },
    
    /// 編輯任務
    Edit {
        /// 任務 ID
        id: u32,
        
        /// 新標題
        #[arg(short, long)]
        title: Option<String>,
        
        /// 新描述
        #[arg(short = 'D', long)]
        description: Option<String>,
        
        /// 新優先順序
        #[arg(short, long)]
        priority: Option<String>,
        
        /// 新到期日
        #[arg(short = 'd', long)]
        due: Option<String>,
    },
    
    /// 為任務添加標籤
    Tag {
        /// 任務 ID
        id: u32,
        
        /// 要添加的標籤
        #[arg(short, long)]
        add: Vec<String>,
        
        /// 要移除的標籤
        #[arg(short, long)]
        remove: Vec<String>,
    },
    
    /// 顯示統計資訊
    Stats,
    
    /// 顯示任務詳情
    Show {
        /// 任務 ID
        id: u32,
    },
}

impl Commands {
    pub fn parse_priority(priority_str: &str) -> crate::error::Result<Priority> {
        Priority::from_str(priority_str)
    }
    
    pub fn parse_status(status_str: &str) -> crate::error::Result<TaskStatus> {
        TaskStatus::from_str(status_str)
    }
    
    pub fn parse_date(date_str: &str) -> crate::error::Result<NaiveDate> {
        NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
            .map_err(|_| crate::error::TodoError::InvalidDate {
                date: date_str.to_string(),
            })
    }
}

步驟 6:主要邏輯

src/lib.rs

pub mod cli;
pub mod error;
pub mod storage;
pub mod task;

use crate::cli::{Cli, Commands};
use crate::error::Result;
use crate::storage::TodoStore;
use crate::task::{Priority, TaskStatus};
use colored::*;
use std::io::{self, Write};

pub fn run(cli: Cli) -> Result<()> {
    let mut store = TodoStore::new(cli.file)?;
    
    match cli.command {
        Commands::Add { title, description, priority, due, tag } => {
            let id = store.add_task(title.clone())?;
            
            // 設定可選屬性
            if let Some(desc) = description {
                store.update_task(id, |task| {
                    task.description = Some(desc);
                    Ok(())
                })?;
            }
            
            if let Some(priority_str) = priority {
                let priority = Commands::parse_priority(&priority_str)?;
                store.update_task(id, |task| {
                    task.set_priority(priority);
                    Ok(())
                })?;
            }
            
            if let Some(due_str) = due {
                let due_date = Commands::parse_date(&due_str)?;
                store.update_task(id, |task| {
                    task.due_date = Some(due_date);
                    Ok(())
                })?;
            }
            
            for tag_name in tag {
                store.update_task(id, |task| {
                    task.add_tag(tag_name);
                    Ok(())
                })?;
            }
            
            println!("{} 任務已新增: {} (ID: {})", 
                     "✓".green(), title.bold(), id.to_string().cyan());
        },
        
        Commands::List { status, priority, tag, overdue } => {
            let status_filter = if let Some(s) = status {
                Some(Commands::parse_status(&s)?)
            } else {
                None
            };
            
            let priority_filter = if let Some(p) = priority {
                Some(Commands::parse_priority(&p)?)
            } else {
                None
            };
            
            let mut tasks = store.list_filtered_tasks(status_filter, priority_filter, tag);
            
            if overdue {
                tasks.retain(|task| task.is_overdue());
            }
            
            if tasks.is_empty() {
                println!("{}", "沒有符合條件的任務".yellow());
                return Ok(());
            }
            
            print_task_list(&tasks);
        },
        
        Commands::Complete { id } => {
            store.update_task(id, |task| {
                task.set_status(TaskStatus::Completed)
            })?;
            
            let task = store.get_task(id)?;
            println!("{} 任務已完成: {}", "✓".green(), task.title.bold());
        },
        
        Commands::Start { id } => {
            store.update_task(id, |task| {
                task.set_status(TaskStatus::InProgress)
            })?;
            
            let task = store.get_task(id)?;
            println!("{} 任務已開始: {}", "▶".blue(), task.title.bold());
        },
        
        Commands::Cancel { id } => {
            store.update_task(id, |task| {
                task.set_status(TaskStatus::Cancelled)
            })?;
            
            let task = store.get_task(id)?;
            println!("{} 任務已取消: {}", "✗".red(), task.title.bold());
        },
        
        Commands::Delete { id, force } => {
            let task = store.get_task(id)?;
            
            if !force {
                print!("確定要刪除任務 '{}' 嗎? (y/N): ", task.title);
                io::stdout().flush().unwrap();
                
                let mut input = String::new();
                io::stdin().read_line(&mut input).unwrap();
                
                if !input.trim().to_lowercase().starts_with('y') {
                    println!("取消刪除");
                    return Ok(());
                }
            }
            
            let deleted_task = store.delete_task(id)?;
            println!("{} 任務已刪除: {}", "🗑".yellow(), deleted_task.title.bold());
        },
        
        Commands::Edit { id, title, description, priority, due } => {
            store.update_task(id, |task| {
                if let Some(new_title) = title {
                    if new_title.trim().is_empty() {
                        return Err(crate::error::TodoError::EmptyTitle);
                    }
                    task.title = new_title.trim().to_string();
                }
                
                if let Some(new_desc) = description {
                    task.description = Some(new_desc);
                }
                
                if let Some(priority_str) = priority {
                    let new_priority = Commands::parse_priority(&priority_str)?;
                    task.set_priority(new_priority);
                }
                
                if let Some(due_str) = due {
                    let due_date = Commands::parse_date(&due_str)?;
                    task.due_date = Some(due_date);
                }
                
                Ok(())
            })?;
            
            let task = store.get_task(id)?;
            println!("{} 任務已更新: {}", "✓".green(), task.title.bold());
        },
        
        Commands::Tag { id, add, remove } => {
            store.update_task(id, |task| {
                for tag in add {
                    task.add_tag(tag);
                }
                
                for tag in remove {
                    task.remove_tag(&tag);
                }
                
                Ok(())
            })?;
            
            let task = store.get_task(id)?;
            println!("{} 標籤已更新: {}", "🏷".yellow(), task.title.bold());
        },
        
        Commands::Stats => {
            let stats = store.get_statistics();
            println!("{}", stats);
        },
        
        Commands::Show { id } => {
            let task = store.get_task(id)?;
            print_task_detail(task);
        },
    }
    
    Ok(())
}

fn print_task_list(tasks: &[&crate::task::Task]) {
    println!("{} 任務列表 {}", "=".repeat(10), "=".repeat(10));
    
    for task in tasks {
        let status_color = match task.status {
            TaskStatus::Pending => "white",
            TaskStatus::InProgress => "blue",
            TaskStatus::Completed => "green",
            TaskStatus::Cancelled => "red",
        };
        
        let priority_indicator = match task.priority {
            Priority::Low => "●".green(),
            Priority::Medium => "●".yellow(),
            Priority::High => "●".red(),
            Priority::Urgent => "●".purple(),
        };
        
        print!("{} ", priority_indicator);
        
        if task.is_overdue() {
            print!("{} ", "⚠️ ".red());
        }
        
        println!("{}", task.to_string().color(status_color));
    }
}

fn print_task_detail(task: &crate::task::Task) {
    println!("{}", "任務詳情".bold().underline());
    println!("ID: {}", task.id.to_string().cyan());
    println!("標題: {}", task.title.bold());
    
    if let Some(desc) = &task.description {
        println!("描述: {}", desc);
    }
    
    let status_color = match task.status {
        TaskStatus::Pending => "white",
        TaskStatus::InProgress => "blue", 
        TaskStatus::Completed => "green",
        TaskStatus::Cancelled => "red",
    };
    
    println!("狀態: {}", task.status.to_string().color(status_color));
    println!("優先順序: {}", task.priority.to_string().color(task.priority.color()));
    
    if let Some(due_date) = task.due_date {
        let days_until = task.days_until_due().unwrap_or(0);
        if task.is_overdue() {
            println!("到期日: {} {}", due_date.to_string().red(), format!("(逾期 {} 天)", -days_until).red());
        } else if days_until <= 3 {
            println!("到期日: {} {}", due_date.to_string().yellow(), format!("({}天內到期)", days_until).yellow());
        } else {
            println!("到期日: {}", due_date);
        }
    }
    
    if !task.tags.is_empty() {
        println!("標籤: {}", task.tags.join(", ").cyan());
    }
    
    println!("建立時間: {}", task.created_at.format("%Y-%m-%d %H:%M:%S"));
    println!("更新時間: {}", task.updated_at.format("%Y-%m-%d %H:%M:%S"));
}

步驟 7:主程式進入點

src/main.rs

use clap::Parser;
use todo_cli::cli::Cli;
use todo_cli::run;

fn main() {
    let cli = Cli::parse();
    
    if let Err(e) = run(cli) {
        eprintln!("錯誤: {}", e);
        std::process::exit(1);
    }
}

步驟 8:更新 Cargo.toml 依賴

[package]
name = "todo-cli"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "todo-cli"
path = "src/main.rs"

[dependencies]
clap = { version = "4.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
colored = "2.0"
anyhow = "1.0"
thiserror = "1.0"
dirs = "5.0"

步驟 9:建置和測試

建置專案:

cd day14
cargo build --release

測試基本功能:

# 新增任務
./target/release/todo-cli add "學習 Rust" --priority high --due "2024-01-15"

# 列出所有任務
./target/release/todo-cli list

# 開始任務
./target/release/todo-cli start 1

# 完成任務
./target/release/todo-cli complete 1

# 顯示統計
./target/release/todo-cli stats

步驟 10:進階功能和優化

添加匯入/匯出功能

// 在 Commands 枚舉中添加
Export {
    /// 匯出檔案路徑
    #[arg(short, long)]
    output: std::path::PathBuf,
    
    /// 匯出格式 (json, csv)
    #[arg(short, long, default_value = "json")]
    format: String,
},

Import {
    /// 匯入檔案路徑
    #[arg(short, long)]
    input: std::path::PathBuf,
    
    /// 匯入格式 (json, csv)
    #[arg(short, long, default_value = "json")]
    format: String,
},

添加搜尋功能

Search {
    /// 搜尋關鍵字
    query: String,
    
    /// 在標題中搜尋
    #[arg(long)]
    title: bool,
    
    /// 在描述中搜尋
    #[arg(long)]
    description: bool,
    
    /// 在標籤中搜尋
    #[arg(long)]
    tags: bool,
},

cli 使用舉例

# 基本用法
todo-cli add "完成 Rust 專案" --priority urgent --due "2024-01-20"
todo-cli add "買菜" --tag 生活 --tag 購物
todo-cli add "讀書" --description "閱讀 Rust 程式設計語言" --priority medium

# 列出不同狀態的任務
todo-cli list --status pending
todo-cli list --priority high
todo-cli list --overdue

# 管理任務
todo-cli start 1
todo-cli complete 1
todo-cli edit 2 --title "買菜和做飯" --priority high

# 標籤管理
todo-cli tag 2 --add 緊急 --remove 購物

# 查看詳情和統計
todo-cli show 1
todo-cli stats

# 刪除任務
todo-cli delete 3 --force

Follow

1. 基礎練習:功能擴展

為 CLI 工具添加以下功能:

// 1. 添加任務模板功能
todo-cli template create "每日站會" --description "參加團隊每日站會" --priority medium --tag 工作

// 2. 添加任務重複功能
todo-cli repeat 1 --interval daily --count 7

// 3. 添加任務依賴關係
todo-cli depend 2 --requires 1  // 任務2依賴任務1完成

2. 進階練習:數據分析

實作以下分析功能:

// 1. 生產力報告
todo-cli report --period week  // 週報
todo-cli report --period month // 月報

// 2. 時間追蹤
// 為任務添加時間追蹤功能
todo-cli time start 1  // 開始計時
todo-cli time stop 1   // 停止計時
todo-cli time report   // 時間報告

// 3. 任務建議
todo-cli suggest  // 根據歷史數據建議下一個任務

3. 挑戰練習:整合功能

// 1. 與日曆整合
todo-cli calendar sync  // 同步到系統日曆

// 2. 提醒功能
todo-cli remind 1 --before "1 hour"  // 任務提醒

// 3. 團隊協作
todo-cli share 1 --user "teammate@example.com"  // 分享任務
todo-cli sync --server "http://todo-server.com"  // 同步到服務器

總結

第三部分將更加注重實用性和效能,我們會學習如何高效地處理和轉換資料,並建立更複雜的應用程式!


上一篇
Day 13: 錯誤處理基礎
下一篇
Day 15: 集合:高效的資料處理
系列文
30天Rust從零到全端15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言