Files
empeve/src/paths.rs
vikingowl 6f714e58fa Implement comprehensive improvement roadmap (Phases 0-4)
Phase 0 - Quick fixes:
- Fix catalog entries() return type (removed extra indirection)
- Fix welcome string (mpv-mgr → empeve)
- Fix HEAD detachment on update (branch-aware fast-forward)
- Add fetch_rev with branch detection

Phase 1 - Git model ("rev means rev"):
- Add RevType enum (Commit/Tag/Branch/Default)
- Add UpdateResult enum for update outcomes
- Implement clone_with_rev for proper revision checkout
- Pinned repos (commits/tags) skip auto-update

Phase 2 - Discovery & install fidelity:
- Support init.lua and named entry points for multi-file scripts
- Better asset mapping with prefix matching for configs
- Proactive target directory creation

Phase 3 - UX and quality-of-life:
- Add --verbose flag to status command
- Add 'empeve doctor' diagnostic command
- Improve error messages with actionable hints

Phase 4 - Feature expansion:
- External TOML catalog system (extensible)
- Import --convert-local for local script management
- Lockfile support for reproducible installations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 03:44:37 +01:00

215 lines
6.3 KiB
Rust

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<Self> {
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<Self> {
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<DetectedTarget> {
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"
);
}
}