Compare commits

..

4 Commits

Author SHA1 Message Date
Benedikt Peetz a6d176b6e9
Fix(event_handlers/ci_output): Remove because not needed anymore 2023-07-20 21:50:30 +02:00
Benedikt Peetz 20c751fd7f
Fix(treewide): Update codebase to new lua_macros api 2023-07-20 21:49:56 +02:00
Benedikt Peetz c243c90cab
Fix(lua_macros): Rework to support new command focused layout
The internal commands are wrapped by a lua api, which allows to write
the whole thing a little bit more language agnostic.
2023-07-20 21:43:41 +02:00
Benedikt Peetz 27e3ff228c
Build(flake): Make it easier to switch to nightly 2023-07-20 21:39:38 +02:00
15 changed files with 376 additions and 225 deletions

View File

@ -18,4 +18,4 @@ tokio-util = "0.7"
serde = "1.0" serde = "1.0"
cli-log = "2.0" cli-log = "2.0"
indexmap = "2.0.0" indexmap = "2.0.0"
mlua = { version = "0.8.9", features = ["lua54", "async"] } mlua = { version = "0.8.9", features = ["lua54", "async", "send"] }

View File

@ -45,10 +45,13 @@
overlays = [(import rust-overlay)]; overlays = [(import rust-overlay)];
}; };
#rust-nightly = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default); nightly = true;
rust-stable = pkgs.rust-bin.stable.latest.default; rust =
if nightly
then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)
else pkgs.rust-bin.stable.latest.default;
craneLib = (crane.mkLib pkgs).overrideToolchain rust-stable; craneLib = (crane.mkLib pkgs).overrideToolchain rust;
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
pkg-config pkg-config
@ -79,7 +82,7 @@
statix statix
ltex-ls ltex-ls
rust-stable rust
rust-analyzer rust-analyzer
cargo-edit cargo-edit
cargo-expand cargo-expand

View File

@ -3,61 +3,67 @@ mod struct_to_ci_enum;
use mark_as_ci_command::generate_final_function; use mark_as_ci_command::generate_final_function;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote; use quote::{format_ident, quote};
use struct_to_ci_enum::{generate_command_enum, generate_generate_ci_function}; use struct_to_ci_enum::{generate_command_enum, generate_generate_ci_function, generate_help_function};
use syn::{self, ItemFn, Field, parse::Parser}; use syn::{self, parse_quote, parse_str, DeriveInput, FieldMutability, ItemFn, Token, Visibility};
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn turn_struct_to_ci_commands(_attrs: TokenStream, input: TokenStream) -> TokenStream { pub fn turn_struct_to_ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree // Construct a representation of Rust code as a syntax tree
// that we can manipulate // that we can manipulate
let input = syn::parse(input) let mut input: DeriveInput = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code");
.expect("This should always be valid rust code, as it's extracted from direct code");
let mut named_fields = match &input.data {
syn::Data::Struct(input) => match &input.fields {
syn::Fields::Named(named_fields) => named_fields,
_ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"),
},
_ => unimplemented!("The macro only works for structs"),
}
.to_owned();
let attr_parsed = parse_quote! {
/// This is a help function
};
named_fields.named.push(syn::Field {
attrs: vec![attr_parsed],
// attrs: attr_parser
// .parse("#[doc = r\"This is a help function\"]".to_token_stream().into())
// .expect("See reason for other one"),
vis: Visibility::Inherited,
mutability: FieldMutability::None,
ident: Some(format_ident!("help")),
colon_token: Some(Token![:](Span::call_site())),
ty: parse_str("fn(Option<String>) -> String").expect("This is static and valid rust code"),
});
match &mut input.data {
syn::Data::Struct(input) => input.fields = syn::Fields::Named(named_fields.clone()),
_ => unreachable!("This was a DataStruct before"),
};
// Build the trait implementation // Build the trait implementation
let generate_ci_function: TokenStream2 = generate_generate_ci_function(&input); let generate_ci_function: TokenStream2 = generate_generate_ci_function(&input);
let command_enum = generate_command_enum(&input); let command_enum = generate_command_enum(&named_fields);
let help_function = generate_help_function(&named_fields);
quote! { quote! {
#command_enum #command_enum
#generate_ci_function #generate_ci_function
//#help_function
} }
.into() .into()
} }
/// Generate a default lua function implementation.
// TODO: Is this needed?
#[proc_macro_attribute]
pub fn gen_lua_function(_attrs: TokenStream, input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let parser = Field::parse_named;
let input = parser.parse(input)
.expect("This is only defined for named fileds.");
quote! {
#input
}
.into()
}
/// Turn a function into a valid ci command function
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn ci_command(_attrs: TokenStream, input: TokenStream) -> TokenStream { pub fn ci_command(_attrs: TokenStream, input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree let mut input: ItemFn = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code");
// that we can manipulate let output_function = generate_final_function(&mut input);
let mut input: ItemFn = syn::parse(input) output_function.into()
.expect("This should always be valid rust code, as it's extracted from direct code");
// Build the trait implementation
let output_function: TokenStream2 = generate_final_function(&mut input);
//panic!("{:#?}", output_function);
quote! {
#output_function
}
.into()
} }

View File

@ -58,18 +58,13 @@ fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn {
// There should only be two segments (the type is <String, rlua::Error>) // There should only be two segments (the type is <String, rlua::Error>)
if filtered_paths.len() > 2 { if filtered_paths.len() > 2 {
unreachable!( unreachable!("There should be no more than two filtered_output, but got: {:#?}", filtered_paths)
"There should be no more than two filtered_output, but got: {:#?}",
filtered_paths
)
} else if filtered_paths.len() <= 0 { } else if filtered_paths.len() <= 0 {
unreachable!( unreachable!("There should be more than zero filtered_output, but got: {:#?}", filtered_paths)
"There should be more than zero filtered_output, but got: {:#?}",
filtered_paths
)
} }
if filtered_paths.len() == 2 { if filtered_paths.len() == 2 {
// There is something else than rlua // There is something else than mlua::Error
let gen_type = if let GenericArgument::Type(ret_type) = let gen_type = if let GenericArgument::Type(ret_type) =
filtered_paths filtered_paths
.first() .first()
@ -97,36 +92,64 @@ fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn {
} }
_ => unimplemented!("Only for path types"), _ => unimplemented!("Only for path types"),
}; };
let send_data = match return_type { let send_data = match return_type {
ReturnType::Default => { ReturnType::Default => {
quote! { quote! {
{ {
Event::CommandEvent(Command::#function_name_pascal) Event::CommandEvent(Command::#function_name_pascal, None)
} }
} }
} }
ReturnType::Type(_, _) => { ReturnType::Type(_, _) => {
quote! { quote! {
{ {
Event::CommandEvent(Command::#function_name_pascal(input_str.clone())) 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! { quote! {
{ {
let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::<String>(256);
let tx: let tx:
core::cell::Ref< core::cell::Ref<tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>> =
tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>
> =
lua lua
.app_data_ref() .app_data_ref()
.expect("This exists, it was set before"); .expect("This exists, it was set before");
(*tx) (*tx)
.send(#send_data) .try_send(#send_data)
.await
.expect("This should work, as the reciever is not dropped"); .expect("This should work, as the reciever is not dropped");
#output_return
} }
} }
} }
@ -134,6 +157,7 @@ fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn {
let tx_send_block: Block = let tx_send_block: Block =
syn::parse(tx_send.into()).expect("This is a static string, it will always parse"); syn::parse(tx_send.into()).expect("This is a static string, it will always parse");
let tx_send_expr_block = ExprBlock { let tx_send_expr_block = ExprBlock {
attrs: vec![], attrs: vec![],
label: None, label: None,

View File

@ -0,0 +1,65 @@
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::Type;
pub fn generate_command_enum(input: &syn::FieldsNamed) -> TokenStream2 {
let input_tokens: TokenStream2 = input
.named
.iter()
.map(|field| -> TokenStream2 {
let field_ident = field
.ident
.as_ref()
.expect("These are only the named fields, thus they should all have a ident.");
let enum_variant_type = match &field.ty {
syn::Type::BareFn(function) => {
let return_path = &function.inputs;
let input_type: Option<Type> = if return_path.len() == 1 {
Some(
return_path
.last()
.expect("The last element exists")
.ty
.clone(),
)
} else if return_path.len() == 0 {
None
} else {
panic!("The Function can only take on argument, or none");
};
input_type
}
_ => unimplemented!("This is only implemented for bare function types"),
};
let enum_variant_name = format_ident!(
"{}",
field_ident
.to_string()
.from_case(Case::Snake)
.to_case(Case::Pascal)
);
if enum_variant_type.is_some() {
quote! {
#enum_variant_name (#enum_variant_type),
}
.into()
} else {
quote! {
#enum_variant_name,
}
}
})
.collect();
let gen = quote! {
#[derive(Debug)]
pub enum Command {
#input_tokens
}
};
gen.into()
}

View File

@ -1,63 +1,13 @@
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use syn::{self, ReturnType}; use syn::{parse_quote, ReturnType, Type};
pub fn generate_generate_ci_function(input: &syn::DeriveInput) -> TokenStream2 { fn generate_ci_function_exposure(field: &syn::Field) -> TokenStream2 {
let mut functions_to_generate: Vec<TokenStream2> = vec![];
let input_tokens: TokenStream2 = match &input.data {
syn::Data::Struct(input) => match &input.fields {
syn::Fields::Named(named_fields) => named_fields
.named
.iter()
.map(|field| -> TokenStream2 {
if field.attrs.iter().any(|attribute| {
attribute.path()
== &syn::parse_str::<syn::Path>("gen_default_lua_function")
.expect("This is valid rust code")
}) {
let function_name = field
.ident
.as_ref()
.expect("These are only the named field, thus they all should have a name.");
functions_to_generate.push(quote! {
#[ci_command]
async fn #function_name(lua: &mlua::Lua, input_str: String) -> Result<(), mlua::Error> {
Ok(())
}
});
generate_ci_part(field)
} else {
generate_ci_part(field)
}
})
.collect(),
_ => unimplemented!("Only implemented for named fileds"),
},
_ => unimplemented!("Only implemented for structs"),
};
let functions_to_generate: TokenStream2 = functions_to_generate.into_iter().collect();
let gen = quote! {
pub fn generate_ci_functions(
lua: &mut mlua::Lua,
tx: tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>)
{
lua.set_app_data(tx);
let globals = lua.globals();
#input_tokens
}
#functions_to_generate
};
gen.into()
}
fn generate_ci_part(field: &syn::Field) -> TokenStream2 {
let field_ident = field let field_ident = field
.ident .ident
.as_ref() .as_ref()
.expect("These are only the named field, thus they all should have a name."); .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_ident = format_ident!("fun_{}", field_ident);
let function_name = format!("{}", field_ident); let function_name = format!("{}", field_ident);
quote! { quote! {
@ -79,62 +29,77 @@ fn generate_ci_part(field: &syn::Field) -> TokenStream2 {
.into() .into()
} }
pub fn generate_command_enum(input: &syn::DeriveInput) -> TokenStream2 { pub fn generate_generate_ci_function(input: &syn::DeriveInput) -> TokenStream2 {
let input_tokens: TokenStream2 = match &input.data { let mut functions_to_generate: Vec<TokenStream2> = vec![];
let functions_to_export_in_lua: TokenStream2 = match &input.data {
syn::Data::Struct(input) => match &input.fields { syn::Data::Struct(input) => match &input.fields {
syn::Fields::Named(named_fields) => named_fields syn::Fields::Named(named_fields) => named_fields
.named .named
.iter() .iter()
.map(|field| -> TokenStream2 { .map(|field| -> TokenStream2 {
let field_ident = field let input_type = match &field.ty {
.ident syn::Type::BareFn(bare_fn) => {
.as_ref() if bare_fn.inputs.len() == 1 {
.expect("These are only the named field, thus they all should have a name."); bare_fn.inputs.last().expect("The last element exists").ty.clone()
} else if bare_fn.inputs.len() == 0 {
let enum_variant_type = match &field.ty { let input_type: Type = parse_quote! {()};
syn::Type::BareFn(function) => { input_type
let return_path: &ReturnType = &function.output; } else {
match return_path { panic!("The Function can only take on argument, or none");
ReturnType::Default => None,
ReturnType::Type(_, return_type) => Some(match *(return_type.to_owned()) {
syn::Type::Path(path_type) => path_type
.path
.get_ident()
.expect("A path should either be complete, or only conain one segment")
.to_owned(),
_ => unimplemented!("This is only implemented for path types"),
}),
} }
} }
_ => unimplemented!("This is only implemented for bare function types"), _ => 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 enum_variant_name = format_ident!( let function_name = field
"{}", .ident
field_ident.to_string().from_case(Case::Snake).to_case(Case::Pascal) .as_ref()
); .expect("These are only the named field, thus they all should have a name.");
if enum_variant_type.is_some() {
quote! { if let Some(ret_type) = return_type {
#enum_variant_name (#enum_variant_type), functions_to_generate.push(quote! {
#[ci_command]
async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<#ret_type, mlua::Error> {
} }
.into() });
} else { } else {
quote! { functions_to_generate.push(quote! {
#enum_variant_name, #[ci_command]
async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<(), mlua::Error> {
} }
});
} }
generate_ci_function_exposure(field)
}) })
.collect(), .collect(),
_ => unimplemented!("Only implemented for named fileds"), _ => unimplemented!("Only implemented for named fileds"),
}, },
_ => unimplemented!("Only implemented for structs"), _ => unimplemented!("Only implemented for structs"),
}; };
let functions_to_generate: TokenStream2 = functions_to_generate.into_iter().collect();
let gen = quote! { let gen = quote! {
#[derive(Debug)] pub fn generate_ci_functions(
pub enum Command { lua: &mut mlua::Lua,
#input_tokens tx: tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>
)
{
lua.set_app_data(tx);
let globals = lua.globals();
#functions_to_export_in_lua
} }
#functions_to_generate
}; };
gen.into() gen.into()
} }

View File

@ -0,0 +1,53 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
pub fn generate_help_function(input: &syn::FieldsNamed) -> TokenStream2 {
let input: Vec<_> = input.named.iter().collect();
let combined_help_text: TokenStream2 = input
.iter()
.map(|field| {
let attrs_with_doc: Vec<TokenStream2> = field
.attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc") {
let help_text = attr
.meta
.require_name_value()
.expect("This is a named value type, because all doc comments work this way")
.value
.clone();
Some(help_text.into_token_stream().into())
} else {
return None;
}
})
.collect();
if attrs_with_doc.len() == 0 {
// TODO there should be a better panic function, than the generic one
panic!(
"The command named: `{}`, does not provide a help message",
field.ident.as_ref().expect("These are all named")
);
} else {
let help_text_for_one_command_combined: TokenStream2 = attrs_with_doc.into_iter().collect();
return help_text_for_one_command_combined;
}
})
.collect();
quote! {
#[ci_command]
async fn help(
lua: &mlua::Lua,
input_str: Option<String>
) -> Result<String, mlua::Error> {
// TODO add a way to filter the help based on the input
let output = "These functions exist:\n";
output.push_str(#combined_help_text);
Ok(output)
}
}
}

View File

@ -0,0 +1,7 @@
pub mod generate_command_enum;
pub mod generate_generate_ci_function;
pub mod generate_help_function;
pub use generate_command_enum::*;
pub use generate_generate_ci_function::*;
pub use generate_help_function::*;

View File

@ -1,9 +1,11 @@
// FIXME: This file needs documentation with examples of how the proc macros work. // FIXME: This file needs documentation with examples of how the proc macros work.
// for now use `cargo expand app::command_interface` for an overview // for now use `cargo expand app::command_interface` for an overview
use lua_macros::{ci_command, turn_struct_to_ci_commands};
use super::events::event_types::Event; 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. /// 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. /// I.e. Rust should throw a compile error, when one field is added, but not a matching function.
/// ///
@ -20,41 +22,28 @@ use super::events::event_types::Event;
/// ``` /// ```
/// where $return_type is the type returned by the function (the only supported ones are right now /// where $return_type is the type returned by the function (the only supported ones are right now
/// `String` and `()`). /// `String` and `()`).
#[turn_struct_to_ci_commands]
#[turn_struct_to_ci_command_enum]
struct Commands { struct Commands {
/// Greets the user /// Greets the user
greet: fn(usize) -> String, greet: fn(String) -> String,
/// Closes the application /// Closes the application
#[gen_default_lua_function] //#[expose(lua)]
exit: fn(), exit: fn(),
/// Shows the command line /// Shows the command line
#[gen_default_lua_function]
command_line_show: fn(), command_line_show: fn(),
/// Hides the command line /// Hides the command line
#[gen_default_lua_function]
command_line_hide: fn(), command_line_hide: fn(),
/// Go to the next plane /// Go to the next plane
#[gen_default_lua_function]
cycle_planes: fn(), cycle_planes: fn(),
/// Go to the previous plane /// Go to the previous plane
#[gen_default_lua_function]
cycle_planes_rev: fn(), cycle_planes_rev: fn(),
/// Send a message to the current room /// Send a message to the current room
/// The send message is interpreted literally. /// The send message is interpreted literally.
room_message_send: fn(String) -> String, room_message_send: fn(String) -> String,
} }
#[ci_command]
async fn greet(lua: &mlua::Lua, input_str: String) -> Result<String, mlua::Error> {
Ok(format!("Name is {}", input_str))
}
#[ci_command]
async fn room_message_send(lua: &mlua::Lua, input_str: String) -> Result<String, mlua::Error> {
Ok(format!("Sent message: {}", input_str))
}

View File

@ -1,11 +0,0 @@
use anyhow::Result;
use cli_log::info;
use crate::app::{events::event_types::EventStatus, App};
pub async fn handle(app: &mut App<'_>, output: &String) -> Result<EventStatus> {
info!("Recieved command output: `{}`", output);
app.ui.set_command_output(output);
Ok(EventStatus::Ok)
}

View File

@ -2,39 +2,65 @@ use crate::app::{command_interface::Command, events::event_types::EventStatus, A
use anyhow::Result; use anyhow::Result;
use cli_log::info; use cli_log::info;
pub async fn handle(app: &mut App<'_>, command: &Command) -> Result<EventStatus> { pub async fn handle(
app: &mut App<'_>,
command: &Command,
send_output: bool,
) -> Result<(EventStatus, String)> {
macro_rules! set_status_output {
($str:expr) => {
if send_output {
app.ui.set_command_output($str);
}
};
($str:expr, $($args:ident),+) => {
if send_output {
app.ui.set_command_output(&format!($str, $($args),+));
}
};
}
info!("Handling command: {:#?}", command); info!("Handling command: {:#?}", command);
Ok(match command { Ok(match command {
Command::Exit => EventStatus::Terminate, Command::Exit => (
EventStatus::Terminate,
"Terminated the application".to_owned(),
),
Command::CommandLineShow => { Command::CommandLineShow => {
app.ui.cli_enable(); app.ui.cli_enable();
EventStatus::Ok set_status_output!("CLI online");
(EventStatus::Ok, "".to_owned())
} }
Command::CommandLineHide => { Command::CommandLineHide => {
app.ui.cli_disable(); app.ui.cli_disable();
EventStatus::Ok set_status_output!("CLI offline");
(EventStatus::Ok, "".to_owned())
} }
Command::CyclePlanes => { Command::CyclePlanes => {
app.ui.cycle_main_input_position(); app.ui.cycle_main_input_position();
EventStatus::Ok set_status_output!("Switched main input position");
(EventStatus::Ok, "".to_owned())
} }
Command::CyclePlanesRev => { Command::CyclePlanesRev => {
app.ui.cycle_main_input_position_rev(); app.ui.cycle_main_input_position_rev();
EventStatus::Ok set_status_output!("Switched main input position; reversed");
(EventStatus::Ok, "".to_owned())
} }
Command::RoomMessageSend(msg) => { Command::RoomMessageSend(msg) => {
if let Some(room) = app.status.room_mut() { if let Some(room) = app.status.room_mut() {
room.send(msg.clone()).await?; room.send(msg.clone()).await?;
} }
EventStatus::Ok set_status_output!("Send message: `{}`", msg);
(EventStatus::Ok, "".to_owned())
} }
Command::Greet(name) => { Command::Greet(name) => {
info!("Greated {}", name); info!("Greated {}", name);
EventStatus::Ok set_status_output!("Hi, {}!", name);
(EventStatus::Ok, "".to_owned())
} }
Command::Help(_) => todo!(),
}) })
} }

View File

@ -1,24 +1,38 @@
use std::{sync::Arc, time::Duration};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use cli_log::info; use cli_log::{debug, info};
use tokio::{task, time::timeout};
use crate::app::{ use crate::app::{events::event_types::EventStatus, App};
events::event_types::{Event, EventStatus},
App,
};
pub async fn handle(app: &mut App<'_>, command: &str) -> Result<EventStatus> { pub async fn handle(app: &mut App<'_>, command: String) -> Result<EventStatus> {
info!("Recieved ci command: `{command}`; executing.."); info!("Recieved ci command: `{command}`; executing..");
// TODO: Should the ci support more than strings? let local = task::LocalSet::new();
let output = app
.lua
.load(command)
.eval_async::<String>()
.await
.with_context(|| format!("Failed to execute: `{command}`"))?;
info!("Function evaluated to: `{output}`");
app.tx.send(Event::CiOutput(output)).await.context("Failed to send ci output to internal event stream")?; // 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::<String>()
.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);
Ok(EventStatus::Ok) Ok(EventStatus::Ok)
} }

View File

@ -16,14 +16,14 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::Exit)) .send(Event::CommandEvent(Command::Exit, None))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::CyclePlanes)) .send(Event::CommandEvent(Command::CyclePlanes, None))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
@ -31,7 +31,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::CyclePlanesRev)) .send(Event::CommandEvent(Command::CyclePlanesRev, None))
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
@ -40,7 +40,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::CommandLineShow)) .send(Event::CommandEvent(Command::CommandLineShow, None))
.await?; .await?;
} }
input => match app.ui.input_position() { input => match app.ui.input_position() {
@ -52,9 +52,10 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::RoomMessageSend( .send(Event::CommandEvent(
app.ui.message_compose.lines().join("\n"), Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")),
))) None,
))
.await?; .await?;
app.ui.message_compose_clear(); app.ui.message_compose_clear();
} }

View File

@ -1,11 +1,13 @@
mod handlers; mod handlers;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use cli_log::{info, trace};
use crossterm::event::Event as CrosstermEvent; use crossterm::event::Event as CrosstermEvent;
use tokio::sync::mpsc::Sender;
use crate::app::{command_interface::Command, status::State, App}; use crate::app::{command_interface::Command, status::State, App};
use self::handlers::{ci_output, command, lua_command, main, matrix, setup}; use self::handlers::{command, lua_command, main, matrix, setup};
use super::EventStatus; use super::EventStatus;
@ -13,32 +15,39 @@ use super::EventStatus;
pub enum Event { pub enum Event {
InputEvent(CrosstermEvent), InputEvent(CrosstermEvent),
MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse), MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse),
CommandEvent(Command), CommandEvent(Command, Option<Sender<String>>),
CiOutput(String),
LuaCommand(String), LuaCommand(String),
} }
impl Event { impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> { pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
trace!("Recieved event to handle: `{:#?}`", &self);
match &self { match &self {
Event::MatrixEvent(event) => matrix::handle(app, event) Event::MatrixEvent(event) => matrix::handle(app, event)
.await .await
.with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)), .with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)),
Event::CommandEvent(event) => command::handle(app, event) Event::CommandEvent(event, callback_tx) => {
.await let (result, output) = command::handle(app, event, callback_tx.is_some())
.with_context(|| format!("Failed to handle command event: `{:#?}`", event)), .await
Event::CiOutput(output) => ci_output::handle(app, output).await.with_context(|| { .with_context(|| format!("Failed to handle command event: `{:#?}`", event))?;
format!("Failed to handle command interface output: `{:#?}`", output)
}), if let Some(callback_tx) = callback_tx {
Event::LuaCommand(lua_code) => { callback_tx
lua_command::handle(app, lua_code).await.with_context(|| { .send(output.clone())
format!("Failed to handle lua code: `{:#?}`", lua_code) .await
}) .with_context(|| format!("Failed to send command output: {}", output))?;
}
Ok(result)
} }
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() { Event::InputEvent(event) => match app.status.state() {
State::None => Ok(EventStatus::Ok), State::None => unreachable!(
"This state should not be available, when we are in the input handling"
),
State::Main => main::handle(app, event) State::Main => main::handle(app, event)
.await .await
.with_context(|| format!("Failed to handle input event: `{:#?}`", event)), .with_context(|| format!("Failed to handle input event: `{:#?}`", event)),

View File

@ -2,9 +2,9 @@ pub mod command_interface;
pub mod events; pub mod events;
pub mod status; pub mod status;
use std::path::Path; use std::{path::Path, sync::Arc};
use anyhow::{Error, Result, Context}; use anyhow::{Context, Error, Result};
use cli_log::info; use cli_log::info;
use matrix_sdk::Client; use matrix_sdk::Client;
use mlua::Lua; use mlua::Lua;
@ -31,16 +31,16 @@ pub struct App<'ui> {
input_listener_killer: CancellationToken, input_listener_killer: CancellationToken,
matrix_listener_killer: CancellationToken, matrix_listener_killer: CancellationToken,
lua: Lua, lua: Arc<Lua>,
} }
impl App<'_> { impl App<'_> {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
fn set_up_lua(tx: mpsc::Sender<Event>) -> Lua { fn set_up_lua(tx: mpsc::Sender<Event>) -> Arc<Lua> {
let mut lua = Lua::new(); let mut lua = Lua::new();
generate_ci_functions(&mut lua, tx); generate_ci_functions(&mut lua, tx);
lua Arc::new(lua)
} }
let path: &std::path::Path = Path::new("userdata/accounts.json"); let path: &std::path::Path = Path::new("userdata/accounts.json");