From 9139fa277634d148ec7309942cbdf3b464ad1467 Mon Sep 17 00:00:00 2001 From: Soispha Date: Mon, 16 Oct 2023 14:04:03 +0200 Subject: [PATCH] feat(command_interface): Support user specified keymappings --- .gitignore | 3 + config/lua/init.lua | 26 ++ .../lua_wrapper/rust_wrapper_functions/mod.rs | 14 +- .../command_list/input_cleaner.vim | 8 + src/app/command_interface/command_list/mod.rs | 63 +++- .../command_transfer_value/lua.rs | 13 +- .../command_transfer_value/support_types.rs | 55 ++-- .../event_types/event/handlers/command.rs | 91 +++++- .../event_types/event/handlers/function.rs | 11 + .../event_types/event/handlers/input.rs | 95 ++++++ .../events/event_types/event/handlers/mod.rs | 3 +- src/app/events/event_types/event/mod.rs | 22 +- src/app/keymappings/key/chars.rs | 25 ++ src/app/keymappings/key/key.rs | 297 ++++++++++++++++++ src/app/keymappings/key/key_value.rs | 143 +++++++++ src/app/keymappings/key/keys.rs | 117 +++++++ src/app/keymappings/key/mod.rs | 160 ++++++++++ src/app/keymappings/mod.rs | 2 + src/app/keymappings/trie.rs | 237 ++++++++++++++ src/app/mod.rs | 20 +- src/app/status.rs | 30 +- 21 files changed, 1369 insertions(+), 66 deletions(-) create mode 100644 config/lua/init.lua create mode 100644 src/app/command_interface/command_list/input_cleaner.vim create mode 100644 src/app/events/event_types/event/handlers/function.rs create mode 100644 src/app/events/event_types/event/handlers/input.rs create mode 100644 src/app/keymappings/key/chars.rs create mode 100644 src/app/keymappings/key/key.rs create mode 100644 src/app/keymappings/key/key_value.rs create mode 100644 src/app/keymappings/key/keys.rs create mode 100644 src/app/keymappings/key/mod.rs create mode 100644 src/app/keymappings/mod.rs create mode 100644 src/app/keymappings/trie.rs diff --git a/.gitignore b/.gitignore index fe48077..a886a30 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ trinitrix.log # IDE stuff .idea .direnv + +# Lua LS stuff +.luarc.json diff --git a/config/lua/init.lua b/config/lua/init.lua new file mode 100644 index 0000000..7bc9d76 --- /dev/null +++ b/config/lua/init.lua @@ -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", "", 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) diff --git a/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs b/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs index 0f16dd5..c0dc052 100644 --- a/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs +++ b/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs @@ -4,8 +4,9 @@ use quote::quote; use syn::{punctuated::Punctuated, token::Comma, GenericArgument, Lifetime, Token, Type}; use crate::{ + command_enum_parsing::{Field, FunctionDeclaration, NamespacePath}, 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( @@ -97,10 +98,17 @@ fn get_and_add_lifetimes_form_inputs_and_outputs<'a>( .collect(); 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), } } diff --git a/src/app/command_interface/command_list/input_cleaner.vim b/src/app/command_interface/command_list/input_cleaner.vim new file mode 100644 index 0000000..5ebcb20 --- /dev/null +++ b/src/app/command_interface/command_list/input_cleaner.vim @@ -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*}/ diff --git a/src/app/command_interface/command_list/mod.rs b/src/app/command_interface/command_list/mod.rs index 15ca747..9826c58 100644 --- a/src/app/command_interface/command_list/mod.rs +++ b/src/app/command_interface/command_list/mod.rs @@ -20,6 +20,19 @@ commands { declare print: fn(CommandTransferValue), 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 namespace debug { /// Greets the user @@ -64,15 +77,61 @@ commands { 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: + /// "ba" => "Ctrl+a" then "b" then "a" + /// "" => "A" or "Shift+a" + /// "A" => "A" + /// " " => "Alt+a" () or "Meta+a"() (most terminals can't really differentiate between these characters) + /// "a" => "a" then "Ctrl+b" then "Ctrl+a" (also works for Shift, Alt and Super) + /// "" => "Ctrl+Shift+Alt+b" (the ordering doesn't matter) + /// "a " => "a" then a literal space (" ") + /// "å🙂" => "å" then "🙂" (full Unicode support!) + /// "" => escape key + /// "" => F3 key + /// "" => backspace key (and so forth) + /// "" => a literal "-" + /// "" or "" => a literal "<" + /// "" or "" => 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 { /// Send an error to the default error output declare raise_error: fn(String), + /// Send output to the default output /// This is mainly used to display the final /// output of evaluated lua commands. 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 /// ensuring memory locations stay allocated in garbage collected language is hard) /// @@ -82,7 +141,7 @@ commands { /// namespace get actually created // FIXME(@soispha): Add an attribute to namespaces to avoid having to use // empty functions <2023-10-14> - declare __private_initializer: fn(), + declare private_initializer_private: fn(), }, }, }, diff --git a/src/app/command_interface/command_transfer_value/lua.rs b/src/app/command_interface/command_transfer_value/lua.rs index 36f8f65..bd03ea3 100644 --- a/src/app/command_interface/command_transfer_value/lua.rs +++ b/src/app/command_interface/command_transfer_value/lua.rs @@ -8,13 +8,12 @@ use super::{support_types::Function, CommandTransferValue, Table}; impl<'lua> FromLua<'lua> for Function { fn from_lua(value: Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result { match value { - Value::String(function_id) => { - return Ok(Function::new( - function_id - .to_str() - .context("Failed to convert lua string to rust string")? - .to_owned(), - )) + Value::Integer(function_id) => { + return Ok(Function::new(function_id.try_into().expect( + "We should never have i64::MAX functions + registered. The stack will + probably overflow, before this i64 overflows", + ))) } _ => unreachable!("The Function type can only take functions!"), }; diff --git a/src/app/command_interface/command_transfer_value/support_types.rs b/src/app/command_interface/command_transfer_value/support_types.rs index d9ade5d..031e205 100644 --- a/src/app/command_interface/command_transfer_value/support_types.rs +++ b/src/app/command_interface/command_transfer_value/support_types.rs @@ -3,52 +3,51 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, }; +use cli_log::info; use mlua::{ErrorContext, IntoLua, Lua, Table}; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Function { - id: String, + id: usize, } impl Function { - pub fn new(function_uuid: String) -> Self { - Function { id: function_uuid } + fn get_private<'lua>(lua: &'lua Lua) -> mlua::Result> { + 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 { // TODO(@soispha): Does this expose a vulnerability, as the ids are predictable? <2023-10-14> static COUNTER: AtomicUsize = AtomicUsize::new(0); let id = COUNTER.fetch_add(1, Ordering::Relaxed); - let globals = lua.globals(); - // This is always initialized, as the namespaces are specified in the 'command_list' module - 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'")?; + let private = Self::get_private(lua)?; + info!("Registering function '{}'", id); private.set(id, function)?; - Ok(Function::new(format!("{}", id))) + Ok(Function::new(id)) } pub fn call(&self, lua: &Lua) -> mlua::Result<()> { - // This is always initialized, as the namespaces are specified in the 'command_list' module - let private: Table = lua - .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'")?; + let private = Self::get_private(lua)?; + + info!("Calling function '{}'", &self.id); + let function: mlua::Function = private - .get(self.id.clone()) + .get(self.id) .context("Failed to get function associated with callback!")?; function.call(()).context("Failed to call function") diff --git a/src/app/events/event_types/event/handlers/command.rs b/src/app/events/event_types/event/handlers/command.rs index f984892..76f2c41 100644 --- a/src/app/events/event_types/event/handlers/command.rs +++ b/src/app/events/event_types/event/handlers/command.rs @@ -1,16 +1,21 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use anyhow::{Error, Result}; -use cli_log::{trace, warn}; +use cli_log::{info, trace, warn}; +use crossterm::event::Event; use tokio::sync::oneshot; use crate::{ app::{ command_interface::{ command_transfer_value::{CommandTransferValue, Table}, - Api, Command, Debug, Raw, Trinitrix, Ui, + Api, Command, Debug, Keymaps, Raw, Trinitrix, Ui, }, events::event_types::EventStatus, + keymappings::{ + key::{Key, Keys}, + trie::Node, + }, status::State, App, }, @@ -32,18 +37,18 @@ pub async fn handle( // is reserved for functions called only for their output (for example `greet()`). macro_rules! send_status_output { ($str:expr) => { - app.status.add_status_message($str.to_owned()); + app.status.add_status_message($str.to_owned()) }; ($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 { ($str:expr) => { - app.status.add_error_message($str.to_owned()); + app.status.add_error_message($str.to_owned()) }; ($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 { @@ -107,7 +112,7 @@ pub async fn handle( room.send(msg.clone()).await?; send_status_output!("Sent message: `{}`", msg); } 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> warn!("Can't send message: `{}`, as there is no open room!", &msg); } @@ -148,6 +153,46 @@ pub async fn handle( 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::() + .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 { Raw::RaiseError(err) => { send_error_output!(err); @@ -164,8 +209,38 @@ pub async fn handle( // no-op, read the comment about it in the `command_list` 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 + } }, }) } diff --git a/src/app/events/event_types/event/handlers/function.rs b/src/app/events/event_types/event/handlers/function.rs new file mode 100644 index 0000000..44c848b --- /dev/null +++ b/src/app/events/event_types/event/handlers/function.rs @@ -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 { + app.lua.execute_function(function).await; + + Ok(EventStatus::Ok) +} diff --git a/src/app/events/event_types/event/handlers/input.rs b/src/app/events/event_types/event/handlers/input.rs new file mode 100644 index 0000000..a4b72b6 --- /dev/null +++ b/src/app/events/event_types/event/handlers/input.rs @@ -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 { + 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) +} diff --git a/src/app/events/event_types/event/handlers/mod.rs b/src/app/events/event_types/event/handlers/mod.rs index 2c08e0d..dd58e16 100644 --- a/src/app/events/event_types/event/handlers/mod.rs +++ b/src/app/events/event_types/event/handlers/mod.rs @@ -1,5 +1,5 @@ // input events -pub mod main; +pub mod input; pub mod setup; // matrix @@ -8,3 +8,4 @@ pub mod matrix; // ci pub mod command; pub mod lua_command; +pub mod function; diff --git a/src/app/events/event_types/event/mod.rs b/src/app/events/event_types/event/mod.rs index dd2e131..d327e11 100644 --- a/src/app/events/event_types/event/mod.rs +++ b/src/app/events/event_types/event/mod.rs @@ -5,12 +5,11 @@ use cli_log::trace; use crossterm::event::Event as CrosstermEvent; use tokio::sync::oneshot; -use self::handlers::{command, lua_command, main, matrix, setup}; use super::EventStatus; use crate::app::{ - command_interface::{command_transfer_value::CommandTransferValue, Command}, + command_interface::{command_transfer_value::{CommandTransferValue, support_types::Function}, Command}, status::State, - App, + App, events::event_types::event::handlers::{input, matrix, command, lua_command, setup, function}, }; #[derive(Debug)] @@ -19,6 +18,7 @@ pub enum Event { MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse), CommandEvent(Command, Option>), LuaCommand(String), + Function(Function), } impl Event { @@ -32,23 +32,21 @@ impl Event { Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx) .await .with_context(|| format!("Failed to handle command event: `{:#?}`", event)), + Event::LuaCommand(lua_code) => lua_command::handle(app, lua_code.to_owned()) .await .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() { - 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(|| { format!("Failed to handle input (setup) event: `{:#?}`", event) }), + _ => input::handle(app, &event).await.with_context(|| { + format!("Failed to handle input (non-setup) event: `{:#?}`", event) + }), }, } } diff --git a/src/app/keymappings/key/chars.rs b/src/app/keymappings/key/chars.rs new file mode 100644 index 0000000..942dbce --- /dev/null +++ b/src/app/keymappings/key/chars.rs @@ -0,0 +1,25 @@ +use std::{collections::VecDeque, fmt::Display}; + +#[derive(Debug)] +pub(super) struct Chars(pub(super) VecDeque); +impl Chars { + pub(super) fn peek(&self) -> Option<&char> { + self.0.front() + } + pub(super) fn pop(&mut self) -> Option { + 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::()[..]) + } +} diff --git a/src/app/keymappings/key/key.rs b/src/app/keymappings/key/key.rs new file mode 100644 index 0000000..74f06cb --- /dev/null +++ b/src/app/keymappings/key/key.rs @@ -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, +} +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 { + assert_eq!(chars.pop().expect("This is a developer error"), '<'); + let mut parse_buffer: Vec = 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::(), + &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 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 { + 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 { + 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); + } + } +} diff --git a/src/app/keymappings/key/key_value.rs b/src/app/keymappings/key/key_value.rs new file mode 100644 index 0000000..8a526df --- /dev/null +++ b/src/app/keymappings/key/key_value.rs @@ -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(""), + KeyValue::Enter => w(""), + KeyValue::Left => w(""), + KeyValue::Right => w(""), + KeyValue::Up => w(""), + KeyValue::Down => w(""), + KeyValue::Home => w(""), + KeyValue::End => w(""), + KeyValue::PageUp => w(""), + KeyValue::PageDown => w(""), + KeyValue::Tab => w(""), + KeyValue::BackTab => w(""), + KeyValue::Delete => w(""), + KeyValue::Insert => w(""), + KeyValue::F(n) => f.write_fmt(format_args!("", n)), + KeyValue::Char(c) => { + match c { + '<' => w(""), + '>' => w(""), + '-' => w(""), + c => f.write_char(*c), + } + + }, + KeyValue::Null => w(""), + KeyValue::Esc => w(""), + KeyValue::CapsLock => w(""), + KeyValue::ScrollLock => w(""), + KeyValue::NumLock => w(""), + KeyValue::PrintScreen => w(""), + KeyValue::Pause => w(""), + KeyValue::Menu => w(""), + KeyValue::KeypadBegin => w(""), + } + } +} + +impl From 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 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, + } + } +} diff --git a/src/app/keymappings/key/keys.rs b/src/app/keymappings/key/keys.rs new file mode 100644 index 0000000..746465c --- /dev/null +++ b/src/app/keymappings/key/keys.rs @@ -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); + +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 { + 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 { + 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::(), + ) + } +} + +impl FromStr for Keys { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut output: Vec = 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)) + } +} diff --git a/src/app/keymappings/key/mod.rs b/src/app/keymappings/key/mod.rs new file mode 100644 index 0000000..97806e6 --- /dev/null +++ b/src/app/keymappings/key/mod.rs @@ -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; + + // "ba" => "Ctrl+a" && "b" && "a" + // "" => "A" || "Shift+a" + // "A" => "A" + // " " => "Alt+a" || "Super+a" + // "a" => "a" && "Ctrl+b" && "Ctrl+a" + // "" => "Ctrl+Shift+Alt+b" + // "a " => "a" && " " + // "å🙂" => "å" && "🙂" + // "" => escape key + // "" => backspace key (and so forth) + // "" => "-" + // "" || "" => "<" + // "" || "" => ">" + #[test] + fn test_simple() { + let keys: Keys = "".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!(">".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!(">".to_owned(), key.to_string_repr()); + } + + #[test] + fn test_extra_special_keys() { + // The part works! Although we should probably not encourage it + let keys: Keys = ">>".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 = "".parse(); + assert!(keys.is_err()) + } + + #[test] + fn test_complex() { + let keys: Keys = ">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('🙂')) + }, + ]) + ) + } +} diff --git a/src/app/keymappings/mod.rs b/src/app/keymappings/mod.rs new file mode 100644 index 0000000..1346c2a --- /dev/null +++ b/src/app/keymappings/mod.rs @@ -0,0 +1,2 @@ +pub mod key; +pub mod trie; diff --git a/src/app/keymappings/trie.rs b/src/app/keymappings/trie.rs new file mode 100644 index 0000000..cc905eb --- /dev/null +++ b/src/app/keymappings/trie.rs @@ -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 { + children: HashMap>>, + value: Option, + is_terminal: bool, + is_child: bool, +} + +impl Node { + pub fn new() -> Node { + Node { + children: HashMap::new(), + is_terminal: true, + is_child: false, + value: None, + } + } + pub fn new_child() -> Node { + 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>>, 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::().unwrap() + } + fn collect(nodes: Option<(Vec<&Box>>, 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 = Node::new(); + assert_eq!(trie.collect_values_all(), Vec::<&bool>::new()); + } + + #[test] + fn test_insert() { + let mut trie: Node = 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 = 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 = Node::new(); + let mut output: Node = 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 = 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()); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index dfd53cd..fd9c8b0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,9 +1,13 @@ pub mod command_interface; +pub mod keymappings; pub mod config; pub mod events; pub mod status; -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use anyhow::{Context, Error, Result}; use cli_log::{info, warn}; @@ -12,7 +16,12 @@ use matrix_sdk::Client; use tokio::sync::mpsc; 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::{ accounts::{Account, AccountsManager}, app::{ @@ -22,8 +31,8 @@ use crate::{ ui::{central, setup}, }; -pub struct App<'ui> { - ui: central::UI<'ui>, +pub struct App<'runtime> { + ui: central::UI<'runtime>, accounts_manager: AccountsManager, status: Status, @@ -36,6 +45,8 @@ pub struct App<'ui> { lua: LuaCommandManager, project_dirs: ProjectDirs, + + key_mappings: HashMap>, } impl App<'_> { @@ -66,6 +77,7 @@ impl App<'_> { "Failed to allocate project direcectory paths, \ please ensure your $HOME is set correctly", )?, + key_mappings: HashMap::new(), }) } diff --git a/src/app/status.rs b/src/app/status.rs index da0f607..b908ad9 100644 --- a/src/app/status.rs +++ b/src/app/status.rs @@ -1,6 +1,6 @@ use core::fmt; -use anyhow::{Error, Result}; +use anyhow::{bail, Error, Result}; use cli_log::warn; use indexmap::IndexMap; use matrix_sdk::{ @@ -12,12 +12,36 @@ use matrix_sdk::{ Client, }; +use super::keymappings::key::Keys; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] pub enum State { Normal, Insert, Command, /// Temporary workaround until command based login is working Setup, + /// Only used internally to signal, that we are waiting on further keyinputs, if multiple + /// keymappings have the same prefix + KeyInputPending { + old_state: Box, + pending_keys: Keys, + }, +} + +impl State { + pub fn from_char(c: &char) -> Result { + 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 { @@ -60,6 +84,10 @@ impl fmt::Display for State { Self::Insert => write!(f, "Insert"), Self::Command => write!(f, "Command"), Self::Setup => write!(f, "Setup (!! workaround !!)"), + Self::KeyInputPending { + old_state: _, + pending_keys: _, + } => write!(f, "Key Input Pending"), } } }