From ebb16a20de8e326b01514dae69e2ceba681d0461 Mon Sep 17 00:00:00 2001 From: Soispha Date: Sun, 23 Jul 2023 15:53:31 +0200 Subject: [PATCH 01/18] Feat(treewide): Add a feature based layout and repl subcommand Compiling the whole tui stack, just to debug the lua command line seems counterproductive to me. This allows to just compile the needed parts for a basic lua repl. As of yet the repl is just a mock-up, as the event handling can, as of right now, not easily be separated from the tui. To activate specific features add specify the on the cargo command line like this: ``` cargo run --features "cli tui" ``` or add them to the `default` feature set in the `Cargo.toml`. --- Cargo.lock | 205 ++++++++++++++---- Cargo.toml | 33 ++- src/cli.rs | 23 ++ src/main.rs | 40 +++- src/repl/mod.rs | 41 ++++ src/{ => tui_app}/accounts/mod.rs | 0 src/{ => tui_app}/app/command_interface.rs | 0 .../event_types/event/handlers/command.rs | 0 .../event_types/event/handlers/lua_command.rs | 0 .../events/event_types/event/handlers/main.rs | 0 .../event_types/event/handlers/matrix.rs | 0 .../events/event_types/event/handlers/mod.rs | 0 .../event_types/event/handlers/setup.rs | 0 .../app/events/event_types/event/mod.rs | 0 .../app/events/event_types/event_status.rs | 0 .../app/events/event_types/mod.rs | 0 src/{ => tui_app}/app/events/mod.rs | 0 src/{ => tui_app}/app/mod.rs | 50 ++++- src/{ => tui_app}/app/status.rs | 0 src/tui_app/mod.rs | 7 + src/{ => tui_app}/ui/central/mod.rs | 0 src/{ => tui_app}/ui/central/update/mod.rs | 0 .../ui/central/update/widgets/messages.rs | 0 .../ui/central/update/widgets/mod.rs | 0 .../ui/central/update/widgets/room_info.rs | 0 .../ui/central/update/widgets/rooms.rs | 0 .../ui/central/update/widgets/status.rs | 0 src/{ => tui_app}/ui/mod.rs | 0 src/{ => tui_app}/ui/setup.rs | 0 29 files changed, 326 insertions(+), 73 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/repl/mod.rs rename src/{ => tui_app}/accounts/mod.rs (100%) rename src/{ => tui_app}/app/command_interface.rs (100%) rename src/{ => tui_app}/app/events/event_types/event/handlers/command.rs (100%) rename src/{ => tui_app}/app/events/event_types/event/handlers/lua_command.rs (100%) rename src/{ => tui_app}/app/events/event_types/event/handlers/main.rs (100%) rename src/{ => tui_app}/app/events/event_types/event/handlers/matrix.rs (100%) rename src/{ => tui_app}/app/events/event_types/event/handlers/mod.rs (100%) rename src/{ => tui_app}/app/events/event_types/event/handlers/setup.rs (100%) rename src/{ => tui_app}/app/events/event_types/event/mod.rs (100%) rename src/{ => tui_app}/app/events/event_types/event_status.rs (100%) rename src/{ => tui_app}/app/events/event_types/mod.rs (100%) rename src/{ => tui_app}/app/events/mod.rs (100%) rename src/{ => tui_app}/app/mod.rs (76%) rename src/{ => tui_app}/app/status.rs (100%) create mode 100644 src/tui_app/mod.rs rename src/{ => tui_app}/ui/central/mod.rs (100%) rename src/{ => tui_app}/ui/central/update/mod.rs (100%) rename src/{ => tui_app}/ui/central/update/widgets/messages.rs (100%) rename src/{ => tui_app}/ui/central/update/widgets/mod.rs (100%) rename src/{ => tui_app}/ui/central/update/widgets/room_info.rs (100%) rename src/{ => tui_app}/ui/central/update/widgets/rooms.rs (100%) rename src/{ => tui_app}/ui/central/update/widgets/status.rs (100%) rename src/{ => tui_app}/ui/mod.rs (100%) rename src/{ => tui_app}/ui/setup.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index c5ca408..fe623ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.72" @@ -145,9 +194,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.71" +version = "0.1.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2 1.0.66", "quote 1.0.31", @@ -228,6 +277,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + [[package]] name = "blake3" version = "1.4.1" @@ -382,6 +437,47 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + [[package]] name = "cli-log" version = "2.0.0" @@ -400,9 +496,15 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "const-oid" version = "0.7.1" @@ -486,7 +588,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crossterm_winapi", "libc", "mio", @@ -690,9 +792,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" @@ -738,12 +840,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "file-size" @@ -1003,6 +1102,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.2" @@ -1194,23 +1299,23 @@ dependencies = [ "web-sys", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - [[package]] name = "ipnet" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1267,9 +1372,9 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "lock_api" @@ -1614,7 +1719,7 @@ version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -2064,7 +2169,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2073,7 +2178,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2199,7 +2304,7 @@ dependencies = [ "thiserror", "tracing", "url", - "uuid 1.4.0", + "uuid 1.4.1", "wildmatch", ] @@ -2255,13 +2360,12 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.23" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ - "bitflags", + "bitflags 2.3.3", "errno", - "io-lifetimes", "libc", "linux-raw-sys", "windows-sys", @@ -2290,11 +2394,11 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -2303,9 +2407,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -2313,9 +2417,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.171" +version = "1.0.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "3b88756493a5bd5e5395d53baa70b194b05764ab85b59e43e4b8f4e1192fa9b1" dependencies = [ "serde_derive", ] @@ -2331,9 +2435,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "6e5c3a298c7f978e53536f95a63bdc4c4a64550582f31a0359a9afda6aede62e" dependencies = [ "proc-macro2 1.0.66", "quote 1.0.31", @@ -2520,11 +2624,10 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" dependencies = [ - "autocfg 1.1.0", "cfg-if", "fastrand", "redox_syscall 0.3.5", @@ -2700,12 +2803,14 @@ name = "trinitrix" version = "0.1.0" dependencies = [ "anyhow", + "clap", "cli-log", "crossterm", "indexmap 2.0.0", "lua_macros", "matrix-sdk", "mlua", + "once_cell", "serde", "tokio", "tokio-util", @@ -2725,7 +2830,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cassowary", "crossterm", "unicode-segmentation", @@ -2808,6 +2913,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "0.7.4" @@ -2828,9 +2939,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ "getrandom 0.2.10", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 0d6971f..c28dc41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,16 +6,29 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["cli"] +full = ["cli", "tui"] + +cli = ["tokio/io-std"] +tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"] + [dependencies] -lua_macros = { path = "./lua_macros" } -tui = "0.19" -tui-textarea = { version = "0.2", features = ["crossterm"] } -crossterm = "0.25" -matrix-sdk = "0.6" -anyhow = "1.0" -tokio = { version = "1.29", features = ["macros", "rt-multi-thread"] } -tokio-util = "0.7" -serde = "1.0" +clap = { version = "4.3.19", features = ["derive"] } cli-log = "2.0" -indexmap = "2.0.0" +anyhow = "1.0" +matrix-sdk = "0.6" +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"] } +once_cell = "1.18.0" + +# tui feature specific parts +tui = {version = "0.19", optional = true} +tui-textarea = { version = "0.2", features = ["crossterm"], optional = true } +crossterm = { version = "0.25", optional = true } +tokio-util = { version = "0.7", optional = true } +serde = { version = "1.0", optional = true } +indexmap = { version = "2.0.0", optional = true } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..9717a22 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,23 @@ +use clap::{Parser, Subcommand}; + +// TODO: The description could be better +/// A terminal client for the matrix chat protocol +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Args { + #[command(subcommand)] + /// The subcommand to execute, default is start + pub subcommand: Option, +} +#[derive(Subcommand, Debug)] +pub enum Command { + /// Starts a repl, for the lua interface + #[cfg(feature = "cli")] + #[clap(value_parser)] + Repl {}, + + /// Starts the main tui client + #[cfg(feature = "tui")] + #[clap(value_parser)] + Start {}, +} diff --git a/src/main.rs b/src/main.rs index 3ef9611..f613497 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,43 @@ -mod accounts; -mod app; -mod ui; +mod cli; +pub mod event_handler; + +#[cfg(feature = "tui")] +mod tui_app; + +#[cfg(feature = "cli")] +mod repl; + +use clap::Parser; + +use crate::cli::{Args, Command}; + +#[cfg(feature = "tui")] +pub use tui_app::*; #[tokio::main] async fn main() -> anyhow::Result<()> { cli_log::init_cli_log!(); - let mut app = app::App::new()?; - app.run().await?; + let args = Args::parse(); + let command = args.subcommand.unwrap_or( + #[cfg(all(feature = "tui", not(feature = "cli")))] + Command::Start {}, + #[cfg(all(feature = "cli", not(feature = "tui")))] + Command::Repl {}, + #[cfg(all(feature = "cli", feature = "tui"))] + Command::Start {}, + ); + match command { + #[cfg(feature = "cli")] + Command::Repl {} => { + repl::run().await?; + } + #[cfg(feature = "tui")] + Command::Start {} => { + let mut app = app::App::new()?; + app.run().await?; + } + }; Ok(()) } diff --git a/src/repl/mod.rs b/src/repl/mod.rs new file mode 100644 index 0000000..9dacdce --- /dev/null +++ b/src/repl/mod.rs @@ -0,0 +1,41 @@ +use std::io::ErrorKind; + +use anyhow::{Context, Result}; +use cli_log::{info, warn}; +use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt}; + +pub async fn run() -> Result<()> { + let mut stdin = stdin(); + let mut stdout = stdout(); + let mut buffer = [0; 1]; + let mut new_command = vec![]; + + loop { + stdout + .write("trinitrix:> ".as_bytes()) + .await + .context("Failed to write prompt")?; + stdout.flush().await.context("Failed to flush prompt")?; + new_command.clear(); + loop { + if let Err(err) = stdin.read_exact(&mut buffer).await { + if err.kind() == ErrorKind::UnexpectedEof { + warn!("Unexpected EOF, we assume the user quit."); + return Ok(()); + } else { + Err(err).context("Failed to read next character")?; + } + } + if buffer == "\n".as_bytes() { + break; + } else { + new_command.append(&mut buffer.to_vec()); + } + } + info!( + "Got user repl input: {}", + String::from_utf8(new_command.clone()) + .context("Failed to convert user input to utf8 string")? + ) + } +} diff --git a/src/accounts/mod.rs b/src/tui_app/accounts/mod.rs similarity index 100% rename from src/accounts/mod.rs rename to src/tui_app/accounts/mod.rs diff --git a/src/app/command_interface.rs b/src/tui_app/app/command_interface.rs similarity index 100% rename from src/app/command_interface.rs rename to src/tui_app/app/command_interface.rs diff --git a/src/app/events/event_types/event/handlers/command.rs b/src/tui_app/app/events/event_types/event/handlers/command.rs similarity index 100% rename from src/app/events/event_types/event/handlers/command.rs rename to src/tui_app/app/events/event_types/event/handlers/command.rs diff --git a/src/app/events/event_types/event/handlers/lua_command.rs b/src/tui_app/app/events/event_types/event/handlers/lua_command.rs similarity index 100% rename from src/app/events/event_types/event/handlers/lua_command.rs rename to src/tui_app/app/events/event_types/event/handlers/lua_command.rs diff --git a/src/app/events/event_types/event/handlers/main.rs b/src/tui_app/app/events/event_types/event/handlers/main.rs similarity index 100% rename from src/app/events/event_types/event/handlers/main.rs rename to src/tui_app/app/events/event_types/event/handlers/main.rs diff --git a/src/app/events/event_types/event/handlers/matrix.rs b/src/tui_app/app/events/event_types/event/handlers/matrix.rs similarity index 100% rename from src/app/events/event_types/event/handlers/matrix.rs rename to src/tui_app/app/events/event_types/event/handlers/matrix.rs diff --git a/src/app/events/event_types/event/handlers/mod.rs b/src/tui_app/app/events/event_types/event/handlers/mod.rs similarity index 100% rename from src/app/events/event_types/event/handlers/mod.rs rename to src/tui_app/app/events/event_types/event/handlers/mod.rs diff --git a/src/app/events/event_types/event/handlers/setup.rs b/src/tui_app/app/events/event_types/event/handlers/setup.rs similarity index 100% rename from src/app/events/event_types/event/handlers/setup.rs rename to src/tui_app/app/events/event_types/event/handlers/setup.rs diff --git a/src/app/events/event_types/event/mod.rs b/src/tui_app/app/events/event_types/event/mod.rs similarity index 100% rename from src/app/events/event_types/event/mod.rs rename to src/tui_app/app/events/event_types/event/mod.rs diff --git a/src/app/events/event_types/event_status.rs b/src/tui_app/app/events/event_types/event_status.rs similarity index 100% rename from src/app/events/event_types/event_status.rs rename to src/tui_app/app/events/event_types/event_status.rs diff --git a/src/app/events/event_types/mod.rs b/src/tui_app/app/events/event_types/mod.rs similarity index 100% rename from src/app/events/event_types/mod.rs rename to src/tui_app/app/events/event_types/mod.rs diff --git a/src/app/events/mod.rs b/src/tui_app/app/events/mod.rs similarity index 100% rename from src/app/events/mod.rs rename to src/tui_app/app/events/mod.rs diff --git a/src/app/mod.rs b/src/tui_app/app/mod.rs similarity index 76% rename from src/app/mod.rs rename to src/tui_app/app/mod.rs index 18da6aa..65426fb 100644 --- a/src/app/mod.rs +++ b/src/tui_app/app/mod.rs @@ -2,14 +2,17 @@ pub mod command_interface; pub mod events; pub mod status; -use std::{path::Path, sync::Arc}; +use std::path::Path; use anyhow::{Context, Error, Result}; use cli_log::info; use matrix_sdk::Client; -use mlua::Lua; +use once_cell::sync::OnceCell; use status::{State, Status}; -use tokio::sync::mpsc; +use tokio::{ + sync::{mpsc, Mutex}, + task::LocalSet, +}; use tokio_util::sync::CancellationToken; use crate::{ @@ -31,16 +34,42 @@ pub struct App<'ui> { input_listener_killer: CancellationToken, matrix_listener_killer: CancellationToken, - lua: Arc, + lua_command_tx: mpsc::Sender, } impl App<'_> { pub fn new() -> Result { - fn set_up_lua(tx: mpsc::Sender) -> Arc { - let mut lua = Lua::new(); + fn set_up_lua(tx: mpsc::Sender) -> mpsc::Sender { + info!("Setting up Lua context.."); + static LUA: OnceCell> = OnceCell::new(); - generate_ci_functions(&mut lua, tx); - Arc::new(lua) + let (lua_command_tx, mut rx) = mpsc::channel(256); + + let local_set = LocalSet::new(); + local_set.spawn_local(async move { + let lua = LUA + .get_or_init(|| Mutex::new(generate_ci_functions(mlua::Lua::new(), tx))) + .lock() + .await; + info!("Initialized Lua context"); + + while let Some(command) = rx.recv().await { + info!("Recieved command to execute: `{}`", &command); + let output = lua + .load(&command) + // FIXME this assumes string output only + .eval_async::() + .await + .with_context(|| format!("Failed to execute: `{command}`")); + info!( + "Function `{}` returned: `{}`", + command, + output.unwrap_or("".to_owned()) + ); + } + }); + + lua_command_tx } let path: &std::path::Path = Path::new("userdata/accounts.json"); @@ -62,7 +91,7 @@ impl App<'_> { input_listener_killer: CancellationToken::new(), matrix_listener_killer: CancellationToken::new(), - lua: set_up_lua(tx), + lua_command_tx: set_up_lua(tx), }) } @@ -87,11 +116,10 @@ impl App<'_> { self.ui.update(&self.status).await?; let event = self.rx.recv().await.context("Failed to get next event")?; - match event.handle(self).await? { event_types::EventStatus::Ok => (), event_types::EventStatus::Terminate => break, - _ => (), + _ => todo!(), }; } diff --git a/src/app/status.rs b/src/tui_app/app/status.rs similarity index 100% rename from src/app/status.rs rename to src/tui_app/app/status.rs diff --git a/src/tui_app/mod.rs b/src/tui_app/mod.rs new file mode 100644 index 0000000..c0d91db --- /dev/null +++ b/src/tui_app/mod.rs @@ -0,0 +1,7 @@ +pub mod app; +pub mod ui; +pub mod accounts; + +pub use app::*; +pub use ui::*; +pub use accounts::*; diff --git a/src/ui/central/mod.rs b/src/tui_app/ui/central/mod.rs similarity index 100% rename from src/ui/central/mod.rs rename to src/tui_app/ui/central/mod.rs diff --git a/src/ui/central/update/mod.rs b/src/tui_app/ui/central/update/mod.rs similarity index 100% rename from src/ui/central/update/mod.rs rename to src/tui_app/ui/central/update/mod.rs diff --git a/src/ui/central/update/widgets/messages.rs b/src/tui_app/ui/central/update/widgets/messages.rs similarity index 100% rename from src/ui/central/update/widgets/messages.rs rename to src/tui_app/ui/central/update/widgets/messages.rs diff --git a/src/ui/central/update/widgets/mod.rs b/src/tui_app/ui/central/update/widgets/mod.rs similarity index 100% rename from src/ui/central/update/widgets/mod.rs rename to src/tui_app/ui/central/update/widgets/mod.rs diff --git a/src/ui/central/update/widgets/room_info.rs b/src/tui_app/ui/central/update/widgets/room_info.rs similarity index 100% rename from src/ui/central/update/widgets/room_info.rs rename to src/tui_app/ui/central/update/widgets/room_info.rs diff --git a/src/ui/central/update/widgets/rooms.rs b/src/tui_app/ui/central/update/widgets/rooms.rs similarity index 100% rename from src/ui/central/update/widgets/rooms.rs rename to src/tui_app/ui/central/update/widgets/rooms.rs diff --git a/src/ui/central/update/widgets/status.rs b/src/tui_app/ui/central/update/widgets/status.rs similarity index 100% rename from src/ui/central/update/widgets/status.rs rename to src/tui_app/ui/central/update/widgets/status.rs diff --git a/src/ui/mod.rs b/src/tui_app/ui/mod.rs similarity index 100% rename from src/ui/mod.rs rename to src/tui_app/ui/mod.rs diff --git a/src/ui/setup.rs b/src/tui_app/ui/setup.rs similarity index 100% rename from src/ui/setup.rs rename to src/tui_app/ui/setup.rs From 189ae509f8c15edb97c789a646a296dda34d368c Mon Sep 17 00:00:00 2001 From: Soispha Date: Sun, 23 Jul 2023 16:35:18 +0200 Subject: [PATCH 02/18] Fix(app::command_interface): Provide pre-generated file, to check syntax --- .../app/command_interface.lua_macros.rs | 54 ++++++++ src/tui_app/app/command_interface.rs | 116 +++++++++--------- 2 files changed, 115 insertions(+), 55 deletions(-) create mode 100644 src/tui_app/app/command_interface.lua_macros.rs diff --git a/src/tui_app/app/command_interface.lua_macros.rs b/src/tui_app/app/command_interface.lua_macros.rs new file mode 100644 index 0000000..ea1cad7 --- /dev/null +++ b/src/tui_app/app/command_interface.lua_macros.rs @@ -0,0 +1,54 @@ +// FIXME: This file needs documentation with examples of how the proc macros work. +// for now use `cargo expand app::command_interface` for an overview + +use std::{io::{Error, ErrorKind}, sync::Arc}; + +use lua_macros::{ci_command, turn_struct_to_ci_command_enum}; + +use crate::app::event_types::Event; +/// This struct is here to guarantee, that all functions actually end up in the lua context. +/// I.e. Rust should throw a compile error, when one field is added, but not a matching function. +/// +/// What it does: +/// - Generates a `generate_ci_functions` function, which wraps the specified rust in functions +/// in lua and exports them to the globals in the context provided as argument. +/// - Generates a Commands enum, which contains every Camel cased version of the fields. +/// +/// Every command specified here should have a function named $command_name, where $command_name is the snake cased name of the field. +/// +/// This function is exported to the lua context, thus it's signature must be: +/// ```rust +/// fn $command_name(context: Context, input_string: String) -> Result<$return_type, rlua::Error> {} +/// ``` +/// where $return_type is the type returned by the function (the only supported ones are right now +/// `String` and `()`). + +#[turn_struct_to_ci_command_enum] +struct Commands { + /// Greets the user + greet: fn(String) -> String, + + /// Closes the application + //#[expose(lua)] + exit: fn(), + + /// Shows the command line + command_line_show: fn(), + + /// Hides the command line + command_line_hide: fn(), + + /// Go to the next plane + cycle_planes: fn(), + /// Go to the previous plane + cycle_planes_rev: fn(), + + /// Sets the current app mode to Normal / navigation mode + set_mode_normal: fn(), + /// Sets the current app mode to Insert / editing mode + set_mode_insert: fn(), + + /// Send a message to the current room + /// The send message is interpreted literally. + room_message_send: fn(String) -> String, +} diff --git a/src/tui_app/app/command_interface.rs b/src/tui_app/app/command_interface.rs index e475113..59a9740 100644 --- a/src/tui_app/app/command_interface.rs +++ b/src/tui_app/app/command_interface.rs @@ -1,57 +1,63 @@ -// 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 cli_log::debug; -use std::{ - io::{Error, ErrorKind}, - sync::Arc, -}; - -use lua_macros::{ci_command, turn_struct_to_ci_command_enum}; - -use crate::app::event_types::Event; -/// This struct is here to guarantee, that all functions actually end up in the lua context. -/// I.e. Rust should throw a compile error, when one field is added, but not a matching function. -/// -/// What it does: -/// - Generates a `generate_ci_functions` function, which wraps the specified rust in functions -/// in lua and exports them to the globals in the context provided as argument. -/// - Generates a Commands enum, which contains every Camel cased version of the fields. -/// -/// Every command specified here should have a function named $command_name, where $command_name is the snake cased name of the field. -/// -/// This function is exported to the lua context, thus it's signature must be: -/// ```rust -/// fn $command_name(context: Context, input_string: String) -> Result<$return_type, rlua::Error> {} -/// ``` -/// where $return_type is the type returned by the function (the only supported ones are right now -/// `String` and `()`). - -#[turn_struct_to_ci_command_enum] -struct Commands { - /// Greets the user - greet: fn(String) -> String, - - /// Closes the application - //#[expose(lua)] - exit: fn(), - - /// Shows the command line - command_line_show: fn(), - - /// Hides the command line - command_line_hide: fn(), - - /// Go to the next plane - cycle_planes: fn(), - /// Go to the previous plane - cycle_planes_rev: fn(), - - /// Sets the current app mode to Normal / navigation mode - set_mode_normal: fn(), - /// Sets the current app mode to Insert / editing mode - set_mode_insert: fn(), - - /// Send a message to the current room - /// The send message is interpreted literally. - room_message_send: fn(String) -> String, +#[derive(Debug)] +pub enum Command { + Greet(String), + Exit, + CommandLineShow, + CommandLineHide, + CyclePlanes, + CyclePlanesRev, + RoomMessageSend(String), + Help(Option), +} + +pub fn generate_ci_functions( + lua: mlua::Lua, + tx: tokio::sync::mpsc::Sender, +) -> 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 { + let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::(256); + let tx: core::cell::Ref> = + lua.app_data_ref().expect("This exists, it was set before"); + + 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"), + ))); + } } From c7a4d5a8ab51b60e7add730c110ec70f9da62f27 Mon Sep 17 00:00:00 2001 From: Soispha Date: Mon, 24 Jul 2023 23:38:16 +0200 Subject: [PATCH 03/18] Refactor(treewide): Remove the repl, reuse of e. handling is hard The event handling is deeply ingrained in the ui code, the commands are focused around the ui code, in short splitting of the event handling and command system from the ui is intentionally hard and in my opinion not really worth it right now. --- Cargo.toml | 7 ++----- src/cli.rs | 6 ------ src/main.rs | 25 ++++--------------------- src/repl/mod.rs | 41 ----------------------------------------- src/tui_app/mod.rs | 6 +++--- 5 files changed, 9 insertions(+), 76 deletions(-) delete mode 100644 src/repl/mod.rs diff --git a/Cargo.toml b/Cargo.toml index c28dc41..a60ace5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,7 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["cli"] -full = ["cli", "tui"] - -cli = ["tokio/io-std"] +default = ["tui"] tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"] [dependencies] @@ -18,7 +15,7 @@ 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"] } +tokio = { version = "1.29", features = ["macros", "rt-multi-thread", "io-std"] } # lua stuff lua_macros = { path = "./lua_macros" } diff --git a/src/cli.rs b/src/cli.rs index 9717a22..e23f01e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,13 +11,7 @@ pub struct Args { } #[derive(Subcommand, Debug)] pub enum Command { - /// Starts a repl, for the lua interface - #[cfg(feature = "cli")] - #[clap(value_parser)] - Repl {}, - /// Starts the main tui client - #[cfg(feature = "tui")] #[clap(value_parser)] Start {}, } diff --git a/src/main.rs b/src/main.rs index f613497..b14a469 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,12 @@ +//mod app; +//mod ui; +//mod accounts; mod cli; -pub mod event_handler; - -#[cfg(feature = "tui")] mod tui_app; -#[cfg(feature = "cli")] -mod repl; - use clap::Parser; use crate::cli::{Args, Command}; - -#[cfg(feature = "tui")] pub use tui_app::*; #[tokio::main] @@ -19,20 +14,8 @@ async fn main() -> anyhow::Result<()> { cli_log::init_cli_log!(); let args = Args::parse(); - let command = args.subcommand.unwrap_or( - #[cfg(all(feature = "tui", not(feature = "cli")))] - Command::Start {}, - #[cfg(all(feature = "cli", not(feature = "tui")))] - Command::Repl {}, - #[cfg(all(feature = "cli", feature = "tui"))] - Command::Start {}, - ); + let command = args.subcommand.unwrap_or(Command::Start {}); match command { - #[cfg(feature = "cli")] - Command::Repl {} => { - repl::run().await?; - } - #[cfg(feature = "tui")] Command::Start {} => { let mut app = app::App::new()?; app.run().await?; diff --git a/src/repl/mod.rs b/src/repl/mod.rs deleted file mode 100644 index 9dacdce..0000000 --- a/src/repl/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::io::ErrorKind; - -use anyhow::{Context, Result}; -use cli_log::{info, warn}; -use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt}; - -pub async fn run() -> Result<()> { - let mut stdin = stdin(); - let mut stdout = stdout(); - let mut buffer = [0; 1]; - let mut new_command = vec![]; - - loop { - stdout - .write("trinitrix:> ".as_bytes()) - .await - .context("Failed to write prompt")?; - stdout.flush().await.context("Failed to flush prompt")?; - new_command.clear(); - loop { - if let Err(err) = stdin.read_exact(&mut buffer).await { - if err.kind() == ErrorKind::UnexpectedEof { - warn!("Unexpected EOF, we assume the user quit."); - return Ok(()); - } else { - Err(err).context("Failed to read next character")?; - } - } - if buffer == "\n".as_bytes() { - break; - } else { - new_command.append(&mut buffer.to_vec()); - } - } - info!( - "Got user repl input: {}", - String::from_utf8(new_command.clone()) - .context("Failed to convert user input to utf8 string")? - ) - } -} diff --git a/src/tui_app/mod.rs b/src/tui_app/mod.rs index c0d91db..8da28d6 100644 --- a/src/tui_app/mod.rs +++ b/src/tui_app/mod.rs @@ -2,6 +2,6 @@ pub mod app; pub mod ui; pub mod accounts; -pub use app::*; -pub use ui::*; -pub use accounts::*; +//pub use app::*; +//pub use ui::*; +//pub use accounts::*; From 1fe04ca5c68c10d84045447b909bcb657d626290 Mon Sep 17 00:00:00 2001 From: Soispha Date: Mon, 24 Jul 2023 23:45:44 +0200 Subject: [PATCH 04/18] Fix(lua_command::handle): Move lua_command handler to separate thread This makes it possible to have lua code execute commands and receive their output value, without risking a deadlock. --- .../event_types/event/handlers/lua_command.rs | 36 ++------ src/tui_app/app/mod.rs | 90 +++++++++++++------ 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/src/tui_app/app/events/event_types/event/handlers/lua_command.rs b/src/tui_app/app/events/event_types/event/handlers/lua_command.rs index e22833d..6e7460e 100644 --- a/src/tui_app/app/events/event_types/event/handlers/lua_command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/lua_command.rs @@ -1,38 +1,14 @@ -use std::{sync::Arc, time::Duration}; - -use anyhow::{Context, Result}; -use cli_log::{debug, info}; -use tokio::{task, time::timeout}; +use anyhow::Result; +use cli_log::trace; use crate::app::{events::event_types::EventStatus, App}; +// 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 { - info!("Recieved ci command: `{command}`; executing.."); + trace!("Recieved ci command: `{command}`; executing.."); - let local = task::LocalSet::new(); - - // Run the local task set. - let output = local - .run_until(async move { - let lua = Arc::clone(&app.lua); - debug!("before_handle"); - let c_handle = task::spawn_local(async move { - lua.load(&command) - // FIXME this assumes string output only - .eval_async::() - .await - .with_context(|| format!("Failed to execute: `{command}`")) - }); - debug!("after_handle"); - c_handle - }) - .await; - debug!("after_thread"); - - let output = timeout(Duration::from_secs(10), output) - .await - .context("Failed to join lua command executor")???; - info!("Command returned: `{}`", output); + app.lua_command_tx.send(command).await?; Ok(EventStatus::Ok) } diff --git a/src/tui_app/app/mod.rs b/src/tui_app/app/mod.rs index 65426fb..f70f91f 100644 --- a/src/tui_app/app/mod.rs +++ b/src/tui_app/app/mod.rs @@ -2,22 +2,26 @@ pub mod command_interface; pub mod events; pub mod status; -use std::path::Path; +use std::{path::Path, thread}; use anyhow::{Context, Error, Result}; -use cli_log::info; +use cli_log::{error, info}; use matrix_sdk::Client; use once_cell::sync::OnceCell; use status::{State, Status}; use tokio::{ + runtime::Builder, sync::{mpsc, Mutex}, - task::LocalSet, + task::{self, LocalSet}, }; use tokio_util::sync::CancellationToken; use crate::{ accounts::{Account, AccountsManager}, - app::{command_interface::generate_ci_functions, events::event_types::Event}, + app::{ + command_interface::{generate_ci_functions, Command}, + events::event_types::Event, + }, ui::{central, setup}, }; @@ -39,34 +43,66 @@ pub struct App<'ui> { impl App<'_> { pub fn new() -> Result { - fn set_up_lua(tx: mpsc::Sender) -> mpsc::Sender { + fn set_up_lua(event_call_tx: mpsc::Sender) -> mpsc::Sender { + async fn exec_lua_command( + command: &str, + event_call_tx: mpsc::Sender, + ) -> Result<()> { + let second_event_call_tx = event_call_tx.clone(); + let lua = LUA + .get_or_init(|| { + Mutex::new(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::() + .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> = OnceCell::new(); - let (lua_command_tx, mut rx) = mpsc::channel(256); + let (lua_command_tx, mut rx) = mpsc::channel::(256); - let local_set = LocalSet::new(); - local_set.spawn_local(async move { - let lua = LUA - .get_or_init(|| Mutex::new(generate_ci_functions(mlua::Lua::new(), tx))) - .lock() - .await; - info!("Initialized Lua context"); + 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(); - while let Some(command) = rx.recv().await { - info!("Recieved command to execute: `{}`", &command); - let output = lua - .load(&command) - // FIXME this assumes string output only - .eval_async::() - .await - .with_context(|| format!("Failed to execute: `{command}`")); - info!( - "Function `{}` returned: `{}`", - command, - output.unwrap_or("".to_owned()) - ); - } + 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 From 0288bdb0ad824e4c62ccd6e3011637c18636dde6 Mon Sep 17 00:00:00 2001 From: Soispha Date: Mon, 24 Jul 2023 23:53:21 +0200 Subject: [PATCH 05/18] Feat(ui): Add status panel, which shows command statuses and errors --- src/tui_app/app/command_interface.rs | 1 + .../event_types/event/handlers/command.rs | 98 +++++++++----- .../app/events/event_types/event/mod.rs | 20 +-- src/tui_app/app/status.rs | 50 ++++++- src/tui_app/ui/central/mod.rs | 124 +++++++++++++++++- src/tui_app/ui/central/update/mod.rs | 103 ++------------- .../central/update/widgets/command_monitor.rs | 34 +++++ src/tui_app/ui/central/update/widgets/mod.rs | 1 + 8 files changed, 282 insertions(+), 149 deletions(-) create mode 100644 src/tui_app/ui/central/update/widgets/command_monitor.rs diff --git a/src/tui_app/app/command_interface.rs b/src/tui_app/app/command_interface.rs index 59a9740..610c28c 100644 --- a/src/tui_app/app/command_interface.rs +++ b/src/tui_app/app/command_interface.rs @@ -2,6 +2,7 @@ use cli_log::debug; #[derive(Debug)] pub enum Command { + RaiseError(String), Greet(String), Exit, CommandLineShow, diff --git a/src/tui_app/app/events/event_types/event/handlers/command.rs b/src/tui_app/app/events/event_types/event/handlers/command.rs index 4a3f9dc..241a533 100644 --- a/src/tui_app/app/events/event_types/event/handlers/command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/command.rs @@ -1,55 +1,84 @@ -use crate::{ - app::{command_interface::Command, events::event_types::EventStatus, status::State, App}, - ui::central::InputPosition, -}; -use anyhow::Result; -use cli_log::info; +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; pub async fn handle( app: &mut App<'_>, command: &Command, - send_output: bool, -) -> Result<(EventStatus, String)> { - macro_rules! set_status_output { + output_callback: &Option>, +) -> Result { + // A command returns both _status output_ (what you would normally print to stderr) + // and _main output_ (the output which is normally printed to stdout). + // We simulate these by returning the main output to the lua function, and printing the + // status output to a status ui field. + // + // Every function should return some status output to show the user, that something is + // happening, while only some functions return some value to the main output, as this + // is reserved for functions called only for their output (for example `greet()`). + macro_rules! send_status_output { ($str:expr) => { - if send_output { - app.ui.set_command_output($str); + app.status.add_status_message($str.to_owned()); + }; + ($str:expr, $($args:ident),+) => { + app.status.add_status_message(format!($str, $($args),+)); + }; + } + macro_rules! send_error_output { + ($str:expr) => { + app.status.add_error_message($str.to_owned()); + }; + ($str:expr, $($args:ident),+) => { + app.status.add_error_message(format!($str, $($args),+)); + }; + } + macro_rules! send_main_output { + ($str:expr) => { + if let Some(sender) = output_callback { + sender + .send($str.to_owned()) + .await + .context("Failed to send command main output")?; } }; ($str:expr, $($args:ident),+) => { - if send_output { - app.ui.set_command_output(&format!($str, $($args),+)); + if let Some(sender) = output_callback { + sender + .send(format!($str, $($args),+)) + .await + .context("Failed to send command main output")?; } }; } - info!("Handling command: {:#?}", command); + + trace!("Handling command: {:#?}", command); Ok(match command { - Command::Exit => ( - EventStatus::Terminate, - "Terminated the application".to_owned(), - ), + Command::Exit => { + send_status_output!("Terminating the application.."); + EventStatus::Terminate + } Command::CommandLineShow => { app.ui.cli_enable(); - set_status_output!("CLI online"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("CLI online"); + EventStatus::Ok } Command::CommandLineHide => { app.ui.cli_disable(); - set_status_output!("CLI offline"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("CLI offline"); + EventStatus::Ok } Command::CyclePlanes => { app.ui.cycle_main_input_position(); - set_status_output!("Switched main input position"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Switched main input position"); + EventStatus::Ok } Command::CyclePlanesRev => { app.ui.cycle_main_input_position_rev(); - set_status_output!("Switched main input position; reversed"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Switched main input position; reversed"); + EventStatus::Ok } Command::SetModeNormal => { @@ -67,15 +96,22 @@ pub async fn handle( Command::RoomMessageSend(msg) => { if let Some(room) = app.status.room_mut() { room.send(msg.clone()).await?; + } else { + // TODO(@Soispha): Should this raise a lua error? It could be very confusing, + // when a user doesn't read the log. + warn!("Can't send message: `{}`, as there is no open room!", &msg); } - set_status_output!("Send message: `{}`", msg); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Send message: `{}`", msg); + EventStatus::Ok } Command::Greet(name) => { - info!("Greated {}", name); - set_status_output!("Hi, {}!", name); - (EventStatus::Ok, "".to_owned()) + send_main_output!("Hi, {}!", name); + EventStatus::Ok } Command::Help(_) => todo!(), + Command::RaiseError(err) => { + send_error_output!(err); + EventStatus::Ok + }, }) } diff --git a/src/tui_app/app/events/event_types/event/mod.rs b/src/tui_app/app/events/event_types/event/mod.rs index c279439..309e506 100644 --- a/src/tui_app/app/events/event_types/event/mod.rs +++ b/src/tui_app/app/events/event_types/event/mod.rs @@ -1,7 +1,7 @@ mod handlers; use anyhow::{Context, Result}; -use cli_log::{info, trace}; +use cli_log::trace; use crossterm::event::Event as CrosstermEvent; use tokio::sync::mpsc::Sender; @@ -27,22 +27,12 @@ impl Event { .await .with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)), - Event::CommandEvent(event, callback_tx) => { - let (result, output) = command::handle(app, event, callback_tx.is_some()) - .await - .with_context(|| format!("Failed to handle command event: `{:#?}`", event))?; - - if let Some(callback_tx) = callback_tx { - callback_tx - .send(output.clone()) - .await - .with_context(|| format!("Failed to send command output: {}", output))?; - } - Ok(result) - } + Event::CommandEvent(event, callback_tx) => command::handle(app, event, callback_tx) + .await + .with_context(|| format!("Failed to handle command event: `{:#?}`", event)), Event::LuaCommand(lua_code) => lua_command::handle(app, lua_code.to_owned()) .await - .with_context(|| format!("Failed to handle lua code: `{:#?}`", lua_code)), + .with_context(|| format!("Failed to handle lua code: `{}`", lua_code)), Event::InputEvent(event) => match app.status.state() { State::Normal => main::handle_normal(app, event) diff --git a/src/tui_app/app/status.rs b/src/tui_app/app/status.rs index 0334301..770249a 100644 --- a/src/tui_app/app/status.rs +++ b/src/tui_app/app/status.rs @@ -28,6 +28,19 @@ pub struct Room { view_scroll: Option, } +pub struct StatusMessage { + content: String, + is_error: bool, +} +impl StatusMessage { + pub fn content(&self) -> String { + self.content.clone() + } + pub fn is_error(&self) -> bool { + self.is_error + } +} + pub struct Status { state: State, account_name: String, @@ -36,6 +49,7 @@ pub struct Status { client: Option, rooms: IndexMap, current_room_id: String, + status_messages: Vec, } impl fmt::Display for State { @@ -137,15 +151,41 @@ impl Status { }; Self { - state: State::Normal, - account_name: "".to_string(), - account_user_id: "".to_string(), - client, + state: State::None, + account_name: "".to_owned(), + account_user_id: "".to_owned(), + client, rooms, - current_room_id: "".to_string(), + current_room_id: "".to_owned(), + status_messages: vec![StatusMessage { + content: "Initialized!".to_owned(), + is_error: false, + }], } } + pub fn add_status_message(&mut self, msg: String) { + // TODO(@Soispha): This could allocate a lot of ram, when we don't + // add a limit to the messages. + // This needs to be proven. + self.status_messages.push(StatusMessage { + content: msg, + is_error: false, + }) + } + pub fn add_error_message(&mut self, msg: String) { + // TODO(@Soispha): This could allocate a lot of ram, when we don't + // add a limit to the messages. + // This needs to be proven. + self.status_messages.push(StatusMessage { + content: msg, + is_error: true, + }) + } + pub fn status_messages(&self) -> &Vec { + &self.status_messages + } + pub fn account_name(&self) -> &String { &self.account_name } diff --git a/src/tui_app/ui/central/mod.rs b/src/tui_app/ui/central/mod.rs index b2bf4e1..c889406 100644 --- a/src/tui_app/ui/central/mod.rs +++ b/src/tui_app/ui/central/mod.rs @@ -11,12 +11,13 @@ use crossterm::{ }; use tui::{ backend::CrosstermBackend, + style::Color, widgets::{Block, Borders, ListState}, Terminal, }; use tui_textarea::TextArea; -use crate::ui::terminal_prepare; +use crate::ui::{terminal_prepare, textarea_inactivate, textarea_activate}; use super::setup; @@ -28,10 +29,121 @@ pub enum InputPosition { Rooms, Messages, MessageCompose, + CommandMonitor, RoomInfo, CLI, } +impl InputPosition { + // calculate to widgets colors, based of which widget is currently selected + pub fn colors( + &self, + mut cli: &mut Option>, + mut message_compose: &mut TextArea<'_>, + ) -> Vec { + match self { + InputPosition::Status => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::Rooms => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::Messages => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::MessageCompose => { + textarea_activate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::RoomInfo => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::White, + Color::DarkGray, + ] + } + InputPosition::CLI => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_activate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::CommandMonitor => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::White, + ] + } + } + } +} + pub struct UI<'a> { terminal: Terminal>, input_position: InputPosition, @@ -89,7 +201,8 @@ impl UI<'_> { InputPosition::Status => InputPosition::Rooms, InputPosition::Rooms => InputPosition::Messages, InputPosition::Messages => InputPosition::MessageCompose, - InputPosition::MessageCompose => InputPosition::RoomInfo, + InputPosition::MessageCompose => InputPosition::CommandMonitor, + InputPosition::CommandMonitor => InputPosition::RoomInfo, InputPosition::RoomInfo => match self.cli { Some(_) => InputPosition::CLI, None => InputPosition::Status, @@ -107,7 +220,8 @@ impl UI<'_> { InputPosition::Rooms => InputPosition::Status, InputPosition::Messages => InputPosition::Rooms, InputPosition::MessageCompose => InputPosition::Messages, - InputPosition::RoomInfo => InputPosition::MessageCompose, + InputPosition::RoomInfo => InputPosition::CommandMonitor, + InputPosition::CommandMonitor => InputPosition::MessageCompose, InputPosition::CLI => InputPosition::RoomInfo, }; } @@ -157,12 +271,12 @@ impl UI<'_> { } pub async fn update_setup(&mut self) -> Result<()> { - let ui = match &mut self.setup_ui { + let setup_ui = match &mut self.setup_ui { Some(c) => c, None => bail!("SetupUI instance not found"), }; - ui.update(&mut self.terminal).await?; + setup_ui.update(&mut self.terminal).await?; Ok(()) } diff --git a/src/tui_app/ui/central/update/mod.rs b/src/tui_app/ui/central/update/mod.rs index 2b1ed2d..67cfb31 100644 --- a/src/tui_app/ui/central/update/mod.rs +++ b/src/tui_app/ui/central/update/mod.rs @@ -1,20 +1,13 @@ use std::cmp; use anyhow::{Context, Result}; -use tui::{ - layout::{Constraint, Direction, Layout}, - style::{Color, Style}, - widgets::{Block, Borders, Paragraph}, -}; +use tui::layout::{Constraint, Direction, Layout}; -use crate::{ - app::status::Status, - ui::{textarea_activate, textarea_inactivate}, -}; +use crate::app::status::Status; -use self::widgets::{messages, room_info, rooms, status}; +use self::widgets::{command_monitor, messages, room_info, rooms, status}; -use super::{InputPosition, UI}; +use super::UI; pub mod widgets; @@ -66,90 +59,12 @@ impl UI<'_> { let right_chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Min(4)].as_ref()) + .constraints([Constraint::Length(5), Constraint::Min(4)].as_ref()) .split(main_chunks[2]); - // calculate to widgets colors, based of which widget is currently selected - let colors = match self.input_position { - InputPosition::Status => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::White, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::Rooms => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::White, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::Messages => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::White, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::MessageCompose => { - textarea_activate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::RoomInfo => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::White, - ] - } - InputPosition::CLI => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_activate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - }; + let colors = self + .input_position + .colors(&mut self.cli, &mut self.message_compose); // initiate the widgets let mode_indicator = Paragraph::new(status.state().to_string()) @@ -164,6 +79,7 @@ impl UI<'_> { let (messages_panel, mut messages_state) = messages::init(status.room(), &colors) .context("Failed to initiate the messages widget")?; let room_info_panel = room_info::init(status.room(), &colors); + let command_monitor = command_monitor::init(status.status_messages(), &colors); // render the widgets self.terminal.draw(|frame| { @@ -177,6 +93,7 @@ impl UI<'_> { None => (), }; frame.render_widget(room_info_panel, right_chunks[0]); + frame.render_widget(command_monitor, right_chunks[1]); })?; Ok(()) diff --git a/src/tui_app/ui/central/update/widgets/command_monitor.rs b/src/tui_app/ui/central/update/widgets/command_monitor.rs new file mode 100644 index 0000000..9e548ed --- /dev/null +++ b/src/tui_app/ui/central/update/widgets/command_monitor.rs @@ -0,0 +1,34 @@ +use tui::{ + layout::Alignment, + style::{Color, Style}, + text::Text, + widgets::{Block, Borders, Paragraph}, +}; + +use crate::{app::status::StatusMessage, ui::central::InputPosition}; + +pub fn init<'a>(status_events: &Vec, colors: &Vec) -> Paragraph<'a> { + let mut command_monitor = Text::default(); + + status_events.iter().for_each(|event| { + // TODO(@Soispha): The added text (`event.content()`) doesn't wrap nicely, + // it would be nice if it did. + command_monitor.extend(Text::styled( + event.content(), + Style::default().fg(if event.is_error() { + Color::Red + } else { + Color::Cyan + }), + )); + }); + + Paragraph::new(command_monitor) + .block( + Block::default() + .title("Command Montior") + .borders(Borders::ALL) + .style(Style::default().fg(colors[InputPosition::CommandMonitor as usize])), + ) + .alignment(Alignment::Center) +} diff --git a/src/tui_app/ui/central/update/widgets/mod.rs b/src/tui_app/ui/central/update/widgets/mod.rs index 850c27f..d145b95 100644 --- a/src/tui_app/ui/central/update/widgets/mod.rs +++ b/src/tui_app/ui/central/update/widgets/mod.rs @@ -2,3 +2,4 @@ pub mod messages; pub mod room_info; pub mod rooms; pub mod status; +pub mod command_monitor; From a3b49b17f4dfb788602a72f1a4e46ef872633112 Mon Sep 17 00:00:00 2001 From: Soispha Date: Mon, 24 Jul 2023 23:54:45 +0200 Subject: [PATCH 06/18] Fix(handlers::main): Close ci after a command input --- .../events/event_types/event/handlers/main.rs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/tui_app/app/events/event_types/event/handlers/main.rs b/src/tui_app/app/events/event_types/event/handlers/main.rs index 6396ed1..ebb600f 100644 --- a/src/tui_app/app/events/event_types/event/handlers/main.rs +++ b/src/tui_app/app/events/event_types/event/handlers/main.rs @@ -135,26 +135,29 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R }; } central::InputPosition::CLI => { - if let Some(_) = app.ui.cli { + if let Some(cli) = &app.ui.cli { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, .. }) => { - let ci_event = app.ui - .cli - .as_mut() - .expect("This is already checked") - .lines() - .get(0) - .expect( - "There can only be one line in the buffer, as we collect it on enter being inputted" - ) - .to_owned(); + let ci_event = cli + .lines() + .get(0) + .expect( + "One line always exists, + and others can't exists + because we collect on + enter", + ) + .to_owned(); app.tx .send(Event::LuaCommand(ci_event)) .await .context("Failed to send lua command to internal event stream")?; + app.tx + .send(Event::CommandEvent(Command::CommandLineHide, None)) + .await?; } _ => { app.ui From 27ad48c5e94890997cb41c72bc84404bb0ac1093 Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 16:59:05 +0200 Subject: [PATCH 07/18] 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! --- {lua_macros => language_macros}/.gitignore | 0 {lua_macros => language_macros}/Cargo.toml | 2 +- .../src/generate/command_enum/mod.rs | 50 +++++ .../lua_functions_to_globals/mod.rs | 54 ++++++ .../src/generate/lua_wrapper/mod.rs | 20 ++ .../lua_wrapper/rust_wrapper_functions/mod.rs | 176 ++++++++++++++++++ language_macros/src/generate/mod.rs | 65 +++++++ language_macros/src/lib.rs | 93 +++++++++ lua_macros/src/lib.rs | 69 ------- lua_macros/src/mark_as_ci_command.rs | 173 ----------------- .../generate_command_enum.rs | 65 ------- .../generate_generate_ci_function.rs | 105 ----------- .../generate_help_function.rs | 53 ------ lua_macros/src/struct_to_ci_enum/mod.rs | 7 - 14 files changed, 459 insertions(+), 473 deletions(-) rename {lua_macros => language_macros}/.gitignore (100%) rename {lua_macros => language_macros}/Cargo.toml (90%) create mode 100644 language_macros/src/generate/command_enum/mod.rs create mode 100644 language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs create mode 100644 language_macros/src/generate/lua_wrapper/mod.rs create mode 100644 language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs create mode 100644 language_macros/src/generate/mod.rs create mode 100644 language_macros/src/lib.rs delete mode 100644 lua_macros/src/lib.rs delete mode 100644 lua_macros/src/mark_as_ci_command.rs delete mode 100644 lua_macros/src/struct_to_ci_enum/generate_command_enum.rs delete mode 100644 lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs delete mode 100644 lua_macros/src/struct_to_ci_enum/generate_help_function.rs delete mode 100644 lua_macros/src/struct_to_ci_enum/mod.rs diff --git a/lua_macros/.gitignore b/language_macros/.gitignore similarity index 100% rename from lua_macros/.gitignore rename to language_macros/.gitignore diff --git a/lua_macros/Cargo.toml b/language_macros/Cargo.toml similarity index 90% rename from lua_macros/Cargo.toml rename to language_macros/Cargo.toml index cb754e2..89d759e 100644 --- a/lua_macros/Cargo.toml +++ b/language_macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "lua_macros" +name = "language_macros" version = "0.1.0" edition = "2021" diff --git a/language_macros/src/generate/command_enum/mod.rs b/language_macros/src/generate/command_enum/mod.rs new file mode 100644 index 0000000..72d5401 --- /dev/null +++ b/language_macros/src/generate/command_enum/mod.rs @@ -0,0 +1,50 @@ +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{DeriveInput, Field, Type}; + +use super::{get_input_type_of_bare_fn_field, parse_derive_input_as_named_fields}; + +pub fn command_enum(input: &DeriveInput) -> TokenStream2 { + let named_fields = parse_derive_input_as_named_fields(input); + let fields: TokenStream2 = named_fields + .named + .iter() + .map(|field| turn_struct_fieled_to_enum(field)) + .collect(); + + quote! { + #[derive(Debug)] + pub enum Command { + #fields + } + } +} + +fn turn_struct_fieled_to_enum(field: &Field) -> TokenStream2 { + let field_name = format_ident!( + "{}", + field + .ident + .as_ref() + .expect("These are named fields, it should be Some()") + .to_string() + .from_case(Case::Snake) + .to_case(Case::Pascal) + ); + + let input_type: Option = get_input_type_of_bare_fn_field(field); + + match input_type { + Some(input_type) => { + quote! { + #field_name(#input_type), + } + } + None => { + quote! { + #field_name, + } + } + } +} diff --git a/language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs b/language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs new file mode 100644 index 0000000..194fb53 --- /dev/null +++ b/language_macros/src/generate/lua_wrapper/lua_functions_to_globals/mod.rs @@ -0,0 +1,54 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{DeriveInput, Field}; + +use crate::generate::parse_derive_input_as_named_fields; + +pub fn generate_add_lua_functions_to_globals(input: &DeriveInput) -> TokenStream2 { + let named_fields = parse_derive_input_as_named_fields(input); + let function_adders: TokenStream2 = named_fields + .named + .iter() + .map(|field| generate_function_adder(field)) + .collect(); + + quote! { + pub fn add_lua_functions_to_globals( + lua: mlua::Lua, + tx: tokio::sync::mpsc::Sender, + ) -> mlua::Lua { + lua.set_app_data(tx); + let globals = lua.globals(); + + #function_adders + + drop(globals); + lua + } + } +} + +fn generate_function_adder(field: &Field) -> TokenStream2 { + let field_ident = field + .ident + .as_ref() + .expect("This is should be a named field"); + + let function_ident = format_ident!("wrapped_lua_function_{}", field_ident); + let function_name = field_ident.to_string(); + + quote! { + { + let #function_ident = lua.create_async_function(#field_ident).expect( + &format!( + "The function: `{}` should be defined", + #function_name + ) + ); + + globals.set(#function_name, #function_ident).expect( + "Setting a static global value should work" + ); + } + } +} diff --git a/language_macros/src/generate/lua_wrapper/mod.rs b/language_macros/src/generate/lua_wrapper/mod.rs new file mode 100644 index 0000000..8672d47 --- /dev/null +++ b/language_macros/src/generate/lua_wrapper/mod.rs @@ -0,0 +1,20 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::DeriveInput; + +use crate::generate::lua_wrapper::{ + lua_functions_to_globals::generate_add_lua_functions_to_globals, + rust_wrapper_functions::generate_rust_wrapper_functions, +}; + +mod lua_functions_to_globals; +mod rust_wrapper_functions; + +pub fn lua_wrapper(input: &DeriveInput) -> TokenStream2 { + let add_lua_functions_to_globals = generate_add_lua_functions_to_globals(input); + let rust_wrapper_functions = generate_rust_wrapper_functions(input); + quote! { + #add_lua_functions_to_globals + #rust_wrapper_functions + } +} diff --git a/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs b/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs new file mode 100644 index 0000000..706e463 --- /dev/null +++ b/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs @@ -0,0 +1,176 @@ +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + punctuated::Punctuated, token::Comma, DeriveInput, Field, GenericArgument, Lifetime, Type, +}; + +use crate::generate::{ + get_input_type_of_bare_fn_field, get_return_type_of_bare_fn_field, + parse_derive_input_as_named_fields, +}; + +pub fn generate_rust_wrapper_functions(input: &DeriveInput) -> TokenStream2 { + let named_fields = parse_derive_input_as_named_fields(input); + let wrapped_functions: TokenStream2 = named_fields + .named + .iter() + .map(|field| wrap_lua_function(field)) + .collect(); + + quote! { + #wrapped_functions + } +} + +fn wrap_lua_function(field: &Field) -> TokenStream2 { + let input_type = get_input_type_of_bare_fn_field(field); + let return_type = get_return_type_of_bare_fn_field(field); + + let function_name = field.ident.as_ref().expect("This should be a named field"); + let function_body = get_function_body(field, input_type.is_some(), &return_type); + + let lifetime_args = + get_and_add_lifetimes_form_inputs_and_outputs(input_type.clone(), return_type); + + let input_type = input_type + .unwrap_or(syn::parse(quote! {()}.into()).expect("This is static, it always works")); + + quote! { + async fn #function_name <#lifetime_args>( + lua: &mlua::Lua, + input: #input_type + ) -> Result { + #function_body + } + } +} + +fn get_and_add_lifetimes_form_inputs_and_outputs<'a>( + input_type: Option, + return_type: Option, +) -> Punctuated { + fn get_lifetime_args_from_type<'a>(return_type: syn::Type) -> Option> { + match return_type { + syn::Type::Path(path) => { + let args_to_final_path_segment = &path + .path + .segments + .last() + .expect("The path should have a last segment") + .arguments; + match args_to_final_path_segment { + syn::PathArguments::None => + /* We ignore this case */ + { + None + } + syn::PathArguments::AngleBracketed(angle) => { + let lifetime_args: Vec<_> = angle + .args + .iter() + .filter_map(|arg| { + if let GenericArgument::Lifetime(lifetime) = arg { + Some(lifetime.to_owned()) + } else { + None + } + }) + .collect(); + return Some(lifetime_args); + } + syn::PathArguments::Parenthesized(_) => todo!(), + } + } + _ => todo!(), + } + } + + let mut output: Punctuated = Punctuated::new(); + if let Some(input_type) = input_type { + let lifetime_args = get_lifetime_args_from_type(input_type).unwrap_or(vec![]); + lifetime_args.into_iter().for_each(|arg| output.push(arg)); + } + if let Some(return_type) = return_type { + let lifetime_args = get_lifetime_args_from_type(return_type).unwrap_or(vec![]); + lifetime_args.into_iter().for_each(|arg| output.push(arg)); + } + output +} + +fn get_function_body(field: &Field, has_input: bool, output_type: &Option) -> TokenStream2 { + let command_name = field + .ident + .as_ref() + .expect("These are named fields, it should be Some()") + .to_string() + .from_case(Case::Snake) + .to_case(Case::Pascal); + let command_ident = format_ident!("{}", command_name); + + let send_output = if has_input { + quote! { + Event::CommandEvent( + Command::#command_ident(input.clone()), + Some(callback_tx), + ) + } + } else { + quote! { + Event::CommandEvent( + Command::#command_ident, + Some(callback_tx), + ) + } + }; + + let function_return = if let Some(_) = output_type { + quote! { + let output: mlua::Value = lua.to_value(output).expect("This conversion should (indirectely) be checked at compile time"); + return Ok(output); + } + } else { + quote! { + return Ok(()); + } + }; + 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(()); + } + }; + + quote! { + let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::(); + let tx: core::cell::Ref> = + lua.app_data_ref().expect("This should exist, it was set before"); + + (*tx) + .send(#send_output) + .await + .expect("This should work, as the receiver is not dropped"); + + cli_log::info!("Sent CommandEvent: `{}`", #command_name); + + match callback_rx.await { + Ok(output) => { + cli_log::info!( + "Lua function: `{}` returned output to lua: `{}`", #command_name, &output + ); + #function_return + }, + Err(err) => { + #does_function_expect_output + } + }; + } +} diff --git a/language_macros/src/generate/mod.rs b/language_macros/src/generate/mod.rs new file mode 100644 index 0000000..972bbf8 --- /dev/null +++ b/language_macros/src/generate/mod.rs @@ -0,0 +1,65 @@ +mod command_enum; +mod lua_wrapper; + +pub use command_enum::command_enum; +pub use lua_wrapper::lua_wrapper; +use syn::{DeriveInput, Field, FieldsNamed, ReturnType, Type, TypeBareFn}; + +pub fn parse_derive_input_as_named_fields(input: &DeriveInput) -> FieldsNamed { + match &input.data { + syn::Data::Struct(input) => match &input.fields { + syn::Fields::Named(named_fields) => named_fields, + _ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"), + }, + _ => unimplemented!("The macro only works for structs"), + } + .to_owned() +} + +pub fn get_bare_fn_input_type(function: &TypeBareFn) -> Option { + if function.inputs.len() == 1 { + Some( + function + .inputs + .first() + .expect("Only one element exists, we checked the length above") + .ty + .clone(), + ) + } else if function.inputs.len() == 0 { + // No inputs, so we can't return a type + None + } else { + unreachable!( + "The Function can only take one or zero arguments. + Use a tuple `(arg1, arg2)` if you want more" + ); + } +} + +pub fn get_input_type_of_bare_fn_field(field: &Field) -> Option { + match &field.ty { + syn::Type::BareFn(function) => get_bare_fn_input_type(&function), + _ => unimplemented!( + "Please specify the type as a bare fn type. + That is: `fn() -> `" + ), + } +} +pub fn get_return_type_of_bare_fn_field(field: &Field) -> Option { + match &field.ty { + syn::Type::BareFn(function) => get_bare_fn_return_type(&function), + _ => unimplemented!( + "Please specify the type as a bare fn type. + That is: `fn() -> `" + ), + } +} + +pub fn get_bare_fn_return_type(function: &TypeBareFn) -> Option { + let return_path: &ReturnType = &function.output; + match return_path { + ReturnType::Default => None, + ReturnType::Type(_, return_type) => Some(*return_type.to_owned()), + } +} diff --git a/language_macros/src/lib.rs b/language_macros/src/lib.rs new file mode 100644 index 0000000..93ac287 --- /dev/null +++ b/language_macros/src/lib.rs @@ -0,0 +1,93 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::DeriveInput; + +mod generate; + +/// This is the heart of the command api +/// It mainly does two things: +/// - Generate a command enum +/// - Wrap the enum in all supported languages (only lua for now) +/// - Generate wrapper lua function for each command +/// - Generate a `add_lua_functions_to_globals` function, which adds +/// the rust wrapper functions to the lua globals. +/// +/// The input and output values of the wrapped functions are derived from the values specified in +/// the `Commands` struct. +/// The returned values will be returned directly to the lua context, this allows to nest functions. +/// +/// For example this rust code: +/// ```rust +/// #[ci_command_enum] +/// struct Commands { +/// /// Greets the user +/// greet: fn(String) -> String, +/// } +/// ``` +/// results in this expanded code: +/// ```rust +/// #[derive(Debug)] +/// pub enum Command { +/// Greet(String), +/// } +/// pub fn add_lua_functions_to_globals( +/// lua: mlua::Lua, +/// tx: tokio::sync::mpsc::Sender, +/// ) -> mlua::Lua { +/// lua.set_app_data(tx); +/// let globals = lua.globals(); +/// { +/// let wrapped_lua_function_greet = lua +/// .create_async_function(greet) +/// .expect( +/// format!( +/// "The function: `{}` should be defined", +/// "greet", +/// ) +/// ); +/// globals +/// .set("greet", wrapped_lua_function_greet) +/// .expect("Setting a static global value should work"); +/// } +/// drop(globals); +/// lua +/// } +/// async fn greet(lua: &mlua::Lua, input: String) -> Result { +/// let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::(); +/// let tx: core::cell::Ref> = lua +/// .app_data_ref() +/// .expect("This should exist, it was set before"); +/// (*tx) +/// .send(Event::CommandEvent(Command::Greet(input.clone()), Some(callback_tx))) +/// .await +/// .expect("This should work, as the receiver is not dropped"); +/// match callback_rx.await { +/// Ok(output) => { +/// return Ok(output); +/// } +/// Err(err) => { +/// return Err(mlua::Error::ExternalError(std::sync::Arc::new(err))); +/// } +/// }; +/// } +/// ``` +#[proc_macro_attribute] +pub fn ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream { + // Construct a representation of Rust code as a syntax tree + // that we can manipulate + let input: DeriveInput = syn::parse(input) + .expect("This should always be valid rust code, as it's extracted from direct code"); + + // Build the language wrappers + let lua_wrapper: TokenStream2 = generate::lua_wrapper(&input); + + // Build the final enum + let command_enum = generate::command_enum(&input); + + quote! { + #command_enum + #lua_wrapper + } + .into() +} diff --git a/lua_macros/src/lib.rs b/lua_macros/src/lib.rs deleted file mode 100644 index 871dd3f..0000000 --- a/lua_macros/src/lib.rs +++ /dev/null @@ -1,69 +0,0 @@ -mod mark_as_ci_command; -mod struct_to_ci_enum; - -use mark_as_ci_command::generate_final_function; -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use struct_to_ci_enum::{generate_command_enum, generate_generate_ci_function, generate_help_function}; -use syn::{self, parse_quote, parse_str, DeriveInput, FieldMutability, ItemFn, Token, Visibility}; - -#[proc_macro_attribute] -pub fn turn_struct_to_ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream { - // Construct a representation of Rust code as a syntax tree - // that we can manipulate - let mut input: DeriveInput = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code"); - - let mut named_fields = match &input.data { - syn::Data::Struct(input) => match &input.fields { - syn::Fields::Named(named_fields) => named_fields, - _ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"), - }, - _ => unimplemented!("The macro only works for structs"), - } - .to_owned(); - - let attr_parsed = parse_quote! { - /// This is a help function - }; - - named_fields.named.push(syn::Field { - attrs: vec![attr_parsed], - // attrs: attr_parser - // .parse("#[doc = r\"This is a help function\"]".to_token_stream().into()) - // .expect("See reason for other one"), - vis: Visibility::Inherited, - mutability: FieldMutability::None, - ident: Some(format_ident!("help")), - colon_token: Some(Token![:](Span::call_site())), - ty: parse_str("fn(Option) -> String").expect("This is static and valid rust code"), - }); - - match &mut input.data { - syn::Data::Struct(input) => input.fields = syn::Fields::Named(named_fields.clone()), - _ => unreachable!("This was a DataStruct before"), - }; - - // Build the trait implementation - let generate_ci_function: TokenStream2 = generate_generate_ci_function(&input); - - let command_enum = generate_command_enum(&named_fields); - - let help_function = generate_help_function(&named_fields); - - quote! { - #command_enum - - #generate_ci_function - - //#help_function - } - .into() -} - -#[proc_macro_attribute] -pub fn ci_command(_attrs: TokenStream, input: TokenStream) -> TokenStream { - let mut input: ItemFn = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code"); - let output_function = generate_final_function(&mut input); - output_function.into() -} diff --git a/lua_macros/src/mark_as_ci_command.rs b/lua_macros/src/mark_as_ci_command.rs deleted file mode 100644 index 76e2fc1..0000000 --- a/lua_macros/src/mark_as_ci_command.rs +++ /dev/null @@ -1,173 +0,0 @@ -use convert_case::{Case, Casing}; -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote, ToTokens}; -use syn::{Block, Expr, ExprBlock, GenericArgument, ReturnType, Stmt, Type}; - -pub fn generate_final_function(input: &mut syn::ItemFn) -> TokenStream2 { - append_tx_send_code(input); - - let output: TokenStream2 = syn::parse(input.into_token_stream().into()) - .expect("This is generated from valid rust code, it should stay that way."); - - output -} - -fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn { - let function_name_pascal = format_ident!( - "{}", - input - .sig - .ident - .clone() - .to_string() - .from_case(Case::Snake) - .to_case(Case::Pascal) - ); - - let tx_send = match &input.sig.output { - syn::ReturnType::Default => { - unreachable!("All functions should have a output of (Result<$type, rlua::Error>)"); - } - syn::ReturnType::Type(_, ret_type) => { - let return_type = match *(ret_type.clone()) { - syn::Type::Path(path) => { - match path - .path - .segments - .first() - .expect("This is expected to be only one path segment") - .arguments - .to_owned() - { - syn::PathArguments::AngleBracketed(angled_path) => { - let angled_path = angled_path.args.to_owned(); - let filtered_paths: Vec<_> = angled_path - .into_iter() - .filter(|generic_arg| { - if let GenericArgument::Type(generic_type) = generic_arg { - if let Type::Path(_) = generic_type { - true - } else { - false - } - } else { - false - } - }) - .collect(); - - // There should only be two segments (the type is ) - if filtered_paths.len() > 2 { - unreachable!("There should be no more than two filtered_output, but got: {:#?}", filtered_paths) - } else if filtered_paths.len() <= 0 { - unreachable!("There should be more than zero filtered_output, but got: {:#?}", filtered_paths) - } - - if filtered_paths.len() == 2 { - // There is something else than mlua::Error - let gen_type = if let GenericArgument::Type(ret_type) = - filtered_paths - .first() - .expect("One path segment should exists") - .to_owned() - { - ret_type - } else { - unreachable!("These were filtered above."); - }; - let return_type_as_type_prepared = quote! {-> #gen_type}; - - let return_type_as_return_type: ReturnType = syn::parse( - return_type_as_type_prepared.to_token_stream().into(), - ) - .expect("This is valid."); - return_type_as_return_type - } else { - // There is only mlua::Error left - ReturnType::Default - } - } - _ => unimplemented!("Only for angled paths"), - } - } - _ => unimplemented!("Only for path types"), - }; - - let send_data = match return_type { - ReturnType::Default => { - quote! { - { - Event::CommandEvent(Command::#function_name_pascal, None) - } - } - } - ReturnType::Type(_, _) => { - quote! { - { - Event::CommandEvent(Command::#function_name_pascal(input.clone()), Some(callback_tx)) - } - } - } - }; - - let output_return = match return_type { - ReturnType::Default => { - quote! { - { - return Ok(()); - } - } - } - ReturnType::Type(_, _) => { - quote! { - { - if let Some(output) = callback_rx.recv().await { - callback_rx.close(); - return Ok(output); - } else { - return Err(mlua::Error::ExternalError(Arc::new(Error::new( - ErrorKind::Other, - "Callback reciever dropped", - )))); - } - } - } - } - }; - - quote! { - { - let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::(256); - - let tx: - core::cell::Ref> = - lua - .app_data_ref() - .expect("This exists, it was set before"); - - (*tx) - .try_send(#send_data) - .expect("This should work, as the reciever is not dropped"); - - #output_return - } - } - } - }; - - let tx_send_block: Block = - syn::parse(tx_send.into()).expect("This is a static string, it will always parse"); - - let tx_send_expr_block = ExprBlock { - attrs: vec![], - label: None, - block: tx_send_block, - }; - let mut tx_send_stmt = vec![Stmt::Expr(Expr::Block(tx_send_expr_block), None)]; - - let mut new_stmts: Vec = Vec::with_capacity(input.block.stmts.len() + 1); - new_stmts.append(&mut tx_send_stmt); - new_stmts.append(&mut input.block.stmts); - input.block.stmts = new_stmts; - input -} diff --git a/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs b/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs deleted file mode 100644 index 8e5fd7e..0000000 --- a/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs +++ /dev/null @@ -1,65 +0,0 @@ -use convert_case::{Case, Casing}; -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::Type; - -pub fn generate_command_enum(input: &syn::FieldsNamed) -> TokenStream2 { - let input_tokens: TokenStream2 = input - .named - .iter() - .map(|field| -> TokenStream2 { - let field_ident = field - .ident - .as_ref() - .expect("These are only the named fields, thus they should all have a ident."); - - let enum_variant_type = match &field.ty { - syn::Type::BareFn(function) => { - let return_path = &function.inputs; - - let input_type: Option = if return_path.len() == 1 { - Some( - return_path - .last() - .expect("The last element exists") - .ty - .clone(), - ) - } else if return_path.len() == 0 { - None - } else { - panic!("The Function can only take on argument, or none"); - }; - input_type - } - _ => unimplemented!("This is only implemented for bare function types"), - }; - - let enum_variant_name = format_ident!( - "{}", - field_ident - .to_string() - .from_case(Case::Snake) - .to_case(Case::Pascal) - ); - if enum_variant_type.is_some() { - quote! { - #enum_variant_name (#enum_variant_type), - } - .into() - } else { - quote! { - #enum_variant_name, - } - } - }) - .collect(); - - let gen = quote! { - #[derive(Debug)] - pub enum Command { - #input_tokens - } - }; - gen.into() -} diff --git a/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs b/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs deleted file mode 100644 index 3c690ec..0000000 --- a/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs +++ /dev/null @@ -1,105 +0,0 @@ -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::{parse_quote, ReturnType, Type}; - -fn generate_ci_function_exposure(field: &syn::Field) -> TokenStream2 { - let field_ident = field - .ident - .as_ref() - .expect("These are only the named field, thus they all should have a name."); - - let function_name_ident = format_ident!("fun_{}", field_ident); - let function_name = format!("{}", field_ident); - quote! { - let #function_name_ident = lua.create_async_function(#field_ident).expect( - &format!( - "The function: `{}` should be defined", - #function_name - ) - ); - - globals.set(#function_name, #function_name_ident).expect( - &format!( - "Setting a static global value ({}, fun_{}) should work", - #function_name, - #function_name - ) - ); - } - .into() -} - -pub fn generate_generate_ci_function(input: &syn::DeriveInput) -> TokenStream2 { - let mut functions_to_generate: Vec = vec![]; - - let functions_to_export_in_lua: TokenStream2 = match &input.data { - syn::Data::Struct(input) => match &input.fields { - syn::Fields::Named(named_fields) => named_fields - .named - .iter() - .map(|field| -> TokenStream2 { - let input_type = match &field.ty { - syn::Type::BareFn(bare_fn) => { - if bare_fn.inputs.len() == 1 { - bare_fn.inputs.last().expect("The last element exists").ty.clone() - } else if bare_fn.inputs.len() == 0 { - let input_type: Type = parse_quote! {()}; - input_type - } else { - panic!("The Function can only take on argument, or none"); - } - } - _ => unimplemented!("This is only implemented for bare function types"), - }; - let return_type = match &field.ty { - syn::Type::BareFn(function) => { - let return_path: &ReturnType = &function.output; - match return_path { - ReturnType::Default => None, - ReturnType::Type(_, return_type) => Some(return_type.to_owned()) } - } - _ => unimplemented!("This is only implemented for bare function types"), - }; - - let function_name = field - .ident - .as_ref() - .expect("These are only the named field, thus they all should have a name."); - - if let Some(ret_type) = return_type { - functions_to_generate.push(quote! { - #[ci_command] - async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<#ret_type, mlua::Error> { - } - }); - } else { - functions_to_generate.push(quote! { - #[ci_command] - async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<(), mlua::Error> { - } - }); - } - generate_ci_function_exposure(field) - }) - .collect(), - - _ => unimplemented!("Only implemented for named fileds"), - }, - _ => unimplemented!("Only implemented for structs"), - }; - - let functions_to_generate: TokenStream2 = functions_to_generate.into_iter().collect(); - let gen = quote! { - pub fn generate_ci_functions( - lua: &mut mlua::Lua, - tx: tokio::sync::mpsc::Sender - ) - { - lua.set_app_data(tx); - let globals = lua.globals(); - #functions_to_export_in_lua - } - #functions_to_generate - }; - gen.into() -} diff --git a/lua_macros/src/struct_to_ci_enum/generate_help_function.rs b/lua_macros/src/struct_to_ci_enum/generate_help_function.rs deleted file mode 100644 index 2987272..0000000 --- a/lua_macros/src/struct_to_ci_enum/generate_help_function.rs +++ /dev/null @@ -1,53 +0,0 @@ -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens}; - -pub fn generate_help_function(input: &syn::FieldsNamed) -> TokenStream2 { - let input: Vec<_> = input.named.iter().collect(); - - let combined_help_text: TokenStream2 = input - .iter() - .map(|field| { - let attrs_with_doc: Vec = field - .attrs - .iter() - .filter_map(|attr| { - if attr.path().is_ident("doc") { - let help_text = attr - .meta - .require_name_value() - .expect("This is a named value type, because all doc comments work this way") - .value - .clone(); - Some(help_text.into_token_stream().into()) - } else { - return None; - } - }) - .collect(); - if attrs_with_doc.len() == 0 { - // TODO there should be a better panic function, than the generic one - panic!( - "The command named: `{}`, does not provide a help message", - field.ident.as_ref().expect("These are all named") - ); - } else { - let help_text_for_one_command_combined: TokenStream2 = attrs_with_doc.into_iter().collect(); - return help_text_for_one_command_combined; - } - }) - .collect(); - - quote! { - #[ci_command] - async fn help( - lua: &mlua::Lua, - input_str: Option - ) -> Result { - // TODO add a way to filter the help based on the input - - let output = "These functions exist:\n"; - output.push_str(#combined_help_text); - Ok(output) - } - } -} diff --git a/lua_macros/src/struct_to_ci_enum/mod.rs b/lua_macros/src/struct_to_ci_enum/mod.rs deleted file mode 100644 index 4d93f98..0000000 --- a/lua_macros/src/struct_to_ci_enum/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod generate_command_enum; -pub mod generate_generate_ci_function; -pub mod generate_help_function; - -pub use generate_command_enum::*; -pub use generate_generate_ci_function::*; -pub use generate_help_function::*; From fbcf572f47ce4b0245ce21bc5e934253b68d6e67 Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 17:11:52 +0200 Subject: [PATCH 08/18] Build(flake+Cargo): Update lockfiles --- Cargo.lock | 135 +++++++++++++++++++++++++++-------------------------- flake.lock | 12 ++--- 2 files changed, 74 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe623ac..baaf744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] @@ -957,8 +957,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 +1358,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 +1411,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" @@ -1675,9 +1675,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 +1735,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 +1850,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 +1955,7 @@ dependencies = [ "anyhow", "itertools", "proc-macro2 1.0.66", - "quote 1.0.31", + "quote 1.0.32", "syn 1.0.109", ] @@ -1970,9 +1970,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 +2339,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 +2388,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 +2417,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 +2435,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 +2493,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 +2607,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 +2637,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]] @@ -2693,6 +2693,7 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot 0.12.1", "pin-project-lite", "socket2", "tokio-macros", @@ -2706,8 +2707,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 +2786,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 +2808,7 @@ dependencies = [ "cli-log", "crossterm", "indexmap 2.0.0", - "lua_macros", + "language_macros", "matrix-sdk", "mlua", "once_cell", @@ -3033,8 +3034,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 +3057,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 +3068,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 +3210,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 +3254,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", ] diff --git a/flake.lock b/flake.lock index f952431..a386b6f 100644 --- a/flake.lock +++ b/flake.lock @@ -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": { From 3ca01912b9e64c39b03e5a052966ac6a2fb30426 Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 17:12:33 +0200 Subject: [PATCH 09/18] Build(flake): Switch back to stable --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 9ce72bf..1b84e4d 100644 --- a/flake.nix +++ b/flake.nix @@ -45,7 +45,7 @@ overlays = [(import rust-overlay)]; }; - nightly = true; + nightly = false; rust = if nightly then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default) From 7489f06a7ce9649a9b852a0701b74f080022b5c9 Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 17:19:57 +0200 Subject: [PATCH 10/18] Fix(app::command_interface): Use new language_macro api --- Cargo.toml | 4 +- .../app/command_interface.lua_macros.rs | 54 ---------------- src/tui_app/app/command_interface.rs | 64 ------------------- src/tui_app/app/command_interface/mod.rs | 48 ++++++++++++++ 4 files changed, 50 insertions(+), 120 deletions(-) delete mode 100644 src/tui_app/app/command_interface.lua_macros.rs delete mode 100644 src/tui_app/app/command_interface.rs create mode 100644 src/tui_app/app/command_interface/mod.rs diff --git a/Cargo.toml b/Cargo.toml index a60ace5..465b983 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,10 @@ 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" } +language_macros = { path = "./language_macros" } mlua = { version = "0.8.9", features = ["lua54", "async", "send"] } once_cell = "1.18.0" diff --git a/src/tui_app/app/command_interface.lua_macros.rs b/src/tui_app/app/command_interface.lua_macros.rs deleted file mode 100644 index ea1cad7..0000000 --- a/src/tui_app/app/command_interface.lua_macros.rs +++ /dev/null @@ -1,54 +0,0 @@ -// FIXME: This file needs documentation with examples of how the proc macros work. -// for now use `cargo expand app::command_interface` for an overview - -use std::{io::{Error, ErrorKind}, sync::Arc}; - -use lua_macros::{ci_command, turn_struct_to_ci_command_enum}; - -use crate::app::event_types::Event; -/// This struct is here to guarantee, that all functions actually end up in the lua context. -/// I.e. Rust should throw a compile error, when one field is added, but not a matching function. -/// -/// What it does: -/// - Generates a `generate_ci_functions` function, which wraps the specified rust in functions -/// in lua and exports them to the globals in the context provided as argument. -/// - Generates a Commands enum, which contains every Camel cased version of the fields. -/// -/// Every command specified here should have a function named $command_name, where $command_name is the snake cased name of the field. -/// -/// This function is exported to the lua context, thus it's signature must be: -/// ```rust -/// fn $command_name(context: Context, input_string: String) -> Result<$return_type, rlua::Error> {} -/// ``` -/// where $return_type is the type returned by the function (the only supported ones are right now -/// `String` and `()`). - -#[turn_struct_to_ci_command_enum] -struct Commands { - /// Greets the user - greet: fn(String) -> String, - - /// Closes the application - //#[expose(lua)] - exit: fn(), - - /// Shows the command line - command_line_show: fn(), - - /// Hides the command line - command_line_hide: fn(), - - /// Go to the next plane - cycle_planes: fn(), - /// Go to the previous plane - cycle_planes_rev: fn(), - - /// Sets the current app mode to Normal / navigation mode - set_mode_normal: fn(), - /// Sets the current app mode to Insert / editing mode - set_mode_insert: fn(), - - /// Send a message to the current room - /// The send message is interpreted literally. - room_message_send: fn(String) -> String, -} diff --git a/src/tui_app/app/command_interface.rs b/src/tui_app/app/command_interface.rs deleted file mode 100644 index 610c28c..0000000 --- a/src/tui_app/app/command_interface.rs +++ /dev/null @@ -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), -} - -pub fn generate_ci_functions( - lua: mlua::Lua, - tx: tokio::sync::mpsc::Sender, -) -> 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 { - let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::(256); - let tx: core::cell::Ref> = - lua.app_data_ref().expect("This exists, it was set before"); - - 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"), - ))); - } -} diff --git a/src/tui_app/app/command_interface/mod.rs b/src/tui_app/app/command_interface/mod.rs new file mode 100644 index 0000000..b3ebb88 --- /dev/null +++ b/src/tui_app/app/command_interface/mod.rs @@ -0,0 +1,48 @@ +// Use `cargo expand app::command_interface` for an overview of the file contents + +pub mod lua_command_manger; + +use language_macros::ci_command_enum; + +use crate::app::Event; + +#[ci_command_enum] +struct Commands { + /// Greets the user + greet: fn(String) -> String, + + /// Closes the application + exit: fn(), + + /// Shows the command line + command_line_show: fn(), + + /// Hides the command line + command_line_hide: fn(), + + /// Go to the next plane + cycle_planes: fn(), + /// Go to the previous plane + cycle_planes_rev: fn(), + + /// Sets the current app mode to Normal / navigation mode + set_mode_normal: fn(), + /// Sets the current app mode to Insert / editing mode + set_mode_insert: fn(), + + /// Send a message to the current room + /// The sent message is interpreted literally. + room_message_send: fn(String), + + /// Open the help pages at the first occurrence of + /// the input string if it is Some, otherwise open + /// the help pages at the start + help: fn(Option), + + /// Send an error to the default error output + raise_error: fn(String), + /// Send output to the default output + /// This is mainly used to display the final + /// output of evaluated lua commands. + display_output: fn(String), +} From 1a35bb152c994d2087993a468c172129993a2274 Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 21:04:04 +0200 Subject: [PATCH 11/18] Feat(treewide): Add a way for Commands to return more than just strings --- Cargo.lock | 12 +- Cargo.toml | 2 +- .../lua_wrapper/rust_wrapper_functions/mod.rs | 67 ++++++- .../lua_command_manager/mod.rs | 189 ++++++++++++++++++ src/tui_app/app/command_interface/mod.rs | 7 +- .../event_types/event/handlers/command.rs | 32 +-- .../event_types/event/handlers/lua_command.rs | 7 +- .../event_types/event/handlers/matrix.rs | 7 +- .../event_types/event/handlers/setup.rs | 10 +- .../app/events/event_types/event/mod.rs | 18 +- src/tui_app/app/mod.rs | 85 +------- 11 files changed, 325 insertions(+), 111 deletions(-) create mode 100644 src/tui_app/app/command_interface/lua_command_manager/mod.rs diff --git a/Cargo.lock b/Cargo.lock index baaf744..377d862 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -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]] @@ -2693,7 +2704,6 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot 0.12.1", "pin-project-lite", "socket2", "tokio-macros", diff --git a/Cargo.toml b/Cargo.toml index 465b983..e3d79a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ tokio = { version = "1.29", features = ["macros", "rt-multi-thread"] } # lua stuff language_macros = { path = "./language_macros" } -mlua = { version = "0.8.9", features = ["lua54", "async", "send"] } +mlua = { version = "0.8.9", features = ["lua54", "async", "send", "serialize"] } once_cell = "1.18.0" # tui feature specific parts diff --git a/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs b/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs index 706e463..2557a5f 100644 --- a/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs +++ b/language_macros/src/generate/lua_wrapper/rust_wrapper_functions/mod.rs @@ -126,12 +126,71 @@ fn get_function_body(field: &Field, has_input: bool, output_type: &Option) let function_return = if let Some(_) = output_type { quote! { - let output: mlua::Value = lua.to_value(output).expect("This conversion should (indirectely) be checked at compile time"); - return Ok(output); + 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(()); + return Ok(mlua::Value::Nil); } }; let does_function_expect_output = if output_type.is_some() { @@ -145,7 +204,7 @@ fn get_function_body(field: &Field, has_input: bool, output_type: &Option) } else { quote! { // We didn't receive output and didn't expect output. Everything went well! - return Ok(()); + return Ok(mlua::Value::Nil); } }; diff --git a/src/tui_app/app/command_interface/lua_command_manager/mod.rs b/src/tui_app/app/command_interface/lua_command_manager/mod.rs new file mode 100644 index 0000000..3116f0c --- /dev/null +++ b/src/tui_app/app/command_interface/lua_command_manager/mod.rs @@ -0,0 +1,189 @@ +use std::{collections::HashMap, fmt::Display, thread}; + +use anyhow::{Context, Result}; +use cli_log::{error, info, debug}; +use mlua::{Function, Value}; +use once_cell::sync::OnceCell; +use serde::{Deserialize, Serialize}; +use tokio::{ + runtime::Builder, + sync::{mpsc, Mutex}, + task::{self, LocalSet}, +}; + +use crate::app::{ + command_interface::{add_lua_functions_to_globals, Command}, + events::event_types::Event, +}; + +static LUA: OnceCell> = OnceCell::new(); +pub type Table = HashMap; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum CommandTransferValue { + /// `nil` or `null` or `undefined`; anything which goes in that group of types. + Nil, + + /// `true` or `false`. + Boolean(bool), + + // A "light userdata" object, equivalent to a raw pointer. + // /*TODO*/ LightUserData(LightUserData), + + /// An integer number. + Integer(i64), + + /// A floating point number. + Number(f64), + + /// A string + String(String), + + /// A table, directory or HashMap + Table(HashMap), + + // Reference to a Lua function (or closure). + // /* TODO */ Function(Function), + + // Reference to a Lua thread (or coroutine). + // /* TODO */ Thread(Thread<'lua>), + + // Reference to a userdata object that holds a custom type which implements `UserData`. + // Special builtin userdata types will be represented as other `Value` variants. + // /* TODO */ UserData(AnyUserData), + + // `Error` is a special builtin userdata type. When received from Lua it is implicitly cloned. + // /* TODO */ Error(Error), +} + +impl Display for CommandTransferValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CommandTransferValue::Nil => f.write_str("Nil"), + CommandTransferValue::Boolean(bool) => f.write_str(&format!("{}", bool)), + CommandTransferValue::Integer(int) => f.write_str(&format!("{}", int)), + CommandTransferValue::Number(num) => f.write_str(&format!("{}", num)), + CommandTransferValue::String(str) => f.write_str(&format!("{}", str)), + // TODO(@Soispha): The following line should be a real display call, but how do you + // format a HashMap? + CommandTransferValue::Table(table) => f.write_str(&format!("{:#?}", table)), + } + } +} + +pub struct LuaCommandManager { + lua_command_tx: mpsc::Sender, +} + +impl From for CommandTransferValue { + fn from(s: String) -> Self { + Self::String(s.to_owned()) + } +} +impl From for CommandTransferValue { + fn from(s: f64) -> Self { + Self::Number(s.to_owned()) + } +} +impl From for CommandTransferValue { + fn from(s: i64) -> Self { + Self::Integer(s.to_owned()) + } +} +impl From> for CommandTransferValue { + fn from(s: HashMap) -> Self { + Self::Table(s.to_owned()) + } +} +impl From for CommandTransferValue { + fn from(s: bool) -> Self { + Self::Boolean(s.to_owned()) + } +} +impl From<()> for CommandTransferValue { + fn from(_: ()) -> Self { + Self::Nil + } +} + +impl LuaCommandManager { + pub async fn execute_code(&self, code: String) { + self.lua_command_tx + .send(code) + .await + .expect("The receiver should not be dropped at this time"); + } + + pub fn new(event_call_tx: mpsc::Sender) -> Self { + info!("Spawning lua code execution thread..."); + let (lua_command_tx, mut lua_command_rx) = mpsc::channel::(256); + thread::spawn(move || { + let rt = Builder::new_current_thread().enable_all().build().expect( + "Should always be able to build \ + tokio runtime for lua command handling", + ); + let local = LocalSet::new(); + local.spawn_local(async move { + info!( + "Lua command handling initialized, \ + waiting for commands.." + ); + while let Some(command) = lua_command_rx.recv().await { + debug!("Recieved lua code: {}", &command); + let local_event_call_tx = event_call_tx.clone(); + + task::spawn_local(async move { + exec_lua_command(&command, local_event_call_tx) + .await + .expect( + "This should return all relevent errors \ + by other messages, \ + this should never error", + ); + }); + } + }); + rt.block_on(local); + }); + + LuaCommandManager { lua_command_tx } + } +} + +async fn exec_lua_command(command: &str, event_call_tx: mpsc::Sender) -> Result<()> { + let second_event_call_tx = event_call_tx.clone(); + let lua = LUA + .get_or_init(|| { + Mutex::new(add_lua_functions_to_globals( + mlua::Lua::new(), + second_event_call_tx, + )) + }) + .lock() + .await; + + info!("Recieved code to execute: `{}`, executing...", &command); + let output = lua.load(command).eval_async::().await; + match output { + Ok(out) => { + let to_string_fn: Function = lua.globals().get("tostring").expect("This always exists"); + let output: String = to_string_fn.call(out).expect("tostring should not error"); + info!("Function `{}` returned: `{}`", command, &output); + + event_call_tx + .send(Event::CommandEvent(Command::DisplayOutput(output), None)) + .await + .context("Failed to send lua output command")? + } + Err(err) => { + error!("Function `{}` returned error: `{}`", command, err); + event_call_tx + .send(Event::CommandEvent( + Command::RaiseError(err.to_string()), + None, + )) + .await?; + } + }; + Ok(()) +} diff --git a/src/tui_app/app/command_interface/mod.rs b/src/tui_app/app/command_interface/mod.rs index b3ebb88..264b2c0 100644 --- a/src/tui_app/app/command_interface/mod.rs +++ b/src/tui_app/app/command_interface/mod.rs @@ -1,9 +1,14 @@ // Use `cargo expand app::command_interface` for an overview of the file contents -pub mod lua_command_manger; +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] diff --git a/src/tui_app/app/events/event_types/event/handlers/command.rs b/src/tui_app/app/events/event_types/event/handlers/command.rs index 241a533..26deb6b 100644 --- a/src/tui_app/app/events/event_types/event/handlers/command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/command.rs @@ -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>, + output_callback: Option>, ) -> Result { - // 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)))?; } }; } diff --git a/src/tui_app/app/events/event_types/event/handlers/lua_command.rs b/src/tui_app/app/events/event_types/event/handlers/lua_command.rs index 6e7460e..9111330 100644 --- a/src/tui_app/app/events/event_types/event/handlers/lua_command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/lua_command.rs @@ -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 { +pub async fn handle( + app: &mut App<'_>, + command: String, +) -> Result { trace!("Recieved ci command: `{command}`; executing.."); - app.lua_command_tx.send(command).await?; + app.lua.execute_code(command).await; Ok(EventStatus::Ok) } diff --git a/src/tui_app/app/events/event_types/event/handlers/matrix.rs b/src/tui_app/app/events/event_types/event/handlers/matrix.rs index 5215a28..2a86034 100644 --- a/src/tui_app/app/events/event_types/event/handlers/matrix.rs +++ b/src/tui_app/app/events/event_types/event/handlers/matrix.rs @@ -1,9 +1,12 @@ -use matrix_sdk::deserialized_responses::SyncResponse; use anyhow::Result; +use matrix_sdk::deserialized_responses::SyncResponse; use crate::app::{events::event_types::EventStatus, App}; -pub async fn handle<'a>(app: &mut App<'a>, sync: &SyncResponse) -> Result { +pub async fn handle( + app: &mut App<'_>, + sync: &SyncResponse, +) -> Result { for (m_room_id, m_room) in sync.rooms.join.iter() { let room = match app.status.get_room_mut(m_room_id) { Some(r) => r, diff --git a/src/tui_app/app/events/event_types/event/handlers/setup.rs b/src/tui_app/app/events/event_types/event/handlers/setup.rs index 8e22128..d879e99 100644 --- a/src/tui_app/app/events/event_types/event/handlers/setup.rs +++ b/src/tui_app/app/events/event_types/event/handlers/setup.rs @@ -1,9 +1,15 @@ use anyhow::{bail, Context, Result}; use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent}; -use crate::{app::{events::event_types::EventStatus, App}, ui::setup}; +use crate::{ + app::{events::event_types::EventStatus, App}, + ui::setup, +}; -pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { +pub async fn handle( + app: &mut App<'_>, + input_event: &CrosstermEvent, +) -> Result { let ui = match &mut app.ui.setup_ui { Some(ui) => ui, None => bail!("SetupUI instance not found"), diff --git a/src/tui_app/app/events/event_types/event/mod.rs b/src/tui_app/app/events/event_types/event/mod.rs index 309e506..2917519 100644 --- a/src/tui_app/app/events/event_types/event/mod.rs +++ b/src/tui_app/app/events/event_types/event/mod.rs @@ -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,33 +15,35 @@ use super::EventStatus; pub enum Event { InputEvent(CrosstermEvent), MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse), - CommandEvent(Command, Option>), + CommandEvent(Command, Option>), LuaCommand(String), } impl Event { - pub async fn handle(&self, app: &mut App<'_>) -> Result { + pub async fn handle(self, app: &mut App<'_>) -> Result { trace!("Recieved event to handle: `{:#?}`", &self); - match &self { - Event::MatrixEvent(event) => matrix::handle(app, event) + match self { + Event::MatrixEvent(event) => matrix::handle(app, &event) .await .with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)), - Event::CommandEvent(event, callback_tx) => 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()) .await .with_context(|| format!("Failed to handle lua code: `{}`", lua_code)), + Event::InputEvent(event) => match app.status.state() { State::Normal => main::handle_normal(app, event) .await .with_context(|| format!("Failed to handle input event: `{:#?}`", event)), State::Insert => main::handle_insert(app, event) + 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)), }, diff --git a/src/tui_app/app/mod.rs b/src/tui_app/app/mod.rs index f70f91f..ad52a11 100644 --- a/src/tui_app/app/mod.rs +++ b/src/tui_app/app/mod.rs @@ -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, + lua: LuaCommandManager, } impl App<'_> { pub fn new() -> Result { - fn set_up_lua(event_call_tx: mpsc::Sender) -> mpsc::Sender { - async fn exec_lua_command( - command: &str, - event_call_tx: mpsc::Sender, - ) -> Result<()> { - let second_event_call_tx = event_call_tx.clone(); - let lua = LUA - .get_or_init(|| { - Mutex::new(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::() - .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> = OnceCell::new(); - - let (lua_command_tx, mut rx) = mpsc::channel::(256); - - thread::spawn(move || { - let rt = Builder::new_current_thread().enable_all().build().expect( - "Should always be able to build tokio runtime for lua command handling", - ); - let local = LocalSet::new(); - local.spawn_local(async move { - info!("Lua command handling initialized, waiting for commands.."); - while let Some(command) = 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), }) } From c0a1fc0a021029c804bc931818f746dcd02e65eb Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 21:08:56 +0200 Subject: [PATCH 12/18] 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). --- src/tui_app/app/command_interface/mod.rs | 6 ++++++ .../app/events/event_types/event/handlers/command.rs | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/tui_app/app/command_interface/mod.rs b/src/tui_app/app/command_interface/mod.rs index 264b2c0..c382f73 100644 --- a/src/tui_app/app/command_interface/mod.rs +++ b/src/tui_app/app/command_interface/mod.rs @@ -13,6 +13,12 @@ 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, diff --git a/src/tui_app/app/events/event_types/event/handlers/command.rs b/src/tui_app/app/events/event_types/event/handlers/command.rs index 26deb6b..9e257c4 100644 --- a/src/tui_app/app/events/event_types/event/handlers/command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/command.rs @@ -116,10 +116,16 @@ pub async fn handle( 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::Help(_) => todo!(), Command::RaiseError(err) => { send_error_output!(err); EventStatus::Ok - }, + } }) } From 909fc01a4845c965622976a01a96e3e1237794b4 Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 21:12:17 +0200 Subject: [PATCH 13/18] 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. --- src/tui_app/app/command_interface/mod.rs | 4 ++++ .../app/events/event_types/event/handlers/command.rs | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/tui_app/app/command_interface/mod.rs b/src/tui_app/app/command_interface/mod.rs index c382f73..0418470 100644 --- a/src/tui_app/app/command_interface/mod.rs +++ b/src/tui_app/app/command_interface/mod.rs @@ -22,6 +22,10 @@ struct Commands { /// 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(), diff --git a/src/tui_app/app/events/event_types/event/handlers/command.rs b/src/tui_app/app/events/event_types/event/handlers/command.rs index 9e257c4..1672e96 100644 --- a/src/tui_app/app/events/event_types/event/handlers/command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/command.rs @@ -122,6 +122,12 @@ pub async fn handle( 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); From f7b161fb55645c4984ea7dbd2b24b79ad9b0a9cb Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 21:14:36 +0200 Subject: [PATCH 14/18] 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. --- .../app/events/event_types/event/handlers/command.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tui_app/app/events/event_types/event/handlers/command.rs b/src/tui_app/app/events/event_types/event/handlers/command.rs index 1672e96..554c91a 100644 --- a/src/tui_app/app/events/event_types/event/handlers/command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/command.rs @@ -67,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"); From a80245c523ad8505e7d388802619857c6fc7ad98 Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 21:15:53 +0200 Subject: [PATCH 15/18] 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. --- src/tui_app/app/events/event_types/event/handlers/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui_app/app/events/event_types/event/handlers/command.rs b/src/tui_app/app/events/event_types/event/handlers/command.rs index 554c91a..69c43e1 100644 --- a/src/tui_app/app/events/event_types/event/handlers/command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/command.rs @@ -112,12 +112,12 @@ 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) => { From 258f784098c81759a1ae992d35e908ebe72cc167 Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 21:17:37 +0200 Subject: [PATCH 16/18] Fix(handlers::main): Don't close the cli after an entered command --- src/tui_app/app/events/event_types/event/handlers/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tui_app/app/events/event_types/event/handlers/main.rs b/src/tui_app/app/events/event_types/event/handlers/main.rs index ebb600f..4462257 100644 --- a/src/tui_app/app/events/event_types/event/handlers/main.rs +++ b/src/tui_app/app/events/event_types/event/handlers/main.rs @@ -10,7 +10,10 @@ use crate::{ ui::central, }; -pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { +pub async fn handle( + app: &mut App<'_>, + input_event: &CrosstermEvent, +) -> Result { match input_event { CrosstermEvent::Key(KeyEvent { code: KeyCode::Esc, .. @@ -155,9 +158,6 @@ pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> R .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 From 855d4876933c3819911417d44f6ebb6b04fe1c6c Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 21:19:50 +0200 Subject: [PATCH 17/18] Refactor(treewide): Remove the useless `tui_app` directory --- src/{tui_app => }/accounts/mod.rs | 0 .../app/command_interface/lua_command_manager/mod.rs | 0 src/{tui_app => }/app/command_interface/mod.rs | 0 .../app/events/event_types/event/handlers/command.rs | 0 .../app/events/event_types/event/handlers/lua_command.rs | 0 .../app/events/event_types/event/handlers/main.rs | 0 .../app/events/event_types/event/handlers/matrix.rs | 0 .../app/events/event_types/event/handlers/mod.rs | 0 .../app/events/event_types/event/handlers/setup.rs | 0 src/{tui_app => }/app/events/event_types/event/mod.rs | 0 src/{tui_app => }/app/events/event_types/event_status.rs | 0 src/{tui_app => }/app/events/event_types/mod.rs | 0 src/{tui_app => }/app/events/mod.rs | 0 src/{tui_app => }/app/mod.rs | 0 src/{tui_app => }/app/status.rs | 0 src/main.rs | 8 +++----- src/tui_app/mod.rs | 7 ------- src/{tui_app => }/ui/central/mod.rs | 0 src/{tui_app => }/ui/central/update/mod.rs | 0 .../ui/central/update/widgets/command_monitor.rs | 0 src/{tui_app => }/ui/central/update/widgets/messages.rs | 0 src/{tui_app => }/ui/central/update/widgets/mod.rs | 0 src/{tui_app => }/ui/central/update/widgets/room_info.rs | 0 src/{tui_app => }/ui/central/update/widgets/rooms.rs | 0 src/{tui_app => }/ui/central/update/widgets/status.rs | 0 src/{tui_app => }/ui/mod.rs | 0 src/{tui_app => }/ui/setup.rs | 0 27 files changed, 3 insertions(+), 12 deletions(-) rename src/{tui_app => }/accounts/mod.rs (100%) rename src/{tui_app => }/app/command_interface/lua_command_manager/mod.rs (100%) rename src/{tui_app => }/app/command_interface/mod.rs (100%) rename src/{tui_app => }/app/events/event_types/event/handlers/command.rs (100%) rename src/{tui_app => }/app/events/event_types/event/handlers/lua_command.rs (100%) rename src/{tui_app => }/app/events/event_types/event/handlers/main.rs (100%) rename src/{tui_app => }/app/events/event_types/event/handlers/matrix.rs (100%) rename src/{tui_app => }/app/events/event_types/event/handlers/mod.rs (100%) rename src/{tui_app => }/app/events/event_types/event/handlers/setup.rs (100%) rename src/{tui_app => }/app/events/event_types/event/mod.rs (100%) rename src/{tui_app => }/app/events/event_types/event_status.rs (100%) rename src/{tui_app => }/app/events/event_types/mod.rs (100%) rename src/{tui_app => }/app/events/mod.rs (100%) rename src/{tui_app => }/app/mod.rs (100%) rename src/{tui_app => }/app/status.rs (100%) delete mode 100644 src/tui_app/mod.rs rename src/{tui_app => }/ui/central/mod.rs (100%) rename src/{tui_app => }/ui/central/update/mod.rs (100%) rename src/{tui_app => }/ui/central/update/widgets/command_monitor.rs (100%) rename src/{tui_app => }/ui/central/update/widgets/messages.rs (100%) rename src/{tui_app => }/ui/central/update/widgets/mod.rs (100%) rename src/{tui_app => }/ui/central/update/widgets/room_info.rs (100%) rename src/{tui_app => }/ui/central/update/widgets/rooms.rs (100%) rename src/{tui_app => }/ui/central/update/widgets/status.rs (100%) rename src/{tui_app => }/ui/mod.rs (100%) rename src/{tui_app => }/ui/setup.rs (100%) diff --git a/src/tui_app/accounts/mod.rs b/src/accounts/mod.rs similarity index 100% rename from src/tui_app/accounts/mod.rs rename to src/accounts/mod.rs diff --git a/src/tui_app/app/command_interface/lua_command_manager/mod.rs b/src/app/command_interface/lua_command_manager/mod.rs similarity index 100% rename from src/tui_app/app/command_interface/lua_command_manager/mod.rs rename to src/app/command_interface/lua_command_manager/mod.rs diff --git a/src/tui_app/app/command_interface/mod.rs b/src/app/command_interface/mod.rs similarity index 100% rename from src/tui_app/app/command_interface/mod.rs rename to src/app/command_interface/mod.rs diff --git a/src/tui_app/app/events/event_types/event/handlers/command.rs b/src/app/events/event_types/event/handlers/command.rs similarity index 100% rename from src/tui_app/app/events/event_types/event/handlers/command.rs rename to src/app/events/event_types/event/handlers/command.rs diff --git a/src/tui_app/app/events/event_types/event/handlers/lua_command.rs b/src/app/events/event_types/event/handlers/lua_command.rs similarity index 100% rename from src/tui_app/app/events/event_types/event/handlers/lua_command.rs rename to src/app/events/event_types/event/handlers/lua_command.rs diff --git a/src/tui_app/app/events/event_types/event/handlers/main.rs b/src/app/events/event_types/event/handlers/main.rs similarity index 100% rename from src/tui_app/app/events/event_types/event/handlers/main.rs rename to src/app/events/event_types/event/handlers/main.rs diff --git a/src/tui_app/app/events/event_types/event/handlers/matrix.rs b/src/app/events/event_types/event/handlers/matrix.rs similarity index 100% rename from src/tui_app/app/events/event_types/event/handlers/matrix.rs rename to src/app/events/event_types/event/handlers/matrix.rs diff --git a/src/tui_app/app/events/event_types/event/handlers/mod.rs b/src/app/events/event_types/event/handlers/mod.rs similarity index 100% rename from src/tui_app/app/events/event_types/event/handlers/mod.rs rename to src/app/events/event_types/event/handlers/mod.rs diff --git a/src/tui_app/app/events/event_types/event/handlers/setup.rs b/src/app/events/event_types/event/handlers/setup.rs similarity index 100% rename from src/tui_app/app/events/event_types/event/handlers/setup.rs rename to src/app/events/event_types/event/handlers/setup.rs diff --git a/src/tui_app/app/events/event_types/event/mod.rs b/src/app/events/event_types/event/mod.rs similarity index 100% rename from src/tui_app/app/events/event_types/event/mod.rs rename to src/app/events/event_types/event/mod.rs diff --git a/src/tui_app/app/events/event_types/event_status.rs b/src/app/events/event_types/event_status.rs similarity index 100% rename from src/tui_app/app/events/event_types/event_status.rs rename to src/app/events/event_types/event_status.rs diff --git a/src/tui_app/app/events/event_types/mod.rs b/src/app/events/event_types/mod.rs similarity index 100% rename from src/tui_app/app/events/event_types/mod.rs rename to src/app/events/event_types/mod.rs diff --git a/src/tui_app/app/events/mod.rs b/src/app/events/mod.rs similarity index 100% rename from src/tui_app/app/events/mod.rs rename to src/app/events/mod.rs diff --git a/src/tui_app/app/mod.rs b/src/app/mod.rs similarity index 100% rename from src/tui_app/app/mod.rs rename to src/app/mod.rs diff --git a/src/tui_app/app/status.rs b/src/app/status.rs similarity index 100% rename from src/tui_app/app/status.rs rename to src/app/status.rs diff --git a/src/main.rs b/src/main.rs index b14a469..0c8e5b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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<()> { diff --git a/src/tui_app/mod.rs b/src/tui_app/mod.rs deleted file mode 100644 index 8da28d6..0000000 --- a/src/tui_app/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod app; -pub mod ui; -pub mod accounts; - -//pub use app::*; -//pub use ui::*; -//pub use accounts::*; diff --git a/src/tui_app/ui/central/mod.rs b/src/ui/central/mod.rs similarity index 100% rename from src/tui_app/ui/central/mod.rs rename to src/ui/central/mod.rs diff --git a/src/tui_app/ui/central/update/mod.rs b/src/ui/central/update/mod.rs similarity index 100% rename from src/tui_app/ui/central/update/mod.rs rename to src/ui/central/update/mod.rs diff --git a/src/tui_app/ui/central/update/widgets/command_monitor.rs b/src/ui/central/update/widgets/command_monitor.rs similarity index 100% rename from src/tui_app/ui/central/update/widgets/command_monitor.rs rename to src/ui/central/update/widgets/command_monitor.rs diff --git a/src/tui_app/ui/central/update/widgets/messages.rs b/src/ui/central/update/widgets/messages.rs similarity index 100% rename from src/tui_app/ui/central/update/widgets/messages.rs rename to src/ui/central/update/widgets/messages.rs diff --git a/src/tui_app/ui/central/update/widgets/mod.rs b/src/ui/central/update/widgets/mod.rs similarity index 100% rename from src/tui_app/ui/central/update/widgets/mod.rs rename to src/ui/central/update/widgets/mod.rs diff --git a/src/tui_app/ui/central/update/widgets/room_info.rs b/src/ui/central/update/widgets/room_info.rs similarity index 100% rename from src/tui_app/ui/central/update/widgets/room_info.rs rename to src/ui/central/update/widgets/room_info.rs diff --git a/src/tui_app/ui/central/update/widgets/rooms.rs b/src/ui/central/update/widgets/rooms.rs similarity index 100% rename from src/tui_app/ui/central/update/widgets/rooms.rs rename to src/ui/central/update/widgets/rooms.rs diff --git a/src/tui_app/ui/central/update/widgets/status.rs b/src/ui/central/update/widgets/status.rs similarity index 100% rename from src/tui_app/ui/central/update/widgets/status.rs rename to src/ui/central/update/widgets/status.rs diff --git a/src/tui_app/ui/mod.rs b/src/ui/mod.rs similarity index 100% rename from src/tui_app/ui/mod.rs rename to src/ui/mod.rs diff --git a/src/tui_app/ui/setup.rs b/src/ui/setup.rs similarity index 100% rename from src/tui_app/ui/setup.rs rename to src/ui/setup.rs From 84c13fd6f8de3c22a09107de67f20651fd70bd9d Mon Sep 17 00:00:00 2001 From: Soispha Date: Wed, 26 Jul 2023 22:25:08 +0200 Subject: [PATCH 18/18] Fix(treewide): Resolve merge conflicts --- .../event_types/event/handlers/command.rs | 14 +++++----- .../events/event_types/event/handlers/main.rs | 2 +- src/app/events/event_types/event/mod.rs | 26 ++++++++++--------- src/app/status.rs | 2 +- src/ui/central/update/mod.rs | 2 +- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/app/events/event_types/event/handlers/command.rs b/src/app/events/event_types/event/handlers/command.rs index 69c43e1..de301f9 100644 --- a/src/app/events/event_types/event/handlers/command.rs +++ b/src/app/events/event_types/event/handlers/command.rs @@ -4,14 +4,14 @@ use anyhow::{Error, Result}; use cli_log::{trace, warn}; use tokio::sync::oneshot; -use crate::app::{ +use crate::{app::{ command_interface::{ lua_command_manager::{CommandTransferValue, Table}, Command, }, events::event_types::EventStatus, - App, -}; + App, status::State, +}, ui::central::InputPosition}; pub async fn handle( app: &mut App<'_>, @@ -99,14 +99,14 @@ pub async fn handle( Command::SetModeNormal => { app.status.set_state(State::Normal); - set_status_output!("Set input mode to Normal"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Set input mode to Normal"); + EventStatus::Ok } Command::SetModeInsert => { app.status.set_state(State::Insert); app.ui.set_input_position(InputPosition::MessageCompose); - set_status_output!("Set input mode to Insert"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Set input mode to Insert"); + EventStatus::Ok } Command::RoomMessageSend(msg) => { diff --git a/src/app/events/event_types/event/handlers/main.rs b/src/app/events/event_types/event/handlers/main.rs index 4462257..6ddcce5 100644 --- a/src/app/events/event_types/event/handlers/main.rs +++ b/src/app/events/event_types/event/handlers/main.rs @@ -10,7 +10,7 @@ use crate::{ ui::central, }; -pub async fn handle( +pub async fn handle_normal( app: &mut App<'_>, input_event: &CrosstermEvent, ) -> Result { diff --git a/src/app/events/event_types/event/mod.rs b/src/app/events/event_types/event/mod.rs index 2917519..27bba87 100644 --- a/src/app/events/event_types/event/mod.rs +++ b/src/app/events/event_types/event/mod.rs @@ -5,7 +5,11 @@ use cli_log::trace; use crossterm::event::Event as CrosstermEvent; use tokio::sync::oneshot; -use crate::app::{command_interface::{Command, lua_command_manager::CommandTransferValue}, status::State, App}; +use crate::app::{ + command_interface::{lua_command_manager::CommandTransferValue, Command}, + status::State, + App, +}; use self::handlers::{command, lua_command, main, matrix, setup}; @@ -34,18 +38,16 @@ impl Event { .await .with_context(|| format!("Failed to handle lua code: `{}`", lua_code)), - Event::InputEvent(event) => match app.status.state() { - State::Normal => main::handle_normal(app, event) - .await - .with_context(|| format!("Failed to handle input event: `{:#?}`", event)), - State::Insert => main::handle_insert(app, event) - State::Main => main::handle(app, &event) - .await - .with_context(|| format!("Failed to handle input event: `{:#?}`", event)), - State::Setup => setup::handle(app, &event) - .await - .with_context(|| format!("Failed to handle input event: `{:#?}`", event)), + State::Normal => main::handle_normal(app, &event).await.with_context(|| { + format!("Failed to handle input (normal) event: `{:#?}`", event) + }), + State::Insert => main::handle_insert(app, &event).await.with_context(|| { + format!("Failed to handle input (insert) event: `{:#?}`", event) + }), + State::Setup => setup::handle(app, &event).await.with_context(|| { + format!("Failed to handle input (setup) event: `{:#?}`", event) + }), }, } } diff --git a/src/app/status.rs b/src/app/status.rs index 770249a..9ce211c 100644 --- a/src/app/status.rs +++ b/src/app/status.rs @@ -151,7 +151,7 @@ impl Status { }; Self { - state: State::None, + state: State::Normal, account_name: "".to_owned(), account_user_id: "".to_owned(), client, diff --git a/src/ui/central/update/mod.rs b/src/ui/central/update/mod.rs index 67cfb31..fcd06b0 100644 --- a/src/ui/central/update/mod.rs +++ b/src/ui/central/update/mod.rs @@ -1,7 +1,7 @@ use std::cmp; use anyhow::{Context, Result}; -use tui::layout::{Constraint, Direction, Layout}; +use tui::{layout::{Constraint, Direction, Layout}, widgets::{Paragraph, Block, Borders}, style::{Style, Color}}; use crate::app::status::Status;