feat(command_interface): Support user specified keymappings

This commit is contained in:
Benedikt Peetz 2023-10-16 14:04:03 +02:00
parent 602eba6a60
commit 9139fa2776
Signed by: bpeetz
GPG Key ID: A5E94010C3A642AD
21 changed files with 1369 additions and 66 deletions

3
.gitignore vendored
View File

@ -8,3 +8,6 @@ trinitrix.log
# IDE stuff # IDE stuff
.idea .idea
.direnv .direnv
# Lua LS stuff
.luarc.json

26
config/lua/init.lua Normal file
View File

@ -0,0 +1,26 @@
--- Add a new keymap. This is just a convenience function which registers the function
-- and at the same time deals with the fact that the whole trinitrix api is async.
---@param mode string
---@param key string
---@param callback function
trinitrix.std.keymaps.add = function(mode, key, callback)
local callback_key = trinitrix.api.register_function(function()
local co = coroutine.create(callback)
while coroutine.status(co) ~= "dead" do
coroutine.resume(co)
end
end)
trinitrix.api.keymaps.add(mode, key, callback_key)
end
trinitrix.std.keymaps.add("ci", "<ESC>", trinitrix.api.ui.set_mode_normal)
trinitrix.std.keymaps.add("n", ":", trinitrix.api.ui.command_line_show)
trinitrix.std.keymaps.add("n", "i", trinitrix.api.ui.set_mode_insert)
-- a simple test to prove that key chords work
trinitrix.std.keymaps.add("n", "jj", function() print("hi") end)
trinitrix.std.keymaps.add("n", "q", trinitrix.api.exit)

View File

@ -4,8 +4,9 @@ use quote::quote;
use syn::{punctuated::Punctuated, token::Comma, GenericArgument, Lifetime, Token, Type}; use syn::{punctuated::Punctuated, token::Comma, GenericArgument, Lifetime, Token, Type};
use crate::{ use crate::{
command_enum_parsing::{Field, FunctionDeclaration, NamespacePath},
generate::{get_input_type_of_bare_fn_field, get_return_type_of_bare_fn_field}, generate::{get_input_type_of_bare_fn_field, get_return_type_of_bare_fn_field},
DataCommandEnum, command_enum_parsing::{NamespacePath, Field, FunctionDeclaration}, DataCommandEnum,
}; };
pub fn generate_rust_wrapper_functions( pub fn generate_rust_wrapper_functions(
@ -97,10 +98,17 @@ fn get_and_add_lifetimes_form_inputs_and_outputs<'a>(
.collect(); .collect();
return Some(lifetime_args); return Some(lifetime_args);
} }
syn::PathArguments::Parenthesized(_) => todo!(), syn::PathArguments::Parenthesized(_) => todo!("Parenthesized Life time"),
} }
} }
_ => todo!(), syn::Type::Tuple(_) => {
// TODO(@soispha): I don't really know if tuples can have lifetimes, but let's just
// ignore them for now <2023-10-14>
dbg!("Ignoring tuple lifetime!");
None
}
non_path => todo!("Non path lifetime: {:#?}", non_path),
} }
} }

View File

@ -0,0 +1,8 @@
%s/::core::fmt::Formatter::debug_tuple_field1_finish(f,\(.*\))/println!(\1); return Ok(())
%s/::alloc::fmt::format(format_args!(\n\s*\(".*"\),\n\s*\(".*"\),\n\s*));/format!(\1,\2);
%s/::alloc::fmt::format(format_args!(\n\s*\(".*"\),\n\s*\(".*"\)\n\s*));/format!(\1,\2);
%s/::alloc::fmt::format(format_args!(\(".*"\), \(".*"\)));/format!(\1,\2);
%s/let lvl = ::log::Level::Info;\n\s*if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {\(\n\s*.*\)\{12}//g
%s/::log::__private_api::Option::None,\n\s*);\n\s*}/

View File

@ -20,6 +20,19 @@ commands {
declare print: fn(CommandTransferValue), declare print: fn(CommandTransferValue),
namespace trinitrix { namespace trinitrix {
/// Language specific functions, which mirror the `trinitrix.api` namespace.
/// That is, if you have to choose between a `std` and a `api` function choose the `std`
/// one as it will most likely be more high-level and easier to use (as it isn't abstracted
/// over multiple languages). Feel free to drop down to the lower level api, if you feel
/// like that more, it should be as stable and user-oriented as the `std` functions
namespace std {
/// This command is a no-op, it's just here to ensure that the 'std'
/// namespace get actually created
// FIXME(@soispha): Add an attribute to namespaces to avoid having to use
// empty functions <2023-10-14>
declare private_initializer_std: fn(),
},
/// Debug only functions, these are effectively useless /// Debug only functions, these are effectively useless
namespace debug { namespace debug {
/// Greets the user /// Greets the user
@ -64,15 +77,61 @@ commands {
declare set_mode_insert: fn(), declare set_mode_insert: fn(),
}, },
/// Functions only used internally within Name /// Manipulate keymappings, the mode is specified as a String build up of all mode
/// the keymapping should be active in. The mapping works as follows:
/// n => normal Mode
/// c => command Mode
/// i => insert Mode
///
/// The key works in a similar matter, specifying the required keypresses to trigger the
/// callback. For example "aba" for require the user to press "a" then "b" then "a" again
/// to trigger the mapping. Special characters are encoded as follows:
/// "<C-a>ba" => "Ctrl+a" then "b" then "a"
/// "<S-a>" => "A" or "Shift+a"
/// "A" => "A"
/// "<M-a> " => "Alt+a" (<A-a>) or "Meta+a"(<M-a>) (most terminals can't really differentiate between these characters)
/// "a<C-b><C-a>" => "a" then "Ctrl+b" then "Ctrl+a" (also works for Shift, Alt and Super)
/// "<CSM-b>" => "Ctrl+Shift+Alt+b" (the ordering doesn't matter)
/// "a " => "a" then a literal space (" ")
/// "å🙂" => "å" then "🙂" (full Unicode support!)
/// "<ESC>" => escape key
/// "<F3>" => F3 key
/// "<BACKSPACE>" => backspace key (and so forth)
/// "<DASH>" => a literal "-"
/// "<ANGULAR_BRACKET_OPEN>" or "<ABO>" => a literal "<"
/// "<ANGULAR_BRACKET_CLOSE>" or "<ABC>" => a literal ">"
///
/// The callback MUST be registered first by calling
/// `trinitrix.api.register_function()` the returned value can than be used to
/// set the keymap.
namespace keymaps {
/// Add a new keymapping
declare add: fn((/* mode: */ String, /* key: */ String, /* callback: */ Function)),
/// Remove a keymapping
///
/// Does nothing, if the keymapping doesn't exists
declare remove: fn((/* mode: */ String, /* key: */ String)),
/// List declared keymappings
declare get: fn(/* mode: */ String),
},
/// Functions only used internally within Trinitrix
namespace raw { namespace raw {
/// Send an error to the default error output /// Send an error to the default error output
declare raise_error: fn(String), declare raise_error: fn(String),
/// Send output to the default output /// Send output to the default output
/// This is mainly used to display the final /// This is mainly used to display the final
/// output of evaluated lua commands. /// output of evaluated lua commands.
declare display_output: fn(String), declare display_output: fn(String),
/// Input a character without checking for possible keymaps
/// If the current state does not expect input, this character is ignored
/// The encoding is the same as in the `trinitrix.api.keymaps` commands
declare send_input_unprocessed: fn(String),
/// This namespace is used to store some command specific data (like functions, as /// This namespace is used to store some command specific data (like functions, as
/// ensuring memory locations stay allocated in garbage collected language is hard) /// ensuring memory locations stay allocated in garbage collected language is hard)
/// ///
@ -82,7 +141,7 @@ commands {
/// namespace get actually created /// namespace get actually created
// FIXME(@soispha): Add an attribute to namespaces to avoid having to use // FIXME(@soispha): Add an attribute to namespaces to avoid having to use
// empty functions <2023-10-14> // empty functions <2023-10-14>
declare __private_initializer: fn(), declare private_initializer_private: fn(),
}, },
}, },
}, },

View File

@ -8,13 +8,12 @@ use super::{support_types::Function, CommandTransferValue, Table};
impl<'lua> FromLua<'lua> for Function { impl<'lua> FromLua<'lua> for Function {
fn from_lua(value: Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> { fn from_lua(value: Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> {
match value { match value {
Value::String(function_id) => { Value::Integer(function_id) => {
return Ok(Function::new( return Ok(Function::new(function_id.try_into().expect(
function_id "We should never have i64::MAX functions
.to_str() registered. The stack will
.context("Failed to convert lua string to rust string")? probably overflow, before this i64 overflows",
.to_owned(), )))
))
} }
_ => unreachable!("The Function type can only take functions!"), _ => unreachable!("The Function type can only take functions!"),
}; };

View File

@ -3,52 +3,51 @@ use std::{
sync::atomic::{AtomicUsize, Ordering}, sync::atomic::{AtomicUsize, Ordering},
}; };
use cli_log::info;
use mlua::{ErrorContext, IntoLua, Lua, Table}; use mlua::{ErrorContext, IntoLua, Lua, Table};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Function { pub struct Function {
id: String, id: usize,
} }
impl Function { impl Function {
pub fn new(function_uuid: String) -> Self { fn get_private<'lua>(lua: &'lua Lua) -> mlua::Result<Table<'lua>> {
Function { id: function_uuid } let private: Table = lua
.globals()
// This is always initialized, as the namespaces are specified in the 'command_list' module
.get::<&str, mlua::Table>("trinitrix")
.context("Failed to access 'trinitrix'")?
.get::<&str, mlua::Table>("api")
.context("Failed to access 'api'")?
.get::<&str, mlua::Table>("raw")
.context("Failed to access 'raw'")?
.get::<&str, mlua::Table>("__private")
.context("Failed to access '__private'")?;
Ok(private)
}
pub fn new(function_id: usize) -> Self {
Function { id: function_id }
} }
pub fn from_lua_function(function: mlua::Function, lua: &Lua) -> mlua::Result<Self> { pub fn from_lua_function(function: mlua::Function, lua: &Lua) -> mlua::Result<Self> {
// TODO(@soispha): Does this expose a vulnerability, as the ids are predictable? <2023-10-14> // TODO(@soispha): Does this expose a vulnerability, as the ids are predictable? <2023-10-14>
static COUNTER: AtomicUsize = AtomicUsize::new(0); static COUNTER: AtomicUsize = AtomicUsize::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed); let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let globals = lua.globals(); let private = Self::get_private(lua)?;
// This is always initialized, as the namespaces are specified in the 'command_list' module info!("Registering function '{}'", id);
let private: Table = globals
.get::<&str, mlua::Table>("trinitrix")
.context("Failed to access 'trinitrix'")?
.get::<&str, mlua::Table>("api")
.context("Failed to access 'api'")?
.get::<&str, mlua::Table>("raw")
.context("Failed to access 'raw'")?
.get::<&str, mlua::Table>("__private")
.context("Failed to access '__private'")?;
private.set(id, function)?; private.set(id, function)?;
Ok(Function::new(format!("{}", id))) Ok(Function::new(id))
} }
pub fn call(&self, lua: &Lua) -> mlua::Result<()> { pub fn call(&self, lua: &Lua) -> mlua::Result<()> {
// This is always initialized, as the namespaces are specified in the 'command_list' module let private = Self::get_private(lua)?;
let private: Table = lua
.globals() info!("Calling function '{}'", &self.id);
.get::<&str, mlua::Table>("trinitrix")
.context("Failed to access 'trinitrix'")?
.get::<&str, mlua::Table>("api")
.context("Failed to access 'api'")?
.get::<&str, mlua::Table>("raw")
.context("Failed to access 'raw'")?
.get::<&str, mlua::Table>("__private")
.context("Failed to access '__private'")?;
let function: mlua::Function = private let function: mlua::Function = private
.get(self.id.clone()) .get(self.id)
.context("Failed to get function associated with callback!")?; .context("Failed to get function associated with callback!")?;
function.call(()).context("Failed to call function") function.call(()).context("Failed to call function")

View File

@ -1,16 +1,21 @@
use std::collections::HashMap; use std::{collections::HashMap, str::FromStr};
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use cli_log::{trace, warn}; use cli_log::{info, trace, warn};
use crossterm::event::Event;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use crate::{ use crate::{
app::{ app::{
command_interface::{ command_interface::{
command_transfer_value::{CommandTransferValue, Table}, command_transfer_value::{CommandTransferValue, Table},
Api, Command, Debug, Raw, Trinitrix, Ui, Api, Command, Debug, Keymaps, Raw, Trinitrix, Ui,
}, },
events::event_types::EventStatus, events::event_types::EventStatus,
keymappings::{
key::{Key, Keys},
trie::Node,
},
status::State, status::State,
App, App,
}, },
@ -32,18 +37,18 @@ pub async fn handle(
// is reserved for functions called only for their output (for example `greet()`). // is reserved for functions called only for their output (for example `greet()`).
macro_rules! send_status_output { macro_rules! send_status_output {
($str:expr) => { ($str:expr) => {
app.status.add_status_message($str.to_owned()); app.status.add_status_message($str.to_owned())
}; };
($str:expr, $($args:ident),+) => { ($str:expr, $($args:ident),+) => {
app.status.add_status_message(format!($str, $($args),+)); app.status.add_status_message(format!($str, $($args),+))
}; };
} }
macro_rules! send_error_output { macro_rules! send_error_output {
($str:expr) => { ($str:expr) => {
app.status.add_error_message($str.to_owned()); app.status.add_error_message($str.to_owned())
}; };
($str:expr, $($args:ident),+) => { ($str:expr, $($args:ident),+) => {
app.status.add_error_message(format!($str, $($args),+)); app.status.add_error_message(format!($str, $($args),+))
}; };
} }
macro_rules! send_main_output { macro_rules! send_main_output {
@ -107,7 +112,7 @@ pub async fn handle(
room.send(msg.clone()).await?; room.send(msg.clone()).await?;
send_status_output!("Sent message: `{}`", msg); send_status_output!("Sent message: `{}`", msg);
} else { } else {
// // FIXME(@soispha): This should raise an error within lua, as it would // FIXME(@soispha): This should raise an error within lua, as it would
// otherwise be very confusing <2023-09-20> // otherwise be very confusing <2023-09-20>
warn!("Can't send message: `{}`, as there is no open room!", &msg); warn!("Can't send message: `{}`, as there is no open room!", &msg);
} }
@ -148,6 +153,46 @@ pub async fn handle(
EventStatus::Ok EventStatus::Ok
} }
}, },
Api::Keymaps(keymaps) => match keymaps {
Keymaps::Add((mode, key, callback)) => {
mode.chars().for_each(|char| {
info!("Setting keymaping ('{}') for mode '{}'", key, char);
let parsed_keys = key
.parse::<Keys>()
.map_err(|err| {
send_error_output!(err.to_string());
})
.expect("We dealt with the error");
match State::from_char(&char) {
Ok(state) => {
info!("Set for state '{}'", state);
let trie;
if let Some(collected_trie) = app.key_mappings.get_mut(&state) {
trie = collected_trie;
} else {
app.key_mappings.insert(state.clone(), Node::new());
trie = app
.key_mappings
.get_mut(&state)
.expect("Should be set");
}
trie.insert(&parsed_keys, callback.to_owned())
.map_err(|err| {
send_error_output!(err.to_string());
})
.expect("We already dealt with the error")
}
Err(err) => send_error_output!(err.to_string()),
};
});
EventStatus::Ok
}
// TODO(@soispha): Well.., we should probably add these functions: <2023-10-15>
Keymaps::Remove((mode, key)) => todo!(),
Keymaps::Get(mode) => todo!(),
},
Api::Raw(raw) => match raw { Api::Raw(raw) => match raw {
Raw::RaiseError(err) => { Raw::RaiseError(err) => {
send_error_output!(err); send_error_output!(err);
@ -164,8 +209,38 @@ pub async fn handle(
// no-op, read the comment about it in the `command_list` // no-op, read the comment about it in the `command_list`
EventStatus::Ok EventStatus::Ok
} }
Raw::SendInputUnprocessed(char) => match app.status.state() {
State::Insert => {
let key = Key::from_str(char)?;
let cross_input: Event = key.try_into()?;
app.ui
.message_compose
.input(tui_textarea::Input::from(cross_input));
EventStatus::Ok
}
State::Command => {
let key = Key::from_str(char)?;
let cross_input: Event = key.try_into()?;
app.ui
.cli
.as_mut()
.expect("This should exist, when the state is 'Command'")
.input(tui_textarea::Input::from(cross_input));
EventStatus::Ok
}
State::Normal
| State::Setup
| State::KeyInputPending {
old_state: _,
pending_keys: _,
} => EventStatus::Ok,
},
}, },
}, },
Trinitrix::Std(_) => {
// no-op, read the comment about it in the `command_list`
EventStatus::Ok
}
}, },
}) })
} }

View File

@ -0,0 +1,11 @@
use anyhow::Result;
use crate::app::{events::event_types::EventStatus, App, command_interface::command_transfer_value::support_types::Function};
// TODO(@soispha): We just assume for now that all functions originate in lua. This module will in
// future versions house check for the language the function came from <2023-10-15>
pub async fn handle(app: &mut App<'_>, function: Function) -> Result<EventStatus> {
app.lua.execute_function(function).await;
Ok(EventStatus::Ok)
}

View File

@ -0,0 +1,95 @@
use anyhow::Result;
use cli_log::info;
use crossterm::event::Event as CrosstermEvent;
use crate::app::{
command_interface::{Api::Raw, Command, Raw::SendInputUnprocessed, Trinitrix::Api},
events::event_types::{Event, EventStatus},
keymappings::key::{Key, Keys},
status::State,
App,
};
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
async fn default(converted_key: Key, app: &mut App<'_>, old_state: &State) -> Result<()> {
// Just let the input event slip through if no keymap matches
app.tx
.send(Event::CommandEvent(
Command::Trinitrix(Api(Raw(SendInputUnprocessed(
converted_key.to_string_repr(),
)))),
None,
))
.await?;
app.status.set_state(old_state.to_owned());
Ok(())
}
if let CrosstermEvent::Key(_) = input_event {
// r
// |
// a
// / \
// b a
// |
// c
//
//
//
// r->a->a: Some([a]) -> a.is_child() && a.is_terminal() ? call a : *key input pending*
// r->a->b: Some([b]) -> b.is_child() && b.is_terminal() ? *call b* : key input pending
// r->a->c: None -> continue
// r->a->a->c: Some([c]) -> c.is_child() && c.is_terminal() ? *call c* : key input pending
//
// r->a: Some([a, b]) -> key input pending
let converted_key: Key = input_event.try_into()?;
info!("Received input to handle: '{}'", converted_key);
let mut converted_keys: Keys = Keys::new(converted_key);
let mut old_state = app.status.state().clone();
if let State::KeyInputPending {
old_state: old,
pending_keys,
} = app.status.state().clone()
{
info!("Found KeyInputPending mode!");
old_state = *old;
converted_keys = pending_keys.join(converted_key);
}
if let Some(key_maps) = app.key_mappings.get(&old_state) {
if let Some((possible_key_maps, should_call)) = key_maps.get(&converted_keys) {
info!("possible key maps: {:#?}", possible_key_maps);
if possible_key_maps.len() == 1 {
let possible_key_map = possible_key_maps.get(0).expect("The len is 1");
if possible_key_map.is_child() && possible_key_map.is_terminal() && should_call {
let function = possible_key_map
.value()
.expect("This node is terminal and a child, it should have a value");
app.tx.send(Event::Function(*function)).await?;
app.status.set_state(old_state.to_owned());
} else {
// The choice does not have a value attached to it (might be a waypoint)
app.status.set_state(State::KeyInputPending {
old_state: Box::new(old_state.to_owned()),
pending_keys: converted_keys,
});
}
} else {
app.status.set_state(State::KeyInputPending {
old_state: Box::new(old_state.to_owned()),
pending_keys: converted_keys,
})
}
} else {
default(converted_key, app, &old_state).await?
}
} else {
default(converted_key, app, &old_state).await?
}
}
Ok(EventStatus::Ok)
}

View File

@ -1,5 +1,5 @@
// input events // input events
pub mod main; pub mod input;
pub mod setup; pub mod setup;
// matrix // matrix
@ -8,3 +8,4 @@ pub mod matrix;
// ci // ci
pub mod command; pub mod command;
pub mod lua_command; pub mod lua_command;
pub mod function;

View File

@ -5,12 +5,11 @@ use cli_log::trace;
use crossterm::event::Event as CrosstermEvent; use crossterm::event::Event as CrosstermEvent;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use self::handlers::{command, lua_command, main, matrix, setup};
use super::EventStatus; use super::EventStatus;
use crate::app::{ use crate::app::{
command_interface::{command_transfer_value::CommandTransferValue, Command}, command_interface::{command_transfer_value::{CommandTransferValue, support_types::Function}, Command},
status::State, status::State,
App, App, events::event_types::event::handlers::{input, matrix, command, lua_command, setup, function},
}; };
#[derive(Debug)] #[derive(Debug)]
@ -19,6 +18,7 @@ pub enum Event {
MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse), MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse),
CommandEvent(Command, Option<oneshot::Sender<CommandTransferValue>>), CommandEvent(Command, Option<oneshot::Sender<CommandTransferValue>>),
LuaCommand(String), LuaCommand(String),
Function(Function),
} }
impl Event { impl Event {
@ -32,23 +32,21 @@ impl Event {
Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx) Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx)
.await .await
.with_context(|| format!("Failed to handle command event: `{:#?}`", event)), .with_context(|| format!("Failed to handle command event: `{:#?}`", event)),
Event::LuaCommand(lua_code) => lua_command::handle(app, lua_code.to_owned()) Event::LuaCommand(lua_code) => lua_command::handle(app, lua_code.to_owned())
.await .await
.with_context(|| format!("Failed to handle lua code: `{}`", lua_code)), .with_context(|| format!("Failed to handle lua code: `{}`", lua_code)),
Event::Function(function) => function::handle(app, function.to_owned())
.await
.with_context(|| format!("Failed to handle function: `{}`", function)),
Event::InputEvent(event) => match app.status.state() { Event::InputEvent(event) => match app.status.state() {
State::Normal => main::handle_normal(app, &event).await.with_context(|| {
format!("Failed to handle input (normal) event: `{:#?}`", event)
}),
State::Insert => main::handle_insert(app, &event).await.with_context(|| {
format!("Failed to handle input (insert) event: `{:#?}`", event)
}),
State::Command => main::handle_command(app, &event).await.with_context(|| {
format!("Failed to handle input (command) event: `{:#?}`", event)
}),
State::Setup => setup::handle(app, &event).await.with_context(|| { State::Setup => setup::handle(app, &event).await.with_context(|| {
format!("Failed to handle input (setup) event: `{:#?}`", event) format!("Failed to handle input (setup) event: `{:#?}`", event)
}), }),
_ => input::handle(app, &event).await.with_context(|| {
format!("Failed to handle input (non-setup) event: `{:#?}`", event)
}),
}, },
} }
} }

View File

@ -0,0 +1,25 @@
use std::{collections::VecDeque, fmt::Display};
#[derive(Debug)]
pub(super) struct Chars(pub(super) VecDeque<char>);
impl Chars {
pub(super) fn peek(&self) -> Option<&char> {
self.0.front()
}
pub(super) fn pop(&mut self) -> Option<char> {
self.0.pop_front()
}
pub(super) fn prepend(&mut self, char_to_prepend: char) {
let mut new_vec = VecDeque::with_capacity(self.0.len() + 1);
new_vec.push_back(char_to_prepend);
new_vec.append(&mut self.0);
self.0 = new_vec;
}
}
impl Display for Chars {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.iter().collect::<String>()[..])
}
}

View File

@ -0,0 +1,297 @@
use std::{str::FromStr, fmt::Display};
use anyhow::{bail, Context};
use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use super::{Chars, KeyValue, Keys};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct Key {
// Modifiers
pub(super) alt: bool,
pub(super) ctrl: bool,
pub(super) meta: bool,
pub(super) shift: bool,
pub(super) value: Option<KeyValue>,
}
impl Key {
pub fn new() -> Self {
Key {
alt: false,
ctrl: false,
meta: false,
shift: false,
value: None,
}
}
pub fn value(&self) -> Option<&KeyValue> {
self.value.as_ref()
}
pub fn to_string_repr(self) -> String {
let mut output = String::new();
if self.alt || self.ctrl || self.meta || self.shift {
output.push('<')
}
if self.alt {
output.push('A');
}
if self.ctrl {
output.push('C');
}
if self.meta {
output.push('M');
}
if self.shift {
output.push('S');
}
if self.alt || self.ctrl || self.meta || self.shift {
output.push('-')
}
output.push_str(
&self
.value
.expect("There can be no Nones here, if the Key comes from the public api")
.to_string(),
);
if self.alt || self.ctrl || self.meta || self.shift {
output.push('>')
}
output
}
fn merge_with(mut self, other: Key) -> Self {
// Modifiers
self.alt = self.alt || other.alt;
self.ctrl = self.ctrl || other.ctrl;
self.meta = self.meta || other.meta;
self.shift = self.shift || other.shift;
self.value = Some(self.value.unwrap_or(other.value.unwrap_or(KeyValue::Null)));
self
}
pub(super) fn parse(chars: &mut Chars) -> anyhow::Result<Self> {
assert_eq!(chars.pop().expect("This is a developer error"), '<');
let mut parse_buffer: Vec<char> = Vec::new();
let mut reached_non_modifier = false;
let mut output_key_filled = false;
let mut output_key = Key::new();
while let Some(char) = chars.pop() {
if char == '>' {
break;
} else {
if char.is_ascii_uppercase()
|| char.is_numeric() && !reached_non_modifier && !output_key_filled
{
parse_buffer.push(char);
} else if char == '-' && !reached_non_modifier && !output_key_filled {
// We moved to the modified char
reached_non_modifier = true;
// Our parse_buffer should only contain modifiers:
let mut alt = false;
let mut ctrl = false;
let mut meta = false;
let mut shift = false;
for char in &parse_buffer {
match char {
'A' => alt = true,
'C' => ctrl = true,
'M' => meta = true,
'S' => shift = true,
char => bail!(
"The char ('{}') is not a valid descriptor of a modifier",
char
),
}
}
output_key = Key {
alt,
ctrl,
meta,
shift,
value: None,
};
} else if reached_non_modifier && !output_key_filled {
if char == '<' {
chars.prepend('<');
let key = Key::parse(chars)?;
output_key = output_key.merge_with(key);
} else {
output_key.value = Some(KeyValue::Char(char));
}
output_key_filled = true;
} else {
bail!(
"Your can not put a this char here!
parse_buffer: '{}';
char: '{}';
chars: '{:#?}';
output_key: '{:#?}' ",
&parse_buffer.iter().collect::<String>(),
&char,
&chars,
&output_key
)
}
}
}
if output_key_filled {
Ok(output_key)
} else {
let mut parse_buffer = Chars(parse_buffer.into());
let get_output = |value: KeyValue| -> Key {
let key = Key {
alt: false,
ctrl: false,
meta: false,
shift: false,
value: Some(value),
};
return key.merge_with(output_key);
};
if let Some(char) = parse_buffer.peek() {
if char == &'F' {
let _f = parse_buffer.pop();
let number: u8 = parse_buffer.to_string().parse().with_context(|| {
format!("Failed to parse buffer ('{}') as u8", &parse_buffer)
})?;
Ok(get_output(KeyValue::F(number)))
} else {
match &parse_buffer.to_string()[..] {
"BACKSPACE" => Ok(get_output(KeyValue::Backspace)),
"ENTER" => Ok(get_output(KeyValue::Enter)),
"LEFT" => Ok(get_output(KeyValue::Left)),
"RIGHT" => Ok(get_output(KeyValue::Right)),
"UP" => Ok(get_output(KeyValue::Up)),
"DOWN" => Ok(get_output(KeyValue::Down)),
"HOME" => Ok(get_output(KeyValue::Home)),
"END" => Ok(get_output(KeyValue::End)),
"PAGEUP" => Ok(get_output(KeyValue::PageUp)),
"PAGEDOWN" => Ok(get_output(KeyValue::PageDown)),
"TAB" => Ok(get_output(KeyValue::Tab)),
"BACKTAB" => Ok(get_output(KeyValue::BackTab)),
"DELETE" => Ok(get_output(KeyValue::Delete)),
"INSERT" => Ok(get_output(KeyValue::Insert)),
"ESC" => Ok(get_output(KeyValue::Esc)),
"CAPSLOCK" => Ok(get_output(KeyValue::CapsLock)),
"SCROLLlOCK" => Ok(get_output(KeyValue::ScrollLock)),
"NUMLOCK" => Ok(get_output(KeyValue::NumLock)),
"PRINTSCREEN" => Ok(get_output(KeyValue::PrintScreen)),
"PAUSE" => Ok(get_output(KeyValue::Pause)),
"MENU" => Ok(get_output(KeyValue::Menu)),
"KEYPADBEGIN" => Ok(get_output(KeyValue::KeypadBegin)),
"DASH" => Ok(get_output(KeyValue::Char('-'))),
"ANGULAR_BRACKET_OPEN" | "ABO" => Ok(get_output(KeyValue::Char('<'))),
"ANGULAR_BRACKET_CLOSE" | "ABC" => Ok(get_output(KeyValue::Char('>'))),
other_str => bail!(
"The String ('{}') is not a correct special key name!",
other_str
),
}
}
} else {
bail!(
"You need to put something into the angulare brackets (<>)
parse_buffer: '{}';
chars: '{:#?}';",
&parse_buffer,
&chars,
)
}
}
}
}
impl Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_string_repr())
}
}
impl Into<Event> for Key {
fn into(self) -> Event {
let mut modifiers;
{
modifiers = KeyModifiers::all();
if !self.alt {
modifiers.remove(KeyModifiers::ALT);
}
if !self.ctrl {
modifiers.remove(KeyModifiers::CONTROL);
}
if !self.meta {
modifiers.remove(KeyModifiers::META);
modifiers.remove(KeyModifiers::SUPER);
}
if !self.shift {
modifiers.remove(KeyModifiers::SHIFT);
}
modifiers.remove(KeyModifiers::HYPER);
modifiers.remove(KeyModifiers::META);
if self.alt || self.ctrl || self.meta || self.shift {
modifiers.remove(KeyModifiers::NONE);
}
}
let output = Event::Key(KeyEvent {
code: self.value.unwrap_or(KeyValue::Null).into(),
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
});
output
}
}
impl TryFrom<&Event> for Key {
type Error = anyhow::Error;
fn try_from(value: &Event) -> std::result::Result<Self, Self::Error> {
let mut output_key: Key = Key::new();
match value {
Event::Key(key_event) => {
{
let key_mods = key_event.modifiers;
output_key.alt = KeyModifiers::intersects(&key_mods, KeyModifiers::ALT);
output_key.ctrl = KeyModifiers::intersects(&key_mods, KeyModifiers::CONTROL);
output_key.meta = KeyModifiers::intersects(&key_mods, KeyModifiers::META)
|| KeyModifiers::intersects(&key_mods, KeyModifiers::SUPER);
output_key.shift = KeyModifiers::intersects(&key_mods, KeyModifiers::SHIFT);
// let hyper = KeyModifiers::intersects(&key_mods, KeyModifiers::HYPER);
// let none = KeyModifiers::intersects(&key_mods, KeyModifiers::NONE);
}
{
let key_code = key_event.code;
output_key.value = Some(key_code.into());
}
Ok(output_key)
}
Event::Mouse(_)
| Event::Paste(_)
| Event::Resize(_, _)
| Event::FocusGained
| Event::FocusLost => bail!("Only supports parsing from key event"),
}
}
}
impl FromStr for Key {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut keys: Keys = s.parse().context("Failed to parse string as keys")?;
if keys.0.len() == 1 {
let key = keys
.0
.pop()
.expect("The vec should have exactly one element");
return Ok(key);
} else {
bail!("The string ('{}') contains more than one key", &s);
}
}
}

View File

@ -0,0 +1,143 @@
use std::fmt::{Display, Write};
use crossterm::event::KeyCode;
// taken directly from crossterm
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum KeyValue {
Backspace,
Enter,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
Tab,
BackTab,
Delete,
Insert,
F(u8),
Char(char),
Null, // TODO(@soispha): What is this key? <2023-10-15>
Esc,
CapsLock,
ScrollLock,
NumLock,
PrintScreen,
Pause,
Menu,
KeypadBegin,
// TODO(@soispha): We could add support for these: <2023-10-15>
// Media(MediaKeyCode),
}
impl Display for KeyValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut w = |str| return f.write_str(str);
match self {
KeyValue::Backspace => w("<BACKSPACE>"),
KeyValue::Enter => w("<ENTER>"),
KeyValue::Left => w("<LEFT>"),
KeyValue::Right => w("<RIGHT>"),
KeyValue::Up => w("<UP>"),
KeyValue::Down => w("<DOWN>"),
KeyValue::Home => w("<HOME>"),
KeyValue::End => w("<END>"),
KeyValue::PageUp => w("<PAGEUP>"),
KeyValue::PageDown => w("<PAGEDOWN>"),
KeyValue::Tab => w("<TAB>"),
KeyValue::BackTab => w("<BACKTAB>"),
KeyValue::Delete => w("<DELETE>"),
KeyValue::Insert => w("<INSERT>"),
KeyValue::F(n) => f.write_fmt(format_args!("<F{}>", n)),
KeyValue::Char(c) => {
match c {
'<' => w("<ANGULAR_BRACKET_OPEN>"),
'>' => w("<ANGULAR_BRACKET_CLOSE>"),
'-' => w("<DASH>"),
c => f.write_char(*c),
}
},
KeyValue::Null => w("<NULL>"),
KeyValue::Esc => w("<ESC>"),
KeyValue::CapsLock => w("<CAPSLOCK>"),
KeyValue::ScrollLock => w("<SCROLLLOCK>"),
KeyValue::NumLock => w("<NUMLOCK>"),
KeyValue::PrintScreen => w("<PRINTSCREEN>"),
KeyValue::Pause => w("<PAUSE>"),
KeyValue::Menu => w("<MENU>"),
KeyValue::KeypadBegin => w("<KEYPADBEGIN>"),
}
}
}
impl From<KeyCode> for KeyValue {
fn from(value: KeyCode) -> Self {
match value {
KeyCode::Backspace => Self::Backspace,
KeyCode::Enter => Self::Enter,
KeyCode::Left => Self::Left,
KeyCode::Right => Self::Right,
KeyCode::Up => Self::Up,
KeyCode::Down => Self::Down,
KeyCode::Home => Self::Home,
KeyCode::End => Self::End,
KeyCode::PageUp => Self::PageUp,
KeyCode::PageDown => Self::PageDown,
KeyCode::Tab => Self::Tab,
KeyCode::BackTab => Self::BackTab,
KeyCode::Delete => Self::Delete,
KeyCode::Insert => Self::Insert,
KeyCode::F(n) => Self::F(n),
KeyCode::Char(c) => Self::Char(c),
KeyCode::Null => Self::Null,
KeyCode::Esc => Self::Esc,
KeyCode::CapsLock => Self::CapsLock,
KeyCode::ScrollLock => Self::ScrollLock,
KeyCode::NumLock => Self::NumLock,
KeyCode::PrintScreen => Self::PrintScreen,
KeyCode::Pause => Self::Pause,
KeyCode::Menu => Self::Menu,
KeyCode::KeypadBegin => Self::KeypadBegin,
// FIXME(@soispha): This reduces our information, casting a KeyCode to a KeyValue
// and back again would not equal the original KeyCode <2023-10-15>
KeyCode::Media(_) => Self::Null,
KeyCode::Modifier(_) => Self::Null,
}
}
}
impl Into<KeyCode> for KeyValue {
fn into(self) -> KeyCode {
match self {
Self::Backspace => KeyCode::Backspace,
Self::Enter => KeyCode::Enter,
Self::Left => KeyCode::Left,
Self::Right => KeyCode::Right,
Self::Up => KeyCode::Up,
Self::Down => KeyCode::Down,
Self::Home => KeyCode::Home,
Self::End => KeyCode::End,
Self::PageUp => KeyCode::PageUp,
Self::PageDown => KeyCode::PageDown,
Self::Tab => KeyCode::Tab,
Self::BackTab => KeyCode::BackTab,
Self::Delete => KeyCode::Delete,
Self::Insert => KeyCode::Insert,
Self::F(n) => KeyCode::F(n),
Self::Char(c) => KeyCode::Char(c),
Self::Null => KeyCode::Null,
Self::Esc => KeyCode::Esc,
Self::CapsLock => KeyCode::CapsLock,
Self::ScrollLock => KeyCode::ScrollLock,
Self::NumLock => KeyCode::NumLock,
Self::PrintScreen => KeyCode::PrintScreen,
Self::Pause => KeyCode::Pause,
Self::Menu => KeyCode::Menu,
Self::KeypadBegin => KeyCode::KeypadBegin,
}
}
}

View File

@ -0,0 +1,117 @@
use anyhow::Context;
use super::{Chars, Key, KeyValue};
use std::{fmt::Display, str::FromStr};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct Keys(pub(super) Vec<Key>);
impl Keys {
pub fn new(initial_value: Key) -> Self {
Keys(vec![initial_value])
}
pub fn join(&self, key: Key) -> Keys {
let mut output = self.0.clone();
output.push(key);
Keys(output)
}
}
pub struct KeysIter {
keys: Keys,
index: usize,
}
pub struct KeysIterB<'a> {
keys: &'a Keys,
index: usize,
}
impl IntoIterator for Keys {
type Item = Key;
type IntoIter = KeysIter;
fn into_iter(self) -> Self::IntoIter {
KeysIter {
keys: self,
index: 0,
}
}
}
impl Iterator for KeysIter {
type Item = Key;
fn next(&mut self) -> Option<Self::Item> {
let output;
if self.keys.0.len() <= self.index {
output = None;
} else {
output = Some(self.keys.0[self.index]);
}
self.index += 1;
output
}
}
impl<'a> IntoIterator for &'a Keys {
type Item = &'a Key;
type IntoIter = KeysIterB<'a>;
fn into_iter(self) -> Self::IntoIter {
KeysIterB {
keys: self,
index: 0,
}
}
}
impl<'a> Iterator for KeysIterB<'a> {
type Item = &'a Key;
fn next(&mut self) -> Option<Self::Item> {
let output;
output = self.keys.0.get(self.index);
self.index += 1;
output
}
}
impl Display for Keys {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&self
.into_iter()
.map(|key| key.to_string_repr())
.collect::<String>(),
)
}
}
impl FromStr for Keys {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut output: Vec<Key> = vec![];
let mut chars = Chars(s.chars().collect());
while let Some(char) = chars.pop() {
match char {
'<' => {
chars.prepend('<');
let key = Key::parse(&mut chars)
.with_context(|| format!("Failed to parse keymapping ('{}')", &chars))?;
output.push(key)
}
other_char => output.push(Key {
alt: false,
ctrl: false,
meta: false,
shift: false,
value: Some(KeyValue::Char(other_char)),
}),
}
}
Ok(Keys(output))
}
}

View File

@ -0,0 +1,160 @@
pub mod key;
pub mod keys;
pub mod chars;
pub mod key_value;
pub use key::*;
pub use keys::*;
pub(self) use chars::*;
pub use key_value::*;
#[cfg(test)]
mod test {
use crate::app::keymappings::key::{Key, KeyValue};
use anyhow::Error;
use pretty_assertions::assert_eq;
use super::Keys;
// "<C-a>ba" => "Ctrl+a" && "b" && "a"
// "<S-a>" => "A" || "Shift+a"
// "A" => "A"
// "<M-a> " => "Alt+a" || "Super+a"
// "a<C-b><C-a>" => "a" && "Ctrl+b" && "Ctrl+a"
// "<CSM-b>" => "Ctrl+Shift+Alt+b"
// "a " => "a" && " "
// "å🙂" => "å" && "🙂"
// "<ESC>" => escape key
// "<BACKSPACE>" => backspace key (and so forth)
// "<DASH>" => "-"
// "<ANGULAR_BRACKET_OPEN>" || "<ABO>" => "<"
// "<ANGULAR_BRACKET_CLOSE>" || "<ABC>" => ">"
#[test]
fn test_simple() {
let keys: Keys = "<C-a>".parse().unwrap();
assert_eq!(
keys,
Keys(vec![Key {
alt: false,
ctrl: true,
meta: false,
shift: false,
value: Some(KeyValue::Char('a'))
}])
)
}
#[test]
fn test_string_repr() {
let key = Key {
alt: true,
ctrl: false,
meta: true,
shift: true,
value: Some(KeyValue::Up),
};
assert_eq!("<AMS-<UP>>".to_owned(), key.to_string_repr());
}
#[test]
fn test_string_repr_special() {
let key = Key {
alt: true,
ctrl: false,
meta: true,
shift: true,
value: Some(KeyValue::Char('<')),
};
assert_eq!("<AMS-<ANGULAR_BRACKET_OPEN>>".to_owned(), key.to_string_repr());
}
#[test]
fn test_extra_special_keys() {
// The <C--> part works! Although we should probably not encourage it
let keys: Keys = "<C--><C-<DASH>><A-<ABO>><ABC>".parse().unwrap();
assert_eq!(
keys,
Keys(vec![
Key {
alt: false,
ctrl: true,
meta: false,
shift: false,
value: Some(KeyValue::Char('-'))
},
Key {
alt: false,
ctrl: true,
meta: false,
shift: false,
value: Some(KeyValue::Char('-'))
},
Key {
alt: true,
ctrl: false,
meta: false,
shift: false,
value: Some(KeyValue::Char('<'))
},
Key {
alt: false,
ctrl: false,
meta: false,
shift: false,
value: Some(KeyValue::Char('>'))
},
])
)
}
#[test]
fn test_false_pattern() {
let keys: Result<Keys, Error> = "<c->".parse();
assert!(keys.is_err())
}
#[test]
fn test_complex() {
let keys: Keys = "<C-a><ACMS-<ESC>>a 🙂".parse().unwrap();
assert_eq!(
keys,
Keys(vec![
Key {
alt: false,
ctrl: true,
meta: false,
shift: false,
value: Some(KeyValue::Char('a'))
},
Key {
alt: true,
ctrl: true,
meta: true,
shift: true,
value: Some(KeyValue::Esc)
},
Key {
alt: false,
ctrl: false,
meta: false,
shift: false,
value: Some(KeyValue::Char('a'))
},
Key {
alt: false,
ctrl: false,
meta: false,
shift: false,
value: Some(KeyValue::Char(' '))
},
Key {
alt: false,
ctrl: false,
meta: false,
shift: false,
value: Some(KeyValue::Char('🙂'))
},
])
)
}
}

View File

@ -0,0 +1,2 @@
pub mod key;
pub mod trie;

237
src/app/keymappings/trie.rs Normal file
View File

@ -0,0 +1,237 @@
use std::collections::HashMap;
use anyhow::bail;
use cli_log::info;
use super::key::{Key, Keys};
#[derive(Debug, PartialEq, Eq)]
pub struct Node<V: std::fmt::Display> {
children: HashMap<Key, Box<Node<V>>>,
value: Option<V>,
is_terminal: bool,
is_child: bool,
}
impl<V: std::fmt::Display> Node<V> {
pub fn new() -> Node<V> {
Node {
children: HashMap::new(),
is_terminal: true,
is_child: false,
value: None,
}
}
pub fn new_child() -> Node<V> {
Node {
children: HashMap::new(),
is_terminal: true,
is_child: true,
value: None,
}
}
pub fn is_terminal(&self) -> bool {
self.is_terminal
}
pub fn is_child(&self) -> bool {
self.is_child
}
/// Get a reference to a child value of this node by key, will return None if the key does not exist.
/// If the key does exists, but does not have any values associated with it, it will return an
/// empty vector
/// The boolean denotes if the returned node is a true end or just a waypoint. It should be
/// called, when the bool is true
pub fn get(&self, keys: &Keys) -> Option<(Vec<&Box<Node<V>>>, bool)> {
let mut current_node = self;
let mut old_node = None;
for char in keys {
// r
// |
// a
// / \
// b a
// |
// c
//
//
//
// r->a->a: Some([a]) -> a.is_child() && a.is_terminal() ? call a : *key input pending*
// r->a->b: Some([b]) -> b.is_child() && b.is_terminal() ? *call b* : key input pending
// r->a->c: None -> continue
// r->a->a->c: Some([c]) -> c.is_child() && c.is_terminal() ? *call c* : key input pending
//
// r->a: Some([a, b]) -> key input pending
if let Some(node) = current_node.children.get(&char) {
old_node = Some((current_node, char));
current_node = node;
} else {
return None;
}
}
if current_node.is_child() && current_node.is_terminal() {
let (on, char) = old_node.expect("At this point, this should be Some");
info!("Returning calling node for char: '{}'", char);
Some((
vec![on
.children
.get(&char)
.expect("This should be some, as this was checked above")],
true,
))
} else {
Some((current_node.children.values().collect(), false))
}
}
/// Insert a key value pair into the trie. The key is supplied as a string to facilitate the
/// creation of nested nodes.
pub fn insert(&mut self, keys: &Keys, value: V) -> anyhow::Result<()> {
let mut current_node = self;
for char in keys {
let child_node = current_node
.children
.entry(char.to_owned())
.or_insert(Box::new(Node::new_child()));
if current_node.value.is_some() {
bail!(
"The key ('{}') contains nodes, which already have a value set!",
keys
);
}
current_node.is_terminal = false;
current_node = child_node
}
if current_node.value.is_some() {
bail!(
"The key ('{}') is already set! The value is: '{}'",
keys,
current_node.value.as_ref().expect("This should be set")
)
} else if current_node.children.len() > 0 {
bail!(
"The node accessed by this key ('{}') has children! You can't set a value for it",
keys
)
} else {
current_node.value = Some(value);
Ok(())
}
}
/// Return the values from this node. If the node does not have a value associated with it,
/// return None
pub fn value(&self) -> Option<&V> {
self.value.as_ref()
}
/// Collect all values from this nodes children. Can be called recursively as it should be
/// tail-recursive.
pub fn collect_values_all(&self) -> Vec<&V> {
if self.is_terminal && self.is_child {
vec![self.value.as_ref().expect("We checked above")]
} else {
let out: Vec<_> = self
.children
.values()
.map(|node| node.collect_values_all())
.flatten()
.collect();
out
}
}
}
#[cfg(test)]
mod test {
use crate::app::keymappings::key::{Key, Keys};
use pretty_assertions::assert_eq;
use super::Node;
fn i(str: &str) -> Keys {
str.parse().unwrap()
}
fn k(str: &str) -> Key {
str.parse::<Key>().unwrap()
}
fn collect<V: std::fmt::Display>(nodes: Option<(Vec<&Box<Node<V>>>, bool)>) -> Vec<&V> {
let (nodes, _should_call) = nodes.unwrap();
nodes
.iter()
.map(|node| node.value())
.filter_map(|x| x)
.collect()
}
#[test]
fn test_empty_values() {
let trie: Node<bool> = Node::new();
assert_eq!(trie.collect_values_all(), Vec::<&bool>::new());
}
#[test]
fn test_insert() {
let mut trie: Node<bool> = Node::new();
trie.insert(&i("abc"), true).unwrap();
trie.insert(&i("abd"), true).unwrap();
trie.insert(&i("aca"), false).unwrap();
let output: Vec<&bool> = vec![&true, &true];
let get_output: Vec<_> = collect(trie.get(&i("ab")));
assert_eq!(get_output, output);
}
#[test]
fn test_duplicate_insert() {
let mut trie: Node<bool> = Node::new();
trie.insert(&i("abc"), true).unwrap();
trie.insert(&i("aca"), true).unwrap();
let output = trie.insert(&i("ab"), false);
dbg!(&trie);
assert!(output.is_err());
}
#[test]
fn test_multiple_get() {
// | | | | |
// a <- Input 1st a | q | i
// / \ | |
// b a <- Input 2nd a | |
// / \ | |
// d e
let mut trie: Node<bool> = Node::new();
let mut output: Node<bool> = Node::new();
trie.insert(&i("aa"), true).unwrap();
trie.insert(&i("abd"), true).unwrap();
trie.insert(&i("abe"), true).unwrap();
output.insert(&i("abd"), true).unwrap();
output.insert(&i("abe"), true).unwrap();
output.insert(&i("aa"), true).unwrap();
output.insert(&i("acd"), true).unwrap();
output.insert(&i("ace"), true).unwrap();
let a_children = &output.children.get(&k("a")).unwrap();
let output_nodes = vec![&a_children.children[&k("a")], &a_children.children[&k("b")]];
let (nodes, _should_call) = trie.get(&i("a")).unwrap();
assert_eq!(nodes, output_nodes);
}
#[test]
fn test_wrong_get() {
let mut trie: Node<bool> = Node::new();
trie.insert(&i("abc"), true).unwrap();
trie.insert(&i("abd"), true).unwrap();
trie.insert(&i("aca"), false).unwrap();
assert!(trie.get(&i("bb")).is_none());
}
}

View File

@ -1,9 +1,13 @@
pub mod command_interface; pub mod command_interface;
pub mod keymappings;
pub mod config; pub mod config;
pub mod events; pub mod events;
pub mod status; pub mod status;
use std::path::{Path, PathBuf}; use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use cli_log::{info, warn}; use cli_log::{info, warn};
@ -12,7 +16,12 @@ use matrix_sdk::Client;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use self::{command_interface::lua_command_manager::LuaCommandManager, events::event_types}; use self::{
command_interface::{
command_transfer_value::support_types::Function, lua_command_manager::LuaCommandManager,
},
events::event_types, keymappings::trie::Node,
};
use crate::{ use crate::{
accounts::{Account, AccountsManager}, accounts::{Account, AccountsManager},
app::{ app::{
@ -22,8 +31,8 @@ use crate::{
ui::{central, setup}, ui::{central, setup},
}; };
pub struct App<'ui> { pub struct App<'runtime> {
ui: central::UI<'ui>, ui: central::UI<'runtime>,
accounts_manager: AccountsManager, accounts_manager: AccountsManager,
status: Status, status: Status,
@ -36,6 +45,8 @@ pub struct App<'ui> {
lua: LuaCommandManager, lua: LuaCommandManager,
project_dirs: ProjectDirs, project_dirs: ProjectDirs,
key_mappings: HashMap<State, Node<Function>>,
} }
impl App<'_> { impl App<'_> {
@ -66,6 +77,7 @@ impl App<'_> {
"Failed to allocate project direcectory paths, \ "Failed to allocate project direcectory paths, \
please ensure your $HOME is set correctly", please ensure your $HOME is set correctly",
)?, )?,
key_mappings: HashMap::new(),
}) })
} }

View File

@ -1,6 +1,6 @@
use core::fmt; use core::fmt;
use anyhow::{Error, Result}; use anyhow::{bail, Error, Result};
use cli_log::warn; use cli_log::warn;
use indexmap::IndexMap; use indexmap::IndexMap;
use matrix_sdk::{ use matrix_sdk::{
@ -12,12 +12,36 @@ use matrix_sdk::{
Client, Client,
}; };
use super::keymappings::key::Keys;
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
pub enum State { pub enum State {
Normal, Normal,
Insert, Insert,
Command, Command,
/// Temporary workaround until command based login is working /// Temporary workaround until command based login is working
Setup, Setup,
/// Only used internally to signal, that we are waiting on further keyinputs, if multiple
/// keymappings have the same prefix
KeyInputPending {
old_state: Box<State>,
pending_keys: Keys,
},
}
impl State {
pub fn from_char(c: &char) -> Result<Self> {
Ok(match c {
'n' => State::Normal,
'i' => State::Insert,
'c' => State::Command,
's' => State::Setup,
_ => bail!(
"The letter '{}' is either not connected to a state or not yet implemented",
c
),
})
}
} }
pub struct Room { pub struct Room {
@ -60,6 +84,10 @@ impl fmt::Display for State {
Self::Insert => write!(f, "Insert"), Self::Insert => write!(f, "Insert"),
Self::Command => write!(f, "Command"), Self::Command => write!(f, "Command"),
Self::Setup => write!(f, "Setup (!! workaround !!)"), Self::Setup => write!(f, "Setup (!! workaround !!)"),
Self::KeyInputPending {
old_state: _,
pending_keys: _,
} => write!(f, "Key Input Pending"),
} }
} }
} }