mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 03:36:41 +01:00
232 lines
8.3 KiB
Rust
232 lines
8.3 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: Modes,
|
|
pub views: Views,
|
|
pub binds: Binds,
|
|
}
|
|
|
|
type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
|
|
type Binds = Arc<RwLock<BTreeMap<Arc<str>, EventMap<TuiEvent, Arc<str>>>>>;
|
|
type Views = Arc<RwLock<BTreeMap<Arc<str>, 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: Modes,
|
|
}
|
|
|
|
/// 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 new (dirs: Option<BaseDirectories>) -> Self {
|
|
Self {
|
|
dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")),
|
|
..Default::default()
|
|
}
|
|
}
|
|
pub fn init (&mut self) -> Usually<()> {
|
|
self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(dsl))?;
|
|
Ok(())
|
|
}
|
|
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!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
|
|
match head {
|
|
Some("mode") if let Some(name) = name =>
|
|
Mode::<Arc<str>>::load_into(&self.modes, &name, &body)?,
|
|
Some("keys") if let Some(name) = name =>
|
|
EventMap::<TuiEvent, Arc<str>>::load_into(&self.binds, &name, &body)?,
|
|
Some("view") if let Some(name) = name => {
|
|
self.views.write().unwrap().insert(name.into(), body.src()?.unwrap_or_default().into());
|
|
},
|
|
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
|
|
}
|
|
Ok(())
|
|
} else {
|
|
return Err(format!("Config::load: expected expr, got: {item:?}").into())
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Mode<Arc<str>> {
|
|
pub fn load_into (modes: &Modes, name: &impl AsRef<str>, body: &impl Dsl) -> Usually<()> {
|
|
let mut mode = Self::default();
|
|
println!("Mode::load_into: {}: {body:?}", name.as_ref());
|
|
body.each(|item|mode.load_one(item))?;
|
|
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
|
Ok(())
|
|
}
|
|
pub fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> {
|
|
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(key)) = expr.head() {
|
|
println!("Mode::load_one: {key} {:?}", expr.tail()?);
|
|
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
|
match key {
|
|
"name" => self.name.push(tail.into()),
|
|
"info" => self.info.push(tail.into()),
|
|
"view" => self.view.push(tail.into()),
|
|
"keys" => tail.each(|expr|{self.keys.push(expr.trim().into()); Ok(())})?,
|
|
"mode" => if let Some(id) = tail.head()? {
|
|
Self::load_into(&self.modes, &id, &tail.tail())?;
|
|
} else {
|
|
return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into());
|
|
},
|
|
_ => {
|
|
return Err(format!("Mode::load_one: unexpected expr: {key:?} {tail:?}").into())
|
|
},
|
|
};
|
|
} else if let Ok(Some(word)) = dsl.word() {
|
|
self.view.push(word.into());
|
|
} else {
|
|
return Err(format!("Mode::load_one: unexpected: {dsl:?}").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 EventMap<TuiEvent, Arc<str>> {
|
|
pub fn load_into (binds: &Binds, name: &impl AsRef<str>, body: &impl Dsl) -> Usually<()> {
|
|
println!("EventMap::load_into: {}: {body:?}", name.as_ref());
|
|
let mut map = Self::new();
|
|
body.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())
|
|
})?;
|
|
binds.write().unwrap().insert(name.as_ref().into(), map);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
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, "*") });
|