input: add InputMap; dsl/output/tui: Atom->Dsl
Some checks are pending
/ build (push) Waiting to run

This commit is contained in:
🪞👃🪞 2025-04-28 04:49:00 +03:00
parent 47b7f7e7f9
commit 35ad371205
15 changed files with 374 additions and 277 deletions

View file

@ -1,54 +0,0 @@
use crate::*;
#[macro_export] macro_rules! defcom {
([$self:ident, $app:ident:$App:ty] $(($Command:ident $((
$Variant:ident [$($($param:ident: $Param:ty),+)?] $expr:expr
))*))*) => {
$(#[derive(Clone, Debug)] pub enum $Command {
$($Variant $(($($Param),+))?),*
})*
$(command!(|$self: $Command, $app: $App|match $self {
$($Command::$Variant $(($($param),+))? => $expr),*
});)*
};
(|$self:ident, $app:ident:$App:ty| $($Command:ident { $(
$Variant:ident $(($($param:ident: $Param:ty),+))? => $expr:expr
)* $(,)? })*) => {
$(#[derive(Clone, Debug)] pub enum $Command {
$($Variant $(($($Param),+))?),*
})*
$(command!(|$self: $Command, $app: $App|match $self {
$($Command::$Variant $(($($param),+))? => $expr),*
});)*
};
}
#[macro_export] macro_rules! command {
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
impl$(<$($l),+>)? Command<$State> for $Command {
fn execute ($self, $state: &mut $State) -> Perhaps<Self> {
Ok($handler)
}
}
};
}
pub trait Command<S>: Send + Sync + Sized {
fn execute (self, state: &mut S) -> Perhaps<Self>;
fn delegate <T> (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
where Self: Sized
{
Ok(self.execute(state)?.map(wrap))
}
}
impl<S, T: Command<S>> Command<S> for Option<T> {
fn execute (self, _: &mut S) -> Perhaps<Self> {
Ok(None)
}
fn delegate <U> (self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps<U>
where Self: Sized
{
Ok(None)
}
}

View file

@ -0,0 +1,21 @@
use crate::*;
pub trait Command<S>: Send + Sync + Sized {
fn execute (self, state: &mut S) -> Perhaps<Self>;
fn delegate <T> (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
where Self: Sized
{
Ok(self.execute(state)?.map(wrap))
}
}
impl<S, T: Command<S>> Command<S> for Option<T> {
fn execute (self, _: &mut S) -> Perhaps<Self> {
Ok(None)
}
fn delegate <U> (self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps<U>
where Self: Sized
{
Ok(None)
}
}

162
input/src/input_dsl.rs Normal file
View file

@ -0,0 +1,162 @@
use crate::*;
/// A [Command] that can be constructed from a [Token].
pub trait DslCommand<'a, C>: TryFromDsl<'a, C> + Command<C> {}
impl<'a, C, T: TryFromDsl<'a, C> + Command<C>> DslCommand<'a, C> for T {}
/// [Input] state that can be matched against a [Value].
pub trait DslInput: Input {
fn matches_dsl (&self, token: &str) -> bool;
}
/// A pre-configured mapping of input events to commands.
pub trait KeyMap<'a, S, C: DslCommand<'a, S>, I: DslInput> {
/// Try to find a command that matches the current input event.
fn command (&'a self, state: &'a S, input: &'a I) -> Option<C>;
}
/// A [SourceIter] can be a [KeyMap].
impl<'a, S, C: DslCommand<'a, S>, I: DslInput> KeyMap<'a, S, C, I> for SourceIter<'a> {
fn command (&'a self, state: &'a S, input: &'a I) -> Option<C> {
let mut iter = self.clone();
while let Some((token, rest)) = iter.next() {
iter = rest;
match token {
Token { value: Value::Exp(0, exp_iter), .. } => {
let mut exp_iter = exp_iter.clone();
match exp_iter.next() {
Some(Token { value: Value::Sym(binding), .. }) => {
if input.matches_dsl(binding) {
if let Some(command) = C::try_from_expr(state, exp_iter.clone()) {
return Some(command)
}
}
},
_ => panic!("invalid config (expected symbol)")
}
},
_ => panic!("invalid config (expected expression)")
}
}
None
}
}
/// A [TokenIter] can be a [KeyMap].
impl<'a, S, C: DslCommand<'a, S>, I: DslInput> KeyMap<'a, S, C, I> for TokenIter<'a> {
fn command (&'a self, state: &'a S, input: &'a I) -> Option<C> {
let mut iter = self.clone();
while let Some(next) = iter.next() {
match next {
Token { value: Value::Exp(0, exp_iter), .. } => {
let mut exp_iter = exp_iter.clone();
match exp_iter.next() {
Some(Token { value: Value::Sym(binding), .. }) => {
if input.matches_dsl(binding) {
if let Some(command) = C::try_from_expr(state, exp_iter.clone()) {
return Some(command)
}
}
},
_ => panic!("invalid config (expected symbol)")
}
},
_ => panic!("invalid config (expected expression)")
}
}
None
}
}
/// A collection of pre-configured mappings of input events to commands,
/// which may be made available subject to given conditions.
pub struct InputMap<'a, S, C: DslCommand<'a, S>, I: DslInput> {
layers: Vec<(
Box<dyn Fn(&S)->bool + Send + Sync + 'a>,
Box<dyn KeyMap<'a, S, C, I> + Send + Sync + 'a>
)>,
}
impl<'a, S, C: DslCommand<'a, S>, I: DslInput> Default for InputMap<'a, S, C, I> {
fn default () -> Self {
Self {
layers: vec![]
}
}
}
impl<'a, S, C: DslCommand<'a, S>, I: DslInput> InputMap<'a, S, C, I> {
pub fn new (keymap: impl KeyMap<'a, S, C, I> + Send + Sync + 'a) -> Self {
Self::default().layer(keymap)
}
pub fn layer (
mut self, keymap: impl KeyMap<'a, S, C, I> + Send + Sync + 'a
) -> Self {
self.add_layer(keymap);
self
}
pub fn add_layer (
&mut self, keymap: impl KeyMap<'a, S, C, I> + Send + Sync + 'a
) -> &mut Self {
self.add_layer_if(|_|true, keymap);
self
}
pub fn layer_if (
mut self,
condition: impl Fn(&S)->bool + Send + Sync + 'a,
keymap: impl KeyMap<'a, S, C, I> + Send + Sync + 'a
) -> Self {
self.add_layer_if(condition, keymap);
self
}
pub fn add_layer_if (
&mut self,
condition: impl Fn(&S)->bool + Send + Sync + 'a,
keymap: impl KeyMap<'a, S, C, I> + Send + Sync + 'a
) -> &mut Self {
self.layers.push((Box::new(condition), Box::new(keymap)));
self
}
}
/// An [InputMap] can be a [KeyMap].
impl<'a, S, C: DslCommand<'a, S>, I: DslInput> KeyMap<'a, S, C, I> for InputMap<'a, S, C, I> {
fn command (&'a self, state: &'a S, input: &'a I) -> Option<C> {
todo!()
}
}
//fn handle <C> (&self, state: &mut T, input: &U) -> Perhaps<bool> {
//for layer in self.layers.iter() {
//if !(layer.0)(state) {
//continue
//}
//let command: Option<C> = SourceIter(layer.1).command::<_, _, _>(state, input);
//if let Some(command) = command {
//if let Some(undo) = command.execute(state)? {
////app.history.push(undo);
//}
//return Ok(Some(true))
//}
//}
//Ok(None)
//}
//}
//fn layer (mut self, keymap: &'static str) -> Self {
//self.add_layer(keymap);
//self
//}
//fn layer_if (mut self, condition: impl Fn(&T)->bool + 'static, keymap: &'static str) -> Self {
//self.add_layer_if(condition, keymap);
//self
//}
//fn add_layer (&mut self, keymap: &'static str) -> &mut Self {
//self.layers.push((Box::new(|_|true), keymap));
//self
//}
//fn add_layer_if (&mut self, condition: impl Fn(&T)->bool + 'static, keymap: &'static str) -> &mut Self {
//self.layers.push((Box::new(condition), keymap));
//self
//}
//}

131
input/src/input_macros.rs Normal file
View file

@ -0,0 +1,131 @@
use crate::*;
/** Implement `Command` for given `State` and collection
* of `Variant` to `handler` mappings. */
#[macro_export] macro_rules! defcom {
([$self:ident, $state:ident:$State:ty] $(($Command:ident $((
$Variant:ident [$($($param:ident: $Param:ty),+)?] $expr:expr
))*))*) => {
$(#[derive(Clone, Debug)] pub enum $Command {
$($Variant $(($($Param),+))?),*
})*
$(command!(|$self: $Command, $state: $State|match $self {
$($Command::$Variant $(($($param),+))? => $expr),*
});)*
};
(|$self:ident, $state:ident:$State:ty| $($Command:ident { $(
$Variant:ident $(($($param:ident: $Param:ty),+))? => $expr:expr
)* $(,)? })*) => {
$(#[derive(Clone, Debug)] pub enum $Command {
$($Variant $(($($Param),+))?),*
})*
$(command!(|$self: $Command, $state: $State|match $self {
$($Command::$Variant $(($($param),+))? => $expr),*
});)*
};
}
/** Implement `Command` for given `State` and `handler` */
#[macro_export] macro_rules! command {
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
impl$(<$($l),+>)? Command<$State> for $Command {
fn execute ($self, $state: &mut $State) -> Perhaps<Self> {
Ok($handler)
}
}
};
}
/** Implement `DslCommand` for given `State` and `Command` */
#[cfg(feature = "dsl")]
#[macro_export] macro_rules! atom_command {
($Command:ty : |$state:ident:<$State:ident: $Trait:path>| { $((
// identifier
$key:literal [
// named parameters
$(
// argument name
$arg:ident
// if type is not provided defaults to Dsl
$(
// type:name separator
:
// argument type
$type:ty
)?
),*
// rest of parameters
$(, ..$rest:ident)?
]
// bound command:
$command:expr
))* }) => {
impl<'a, $State: $Trait> TryFromDsl<'a, $State> for $Command {
fn try_from_expr ($state: &$State, iter: TokenIter) -> Option<Self> {
let iter = iter.clone();
match iter.next() {
$(Some(Token { value: Value::Key($key), .. }) => {
let iter = iter.clone();
$(
let next = iter.next();
if next.is_none() { panic!("no argument: {}", stringify!($arg)); }
let $arg = next.unwrap();
$(let $arg: Option<$type> = Context::<$type>::get($state, &$arg.value);)?
)*
$(let $rest = iter.clone();)?
return $command
},)*
_ => None
}
None
}
}
};
($Command:ty : |$state:ident:$State:ty| { $((
// identifier
$key:literal [
// named parameters
$(
// argument name
$arg:ident
// if type is not provided defaults to Dsl
$(
// type:name separator
:
// argument type
$type:ty
)?
),*
// rest of parameters
$(, ..$rest:ident)?
]
// bound command:
$command:expr
))* }) => {
impl<'a> TryFromDsl<'a, $State> for $Command {
fn try_from_expr ($state: &$State, iter: TokenIter) -> Option<Self> {
let mut iter = iter.clone();
match iter.next() {
$(Some(Token { value: Value::Key($key), .. }) => {
let mut iter = iter.clone();
$(
let next = iter.next();
if next.is_none() { panic!("no argument: {}", stringify!($arg)); }
let $arg = next.unwrap();
$(let $arg: Option<$type> = Context::<$type>::get($state, &$arg.value);)?
)*
$(let $rest = iter.clone();)?
return $command
}),*
_ => None
}
}
}
};
(@bind $state:ident =>$arg:ident ? : $type:ty) => {
let $arg: Option<$type> = Context::<$type>::get($state, $arg);
};
(@bind $state:ident => $arg:ident : $type:ty) => {
let $arg: $type = Context::<$type>::get_or_fail($state, $arg);
};
}

View file

@ -1,177 +0,0 @@
use crate::*;
/// [Input] state that can be matched against a [Value].
pub trait AtomInput: Input {
fn matches_atom (&self, token: &str) -> bool;
}
#[cfg(feature = "dsl")]
pub trait KeyMap<'a> {
/// Try to find a command that matches the current input event.
fn command <S, C: AtomCommand<'a, S>, I: AtomInput> (&'a self, state: &'a S, input: &'a I)
-> Option<C>;
}
#[cfg(feature = "dsl")]
impl<'a> KeyMap<'a> for SourceIter<'a> {
fn command <S, C: AtomCommand<'a, S>, I: AtomInput> (&'a self, state: &'a S, input: &'a I)
-> Option<C>
{
let mut iter = self.clone();
while let Some((token, rest)) = iter.next() {
iter = rest;
match token {
Token { value: Value::Exp(0, exp_iter), .. } => {
let mut exp_iter = exp_iter.clone();
match exp_iter.next() {
Some(Token { value: Value::Sym(binding), .. }) => {
if input.matches_atom(binding) {
if let Some(command) = C::try_from_expr(state, exp_iter.clone()) {
return Some(command)
}
}
},
_ => panic!("invalid config (expected symbol)")
}
},
_ => panic!("invalid config (expected expression)")
}
}
None
}
}
#[cfg(feature = "dsl")]
impl<'a> KeyMap<'a> for TokenIter<'a> {
fn command <S, C: AtomCommand<'a, S>, I: AtomInput> (&'a self, state: &'a S, input: &'a I)
-> Option<C>
{
let mut iter = self.clone();
while let Some(next) = iter.next() {
match next {
Token { value: Value::Exp(0, exp_iter), .. } => {
let mut exp_iter = exp_iter.clone();
match exp_iter.next() {
Some(Token { value: Value::Sym(binding), .. }) => {
if input.matches_atom(binding) {
if let Some(command) = C::try_from_expr(state, exp_iter.clone()) {
return Some(command)
}
}
},
_ => panic!("invalid config (expected symbol)")
}
},
_ => panic!("invalid config (expected expression)")
}
}
None
}
}
/// A [Command] that can be constructed from a [Token].
#[cfg(feature = "dsl")]
pub trait AtomCommand<'a, C>: TryFromAtom<'a, C> + Command<C> {}
#[cfg(feature = "dsl")]
impl<'a, C, T: TryFromAtom<'a, C> + Command<C>> AtomCommand<'a, C> for T {}
/** Implement `AtomCommand` for given `State` and `Command` */
#[cfg(feature = "dsl")]
#[macro_export] macro_rules! atom_command {
($Command:ty : |$state:ident:<$State:ident: $Trait:path>| { $((
// identifier
$key:literal [
// named parameters
$(
// argument name
$arg:ident
// if type is not provided defaults to Atom
$(
// type:name separator
:
// argument type
$type:ty
)?
),*
// rest of parameters
$(, ..$rest:ident)?
]
// bound command:
$command:expr
))* }) => {
impl<'a, $State: $Trait> TryFromAtom<'a, $State> for $Command {
fn try_from_expr ($state: &$State, iter: TokenIter) -> Option<Self> {
let iter = iter.clone();
match iter.next() {
$(Some(Token { value: Value::Key($key), .. }) => {
let iter = iter.clone();
$(
let next = iter.next();
if next.is_none() { panic!("no argument: {}", stringify!($arg)); }
let $arg = next.unwrap();
$(let $arg: Option<$type> = Context::<$type>::get($state, &$arg.value);)?
)*
$(let $rest = iter.clone();)?
return $command
},)*
_ => None
}
None
}
}
};
($Command:ty : |$state:ident:$State:ty| { $((
// identifier
$key:literal [
// named parameters
$(
// argument name
$arg:ident
// if type is not provided defaults to Atom
$(
// type:name separator
:
// argument type
$type:ty
)?
),*
// rest of parameters
$(, ..$rest:ident)?
]
// bound command:
$command:expr
))* }) => {
impl<'a> TryFromAtom<'a, $State> for $Command {
fn try_from_expr ($state: &$State, iter: TokenIter) -> Option<Self> {
let mut iter = iter.clone();
match iter.next() {
$(Some(Token { value: Value::Key($key), .. }) => {
let mut iter = iter.clone();
$(
let next = iter.next();
if next.is_none() { panic!("no argument: {}", stringify!($arg)); }
let $arg = next.unwrap();
$(let $arg: Option<$type> = Context::<$type>::get($state, &$arg.value);)?
)*
$(let $rest = iter.clone();)?
return $command
}),*
_ => None
}
}
}
};
(@bind $state:ident =>$arg:ident ? : $type:ty) => {
let $arg: Option<$type> = Context::<$type>::get($state, $arg);
};
(@bind $state:ident => $arg:ident : $type:ty) => {
let $arg: $type = Context::<$type>::get_or_fail($state, $arg);
};
}
#[cfg(all(test, feature = "dsl"))]
#[test] fn test_atom_keymap () -> Usually<()> {
let keymap = SourceIter::new("");
Ok(())
}

View file

@ -1,19 +1,15 @@
#![feature(associated_type_defaults)]
mod command; pub use self::command::*;
mod handle; pub use self::handle::*;
mod keymap; pub use self::keymap::*;
pub(crate) use std::error::Error;
pub(crate) type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
#[cfg(test)] pub(crate) type Usually<T> = Result<T, Box<dyn Error>>;
#[cfg(feature = "dsl")] pub(crate) use ::tengri_dsl::*;
#[cfg(feature = "dsl")] mod input_dsl;
#[cfg(feature = "dsl")] pub use self::input_dsl::*;
/// Standard error trait.
pub(crate) use std::error::Error;
/// Standard optional result type.
pub(crate) type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
/// Standard result type.
#[cfg(test)]
pub(crate) type Usually<T> = Result<T, Box<dyn Error>>;
mod input_macros;
mod input_command; pub use self::input_command::*;
mod input_handle; pub use self::input_handle::*;
#[cfg(test)]
#[test] fn test_stub_input () -> Usually<()> {
@ -36,3 +32,9 @@ pub(crate) type Usually<T> = Result<T, Box<dyn Error>>;
assert!(!TestInput(false).is_done());
Ok(())
}
#[cfg(all(test, feature = "dsl"))]
#[test] fn test_dsl_keymap () -> Usually<()> {
let keymap = SourceIter::new("");
Ok(())
}