tek/crates/config/config.rs
unspeaker 5ccbb9719f
Some checks are pending
/ build (push) Waiting to run
config: extract
2025-08-23 13:47:56 +03:00

213 lines
7.9 KiB
Rust

#![feature(if_let_guard)]
use ::{
std::{
sync::{Arc, RwLock},
collections::BTreeMap,
path::PathBuf
},
tengri::{
Usually, impl_debug,
tui::TuiEvent,
dsl::{Dsl, DslWord, DslExpr}
},
xdg::BaseDirectories,
};
/// Configuration.
///
/// Contains mode, view, and bind definitions.
#[derive(Default, Debug)]
pub struct Config {
pub dirs: BaseDirectories,
pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>,
pub views: Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>,
pub binds: Arc<RwLock<BTreeMap<Arc<str>, EventMap<TuiEvent, Arc<str>>>>>,
}
/// A set of currently active view and keys definitions,
/// with optional name and description.
#[derive(Default, Debug)]
pub struct Mode<D: Dsl + Ord> {
pub path: PathBuf,
pub name: Vec<D>,
pub info: Vec<D>,
pub view: Vec<D>,
pub keys: Vec<D>,
pub modes: BTreeMap<D, Mode<D>>,
}
/// A collection of input bindings.
#[derive(Debug)]
pub struct EventMap<E, C>(
/// Map of each event (e.g. key combination) to
/// all command expressions bound to it by
/// all loaded input layers.
pub BTreeMap<E, Vec<Binding<C>>>
);
/// An input binding.
#[derive(Debug, Clone)]
pub struct Binding<C> {
pub commands: Arc<[C]>,
pub condition: Option<Condition>,
pub description: Option<Arc<str>>,
pub source: Option<Arc<PathBuf>>,
}
/// Input bindings are only returned if this evaluates to true
#[derive(Clone)]
pub struct Condition(Arc<Box<dyn Fn()->bool + Send + Sync>>);
impl Config {
const CONFIG: &'static str = "tek.edn";
const DEFAULTS: &'static str = include_str!("../../tek.edn");
pub fn init () -> Usually<Self> {
let mut cfgs: Self = Default::default();
cfgs.dirs = BaseDirectories::with_profile("tek", "v0");
cfgs.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(dsl))?;
Ok(cfgs)
}
pub fn init_file (
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
) -> Usually<()> {
if self.dirs.find_config_file(path).is_none() {
println!("Creating {path:?}");
std::fs::write(self.dirs.place_config_file(path)?, defaults);
}
Ok(if let Some(path) = self.dirs.find_config_file(path) {
println!("Loading {path:?}");
let src = std::fs::read_to_string(&path)?;
src.as_str().each(move|item|each(self, item))?;
} else {
return Err(format!("{path}: not found").into())
})
}
pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> {
dsl.each(|item|if let Some(expr) = item.expr()? {
let head = expr.head()?;
let tail = expr.tail()?;
let name = tail.head()?;
let body = tail.tail()?;
println!("{} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
match head {
Some("view") if let Some(name) = name => self.load_view(name.into(), body),
Some("keys") if let Some(name) = name => self.load_bind(name.into(), body),
Some("mode") if let Some(name) = name => self.load_mode(name.into(), body),
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
}
} else {
return Err(format!("Config::load: expected expr, got: {item:?}").into())
})
}
pub fn load_view (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
self.views.write().unwrap().insert(id, dsl.src()?.unwrap_or_default().into());
Ok(())
}
pub fn load_bind (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
let mut map = EventMap::new();
dsl.each(|item|if item.expr().head() == Ok(Some("see")) {
// TODO
Ok(())
} else if let Ok(Some(word)) = item.expr().head().word() {
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
map.add(key, Binding {
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
return Ok(())
} else {
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
}
} else {
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
})?;
self.binds.write().unwrap().insert(id, map);
Ok(())
}
pub fn load_mode (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
let mut mode = Mode::default();
dsl.each(|item|Self::load_mode_one(&mut mode, item))?;
self.modes.write().unwrap().insert(id.into(), Arc::new(mode));
Ok(())
}
pub fn load_mode_one (mode: &mut Mode<Arc<str>>, item: impl Dsl) -> Usually<()> {
Ok(if let Ok(Some(key)) = item.expr().head() {
match key {
"name" => mode.name.push(item.expr()?.tail()?.map(|x|x.trim()).unwrap_or("").into()),
"info" => mode.info.push(item.expr()?.tail()?.map(|x|x.trim()).unwrap_or("").into()),
"keys" => {
item.expr()?.tail()?.each(|item|{mode.keys.push(item.trim().into()); Ok(())})?;
},
"mode" => if let Some(id) = item.expr()?.tail()?.head()? {
let mut submode = Mode::default();
Self::load_mode_one(&mut submode, item.expr()?.tail()?.tail()?)?;
mode.modes.insert(id.into(), submode);
} else {
return Err(format!("load_mode_one: incomplete: {item:?}").into());
},
_ => mode.view.push(item.expr()?.unwrap().into()),
}
} else if let Ok(Some(word)) = item.word() {
mode.view.push(word.into());
} else {
return Err(format!("load_mode_one: unexpected: {item:?}").into());
})
}
}
/// Default is always empty map regardless if `E` and `C` implement [Default].
impl<E, C> Default for EventMap<E, C> {
fn default () -> Self { Self(Default::default()) }
}
impl<E: Clone + Ord, C> EventMap<E, C> {
/// Create a new event map
pub fn new () -> Self {
Default::default()
}
/// Add a binding to an owned event map.
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
self.add(event, binding);
self
}
/// Add a binding to an event map.
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
if !self.0.contains_key(&event) {
self.0.insert(event.clone(), Default::default());
}
self.0.get_mut(&event).unwrap().push(binding);
self
}
/// Return the binding(s) that correspond to an event.
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
self.0.get(event).map(|x|x.as_slice())
}
/// Return the first binding that corresponds to an event, considering conditions.
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
self.query(event)
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
.flatten()
}
}
impl<C> Binding<C> {
pub fn from_dsl (dsl: impl Dsl) -> Usually<Self> {
let command: Option<C> = None;
let condition: Option<Condition> = None;
let description: Option<Arc<str>> = None;
let source: Option<Arc<PathBuf>> = None;
if let Some(command) = command {
Ok(Self { commands: [command].into(), condition, description, source })
} else {
Err(format!("no command in {dsl:?}").into())
}
}
}
impl_debug!(Condition |self, w| { write!(w, "*") });