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}