From 27d00c564c162562c36e04245ee4e7e20b3a555c Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 20 Sep 2023 19:21:44 +0200 Subject: [PATCH] feat(command_interface): Add support for namespaces --- .../src/generate/command_enum/mod.rs | 106 ++++++++--- .../lua_functions_to_globals/mod.rs | 104 +++++++++-- .../src/generate/lua_wrapper/mod.rs | 14 +- .../lua_wrapper/rust_wrapper_functions/mod.rs | 114 ++++++++---- language_macros/src/generate/mod.rs | 17 +- language_macros/src/lib.rs | 139 ++++++++++++-- src/app/command_interface/command_list/mod.rs | 73 ++++++++ .../lua_command_manager/mod.rs | 15 +- src/app/command_interface/mod.rs | 63 +------ .../event_types/event/handlers/command.rs | 176 +++++++++--------- .../events/event_types/event/handlers/main.rs | 41 +++- 11 files changed, 588 insertions(+), 274 deletions(-) create mode 100644 src/app/command_interface/command_list/mod.rs diff --git a/language_macros/src/generate/command_enum/mod.rs b/language_macros/src/generate/command_enum/mod.rs index 72d5401..fe51685 100644 --- a/language_macros/src/generate/command_enum/mod.rs +++ b/language_macros/src/generate/command_enum/mod.rs @@ -1,50 +1,96 @@ use convert_case::{Case, Casing}; use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::{DeriveInput, Field, Type}; +use quote::{format_ident, quote, ToTokens}; +use syn::{punctuated::Punctuated, Token, Type, Ident}; -use super::{get_input_type_of_bare_fn_field, parse_derive_input_as_named_fields}; +use crate::{DataCommandEnum, Field}; -pub fn command_enum(input: &DeriveInput) -> TokenStream2 { - let named_fields = parse_derive_input_as_named_fields(input); - let fields: TokenStream2 = named_fields - .named - .iter() - .map(|field| turn_struct_fieled_to_enum(field)) - .collect(); +use super::get_input_type_of_bare_fn_field; + +pub fn command_enum(input: &DataCommandEnum) -> TokenStream2 { + let (fields, namespace_enums): (TokenStream2, TokenStream2) = + turn_fields_to_enum(&input.fields); quote! { #[derive(Debug)] pub enum Command { #fields } + #namespace_enums } } -fn turn_struct_fieled_to_enum(field: &Field) -> TokenStream2 { - let field_name = format_ident!( - "{}", - field - .ident - .as_ref() - .expect("These are named fields, it should be Some()") - .to_string() - .from_case(Case::Snake) - .to_case(Case::Pascal) - ); +fn turn_fields_to_enum(fields: &Punctuated) -> (TokenStream2, TokenStream2) { + let output: Vec<_> = fields + .iter() + .map(|field| turn_struct_field_to_enum(field)) + .collect(); - let input_type: Option = get_input_type_of_bare_fn_field(field); + let mut fields_output: TokenStream2 = Default::default(); + let mut namespace_enums_output: TokenStream2 = Default::default(); - match input_type { - Some(input_type) => { - quote! { - #field_name(#input_type), + for (fields, namespace_enum) in output { + fields_output.extend(fields.to_token_stream()); + namespace_enums_output.extend(namespace_enum.to_token_stream()); + } + + (fields_output, namespace_enums_output) +} + +fn turn_struct_field_to_enum(field: &Field) -> (TokenStream2, TokenStream2) { + match field { + Field::Function(fun_field) => { + let field_name = format_ident!( + "{}", + fun_field + .name + .to_string() + .from_case(Case::Snake) + .to_case(Case::Pascal) + ); + + let input_type: Option = get_input_type_of_bare_fn_field(fun_field); + + match input_type { + Some(input_type) => ( + quote! { + #field_name(#input_type), + }, + quote! {}, + ), + None => ( + quote! { + #field_name, + }, + quote! {}, + ), } } - None => { - quote! { - #field_name, - } + Field::Namespace(namespace) => { + let (namespace_output_fields, namespace_output_namespace_enums) = + turn_fields_to_enum(&namespace.fields); + let namespace_name: Ident = format_ident!( + "{}", + namespace.path.iter().map(|name| name.to_string()).collect::() + ); + + let new_namespace_name: Ident = format_ident!( + "{}", + namespace_name.to_string().from_case(Case::Snake).to_case(Case::Pascal) + ); + + ( + quote! { + #new_namespace_name(#new_namespace_name), + }, + quote! { + #[derive(Debug)] + pub enum #new_namespace_name { + #namespace_output_fields + } + #namespace_output_namespace_enums + }, + ) } } } diff --git a/language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs b/language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs index 194fb53..a301e2e 100644 --- a/language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs +++ b/language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs @@ -1,16 +1,35 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; -use syn::{DeriveInput, Field}; +use syn::{punctuated::Punctuated, Token}; -use crate::generate::parse_derive_input_as_named_fields; +use crate::{DataCommandEnum, Field, FunctionDeclaration, NamespacePath}; -pub fn generate_add_lua_functions_to_globals(input: &DeriveInput) -> TokenStream2 { - let named_fields = parse_derive_input_as_named_fields(input); - let function_adders: TokenStream2 = named_fields - .named - .iter() - .map(|field| generate_function_adder(field)) - .collect(); +pub fn generate_add_lua_functions_to_globals(input: &DataCommandEnum) -> TokenStream2 { + fn turn_field_to_functions( + input: &Punctuated, + namespace_path: Option<&NamespacePath>, + ) -> TokenStream2 { + input + .iter() + .map(|field| match field { + crate::Field::Function(function) => { + generate_function_adder(function, namespace_path) + } + crate::Field::Namespace(namespace) => { + let mut passed_namespace = + namespace_path.unwrap_or(&Default::default()).clone(); + namespace + .path + .clone() + .into_iter() + .for_each(|val| passed_namespace.push(val)); + + turn_field_to_functions(&namespace.fields, Some(&passed_namespace)) + } + }) + .collect() + } + let function_adders: TokenStream2 = turn_field_to_functions(&input.fields, None); quote! { pub fn add_lua_functions_to_globals( @@ -28,15 +47,67 @@ pub fn generate_add_lua_functions_to_globals(input: &DeriveInput) -> TokenStream } } -fn generate_function_adder(field: &Field) -> TokenStream2 { - let field_ident = field - .ident - .as_ref() - .expect("This is should be a named field"); +fn generate_function_adder( + field: &FunctionDeclaration, + namespace_path: Option<&NamespacePath>, +) -> TokenStream2 { + let field_ident = &field.name; let function_ident = format_ident!("wrapped_lua_function_{}", field_ident); let function_name = field_ident.to_string(); + let setter = if let Some(namespace_path) = namespace_path { + // ```lua + // local globals = { + // ns1: { + // ns_value, + // ns_value2, + // }, + // ns2: { + // ns_value3, + // } + // } + // ns1.ns_value + // ``` + let mut counter = 0; + let namespace_table_gen: TokenStream2 = namespace_path.iter().map(|path| { + let path = path.to_string(); + counter += 1; + let mut set_function: TokenStream2 = Default::default(); + if counter == namespace_path.len() { + set_function = quote! { + table.set(#function_name, #function_ident).expect( + "Setting a static global value should work" + ); + }; + } + quote! { + let table: mlua::Table = { + if table.contains_key(#path).expect("This check should work") { + let table2 = table.get(#path).expect("This was already checked"); + table2 + } else { + table.set(#path, lua.create_table().expect("This should also always work")).expect("Setting this value should work"); + table.get(#path).expect("This was set, just above") + } + }; + #set_function + } + }).collect(); + + quote! { + let table = &globals; + { + #namespace_table_gen + } + } + } else { + quote! { + globals.set(#function_name, #function_ident).expect( + "Setting a static global value should work" + ); + } + }; quote! { { let #function_ident = lua.create_async_function(#field_ident).expect( @@ -45,10 +116,7 @@ fn generate_function_adder(field: &Field) -> TokenStream2 { #function_name ) ); - - globals.set(#function_name, #function_ident).expect( - "Setting a static global value should work" - ); + #setter } } } diff --git a/language_macros/src/generate/lua_wrapper/mod.rs b/language_macros/src/generate/lua_wrapper/mod.rs index 8672d47..ddbb235 100644 --- a/language_macros/src/generate/lua_wrapper/mod.rs +++ b/language_macros/src/generate/lua_wrapper/mod.rs @@ -1,18 +1,20 @@ use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use syn::DeriveInput; -use crate::generate::lua_wrapper::{ - lua_functions_to_globals::generate_add_lua_functions_to_globals, - rust_wrapper_functions::generate_rust_wrapper_functions, +use crate::{ + generate::lua_wrapper::{ + lua_functions_to_globals::generate_add_lua_functions_to_globals, + rust_wrapper_functions::generate_rust_wrapper_functions, + }, + DataCommandEnum, }; mod lua_functions_to_globals; mod rust_wrapper_functions; -pub fn lua_wrapper(input: &DeriveInput) -> TokenStream2 { +pub fn lua_wrapper(input: &DataCommandEnum) -> TokenStream2 { let add_lua_functions_to_globals = generate_add_lua_functions_to_globals(input); - let rust_wrapper_functions = generate_rust_wrapper_functions(input); + let rust_wrapper_functions = generate_rust_wrapper_functions(None, input); quote! { #add_lua_functions_to_globals #rust_wrapper_functions 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 785b886..57f45c6 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 @@ -1,21 +1,39 @@ use convert_case::{Case, Casing}; use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::{ - punctuated::Punctuated, token::Comma, DeriveInput, Field, GenericArgument, Lifetime, Type, +use quote::quote; +use syn::{punctuated::Punctuated, token::Comma, GenericArgument, Lifetime, Token, Type}; + +use crate::{ + generate::{get_input_type_of_bare_fn_field, get_return_type_of_bare_fn_field}, + DataCommandEnum, Field, FunctionDeclaration, NamespacePath, }; -use crate::generate::{ - get_input_type_of_bare_fn_field, get_return_type_of_bare_fn_field, - parse_derive_input_as_named_fields, -}; +pub fn generate_rust_wrapper_functions( + namespace: Option<&NamespacePath>, + input: &DataCommandEnum, +) -> TokenStream2 { + generate_rust_wrapper_functions_rec(namespace, &input.fields) +} -pub fn generate_rust_wrapper_functions(input: &DeriveInput) -> TokenStream2 { - let named_fields = parse_derive_input_as_named_fields(input); - let wrapped_functions: TokenStream2 = named_fields - .named +pub fn generate_rust_wrapper_functions_rec( + namespace: Option<&NamespacePath>, + input: &Punctuated, +) -> TokenStream2 { + let wrapped_functions: TokenStream2 = input .iter() - .map(|field| wrap_lua_function(field)) + .map(|field| match field { + Field::Function(fun_field) => { + wrap_lua_function(namespace.unwrap_or(&Default::default()), fun_field) + } + Field::Namespace(nasp) => { + let mut passed_namespace = namespace.unwrap_or(&Default::default()).clone(); + nasp.path + .clone() + .into_iter() + .for_each(|val| passed_namespace.push(val)); + generate_rust_wrapper_functions_rec(Some(&passed_namespace), &nasp.fields) + } + }) .collect(); quote! { @@ -23,12 +41,12 @@ pub fn generate_rust_wrapper_functions(input: &DeriveInput) -> TokenStream2 { } } -fn wrap_lua_function(field: &Field) -> TokenStream2 { +fn wrap_lua_function(namespace: &NamespacePath, field: &FunctionDeclaration) -> TokenStream2 { let input_type = get_input_type_of_bare_fn_field(field); let return_type = get_return_type_of_bare_fn_field(field); - let function_name = field.ident.as_ref().expect("This should be a named field"); - let function_body = get_function_body(field, input_type.is_some(), &return_type); + let function_name = &field.name; + let function_body = get_function_body(&namespace, field, input_type.is_some(), &return_type); let lifetime_args = get_and_add_lifetimes_form_inputs_and_outputs(input_type.clone(), return_type); @@ -98,32 +116,62 @@ fn get_and_add_lifetimes_form_inputs_and_outputs<'a>( output } -fn get_function_body(field: &Field, has_input: bool, output_type: &Option) -> TokenStream2 { +fn get_function_body( + namespace: &NamespacePath, + field: &FunctionDeclaration, + has_input: bool, + output_type: &Option, +) -> TokenStream2 { let command_name = field - .ident - .as_ref() - .expect("These are named fields, it should be Some()") + .name .to_string() .from_case(Case::Snake) .to_case(Case::Pascal); - let command_ident = format_ident!("{}", command_name); - let send_output = if has_input { - quote! { - Event::CommandEvent( - Command::#command_ident(input.clone()), - Some(callback_tx), - ) - } - } else { - quote! { - Event::CommandEvent( - Command::#command_ident, - Some(callback_tx), - ) + let command_ident = { + if has_input { + format!("{}(", command_name) + } else { + command_name.clone() } }; + let command_namespace: String = { + namespace + .iter() + .map(|path| { + let path_enum_name: String = path + .to_string() + .from_case(Case::Snake) + .to_case(Case::Pascal); + + path_enum_name.clone() + "(" + &path_enum_name + "::" + }) + .collect::>() + .join("") + }; + + let send_output: TokenStream2 = { + let finishing_brackets = { + if has_input { + let mut output = "input.clone()".to_owned(); + output.push_str(&(0..namespace.len()).map(|_| ')').collect::()); + output + } else { + (0..namespace.len()).map(|_| ')').collect::() + } + }; + + ("Event::CommandEvent( Command::".to_owned() + + &command_namespace + + &command_ident + + &finishing_brackets + + {if has_input {")"} else {""}} /* Needed as command_name opens one */ + + ",Some(callback_tx))") + .parse() + .expect("This code should be valid") + }; + let function_return = if let Some(_) = output_type { quote! { return Ok(output.into_lua(lua).expect("This conversion should always work")); diff --git a/language_macros/src/generate/mod.rs b/language_macros/src/generate/mod.rs index 972bbf8..4bc329c 100644 --- a/language_macros/src/generate/mod.rs +++ b/language_macros/src/generate/mod.rs @@ -3,18 +3,9 @@ mod lua_wrapper; pub use command_enum::command_enum; pub use lua_wrapper::lua_wrapper; -use syn::{DeriveInput, Field, FieldsNamed, ReturnType, Type, TypeBareFn}; +use syn::{ReturnType, Type, TypeBareFn}; -pub fn parse_derive_input_as_named_fields(input: &DeriveInput) -> FieldsNamed { - match &input.data { - syn::Data::Struct(input) => match &input.fields { - syn::Fields::Named(named_fields) => named_fields, - _ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"), - }, - _ => unimplemented!("The macro only works for structs"), - } - .to_owned() -} +use crate::FunctionDeclaration; pub fn get_bare_fn_input_type(function: &TypeBareFn) -> Option { if function.inputs.len() == 1 { @@ -37,7 +28,7 @@ pub fn get_bare_fn_input_type(function: &TypeBareFn) -> Option { } } -pub fn get_input_type_of_bare_fn_field(field: &Field) -> Option { +pub fn get_input_type_of_bare_fn_field(field: &FunctionDeclaration) -> Option { match &field.ty { syn::Type::BareFn(function) => get_bare_fn_input_type(&function), _ => unimplemented!( @@ -46,7 +37,7 @@ pub fn get_input_type_of_bare_fn_field(field: &Field) -> Option { ), } } -pub fn get_return_type_of_bare_fn_field(field: &Field) -> Option { +pub fn get_return_type_of_bare_fn_field(field: &FunctionDeclaration) -> Option { match &field.ty { syn::Type::BareFn(function) => get_bare_fn_return_type(&function), _ => unimplemented!( diff --git a/language_macros/src/lib.rs b/language_macros/src/lib.rs index 93ac287..708cc6f 100644 --- a/language_macros/src/lib.rs +++ b/language_macros/src/lib.rs @@ -1,7 +1,10 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use syn::DeriveInput; +use syn::{ + braced, parse::Parse, parse_macro_input, punctuated::Punctuated, token, Attribute, Ident, + Token, Type, +}; mod generate; @@ -14,19 +17,17 @@ mod generate; /// the rust wrapper functions to the lua globals. /// /// The input and output values of the wrapped functions are derived from the values specified in -/// the `Commands` struct. -/// The returned values will be returned directly to the lua context, this allows to nest functions. +/// the input to the `parse_command_enum` proc macro. /// /// For example this rust code: -/// ```rust -/// #[ci_command_enum] -/// struct Commands { +/// ```no_run +/// parse_command_enum! { /// /// Greets the user /// greet: fn(String) -> String, /// } /// ``` /// results in this expanded code: -/// ```rust +/// ```no_run /// #[derive(Debug)] /// pub enum Command { /// Greet(String), @@ -72,12 +73,118 @@ mod generate; /// }; /// } /// ``` -#[proc_macro_attribute] -pub fn ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream { - // Construct a representation of Rust code as a syntax tree - // that we can manipulate - let input: DeriveInput = syn::parse(input) - .expect("This should always be valid rust code, as it's extracted from direct code"); +#[derive(Debug)] +struct DataCommandEnum { + #[allow(dead_code)] + commands_token: kw::commands, + + #[allow(dead_code)] + brace_token: token::Brace, + + fields: Punctuated, +} + +mod kw { + syn::custom_keyword!(commands); + syn::custom_keyword!(namespace); + syn::custom_keyword!(declare); +} + +#[derive(Debug)] +enum Field { + Function(FunctionDeclaration), + Namespace(Namespace), +} +#[derive(Debug)] +struct Namespace { + #[allow(dead_code)] + namespace_token: kw::namespace, + + path: NamespacePath, + + #[allow(dead_code)] + brace_token: token::Brace, + + fields: Punctuated, +} +type NamespacePath = Punctuated; + +#[derive(Debug)] +struct FunctionDeclaration { + #[allow(dead_code)] + function_token: kw::declare, + + name: Ident, + + #[allow(dead_code)] + colon_token: Token![:], + + ty: Type, +} + +impl Parse for DataCommandEnum { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let content; + Ok(DataCommandEnum { + commands_token: input.parse()?, + brace_token: braced!(content in input), + fields: content.parse_terminated(Field::parse, Token![,])?, + }) + } +} +impl Parse for Field { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let lookahead = input.lookahead1(); + if input.peek(Token![#]) { + // FIXME(@soispha): We ignore doc comments, which should probably be replaced by adding + // them to the output <2023-09-19> + let _output = input.call(Attribute::parse_outer).unwrap_or(vec![]); + let lookahead = input.lookahead1(); + + if lookahead.peek(kw::namespace) { + input.parse().map(Field::Namespace) + } else if lookahead.peek(kw::declare) { + input.parse().map(Field::Function) + } else { + Err(lookahead.error()) + } + } else { + if lookahead.peek(kw::declare) { + input.parse().map(Field::Function) + } else if lookahead.peek(kw::namespace) { + input.parse().map(Field::Namespace) + } else { + Err(lookahead.error()) + } + } + } +} + +impl Parse for FunctionDeclaration { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + Ok(FunctionDeclaration { + function_token: input.parse()?, + name: input.parse()?, + colon_token: input.parse()?, + ty: input.parse()?, + }) + } +} +impl Parse for Namespace { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let content; + Ok(Namespace { + namespace_token: input.parse()?, + path: NamespacePath::parse_separated_nonempty(input)?, + brace_token: braced!(content in input), + fields: content.parse_terminated(Field::parse, Token![,])?, + }) + } +} + +#[proc_macro] +pub fn parse_command_enum(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DataCommandEnum); // Build the language wrappers let lua_wrapper: TokenStream2 = generate::lua_wrapper(&input); @@ -85,9 +192,9 @@ pub fn ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream { // Build the final enum let command_enum = generate::command_enum(&input); - quote! { + let output = quote! { #command_enum #lua_wrapper - } - .into() + }; + output.into() } diff --git a/src/app/command_interface/command_list/mod.rs b/src/app/command_interface/command_list/mod.rs new file mode 100644 index 0000000..fb93c7b --- /dev/null +++ b/src/app/command_interface/command_list/mod.rs @@ -0,0 +1,73 @@ +// Use `cargo expand app::command_interface::command_list` for an overview of the file contents + +use language_macros::parse_command_enum; + +// TODO(@soispha): Should these paths be moved to the proc macro? +// As they are not static, it could be easier for other people, +// if they stay here. +use crate::app::command_interface::command_transfer_value::CommandTransferValue; +use crate::app::Event; +use mlua::IntoLua; + +parse_command_enum! { +commands { + /// Prints to the output, with a newline. + // HACK(@soispha): The stdlib Lua `print()` function has stdout as output hardcoded, + // redirecting stdout seems too much like a hack thus we are just redefining the print function + // to output to a controlled output. <2023-09-09> + declare print: fn(CommandTransferValue), + + namespace trinitrix { + /// Debug only functions, these are effectively useless + namespace debug { + /// Greets the user + declare greet: fn(String) -> String, + + /// Returns a table of greeted users + declare greet_multiple: fn() -> Table, + }, + + /// General API to change stuff in Name + namespace api { + /// Closes the application + declare exit: fn(), + /// Send a message to the current room + /// The send message is interpreted literally. + declare room_message_send: fn(String), + /// Open the help pages at the first occurrence of + /// the input string if it is Some, otherwise open + /// the help pages at the start + declare help: fn(Option), + + /// Function that change the UI, or UI state + namespace ui { + /// Shows the command line + declare command_line_show: fn(), + + /// Hides the command line + declare command_line_hide: fn(), + + /// Go to the next plane + declare cycle_planes: fn(), + /// Go to the previous plane + declare cycle_planes_rev: fn(), + + /// Sets the current app mode to Normal / navigation mode + declare set_mode_normal: fn(), + /// Sets the current app mode to Insert / editing mode + declare set_mode_insert: fn(), + }, + + /// Functions only used internally within Name + 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), + }, + }, + }, +} +} diff --git a/src/app/command_interface/lua_command_manager/mod.rs b/src/app/command_interface/lua_command_manager/mod.rs index de84bbc..520bf5c 100644 --- a/src/app/command_interface/lua_command_manager/mod.rs +++ b/src/app/command_interface/lua_command_manager/mod.rs @@ -11,7 +11,13 @@ use tokio::{ }; use crate::app::{ - command_interface::{add_lua_functions_to_globals, Command}, + command_interface::{ + add_lua_functions_to_globals, + Api::Raw, + Command, + Raw::{DisplayOutput, RaiseError}, + Trinitrix::Api, + }, events::event_types::Event, }; @@ -87,7 +93,10 @@ async fn exec_lua(lua_code: &str, event_call_tx: mpsc::Sender) -> Result< if output != "nil" { event_call_tx - .send(Event::CommandEvent(Command::DisplayOutput(output), None)) + .send(Event::CommandEvent( + Command::Trinitrix(Api(Raw(DisplayOutput(output)))), + None, + )) .await .context("Failed to send lua output command")? } @@ -96,7 +105,7 @@ async fn exec_lua(lua_code: &str, event_call_tx: mpsc::Sender) -> Result< error!("Lua code `{}` returned error: `{}`", lua_code, err); event_call_tx .send(Event::CommandEvent( - Command::RaiseError(err.to_string()), + Command::Trinitrix(Api(Raw(RaiseError(err.to_string())))), None, )) .await?; diff --git a/src/app/command_interface/mod.rs b/src/app/command_interface/mod.rs index de9daf9..c359734 100644 --- a/src/app/command_interface/mod.rs +++ b/src/app/command_interface/mod.rs @@ -1,64 +1,5 @@ -// Use `cargo expand app::command_interface` for an overview of the file contents - pub mod command_transfer_value; pub mod lua_command_manager; +pub mod command_list; -use language_macros::ci_command_enum; - -// TODO(@Soispha): Should these paths be moved to the proc macro? -// As they are not static, it could be easier for other people, -// if they stay here. -use crate::app::command_interface::command_transfer_value::CommandTransferValue; -use crate::app::Event; -use mlua::IntoLua; - -#[ci_command_enum] -struct Commands { - /// Prints to the output, with a newline. - // HACK(@soispha): The stdlib Lua `print()` function has stdout as output hardcoded, - // redirecting stdout seems too much like a hack thus we are just redefining the print function - // to output to a controlled output. <2023-09-09> - print: fn(CommandTransferValue), - - // Begin debug functions - /// Greets the user - greet: fn(String) -> String, - - /// Returns a table of greeted users - greet_multiple: fn() -> Table, - // End debug functions - /// Closes the application - exit: fn(), - - /// Shows the command line - command_line_show: fn(), - - /// Hides the command line - command_line_hide: fn(), - - /// Go to the next plane - cycle_planes: fn(), - /// Go to the previous plane - cycle_planes_rev: fn(), - - /// Sets the current app mode to Normal / navigation mode - set_mode_normal: fn(), - /// Sets the current app mode to Insert / editing mode - set_mode_insert: fn(), - - /// Send a message to the current room - /// The send message is interpreted literally. - room_message_send: fn(String), - - /// Open the help pages at the first occurrence of - /// the input string if it is Some, otherwise open - /// the help pages at the start - help: fn(Option), - - /// Send an error to the default error output - raise_error: fn(String), - /// Send output to the default output - /// This is mainly used to display the final - /// output of evaluated lua commands. - display_output: fn(String), -} +pub use command_list::*; diff --git a/src/app/events/event_types/event/handlers/command.rs b/src/app/events/event_types/event/handlers/command.rs index 562533d..4b655ad 100644 --- a/src/app/events/event_types/event/handlers/command.rs +++ b/src/app/events/event_types/event/handlers/command.rs @@ -8,7 +8,7 @@ use crate::{ app::{ command_interface::{ command_transfer_value::{CommandTransferValue, Table}, - Command, + Api, Command, Debug, Raw, Trinitrix, Ui, }, events::event_types::EventStatus, status::State, @@ -66,96 +66,102 @@ pub async fn handle( trace!("Handling command: {:#?}", command); Ok(match command { - Command::Exit => { - send_status_output!("Terminating the application.."); - EventStatus::Terminate - } - - Command::DisplayOutput(output) => { - // TODO(@Soispha): This is only used to show the Lua command output to the user. - // Lua commands already receive the output. This should probably be communicated - // better, should it? - send_status_output!(output); - EventStatus::Ok - } Command::Print(output) => { let output_str: String = output.to_string(); send_status_output!(output_str); EventStatus::Ok } - Command::CommandLineShow => { - app.ui.cli_enable(); - app.status.set_state(State::Command); - send_status_output!("CLI online"); - EventStatus::Ok - } - Command::CommandLineHide => { - app.ui.cli_disable(); - send_status_output!("CLI offline"); - EventStatus::Ok - } + Command::Trinitrix(trinitrix) => match trinitrix { + Trinitrix::Debug(debug) => match debug { + Debug::Greet(msg) => { + send_main_output!("Greeting, {}!", msg); + EventStatus::Ok + } + Debug::GreetMultiple => { + let mut table: Table = HashMap::new(); + table.insert("UserId".to_owned(), CommandTransferValue::Integer(2)); + table.insert( + "UserName".to_owned(), + CommandTransferValue::String("James".to_owned()), + ); - Command::CyclePlanes => { - app.ui.cycle_main_input_position(); - send_status_output!("Switched main input position"); - EventStatus::Ok - } - Command::CyclePlanesRev => { - app.ui.cycle_main_input_position_rev(); - send_status_output!("Switched main input position; reversed"); - EventStatus::Ok - } - - Command::SetModeNormal => { - app.status.set_state(State::Normal); - send_status_output!("Set input mode to Normal"); - EventStatus::Ok - } - Command::SetModeInsert => { - app.status.set_state(State::Insert); - app.ui.set_input_position(InputPosition::MessageCompose); - send_status_output!("Set input mode to Insert"); - EventStatus::Ok - } - - Command::RoomMessageSend(msg) => { - if let Some(room) = app.status.room_mut() { - room.send(msg.clone()).await?; - send_status_output!("Sent message: `{}`", msg); - } else { - // TODO(@Soispha): Should this raise a Lua error? It could be very confusing, - // when a user doesn't read the log. - warn!("Can't send message: `{}`, as there is no open room!", &msg); - } - EventStatus::Ok - } - Command::Greet(name) => { - send_main_output!("Hi, {}!", name); - EventStatus::Ok - } - Command::GreetMultiple => { - let mut table: Table = HashMap::new(); - table.insert("UserId".to_owned(), CommandTransferValue::Integer(2)); - table.insert( - "UserName".to_owned(), - CommandTransferValue::String("James".to_owned()), - ); - - let mut second_table: Table = HashMap::new(); - second_table.insert("interface".to_owned(), CommandTransferValue::Integer(3)); - second_table.insert("api".to_owned(), CommandTransferValue::Boolean(true)); - table.insert( - "Versions".to_owned(), - CommandTransferValue::Table(second_table), - ); - send_main_output!(table); - EventStatus::Ok - } - Command::Help(_) => todo!(), - Command::RaiseError(err) => { - send_error_output!(err); - EventStatus::Ok - } + let mut second_table: Table = HashMap::new(); + second_table.insert("interface".to_owned(), CommandTransferValue::Integer(3)); + second_table.insert("api".to_owned(), CommandTransferValue::Boolean(true)); + table.insert( + "Versions".to_owned(), + CommandTransferValue::Table(second_table), + ); + send_main_output!(table); + EventStatus::Ok + } + }, + Trinitrix::Api(api) => match api { + Api::Exit => { + send_status_output!("Terminating the application.."); + EventStatus::Terminate + } + Api::RoomMessageSend(msg) => { + if let Some(room) = app.status.room_mut() { + room.send(msg.clone()).await?; + send_status_output!("Sent message: `{}`", msg); + } else { + // // 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); + } + EventStatus::Ok + } + Api::Help(_) => todo!(), + Api::Ui(ui) => match ui { + Ui::CommandLineShow => { + app.ui.cli_enable(); + app.status.set_state(State::Command); + send_status_output!("CLI online"); + EventStatus::Ok + } + Ui::CommandLineHide => { + app.ui.cli_disable(); + send_status_output!("CLI offline"); + EventStatus::Ok + } + Ui::CyclePlanes => { + app.ui.cycle_main_input_position(); + send_status_output!("Switched main input position"); + EventStatus::Ok + } + Ui::CyclePlanesRev => { + app.ui.cycle_main_input_position_rev(); + send_status_output!("Switched main input position; reversed"); + EventStatus::Ok + } + Ui::SetModeNormal => { + app.status.set_state(State::Normal); + send_status_output!("Set input mode to Normal"); + EventStatus::Ok + } + Ui::SetModeInsert => { + app.status.set_state(State::Insert); + app.ui.set_input_position(InputPosition::MessageCompose); + send_status_output!("Set input mode to Insert"); + EventStatus::Ok + } + }, + Api::Raw(raw) => match raw { + Raw::RaiseError(err) => { + send_error_output!(err); + EventStatus::Ok + } + Raw::DisplayOutput(output) => { + // TODO(@Soispha): This is only used to show the Lua command output to the user. + // Lua commands already receive the output. This should probably be communicated + // better, should it? + send_status_output!(output); + EventStatus::Ok + } + }, + }, + }, }) } diff --git a/src/app/events/event_types/event/handlers/main.rs b/src/app/events/event_types/event/handlers/main.rs index 8822502..64bb632 100644 --- a/src/app/events/event_types/event/handlers/main.rs +++ b/src/app/events/event_types/event/handlers/main.rs @@ -3,7 +3,7 @@ use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers} use crate::{ app::{ - command_interface::Command, + command_interface::{Api, Command, Trinitrix, Ui}, events::event_types::{Event, EventStatus}, App, }, @@ -20,7 +20,10 @@ pub async fn handle_command( code: KeyCode::Esc, .. }) => { app.tx - .send(Event::CommandEvent(Command::SetModeNormal, None)) + .send(Event::CommandEvent( + Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::SetModeNormal))), + None, + )) .await?; } CrosstermEvent::Key(KeyEvent { @@ -62,14 +65,20 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R .. }) => { app.tx - .send(Event::CommandEvent(Command::Exit, None)) + .send(Event::CommandEvent( + Command::Trinitrix(Trinitrix::Api(Api::Exit)), + None, + )) .await?; } CrosstermEvent::Key(KeyEvent { code: KeyCode::Tab, .. }) => { app.tx - .send(Event::CommandEvent(Command::CyclePlanes, None)) + .send(Event::CommandEvent( + Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::CyclePlanes))), + None, + )) .await?; } CrosstermEvent::Key(KeyEvent { @@ -77,7 +86,10 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R .. }) => { app.tx - .send(Event::CommandEvent(Command::CyclePlanesRev, None)) + .send(Event::CommandEvent( + Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::CyclePlanesRev))), + None, + )) .await?; } CrosstermEvent::Key(KeyEvent { @@ -85,7 +97,10 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R .. }) => { app.tx - .send(Event::CommandEvent(Command::CommandLineShow, None)) + .send(Event::CommandEvent( + Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::CommandLineShow))), + None, + )) .await?; } CrosstermEvent::Key(KeyEvent { @@ -93,7 +108,10 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R .. }) => { app.tx - .send(Event::CommandEvent(Command::SetModeInsert, None)) + .send(Event::CommandEvent( + Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::SetModeInsert))), + None, + )) .await?; } input => match app.ui.input_position() { @@ -193,7 +211,10 @@ pub async fn handle_insert(app: &mut App<'_>, event: &CrosstermEvent) -> Result< code: KeyCode::Esc, .. }) => { app.tx - .send(Event::CommandEvent(Command::SetModeNormal, None)) + .send(Event::CommandEvent( + Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::SetModeNormal))), + None, + )) .await?; } CrosstermEvent::Key(KeyEvent { @@ -203,7 +224,9 @@ pub async fn handle_insert(app: &mut App<'_>, event: &CrosstermEvent) -> Result< }) => { app.tx .send(Event::CommandEvent( - Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")), + Command::Trinitrix(Trinitrix::Api(Api::RoomMessageSend( + app.ui.message_compose.lines().join("\n"), + ))), None, )) .await?;