use anyhow::{Context, Result};
use eframe::egui::Color32;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct PathConfig {
pub config_dir: Option<PathBuf>,
}
impl PathConfig {
pub fn from_env_and_cli(cli_dir: Option<PathBuf>) -> Self {
let config_dir =
cli_dir.or_else(|| std::env::var("PLAYA_CONFIG_DIR").ok().map(PathBuf::from));
Self { config_dir }
}
}
pub fn config_file(name: &str, config: &PathConfig) -> PathBuf {
get_config_dir(config).join(name)
}
pub fn data_file(name: &str, config: &PathConfig) -> PathBuf {
get_data_dir(config).join(name)
}
pub fn ensure_dirs(config: &PathConfig) -> Result<()> {
let config_dir = get_config_dir(config);
let data_dir = get_data_dir(config);
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).with_context(|| {
format!(
"Failed to create config directory: {}",
config_dir.display()
)
})?;
}
if data_dir != config_dir && !data_dir.exists() {
std::fs::create_dir_all(&data_dir)
.with_context(|| format!("Failed to create data directory: {}", data_dir.display()))?;
}
Ok(())
}
fn has_local_config_files(dir: &std::path::Path) -> bool {
let files = ["playa.json", "playa.log"];
files.iter().any(|f| dir.join(f).exists())
}
fn get_config_dir(config: &PathConfig) -> PathBuf {
if let Some(dir) = &config.config_dir {
return dir.clone();
}
if let Ok(current_dir) = std::env::current_dir()
&& has_local_config_files(¤t_dir)
{
return current_dir;
}
if let Some(dir) = dirs_next::config_dir() {
return dir.join("playa");
}
PathBuf::from(".")
}
fn get_data_dir(config: &PathConfig) -> PathBuf {
if let Some(dir) = &config.config_dir {
return dir.clone();
}
if let Ok(current_dir) = std::env::current_dir()
&& has_local_config_files(¤t_dir)
{
return current_dir;
}
if let Some(dir) = dirs_next::data_dir() {
return dir.join("playa");
}
PathBuf::from(".")
}
pub const DEFAULT_FPS: f32 = 24.0;
pub const DEFAULT_SPEED: f32 = 1.0;
pub const DEFAULT_OPACITY: f32 = 1.0;
pub const DEFAULT_DIM: (usize, usize) = (1920, 1080);
pub const DEFAULT_NEAR_CLIP: f32 = 1.0;
pub const DEFAULT_FAR_CLIP: f32 = 10000.0;
pub const DEFAULT_FOV: f32 = 39.6;
pub const MIN_SPEED: f32 = 0.001;
pub const DEFAULT_SRC_LEN: i32 = 100;
pub mod frame_status {
use super::*;
pub const PLACEHOLDER: Color32 = Color32::from_rgba_premultiplied(40, 40, 45, 128);
pub const HEADER: Color32 = Color32::from_rgba_premultiplied(60, 100, 180, 128);
pub const LOADING: Color32 = Color32::from_rgba_premultiplied(220, 160, 60, 128);
pub const COMPOSING: Color32 = Color32::from_rgba_premultiplied(180, 100, 220, 128);
pub const EXPIRED: Color32 = Color32::from_rgba_premultiplied(160, 140, 80, 128);
pub const LOADED: Color32 = Color32::from_rgba_premultiplied(80, 200, 120, 128);
pub const ERROR: Color32 = Color32::from_rgba_premultiplied(200, 60, 60, 128);
}
pub mod timeline {
use super::*;
pub const SELECTED_BG: Color32 = Color32::from_rgb(70, 100, 140);
pub const HOVERED_BG: Color32 = Color32::from_rgb(60, 70, 85);
pub const NORMAL_BG: Color32 = Color32::from_rgb(45, 50, 60);
pub const PLAYHEAD: Color32 = Color32::from_rgb(220, 60, 60);
pub const WORK_AREA: Color32 = Color32::from_rgba_premultiplied(100, 140, 200, 80);
}
pub mod ui {
use super::*;
pub const PANEL_BG: Color32 = Color32::from_rgb(30, 32, 36);
pub const SEPARATOR: Color32 = Color32::from_rgb(50, 55, 65);
pub const TEXT_PRIMARY: Color32 = Color32::from_rgb(220, 220, 225);
pub const TEXT_SECONDARY: Color32 = Color32::from_rgb(140, 145, 155);
pub const ACCENT: Color32 = Color32::from_rgb(80, 140, 220);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_file_with_custom_dir() {
let config = PathConfig {
config_dir: Some(PathBuf::from("/custom")),
};
let path = config_file("test.json", &config);
assert_eq!(path, PathBuf::from("/custom/test.json"));
}
#[test]
fn test_data_file_with_custom_dir() {
let config = PathConfig {
config_dir: Some(PathBuf::from("/custom")),
};
let path = data_file("cache.json", &config);
assert_eq!(path, PathBuf::from("/custom/cache.json"));
}
#[test]
fn test_config_file_uses_platform_defaults() {
let config = PathConfig { config_dir: None };
let path = config_file("test.json", &config);
assert!(path.to_string_lossy().contains("playa"));
assert!(path.to_string_lossy().contains("test.json"));
}
#[test]
fn test_show_actual_paths() {
println!("\n=== Platform-specific paths (no local files) ===");
let config = PathConfig { config_dir: None };
let cfg_path = config_file("playa.json", &config);
let cache_path = data_file("playa_data.json", &config);
let log_path = data_file("playa.log", &config);
println!("Config file: {}", cfg_path.display());
println!("Data file: {}", cache_path.display());
println!("Log file: {}", log_path.display());
println!("\n=== With custom directory ===");
let custom_config = PathConfig {
config_dir: Some(PathBuf::from("/tmp/playa-test")),
};
println!(
"Config file: {}",
config_file("playa.json", &custom_config).display()
);
println!(
"Data file: {}",
data_file("playa_data.json", &custom_config).display()
);
println!(
"Log file: {}",
data_file("playa.log", &custom_config).display()
);
}
#[test]
fn test_local_files_priority() {
use std::fs;
let temp_dir = std::env::temp_dir().join("playa_test_local");
let _ = fs::create_dir_all(&temp_dir);
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&temp_dir).unwrap();
let config = PathConfig { config_dir: None };
let path_without_local = config_file("playa.json", &config);
println!("\nWithout local files: {}", path_without_local.display());
assert!(path_without_local.to_string_lossy().contains("playa"));
assert!(!path_without_local.starts_with(&temp_dir));
fs::write(temp_dir.join("playa.json"), "{}").unwrap();
let path_with_local = config_file("playa.json", &config);
println!("With local playa.json: {}", path_with_local.display());
let canonical_result = path_with_local.canonicalize().unwrap();
let canonical_expected = temp_dir.join("playa.json").canonicalize().unwrap();
assert_eq!(canonical_result, canonical_expected);
std::env::set_current_dir(&original_dir).unwrap();
let _ = fs::remove_dir_all(&temp_dir);
}
}