tempo-cli 0.4.0

Automatic project time tracking CLI tool with beautiful terminal interface
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Project {
    pub id: Option<i64>,
    pub name: String,
    pub path: PathBuf,
    pub git_hash: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub is_archived: bool,
    pub description: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProjectStatus {
    Active,
    Archived,
    Tracking,
    Idle,
}

impl Project {
    pub fn new(name: String, path: PathBuf) -> Self {
        let now = Utc::now();
        Self {
            id: None,
            name,
            path,
            git_hash: None,
            created_at: now,
            updated_at: now,
            is_archived: false,
            description: None,
        }
    }

    pub fn with_git_hash(mut self, git_hash: Option<String>) -> Self {
        self.git_hash = git_hash;
        self
    }

    pub fn with_description(mut self, description: Option<String>) -> Self {
        self.description = description;
        self
    }

    pub fn archive(&mut self) {
        self.is_archived = true;
        self.updated_at = Utc::now();
    }

    pub fn unarchive(&mut self) {
        self.is_archived = false;
        self.updated_at = Utc::now();
    }

    pub fn update_path(&mut self, new_path: PathBuf) {
        self.path = new_path;
        self.updated_at = Utc::now();
    }

    pub fn is_git_project(&self) -> bool {
        self.path.join(".git").exists()
    }

    pub fn has_timetrack_marker(&self) -> bool {
        self.path.join(".timetrack").exists()
    }

    pub fn get_canonical_path(&self) -> anyhow::Result<PathBuf> {
        Ok(self.path.canonicalize()?)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkedProject {
    pub id: Option<i64>,
    pub name: String,
    pub description: Option<String>,
    pub created_at: DateTime<Utc>,
    pub is_active: bool,
    pub member_projects: Vec<Project>,
}

impl LinkedProject {
    pub fn new(name: String) -> Self {
        Self {
            id: None,
            name,
            description: None,
            created_at: Utc::now(),
            is_active: true,
            member_projects: Vec::new(),
        }
    }

    pub fn add_project(&mut self, project: Project) {
        self.member_projects.push(project);
    }

    pub fn remove_project(&mut self, project_id: i64) {
        self.member_projects.retain(|p| p.id != Some(project_id));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_project_new() {
        let path = PathBuf::from("/tmp/test-project");
        let project = Project::new("Test Project".to_string(), path.clone());

        assert_eq!(project.name, "Test Project");
        assert_eq!(project.path, path);
        assert!(!project.is_archived);
        assert!(project.git_hash.is_none());
    }

    #[test]
    fn test_project_archive_unarchive() {
        let mut project = Project::new("Test".to_string(), PathBuf::from("/tmp"));

        assert!(!project.is_archived);

        project.archive();
        assert!(project.is_archived);

        project.unarchive();
        assert!(!project.is_archived);
    }

    #[test]
    fn test_project_update_path() {
        let mut project = Project::new("Test".to_string(), PathBuf::from("/tmp/old"));
        let new_path = PathBuf::from("/tmp/new");

        project.update_path(new_path.clone());
        assert_eq!(project.path, new_path);
    }

    #[test]
    fn test_linked_project_management() {
        let mut linked = LinkedProject::new("Meta Project".to_string());
        let p1 = Project::new("P1".to_string(), PathBuf::from("/p1"))
            .with_git_hash(Some("hash1".to_string()));

        linked.add_project(p1.clone());
        assert_eq!(linked.member_projects.len(), 1);
        assert_eq!(linked.member_projects[0].name, "P1");
    }
}