use std::path::Path;
use playr_core::settings::toml::de::DeValue;
use playr_core::settings::toml::Spanned;
use playr_core::settings::{kind, Settings};
use crate::action::{Key, Keymap};
use crate::command::{self, view_name};
use crate::{Theme, View};
pub use playr_core::settings::default_path;
pub const DEFAULT_KEYS: &str = include_str!("keys.toml");
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub settings: Settings,
pub keys: Keymap,
pub theme: Theme,
}
impl Default for Config {
fn default() -> Self {
let mut config = Config {
settings: Settings::default(),
keys: Keymap::empty(),
theme: Theme::Dark,
};
if let Err(errors) = config.apply(DEFAULT_KEYS) {
panic!("bad default key bindings: {errors:?}");
}
config
}
}
impl Config {
pub fn parse(text: &str) -> Result<Config, Vec<String>> {
let mut config = Config::default();
config.apply(text).map(|()| config)
}
pub fn load(path: &Path, required: bool) -> Result<Config, Vec<String>> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(e) if !required && e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Config::default())
}
Err(e) => return Err(vec![format!("{}: {e}", path.display())]),
};
Config::parse(&text).map_err(|errors| {
errors
.into_iter()
.map(|e| format!("{}: {e}", path.display()))
.collect()
})
}
fn apply(&mut self, text: &str) -> Result<(), Vec<String>> {
let (tables, mut errors) = self.settings.apply(text, &["keys", "theme"]);
for (name, value) in tables {
if name.get_ref() == "theme" {
if let Err(e) = self.set_theme(value.get_ref()) {
errors.add(value.span().start, e);
}
continue;
}
let DeValue::Table(keys) = value.get_ref() else {
let message = format!("{} cannot be {}", name.get_ref(), kind(value.get_ref()));
errors.add(value.span().start, message);
continue;
};
for (key, target) in keys {
let (view, bindings) = match target.get_ref() {
DeValue::Table(bindings) => match command::view_named(key.get_ref()) {
Some(view) => (Some(view), bindings.iter().collect()),
None => {
errors.add(key.span().start, not_a_view(key.get_ref()));
continue;
}
},
_ => (None, vec![(key, target)]),
};
for (key, target) in bindings {
if let Err(e) = self.bind(view, key, target) {
errors.add(target.span().start, e);
}
}
}
}
errors.finish()
}
fn set_theme(&mut self, value: &DeValue) -> Result<(), String> {
let names = || Theme::NAMES.map(|t| t.0).join(", ");
let DeValue::String(s) = value else {
return Err(format!("theme cannot be {}", kind(value)));
};
let theme = Theme::NAMES.iter().find(|t| t.0.eq_ignore_ascii_case(s));
self.theme = theme
.ok_or_else(|| format!("unknown theme {s}; themes: {}", names()))?
.1;
Ok(())
}
fn bind(
&mut self,
view: Option<View>,
key: &Spanned<std::borrow::Cow<str>>,
target: &Spanned<DeValue>,
) -> Result<(), String> {
let name = key.get_ref();
let key = Key::parse(name)?;
let DeValue::String(target) = target.get_ref() else {
return Err(format!(
"{name} must be a command string, not {}",
kind(target.get_ref())
));
};
let action = command::key_target(target, view).map_err(|e| {
match command::only_view(target).filter(|v| view != Some(*v)) {
Some(v) => format!("{e}; put {name} under [keys.{}]", view_name(v)),
None => e,
}
})?;
self.keys.bind(view, key, action);
Ok(())
}
}
fn not_a_view(name: &str) -> String {
format!("[keys.{name}] is not a view; views: library, selection, playlists, sampler")
}