use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{RwLock, RwLockReadGuard}; use std::{fs, process}; use cursive::theme::Theme; use platform_dirs::AppDirs; pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b"; #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct ConfigValues { pub default_keybindings: Option, pub keybindings: Option>, pub theme: Option, pub use_nerdfont: Option, pub saved_state: Option, pub audio_cache: Option, pub backend: Option, pub backend_device: Option, pub volnorm: Option, pub volnorm_pregain: Option, pub notify: Option, pub bitrate: Option, pub album_column: Option, } #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct SavedState { pub volume: Option, pub shuffle: Option, pub repeat: Option, } #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct ConfigTheme { pub background: Option, pub primary: Option, pub secondary: Option, pub title: Option, pub playing: Option, pub playing_selected: Option, pub playing_bg: Option, pub highlight: Option, pub highlight_bg: Option, pub error: Option, pub error_bg: Option, pub statusbar_progress: Option, pub statusbar_progress_bg: Option, pub statusbar: Option, pub statusbar_bg: Option, pub cmdline: Option, pub cmdline_bg: Option, pub search_match: Option, } lazy_static! { pub static ref BASE_PATH: RwLock> = RwLock::new(None); } pub struct Config { values: RwLock, } impl Config { pub fn new() -> Self { let values = load().unwrap_or_else(|e| { eprintln!("could not load config: {}", e); process::exit(1); }); Self { values: RwLock::new(values), } } pub fn values(&self) -> RwLockReadGuard { self.values.read().expect("can't readlock config values") } pub fn build_theme(&self) -> Theme { let theme = &self.values().theme; crate::theme::load(theme) } pub fn reload(&self) { let cfg = load().expect("could not reload config"); *self.values.write().expect("can't writelock config values") = cfg } } fn load() -> Result { let path = config_path("config.toml"); load_or_generate_default(path, |_| Ok(ConfigValues::default()), false) } fn proj_dirs() -> AppDirs { match *BASE_PATH.read().expect("can't readlock BASE_PATH") { Some(ref basepath) => AppDirs { cache_dir: basepath.join(".cache"), config_dir: basepath.join(".config"), data_dir: basepath.join(".local/share"), state_dir: basepath.join(".local/state"), }, None => AppDirs::new(Some("ncspot"), true).expect("can't determine project paths"), } } pub fn config_path(file: &str) -> PathBuf { let proj_dirs = proj_dirs(); let cfg_dir = &proj_dirs.config_dir; trace!("{:?}", cfg_dir); if cfg_dir.exists() && !cfg_dir.is_dir() { fs::remove_file(cfg_dir).expect("unable to remove old config file"); } if !cfg_dir.exists() { fs::create_dir_all(cfg_dir).expect("can't create config folder"); } let mut cfg = cfg_dir.to_path_buf(); cfg.push(file); cfg } pub fn cache_path(file: &str) -> PathBuf { let proj_dirs = proj_dirs(); let cache_dir = &proj_dirs.cache_dir; if !cache_dir.exists() { fs::create_dir_all(cache_dir).expect("can't create cache folder"); } let mut pb = cache_dir.to_path_buf(); pb.push(file); pb } /// Configuration and credential file helper /// Creates a default configuration if none exist, otherwise will optionally overwrite /// the file if it fails to parse pub fn load_or_generate_default< P: AsRef, T: serde::Serialize + serde::de::DeserializeOwned, F: Fn(&Path) -> Result, >( path: P, default: F, default_on_parse_failure: bool, ) -> Result { let path = path.as_ref(); // Nothing exists so just write the default and return it if !path.exists() { let value = default(&path)?; return write_content_helper(&path, value); } // load the serialized content. Always report this failure let contents = std::fs::read_to_string(&path) .map_err(|e| format!("Unable to read {}: {}", path.to_string_lossy(), e))?; // Deserialize the content, optionally fall back to default if it fails let result = toml::from_str(&contents); if default_on_parse_failure && result.is_err() { let value = default(&path)?; return write_content_helper(&path, value); } result.map_err(|e| format!("Unable to parse {}: {}", path.to_string_lossy(), e)) } fn write_content_helper, T: serde::Serialize>( path: P, value: T, ) -> Result { let content = toml::to_string_pretty(&value).map_err(|e| format!("Failed serializing value: {}", e))?; fs::write(path.as_ref(), content) .map(|_| value) .map_err(|e| { format!( "Failed writing content to {}: {}", path.as_ref().display(), e ) }) }