Compare commits

...

11 Commits

Author SHA1 Message Date
Benedikt Peetz 7adea99703
Refactor(treewide): Remove the useless `tui_app` directory 2023-07-26 21:19:50 +02:00
Benedikt Peetz 15c705154e
Fix(handlers::main): Don't close the cli after an entered command 2023-07-26 21:17:37 +02:00
Benedikt Peetz 80a8c9ccf5
Fix(handlers::command): Only send success on real success
The `room_message_send()` function told the user about a successful sent
room message, even if that was not the case.
2023-07-26 21:15:53 +02:00
Benedikt Peetz 363a0f8fc4
Fix(handlers::command): Add the missing displayOutput handler
This handler is called by the lua executor, to show the final output of
a lua execution.
2023-07-26 21:14:36 +02:00
Benedikt Peetz fdb6ab1c29
Fix(app::command_interface): Add a `greet_multiple()` func to test tables
This is, like the `greet` function only here to debug the lua api. It
outputs a table.
2023-07-26 21:12:17 +02:00
Benedikt Peetz 57b01f0dbb
Fix(app::command_interface): Add a (workaround) print function
The default print function prints to stdout, which obviously
doesn't work with a tui application. This wrapper however is a
rather poor workaround, as it only works with strings (lua `print`
calls `tostring` to turn non string values in something printable).
2023-07-26 21:08:56 +02:00
Benedikt Peetz 49c9e90ba6
Feat(treewide): Add a way for Commands to return more than just strings 2023-07-26 21:04:04 +02:00
Benedikt Peetz a4c09c7b42
Fix(app::command_interface): Use new language_macro api 2023-07-26 17:19:57 +02:00
Benedikt Peetz ddd64f9ad3
Build(flake): Switch back to stable 2023-07-26 17:12:33 +02:00
Benedikt Peetz ae7f99632a
Build(flake+Cargo): Update lockfiles 2023-07-26 17:11:52 +02:00
Benedikt Peetz dc5d08f1a3
Refactor(language_macros): Complete rewrite
This **should** have everything the other implementation had, but the
api is implemented in a way, which is more in line with the expectation
raised at the lua functions (being that they are only wrappers over the
command api, and nothing more).

Aside of that, this version is actually documented!
2023-07-26 16:59:05 +02:00
47 changed files with 936 additions and 786 deletions

145
Cargo.lock generated
View File

@ -188,8 +188,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -199,8 +199,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -468,8 +468,8 @@ checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
dependencies = [
"heck",
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -659,7 +659,7 @@ dependencies = [
"fnv",
"ident_case",
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"strsim",
"syn 1.0.109",
]
@ -671,7 +671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
"darling_core",
"quote 1.0.31",
"quote 1.0.32",
"syn 1.0.109",
]
@ -714,7 +714,7 @@ checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
dependencies = [
"darling",
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"syn 1.0.109",
]
@ -761,8 +761,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -811,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"
@ -957,8 +966,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -1358,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"
@ -1401,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.31",
"syn 2.0.26",
]
[[package]]
name = "maplit"
version = "1.0.2"
@ -1646,6 +1655,7 @@ checksum = "07366ed2cd22a3b000aed076e2b68896fb46f06f1f5786c5962da73c0af01577"
dependencies = [
"bstr",
"cc",
"erased-serde",
"futures-core",
"futures-task",
"futures-util",
@ -1653,6 +1663,7 @@ dependencies = [
"once_cell",
"pkg-config",
"rustc-hash",
"serde",
]
[[package]]
@ -1675,9 +1686,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg 1.1.0",
]
@ -1735,8 +1746,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -1850,8 +1861,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -1955,7 +1966,7 @@ dependencies = [
"anyhow",
"itertools",
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"syn 1.0.109",
]
@ -1970,9 +1981,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.31"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
dependencies = [
"proc-macro2 1.0.66",
]
@ -2339,7 +2350,7 @@ dependencies = [
"once_cell",
"proc-macro-crate",
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"ruma-identifiers-validation",
"serde",
"syn 1.0.109",
@ -2388,9 +2399,9 @@ dependencies = [
[[package]]
name = "scopeguard"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
@ -2417,9 +2428,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.174"
version = "1.0.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b88756493a5bd5e5395d53baa70b194b05764ab85b59e43e4b8f4e1192fa9b1"
checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b"
dependencies = [
"serde_derive",
]
@ -2435,13 +2446,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.174"
version = "1.0.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e5c3a298c7f978e53536f95a63bdc4c4a64550582f31a0359a9afda6aede62e"
checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -2493,9 +2504,9 @@ dependencies = [
[[package]]
name = "signal-hook"
version = "0.3.16"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
@ -2607,18 +2618,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.26"
version = "2.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"unicode-ident",
]
@ -2637,22 +2648,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -2706,8 +2717,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -2785,8 +2796,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -2807,7 +2818,7 @@ dependencies = [
"cli-log",
"crossterm",
"indexmap 2.0.0",
"lua_macros",
"language_macros",
"matrix-sdk",
"mlua",
"once_cell",
@ -3033,8 +3044,8 @@ dependencies = [
"log",
"once_cell",
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
"wasm-bindgen-shared",
]
@ -3056,7 +3067,7 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote 1.0.31",
"quote 1.0.32",
"wasm-bindgen-macro-support",
]
@ -3067,8 +3078,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3209,9 +3220,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11"
dependencies = [
"memchr",
]
@ -3253,6 +3264,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]

View File

@ -15,11 +15,11 @@ clap = { version = "4.3.19", features = ["derive"] }
cli-log = "2.0"
anyhow = "1.0"
matrix-sdk = "0.6"
tokio = { version = "1.29", features = ["macros", "rt-multi-thread", "io-std"] }
tokio = { version = "1.29", features = ["macros", "rt-multi-thread"] }
# lua stuff
lua_macros = { path = "./lua_macros" }
mlua = { version = "0.8.9", features = ["lua54", "async", "send"] }
language_macros = { path = "./language_macros" }
mlua = { version = "0.8.9", features = ["lua54", "async", "send", "serialize"] }
once_cell = "1.18.0"
# tui feature specific parts

View File

@ -65,11 +65,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1689449371,
"narHash": "sha256-sK3Oi8uEFrFPL83wKPV6w0+96NrmwqIpw9YFffMifVg=",
"lastModified": 1690327932,
"narHash": "sha256-Fv7PYZxN4eo0K6zXhHG/vOc+e2iuqQ5ywDrh0yeRjP0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "29bcead8405cfe4c00085843eb372cc43837bb9d",
"rev": "a9b47d85504bdd199e90846622c76aa0bfeabfac",
"type": "github"
},
"original": {
@ -98,11 +98,11 @@
]
},
"locked": {
"lastModified": 1689561325,
"narHash": "sha256-+UABrHUXtWJSc9mM7oEKPIYQEhTzUVVNy2IPG9Lfrj0=",
"lastModified": 1690338181,
"narHash": "sha256-Sz2oQ9aNS3MVncnCMndr0302G26UrFUfPynoH2iLjsg=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d8a38aea13c67dc2ce10cff93eb274dcf455753f",
"rev": "b7f0b7b58b3c6f14a1377ec31a3d78b23ab843ec",
"type": "github"
},
"original": {

View File

@ -45,7 +45,7 @@
overlays = [(import rust-overlay)];
};
nightly = true;
nightly = false;
rust =
if nightly
then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)

View File

@ -1,5 +1,5 @@
[package]
name = "lua_macros"
name = "language_macros"
version = "0.1.0"
edition = "2021"

View File

@ -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(<name>)")
.to_string()
.from_case(Case::Snake)
.to_case(Case::Pascal)
);
let input_type: Option<Type> = get_input_type_of_bare_fn_field(field);
match input_type {
Some(input_type) => {
quote! {
#field_name(#input_type),
}
}
None => {
quote! {
#field_name,
}
}
}
}

View File

@ -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<Event>,
) -> 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"
);
}
}
}

View File

@ -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
}
}

View File

@ -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<mlua::Value, mlua::Error> {
#function_body
}
}
}
fn get_and_add_lifetimes_form_inputs_and_outputs<'a>(
input_type: Option<syn::Type>,
return_type: Option<syn::Type>,
) -> Punctuated<Lifetime, Comma> {
fn get_lifetime_args_from_type<'a>(return_type: syn::Type) -> Option<Vec<Lifetime>> {
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<Lifetime, Comma> = 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<Type>) -> TokenStream2 {
let command_name = field
.ident
.as_ref()
.expect("These are named fields, it should be Some(<name>)")
.to_string()
.from_case(Case::Snake)
.to_case(Case::Pascal);
let command_ident = format_ident!("{}", command_name);
let send_output = if has_input {
quote! {
Event::CommandEvent(
Command::#command_ident(input.clone()),
Some(callback_tx),
)
}
} else {
quote! {
Event::CommandEvent(
Command::#command_ident,
Some(callback_tx),
)
}
};
let 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::<CommandTransferValue>();
let tx: core::cell::Ref<tokio::sync::mpsc::Sender<Event>> =
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
}
};
}
}

View File

@ -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<Type> {
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<Type> {
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(<args>) -> <outputs>`"
),
}
}
pub fn get_return_type_of_bare_fn_field(field: &Field) -> Option<Type> {
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(<args>) -> <outputs>`"
),
}
}
pub fn get_bare_fn_return_type(function: &TypeBareFn) -> Option<Type> {
let return_path: &ReturnType = &function.output;
match return_path {
ReturnType::Default => None,
ReturnType::Type(_, return_type) => Some(*return_type.to_owned()),
}
}

View File

@ -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<Event>,
/// ) -> 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<String, mlua::Error> {
/// let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::<String>();
/// let tx: core::cell::Ref<tokio::sync::mpsc::Sender<Event>> = 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()
}

View File

@ -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>) -> 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()
}

View File

@ -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 <String, rlua::Error>)
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::<String>(256);
let tx:
core::cell::Ref<tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>> =
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<Stmt> = 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
}

View File

@ -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<Type> = 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()
}

View File

@ -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<TokenStream2> = 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<crate::app::events::event_types::Event>
)
{
lua.set_app_data(tx);
let globals = lua.globals();
#functions_to_export_in_lua
}
#functions_to_generate
};
gen.into()
}

View File

@ -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<TokenStream2> = 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<String>
) -> Result<String, mlua::Error> {
// 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)
}
}
}

View File

@ -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::*;

View File

@ -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<Mutex<mlua::Lua>> = OnceCell::new();
pub type Table = HashMap<String, CommandTransferValue>;
#[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<String, CommandTransferValue>),
// 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<String>,
}
impl From<String> for CommandTransferValue {
fn from(s: String) -> Self {
Self::String(s.to_owned())
}
}
impl From<f64> for CommandTransferValue {
fn from(s: f64) -> Self {
Self::Number(s.to_owned())
}
}
impl From<i64> for CommandTransferValue {
fn from(s: i64) -> Self {
Self::Integer(s.to_owned())
}
}
impl From<HashMap<String, CommandTransferValue>> for CommandTransferValue {
fn from(s: HashMap<String, CommandTransferValue>) -> Self {
Self::Table(s.to_owned())
}
}
impl From<bool> 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<Event>) -> Self {
info!("Spawning lua code execution thread...");
let (lua_command_tx, mut lua_command_rx) = mpsc::channel::<String>(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<Event>) -> 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::<Value>().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(())
}

View File

@ -0,0 +1,58 @@
// 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(),
/// 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<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

@ -1,14 +1,24 @@
use crate::app::{command_interface::Command, events::event_types::EventStatus, App};
use anyhow::{Context, Result};
use cli_log::{info, warn, trace};
use tokio::sync::mpsc;
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,
};
pub async fn handle(
app: &mut App<'_>,
command: &Command,
output_callback: &Option<mpsc::Sender<String>>,
output_callback: Option<oneshot::Sender<CommandTransferValue>>,
) -> Result<EventStatus> {
// A command returns both _status output_ (what you would normally print to stderr)
// 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.
@ -36,17 +46,15 @@ pub async fn handle(
($str:expr) => {
if let Some(sender) = output_callback {
sender
.send($str.to_owned())
.await
.context("Failed to send command main output")?;
.send(CommandTransferValue::from($str))
.map_err(|e| Error::msg(format!("Failed to send command main output: `{}`", e)))?;
}
};
($str:expr, $($args:ident),+) => {
if let Some(sender) = output_callback {
sender
.send(format!($str, $($args),+))
.await
.context("Failed to send command main output")?;
.send(CommandTransferValue::from(format!($str, $($args),+)))
.map_err(|e| Error::msg(format!("Failed to send command main output: `{}`", e)))?;
}
};
}
@ -59,6 +67,14 @@ pub async fn handle(
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();
send_status_output!("CLI online");
@ -84,22 +100,34 @@ pub async fn handle(
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);
}
send_status_output!("Send message: `{}`", msg);
EventStatus::Ok
}
Command::Greet(name) => {
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
},
}
})
}

View File

@ -5,10 +5,13 @@ use crate::app::{events::event_types::EventStatus, App};
// 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<EventStatus> {
pub async fn handle(
app: &mut App<'_>,
command: String,
) -> Result<EventStatus> {
trace!("Recieved ci command: `{command}`; executing..");
app.lua_command_tx.send(command).await?;
app.lua.execute_code(command).await;
Ok(EventStatus::Ok)
}

View File

@ -10,7 +10,10 @@ use crate::{
ui::central,
};
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
pub async fn handle(
app: &mut App<'_>,
input_event: &CrosstermEvent,
) -> Result<EventStatus> {
match input_event {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
@ -170,9 +173,6 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
.send(Event::LuaCommand(ci_event))
.await
.context("Failed to send lua command to internal event stream")?;
app.tx
.send(Event::CommandEvent(Command::CommandLineHide, None))
.await?;
}
_ => {
app.ui

View File

@ -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<EventStatus> {
pub async fn handle(
app: &mut App<'_>,
sync: &SyncResponse,
) -> Result<EventStatus> {
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,

View File

@ -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<EventStatus> {
pub async fn handle(
app: &mut App<'_>,
input_event: &CrosstermEvent,
) -> Result<EventStatus> {
let ui = match &mut app.ui.setup_ui {
Some(ui) => ui,
None => bail!("SetupUI instance not found"),

View File

@ -3,9 +3,9 @@ mod handlers;
use anyhow::{Context, Result};
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::{Command, lua_command_manager::CommandTransferValue}, status::State, App};
use self::handlers::{command, lua_command, main, matrix, setup};
@ -15,19 +15,19 @@ use super::EventStatus;
pub enum Event {
InputEvent(CrosstermEvent),
MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse),
CommandEvent(Command, Option<Sender<String>>),
CommandEvent(Command, Option<oneshot::Sender<CommandTransferValue>>),
LuaCommand(String),
}
impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> {
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) => command::handle(app, event, callback_tx)
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())
@ -38,10 +38,10 @@ impl Event {
State::None => unreachable!(
"This state should not be available, when we are in the input handling"
),
State::Main => main::handle(app, event)
State::Main => main::handle(app, &event)
.await
.with_context(|| format!("Failed to handle input event: `{:#?}`", event)),
State::Setup => setup::handle(app, event)
State::Setup => setup::handle(app, &event)
.await
.with_context(|| format!("Failed to handle input event: `{:#?}`", event)),
},

View File

@ -2,30 +2,24 @@ pub mod command_interface;
pub mod events;
pub mod status;
use std::{path::Path, thread};
use std::path::Path;
use anyhow::{Context, Error, Result};
use cli_log::{error, info};
use cli_log::info;
use matrix_sdk::Client;
use once_cell::sync::OnceCell;
use status::{State, Status};
use tokio::{
runtime::Builder,
sync::{mpsc, Mutex},
task::{self, LocalSet},
};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::{
accounts::{Account, AccountsManager},
app::{
command_interface::{generate_ci_functions, Command},
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>,
@ -38,76 +32,11 @@ pub struct App<'ui> {
input_listener_killer: CancellationToken,
matrix_listener_killer: CancellationToken,
lua_command_tx: mpsc::Sender<String>,
lua: LuaCommandManager,
}
impl App<'_> {
pub fn new() -> Result<Self> {
fn set_up_lua(event_call_tx: mpsc::Sender<Event>) -> mpsc::Sender<String> {
async fn exec_lua_command(
command: &str,
event_call_tx: mpsc::Sender<Event>,
) -> Result<()> {
let second_event_call_tx = event_call_tx.clone();
let lua = LUA
.get_or_init(|| {
Mutex::new(generate_ci_functions(
mlua::Lua::new(),
second_event_call_tx,
))
})
.lock()
.await;
info!("Recieved command to execute: `{}`", &command);
let output = lua
.load(command)
// FIXME this assumes string output only
.eval_async::<String>()
.await;
match output {
Ok(out) => {
info!("Function `{}` returned: `{}`", command, out);
}
Err(err) => {
error!("Function `{}` returned error: `{}`", command, err);
event_call_tx
.send(Event::CommandEvent(
Command::RaiseError(err.to_string()),
None,
))
.await?;
}
};
Ok(())
}
info!("Setting up Lua context..");
static LUA: OnceCell<Mutex<mlua::Lua>> = OnceCell::new();
let (lua_command_tx, mut rx) = mpsc::channel::<String>(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) = rx.recv().await {
info!("Recieved lua command: {}", &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);
});
lua_command_tx
}
let path: &std::path::Path = Path::new("userdata/accounts.json");
let config = if path.exists() {
info!("Reading account config (userdata/accounts.json)");
@ -127,7 +56,7 @@ impl App<'_> {
input_listener_killer: CancellationToken::new(),
matrix_listener_killer: CancellationToken::new(),
lua_command_tx: set_up_lua(tx),
lua: LuaCommandManager::new(tx),
})
}

View File

@ -1,13 +1,11 @@
//mod app;
//mod ui;
//mod accounts;
mod app;
mod ui;
mod accounts;
mod cli;
mod tui_app;
use clap::Parser;
use crate::cli::{Args, Command};
pub use tui_app::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {

View File

@ -1,49 +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(),
/// Send a message to the current room
/// The send message is interpreted literally.
room_message_send: fn(String) -> String,
}

View File

@ -1,64 +0,0 @@
use cli_log::debug;
#[derive(Debug)]
pub enum Command {
RaiseError(String),
Greet(String),
Exit,
CommandLineShow,
CommandLineHide,
CyclePlanes,
CyclePlanesRev,
RoomMessageSend(String),
Help(Option<String>),
}
pub fn generate_ci_functions(
lua: mlua::Lua,
tx: tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>,
) -> mlua::Lua {
lua.set_app_data(tx);
let globals = lua.globals();
let fun_greet = lua.create_async_function(greet).expect(&{
let res = format!("The function: `{}` should be defined", "greet");
res
});
globals.set("greet", fun_greet).expect(&{
let res = format!(
"Setting a static global value ({}, fun_{}) should work",
"greet", "greet",
);
res
});
drop(globals);
lua
}
async fn greet(lua: &mlua::Lua, input: String) -> Result<String, mlua::Error> {
let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::<String>(256);
let tx: core::cell::Ref<tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>> =
lua.app_data_ref().expect("This exists, it was set before");
debug!("Got tx");
(*tx)
.try_send(crate::app::events::event_types::Event::CommandEvent(
Command::Greet(input.clone()),
Some(callback_tx),
))
.expect("This should work, as the reciever is not dropped");
debug!("Sent CommandEvent");
debug!("Returning output...");
if let Some(output) = callback_rx.recv().await {
callback_rx.close();
debug!("returned output");
return Ok(output);
} else {
debug!("Not returning output...");
return Err(mlua::Error::ExternalError(std::sync::Arc::new(
std::io::Error::new(std::io::ErrorKind::Other, "Callback reciever dropped"),
)));
}
}

View File

@ -1,7 +0,0 @@
pub mod app;
pub mod ui;
pub mod accounts;
//pub use app::*;
//pub use ui::*;
//pub use accounts::*;