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>
215 lines
6.3 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|