mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-08-28 21:06:56 +02:00
3e...
This commit is contained in:
parent
816150125d
commit
4fcbcdda86
5 changed files with 607 additions and 566 deletions
496
src/config.rs
Normal file
496
src/config.rs
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
use crate::*;
|
||||
use std::path::PathBuf;
|
||||
use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as NotifyConfig, Watcher};
|
||||
|
||||
/// Write initial contents of configuration.
|
||||
pub fn config_init (config: &Config) -> UsuallyRef<'static, ()> {
|
||||
//println!("\r\ninit {}", quanta::Clock::new().raw());
|
||||
config.clear();
|
||||
let path = Config::CONFIG;
|
||||
let defaults = Config::DEFAULTS;
|
||||
config.stamp.store(quanta::Clock::new().raw(), Relaxed);
|
||||
if config.find_file().is_none() {
|
||||
//println!("Creating {path:?}");
|
||||
std::fs::write(config.place_file()?, defaults)?;
|
||||
}
|
||||
Ok(if let Some(path) = config.find_file() {
|
||||
//println!("Loading {path:?}");
|
||||
let src = std::fs::read_to_string(&path)?;
|
||||
src.as_str().each((), &mut move|ctx, item: &str|{
|
||||
config_add(config, item);
|
||||
Ok(ctx)
|
||||
});
|
||||
} else {
|
||||
return Err(format!("{path}: not found").into())
|
||||
})
|
||||
}
|
||||
|
||||
/// Add statements to configuration from [Dsl] source.
|
||||
pub fn config_add <'a> (config: &'a Config, dsl: &'a str) -> UsuallyRef<'a, &'a Config> {
|
||||
dsl.each(config, &mut move|ctx: &'a Config, item: &'a str|if let Some(expr) = item.expr()? {
|
||||
//if let (Some(name), Some(body)) = (expr.tail()?.head()?, expr.tail()?.tail()?,) {
|
||||
match expr.head()? {
|
||||
Some("mode") => expr.tail()?.map(|tail|modes_add(&config.modes, tail)),
|
||||
Some("keys") => expr.tail()?.map(|tail|load_bind(&config.binds, tail)),
|
||||
Some("view") => expr.tail()?.map(|tail|load_view(&config.views, tail)),
|
||||
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {item:?}").into())
|
||||
}.transpose()?;
|
||||
//}
|
||||
Ok(config)
|
||||
} else {
|
||||
Err(format!("Config::add_one: tried to add empty item").into())
|
||||
})
|
||||
}
|
||||
|
||||
/// Register a mode.
|
||||
pub fn modes_add <'a> (modes: &Modes, expr: &'a str) -> UsuallyRef<'a, ()> {
|
||||
let name = expr.head()?.ok_or("mode: missing name")?;
|
||||
let body = expr.tail()?.ok_or("mode: missing body")?;
|
||||
modes.0.write().unwrap().insert(
|
||||
name.into(),
|
||||
Arc::new(body.each(
|
||||
Mode::default(),
|
||||
move|mut mode: Mode, item: &str|{
|
||||
mode_add(&mut mode, item)?;
|
||||
Ok(mode)
|
||||
}
|
||||
)?));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a definition to the mode.
|
||||
///
|
||||
/// Supported definitions:
|
||||
///
|
||||
/// - (name ...) -> name
|
||||
/// - (info ...) -> description
|
||||
/// - (keys ...) -> key bindings
|
||||
/// - (mode ...) -> submode
|
||||
/// - ... -> view
|
||||
///
|
||||
/// ```
|
||||
/// let mut mode: tek::Mode<std::sync::Arc<str>> = Default::default();
|
||||
/// mode.add("(name hello)").unwrap();
|
||||
/// ```
|
||||
pub fn mode_add <'a> (mode: &'a mut Mode, dsl: &'a str) -> PerhapsRef<'a, &'a mut Mode> {
|
||||
Ok(Some(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
||||
//println!("Mode::add: {head} {:?}", expr.tail());
|
||||
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||
match head {
|
||||
"mode" => {
|
||||
let head = tail.head()?.ok_or("mode: missing name")?;
|
||||
let body = tail.tail()?.ok_or("mode: missing body")?;
|
||||
let modes = mode.modes.clone();
|
||||
let submode = Mode::default();
|
||||
let submode = body.each(submode, move|mut submode: Mode, item: &str|{
|
||||
mode_add(&mut submode, &item);
|
||||
Ok(submode)
|
||||
})?;
|
||||
modes.0.write().unwrap().insert(head.into(), Arc::new(submode));
|
||||
mode
|
||||
},
|
||||
"keys" => {
|
||||
dsl.each(mode, &mut |mode: &'a mut Mode, expr: &'a str|{
|
||||
mode.keys.push(expr.trim().into());
|
||||
Ok(mode)
|
||||
})?
|
||||
},
|
||||
"name" => { mode.name.push(tail.into()); mode },
|
||||
"info" => { mode.info.push(tail.into()); mode },
|
||||
"view" => { mode.view.push(tail.into()); mode },
|
||||
_ => { mode.view.push(expr.into()); mode },
|
||||
}
|
||||
} else if let Ok(Some(word)) = dsl.word() {
|
||||
mode.view.push(word.into());
|
||||
mode
|
||||
} else {
|
||||
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
||||
}))
|
||||
}
|
||||
|
||||
/// Load custom view definition.
|
||||
pub fn load_view <'a> (views: &Views, expr: &'a str,) -> UsuallyRef<'a, ()> {
|
||||
let name = expr.head()?.ok_or("view: missing name")?;
|
||||
let body = expr.tail()?.ok_or("view: missing body")?;
|
||||
views.write().unwrap().insert(
|
||||
name.into(),
|
||||
body.src()?.unwrap_or_default().into()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_bind <'a> (binds: &Binds, expr: &'a str) -> Usually<()> {
|
||||
let name = expr.head()?.ok_or("bind: missing name")?;
|
||||
let body = expr.tail()?.ok_or("bind: missing body")?;
|
||||
binds.write().unwrap().insert(name.into(), {
|
||||
let mut map = Bind::new();
|
||||
body.each((), &mut |_, item: &str|if item.expr().head() == Ok(Some("see")) {
|
||||
// TODO
|
||||
Ok(())
|
||||
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||
let expr = item.expr()?;
|
||||
let head = expr.head()?;
|
||||
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
|
||||
map.add(TuiEvent(event), 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())
|
||||
})?;
|
||||
map
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configuration: mode, view, and bind definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let config = tek::Config::default();
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // Some dizzle.
|
||||
/// // What indentation to use here lol?
|
||||
/// let source = stringify!((mode :menu (name Menu)
|
||||
/// (info Mode selector.) (keys :axis/y :confirm)
|
||||
/// (view (bg (g 0) (bsp/s :ports/out
|
||||
/// (bsp/n :ports/in
|
||||
/// (bg (g 30) (bsp/s (fixed/y 7 :logo)
|
||||
/// (fill :dialog/menu)))))))));
|
||||
/// // Add this definition to the config and try to load it.
|
||||
/// // A "mode" is basically a state machine
|
||||
/// // with associated input and output definitions.
|
||||
/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Config {
|
||||
/// XDG base directories of running user.
|
||||
pub dirs: BaseDirectories,
|
||||
/// Active collection of interaction modes.
|
||||
pub modes: Modes,
|
||||
/// Active collection of event bindings.
|
||||
pub binds: Binds,
|
||||
/// Active collection of view definitions.
|
||||
pub views: Views,
|
||||
/// Error caught during reloading
|
||||
pub error: RwLock<Option<Arc<str>>>,
|
||||
/// Timestamp
|
||||
pub stamp: AtomicU64,
|
||||
/// Watcher
|
||||
pub watch: RwLock<Option<PollWatcher>>
|
||||
}
|
||||
|
||||
/// Collection of UI modes.
|
||||
///
|
||||
/// ```
|
||||
/// let mut modes = tek::Modes::default();
|
||||
/// assert_eq!(modes.len(), 0);
|
||||
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
|
||||
/// assert_eq!(modes.len(), 1);
|
||||
/// ```
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Modes(Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>);
|
||||
|
||||
/// Group of view and keys definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Mode {
|
||||
pub path: PathBuf,
|
||||
pub name: Vec<Arc<str>>,
|
||||
pub info: Vec<Arc<str>>,
|
||||
pub view: Vec<Arc<str>>,
|
||||
pub keys: Vec<Arc<str>>,
|
||||
pub modes: Modes,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
/// Default configuration directory.
|
||||
const CONFIG_DIR: &'static str = "tek";
|
||||
|
||||
/// Default configuration subdirectory.
|
||||
const CONFIG_SUB: &'static str = "v0";
|
||||
|
||||
/// Default configuration file name.
|
||||
const CONFIG: &'static str = "tek.edn";
|
||||
|
||||
/// Default configuration contents.
|
||||
const DEFAULTS: &'static str = include_str!("tek.edn");
|
||||
|
||||
/// Create a new app configuration from a set of XDG base directories,
|
||||
pub fn new (dirs: Option<BaseDirectories>) -> Self {
|
||||
Self {
|
||||
dirs: dirs.unwrap_or_else(||{
|
||||
BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB)
|
||||
}),
|
||||
stamp: quanta::Clock::new().raw().into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create, initialize, and watch a new configuration.
|
||||
pub fn watched <T: 'static> (callback: impl FnOnce(Arc<Config>)->T) -> Usually<T> {
|
||||
let config = Self::init_new(None)?;
|
||||
Self::watch(config.clone(), None)?;
|
||||
let result = callback(config);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Create and initialize a new configuration.
|
||||
pub fn init_new (_dirs: Option<BaseDirectories>) -> UsuallyRef<'static, Arc<Self>> {
|
||||
let config = Arc::new(Self::new(None));
|
||||
config_init(&config)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Watch a config's file for changes.
|
||||
pub fn watch (config: Arc<Config>, poll: Option<Duration>) -> UsuallyRef<'static, ()> {
|
||||
let handler = {
|
||||
let config = config.clone();
|
||||
move |result|match result {
|
||||
Ok(_events) => if let Err(e) = config_init(&config) {
|
||||
*config.error.write().unwrap() = Some(format!("{e:?}").into());
|
||||
panic!("{e:?}");
|
||||
} else {
|
||||
//println!("config updated");
|
||||
},
|
||||
Err(errors) => {
|
||||
panic!("{errors:?}");
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new(
|
||||
handler,
|
||||
NotifyConfig::default()
|
||||
.with_poll_interval(poll.unwrap_or(Duration::from_millis(250)))
|
||||
)?;
|
||||
if let Some(path) = config.get_file() {
|
||||
//println!("watching: {path:?}");
|
||||
watcher.watch(&path, RecursiveMode::NonRecursive)?;
|
||||
*config.watch.write().unwrap() = Some(watcher);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("no config path").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the config file.
|
||||
fn find_file (&self) -> Option<PathBuf> {
|
||||
self.dirs.find_config_file(Self::CONFIG)
|
||||
}
|
||||
|
||||
/// Place config file in default location.
|
||||
fn place_file (&self) -> Result<PathBuf, std::io::Error> {
|
||||
self.dirs.place_config_file(Self::CONFIG)
|
||||
}
|
||||
|
||||
/// Get path to config file.
|
||||
fn get_file (&self) -> Option<PathBuf> {
|
||||
self.dirs.get_config_file(Self::CONFIG)
|
||||
}
|
||||
|
||||
/// Get a mode by name.
|
||||
pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode>> {
|
||||
self.modes.get(mode)
|
||||
}
|
||||
|
||||
/// Print the configuration.
|
||||
pub fn print (&self) {
|
||||
print_config(self)
|
||||
}
|
||||
|
||||
/// Make this configuration empty.
|
||||
fn clear (&self) {
|
||||
*self.modes.0.write().unwrap() = Default::default();
|
||||
*self.views.write().unwrap() = Default::default();
|
||||
*self.binds.write().unwrap() = Default::default();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub use self::view::*;
|
||||
mod view {
|
||||
use crate::*;
|
||||
/// Collection of custom view definitions.
|
||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||
}
|
||||
|
||||
pub use self::mode::*;
|
||||
mod mode {
|
||||
use crate::*;
|
||||
|
||||
impl Modes {
|
||||
/// Get a mode by name.
|
||||
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode>> {
|
||||
self.0.read().unwrap().get(name.as_ref()).cloned()
|
||||
}
|
||||
/// Run something for each mode.
|
||||
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) {
|
||||
for (k, v) in self.0.read().unwrap().iter() {
|
||||
let _ = ator(k.as_ref(), v.as_ref());
|
||||
}
|
||||
}
|
||||
/// Count modes.
|
||||
pub fn len (&self) -> usize {
|
||||
self.0.read().unwrap().len()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use self::bind::*;
|
||||
mod bind {
|
||||
use crate::*;
|
||||
|
||||
/// Collection of input bindings.
|
||||
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
|
||||
|
||||
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
|
||||
///
|
||||
/// ```
|
||||
/// let lang = "(@x (nop)) (@y (nop) (nop))";
|
||||
/// let bind = tek::Bind::<tek::tengri::TuiEvent, std::sync::Arc<str>>::load(&lang).unwrap();
|
||||
/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1));
|
||||
/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2));
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Bind<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>>>
|
||||
);
|
||||
|
||||
/// A sequence of zero or more commands (e.g. [AppCommand]),
|
||||
/// optionally filtered by [Condition] to form layers.
|
||||
///
|
||||
/// ```
|
||||
/// //FIXME: Why does it overflow?
|
||||
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
|
||||
/// ```
|
||||
#[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>>,
|
||||
}
|
||||
|
||||
/// Condition that must evaluate to true in order to enable an input layer.
|
||||
///
|
||||
/// ```
|
||||
/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true})));
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Condition(pub Arc<Box<dyn Fn()->bool + Send + Sync>>);
|
||||
|
||||
impl Bind<TuiEvent, Arc<str>> {
|
||||
|
||||
}
|
||||
|
||||
/// Default is always empty map regardless if `E` and `C` implement [Default].
|
||||
impl<E, C> Default for Bind<E, C> {
|
||||
fn default () -> Self { Self(Default::default()) }
|
||||
}
|
||||
|
||||
impl<C: Default> Default for Binding<C> {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
commands: Default::default(),
|
||||
condition: Default::default(),
|
||||
description: Default::default(),
|
||||
source: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Clone + Ord, C> Bind<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_debug!(Condition |self, w| { write!(w, "*") });
|
||||
}
|
||||
|
||||
pub fn print_config (config: &Config) {
|
||||
use ::ansi_term::Color::*;
|
||||
println!("{:?}", config.dirs);
|
||||
for (k, v) in config.views.read().unwrap().iter() {
|
||||
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
|
||||
}
|
||||
for (k, v) in config.binds.read().unwrap().iter() {
|
||||
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
|
||||
for (k, v) in v.0.iter() {
|
||||
print!("{} ", &Yellow.paint(match &k.0 {
|
||||
Event::Key(KeyEvent { modifiers, .. }) =>
|
||||
format!("{:>16}", format!("{modifiers}")),
|
||||
_ => unimplemented!()
|
||||
}));
|
||||
print!("{}", &Yellow.bold().paint(match &k.0 {
|
||||
Event::Key(KeyEvent { code, .. }) =>
|
||||
format!("{:<10}", format!("{code}")),
|
||||
_ => unimplemented!()
|
||||
}));
|
||||
for v in v.iter() {
|
||||
print!(" => {:?}", v.commands);
|
||||
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
|
||||
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
|
||||
//println!(" {:?}", v.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
config.modes.for_each(|k, v|{
|
||||
println!();
|
||||
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
|
||||
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
|
||||
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
|
||||
print!("\n{}", Blue.paint("KEYS"));
|
||||
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
|
||||
println!();
|
||||
v.modes.for_each(|k, v|{
|
||||
print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name);
|
||||
print!( " INFO={:?}", v.info);
|
||||
print!( " VIEW={:?}", v.view);
|
||||
println!(" KEYS={:?}", v.keys);
|
||||
});
|
||||
print!("{}", Blue.paint("VIEW"));
|
||||
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
|
||||
println!();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue