Compare commits

...

7 Commits

Author SHA1 Message Date
Benedikt Peetz 7aea23f3b6
fix(handlers/main)!: Change exit key to `q`
`Esc` is bound to exit a deeper mode (like `insert` or `command`) and
return to `normal` mode. This however is rather inconvenient,
when `Esc` also exits Trinitrix in general, as spamming `Esc` then
becomes impossible.
This changes also puts Trinitrix more in line with vim (although vim
does not allow exiting with `q`).

BREAKING CHANGE: To exit press `q` instead of `Esc`.
2023-09-09 19:57:49 +02:00
Benedikt Peetz 74f3b25827
feat(states): Add `command` state for command only keymappings 2023-09-09 19:57:20 +02:00
Benedikt Peetz bc1fc0cc02
refactor(LuaCommandManager): Express the semantics of Lua code explicitly 2023-09-09 19:55:16 +02:00
Benedikt Peetz ed37d1239f
feat(CommandTransferValue): Generalize the API to facilitate multiple languages
The code for the `CommandTransferValue` was deeply entrenched in the
whole Lua execution code, which is obviously not ideal. In trying to
alleviate that, I decided to generalize the API in a way, that makes
adding new languages a lot easier.
2023-09-09 19:49:56 +02:00
Benedikt Peetz 42b8793dd0
Fix(lua): Upgrade to mlua 9.1, which introduced braking changes 2023-09-09 19:44:00 +02:00
Benedikt Peetz eb63cb6247
build(flake): Wrap mold so that it works with the nix supplied libraries 2023-09-03 22:11:41 +02:00
Benedikt Peetz a8112d554e
build(treewide): Update 2023-09-03 22:08:35 +02:00
14 changed files with 713 additions and 766 deletions

652
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,15 +11,15 @@ default = ["tui"]
tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"] tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"]
[dependencies] [dependencies]
clap = { version = "4.3.19", features = ["derive"] } clap = { version = "4.4.2", features = ["derive"] }
cli-log = "2.0" cli-log = "2.0"
anyhow = "1.0" anyhow = "1.0"
matrix-sdk = "0.6" matrix-sdk = "0.6"
tokio = { version = "1.29", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.32", features = ["macros", "rt-multi-thread"] }
# lua stuff # lua stuff
language_macros = { path = "./language_macros" } language_macros = { path = "./language_macros" }
mlua = { version = "0.8.9", features = ["lua54", "async", "send", "serialize"] } mlua = { version = "0.9.1", features = ["lua54", "async", "send", "serialize"] }
once_cell = "1.18.0" once_cell = "1.18.0"
# tui feature specific parts # tui feature specific parts

View File

@ -16,11 +16,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1688772518, "lastModified": 1693762390,
"narHash": "sha256-ol7gZxwvgLnxNSZwFTDJJ49xVY5teaSvF7lzlo3YQfM=", "narHash": "sha256-IzmbGqrzVUzyc1IxgH62nQ9l6Dz7hq6JWIeyZiDZqKw=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e", "rev": "7b92b595c947b3c82ba04f78e58446faac20c4cb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -50,11 +50,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1689068808, "lastModified": 1692799911,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -65,11 +65,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1690327932, "lastModified": 1693654884,
"narHash": "sha256-Fv7PYZxN4eo0K6zXhHG/vOc+e2iuqQ5ywDrh0yeRjP0=", "narHash": "sha256-EqKKEl+IOS8TSjkt+xn1qGpsjnx5/ag33YNQ1+c7OuM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a9b47d85504bdd199e90846622c76aa0bfeabfac", "rev": "e7f35e03abd06a2faef6684d0de813370e13bda8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -100,11 +100,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1690338181, "lastModified": 1693707092,
"narHash": "sha256-Sz2oQ9aNS3MVncnCMndr0302G26UrFUfPynoH2iLjsg=", "narHash": "sha256-HR1EnynBSPqbt+04/yxxqsG1E3n6uXrOl7SPco/UnYo=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "b7f0b7b58b3c6f14a1377ec31a3d78b23ab843ec", "rev": "98ccb73e6eefc481da6039ee57ad8818d1ca8d56",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -116,11 +116,11 @@
"rustc_cranelift_backend": { "rustc_cranelift_backend": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1690205271, "lastModified": 1693573696,
"narHash": "sha256-mMj1dSlGzM+jCgtVX/KNxdYPXohS22RyU/uWmhR7EA0=", "narHash": "sha256-9V2JGtm7tiAAxaIMXzHLliS7rRyuogMxCmP8u9VRjBE=",
"owner": "bjorn3", "owner": "bjorn3",
"repo": "rustc_codegen_cranelift", "repo": "rustc_codegen_cranelift",
"rev": "6641b3a548a425eae518b675e43b986094daf609", "rev": "0559de65672243a97d6cd6e6ee6a8e8c291ef4ce",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -133,7 +133,7 @@
"rustc_cranelift_backend_src": { "rustc_cranelift_backend_src": {
"flake": false, "flake": false,
"locked": { "locked": {
"narHash": "sha256-E4n1Dqne8/9XHVKFqApdp2baxXun0DMl+oHfo4+oq3Q=", "narHash": "sha256-h7VLPtyiXg5KpbQKPo3RKl/+AnTWqimKqAXc1VngYEo=",
"type": "tarball", "type": "tarball",
"url": "https://github.com/bjorn3/rustc_codegen_cranelift/releases/download/dev/cg_clif-x86_64-unknown-linux-gnu.tar.xz" "url": "https://github.com/bjorn3/rustc_codegen_cranelift/releases/download/dev/cg_clif-x86_64-unknown-linux-gnu.tar.xz"
}, },

272
flake.nix
View File

@ -52,139 +52,161 @@
... ...
}: }:
flake-utils.lib.eachDefaultSystem (system: let flake-utils.lib.eachDefaultSystem (system: let
c_rust = pkgs.rust-bin.fromRustupToolchainFile "${rustc_cranelift_backend}/rust-toolchain"; inherit (pkgs) lib;
rcb = pkgs.stdenv.mkDerivation { bintools-wrapper = "${nixpkgs}/pkgs/build-support/bintools-wrapper";
pname = "rustc_cranelift_backend"; mold' = pkgs.symlinkJoin {
version = "1.0"; name = "mold";
buildInputs = [c_rust]; paths = [pkgs.mold];
nativeBuildInputs = with pkgs; [gawk fd]; nativeBuildInputs = [pkgs.makeWrapper];
srcs = ["${rustc_cranelift_backend_src}" "${c_rust}"]; suffixSalt = lib.replaceStrings ["-" "."] ["_" "_"] pkgs.targetPlatform.config;
sourceRoot = "."; postBuild = ''
postUnpack = '' for bin in ${pkgs.mold}/bin/*; do
rust_dir="$(fd . --max-depth 1 | awk '!/source/ && !/env-vars/')" rm $out/bin/"$(basename "$bin")"
# remove unneeded stuff export prog="$bin"
rm -r "$rust_dir"/{bin,nix-support,share}; substituteAll "${bintools-wrapper}/ld-wrapper.sh" $out/bin/"$(basename "$bin")"
rm -r "$rust_dir"/lib/rustlib/{etc,src,rustc-src}; chmod +x $out/bin/"$(basename "$bin")"
cp -r "$rust_dir"/. source/extra_dependencies mkdir -p $out/nix-support
rm -r "$rust_dir" substituteAll "${bintools-wrapper}/add-flags.sh" $out/nix-support/add-flags.sh
cd source substituteAll "${bintools-wrapper}/add-hardening.sh" $out/nix-support/add-hardening.sh
''; substituteAll "${bintools-wrapper}/../wrapper-common/utils.bash" $out/nix-support/utils.bash
postPatch = '' done
# patch bins '';
for file in $(fd . --type file);do };
file="$(file --mime "$file" | awk 'BEGIN{FS=":"}/application\/x-pie-executable/{print $1}')"; c_rust = pkgs.rust-bin.fromRustupToolchainFile "${rustc_cranelift_backend}/rust-toolchain";
if [ "$file" ]; then rcb = pkgs.stdenv.mkDerivation {
echo "file: '$file' matches"; pname = "rustc_cranelift_backend";
correct_interpreter_path="$(ldd "$file" | tail -n1 | awk 'BEGIN{FS="=> "} {print $2}' | awk 'BEGIN{FS=" "}{print $1}')" version = "1.0";
echo "correct interpreter path is: '$correct_interpreter_path'" buildInputs = [c_rust];
patchelf --set-interpreter "$correct_interpreter_path" "$file" nativeBuildInputs = with pkgs; [gawk fd];
srcs = ["${rustc_cranelift_backend_src}" "${c_rust}"];
sourceRoot = ".";
postUnpack = ''
rust_dir="$(fd . --max-depth 1 | awk '!/source/ && !/env-vars/')"
if [ "$(patchelf --print-interpreter "$file")" = "$correct_interpreter_path" ];then # remove unneeded stuff
echo "Set interpreter"; rm -r "$rust_dir"/{bin,nix-support,share};
else rm -r "$rust_dir"/lib/rustlib/{etc,src,rustc-src};
echo "Failed to set interprter, the interpreter still is $(patchelf --print-interpreter )";
exit 1
fi
fi
done
# patch libs cp -r "$rust_dir"/. source/extra_dependencies
all_files=$(mktemp); rm -r "$rust_dir"
for file in $(fd .);do cd source
canonical_path="$(readlink -f "$file")" '';
file="$(file --mime "$canonical_path" | awk 'BEGIN{FS=":"}/application\/x-sharedlib/{print $1}')"; postPatch = ''
if [ "$file" ]; then # patch bins
echo "$file" >> $all_files; for file in $(fd . --type file);do
fi file="$(file --mime "$file" | awk 'BEGIN{FS=":"}/application\/x-pie-executable/{print $1}')";
done if [ "$file" ]; then
while read -r file; do echo "file: '$file' matches";
echo "___________________"; correct_interpreter_path="$(ldd "$file" | tail -n1 | awk 'BEGIN{FS="=> "} {print $2}' | awk 'BEGIN{FS=" "}{print $1}')"
echo "Checking file: '$file'"; echo "correct interpreter path is: '$correct_interpreter_path'"
is_missing="$(ldd "$file" | awk 'BEGIN{FS="=>"}{if (/not found/){print $1}}')" patchelf --set-interpreter "$correct_interpreter_path" "$file"
if [ "$is_missing" ];then
echo "Warning: The following things are missing:";
for line in $is_missing; do
echo " $line";
done
echo
for line in $is_missing; do
echo "Searching for a substitute for '$line'"
substitute="$(grep "$line" "$all_files" | awk 'BEGIN{FS=" "}{print $1}' | tail -n1)";
if [ "$substitute" ]; then
echo "Found '$substitute', which can substitute '$line'"
echo "Patching.."
patchelf --replace-needed "$line" "$(readlink -f "$substitute")" "$file";
else
echo "Error: Failed to find a substitute"
fi
done
else
echo "This file is not missing anything"
fi
done < $all_files
rm $all_files
'';
installPhase = ''
install -d $out/
cp -r ./. $out/
'';
};
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
nightly = true; if [ "$(patchelf --print-interpreter "$file")" = "$correct_interpreter_path" ];then
rust = echo "Set interpreter";
if nightly else
then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default) echo "Failed to set interprter, the interpreter still is $(patchelf --print-interpreter )";
else pkgs.rust-bin.stable.latest.default; exit 1
fi
fi
done
craneLib = (crane.mkLib pkgs).overrideToolchain rust; # patch libs
all_files=$(mktemp);
for file in $(fd .);do
canonical_path="$(readlink -f "$file")"
file="$(file --mime "$canonical_path" | awk 'BEGIN{FS=":"}/application\/x-sharedlib/{print $1}')";
if [ "$file" ]; then
echo "$file" >> $all_files;
fi
done
while read -r file; do
echo "___________________";
echo "Checking file: '$file'";
is_missing="$(ldd "$file" | awk 'BEGIN{FS="=>"}{if (/not found/){print $1}}')"
if [ "$is_missing" ];then
echo "Warning: The following things are missing:";
for line in $is_missing; do
echo " $line";
done
echo
for line in $is_missing; do
echo "Searching for a substitute for '$line'"
substitute="$(grep "$line" "$all_files" | awk 'BEGIN{FS=" "}{print $1}' | tail -n1)";
if [ "$substitute" ]; then
echo "Found '$substitute', which can substitute '$line'"
echo "Patching.."
patchelf --replace-needed "$line" "$(readlink -f "$substitute")" "$file";
else
echo "Error: Failed to find a substitute"
fi
done
else
echo "This file is not missing anything"
fi
done < $all_files
rm $all_files
'';
installPhase = ''
install -d $out/
cp -r ./. $out/
'';
};
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
nativeBuildInputs = with pkgs; [ nightly = true;
pkg-config rust =
mold if nightly
rcb then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)
else pkgs.rust-bin.stable.latest.default;
craneLib = (crane.mkLib pkgs).overrideToolchain rust;
nativeBuildInputs = with pkgs; [
pkg-config
mold'
rcb
];
buildInputs = with pkgs; [
openssl
lua54Packages.lua
];
craneBuild = craneLib.buildPackage {
src = craneLib.cleanCargoSource ./.;
doCheck = true;
inherit nativeBuildInputs buildInputs;
};
in {
packages = {
default = craneBuild;
};
app.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/trinitix";
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nil
alejandra
statix
ltex-ls
rust
rust-analyzer
cargo-edit
cargo-expand
]; ];
buildInputs = with pkgs; [ inherit nativeBuildInputs buildInputs;
openssl };
lua54Packages.lua });
];
craneBuild = craneLib.buildPackage {
src = craneLib.cleanCargoSource ./.;
doCheck = true;
inherit nativeBuildInputs buildInputs;
};
in {
packages = {
default = craneBuild;
};
app.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/trinitix";
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nil
alejandra
statix
ltex-ls
rust
rust-analyzer
cargo-edit
cargo-expand
];
inherit nativeBuildInputs buildInputs;
};
});
} }
# vim: ts=2 # vim: ts=2

View File

@ -126,67 +126,7 @@ fn get_function_body(field: &Field, has_input: bool, output_type: &Option<Type>)
let function_return = if let Some(_) = output_type { let function_return = if let Some(_) = output_type {
quote! { quote! {
let converted_output = lua return Ok(output.into_lua(lua).expect("This conversion should always work"));
.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 { } else {
quote! { quote! {
@ -196,7 +136,7 @@ fn get_function_body(field: &Field, has_input: bool, output_type: &Option<Type>)
let does_function_expect_output = if output_type.is_some() { let does_function_expect_output = if output_type.is_some() {
quote! { quote! {
// We didn't receive output but expected output. Raise an error to notify the lua code // We didn't receive output but expected output. Raise an error to notify the lua code
// about it // about it.
return Err(mlua::Error::ExternalError(std::sync::Arc::new( return Err(mlua::Error::ExternalError(std::sync::Arc::new(
err err
))); )));
@ -210,7 +150,7 @@ fn get_function_body(field: &Field, has_input: bool, output_type: &Option<Type>)
quote! { quote! {
let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::<CommandTransferValue>(); let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::<CommandTransferValue>();
let tx: core::cell::Ref<tokio::sync::mpsc::Sender<Event>> = let tx: mlua::AppDataRef<tokio::sync::mpsc::Sender<Event>> =
lua.app_data_ref().expect("This should exist, it was set before"); lua.app_data_ref().expect("This should exist, it was set before");
(*tx) (*tx)

View File

@ -0,0 +1,94 @@
use cli_log::{debug, info};
use mlua::{IntoLua, LuaSerdeExt, Table, Value};
use super::CommandTransferValue;
impl<'lua> IntoLua<'lua> for CommandTransferValue {
fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
let converted_output = lua.to_value(&self)?;
return unwrap(converted_output, lua);
}
}
fn unwrap<'lua>(
value_to_unwrap: Value<'lua>,
lua: &'lua mlua::Lua,
) -> mlua::Result<mlua::Value<'lua>> {
fn unwrap_first_level<'lua>(table: Table<'lua>) -> mlua::Result<Value<'lua>> {
let (_, value): (Value, Value) = table
.pairs()
.next()
.expect("Exactly one item should extist")?;
Ok(value)
}
// That converted value looks somewhat like this (e.g. a String):
// ```
// {
// ["String"] = "hi",
// }
// ```
// or like this (e.g. a table):
// ```
// {
// ["Table"] = {
// ["UserId"] = {
// ["Integer"] = 2,
// },
// ["UserName"] = {
// ["String"] = "James",
// },
// },
// }
// ```
if let Value::Table(table) = value_to_unwrap {
let value = unwrap_first_level(table)?;
if let Value::Table(wrapped_table) = value {
info!("We've got a wtable! wtable: \n{:#?}", wrapped_table);
// we now have a wrapped table value for example like this:
// ```
// {
// ["UserId"] = {
// ["Integer"] = 2,
// },
// ["UserName"] = {
// ["String"] = "James",
// },
// ["Versions"] = {
// ["Table"] = {
// ["api"] = {
// ["Boolean"] = true,
// },
// ["interface"] = {
// ["Integer"] = 3,
// },
// },
// },
// }
// ```
let output_table: Table = lua
.load("{}")
.eval()
.expect("This is static, it should always work");
// FIXME(@soispha): This still fails for nested tables (i.e. the table above), as it
// unpacks too much. While unpacking the while loop should stop, when a key is not from
// the CommandTransferValue family (i.e. ["Integer", "Boolean", "String", "Table",
// etc.]) <2023-09-09>
for pair in wrapped_table.pairs::<Value, Value>() {
let (key, mut raw_value) = pair?;
while let Value::Table(raw_table) = raw_value {
raw_value = unwrap_first_level(raw_table)?;
}
output_table.set(key, raw_value)?;
}
return Ok(output_table.into_lua(lua)?);
} else {
info!("We've got a normal output! output: {:#?}", value);
// we had a simple wrapped value, which is already unwrapped, thus it can be
// returned directly
return Ok(value);
}
} else {
unreachable!("The returned table should always only contain one element");
}
}

View File

@ -0,0 +1,60 @@
use std::{collections::HashMap, fmt::Display};
use serde::{Deserialize, Serialize};
pub mod type_conversions;
// language support
pub mod lua;
pub type Table = HashMap<String, CommandTransferValue>;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum CommandTransferValue {
/// `nil` or `null` or `undefined`; anything which goes in that group of types.
Nil,
/// `true` or `false`.
Boolean(bool),
// A “light userdata” object, equivalent to a raw pointer.
// /* TODO */ LightUserData(LightUserData),
/// An integer number.
Integer(i64),
/// A floating point number.
Number(f64),
/// A string
String(String),
/// A table, dictionary or HashMap
Table(HashMap<String, CommandTransferValue>),
// Reference to a Lua function (or closure).
// /* TODO */ Function(Function),
// Reference to a Lua thread (or coroutine).
// /* TODO */ Thread(Thread<'lua>),
// Reference to an 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)),
}
}
}

View File

@ -0,0 +1,34 @@
use std::collections::HashMap;
use super::CommandTransferValue;
impl From<String> for CommandTransferValue {
fn from(s: String) -> Self {
Self::String(s.to_owned())
}
}
impl From<f64> for CommandTransferValue {
fn from(s: f64) -> Self {
Self::Number(s.to_owned())
}
}
impl From<i64> for CommandTransferValue {
fn from(s: i64) -> Self {
Self::Integer(s.to_owned())
}
}
impl From<HashMap<String, CommandTransferValue>> for CommandTransferValue {
fn from(s: HashMap<String, CommandTransferValue>) -> Self {
Self::Table(s.to_owned())
}
}
impl From<bool> for CommandTransferValue {
fn from(s: bool) -> Self {
Self::Boolean(s.to_owned())
}
}
impl From<()> for CommandTransferValue {
fn from(_: ()) -> Self {
Self::Nil
}
}

View File

@ -1,10 +1,9 @@
use std::{collections::HashMap, fmt::Display, thread}; use std::thread;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use cli_log::{error, info, debug}; use cli_log::{debug, error, info};
use mlua::{Function, Value}; use mlua::{Function, Value};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use tokio::{ use tokio::{
runtime::Builder, runtime::Builder,
sync::{mpsc, Mutex}, sync::{mpsc, Mutex},
@ -17,95 +16,13 @@ use crate::app::{
}; };
static LUA: OnceCell<Mutex<mlua::Lua>> = OnceCell::new(); static LUA: OnceCell<Mutex<mlua::Lua>> = OnceCell::new();
pub type Table = HashMap<String, CommandTransferValue>;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum CommandTransferValue {
/// `nil` or `null` or `undefined`; anything which goes in that group of types.
Nil,
/// `true` or `false`.
Boolean(bool),
// A "light userdata" object, equivalent to a raw pointer.
// /*TODO*/ LightUserData(LightUserData),
/// An integer number.
Integer(i64),
/// A floating point number.
Number(f64),
/// A string
String(String),
/// A table, directory or HashMap
Table(HashMap<String, CommandTransferValue>),
// Reference to a Lua function (or closure).
// /* TODO */ Function(Function),
// Reference to a Lua thread (or coroutine).
// /* TODO */ Thread(Thread<'lua>),
// Reference to a userdata object that holds a custom type which implements `UserData`.
// Special builtin userdata types will be represented as other `Value` variants.
// /* TODO */ UserData(AnyUserData),
// `Error` is a special builtin userdata type. When received from Lua it is implicitly cloned.
// /* TODO */ Error(Error),
}
impl Display for CommandTransferValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CommandTransferValue::Nil => f.write_str("Nil"),
CommandTransferValue::Boolean(bool) => f.write_str(&format!("{}", bool)),
CommandTransferValue::Integer(int) => f.write_str(&format!("{}", int)),
CommandTransferValue::Number(num) => f.write_str(&format!("{}", num)),
CommandTransferValue::String(str) => f.write_str(&format!("{}", str)),
// TODO(@Soispha): The following line should be a real display call, but how do you
// format a HashMap?
CommandTransferValue::Table(table) => f.write_str(&format!("{:#?}", table)),
}
}
}
/// This structure contains the necessary state for running an embedded Lua runtime (i.e.
/// the tread, the Lua memory, etc.).
pub struct LuaCommandManager { pub struct LuaCommandManager {
lua_command_tx: mpsc::Sender<String>, lua_command_tx: mpsc::Sender<String>,
} }
impl From<String> for CommandTransferValue {
fn from(s: String) -> Self {
Self::String(s.to_owned())
}
}
impl From<f64> for CommandTransferValue {
fn from(s: f64) -> Self {
Self::Number(s.to_owned())
}
}
impl From<i64> for CommandTransferValue {
fn from(s: i64) -> Self {
Self::Integer(s.to_owned())
}
}
impl From<HashMap<String, CommandTransferValue>> for CommandTransferValue {
fn from(s: HashMap<String, CommandTransferValue>) -> Self {
Self::Table(s.to_owned())
}
}
impl From<bool> for CommandTransferValue {
fn from(s: bool) -> Self {
Self::Boolean(s.to_owned())
}
}
impl From<()> for CommandTransferValue {
fn from(_: ()) -> Self {
Self::Nil
}
}
impl LuaCommandManager { impl LuaCommandManager {
pub async fn execute_code(&self, code: String) { pub async fn execute_code(&self, code: String) {
self.lua_command_tx self.lua_command_tx
@ -125,21 +42,19 @@ impl LuaCommandManager {
let local = LocalSet::new(); let local = LocalSet::new();
local.spawn_local(async move { local.spawn_local(async move {
info!( info!(
"Lua command handling initialized, \ "Lua command handling initialized, \
waiting for commands.." waiting for commands.."
); );
while let Some(command) = lua_command_rx.recv().await { while let Some(command) = lua_command_rx.recv().await {
debug!("Recieved lua code: {}", &command); debug!("Recieved lua code (in LuaCommandHandler): {}", &command);
let local_event_call_tx = event_call_tx.clone(); let local_event_call_tx = event_call_tx.clone();
task::spawn_local(async move { task::spawn_local(async move {
exec_lua_command(&command, local_event_call_tx) exec_lua(&command, local_event_call_tx).await.expect(
.await "This should return all relevent errors \
.expect(
"This should return all relevent errors \
by other messages, \ by other messages, \
this should never error", this should never error",
); );
}); });
} }
}); });
@ -150,7 +65,7 @@ impl LuaCommandManager {
} }
} }
async fn exec_lua_command(command: &str, event_call_tx: mpsc::Sender<Event>) -> Result<()> { async fn exec_lua(lua_code: &str, event_call_tx: mpsc::Sender<Event>) -> Result<()> {
let second_event_call_tx = event_call_tx.clone(); let second_event_call_tx = event_call_tx.clone();
let lua = LUA let lua = LUA
.get_or_init(|| { .get_or_init(|| {
@ -162,21 +77,23 @@ async fn exec_lua_command(command: &str, event_call_tx: mpsc::Sender<Event>) ->
.lock() .lock()
.await; .await;
info!("Recieved code to execute: `{}`, executing...", &command); info!("Recieved code to execute: `{}`, executing...", &lua_code);
let output = lua.load(command).eval_async::<Value>().await; let output = lua.load(lua_code).eval_async::<Value>().await;
match output { match output {
Ok(out) => { Ok(out) => {
let to_string_fn: Function = lua.globals().get("tostring").expect("This always exists"); 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"); let output: String = to_string_fn.call(out).expect("tostring should not error");
info!("Function `{}` returned: `{}`", command, &output); info!("Lua code `{}` evaluated to: `{}`", lua_code, &output);
event_call_tx if output != "nil" {
.send(Event::CommandEvent(Command::DisplayOutput(output), None)) event_call_tx
.await .send(Event::CommandEvent(Command::DisplayOutput(output), None))
.context("Failed to send lua output command")? .await
.context("Failed to send lua output command")?
}
} }
Err(err) => { Err(err) => {
error!("Function `{}` returned error: `{}`", command, err); error!("Lua code `{}` returned error: `{}`", lua_code, err);
event_call_tx event_call_tx
.send(Event::CommandEvent( .send(Event::CommandEvent(
Command::RaiseError(err.to_string()), Command::RaiseError(err.to_string()),

View File

@ -1,14 +1,15 @@
// Use `cargo expand app::command_interface` for an overview of the file contents // Use `cargo expand app::command_interface` for an overview of the file contents
pub mod command_transfer_value;
pub mod lua_command_manager; pub mod lua_command_manager;
use language_macros::ci_command_enum; use language_macros::ci_command_enum;
// TODO(@Soispha): Should these paths be moved to the proc macro? // TODO(@Soispha): Should these paths be moved to the proc macro?
// As they are not static, it could be easier for other people, // As they are not static, it could be easier for other people,
// if they stay here // if they stay here.
use lua_command_manager::CommandTransferValue; use crate::app::command_interface::command_transfer_value::CommandTransferValue;
use mlua::LuaSerdeExt; use mlua::IntoLua;
use crate::app::Event; use crate::app::Event;
#[ci_command_enum] #[ci_command_enum]
@ -25,7 +26,6 @@ struct Commands {
/// Returns a table of greeted users /// Returns a table of greeted users
greet_multiple: fn() -> Table, greet_multiple: fn() -> Table,
// End debug functions // End debug functions
/// Closes the application /// Closes the application
exit: fn(), exit: fn(),
@ -46,7 +46,7 @@ struct Commands {
set_mode_insert: fn(), set_mode_insert: fn(),
/// Send a message to the current room /// Send a message to the current room
/// The sent message is interpreted literally. /// The send message is interpreted literally.
room_message_send: fn(String), room_message_send: fn(String),
/// Open the help pages at the first occurrence of /// Open the help pages at the first occurrence of

View File

@ -4,14 +4,18 @@ use anyhow::{Error, Result};
use cli_log::{trace, warn}; use cli_log::{trace, warn};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use crate::{app::{ use crate::{
command_interface::{ app::{
lua_command_manager::{CommandTransferValue, Table}, command_interface::{
Command, command_transfer_value::{CommandTransferValue, Table},
Command,
},
events::event_types::EventStatus,
status::State,
App,
}, },
events::event_types::EventStatus, ui::central::InputPosition,
App, status::State, };
}, ui::central::InputPosition};
pub async fn handle( pub async fn handle(
app: &mut App<'_>, app: &mut App<'_>,
@ -20,8 +24,8 @@ pub async fn handle(
) -> Result<EventStatus> { ) -> Result<EventStatus> {
// A command can both return _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). // 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 // We simulate these by returning the main output to the Lua function, and printing the
// status output to a status ui field. // status output to a status UI field.
// //
// Every function should return some status output to show the user, that something is // 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 // happening, while only some functions return some value to the main output, as this
@ -68,7 +72,7 @@ pub async fn handle(
} }
Command::DisplayOutput(output) => { Command::DisplayOutput(output) => {
// TODO(@Soispha): This is only used to show the lua command output to the user. // 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 // Lua commands already receive the output. This should probably be communicated
// better, should it? // better, should it?
send_status_output!(output); send_status_output!(output);
@ -77,6 +81,7 @@ pub async fn handle(
Command::CommandLineShow => { Command::CommandLineShow => {
app.ui.cli_enable(); app.ui.cli_enable();
app.status.set_state(State::Command);
send_status_output!("CLI online"); send_status_output!("CLI online");
EventStatus::Ok EventStatus::Ok
} }
@ -114,7 +119,7 @@ pub async fn handle(
room.send(msg.clone()).await?; room.send(msg.clone()).await?;
send_status_output!("Sent message: `{}`", msg); send_status_output!("Sent message: `{}`", msg);
} else { } else {
// TODO(@Soispha): Should this raise a lua error? It could be very confusing, // TODO(@Soispha): Should this raise a Lua error? It could be very confusing,
// when a user doesn't read the log. // when a user doesn't read the log.
warn!("Can't send message: `{}`, as there is no open room!", &msg); warn!("Can't send message: `{}`, as there is no open room!", &msg);
} }
@ -126,13 +131,25 @@ pub async fn handle(
} }
Command::Print(output) => { Command::Print(output) => {
// FIXME(@Soispha): This only works with strings, which is a clear downside to the // 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 // original print function. Find a way to just use the original one.
send_main_output!("{}", output); send_main_output!("{}", output);
EventStatus::Ok EventStatus::Ok
} }
Command::GreetMultiple => { Command::GreetMultiple => {
let mut table: Table = HashMap::new(); let mut table: Table = HashMap::new();
table.insert("UserName1".to_owned(), CommandTransferValue::Integer(2)); table.insert("UserId".to_owned(), CommandTransferValue::Integer(2));
table.insert(
"UserName".to_owned(),
CommandTransferValue::String("James".to_owned()),
);
let mut second_table: Table = HashMap::new();
second_table.insert("interface".to_owned(), CommandTransferValue::Integer(3));
second_table.insert("api".to_owned(), CommandTransferValue::Boolean(true));
table.insert(
"Versions".to_owned(),
CommandTransferValue::Table(second_table),
);
send_main_output!(table); send_main_output!(table);
EventStatus::Ok EventStatus::Ok
} }

View File

@ -10,13 +10,56 @@ use crate::{
ui::central, ui::central,
}; };
pub async fn handle_normal( pub async fn handle_command(
app: &mut App<'_>, app: &mut App<'_>,
input_event: &CrosstermEvent, input_event: &CrosstermEvent,
) -> Result<EventStatus> { ) -> Result<EventStatus> {
if let Some(cli) = &app.ui.cli {
match input_event {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => {
app.tx
.send(Event::CommandEvent(Command::SetModeNormal, None))
.await?;
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter,
..
}) => {
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.ui
.cli
.as_mut()
.expect("This is already checked")
.input(tui_textarea::Input::from(input_event.to_owned()));
}
}
} else {
unreachable!("The cli should not be active while no cli is defined");
}
Ok(EventStatus::Ok)
}
pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
match input_event { match input_event {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Char('q'),
..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::Exit, None)) .send(Event::CommandEvent(Command::Exit, None))
@ -137,38 +180,7 @@ pub async fn handle_normal(
_ => (), _ => (),
}; };
} }
central::InputPosition::CLI => {
if let Some(cli) = &app.ui.cli {
match input {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter,
..
}) => {
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.ui
.cli
.as_mut()
.expect("This is already checked")
.input(tui_textarea::Input::from(input.to_owned()));
}
};
};
}
_ => (), _ => (),
}, },
}; };

View File

@ -6,7 +6,7 @@ use crossterm::event::Event as CrosstermEvent;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use crate::app::{ use crate::app::{
command_interface::{lua_command_manager::CommandTransferValue, Command}, command_interface::{command_transfer_value::CommandTransferValue, Command},
status::State, status::State,
App, App,
}; };
@ -45,6 +45,9 @@ impl Event {
State::Insert => main::handle_insert(app, &event).await.with_context(|| { State::Insert => main::handle_insert(app, &event).await.with_context(|| {
format!("Failed to handle input (insert) event: `{:#?}`", event) format!("Failed to handle input (insert) event: `{:#?}`", event)
}), }),
State::Command => main::handle_command(app, &event).await.with_context(|| {
format!("Failed to handle input (command) event: `{:#?}`", event)
}),
State::Setup => setup::handle(app, &event).await.with_context(|| { State::Setup => setup::handle(app, &event).await.with_context(|| {
format!("Failed to handle input (setup) event: `{:#?}`", event) format!("Failed to handle input (setup) event: `{:#?}`", event)
}), }),

View File

@ -15,6 +15,7 @@ use matrix_sdk::{
pub enum State { pub enum State {
Normal, Normal,
Insert, Insert,
Command,
/// Temporary workaround until command based login is working /// Temporary workaround until command based login is working
Setup, Setup,
} }
@ -57,6 +58,7 @@ impl fmt::Display for State {
match self { match self {
Self::Normal => write!(f, "Normal"), Self::Normal => write!(f, "Normal"),
Self::Insert => write!(f, "Insert"), Self::Insert => write!(f, "Insert"),
Self::Command => write!(f, "Command"),
Self::Setup => write!(f, "Setup (!! workaround !!)"), Self::Setup => write!(f, "Setup (!! workaround !!)"),
} }
} }