274 lines
8.9 KiB
Rust
274 lines
8.9 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::path::PathBuf;
|
|
|
|
/// Application configuration (persistent)
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AppConfig {
|
|
/// Recent files list (newest first, max 10 items)
|
|
#[serde(default)]
|
|
pub recent_files: Vec<PathBuf>,
|
|
|
|
// User Preferences
|
|
/// Default BPM for new projects
|
|
#[serde(default = "defaults::bpm")]
|
|
pub bpm: u32,
|
|
|
|
/// Default framerate for new projects
|
|
#[serde(default = "defaults::framerate")]
|
|
pub framerate: u32,
|
|
|
|
/// Default file width in pixels
|
|
#[serde(default = "defaults::file_width")]
|
|
pub file_width: u32,
|
|
|
|
/// Default file height in pixels
|
|
#[serde(default = "defaults::file_height")]
|
|
pub file_height: u32,
|
|
|
|
/// Scroll speed multiplier
|
|
#[serde(default = "defaults::scroll_speed")]
|
|
pub scroll_speed: f64,
|
|
|
|
/// Audio buffer size in samples (128, 256, 512, 1024, 2048, 4096)
|
|
#[serde(default = "defaults::audio_buffer_size")]
|
|
pub audio_buffer_size: u32,
|
|
|
|
/// Reopen last session on startup
|
|
#[serde(default = "defaults::reopen_last_session")]
|
|
pub reopen_last_session: bool,
|
|
|
|
/// Restore layout when opening files
|
|
#[serde(default = "defaults::restore_layout_from_file")]
|
|
pub restore_layout_from_file: bool,
|
|
|
|
/// Enable debug mode
|
|
#[serde(default = "defaults::debug")]
|
|
pub debug: bool,
|
|
|
|
/// Show waveforms as stacked stereo instead of combined mono
|
|
#[serde(default = "defaults::waveform_stereo")]
|
|
pub waveform_stereo: bool,
|
|
|
|
/// Theme mode ("light", "dark", or "system")
|
|
#[serde(default = "defaults::theme_mode")]
|
|
pub theme_mode: String,
|
|
}
|
|
|
|
impl Default for AppConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
recent_files: Vec::new(),
|
|
bpm: defaults::bpm(),
|
|
framerate: defaults::framerate(),
|
|
file_width: defaults::file_width(),
|
|
file_height: defaults::file_height(),
|
|
scroll_speed: defaults::scroll_speed(),
|
|
audio_buffer_size: defaults::audio_buffer_size(),
|
|
reopen_last_session: defaults::reopen_last_session(),
|
|
restore_layout_from_file: defaults::restore_layout_from_file(),
|
|
debug: defaults::debug(),
|
|
waveform_stereo: defaults::waveform_stereo(),
|
|
theme_mode: defaults::theme_mode(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AppConfig {
|
|
/// Load config from standard location
|
|
/// Returns default config if file doesn't exist or is malformed
|
|
pub fn load() -> Self {
|
|
match Self::try_load() {
|
|
Ok(config) => config,
|
|
Err(e) => {
|
|
eprintln!("⚠️ Failed to load config: {}", e);
|
|
eprintln!(" Using default configuration");
|
|
Self::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Try to load config, returning error if something goes wrong
|
|
fn try_load() -> Result<Self, Box<dyn std::error::Error>> {
|
|
let config_path = Self::config_path()?;
|
|
|
|
if !config_path.exists() {
|
|
return Ok(Self::default());
|
|
}
|
|
|
|
let contents = std::fs::read_to_string(&config_path)?;
|
|
let config: AppConfig = serde_json::from_str(&contents)?;
|
|
Ok(config)
|
|
}
|
|
|
|
/// Save config to standard location
|
|
/// Logs error but doesn't block if save fails
|
|
pub fn save(&self) {
|
|
if let Err(e) = self.try_save() {
|
|
eprintln!("⚠️ Failed to save config: {}", e);
|
|
}
|
|
}
|
|
|
|
/// Try to save config atomically (write to temp, then rename)
|
|
fn try_save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
let config_path = Self::config_path()?;
|
|
|
|
// Ensure parent directory exists
|
|
if let Some(parent) = config_path.parent() {
|
|
std::fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
// Serialize to JSON with pretty formatting
|
|
let json = serde_json::to_string_pretty(self)?;
|
|
|
|
// Atomic write: write to temp file, then rename
|
|
let temp_path = config_path.with_extension("json.tmp");
|
|
std::fs::write(&temp_path, json)?;
|
|
std::fs::rename(temp_path, config_path)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get cross-platform config file path
|
|
fn config_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
use directories::ProjectDirs;
|
|
|
|
let proj_dirs = ProjectDirs::from("", "", "lightningbeam")
|
|
.ok_or("Failed to determine config directory")?;
|
|
|
|
Ok(proj_dirs.config_dir().join("config.json"))
|
|
}
|
|
|
|
/// Add a file to recent files list
|
|
/// - Canonicalize path (resolve relative paths and symlinks)
|
|
/// - Move to front if already in list (remove duplicates)
|
|
/// - Enforce 10-item limit (LRU eviction)
|
|
/// - Auto-save config
|
|
pub fn add_recent_file(&mut self, path: PathBuf) {
|
|
// Try to canonicalize path (absolute, resolve symlinks)
|
|
let canonical = match path.canonicalize() {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
// Canonicalize can fail for unsaved files or deleted files
|
|
eprintln!("⚠️ Could not canonicalize path {:?}: {}", path, e);
|
|
return; // Don't add non-existent paths
|
|
}
|
|
};
|
|
|
|
// Remove if already present (we'll add to front)
|
|
self.recent_files.retain(|p| p != &canonical);
|
|
|
|
// Add to front
|
|
self.recent_files.insert(0, canonical);
|
|
|
|
// Enforce 10-item limit
|
|
self.recent_files.truncate(10);
|
|
|
|
// Auto-save
|
|
self.save();
|
|
}
|
|
|
|
/// Get recent files list, filtering out files that no longer exist
|
|
/// Returns newest first
|
|
pub fn get_recent_files(&self) -> Vec<PathBuf> {
|
|
self.recent_files
|
|
.iter()
|
|
.filter(|p| p.exists())
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
/// Clear all recent files
|
|
pub fn clear_recent_files(&mut self) {
|
|
self.recent_files.clear();
|
|
self.save();
|
|
}
|
|
|
|
/// Validate BPM range (20-300)
|
|
pub fn validate_bpm(&self) -> Result<(), String> {
|
|
if self.bpm >= 20 && self.bpm <= 300 {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("BPM must be between 20 and 300 (got {})", self.bpm))
|
|
}
|
|
}
|
|
|
|
/// Validate framerate range (1-120)
|
|
pub fn validate_framerate(&self) -> Result<(), String> {
|
|
if self.framerate >= 1 && self.framerate <= 120 {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("Framerate must be between 1 and 120 (got {})", self.framerate))
|
|
}
|
|
}
|
|
|
|
/// Validate file width range (100-10000)
|
|
pub fn validate_file_width(&self) -> Result<(), String> {
|
|
if self.file_width >= 100 && self.file_width <= 10000 {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("File width must be between 100 and 10000 (got {})", self.file_width))
|
|
}
|
|
}
|
|
|
|
/// Validate file height range (100-10000)
|
|
pub fn validate_file_height(&self) -> Result<(), String> {
|
|
if self.file_height >= 100 && self.file_height <= 10000 {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("File height must be between 100 and 10000 (got {})", self.file_height))
|
|
}
|
|
}
|
|
|
|
/// Validate scroll speed range (0.1-10.0)
|
|
pub fn validate_scroll_speed(&self) -> Result<(), String> {
|
|
if self.scroll_speed >= 0.1 && self.scroll_speed <= 10.0 {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("Scroll speed must be between 0.1 and 10.0 (got {})", self.scroll_speed))
|
|
}
|
|
}
|
|
|
|
/// Validate audio buffer size (must be 128, 256, 512, 1024, 2048, or 4096)
|
|
pub fn validate_audio_buffer_size(&self) -> Result<(), String> {
|
|
match self.audio_buffer_size {
|
|
128 | 256 | 512 | 1024 | 2048 | 4096 => Ok(()),
|
|
_ => Err(format!("Audio buffer size must be 128, 256, 512, 1024, 2048, or 4096 (got {})", self.audio_buffer_size))
|
|
}
|
|
}
|
|
|
|
/// Validate theme mode (must be "light", "dark", or "system")
|
|
pub fn validate_theme_mode(&self) -> Result<(), String> {
|
|
match self.theme_mode.to_lowercase().as_str() {
|
|
"light" | "dark" | "system" => Ok(()),
|
|
_ => Err(format!("Theme mode must be 'light', 'dark', or 'system' (got '{}')", self.theme_mode))
|
|
}
|
|
}
|
|
|
|
/// Validate all preferences
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
self.validate_bpm()?;
|
|
self.validate_framerate()?;
|
|
self.validate_file_width()?;
|
|
self.validate_file_height()?;
|
|
self.validate_scroll_speed()?;
|
|
self.validate_audio_buffer_size()?;
|
|
self.validate_theme_mode()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Default values for preferences (matches JS implementation)
|
|
mod defaults {
|
|
pub fn bpm() -> u32 { 120 }
|
|
pub fn framerate() -> u32 { 24 }
|
|
pub fn file_width() -> u32 { 800 }
|
|
pub fn file_height() -> u32 { 600 }
|
|
pub fn scroll_speed() -> f64 { 1.0 }
|
|
pub fn audio_buffer_size() -> u32 { 256 }
|
|
pub fn reopen_last_session() -> bool { false }
|
|
pub fn restore_layout_from_file() -> bool { true }
|
|
pub fn debug() -> bool { false }
|
|
pub fn waveform_stereo() -> bool { false }
|
|
pub fn theme_mode() -> String { "system".to_string() }
|
|
}
|