diff --git a/Cargo.lock b/Cargo.lock index 81473cd..cd1de1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.72" @@ -388,6 +437,47 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + [[package]] name = "cli-log" version = "2.0.0" @@ -409,6 +499,12 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "const-oid" version = "0.7.1" @@ -715,6 +811,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.1" @@ -1006,6 +1111,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.2" @@ -1203,6 +1314,17 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1245,6 +1367,16 @@ dependencies = [ "serde", ] +[[package]] +name = "language_macros" +version = "0.1.0" +dependencies = [ + "convert_case", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1288,16 +1420,6 @@ dependencies = [ "hashbrown 0.12.3", ] -[[package]] -name = "lua_macros" -version = "0.1.0" -dependencies = [ - "convert_case", - "proc-macro2 1.0.66", - "quote 1.0.32", - "syn 2.0.27", -] - [[package]] name = "maplit" version = "1.0.2" @@ -1533,6 +1655,7 @@ checksum = "07366ed2cd22a3b000aed076e2b68896fb46f06f1f5786c5962da73c0af01577" dependencies = [ "bstr", "cc", + "erased-serde", "futures-core", "futures-task", "futures-util", @@ -1540,6 +1663,7 @@ dependencies = [ "once_cell", "pkg-config", "rustc-hash", + "serde", ] [[package]] @@ -2690,12 +2814,14 @@ name = "trinitrix" version = "0.1.0" dependencies = [ "anyhow", + "clap", "cli-log", "crossterm", "indexmap 2.0.0", - "lua_macros", + "language_macros", "matrix-sdk", "mlua", + "once_cell", "serde", "tokio", "tokio-util", @@ -2798,6 +2924,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index 24dea8e..af8e29a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,19 +6,29 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["tui"] +tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"] + [dependencies] -lua_macros = { path = "./lua_macros" } -tui = "0.19" -tui-textarea = { version = "0.2", features = ["crossterm"] } -crossterm = "0.25" -matrix-sdk = "0.6" -anyhow = "1.0" -tokio = { version = "1.29", features = ["macros", "rt-multi-thread"] } -tokio-util = "0.7" -serde = "1.0" +clap = { version = "4.3.19", features = ["derive"] } cli-log = "2.0" -indexmap = "2.0.0" -mlua = { version = "0.8.9", features = ["lua54", "async", "send"] } +anyhow = "1.0" +matrix-sdk = "0.6" +tokio = { version = "1.29", features = ["macros", "rt-multi-thread"] } + +# lua stuff +language_macros = { path = "./language_macros" } +mlua = { version = "0.8.9", features = ["lua54", "async", "send", "serialize"] } +once_cell = "1.18.0" + +# tui feature specific parts +tui = {version = "0.19", optional = true} +tui-textarea = { version = "0.2", features = ["crossterm"], optional = true } +crossterm = { version = "0.25", optional = true } +tokio-util = { version = "0.7", optional = true } +serde = { version = "1.0", optional = true } +indexmap = { version = "2.0.0", optional = true } [profile.release] lto = true diff --git a/lua_macros/.gitignore b/language_macros/.gitignore similarity index 100% rename from lua_macros/.gitignore rename to language_macros/.gitignore diff --git a/lua_macros/Cargo.toml b/language_macros/Cargo.toml similarity index 90% rename from lua_macros/Cargo.toml rename to language_macros/Cargo.toml index cb754e2..89d759e 100644 --- a/lua_macros/Cargo.toml +++ b/language_macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "lua_macros" +name = "language_macros" version = "0.1.0" edition = "2021" diff --git a/language_macros/src/generate/command_enum/mod.rs b/language_macros/src/generate/command_enum/mod.rs new file mode 100644 index 0000000..72d5401 --- /dev/null +++ b/language_macros/src/generate/command_enum/mod.rs @@ -0,0 +1,50 @@ +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{DeriveInput, Field, Type}; + +use super::{get_input_type_of_bare_fn_field, parse_derive_input_as_named_fields}; + +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(); + + quote! { + #[derive(Debug)] + pub enum Command { + #fields + } + } +} + +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) + ); + + let input_type: Option = get_input_type_of_bare_fn_field(field); + + match input_type { + Some(input_type) => { + quote! { + #field_name(#input_type), + } + } + None => { + quote! { + #field_name, + } + } + } +} 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 new file mode 100644 index 0000000..194fb53 --- /dev/null +++ b/language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs @@ -0,0 +1,54 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{DeriveInput, Field}; + +use crate::generate::parse_derive_input_as_named_fields; + +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(); + + quote! { + pub fn add_lua_functions_to_globals( + lua: mlua::Lua, + tx: tokio::sync::mpsc::Sender, + ) -> mlua::Lua { + lua.set_app_data(tx); + let globals = lua.globals(); + + #function_adders + + drop(globals); + lua + } + } +} + +fn generate_function_adder(field: &Field) -> TokenStream2 { + let field_ident = field + .ident + .as_ref() + .expect("This is should be a named field"); + + let function_ident = format_ident!("wrapped_lua_function_{}", field_ident); + let function_name = field_ident.to_string(); + + quote! { + { + let #function_ident = lua.create_async_function(#field_ident).expect( + &format!( + "The function: `{}` should be defined", + #function_name + ) + ); + + globals.set(#function_name, #function_ident).expect( + "Setting a static global value should work" + ); + } + } +} diff --git a/language_macros/src/generate/lua_wrapper/mod.rs b/language_macros/src/generate/lua_wrapper/mod.rs new file mode 100644 index 0000000..8672d47 --- /dev/null +++ b/language_macros/src/generate/lua_wrapper/mod.rs @@ -0,0 +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, +}; + +mod lua_functions_to_globals; +mod rust_wrapper_functions; + +pub fn lua_wrapper(input: &DeriveInput) -> TokenStream2 { + let add_lua_functions_to_globals = generate_add_lua_functions_to_globals(input); + let rust_wrapper_functions = generate_rust_wrapper_functions(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 new file mode 100644 index 0000000..2557a5f --- /dev/null +++ b/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs @@ -0,0 +1,235 @@ +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 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(input: &DeriveInput) -> TokenStream2 { + let named_fields = parse_derive_input_as_named_fields(input); + let wrapped_functions: TokenStream2 = named_fields + .named + .iter() + .map(|field| wrap_lua_function(field)) + .collect(); + + quote! { + #wrapped_functions + } +} + +fn wrap_lua_function(field: &Field) -> 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 lifetime_args = + get_and_add_lifetimes_form_inputs_and_outputs(input_type.clone(), return_type); + + let input_type = input_type + .unwrap_or(syn::parse(quote! {()}.into()).expect("This is static, it always works")); + + quote! { + async fn #function_name <#lifetime_args>( + lua: &mlua::Lua, + input: #input_type + ) -> Result { + #function_body + } + } +} + +fn get_and_add_lifetimes_form_inputs_and_outputs<'a>( + input_type: Option, + return_type: Option, +) -> Punctuated { + fn get_lifetime_args_from_type<'a>(return_type: syn::Type) -> Option> { + match return_type { + syn::Type::Path(path) => { + let args_to_final_path_segment = &path + .path + .segments + .last() + .expect("The path should have a last segment") + .arguments; + match args_to_final_path_segment { + syn::PathArguments::None => + /* We ignore this case */ + { + None + } + syn::PathArguments::AngleBracketed(angle) => { + let lifetime_args: Vec<_> = angle + .args + .iter() + .filter_map(|arg| { + if let GenericArgument::Lifetime(lifetime) = arg { + Some(lifetime.to_owned()) + } else { + None + } + }) + .collect(); + return Some(lifetime_args); + } + syn::PathArguments::Parenthesized(_) => todo!(), + } + } + _ => todo!(), + } + } + + let mut output: Punctuated = Punctuated::new(); + if let Some(input_type) = input_type { + let lifetime_args = get_lifetime_args_from_type(input_type).unwrap_or(vec![]); + lifetime_args.into_iter().for_each(|arg| output.push(arg)); + } + if let Some(return_type) = return_type { + let lifetime_args = get_lifetime_args_from_type(return_type).unwrap_or(vec![]); + lifetime_args.into_iter().for_each(|arg| output.push(arg)); + } + output +} + +fn get_function_body(field: &Field, has_input: bool, output_type: &Option) -> TokenStream2 { + let command_name = field + .ident + .as_ref() + .expect("These are named fields, it should be Some()") + .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 function_return = if let Some(_) = output_type { + quote! { + let converted_output = lua + .to_value(&output) + .expect("This conversion should (indirectely) be checked at compile time"); + if let mlua::Value::Table(table) = converted_output { + let real_output: mlua::Value = match output { + CommandTransferValue::Nil => table + .get("Nil") + .expect("This should exist"), + CommandTransferValue::Boolean(_) => table + .get("Boolean") + .expect("This should exist"), + CommandTransferValue::Integer(_) => table + .get("Integer") + .expect("This should exist"), + CommandTransferValue::Number(_) => table + .get("Number") + .expect("This should exist"), + CommandTransferValue::String(_) => table + .get("String") + .expect("This should exist"), + CommandTransferValue::Table(_) => { + todo!() + // FIXME(@Soispha): This returns a table with the values wrapped the + // same way the values above are wrapped. That is (from the greet_multiple + // function): + // ```json + // { + // "Table": { + // "UserName1": { + // "Integer": 2 + // } + // } + // } + // ``` + // whilst the output should be: + // ```json + // { + // "UserName1": 2 + // } + // ``` + // That table would need to be unpacked, but this requires some recursive + // function, which seems not very performance oriented. + // + // My first (quick) attempt: + //let mut output_table = lua.create_table().expect("This should work?"); + //let initial_table: mlua::Value = table + // .get("Table") + // .expect("This should exist"); + //while let mlua::Value::Table(table) = initial_table { + // for pair in table.pairs() { + // let (key, value) = pair.expect("This should also work?"); + // output_table.set(key, value); + // } + //} + }, + }; + return Ok(real_output); + } else { + unreachable!("Lua serializes these things always in a table"); + } + + } + } else { + quote! { + return Ok(mlua::Value::Nil); + } + }; + let does_function_expect_output = if output_type.is_some() { + quote! { + // We didn't receive output but expected output. Raise an error to notify the lua code + // about it + return Err(mlua::Error::ExternalError(std::sync::Arc::new( + err + ))); + } + } else { + quote! { + // We didn't receive output and didn't expect output. Everything went well! + return Ok(mlua::Value::Nil); + } + }; + + quote! { + let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::(); + let tx: core::cell::Ref> = + lua.app_data_ref().expect("This should exist, it was set before"); + + (*tx) + .send(#send_output) + .await + .expect("This should work, as the receiver is not dropped"); + + cli_log::info!("Sent CommandEvent: `{}`", #command_name); + + match callback_rx.await { + Ok(output) => { + cli_log::info!( + "Lua function: `{}` returned output to lua: `{}`", #command_name, &output + ); + #function_return + }, + Err(err) => { + #does_function_expect_output + } + }; + } +} diff --git a/language_macros/src/generate/mod.rs b/language_macros/src/generate/mod.rs new file mode 100644 index 0000000..972bbf8 --- /dev/null +++ b/language_macros/src/generate/mod.rs @@ -0,0 +1,65 @@ +mod command_enum; +mod lua_wrapper; + +pub use command_enum::command_enum; +pub use lua_wrapper::lua_wrapper; +use syn::{DeriveInput, Field, FieldsNamed, 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() +} + +pub fn get_bare_fn_input_type(function: &TypeBareFn) -> Option { + if function.inputs.len() == 1 { + Some( + function + .inputs + .first() + .expect("Only one element exists, we checked the length above") + .ty + .clone(), + ) + } else if function.inputs.len() == 0 { + // No inputs, so we can't return a type + None + } else { + unreachable!( + "The Function can only take one or zero arguments. + Use a tuple `(arg1, arg2)` if you want more" + ); + } +} + +pub fn get_input_type_of_bare_fn_field(field: &Field) -> Option { + match &field.ty { + syn::Type::BareFn(function) => get_bare_fn_input_type(&function), + _ => unimplemented!( + "Please specify the type as a bare fn type. + That is: `fn() -> `" + ), + } +} +pub fn get_return_type_of_bare_fn_field(field: &Field) -> Option { + match &field.ty { + syn::Type::BareFn(function) => get_bare_fn_return_type(&function), + _ => unimplemented!( + "Please specify the type as a bare fn type. + That is: `fn() -> `" + ), + } +} + +pub fn get_bare_fn_return_type(function: &TypeBareFn) -> Option { + let return_path: &ReturnType = &function.output; + match return_path { + ReturnType::Default => None, + ReturnType::Type(_, return_type) => Some(*return_type.to_owned()), + } +} diff --git a/language_macros/src/lib.rs b/language_macros/src/lib.rs new file mode 100644 index 0000000..93ac287 --- /dev/null +++ b/language_macros/src/lib.rs @@ -0,0 +1,93 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::DeriveInput; + +mod generate; + +/// This is the heart of the command api +/// It mainly does two things: +/// - Generate a command enum +/// - Wrap the enum in all supported languages (only lua for now) +/// - Generate wrapper lua function for each command +/// - Generate a `add_lua_functions_to_globals` function, which adds +/// 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. +/// +/// For example this rust code: +/// ```rust +/// #[ci_command_enum] +/// struct Commands { +/// /// Greets the user +/// greet: fn(String) -> String, +/// } +/// ``` +/// results in this expanded code: +/// ```rust +/// #[derive(Debug)] +/// pub enum Command { +/// Greet(String), +/// } +/// pub fn add_lua_functions_to_globals( +/// lua: mlua::Lua, +/// tx: tokio::sync::mpsc::Sender, +/// ) -> mlua::Lua { +/// lua.set_app_data(tx); +/// let globals = lua.globals(); +/// { +/// let wrapped_lua_function_greet = lua +/// .create_async_function(greet) +/// .expect( +/// format!( +/// "The function: `{}` should be defined", +/// "greet", +/// ) +/// ); +/// globals +/// .set("greet", wrapped_lua_function_greet) +/// .expect("Setting a static global value should work"); +/// } +/// drop(globals); +/// lua +/// } +/// async fn greet(lua: &mlua::Lua, input: String) -> Result { +/// let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::(); +/// let tx: core::cell::Ref> = lua +/// .app_data_ref() +/// .expect("This should exist, it was set before"); +/// (*tx) +/// .send(Event::CommandEvent(Command::Greet(input.clone()), Some(callback_tx))) +/// .await +/// .expect("This should work, as the receiver is not dropped"); +/// match callback_rx.await { +/// Ok(output) => { +/// return Ok(output); +/// } +/// Err(err) => { +/// return Err(mlua::Error::ExternalError(std::sync::Arc::new(err))); +/// } +/// }; +/// } +/// ``` +#[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"); + + // Build the language wrappers + let lua_wrapper: TokenStream2 = generate::lua_wrapper(&input); + + // Build the final enum + let command_enum = generate::command_enum(&input); + + quote! { + #command_enum + #lua_wrapper + } + .into() +} diff --git a/lua_macros/src/lib.rs b/lua_macros/src/lib.rs deleted file mode 100644 index 871dd3f..0000000 --- a/lua_macros/src/lib.rs +++ /dev/null @@ -1,69 +0,0 @@ -mod mark_as_ci_command; -mod struct_to_ci_enum; - -use mark_as_ci_command::generate_final_function; -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use struct_to_ci_enum::{generate_command_enum, generate_generate_ci_function, generate_help_function}; -use syn::{self, parse_quote, parse_str, DeriveInput, FieldMutability, ItemFn, Token, Visibility}; - -#[proc_macro_attribute] -pub fn turn_struct_to_ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream { - // Construct a representation of Rust code as a syntax tree - // that we can manipulate - let mut input: DeriveInput = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code"); - - let mut named_fields = 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(); - - let attr_parsed = parse_quote! { - /// This is a help function - }; - - named_fields.named.push(syn::Field { - attrs: vec![attr_parsed], - // attrs: attr_parser - // .parse("#[doc = r\"This is a help function\"]".to_token_stream().into()) - // .expect("See reason for other one"), - vis: Visibility::Inherited, - mutability: FieldMutability::None, - ident: Some(format_ident!("help")), - colon_token: Some(Token![:](Span::call_site())), - ty: parse_str("fn(Option) -> String").expect("This is static and valid rust code"), - }); - - match &mut input.data { - syn::Data::Struct(input) => input.fields = syn::Fields::Named(named_fields.clone()), - _ => unreachable!("This was a DataStruct before"), - }; - - // Build the trait implementation - let generate_ci_function: TokenStream2 = generate_generate_ci_function(&input); - - let command_enum = generate_command_enum(&named_fields); - - let help_function = generate_help_function(&named_fields); - - quote! { - #command_enum - - #generate_ci_function - - //#help_function - } - .into() -} - -#[proc_macro_attribute] -pub fn ci_command(_attrs: TokenStream, input: TokenStream) -> TokenStream { - let mut input: ItemFn = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code"); - let output_function = generate_final_function(&mut input); - output_function.into() -} diff --git a/lua_macros/src/mark_as_ci_command.rs b/lua_macros/src/mark_as_ci_command.rs deleted file mode 100644 index 76e2fc1..0000000 --- a/lua_macros/src/mark_as_ci_command.rs +++ /dev/null @@ -1,173 +0,0 @@ -use convert_case::{Case, Casing}; -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote, ToTokens}; -use syn::{Block, Expr, ExprBlock, GenericArgument, ReturnType, Stmt, Type}; - -pub fn generate_final_function(input: &mut syn::ItemFn) -> TokenStream2 { - append_tx_send_code(input); - - let output: TokenStream2 = syn::parse(input.into_token_stream().into()) - .expect("This is generated from valid rust code, it should stay that way."); - - output -} - -fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn { - let function_name_pascal = format_ident!( - "{}", - input - .sig - .ident - .clone() - .to_string() - .from_case(Case::Snake) - .to_case(Case::Pascal) - ); - - let tx_send = match &input.sig.output { - syn::ReturnType::Default => { - unreachable!("All functions should have a output of (Result<$type, rlua::Error>)"); - } - syn::ReturnType::Type(_, ret_type) => { - let return_type = match *(ret_type.clone()) { - syn::Type::Path(path) => { - match path - .path - .segments - .first() - .expect("This is expected to be only one path segment") - .arguments - .to_owned() - { - syn::PathArguments::AngleBracketed(angled_path) => { - let angled_path = angled_path.args.to_owned(); - let filtered_paths: Vec<_> = angled_path - .into_iter() - .filter(|generic_arg| { - if let GenericArgument::Type(generic_type) = generic_arg { - if let Type::Path(_) = generic_type { - true - } else { - false - } - } else { - false - } - }) - .collect(); - - // There should only be two segments (the type is ) - if filtered_paths.len() > 2 { - unreachable!("There should be no more than two filtered_output, but got: {:#?}", filtered_paths) - } else if filtered_paths.len() <= 0 { - unreachable!("There should be more than zero filtered_output, but got: {:#?}", filtered_paths) - } - - if filtered_paths.len() == 2 { - // There is something else than mlua::Error - let gen_type = if let GenericArgument::Type(ret_type) = - filtered_paths - .first() - .expect("One path segment should exists") - .to_owned() - { - ret_type - } else { - unreachable!("These were filtered above."); - }; - let return_type_as_type_prepared = quote! {-> #gen_type}; - - let return_type_as_return_type: ReturnType = syn::parse( - return_type_as_type_prepared.to_token_stream().into(), - ) - .expect("This is valid."); - return_type_as_return_type - } else { - // There is only mlua::Error left - ReturnType::Default - } - } - _ => unimplemented!("Only for angled paths"), - } - } - _ => unimplemented!("Only for path types"), - }; - - let send_data = match return_type { - ReturnType::Default => { - quote! { - { - Event::CommandEvent(Command::#function_name_pascal, None) - } - } - } - ReturnType::Type(_, _) => { - quote! { - { - Event::CommandEvent(Command::#function_name_pascal(input.clone()), Some(callback_tx)) - } - } - } - }; - - let output_return = match return_type { - ReturnType::Default => { - quote! { - { - return Ok(()); - } - } - } - ReturnType::Type(_, _) => { - quote! { - { - if let Some(output) = callback_rx.recv().await { - callback_rx.close(); - return Ok(output); - } else { - return Err(mlua::Error::ExternalError(Arc::new(Error::new( - ErrorKind::Other, - "Callback reciever dropped", - )))); - } - } - } - } - }; - - quote! { - { - let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::(256); - - let tx: - core::cell::Ref> = - lua - .app_data_ref() - .expect("This exists, it was set before"); - - (*tx) - .try_send(#send_data) - .expect("This should work, as the reciever is not dropped"); - - #output_return - } - } - } - }; - - let tx_send_block: Block = - syn::parse(tx_send.into()).expect("This is a static string, it will always parse"); - - let tx_send_expr_block = ExprBlock { - attrs: vec![], - label: None, - block: tx_send_block, - }; - let mut tx_send_stmt = vec![Stmt::Expr(Expr::Block(tx_send_expr_block), None)]; - - let mut new_stmts: Vec = Vec::with_capacity(input.block.stmts.len() + 1); - new_stmts.append(&mut tx_send_stmt); - new_stmts.append(&mut input.block.stmts); - input.block.stmts = new_stmts; - input -} diff --git a/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs b/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs deleted file mode 100644 index 8e5fd7e..0000000 --- a/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs +++ /dev/null @@ -1,65 +0,0 @@ -use convert_case::{Case, Casing}; -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::Type; - -pub fn generate_command_enum(input: &syn::FieldsNamed) -> TokenStream2 { - let input_tokens: TokenStream2 = input - .named - .iter() - .map(|field| -> TokenStream2 { - let field_ident = field - .ident - .as_ref() - .expect("These are only the named fields, thus they should all have a ident."); - - let enum_variant_type = match &field.ty { - syn::Type::BareFn(function) => { - let return_path = &function.inputs; - - let input_type: Option = if return_path.len() == 1 { - Some( - return_path - .last() - .expect("The last element exists") - .ty - .clone(), - ) - } else if return_path.len() == 0 { - None - } else { - panic!("The Function can only take on argument, or none"); - }; - input_type - } - _ => unimplemented!("This is only implemented for bare function types"), - }; - - let enum_variant_name = format_ident!( - "{}", - field_ident - .to_string() - .from_case(Case::Snake) - .to_case(Case::Pascal) - ); - if enum_variant_type.is_some() { - quote! { - #enum_variant_name (#enum_variant_type), - } - .into() - } else { - quote! { - #enum_variant_name, - } - } - }) - .collect(); - - let gen = quote! { - #[derive(Debug)] - pub enum Command { - #input_tokens - } - }; - gen.into() -} diff --git a/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs b/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs deleted file mode 100644 index 3c690ec..0000000 --- a/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs +++ /dev/null @@ -1,105 +0,0 @@ -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::{parse_quote, ReturnType, Type}; - -fn generate_ci_function_exposure(field: &syn::Field) -> TokenStream2 { - let field_ident = field - .ident - .as_ref() - .expect("These are only the named field, thus they all should have a name."); - - let function_name_ident = format_ident!("fun_{}", field_ident); - let function_name = format!("{}", field_ident); - quote! { - let #function_name_ident = lua.create_async_function(#field_ident).expect( - &format!( - "The function: `{}` should be defined", - #function_name - ) - ); - - globals.set(#function_name, #function_name_ident).expect( - &format!( - "Setting a static global value ({}, fun_{}) should work", - #function_name, - #function_name - ) - ); - } - .into() -} - -pub fn generate_generate_ci_function(input: &syn::DeriveInput) -> TokenStream2 { - let mut functions_to_generate: Vec = vec![]; - - let functions_to_export_in_lua: TokenStream2 = match &input.data { - syn::Data::Struct(input) => match &input.fields { - syn::Fields::Named(named_fields) => named_fields - .named - .iter() - .map(|field| -> TokenStream2 { - let input_type = match &field.ty { - syn::Type::BareFn(bare_fn) => { - if bare_fn.inputs.len() == 1 { - bare_fn.inputs.last().expect("The last element exists").ty.clone() - } else if bare_fn.inputs.len() == 0 { - let input_type: Type = parse_quote! {()}; - input_type - } else { - panic!("The Function can only take on argument, or none"); - } - } - _ => unimplemented!("This is only implemented for bare function types"), - }; - let return_type = match &field.ty { - syn::Type::BareFn(function) => { - let return_path: &ReturnType = &function.output; - match return_path { - ReturnType::Default => None, - ReturnType::Type(_, return_type) => Some(return_type.to_owned()) } - } - _ => unimplemented!("This is only implemented for bare function types"), - }; - - let function_name = field - .ident - .as_ref() - .expect("These are only the named field, thus they all should have a name."); - - if let Some(ret_type) = return_type { - functions_to_generate.push(quote! { - #[ci_command] - async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<#ret_type, mlua::Error> { - } - }); - } else { - functions_to_generate.push(quote! { - #[ci_command] - async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<(), mlua::Error> { - } - }); - } - generate_ci_function_exposure(field) - }) - .collect(), - - _ => unimplemented!("Only implemented for named fileds"), - }, - _ => unimplemented!("Only implemented for structs"), - }; - - let functions_to_generate: TokenStream2 = functions_to_generate.into_iter().collect(); - let gen = quote! { - pub fn generate_ci_functions( - lua: &mut mlua::Lua, - tx: tokio::sync::mpsc::Sender - ) - { - lua.set_app_data(tx); - let globals = lua.globals(); - #functions_to_export_in_lua - } - #functions_to_generate - }; - gen.into() -} diff --git a/lua_macros/src/struct_to_ci_enum/generate_help_function.rs b/lua_macros/src/struct_to_ci_enum/generate_help_function.rs deleted file mode 100644 index 2987272..0000000 --- a/lua_macros/src/struct_to_ci_enum/generate_help_function.rs +++ /dev/null @@ -1,53 +0,0 @@ -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens}; - -pub fn generate_help_function(input: &syn::FieldsNamed) -> TokenStream2 { - let input: Vec<_> = input.named.iter().collect(); - - let combined_help_text: TokenStream2 = input - .iter() - .map(|field| { - let attrs_with_doc: Vec = field - .attrs - .iter() - .filter_map(|attr| { - if attr.path().is_ident("doc") { - let help_text = attr - .meta - .require_name_value() - .expect("This is a named value type, because all doc comments work this way") - .value - .clone(); - Some(help_text.into_token_stream().into()) - } else { - return None; - } - }) - .collect(); - if attrs_with_doc.len() == 0 { - // TODO there should be a better panic function, than the generic one - panic!( - "The command named: `{}`, does not provide a help message", - field.ident.as_ref().expect("These are all named") - ); - } else { - let help_text_for_one_command_combined: TokenStream2 = attrs_with_doc.into_iter().collect(); - return help_text_for_one_command_combined; - } - }) - .collect(); - - quote! { - #[ci_command] - async fn help( - lua: &mlua::Lua, - input_str: Option - ) -> Result { - // TODO add a way to filter the help based on the input - - let output = "These functions exist:\n"; - output.push_str(#combined_help_text); - Ok(output) - } - } -} diff --git a/lua_macros/src/struct_to_ci_enum/mod.rs b/lua_macros/src/struct_to_ci_enum/mod.rs deleted file mode 100644 index 4d93f98..0000000 --- a/lua_macros/src/struct_to_ci_enum/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod generate_command_enum; -pub mod generate_generate_ci_function; -pub mod generate_help_function; - -pub use generate_command_enum::*; -pub use generate_generate_ci_function::*; -pub use generate_help_function::*; diff --git a/src/app/command_interface.rs b/src/app/command_interface.rs deleted file mode 100644 index e475113..0000000 --- a/src/app/command_interface.rs +++ /dev/null @@ -1,57 +0,0 @@ -// FIXME: This file needs documentation with examples of how the proc macros work. -// for now use `cargo expand app::command_interface` for an overview - -use std::{ - io::{Error, ErrorKind}, - sync::Arc, -}; - -use lua_macros::{ci_command, turn_struct_to_ci_command_enum}; - -use crate::app::event_types::Event; -/// This struct is here to guarantee, that all functions actually end up in the lua context. -/// I.e. Rust should throw a compile error, when one field is added, but not a matching function. -/// -/// What it does: -/// - Generates a `generate_ci_functions` function, which wraps the specified rust in functions -/// in lua and exports them to the globals in the context provided as argument. -/// - Generates a Commands enum, which contains every Camel cased version of the fields. -/// -/// Every command specified here should have a function named $command_name, where $command_name is the snake cased name of the field. -/// -/// This function is exported to the lua context, thus it's signature must be: -/// ```rust -/// fn $command_name(context: Context, input_string: String) -> Result<$return_type, rlua::Error> {} -/// ``` -/// where $return_type is the type returned by the function (the only supported ones are right now -/// `String` and `()`). - -#[turn_struct_to_ci_command_enum] -struct Commands { - /// Greets the user - greet: fn(String) -> String, - - /// Closes the application - //#[expose(lua)] - 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) -> String, -} diff --git a/src/app/command_interface/lua_command_manager/mod.rs b/src/app/command_interface/lua_command_manager/mod.rs new file mode 100644 index 0000000..3116f0c --- /dev/null +++ b/src/app/command_interface/lua_command_manager/mod.rs @@ -0,0 +1,189 @@ +use std::{collections::HashMap, fmt::Display, thread}; + +use anyhow::{Context, Result}; +use cli_log::{error, info, debug}; +use mlua::{Function, Value}; +use once_cell::sync::OnceCell; +use serde::{Deserialize, Serialize}; +use tokio::{ + runtime::Builder, + sync::{mpsc, Mutex}, + task::{self, LocalSet}, +}; + +use crate::app::{ + command_interface::{add_lua_functions_to_globals, Command}, + events::event_types::Event, +}; + +static LUA: OnceCell> = OnceCell::new(); +pub type Table = HashMap; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum CommandTransferValue { + /// `nil` or `null` or `undefined`; anything which goes in that group of types. + Nil, + + /// `true` or `false`. + Boolean(bool), + + // A "light userdata" object, equivalent to a raw pointer. + // /*TODO*/ LightUserData(LightUserData), + + /// An integer number. + Integer(i64), + + /// A floating point number. + Number(f64), + + /// A string + String(String), + + /// A table, directory or HashMap + Table(HashMap), + + // Reference to a Lua function (or closure). + // /* TODO */ Function(Function), + + // Reference to a Lua thread (or coroutine). + // /* TODO */ Thread(Thread<'lua>), + + // Reference to a userdata object that holds a custom type which implements `UserData`. + // Special builtin userdata types will be represented as other `Value` variants. + // /* TODO */ UserData(AnyUserData), + + // `Error` is a special builtin userdata type. When received from Lua it is implicitly cloned. + // /* TODO */ Error(Error), +} + +impl Display for CommandTransferValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CommandTransferValue::Nil => f.write_str("Nil"), + CommandTransferValue::Boolean(bool) => f.write_str(&format!("{}", bool)), + CommandTransferValue::Integer(int) => f.write_str(&format!("{}", int)), + CommandTransferValue::Number(num) => f.write_str(&format!("{}", num)), + CommandTransferValue::String(str) => f.write_str(&format!("{}", str)), + // TODO(@Soispha): The following line should be a real display call, but how do you + // format a HashMap? + CommandTransferValue::Table(table) => f.write_str(&format!("{:#?}", table)), + } + } +} + +pub struct LuaCommandManager { + lua_command_tx: mpsc::Sender, +} + +impl From for CommandTransferValue { + fn from(s: String) -> Self { + Self::String(s.to_owned()) + } +} +impl From for CommandTransferValue { + fn from(s: f64) -> Self { + Self::Number(s.to_owned()) + } +} +impl From for CommandTransferValue { + fn from(s: i64) -> Self { + Self::Integer(s.to_owned()) + } +} +impl From> for CommandTransferValue { + fn from(s: HashMap) -> Self { + Self::Table(s.to_owned()) + } +} +impl From for CommandTransferValue { + fn from(s: bool) -> Self { + Self::Boolean(s.to_owned()) + } +} +impl From<()> for CommandTransferValue { + fn from(_: ()) -> Self { + Self::Nil + } +} + +impl LuaCommandManager { + pub async fn execute_code(&self, code: String) { + self.lua_command_tx + .send(code) + .await + .expect("The receiver should not be dropped at this time"); + } + + pub fn new(event_call_tx: mpsc::Sender) -> Self { + info!("Spawning lua code execution thread..."); + let (lua_command_tx, mut lua_command_rx) = mpsc::channel::(256); + thread::spawn(move || { + let rt = Builder::new_current_thread().enable_all().build().expect( + "Should always be able to build \ + tokio runtime for lua command handling", + ); + let local = LocalSet::new(); + local.spawn_local(async move { + info!( + "Lua command handling initialized, \ + waiting for commands.." + ); + while let Some(command) = lua_command_rx.recv().await { + debug!("Recieved lua code: {}", &command); + let local_event_call_tx = event_call_tx.clone(); + + task::spawn_local(async move { + exec_lua_command(&command, local_event_call_tx) + .await + .expect( + "This should return all relevent errors \ + by other messages, \ + this should never error", + ); + }); + } + }); + rt.block_on(local); + }); + + LuaCommandManager { lua_command_tx } + } +} + +async fn exec_lua_command(command: &str, event_call_tx: mpsc::Sender) -> Result<()> { + let second_event_call_tx = event_call_tx.clone(); + let lua = LUA + .get_or_init(|| { + Mutex::new(add_lua_functions_to_globals( + mlua::Lua::new(), + second_event_call_tx, + )) + }) + .lock() + .await; + + info!("Recieved code to execute: `{}`, executing...", &command); + let output = lua.load(command).eval_async::().await; + match output { + Ok(out) => { + let to_string_fn: Function = lua.globals().get("tostring").expect("This always exists"); + let output: String = to_string_fn.call(out).expect("tostring should not error"); + info!("Function `{}` returned: `{}`", command, &output); + + event_call_tx + .send(Event::CommandEvent(Command::DisplayOutput(output), None)) + .await + .context("Failed to send lua output command")? + } + Err(err) => { + error!("Function `{}` returned error: `{}`", command, err); + event_call_tx + .send(Event::CommandEvent( + Command::RaiseError(err.to_string()), + None, + )) + .await?; + } + }; + Ok(()) +} diff --git a/src/app/command_interface/mod.rs b/src/app/command_interface/mod.rs new file mode 100644 index 0000000..0418470 --- /dev/null +++ b/src/app/command_interface/mod.rs @@ -0,0 +1,63 @@ +// Use `cargo expand app::command_interface` for an overview of the file contents + +pub mod lua_command_manager; + +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 lua_command_manager::CommandTransferValue; +use mlua::LuaSerdeExt; +use crate::app::Event; + +#[ci_command_enum] +struct Commands { + /// Returns the string given to it + // FIXME(@Soispha): This is a workaround because the default print prints to stdout, + // which is obviously not ideal + print: fn(String) -> String, + + // 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 sent 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), +} diff --git a/src/app/events/event_types/event/handlers/command.rs b/src/app/events/event_types/event/handlers/command.rs index 4a3f9dc..de301f9 100644 --- a/src/app/events/event_types/event/handlers/command.rs +++ b/src/app/events/event_types/event/handlers/command.rs @@ -1,81 +1,145 @@ -use crate::{ - app::{command_interface::Command, events::event_types::EventStatus, status::State, App}, - ui::central::InputPosition, -}; -use anyhow::Result; -use cli_log::info; +use std::collections::HashMap; + +use anyhow::{Error, Result}; +use cli_log::{trace, warn}; +use tokio::sync::oneshot; + +use crate::{app::{ + command_interface::{ + lua_command_manager::{CommandTransferValue, Table}, + Command, + }, + events::event_types::EventStatus, + App, status::State, +}, ui::central::InputPosition}; pub async fn handle( app: &mut App<'_>, command: &Command, - send_output: bool, -) -> Result<(EventStatus, String)> { - macro_rules! set_status_output { + output_callback: Option>, +) -> Result { + // A command can both return _status output_ (what you would normally print to stderr) + // and _main output_ (the output which is normally printed to stdout). + // We simulate these by returning the main output to the lua function, and printing the + // status output to a status ui field. + // + // Every function should return some status output to show the user, that something is + // happening, while only some functions return some value to the main output, as this + // is reserved for functions called only for their output (for example `greet()`). + macro_rules! send_status_output { ($str:expr) => { - if send_output { - app.ui.set_command_output($str); + app.status.add_status_message($str.to_owned()); + }; + ($str:expr, $($args:ident),+) => { + app.status.add_status_message(format!($str, $($args),+)); + }; + } + macro_rules! send_error_output { + ($str:expr) => { + app.status.add_error_message($str.to_owned()); + }; + ($str:expr, $($args:ident),+) => { + app.status.add_error_message(format!($str, $($args),+)); + }; + } + macro_rules! send_main_output { + ($str:expr) => { + if let Some(sender) = output_callback { + sender + .send(CommandTransferValue::from($str)) + .map_err(|e| Error::msg(format!("Failed to send command main output: `{}`", e)))?; } }; ($str:expr, $($args:ident),+) => { - if send_output { - app.ui.set_command_output(&format!($str, $($args),+)); + if let Some(sender) = output_callback { + sender + .send(CommandTransferValue::from(format!($str, $($args),+))) + .map_err(|e| Error::msg(format!("Failed to send command main output: `{}`", e)))?; } }; } - info!("Handling command: {:#?}", command); + + trace!("Handling command: {:#?}", command); Ok(match command { - Command::Exit => ( - EventStatus::Terminate, - "Terminated the application".to_owned(), - ), + 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::CommandLineShow => { app.ui.cli_enable(); - set_status_output!("CLI online"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("CLI online"); + EventStatus::Ok } Command::CommandLineHide => { app.ui.cli_disable(); - set_status_output!("CLI offline"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("CLI offline"); + EventStatus::Ok } Command::CyclePlanes => { app.ui.cycle_main_input_position(); - set_status_output!("Switched main input position"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Switched main input position"); + EventStatus::Ok } Command::CyclePlanesRev => { app.ui.cycle_main_input_position_rev(); - set_status_output!("Switched main input position; reversed"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Switched main input position; reversed"); + EventStatus::Ok } Command::SetModeNormal => { app.status.set_state(State::Normal); - set_status_output!("Set input mode to Normal"); - (EventStatus::Ok, "".to_owned()) + 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); - set_status_output!("Set input mode to Insert"); - (EventStatus::Ok, "".to_owned()) + 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); } - set_status_output!("Send message: `{}`", msg); - (EventStatus::Ok, "".to_owned()) + EventStatus::Ok } Command::Greet(name) => { - info!("Greated {}", name); - set_status_output!("Hi, {}!", name); - (EventStatus::Ok, "".to_owned()) + send_main_output!("Hi, {}!", name); + EventStatus::Ok + } + Command::Print(output) => { + // FIXME(@Soispha): This only works with strings, which is a clear downside to the + // original print function. Find a way to just use the original one + send_main_output!("{}", output); + EventStatus::Ok + } + Command::GreetMultiple => { + let mut table: Table = HashMap::new(); + table.insert("UserName1".to_owned(), CommandTransferValue::Integer(2)); + send_main_output!(table); + EventStatus::Ok } Command::Help(_) => todo!(), + Command::RaiseError(err) => { + send_error_output!(err); + EventStatus::Ok + } }) } diff --git a/src/app/events/event_types/event/handlers/lua_command.rs b/src/app/events/event_types/event/handlers/lua_command.rs index e22833d..9111330 100644 --- a/src/app/events/event_types/event/handlers/lua_command.rs +++ b/src/app/events/event_types/event/handlers/lua_command.rs @@ -1,38 +1,17 @@ -use std::{sync::Arc, time::Duration}; - -use anyhow::{Context, Result}; -use cli_log::{debug, info}; -use tokio::{task, time::timeout}; +use anyhow::Result; +use cli_log::trace; use crate::app::{events::event_types::EventStatus, App}; -pub async fn handle(app: &mut App<'_>, command: String) -> Result { - info!("Recieved ci command: `{command}`; executing.."); +// This function is here mainly to reserve this spot for further processing of the lua command. +// TODO(@Soispha): Move the lua executor thread code from app to this module +pub async fn handle( + app: &mut App<'_>, + command: String, +) -> Result { + trace!("Recieved ci command: `{command}`; executing.."); - let local = task::LocalSet::new(); - - // Run the local task set. - let output = local - .run_until(async move { - let lua = Arc::clone(&app.lua); - debug!("before_handle"); - let c_handle = task::spawn_local(async move { - lua.load(&command) - // FIXME this assumes string output only - .eval_async::() - .await - .with_context(|| format!("Failed to execute: `{command}`")) - }); - debug!("after_handle"); - c_handle - }) - .await; - debug!("after_thread"); - - let output = timeout(Duration::from_secs(10), output) - .await - .context("Failed to join lua command executor")???; - info!("Command returned: `{}`", output); + app.lua.execute_code(command).await; Ok(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 6396ed1..6ddcce5 100644 --- a/src/app/events/event_types/event/handlers/main.rs +++ b/src/app/events/event_types/event/handlers/main.rs @@ -10,7 +10,10 @@ use crate::{ ui::central, }; -pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { +pub async fn handle_normal( + app: &mut App<'_>, + input_event: &CrosstermEvent, +) -> Result { match input_event { CrosstermEvent::Key(KeyEvent { code: KeyCode::Esc, .. @@ -135,22 +138,22 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R }; } central::InputPosition::CLI => { - if let Some(_) = app.ui.cli { + if let Some(cli) = &app.ui.cli { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, .. }) => { - let ci_event = app.ui - .cli - .as_mut() - .expect("This is already checked") - .lines() - .get(0) - .expect( - "There can only be one line in the buffer, as we collect it on enter being inputted" - ) - .to_owned(); + let ci_event = cli + .lines() + .get(0) + .expect( + "One line always exists, + and others can't exists + because we collect on + enter", + ) + .to_owned(); app.tx .send(Event::LuaCommand(ci_event)) .await diff --git a/src/app/events/event_types/event/handlers/matrix.rs b/src/app/events/event_types/event/handlers/matrix.rs index 5215a28..2a86034 100644 --- a/src/app/events/event_types/event/handlers/matrix.rs +++ b/src/app/events/event_types/event/handlers/matrix.rs @@ -1,9 +1,12 @@ -use matrix_sdk::deserialized_responses::SyncResponse; use anyhow::Result; +use matrix_sdk::deserialized_responses::SyncResponse; use crate::app::{events::event_types::EventStatus, App}; -pub async fn handle<'a>(app: &mut App<'a>, sync: &SyncResponse) -> Result { +pub async fn handle( + app: &mut App<'_>, + sync: &SyncResponse, +) -> Result { for (m_room_id, m_room) in sync.rooms.join.iter() { let room = match app.status.get_room_mut(m_room_id) { Some(r) => r, diff --git a/src/app/events/event_types/event/handlers/setup.rs b/src/app/events/event_types/event/handlers/setup.rs index 8e22128..d879e99 100644 --- a/src/app/events/event_types/event/handlers/setup.rs +++ b/src/app/events/event_types/event/handlers/setup.rs @@ -1,9 +1,15 @@ use anyhow::{bail, Context, Result}; use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent}; -use crate::{app::{events::event_types::EventStatus, App}, ui::setup}; +use crate::{ + app::{events::event_types::EventStatus, App}, + ui::setup, +}; -pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { +pub async fn handle( + app: &mut App<'_>, + input_event: &CrosstermEvent, +) -> Result { let ui = match &mut app.ui.setup_ui { Some(ui) => ui, None => bail!("SetupUI instance not found"), diff --git a/src/app/events/event_types/event/mod.rs b/src/app/events/event_types/event/mod.rs index c279439..27bba87 100644 --- a/src/app/events/event_types/event/mod.rs +++ b/src/app/events/event_types/event/mod.rs @@ -1,11 +1,15 @@ mod handlers; use anyhow::{Context, Result}; -use cli_log::{info, trace}; +use cli_log::trace; use crossterm::event::Event as CrosstermEvent; -use tokio::sync::mpsc::Sender; +use tokio::sync::oneshot; -use crate::app::{command_interface::Command, status::State, App}; +use crate::app::{ + command_interface::{lua_command_manager::CommandTransferValue, Command}, + status::State, + App, +}; use self::handlers::{command, lua_command, main, matrix, setup}; @@ -15,45 +19,35 @@ use super::EventStatus; pub enum Event { InputEvent(CrosstermEvent), MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse), - CommandEvent(Command, Option>), + CommandEvent(Command, Option>), LuaCommand(String), } impl Event { - pub async fn handle(&self, app: &mut App<'_>) -> Result { + pub async fn handle(self, app: &mut App<'_>) -> Result { trace!("Recieved event to handle: `{:#?}`", &self); - match &self { - Event::MatrixEvent(event) => matrix::handle(app, event) + match self { + Event::MatrixEvent(event) => matrix::handle(app, &event) .await .with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)), - Event::CommandEvent(event, callback_tx) => { - let (result, output) = command::handle(app, event, callback_tx.is_some()) - .await - .with_context(|| format!("Failed to handle command event: `{:#?}`", event))?; - - if let Some(callback_tx) = callback_tx { - callback_tx - .send(output.clone()) - .await - .with_context(|| format!("Failed to send command output: {}", output))?; - } - Ok(result) - } + 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)), + .with_context(|| format!("Failed to handle lua code: `{}`", lua_code)), Event::InputEvent(event) => match app.status.state() { - State::Normal => main::handle_normal(app, event) - .await - .with_context(|| format!("Failed to handle input event: `{:#?}`", event)), - State::Insert => main::handle_insert(app, event) - .await - .with_context(|| format!("Failed to handle input event: `{:#?}`", event)), - State::Setup => setup::handle(app, event) - .await - .with_context(|| format!("Failed to handle input event: `{:#?}`", event)), + 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::Setup => setup::handle(app, &event).await.with_context(|| { + format!("Failed to handle input (setup) event: `{:#?}`", event) + }), }, } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 18da6aa..ad52a11 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2,23 +2,24 @@ pub mod command_interface; pub mod events; pub mod status; -use std::{path::Path, sync::Arc}; +use std::path::Path; use anyhow::{Context, Error, Result}; use cli_log::info; use matrix_sdk::Client; -use mlua::Lua; -use status::{State, Status}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use crate::{ accounts::{Account, AccountsManager}, - app::{command_interface::generate_ci_functions, events::event_types::Event}, + app::{ + events::event_types::Event, + status::{State, Status}, + }, ui::{central, setup}, }; -use self::events::event_types; +use self::{command_interface::lua_command_manager::LuaCommandManager, events::event_types}; pub struct App<'ui> { ui: central::UI<'ui>, @@ -31,18 +32,11 @@ pub struct App<'ui> { input_listener_killer: CancellationToken, matrix_listener_killer: CancellationToken, - lua: Arc, + lua: LuaCommandManager, } impl App<'_> { pub fn new() -> Result { - fn set_up_lua(tx: mpsc::Sender) -> Arc { - let mut lua = Lua::new(); - - generate_ci_functions(&mut lua, tx); - Arc::new(lua) - } - let path: &std::path::Path = Path::new("userdata/accounts.json"); let config = if path.exists() { info!("Reading account config (userdata/accounts.json)"); @@ -62,7 +56,7 @@ impl App<'_> { input_listener_killer: CancellationToken::new(), matrix_listener_killer: CancellationToken::new(), - lua: set_up_lua(tx), + lua: LuaCommandManager::new(tx), }) } @@ -87,11 +81,10 @@ impl App<'_> { self.ui.update(&self.status).await?; let event = self.rx.recv().await.context("Failed to get next event")?; - match event.handle(self).await? { event_types::EventStatus::Ok => (), event_types::EventStatus::Terminate => break, - _ => (), + _ => todo!(), }; } diff --git a/src/app/status.rs b/src/app/status.rs index 0334301..9ce211c 100644 --- a/src/app/status.rs +++ b/src/app/status.rs @@ -28,6 +28,19 @@ pub struct Room { view_scroll: Option, } +pub struct StatusMessage { + content: String, + is_error: bool, +} +impl StatusMessage { + pub fn content(&self) -> String { + self.content.clone() + } + pub fn is_error(&self) -> bool { + self.is_error + } +} + pub struct Status { state: State, account_name: String, @@ -36,6 +49,7 @@ pub struct Status { client: Option, rooms: IndexMap, current_room_id: String, + status_messages: Vec, } impl fmt::Display for State { @@ -138,14 +152,40 @@ impl Status { Self { state: State::Normal, - account_name: "".to_string(), - account_user_id: "".to_string(), - client, + account_name: "".to_owned(), + account_user_id: "".to_owned(), + client, rooms, - current_room_id: "".to_string(), + current_room_id: "".to_owned(), + status_messages: vec![StatusMessage { + content: "Initialized!".to_owned(), + is_error: false, + }], } } + pub fn add_status_message(&mut self, msg: String) { + // TODO(@Soispha): This could allocate a lot of ram, when we don't + // add a limit to the messages. + // This needs to be proven. + self.status_messages.push(StatusMessage { + content: msg, + is_error: false, + }) + } + pub fn add_error_message(&mut self, msg: String) { + // TODO(@Soispha): This could allocate a lot of ram, when we don't + // add a limit to the messages. + // This needs to be proven. + self.status_messages.push(StatusMessage { + content: msg, + is_error: true, + }) + } + pub fn status_messages(&self) -> &Vec { + &self.status_messages + } + pub fn account_name(&self) -> &String { &self.account_name } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..e23f01e --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,17 @@ +use clap::{Parser, Subcommand}; + +// TODO: The description could be better +/// A terminal client for the matrix chat protocol +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Args { + #[command(subcommand)] + /// The subcommand to execute, default is start + pub subcommand: Option, +} +#[derive(Subcommand, Debug)] +pub enum Command { + /// Starts the main tui client + #[clap(value_parser)] + Start {}, +} diff --git a/src/main.rs b/src/main.rs index 3ef9611..0c8e5b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,24 @@ -mod accounts; mod app; mod ui; +mod accounts; +mod cli; + +use clap::Parser; + +use crate::cli::{Args, Command}; #[tokio::main] async fn main() -> anyhow::Result<()> { cli_log::init_cli_log!(); - let mut app = app::App::new()?; - app.run().await?; + let args = Args::parse(); + let command = args.subcommand.unwrap_or(Command::Start {}); + match command { + Command::Start {} => { + let mut app = app::App::new()?; + app.run().await?; + } + }; Ok(()) } diff --git a/src/ui/central/mod.rs b/src/ui/central/mod.rs index b2bf4e1..c889406 100644 --- a/src/ui/central/mod.rs +++ b/src/ui/central/mod.rs @@ -11,12 +11,13 @@ use crossterm::{ }; use tui::{ backend::CrosstermBackend, + style::Color, widgets::{Block, Borders, ListState}, Terminal, }; use tui_textarea::TextArea; -use crate::ui::terminal_prepare; +use crate::ui::{terminal_prepare, textarea_inactivate, textarea_activate}; use super::setup; @@ -28,10 +29,121 @@ pub enum InputPosition { Rooms, Messages, MessageCompose, + CommandMonitor, RoomInfo, CLI, } +impl InputPosition { + // calculate to widgets colors, based of which widget is currently selected + pub fn colors( + &self, + mut cli: &mut Option>, + mut message_compose: &mut TextArea<'_>, + ) -> Vec { + match self { + InputPosition::Status => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::Rooms => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::Messages => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::MessageCompose => { + textarea_activate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::RoomInfo => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::White, + Color::DarkGray, + ] + } + InputPosition::CLI => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_activate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::CommandMonitor => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::White, + ] + } + } + } +} + pub struct UI<'a> { terminal: Terminal>, input_position: InputPosition, @@ -89,7 +201,8 @@ impl UI<'_> { InputPosition::Status => InputPosition::Rooms, InputPosition::Rooms => InputPosition::Messages, InputPosition::Messages => InputPosition::MessageCompose, - InputPosition::MessageCompose => InputPosition::RoomInfo, + InputPosition::MessageCompose => InputPosition::CommandMonitor, + InputPosition::CommandMonitor => InputPosition::RoomInfo, InputPosition::RoomInfo => match self.cli { Some(_) => InputPosition::CLI, None => InputPosition::Status, @@ -107,7 +220,8 @@ impl UI<'_> { InputPosition::Rooms => InputPosition::Status, InputPosition::Messages => InputPosition::Rooms, InputPosition::MessageCompose => InputPosition::Messages, - InputPosition::RoomInfo => InputPosition::MessageCompose, + InputPosition::RoomInfo => InputPosition::CommandMonitor, + InputPosition::CommandMonitor => InputPosition::MessageCompose, InputPosition::CLI => InputPosition::RoomInfo, }; } @@ -157,12 +271,12 @@ impl UI<'_> { } pub async fn update_setup(&mut self) -> Result<()> { - let ui = match &mut self.setup_ui { + let setup_ui = match &mut self.setup_ui { Some(c) => c, None => bail!("SetupUI instance not found"), }; - ui.update(&mut self.terminal).await?; + setup_ui.update(&mut self.terminal).await?; Ok(()) } diff --git a/src/ui/central/update/mod.rs b/src/ui/central/update/mod.rs index 2b1ed2d..fcd06b0 100644 --- a/src/ui/central/update/mod.rs +++ b/src/ui/central/update/mod.rs @@ -1,20 +1,13 @@ use std::cmp; use anyhow::{Context, Result}; -use tui::{ - layout::{Constraint, Direction, Layout}, - style::{Color, Style}, - widgets::{Block, Borders, Paragraph}, -}; +use tui::{layout::{Constraint, Direction, Layout}, widgets::{Paragraph, Block, Borders}, style::{Style, Color}}; -use crate::{ - app::status::Status, - ui::{textarea_activate, textarea_inactivate}, -}; +use crate::app::status::Status; -use self::widgets::{messages, room_info, rooms, status}; +use self::widgets::{command_monitor, messages, room_info, rooms, status}; -use super::{InputPosition, UI}; +use super::UI; pub mod widgets; @@ -66,90 +59,12 @@ impl UI<'_> { let right_chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Min(4)].as_ref()) + .constraints([Constraint::Length(5), Constraint::Min(4)].as_ref()) .split(main_chunks[2]); - // calculate to widgets colors, based of which widget is currently selected - let colors = match self.input_position { - InputPosition::Status => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::White, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::Rooms => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::White, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::Messages => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::White, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::MessageCompose => { - textarea_activate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::RoomInfo => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::White, - ] - } - InputPosition::CLI => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_activate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - }; + let colors = self + .input_position + .colors(&mut self.cli, &mut self.message_compose); // initiate the widgets let mode_indicator = Paragraph::new(status.state().to_string()) @@ -164,6 +79,7 @@ impl UI<'_> { let (messages_panel, mut messages_state) = messages::init(status.room(), &colors) .context("Failed to initiate the messages widget")?; let room_info_panel = room_info::init(status.room(), &colors); + let command_monitor = command_monitor::init(status.status_messages(), &colors); // render the widgets self.terminal.draw(|frame| { @@ -177,6 +93,7 @@ impl UI<'_> { None => (), }; frame.render_widget(room_info_panel, right_chunks[0]); + frame.render_widget(command_monitor, right_chunks[1]); })?; Ok(()) diff --git a/src/ui/central/update/widgets/command_monitor.rs b/src/ui/central/update/widgets/command_monitor.rs new file mode 100644 index 0000000..9e548ed --- /dev/null +++ b/src/ui/central/update/widgets/command_monitor.rs @@ -0,0 +1,34 @@ +use tui::{ + layout::Alignment, + style::{Color, Style}, + text::Text, + widgets::{Block, Borders, Paragraph}, +}; + +use crate::{app::status::StatusMessage, ui::central::InputPosition}; + +pub fn init<'a>(status_events: &Vec, colors: &Vec) -> Paragraph<'a> { + let mut command_monitor = Text::default(); + + status_events.iter().for_each(|event| { + // TODO(@Soispha): The added text (`event.content()`) doesn't wrap nicely, + // it would be nice if it did. + command_monitor.extend(Text::styled( + event.content(), + Style::default().fg(if event.is_error() { + Color::Red + } else { + Color::Cyan + }), + )); + }); + + Paragraph::new(command_monitor) + .block( + Block::default() + .title("Command Montior") + .borders(Borders::ALL) + .style(Style::default().fg(colors[InputPosition::CommandMonitor as usize])), + ) + .alignment(Alignment::Center) +} diff --git a/src/ui/central/update/widgets/mod.rs b/src/ui/central/update/widgets/mod.rs index 850c27f..d145b95 100644 --- a/src/ui/central/update/widgets/mod.rs +++ b/src/ui/central/update/widgets/mod.rs @@ -2,3 +2,4 @@ pub mod messages; pub mod room_info; pub mod rooms; pub mod status; +pub mod command_monitor;