Skip to main content

xbp_cli/
profile.rs

1use crate::codetime::discover_xbp_projects as discover_codetime_xbp_projects;
2use crate::config::global_xbp_paths;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ProjectInfo {
10    pub path: PathBuf,
11    pub name: String,
12    pub last_accessed: DateTime<Utc>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
16pub struct Profile {
17    pub last_project_path: Option<PathBuf>,
18    #[serde(default)]
19    pub recent_projects: Vec<ProjectInfo>,
20}
21
22impl Profile {
23    pub fn get_profile_path() -> Result<PathBuf, String> {
24        Ok(global_xbp_paths()?.root_dir.join("profile.yaml"))
25    }
26
27    pub fn load() -> Result<Self, String> {
28        let profile_path = Self::get_profile_path()?;
29
30        if !profile_path.exists() {
31            #[cfg(target_os = "windows")]
32            if let Some(legacy_path) = legacy_windows_profile_path() {
33                if legacy_path.exists() {
34                    let content = fs::read_to_string(&legacy_path)
35                        .map_err(|e| format!("Failed to read legacy profile: {}", e))?;
36                    let profile: Profile = serde_yaml::from_str(&content)
37                        .map_err(|e| format!("Failed to parse legacy profile: {}", e))?;
38                    let _ = profile.save();
39                    return Ok(profile);
40                }
41            }
42            return Ok(Profile::default());
43        }
44
45        let content = fs::read_to_string(&profile_path)
46            .map_err(|e| format!("Failed to read profile: {}", e))?;
47
48        serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse profile: {}", e))
49    }
50
51    pub fn save(&self) -> Result<(), String> {
52        let profile_path = Self::get_profile_path()?;
53
54        let yaml = serde_yaml::to_string(self)
55            .map_err(|e| format!("Failed to serialize profile: {}", e))?;
56
57        fs::write(&profile_path, yaml).map_err(|e| format!("Failed to write profile: {}", e))
58    }
59
60    pub fn update_last_project(&mut self, path: PathBuf, name: String) {
61        self.last_project_path = Some(path.clone());
62
63        let now = Utc::now();
64
65        if let Some(existing) = self.recent_projects.iter_mut().find(|p| p.path == path) {
66            existing.last_accessed = now;
67            existing.name = name;
68        } else {
69            self.recent_projects.push(ProjectInfo {
70                path,
71                name,
72                last_accessed: now,
73            });
74        }
75
76        self.recent_projects
77            .sort_by_key(|project| std::cmp::Reverse(project.last_accessed));
78
79        if self.recent_projects.len() > 10 {
80            self.recent_projects.truncate(10);
81        }
82    }
83}
84
85#[cfg(target_os = "windows")]
86fn legacy_windows_profile_path() -> Option<PathBuf> {
87    dirs::config_dir().map(|config_dir| config_dir.join("xbp").join("profile.yaml"))
88}
89
90pub fn find_all_xbp_projects() -> Vec<ProjectInfo> {
91    discover_codetime_xbp_projects(std::env::current_dir().ok().as_deref())
92        .into_iter()
93        .map(|project| ProjectInfo {
94            path: PathBuf::from(project.root),
95            name: project.name,
96            last_accessed: Utc::now(),
97        })
98        .collect()
99}
100
101pub fn rank_projects_by_proximity(
102    mut projects: Vec<ProjectInfo>,
103    current_dir: PathBuf,
104) -> Vec<ProjectInfo> {
105    projects.sort_by_key(|project| {
106        let common_components = current_dir
107            .components()
108            .zip(project.path.components())
109            .take_while(|(a, b)| a == b)
110            .count();
111
112        std::cmp::Reverse(common_components)
113    });
114
115    projects
116}