feat(command_interface): Add support for namespaces

This commit is contained in:
Benedikt Peetz 2023-09-20 19:21:44 +02:00
parent 29aa6c1d33
commit 27d00c564c
Signed by: bpeetz
GPG Key ID: A5E94010C3A642AD
11 changed files with 588 additions and 274 deletions

View File

@ -1,50 +1,96 @@
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote}; use quote::{format_ident, quote, ToTokens};
use syn::{DeriveInput, Field, Type}; 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 { use super::get_input_type_of_bare_fn_field;
let named_fields = parse_derive_input_as_named_fields(input);
let fields: TokenStream2 = named_fields pub fn command_enum(input: &DataCommandEnum) -> TokenStream2 {
.named let (fields, namespace_enums): (TokenStream2, TokenStream2) =
.iter() turn_fields_to_enum(&input.fields);
.map(|field| turn_struct_fieled_to_enum(field))
.collect();
quote! { quote! {
#[derive(Debug)] #[derive(Debug)]
pub enum Command { pub enum Command {
#fields #fields
} }
#namespace_enums
} }
} }
fn turn_struct_fieled_to_enum(field: &Field) -> TokenStream2 { fn turn_fields_to_enum(fields: &Punctuated<Field, Token![,]>) -> (TokenStream2, TokenStream2) {
let field_name = format_ident!( let output: Vec<_> = fields
"{}", .iter()
field .map(|field| turn_struct_field_to_enum(field))
.ident .collect();
.as_ref()
.expect("These are named fields, it should be Some(<name>)")
.to_string()
.from_case(Case::Snake)
.to_case(Case::Pascal)
);
let input_type: Option<Type> = 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 { for (fields, namespace_enum) in output {
Some(input_type) => { fields_output.extend(fields.to_token_stream());
quote! { namespace_enums_output.extend(namespace_enum.to_token_stream());
#field_name(#input_type), }
(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<Type> = 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 => { Field::Namespace(namespace) => {
quote! { let (namespace_output_fields, namespace_output_namespace_enums) =
#field_name, turn_fields_to_enum(&namespace.fields);
} let namespace_name: Ident = format_ident!(
"{}",
namespace.path.iter().map(|name| name.to_string()).collect::<String>()
);
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
},
)
} }
} }
} }

View File

@ -1,16 +1,35 @@
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote}; 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 { pub fn generate_add_lua_functions_to_globals(input: &DataCommandEnum) -> TokenStream2 {
let named_fields = parse_derive_input_as_named_fields(input); fn turn_field_to_functions(
let function_adders: TokenStream2 = named_fields input: &Punctuated<Field, Token![,]>,
.named namespace_path: Option<&NamespacePath>,
.iter() ) -> TokenStream2 {
.map(|field| generate_function_adder(field)) input
.collect(); .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! { quote! {
pub fn add_lua_functions_to_globals( 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 { fn generate_function_adder(
let field_ident = field field: &FunctionDeclaration,
.ident namespace_path: Option<&NamespacePath>,
.as_ref() ) -> TokenStream2 {
.expect("This is should be a named field"); let field_ident = &field.name;
let function_ident = format_ident!("wrapped_lua_function_{}", field_ident); let function_ident = format_ident!("wrapped_lua_function_{}", field_ident);
let function_name = field_ident.to_string(); 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! { quote! {
{ {
let #function_ident = lua.create_async_function(#field_ident).expect( let #function_ident = lua.create_async_function(#field_ident).expect(
@ -45,10 +116,7 @@ fn generate_function_adder(field: &Field) -> TokenStream2 {
#function_name #function_name
) )
); );
#setter
globals.set(#function_name, #function_ident).expect(
"Setting a static global value should work"
);
} }
} }
} }

View File

@ -1,18 +1,20 @@
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::quote; use quote::quote;
use syn::DeriveInput;
use crate::generate::lua_wrapper::{ use crate::{
lua_functions_to_globals::generate_add_lua_functions_to_globals, generate::lua_wrapper::{
rust_wrapper_functions::generate_rust_wrapper_functions, lua_functions_to_globals::generate_add_lua_functions_to_globals,
rust_wrapper_functions::generate_rust_wrapper_functions,
},
DataCommandEnum,
}; };
mod lua_functions_to_globals; mod lua_functions_to_globals;
mod rust_wrapper_functions; 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 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! { quote! {
#add_lua_functions_to_globals #add_lua_functions_to_globals
#rust_wrapper_functions #rust_wrapper_functions

View File

@ -1,21 +1,39 @@
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote}; use quote::quote;
use syn::{ use syn::{punctuated::Punctuated, token::Comma, GenericArgument, Lifetime, Token, Type};
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},
DataCommandEnum, Field, FunctionDeclaration, NamespacePath,
}; };
use crate::generate::{ pub fn generate_rust_wrapper_functions(
get_input_type_of_bare_fn_field, get_return_type_of_bare_fn_field, namespace: Option<&NamespacePath>,
parse_derive_input_as_named_fields, input: &DataCommandEnum,
}; ) -> TokenStream2 {
generate_rust_wrapper_functions_rec(namespace, &input.fields)
}
pub fn generate_rust_wrapper_functions(input: &DeriveInput) -> TokenStream2 { pub fn generate_rust_wrapper_functions_rec(
let named_fields = parse_derive_input_as_named_fields(input); namespace: Option<&NamespacePath>,
let wrapped_functions: TokenStream2 = named_fields input: &Punctuated<Field, Token![,]>,
.named ) -> TokenStream2 {
let wrapped_functions: TokenStream2 = input
.iter() .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(); .collect();
quote! { 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 input_type = get_input_type_of_bare_fn_field(field);
let return_type = get_return_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_name = &field.name;
let function_body = get_function_body(field, input_type.is_some(), &return_type); let function_body = get_function_body(&namespace, field, input_type.is_some(), &return_type);
let lifetime_args = let lifetime_args =
get_and_add_lifetimes_form_inputs_and_outputs(input_type.clone(), return_type); 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 output
} }
fn get_function_body(field: &Field, has_input: bool, output_type: &Option<Type>) -> TokenStream2 { fn get_function_body(
namespace: &NamespacePath,
field: &FunctionDeclaration,
has_input: bool,
output_type: &Option<Type>,
) -> TokenStream2 {
let command_name = field let command_name = field
.ident .name
.as_ref()
.expect("These are named fields, it should be Some(<name>)")
.to_string() .to_string()
.from_case(Case::Snake) .from_case(Case::Snake)
.to_case(Case::Pascal); .to_case(Case::Pascal);
let command_ident = format_ident!("{}", command_name);
let send_output = if has_input { let command_ident = {
quote! { if has_input {
Event::CommandEvent( format!("{}(", command_name)
Command::#command_ident(input.clone()), } else {
Some(callback_tx), command_name.clone()
)
}
} else {
quote! {
Event::CommandEvent(
Command::#command_ident,
Some(callback_tx),
)
} }
}; };
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::<Vec<String>>()
.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::<String>());
output
} else {
(0..namespace.len()).map(|_| ')').collect::<String>()
}
};
("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 { let function_return = if let Some(_) = output_type {
quote! { quote! {
return Ok(output.into_lua(lua).expect("This conversion should always work")); return Ok(output.into_lua(lua).expect("This conversion should always work"));

View File

@ -3,18 +3,9 @@ mod lua_wrapper;
pub use command_enum::command_enum; pub use command_enum::command_enum;
pub use lua_wrapper::lua_wrapper; 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 { use crate::FunctionDeclaration;
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<Type> { pub fn get_bare_fn_input_type(function: &TypeBareFn) -> Option<Type> {
if function.inputs.len() == 1 { if function.inputs.len() == 1 {
@ -37,7 +28,7 @@ pub fn get_bare_fn_input_type(function: &TypeBareFn) -> Option<Type> {
} }
} }
pub fn get_input_type_of_bare_fn_field(field: &Field) -> Option<Type> { pub fn get_input_type_of_bare_fn_field(field: &FunctionDeclaration) -> Option<Type> {
match &field.ty { match &field.ty {
syn::Type::BareFn(function) => get_bare_fn_input_type(&function), syn::Type::BareFn(function) => get_bare_fn_input_type(&function),
_ => unimplemented!( _ => unimplemented!(
@ -46,7 +37,7 @@ pub fn get_input_type_of_bare_fn_field(field: &Field) -> Option<Type> {
), ),
} }
} }
pub fn get_return_type_of_bare_fn_field(field: &Field) -> Option<Type> { pub fn get_return_type_of_bare_fn_field(field: &FunctionDeclaration) -> Option<Type> {
match &field.ty { match &field.ty {
syn::Type::BareFn(function) => get_bare_fn_return_type(&function), syn::Type::BareFn(function) => get_bare_fn_return_type(&function),
_ => unimplemented!( _ => unimplemented!(

View File

@ -1,7 +1,10 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::quote; use quote::quote;
use syn::DeriveInput; use syn::{
braced, parse::Parse, parse_macro_input, punctuated::Punctuated, token, Attribute, Ident,
Token, Type,
};
mod generate; mod generate;
@ -14,19 +17,17 @@ mod generate;
/// the rust wrapper functions to the lua globals. /// 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 input and output values of the wrapped functions are derived from the values specified in
/// the `Commands` struct. /// the input to the `parse_command_enum` proc macro.
/// The returned values will be returned directly to the lua context, this allows to nest functions.
/// ///
/// For example this rust code: /// For example this rust code:
/// ```rust /// ```no_run
/// #[ci_command_enum] /// parse_command_enum! {
/// struct Commands {
/// /// Greets the user /// /// Greets the user
/// greet: fn(String) -> String, /// greet: fn(String) -> String,
/// } /// }
/// ``` /// ```
/// results in this expanded code: /// results in this expanded code:
/// ```rust /// ```no_run
/// #[derive(Debug)] /// #[derive(Debug)]
/// pub enum Command { /// pub enum Command {
/// Greet(String), /// Greet(String),
@ -72,12 +73,118 @@ mod generate;
/// }; /// };
/// } /// }
/// ``` /// ```
#[proc_macro_attribute] #[derive(Debug)]
pub fn ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream { struct DataCommandEnum {
// Construct a representation of Rust code as a syntax tree #[allow(dead_code)]
// that we can manipulate commands_token: kw::commands,
let input: DeriveInput = syn::parse(input)
.expect("This should always be valid rust code, as it's extracted from direct code"); #[allow(dead_code)]
brace_token: token::Brace,
fields: Punctuated<Field, Token![,]>,
}
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<Field, Token![,]>,
}
type NamespacePath = Punctuated<Ident, Token![::]>;
#[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<Self> {
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<Self> {
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<Self> {
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<Self> {
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 // Build the language wrappers
let lua_wrapper: TokenStream2 = generate::lua_wrapper(&input); 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 // Build the final enum
let command_enum = generate::command_enum(&input); let command_enum = generate::command_enum(&input);
quote! { let output = quote! {
#command_enum #command_enum
#lua_wrapper #lua_wrapper
} };
.into() output.into()
} }

View File

@ -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<String>),
/// 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),
},
},
},
}
}

View File

@ -11,7 +11,13 @@ use tokio::{
}; };
use crate::app::{ 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, events::event_types::Event,
}; };
@ -87,7 +93,10 @@ async fn exec_lua(lua_code: &str, event_call_tx: mpsc::Sender<Event>) -> Result<
if output != "nil" { if output != "nil" {
event_call_tx event_call_tx
.send(Event::CommandEvent(Command::DisplayOutput(output), None)) .send(Event::CommandEvent(
Command::Trinitrix(Api(Raw(DisplayOutput(output)))),
None,
))
.await .await
.context("Failed to send lua output command")? .context("Failed to send lua output command")?
} }
@ -96,7 +105,7 @@ async fn exec_lua(lua_code: &str, event_call_tx: mpsc::Sender<Event>) -> Result<
error!("Lua code `{}` returned error: `{}`", lua_code, err); error!("Lua code `{}` returned error: `{}`", lua_code, err);
event_call_tx event_call_tx
.send(Event::CommandEvent( .send(Event::CommandEvent(
Command::RaiseError(err.to_string()), Command::Trinitrix(Api(Raw(RaiseError(err.to_string())))),
None, None,
)) ))
.await?; .await?;

View File

@ -1,64 +1,5 @@
// Use `cargo expand app::command_interface` for an overview of the file contents
pub mod command_transfer_value; pub mod command_transfer_value;
pub mod lua_command_manager; pub mod lua_command_manager;
pub mod command_list;
use language_macros::ci_command_enum; pub use command_list::*;
// 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<String>),
/// 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),
}

View File

@ -8,7 +8,7 @@ use crate::{
app::{ app::{
command_interface::{ command_interface::{
command_transfer_value::{CommandTransferValue, Table}, command_transfer_value::{CommandTransferValue, Table},
Command, Api, Command, Debug, Raw, Trinitrix, Ui,
}, },
events::event_types::EventStatus, events::event_types::EventStatus,
status::State, status::State,
@ -66,96 +66,102 @@ pub async fn handle(
trace!("Handling command: {:#?}", command); trace!("Handling command: {:#?}", command);
Ok(match 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) => { Command::Print(output) => {
let output_str: String = output.to_string(); let output_str: String = output.to_string();
send_status_output!(output_str); send_status_output!(output_str);
EventStatus::Ok EventStatus::Ok
} }
Command::CommandLineShow => { Command::Trinitrix(trinitrix) => match trinitrix {
app.ui.cli_enable(); Trinitrix::Debug(debug) => match debug {
app.status.set_state(State::Command); Debug::Greet(msg) => {
send_status_output!("CLI online"); send_main_output!("Greeting, {}!", msg);
EventStatus::Ok EventStatus::Ok
} }
Command::CommandLineHide => { Debug::GreetMultiple => {
app.ui.cli_disable(); let mut table: Table = HashMap::new();
send_status_output!("CLI offline"); table.insert("UserId".to_owned(), CommandTransferValue::Integer(2));
EventStatus::Ok table.insert(
} "UserName".to_owned(),
CommandTransferValue::String("James".to_owned()),
);
Command::CyclePlanes => { let mut second_table: Table = HashMap::new();
app.ui.cycle_main_input_position(); second_table.insert("interface".to_owned(), CommandTransferValue::Integer(3));
send_status_output!("Switched main input position"); second_table.insert("api".to_owned(), CommandTransferValue::Boolean(true));
EventStatus::Ok table.insert(
} "Versions".to_owned(),
Command::CyclePlanesRev => { CommandTransferValue::Table(second_table),
app.ui.cycle_main_input_position_rev(); );
send_status_output!("Switched main input position; reversed"); send_main_output!(table);
EventStatus::Ok EventStatus::Ok
} }
},
Command::SetModeNormal => { Trinitrix::Api(api) => match api {
app.status.set_state(State::Normal); Api::Exit => {
send_status_output!("Set input mode to Normal"); send_status_output!("Terminating the application..");
EventStatus::Ok EventStatus::Terminate
} }
Command::SetModeInsert => { Api::RoomMessageSend(msg) => {
app.status.set_state(State::Insert); if let Some(room) = app.status.room_mut() {
app.ui.set_input_position(InputPosition::MessageCompose); room.send(msg.clone()).await?;
send_status_output!("Set input mode to Insert"); send_status_output!("Sent message: `{}`", msg);
EventStatus::Ok } else {
} // // FIXME(@soispha): This should raise an error within lua, as it would
// otherwise be very confusing <2023-09-20>
Command::RoomMessageSend(msg) => { warn!("Can't send message: `{}`, as there is no open room!", &msg);
if let Some(room) = app.status.room_mut() { }
room.send(msg.clone()).await?; EventStatus::Ok
send_status_output!("Sent message: `{}`", msg); }
} else { Api::Help(_) => todo!(),
// TODO(@Soispha): Should this raise a Lua error? It could be very confusing, Api::Ui(ui) => match ui {
// when a user doesn't read the log. Ui::CommandLineShow => {
warn!("Can't send message: `{}`, as there is no open room!", &msg); app.ui.cli_enable();
} app.status.set_state(State::Command);
EventStatus::Ok send_status_output!("CLI online");
} EventStatus::Ok
Command::Greet(name) => { }
send_main_output!("Hi, {}!", name); Ui::CommandLineHide => {
EventStatus::Ok app.ui.cli_disable();
} send_status_output!("CLI offline");
Command::GreetMultiple => { EventStatus::Ok
let mut table: Table = HashMap::new(); }
table.insert("UserId".to_owned(), CommandTransferValue::Integer(2)); Ui::CyclePlanes => {
table.insert( app.ui.cycle_main_input_position();
"UserName".to_owned(), send_status_output!("Switched main input position");
CommandTransferValue::String("James".to_owned()), EventStatus::Ok
); }
Ui::CyclePlanesRev => {
let mut second_table: Table = HashMap::new(); app.ui.cycle_main_input_position_rev();
second_table.insert("interface".to_owned(), CommandTransferValue::Integer(3)); send_status_output!("Switched main input position; reversed");
second_table.insert("api".to_owned(), CommandTransferValue::Boolean(true)); EventStatus::Ok
table.insert( }
"Versions".to_owned(), Ui::SetModeNormal => {
CommandTransferValue::Table(second_table), app.status.set_state(State::Normal);
); send_status_output!("Set input mode to Normal");
send_main_output!(table); EventStatus::Ok
EventStatus::Ok }
} Ui::SetModeInsert => {
Command::Help(_) => todo!(), app.status.set_state(State::Insert);
Command::RaiseError(err) => { app.ui.set_input_position(InputPosition::MessageCompose);
send_error_output!(err); send_status_output!("Set input mode to Insert");
EventStatus::Ok 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
}
},
},
},
}) })
} }

View File

@ -3,7 +3,7 @@ use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers}
use crate::{ use crate::{
app::{ app::{
command_interface::Command, command_interface::{Api, Command, Trinitrix, Ui},
events::event_types::{Event, EventStatus}, events::event_types::{Event, EventStatus},
App, App,
}, },
@ -20,7 +20,10 @@ pub async fn handle_command(
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::SetModeNormal, None)) .send(Event::CommandEvent(
Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::SetModeNormal))),
None,
))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
@ -62,14 +65,20 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::Exit, None)) .send(Event::CommandEvent(
Command::Trinitrix(Trinitrix::Api(Api::Exit)),
None,
))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::CyclePlanes, None)) .send(Event::CommandEvent(
Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::CyclePlanes))),
None,
))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
@ -77,7 +86,10 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::CyclePlanesRev, None)) .send(Event::CommandEvent(
Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::CyclePlanesRev))),
None,
))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
@ -85,7 +97,10 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::CommandLineShow, None)) .send(Event::CommandEvent(
Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::CommandLineShow))),
None,
))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
@ -93,7 +108,10 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::SetModeInsert, None)) .send(Event::CommandEvent(
Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::SetModeInsert))),
None,
))
.await?; .await?;
} }
input => match app.ui.input_position() { input => match app.ui.input_position() {
@ -193,7 +211,10 @@ pub async fn handle_insert(app: &mut App<'_>, event: &CrosstermEvent) -> Result<
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::SetModeNormal, None)) .send(Event::CommandEvent(
Command::Trinitrix(Trinitrix::Api(Api::Ui(Ui::SetModeNormal))),
None,
))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
@ -203,7 +224,9 @@ pub async fn handle_insert(app: &mut App<'_>, event: &CrosstermEvent) -> Result<
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent( .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, None,
)) ))
.await?; .await?;