xbp 10.30.1

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use crate::codetime::discover_xbp_projects as discover_codetime_xbp_projects;
use crate::config::global_xbp_paths;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

#[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> {
        Ok(global_xbp_paths()?.root_dir.join("profile.yaml"))
    }

    pub fn load() -> Result<Self, String> {
        let profile_path = Self::get_profile_path()?;

        if !profile_path.exists() {
            #[cfg(target_os = "windows")]
            if let Some(legacy_path) = legacy_windows_profile_path() {
                if legacy_path.exists() {
                    let content = fs::read_to_string(&legacy_path)
                        .map_err(|e| format!("Failed to read legacy profile: {}", e))?;
                    let profile: Profile = serde_yaml::from_str(&content)
                        .map_err(|e| format!("Failed to parse legacy profile: {}", e))?;
                    let _ = profile.save();
                    return Ok(profile);
                }
            }
            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_key(|project| std::cmp::Reverse(project.last_accessed));

        if self.recent_projects.len() > 10 {
            self.recent_projects.truncate(10);
        }
    }
}

#[cfg(target_os = "windows")]
fn legacy_windows_profile_path() -> Option<PathBuf> {
    dirs::config_dir().map(|config_dir| config_dir.join("xbp").join("profile.yaml"))
}

pub fn find_all_xbp_projects() -> Vec<ProjectInfo> {
    discover_codetime_xbp_projects(std::env::current_dir().ok().as_deref())
        .into_iter()
        .map(|project| ProjectInfo {
            path: PathBuf::from(project.root),
            name: project.name,
            last_accessed: Utc::now(),
        })
        .collect()
}

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
}