use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectInfo {
pub path: PathBuf,
pub name: String,
pub last_accessed: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Profile {
pub last_project_path: Option<PathBuf>,
#[serde(default)]
pub recent_projects: Vec<ProjectInfo>,
}
impl Profile {
pub fn get_profile_path() -> Result<PathBuf, String> {
let config_dir = if cfg!(target_os = "windows") {
dirs::config_dir()
.ok_or_else(|| "Could not determine config directory".to_string())?
} else {
dirs::config_dir()
.ok_or_else(|| "Could not determine config directory".to_string())?
};
let xbp_config_dir = config_dir.join("xbp");
if !xbp_config_dir.exists() {
fs::create_dir_all(&xbp_config_dir)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
Ok(xbp_config_dir.join("profile.yaml"))
}
pub fn load() -> Result<Self, String> {
let profile_path = Self::get_profile_path()?;
if !profile_path.exists() {
return Ok(Profile::default());
}
let content = fs::read_to_string(&profile_path)
.map_err(|e| format!("Failed to read profile: {}", e))?;
serde_yaml::from_str(&content)
.map_err(|e| format!("Failed to parse profile: {}", e))
}
pub fn save(&self) -> Result<(), String> {
let profile_path = Self::get_profile_path()?;
let yaml = serde_yaml::to_string(self)
.map_err(|e| format!("Failed to serialize profile: {}", e))?;
fs::write(&profile_path, yaml)
.map_err(|e| format!("Failed to write profile: {}", e))
}
pub fn update_last_project(&mut self, path: PathBuf, name: String) {
self.last_project_path = Some(path.clone());
let now = Utc::now();
if let Some(existing) = self.recent_projects.iter_mut().find(|p| p.path == path) {
existing.last_accessed = now;
existing.name = name;
} else {
self.recent_projects.push(ProjectInfo {
path,
name,
last_accessed: now,
});
}
self.recent_projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
if self.recent_projects.len() > 10 {
self.recent_projects.truncate(10);
}
}
}
pub fn find_all_xbp_projects() -> Vec<ProjectInfo> {
let mut projects = Vec::new();
let search_dirs = get_search_directories();
for search_dir in search_dirs {
if !search_dir.exists() {
continue;
}
for entry in WalkDir::new(&search_dir)
.max_depth(5)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.file_name() == Some(std::ffi::OsStr::new(".xbp")) && path.is_dir() {
let xbp_json = path.join("xbp.json");
if xbp_json.exists() {
if let Some(parent) = path.parent() {
let name = extract_project_name(&xbp_json.as_path())
.unwrap_or_else(|| {
parent
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string()
});
projects.push(ProjectInfo {
path: parent.to_path_buf(),
name,
last_accessed: Utc::now(),
});
}
}
} else if path.file_name() == Some(std::ffi::OsStr::new("xbp.json")) && path.is_file() {
if let Some(parent) = path.parent() {
if !parent.join(".xbp").join("xbp.json").exists() {
let name = extract_project_name(path.as_ref())
.unwrap_or_else(|| {
parent
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string()
});
projects.push(ProjectInfo {
path: parent.to_path_buf(),
name,
last_accessed: Utc::now(),
});
}
}
}
}
}
projects.sort_by(|a, b| a.name.cmp(&b.name));
projects.dedup_by(|a, b| a.path == b.path);
projects
}
fn get_search_directories() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(home) = dirs::home_dir() {
dirs.push(home.join("projects"));
dirs.push(home.join("dev"));
dirs.push(home.join("Documents"));
dirs.push(home.join("src"));
dirs.push(home.clone());
}
if cfg!(target_os = "windows") {
if let Ok(current_dir) = std::env::current_dir() {
if let Some(root) = current_dir.ancestors().last() {
dirs.push(root.to_path_buf());
}
}
}
dirs
}
fn extract_project_name(xbp_json_path: &std::path::Path) -> Option<String> {
let content = fs::read_to_string(xbp_json_path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
json.get("project_name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
pub fn rank_projects_by_proximity(
mut projects: Vec<ProjectInfo>,
current_dir: PathBuf,
) -> Vec<ProjectInfo> {
projects.sort_by_key(|project| {
let common_components = current_dir
.components()
.zip(project.path.components())
.take_while(|(a, b)| a == b)
.count();
std::cmp::Reverse(common_components)
});
projects
}