235 lines
6.9 KiB
Rust
235 lines
6.9 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
|
use std::{fs, process};
|
|
|
|
use cursive::theme::Theme;
|
|
use platform_dirs::AppDirs;
|
|
|
|
use crate::playable::Playable;
|
|
use crate::queue;
|
|
|
|
pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
|
pub struct ConfigValues {
|
|
pub default_keybindings: Option<bool>,
|
|
pub keybindings: Option<HashMap<String, String>>,
|
|
pub theme: Option<ConfigTheme>,
|
|
pub use_nerdfont: Option<bool>,
|
|
pub audio_cache: Option<bool>,
|
|
pub backend: Option<String>,
|
|
pub backend_device: Option<String>,
|
|
pub volnorm: Option<bool>,
|
|
pub volnorm_pregain: Option<f32>,
|
|
pub notify: Option<bool>,
|
|
pub bitrate: Option<u32>,
|
|
pub album_column: Option<bool>,
|
|
pub gapless: Option<bool>,
|
|
pub shuffle: Option<bool>,
|
|
pub repeat: Option<queue::RepeatSetting>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
|
pub struct ConfigTheme {
|
|
pub background: Option<String>,
|
|
pub primary: Option<String>,
|
|
pub secondary: Option<String>,
|
|
pub title: Option<String>,
|
|
pub playing: Option<String>,
|
|
pub playing_selected: Option<String>,
|
|
pub playing_bg: Option<String>,
|
|
pub highlight: Option<String>,
|
|
pub highlight_bg: Option<String>,
|
|
pub error: Option<String>,
|
|
pub error_bg: Option<String>,
|
|
pub statusbar_progress: Option<String>,
|
|
pub statusbar_progress_bg: Option<String>,
|
|
pub statusbar: Option<String>,
|
|
pub statusbar_bg: Option<String>,
|
|
pub cmdline: Option<String>,
|
|
pub cmdline_bg: Option<String>,
|
|
pub search_match: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct UserState {
|
|
pub volume: u16,
|
|
pub shuffle: bool,
|
|
pub repeat: queue::RepeatSetting,
|
|
pub queue: Vec<Playable>,
|
|
}
|
|
|
|
impl Default for UserState {
|
|
fn default() -> Self {
|
|
UserState {
|
|
volume: u16::max_value(),
|
|
shuffle: false,
|
|
repeat: queue::RepeatSetting::None,
|
|
queue: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
lazy_static! {
|
|
pub static ref BASE_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
|
}
|
|
|
|
pub struct Config {
|
|
values: RwLock<ConfigValues>,
|
|
state: RwLock<UserState>,
|
|
}
|
|
|
|
impl Config {
|
|
pub fn new() -> Self {
|
|
let values = load().unwrap_or_else(|e| {
|
|
eprintln!("could not load config: {}", e);
|
|
process::exit(1);
|
|
});
|
|
|
|
let mut userstate = {
|
|
let path = config_path("userstate.toml");
|
|
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;
|
|
}
|
|
|
|
Self {
|
|
values: RwLock::new(values),
|
|
state: RwLock::new(userstate),
|
|
}
|
|
}
|
|
|
|
pub fn values(&self) -> RwLockReadGuard<ConfigValues> {
|
|
self.values.read().expect("can't readlock config values")
|
|
}
|
|
|
|
pub fn state(&self) -> RwLockReadGuard<UserState> {
|
|
self.state.read().expect("can't readlock user state")
|
|
}
|
|
|
|
pub fn with_state_mut<F>(&self, cb: F)
|
|
where
|
|
F: Fn(RwLockWriteGuard<UserState>),
|
|
{
|
|
let state_guard = self.state.write().expect("can't writelock user state");
|
|
cb(state_guard);
|
|
}
|
|
|
|
pub fn save_state(&self) {
|
|
let path = config_path("userstate.toml");
|
|
debug!("saving user state to {}", path.display());
|
|
if let Err(e) = write_content_helper(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)
|
|
}
|
|
|
|
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<ConfigValues, String> {
|
|
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;
|
|
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<Path>,
|
|
T: serde::Serialize + serde::de::DeserializeOwned,
|
|
F: Fn(&Path) -> Result<T, String>,
|
|
>(
|
|
path: P,
|
|
default: F,
|
|
default_on_parse_failure: bool,
|
|
) -> Result<T, String> {
|
|
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<P: AsRef<Path>, T: serde::Serialize>(
|
|
path: P,
|
|
value: T,
|
|
) -> Result<T, String> {
|
|
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
|
|
)
|
|
})
|
|
}
|