use std::collections::HashMap; use std::path::PathBuf; use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::{fs, process}; use cursive::theme::Theme; use log::{debug, error}; use platform_dirs::AppDirs; use crate::command::{SortDirection, SortKey}; use crate::model::playable::Playable; use crate::queue; use crate::serialization::{Serializer, CBOR, TOML}; pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b"; pub const CACHE_VERSION: u16 = 1; pub const DEFAULT_COMMAND_KEY: char = ':'; /// The playback state when ncspot is started. #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum PlaybackState { Playing, Paused, Stopped, Default, } /// The focussed library tab when ncspot is started. #[derive(Clone, Serialize, Deserialize, Debug, Hash, strum_macros::EnumIter)] #[serde(rename_all = "lowercase")] pub enum LibraryTab { Tracks, Albums, Artists, Playlists, Podcasts, Browse, } /// The format used to represent tracks in a list. #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct TrackFormat { pub left: Option, pub center: Option, pub right: Option, } impl TrackFormat { pub fn default() -> Self { Self { left: Some(String::from("%artists - %title")), center: Some(String::from("%album")), right: Some(String::from("%saved %duration")), } } } /// The format used when sending desktop notifications about playback status. #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct NotificationFormat { pub title: Option, pub body: Option, } impl NotificationFormat { pub fn default() -> Self { Self { title: Some(String::from("%title")), body: Some(String::from("%artists")), } } } /// The configuration of ncspot. #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct ConfigValues { pub command_key: Option, pub initial_screen: Option, pub default_keybindings: Option, pub keybindings: Option>, pub theme: Option, pub use_nerdfont: Option, pub flip_status_indicators: Option, pub audio_cache: Option, pub audio_cache_size: Option, pub backend: Option, pub backend_device: Option, pub volnorm: Option, pub volnorm_pregain: Option, pub notify: Option, pub bitrate: Option, pub gapless: Option, pub shuffle: Option, pub repeat: Option, pub cover_max_scale: Option, pub playback_state: Option, pub track_format: Option, pub notification_format: Option, pub statusbar_format: Option, pub library_tabs: Option>, pub hide_display_names: Option, pub credentials: Option, } /// Commands used to obtain user credentials automatically. #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct Credentials { pub username_cmd: Option, pub password_cmd: Option, } /// The ncspot theme. #[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 highlight_inactive_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, } /// The ordering that is used when representing a playlist. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SortingOrder { pub key: SortKey, pub direction: SortDirection, } /// The runtime state of the music queue. #[derive(Serialize, Default, Deserialize, Debug, Clone)] pub struct QueueState { pub current_track: Option, pub random_order: Option>, pub track_progress: std::time::Duration, pub queue: Vec, } /// Runtime state that should be persisted accross sessions. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UserState { pub volume: u16, pub shuffle: bool, pub repeat: queue::RepeatSetting, pub queuestate: QueueState, pub playlist_orders: HashMap, pub cache_version: u16, pub playback_state: PlaybackState, } impl Default for UserState { fn default() -> Self { Self { volume: u16::MAX, shuffle: false, repeat: queue::RepeatSetting::None, queuestate: QueueState::default(), playlist_orders: HashMap::new(), cache_version: 0, playback_state: PlaybackState::Default, } } } lazy_static! { /// Configuration files are read/written relative to this directory. pub static ref BASE_PATH: RwLock> = RwLock::new(None); } /// The complete configuration (state + user configuration) of ncspot. pub struct Config { /// The configuration file path. filename: String, /// Configuration set by the user, read only. values: RwLock, /// Runtime state which can't be edited by the user, read/write. state: RwLock, } impl Config { /// Generate the configuration from the user configuration file and the runtime state file. /// `filename` can be used to look for a differently named configuration file. pub fn new(filename: Option) -> Self { let filename = filename.unwrap_or("config.toml".to_owned()); let values = load(&filename).unwrap_or_else(|e| { eprintln!("could not load config: {e}"); process::exit(1); }); let mut userstate = { let path = config_path("userstate.cbor"); CBOR.load_or_generate_default(path, || Ok(UserState::default()), true) .expect("could not load user state") }; if let Some(shuffle) = values.shuffle { userstate.shuffle = shuffle; } if let Some(repeat) = values.repeat { userstate.repeat = repeat; } if let Some(playback_state) = values.playback_state.clone() { userstate.playback_state = playback_state; } Self { filename, values: RwLock::new(values), state: RwLock::new(userstate), } } pub fn values(&self) -> RwLockReadGuard { self.values.read().expect("can't readlock config values") } pub fn state(&self) -> RwLockReadGuard { self.state.read().expect("can't readlock user state") } pub fn with_state_mut(&self, cb: F) where F: Fn(RwLockWriteGuard), { let state_guard = self.state.write().expect("can't writelock user state"); cb(state_guard); } pub fn save_state(&self) { // update cache version number self.with_state_mut(|mut state| state.cache_version = CACHE_VERSION); let path = config_path("userstate.cbor"); debug!("saving user state to {}", path.display()); if let Err(e) = CBOR.write(path, self.state().clone()) { error!("Could not save user state: {}", e); } } pub fn build_theme(&self) -> Theme { let theme = &self.values().theme; crate::theme::load(theme) } /// Reload the configuration file. pub fn reload(&self) { let cfg = load(&self.filename).expect("could not reload config"); *self.values.write().expect("can't writelock config values") = cfg } } /// Parse the configuration file with name `filename` at the configuration base path. fn load(filename: &str) -> Result { let path = config_path(filename); TOML.load_or_generate_default(path, || Ok(ConfigValues::default()), false) } /// Returns the platform app directories for ncspot. /// /// # Panics /// /// This panics if the project directories could not be determined. Use `try_proj_dirs` for a /// non-panicking version. fn proj_dirs() -> AppDirs { try_proj_dirs().unwrap() } /// Returns the plaform app directories for ncspot if they could be determined, /// or an error otherwise. pub fn try_proj_dirs() -> Result { match *BASE_PATH .read() .map_err(|_| String::from("Poisoned RWLock"))? { Some(ref basepath) => Ok(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) .ok_or_else(|| String::from("Couldn't determine platform standard directories")), } } /// Force create the configuration directory at the default project location, removing anything that /// isn't a directory but has the same name. Return the path to the configuration file inside the /// directory. /// /// This doesn't create the file, only the containing directory. pub fn config_path(file: &str) -> PathBuf { let proj_dirs = proj_dirs(); let cfg_dir = &proj_dirs.config_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 } /// Create the cache directory at the default project location, preserving it if it already exists, /// and return the path to the cache file inside the directory. /// /// This doesn't create the file, only the containing directory. 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 } /// Set the configuration base path. All configuration files are read/written relative to this path. pub fn set_configuration_base_path(base_path: Option) { if let Some(basepath) = base_path { if !basepath.exists() { fs::create_dir_all(&basepath).expect("could not create basepath directory"); } *BASE_PATH.write().unwrap() = Some(basepath); } }