mycelium-manager 0.2.3

A robust, production-grade task/plan manager CLI (binary: myc)
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FollowupStatus {
    Open,
    InProgress,
    Done,
    Wontfix,
}

impl fmt::Display for FollowupStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl FromStr for FollowupStatus {
    type Err = crate::error::MyceliumError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "open" => Ok(FollowupStatus::Open),
            "in_progress" | "in-progress" => Ok(FollowupStatus::InProgress),
            "done" => Ok(FollowupStatus::Done),
            "wontfix" | "won't-fix" | "wont-fix" => Ok(FollowupStatus::Wontfix),
            _ => Err(crate::error::MyceliumError::InvalidInput(format!(
                "Invalid follow-up status: {}. Use: open, in_progress, done, wontfix",
                s
            ))),
        }
    }
}

impl FollowupStatus {
    pub fn as_str(&self) -> &'static str {
        match self {
            FollowupStatus::Open => "open",
            FollowupStatus::InProgress => "in_progress",
            FollowupStatus::Done => "done",
            FollowupStatus::Wontfix => "wontfix",
        }
    }

    pub fn emoji(&self) -> &'static str {
        match self {
            FollowupStatus::Open => "📌",
            FollowupStatus::InProgress => "🔄",
            FollowupStatus::Done => "✅",
            FollowupStatus::Wontfix => "🚫",
        }
    }

    pub fn is_open(&self) -> bool {
        matches!(self, FollowupStatus::Open | FollowupStatus::InProgress)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Followup {
    pub id: i64,
    pub body: String,
    pub title: Option<String>,
    pub status: FollowupStatus,
    pub closure_reason: Option<String>,
    pub created_at: DateTime<Local>,
    pub closed_at: Option<DateTime<Local>>,
}

impl Followup {
    pub fn display_title(&self) -> String {
        if let Some(t) = &self.title {
            if !t.is_empty() {
                return t.clone();
            }
        }
        let trimmed = self.body.trim();
        if trimmed.chars().count() <= 60 {
            trimmed.to_string()
        } else {
            let truncated: String = trimmed.chars().take(60).collect();
            format!("{}…", truncated)
        }
    }
}