use std::path::PathBuf; use crate::config::TargetConfig; use crate::error::{EmpveError, Result}; /// Manages all paths used by empeve following XDG conventions #[derive(Debug, Clone)] pub struct Paths { /// Base config directory (~/.config/empeve) pub config_dir: PathBuf, /// Config file path (~/.config/empeve/config.toml) pub config_file: PathBuf, /// Directory for cloned repos (~/.config/empeve/repos) pub repos_dir: PathBuf, /// Directory for external catalog TOML files (~/.config/empeve/catalogs) pub catalogs_dir: PathBuf, /// mpv scripts directory (~/.config/mpv/scripts) pub mpv_scripts_dir: PathBuf, /// mpv script-opts directory (~/.config/mpv/script-opts) pub mpv_script_opts_dir: PathBuf, /// mpv fonts directory (~/.config/mpv/fonts) pub mpv_fonts_dir: PathBuf, /// mpv shaders directory (~/.config/mpv/shaders) pub mpv_shaders_dir: PathBuf, /// Lockfile path (~/.config/empeve/empeve.lock) pub lockfile: PathBuf, } impl Paths { /// Create a new Paths instance using XDG conventions pub fn new() -> Result { let config_base = dirs::config_dir() .ok_or_else(|| EmpveError::Config("Could not determine config directory".into()))?; let config_dir = config_base.join("empeve"); let mpv_dir = config_base.join("mpv"); Ok(Self { config_file: config_dir.join("config.toml"), repos_dir: config_dir.join("repos"), catalogs_dir: config_dir.join("catalogs"), lockfile: config_dir.join("empeve.lock"), config_dir, mpv_scripts_dir: mpv_dir.join("scripts"), mpv_script_opts_dir: mpv_dir.join("script-opts"), mpv_fonts_dir: mpv_dir.join("fonts"), mpv_shaders_dir: mpv_dir.join("shaders"), }) } /// Create a Paths instance with a custom config file path pub fn with_config_file(config_file: PathBuf) -> Result { let mut paths = Self::new()?; paths.config_file = config_file; Ok(paths) } /// Ensure all required directories exist pub fn ensure_directories(&self) -> Result<()> { std::fs::create_dir_all(&self.config_dir)?; std::fs::create_dir_all(&self.repos_dir)?; std::fs::create_dir_all(&self.mpv_scripts_dir)?; Ok(()) } /// Get the local path for a repository based on its identifier pub fn repo_path(&self, identifier: &str) -> PathBuf { let sanitized_name = sanitize_repo_name(identifier); self.repos_dir.join(sanitized_name) } } impl Default for Paths { fn default() -> Self { Self::new().expect("Failed to determine config paths") } } /// Sanitize a repository identifier for use as a directory name fn sanitize_repo_name(identifier: &str) -> String { identifier .trim_start_matches("https://") .trim_start_matches("http://") .trim_start_matches("git@") .replace("github.com/", "") .replace("github.com:", "") .replace(".git", "") .replace('/', "_") .replace(':', "_") } /// Known mpv configuration locations to auto-detect const KNOWN_MPV_CONFIGS: &[(&str, &str)] = &[ ("mpv", "mpv"), ("jellyfin-mpv-shim", "jellyfin-mpv-shim"), ("celluloid", "celluloid"), ("gnome-mpv", "gnome-mpv"), ]; /// Flatpak mpv config locations const FLATPAK_MPV_CONFIGS: &[(&str, &str)] = &[ ("mpv-flatpak", ".var/app/io.mpv.Mpv/config/mpv"), ]; /// Detected mpv config folder #[derive(Debug, Clone)] pub struct DetectedTarget { /// Display name for the target pub name: String, /// Path to the config folder pub path: PathBuf, /// Whether this folder has existing scripts pub has_scripts: bool, /// Number of existing scripts found pub script_count: usize, } impl DetectedTarget { /// Convert to a TargetConfig pub fn to_target_config(&self) -> TargetConfig { TargetConfig::new(&self.name, &self.path) } } /// Detect all mpv configuration folders on the system pub fn detect_mpv_configs() -> Vec { let mut targets = Vec::new(); // Get config base directory let config_base = match dirs::config_dir() { Some(dir) => dir, None => return targets, }; // Check known XDG config locations for (name, subdir) in KNOWN_MPV_CONFIGS { let path = config_base.join(subdir); if path.exists() && path.is_dir() { let (has_scripts, script_count) = check_scripts_dir(&path); targets.push(DetectedTarget { name: name.to_string(), path, has_scripts, script_count, }); } } // Check Flatpak locations if let Some(home) = dirs::home_dir() { for (name, subdir) in FLATPAK_MPV_CONFIGS { let path = home.join(subdir); if path.exists() && path.is_dir() { let (has_scripts, script_count) = check_scripts_dir(&path); targets.push(DetectedTarget { name: name.to_string(), path, has_scripts, script_count, }); } } } targets } /// Check if a mpv config folder has a scripts directory with scripts fn check_scripts_dir(mpv_path: &PathBuf) -> (bool, usize) { let scripts_dir = mpv_path.join("scripts"); if !scripts_dir.exists() || !scripts_dir.is_dir() { return (false, 0); } let count = std::fs::read_dir(&scripts_dir) .map(|entries| { entries .filter_map(|e| e.ok()) .filter(|e| { let name = e.file_name().to_string_lossy().to_string(); !name.starts_with('.') && (name.ends_with(".lua") || name.ends_with(".js") || e.path().is_dir()) }) .count() }) .unwrap_or(0); (count > 0, count) } #[cfg(test)] mod tests { use super::*; #[test] fn test_sanitize_repo_name() { assert_eq!(sanitize_repo_name("user/repo"), "user_repo"); assert_eq!( sanitize_repo_name("https://github.com/user/repo.git"), "user_repo" ); assert_eq!( sanitize_repo_name("git@github.com:user/repo.git"), "user_repo" ); } }