Compare commits

...

24 Commits

Author SHA1 Message Date
Benedikt Peetz bd0ffe9edc
Merge: Branch 'commands'
Multiple things are still missing:
- [ ] Table type support
- [ ] Error messages and Status messages in ci input field (not in the status panel)
- [ ] Better ux in the ci input field (scrollback more than one line, syntax highlighting, &c)
- For further checkboxes take a look at the `FIXME`s and `TODO`s in the code.

The base ci although is already usable and in some way useful
2023-07-26 22:36:59 +02:00
Benedikt Peetz 84c13fd6f8
Fix(treewide): Resolve merge conflicts 2023-07-26 22:25:08 +02:00
Benedikt Peetz 855d487693
Refactor(treewide): Remove the useless `tui_app` directory 2023-07-26 22:15:54 +02:00
Benedikt Peetz 258f784098
Fix(handlers::main): Don't close the cli after an entered command 2023-07-26 22:15:49 +02:00
Benedikt Peetz a80245c523
Fix(handlers::command): Only send success on real success
The `room_message_send()` function told the user about a successful sent
room message, even if that was not the case.
2023-07-26 22:15:18 +02:00
Benedikt Peetz f7b161fb55
Fix(handlers::command): Add the missing displayOutput handler
This handler is called by the lua executor, to show the final output of
a lua execution.
2023-07-26 22:15:17 +02:00
Benedikt Peetz 909fc01a48
Fix(app::command_interface): Add a `greet_multiple()` func to test tables
This is, like the `greet` function only here to debug the lua api. It
outputs a table.
2023-07-26 22:15:15 +02:00
Benedikt Peetz c0a1fc0a02
Fix(app::command_interface): Add a (workaround) print function
The default print function prints to stdout, which obviously
doesn't work with a tui application. This wrapper however is a
rather poor workaround, as it only works with strings (lua `print`
calls `tostring` to turn non string values in something printable).
2023-07-26 22:15:14 +02:00
Benedikt Peetz 1a35bb152c
Feat(treewide): Add a way for Commands to return more than just strings 2023-07-26 22:15:07 +02:00
Benedikt Peetz 7489f06a7c
Fix(app::command_interface): Use new language_macro api 2023-07-26 22:10:19 +02:00
Benedikt Peetz 3ca01912b9
Build(flake): Switch back to stable 2023-07-26 22:08:11 +02:00
Benedikt Peetz fbcf572f47
Build(flake+Cargo): Update lockfiles 2023-07-26 22:08:10 +02:00
Benedikt Peetz 27ad48c5e9
Refactor(language_macros): Complete rewrite
This **should** have everything the other implementation had, but the
api is implemented in a way, which is more in line with the expectation
raised at the lua functions (being that they are only wrappers over the
command api, and nothing more).

Aside of that, this version is actually documented!
2023-07-26 22:08:09 +02:00
Benedikt Peetz a3b49b17f4
Fix(handlers::main): Close ci after a command input 2023-07-26 22:08:07 +02:00
Benedikt Peetz 0288bdb0ad
Feat(ui): Add status panel, which shows command statuses and errors 2023-07-26 22:08:00 +02:00
Benedikt Peetz 1fe04ca5c6
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.
2023-07-26 22:06:42 +02:00
Benedikt Peetz c7a4d5a8ab
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.
2023-07-26 22:06:40 +02:00
Benedikt Peetz 189ae509f8
Fix(app::command_interface): Provide pre-generated file, to check syntax 2023-07-26 22:06:33 +02:00
Benedikt Peetz ebb16a20de
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`.
2023-07-26 22:04:42 +02:00
Benedikt Peetz 3d417d7e15
Build(flake): Remove dead code from cranelift build process 2023-07-26 21:53:44 +02:00
Benedikt Peetz 5e7ed3d084
Build: Lock file maintenance 2023-07-26 21:52:26 +02:00
Benedikt Peetz a3a26df65f
Build(flake): Add rustc cranelift codegen to development shell
This is executed by running `cargo clif` instead of `cargo`.
It _should_ result in decreased compile times (around 30%).
2023-07-26 21:42:16 +02:00
Benedikt Peetz 111d46ef2e
Build(Cargo): Instruct rustc to perform fat lto in release mode 2023-07-26 21:40:15 +02:00
Benedikt Peetz b4d9bea75a
Build(Cargo): Add mold as default linker in both profiles
I measured the linking times by running `cargo rustc --bin trinitrix --
-Z time-passes`, which resulted in following data:

`ld`:
37.794s to link
`mold`:
21.389s to link

The downside for now is, that this requires to have mold installed to
build trinitrix (The nix flake based direnv already has it) and that
mold only works on linux.
2023-07-26 21:38:38 +02:00
36 changed files with 1612 additions and 933 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

342
Cargo.lock generated
View File

@ -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"
@ -139,19 +188,19 @@ 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]]
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",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -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.32",
"syn 2.0.27",
]
[[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",
@ -557,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",
]
@ -569,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",
]
@ -612,7 +714,7 @@ checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
dependencies = [
"darling",
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"syn 1.0.109",
]
@ -659,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]]
@ -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"
@ -709,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"
@ -738,12 +849,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"
@ -858,8 +966,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -1003,6 +1111,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 +1308,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"
@ -1253,6 +1367,16 @@ dependencies = [
"serde",
]
[[package]]
name = "language_macros"
version = "0.1.0"
dependencies = [
"convert_case",
"proc-macro2 1.0.66",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -1267,9 +1391,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"
@ -1296,16 +1420,6 @@ dependencies = [
"hashbrown 0.12.3",
]
[[package]]
name = "lua_macros"
version = "0.1.0"
dependencies = [
"convert_case",
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
]
[[package]]
name = "maplit"
version = "1.0.2"
@ -1541,6 +1655,7 @@ checksum = "07366ed2cd22a3b000aed076e2b68896fb46f06f1f5786c5962da73c0af01577"
dependencies = [
"bstr",
"cc",
"erased-serde",
"futures-core",
"futures-task",
"futures-util",
@ -1548,6 +1663,7 @@ dependencies = [
"once_cell",
"pkg-config",
"rustc-hash",
"serde",
]
[[package]]
@ -1570,9 +1686,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg 1.1.0",
]
@ -1614,7 +1730,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",
@ -1630,8 +1746,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -1745,8 +1861,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -1850,7 +1966,7 @@ dependencies = [
"anyhow",
"itertools",
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"syn 1.0.109",
]
@ -1865,9 +1981,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.31"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
dependencies = [
"proc-macro2 1.0.66",
]
@ -2064,7 +2180,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 +2189,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 +2315,7 @@ dependencies = [
"thiserror",
"tracing",
"url",
"uuid 1.4.0",
"uuid 1.4.1",
"wildmatch",
]
@ -2234,7 +2350,7 @@ dependencies = [
"once_cell",
"proc-macro-crate",
"proc-macro2 1.0.66",
"quote 1.0.31",
"quote 1.0.32",
"ruma-identifiers-validation",
"serde",
"syn 1.0.109",
@ -2255,13 +2371,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",
@ -2284,17 +2399,17 @@ 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"
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 +2418,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 +2428,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.171"
version = "1.0.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b"
dependencies = [
"serde_derive",
]
@ -2331,20 +2446,20 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.171"
version = "1.0.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
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]]
name = "serde_json"
version = "1.0.103"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
dependencies = [
"itoa",
"ryu",
@ -2389,9 +2504,9 @@ dependencies = [
[[package]]
name = "signal-hook"
version = "0.3.16"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
@ -2503,28 +2618,27 @@ 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",
]
[[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",
@ -2534,22 +2648,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -2603,8 +2717,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -2682,8 +2796,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]
[[package]]
@ -2700,12 +2814,14 @@ name = "trinitrix"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"cli-log",
"crossterm",
"indexmap 2.0.0",
"lua_macros",
"language_macros",
"matrix-sdk",
"mlua",
"once_cell",
"serde",
"tokio",
"tokio-util",
@ -2725,7 +2841,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 +2924,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 +2950,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",
@ -2922,8 +3044,8 @@ dependencies = [
"log",
"once_cell",
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
"wasm-bindgen-shared",
]
@ -2945,7 +3067,7 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote 1.0.31",
"quote 1.0.32",
"wasm-bindgen-macro-support",
]
@ -2956,8 +3078,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3098,9 +3220,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11"
dependencies = [
"memchr",
]
@ -3142,6 +3264,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.31",
"syn 2.0.26",
"quote 1.0.32",
"syn 2.0.27",
]

View File

@ -6,16 +6,29 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["tui"]
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"
mlua = { version = "0.8.9", features = ["lua54", "async", "send"] }
anyhow = "1.0"
matrix-sdk = "0.6"
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", "serialize"] }
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 }
[profile.release]
lto = true

View File

@ -65,11 +65,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1689449371,
"narHash": "sha256-sK3Oi8uEFrFPL83wKPV6w0+96NrmwqIpw9YFffMifVg=",
"lastModified": 1690327932,
"narHash": "sha256-Fv7PYZxN4eo0K6zXhHG/vOc+e2iuqQ5ywDrh0yeRjP0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "29bcead8405cfe4c00085843eb372cc43837bb9d",
"rev": "a9b47d85504bdd199e90846622c76aa0bfeabfac",
"type": "github"
},
"original": {
@ -85,7 +85,9 @@
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
"rust-overlay": "rust-overlay",
"rustc_cranelift_backend": "rustc_cranelift_backend",
"rustc_cranelift_backend_src": "rustc_cranelift_backend_src"
}
},
"rust-overlay": {
@ -98,11 +100,11 @@
]
},
"locked": {
"lastModified": 1689561325,
"narHash": "sha256-+UABrHUXtWJSc9mM7oEKPIYQEhTzUVVNy2IPG9Lfrj0=",
"lastModified": 1690338181,
"narHash": "sha256-Sz2oQ9aNS3MVncnCMndr0302G26UrFUfPynoH2iLjsg=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d8a38aea13c67dc2ce10cff93eb274dcf455753f",
"rev": "b7f0b7b58b3c6f14a1377ec31a3d78b23ab843ec",
"type": "github"
},
"original": {
@ -111,6 +113,35 @@
"type": "github"
}
},
"rustc_cranelift_backend": {
"flake": false,
"locked": {
"lastModified": 1690205271,
"narHash": "sha256-mMj1dSlGzM+jCgtVX/KNxdYPXohS22RyU/uWmhR7EA0=",
"owner": "bjorn3",
"repo": "rustc_codegen_cranelift",
"rev": "6641b3a548a425eae518b675e43b986094daf609",
"type": "github"
},
"original": {
"owner": "bjorn3",
"ref": "dev",
"repo": "rustc_codegen_cranelift",
"type": "github"
}
},
"rustc_cranelift_backend_src": {
"flake": false,
"locked": {
"narHash": "sha256-E4n1Dqne8/9XHVKFqApdp2baxXun0DMl+oHfo4+oq3Q=",
"type": "tarball",
"url": "https://github.com/bjorn3/rustc_codegen_cranelift/releases/download/dev/cg_clif-x86_64-unknown-linux-gnu.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://github.com/bjorn3/rustc_codegen_cranelift/releases/download/dev/cg_clif-x86_64-unknown-linux-gnu.tar.xz"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,

179
flake.nix
View File

@ -29,6 +29,16 @@
flake-utils.follows = "flake-utils";
};
};
# cranelift
rustc_cranelift_backend = {
url = "github:bjorn3/rustc_codegen_cranelift/dev";
flake = false;
};
rustc_cranelift_backend_src = {
url = "https://github.com/bjorn3/rustc_codegen_cranelift/releases/download/dev/cg_clif-x86_64-unknown-linux-gnu.tar.xz";
flake = false;
};
};
outputs = {
@ -37,59 +47,144 @@
crane,
flake-utils,
rust-overlay,
rustc_cranelift_backend,
rustc_cranelift_backend_src,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
c_rust = pkgs.rust-bin.fromRustupToolchainFile "${rustc_cranelift_backend}/rust-toolchain";
rcb = pkgs.stdenv.mkDerivation {
pname = "rustc_cranelift_backend";
version = "1.0";
buildInputs = [c_rust];
nativeBuildInputs = with pkgs; [gawk fd];
srcs = ["${rustc_cranelift_backend_src}" "${c_rust}"];
sourceRoot = ".";
postUnpack = ''
rust_dir="$(fd . --max-depth 1 | awk '!/source/ && !/env-vars/')"
nightly = true;
rust =
if nightly
then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)
else pkgs.rust-bin.stable.latest.default;
# remove unneeded stuff
rm -r "$rust_dir"/{bin,nix-support,share};
rm -r "$rust_dir"/lib/rustlib/{etc,src,rustc-src};
craneLib = (crane.mkLib pkgs).overrideToolchain rust;
cp -r "$rust_dir"/. source/extra_dependencies
rm -r "$rust_dir"
cd source
'';
postPatch = ''
# patch bins
for file in $(fd . --type file);do
file="$(file --mime "$file" | awk 'BEGIN{FS=":"}/application\/x-pie-executable/{print $1}')";
if [ "$file" ]; then
echo "file: '$file' matches";
correct_interpreter_path="$(ldd "$file" | tail -n1 | awk 'BEGIN{FS="=> "} {print $2}' | awk 'BEGIN{FS=" "}{print $1}')"
echo "correct interpreter path is: '$correct_interpreter_path'"
patchelf --set-interpreter "$correct_interpreter_path" "$file"
nativeBuildInputs = with pkgs; [
pkg-config
];
buildInputs = with pkgs; [
openssl
lua54Packages.stdlib
];
if [ "$(patchelf --print-interpreter "$file")" = "$correct_interpreter_path" ];then
echo "Set interpreter";
else
echo "Failed to set interprter, the interpreter still is $(patchelf --print-interpreter )";
exit 1
fi
fi
done
craneBuild = craneLib.buildPackage {
src = craneLib.cleanCargoSource ./.;
# 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)];
};
doCheck = true;
inherit nativeBuildInputs buildInputs;
};
in {
packages.default = craneBuild;
nightly = true;
rust =
if nightly
then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)
else pkgs.rust-bin.stable.latest.default;
app.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/trinitix";
};
craneLib = (crane.mkLib pkgs).overrideToolchain rust;
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nil
alejandra
statix
ltex-ls
rust
rust-analyzer
cargo-edit
cargo-expand
nativeBuildInputs = with pkgs; [
pkg-config
mold
rcb
];
inherit nativeBuildInputs buildInputs;
};
});
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
];
inherit nativeBuildInputs buildInputs;
};
});
}
# vim: ts=2

View File

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

View File

@ -0,0 +1,50 @@
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{DeriveInput, Field, Type};
use super::{get_input_type_of_bare_fn_field, parse_derive_input_as_named_fields};
pub fn command_enum(input: &DeriveInput) -> TokenStream2 {
let named_fields = parse_derive_input_as_named_fields(input);
let fields: TokenStream2 = named_fields
.named
.iter()
.map(|field| turn_struct_fieled_to_enum(field))
.collect();
quote! {
#[derive(Debug)]
pub enum Command {
#fields
}
}
}
fn turn_struct_fieled_to_enum(field: &Field) -> TokenStream2 {
let field_name = format_ident!(
"{}",
field
.ident
.as_ref()
.expect("These are named fields, it should be Some(<name>)")
.to_string()
.from_case(Case::Snake)
.to_case(Case::Pascal)
);
let input_type: Option<Type> = get_input_type_of_bare_fn_field(field);
match input_type {
Some(input_type) => {
quote! {
#field_name(#input_type),
}
}
None => {
quote! {
#field_name,
}
}
}
}

View File

@ -0,0 +1,54 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{DeriveInput, Field};
use crate::generate::parse_derive_input_as_named_fields;
pub fn generate_add_lua_functions_to_globals(input: &DeriveInput) -> TokenStream2 {
let named_fields = parse_derive_input_as_named_fields(input);
let function_adders: TokenStream2 = named_fields
.named
.iter()
.map(|field| generate_function_adder(field))
.collect();
quote! {
pub fn add_lua_functions_to_globals(
lua: mlua::Lua,
tx: tokio::sync::mpsc::Sender<Event>,
) -> mlua::Lua {
lua.set_app_data(tx);
let globals = lua.globals();
#function_adders
drop(globals);
lua
}
}
}
fn generate_function_adder(field: &Field) -> TokenStream2 {
let field_ident = field
.ident
.as_ref()
.expect("This is should be a named field");
let function_ident = format_ident!("wrapped_lua_function_{}", field_ident);
let function_name = field_ident.to_string();
quote! {
{
let #function_ident = lua.create_async_function(#field_ident).expect(
&format!(
"The function: `{}` should be defined",
#function_name
)
);
globals.set(#function_name, #function_ident).expect(
"Setting a static global value should work"
);
}
}
}

View File

@ -0,0 +1,20 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::DeriveInput;
use crate::generate::lua_wrapper::{
lua_functions_to_globals::generate_add_lua_functions_to_globals,
rust_wrapper_functions::generate_rust_wrapper_functions,
};
mod lua_functions_to_globals;
mod rust_wrapper_functions;
pub fn lua_wrapper(input: &DeriveInput) -> TokenStream2 {
let add_lua_functions_to_globals = generate_add_lua_functions_to_globals(input);
let rust_wrapper_functions = generate_rust_wrapper_functions(input);
quote! {
#add_lua_functions_to_globals
#rust_wrapper_functions
}
}

View File

@ -0,0 +1,235 @@
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
punctuated::Punctuated, token::Comma, DeriveInput, Field, GenericArgument, Lifetime, Type,
};
use crate::generate::{
get_input_type_of_bare_fn_field, get_return_type_of_bare_fn_field,
parse_derive_input_as_named_fields,
};
pub fn generate_rust_wrapper_functions(input: &DeriveInput) -> TokenStream2 {
let named_fields = parse_derive_input_as_named_fields(input);
let wrapped_functions: TokenStream2 = named_fields
.named
.iter()
.map(|field| wrap_lua_function(field))
.collect();
quote! {
#wrapped_functions
}
}
fn wrap_lua_function(field: &Field) -> TokenStream2 {
let input_type = get_input_type_of_bare_fn_field(field);
let return_type = get_return_type_of_bare_fn_field(field);
let function_name = field.ident.as_ref().expect("This should be a named field");
let function_body = get_function_body(field, input_type.is_some(), &return_type);
let lifetime_args =
get_and_add_lifetimes_form_inputs_and_outputs(input_type.clone(), return_type);
let input_type = input_type
.unwrap_or(syn::parse(quote! {()}.into()).expect("This is static, it always works"));
quote! {
async fn #function_name <#lifetime_args>(
lua: &mlua::Lua,
input: #input_type
) -> Result<mlua::Value, mlua::Error> {
#function_body
}
}
}
fn get_and_add_lifetimes_form_inputs_and_outputs<'a>(
input_type: Option<syn::Type>,
return_type: Option<syn::Type>,
) -> Punctuated<Lifetime, Comma> {
fn get_lifetime_args_from_type<'a>(return_type: syn::Type) -> Option<Vec<Lifetime>> {
match return_type {
syn::Type::Path(path) => {
let args_to_final_path_segment = &path
.path
.segments
.last()
.expect("The path should have a last segment")
.arguments;
match args_to_final_path_segment {
syn::PathArguments::None =>
/* We ignore this case */
{
None
}
syn::PathArguments::AngleBracketed(angle) => {
let lifetime_args: Vec<_> = angle
.args
.iter()
.filter_map(|arg| {
if let GenericArgument::Lifetime(lifetime) = arg {
Some(lifetime.to_owned())
} else {
None
}
})
.collect();
return Some(lifetime_args);
}
syn::PathArguments::Parenthesized(_) => todo!(),
}
}
_ => todo!(),
}
}
let mut output: Punctuated<Lifetime, Comma> = Punctuated::new();
if let Some(input_type) = input_type {
let lifetime_args = get_lifetime_args_from_type(input_type).unwrap_or(vec![]);
lifetime_args.into_iter().for_each(|arg| output.push(arg));
}
if let Some(return_type) = return_type {
let lifetime_args = get_lifetime_args_from_type(return_type).unwrap_or(vec![]);
lifetime_args.into_iter().for_each(|arg| output.push(arg));
}
output
}
fn get_function_body(field: &Field, has_input: bool, output_type: &Option<Type>) -> TokenStream2 {
let command_name = field
.ident
.as_ref()
.expect("These are named fields, it should be Some(<name>)")
.to_string()
.from_case(Case::Snake)
.to_case(Case::Pascal);
let command_ident = format_ident!("{}", command_name);
let send_output = if has_input {
quote! {
Event::CommandEvent(
Command::#command_ident(input.clone()),
Some(callback_tx),
)
}
} else {
quote! {
Event::CommandEvent(
Command::#command_ident,
Some(callback_tx),
)
}
};
let function_return = if let Some(_) = output_type {
quote! {
let converted_output = lua
.to_value(&output)
.expect("This conversion should (indirectely) be checked at compile time");
if let mlua::Value::Table(table) = converted_output {
let real_output: mlua::Value = match output {
CommandTransferValue::Nil => table
.get("Nil")
.expect("This should exist"),
CommandTransferValue::Boolean(_) => table
.get("Boolean")
.expect("This should exist"),
CommandTransferValue::Integer(_) => table
.get("Integer")
.expect("This should exist"),
CommandTransferValue::Number(_) => table
.get("Number")
.expect("This should exist"),
CommandTransferValue::String(_) => table
.get("String")
.expect("This should exist"),
CommandTransferValue::Table(_) => {
todo!()
// FIXME(@Soispha): This returns a table with the values wrapped the
// same way the values above are wrapped. That is (from the greet_multiple
// function):
// ```json
// {
// "Table": {
// "UserName1": {
// "Integer": 2
// }
// }
// }
// ```
// whilst the output should be:
// ```json
// {
// "UserName1": 2
// }
// ```
// That table would need to be unpacked, but this requires some recursive
// function, which seems not very performance oriented.
//
// My first (quick) attempt:
//let mut output_table = lua.create_table().expect("This should work?");
//let initial_table: mlua::Value = table
// .get("Table")
// .expect("This should exist");
//while let mlua::Value::Table(table) = initial_table {
// for pair in table.pairs() {
// let (key, value) = pair.expect("This should also work?");
// output_table.set(key, value);
// }
//}
},
};
return Ok(real_output);
} else {
unreachable!("Lua serializes these things always in a table");
}
}
} else {
quote! {
return Ok(mlua::Value::Nil);
}
};
let does_function_expect_output = if output_type.is_some() {
quote! {
// We didn't receive output but expected output. Raise an error to notify the lua code
// about it
return Err(mlua::Error::ExternalError(std::sync::Arc::new(
err
)));
}
} else {
quote! {
// We didn't receive output and didn't expect output. Everything went well!
return Ok(mlua::Value::Nil);
}
};
quote! {
let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::<CommandTransferValue>();
let tx: core::cell::Ref<tokio::sync::mpsc::Sender<Event>> =
lua.app_data_ref().expect("This should exist, it was set before");
(*tx)
.send(#send_output)
.await
.expect("This should work, as the receiver is not dropped");
cli_log::info!("Sent CommandEvent: `{}`", #command_name);
match callback_rx.await {
Ok(output) => {
cli_log::info!(
"Lua function: `{}` returned output to lua: `{}`", #command_name, &output
);
#function_return
},
Err(err) => {
#does_function_expect_output
}
};
}
}

View File

@ -0,0 +1,65 @@
mod command_enum;
mod lua_wrapper;
pub use command_enum::command_enum;
pub use lua_wrapper::lua_wrapper;
use syn::{DeriveInput, Field, FieldsNamed, ReturnType, Type, TypeBareFn};
pub fn parse_derive_input_as_named_fields(input: &DeriveInput) -> FieldsNamed {
match &input.data {
syn::Data::Struct(input) => match &input.fields {
syn::Fields::Named(named_fields) => named_fields,
_ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"),
},
_ => unimplemented!("The macro only works for structs"),
}
.to_owned()
}
pub fn get_bare_fn_input_type(function: &TypeBareFn) -> Option<Type> {
if function.inputs.len() == 1 {
Some(
function
.inputs
.first()
.expect("Only one element exists, we checked the length above")
.ty
.clone(),
)
} else if function.inputs.len() == 0 {
// No inputs, so we can't return a type
None
} else {
unreachable!(
"The Function can only take one or zero arguments.
Use a tuple `(arg1, arg2)` if you want more"
);
}
}
pub fn get_input_type_of_bare_fn_field(field: &Field) -> Option<Type> {
match &field.ty {
syn::Type::BareFn(function) => get_bare_fn_input_type(&function),
_ => unimplemented!(
"Please specify the type as a bare fn type.
That is: `fn(<args>) -> <outputs>`"
),
}
}
pub fn get_return_type_of_bare_fn_field(field: &Field) -> Option<Type> {
match &field.ty {
syn::Type::BareFn(function) => get_bare_fn_return_type(&function),
_ => unimplemented!(
"Please specify the type as a bare fn type.
That is: `fn(<args>) -> <outputs>`"
),
}
}
pub fn get_bare_fn_return_type(function: &TypeBareFn) -> Option<Type> {
let return_path: &ReturnType = &function.output;
match return_path {
ReturnType::Default => None,
ReturnType::Type(_, return_type) => Some(*return_type.to_owned()),
}
}

View File

@ -0,0 +1,93 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::DeriveInput;
mod generate;
/// This is the heart of the command api
/// It mainly does two things:
/// - Generate a command enum
/// - Wrap the enum in all supported languages (only lua for now)
/// - Generate wrapper lua function for each command
/// - Generate a `add_lua_functions_to_globals` function, which adds
/// the rust wrapper functions to the lua globals.
///
/// The input and output values of the wrapped functions are derived from the values specified in
/// the `Commands` struct.
/// The returned values will be returned directly to the lua context, this allows to nest functions.
///
/// For example this rust code:
/// ```rust
/// #[ci_command_enum]
/// struct Commands {
/// /// Greets the user
/// greet: fn(String) -> String,
/// }
/// ```
/// results in this expanded code:
/// ```rust
/// #[derive(Debug)]
/// pub enum Command {
/// Greet(String),
/// }
/// pub fn add_lua_functions_to_globals(
/// lua: mlua::Lua,
/// tx: tokio::sync::mpsc::Sender<Event>,
/// ) -> mlua::Lua {
/// lua.set_app_data(tx);
/// let globals = lua.globals();
/// {
/// let wrapped_lua_function_greet = lua
/// .create_async_function(greet)
/// .expect(
/// format!(
/// "The function: `{}` should be defined",
/// "greet",
/// )
/// );
/// globals
/// .set("greet", wrapped_lua_function_greet)
/// .expect("Setting a static global value should work");
/// }
/// drop(globals);
/// lua
/// }
/// async fn greet(lua: &mlua::Lua, input: String) -> Result<String, mlua::Error> {
/// let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::<String>();
/// let tx: core::cell::Ref<tokio::sync::mpsc::Sender<Event>> = lua
/// .app_data_ref()
/// .expect("This should exist, it was set before");
/// (*tx)
/// .send(Event::CommandEvent(Command::Greet(input.clone()), Some(callback_tx)))
/// .await
/// .expect("This should work, as the receiver is not dropped");
/// match callback_rx.await {
/// Ok(output) => {
/// return Ok(output);
/// }
/// Err(err) => {
/// return Err(mlua::Error::ExternalError(std::sync::Arc::new(err)));
/// }
/// };
/// }
/// ```
#[proc_macro_attribute]
pub fn ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let input: DeriveInput = syn::parse(input)
.expect("This should always be valid rust code, as it's extracted from direct code");
// Build the language wrappers
let lua_wrapper: TokenStream2 = generate::lua_wrapper(&input);
// Build the final enum
let command_enum = generate::command_enum(&input);
quote! {
#command_enum
#lua_wrapper
}
.into()
}

View File

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

View File

@ -1,173 +0,0 @@
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, ToTokens};
use syn::{Block, Expr, ExprBlock, GenericArgument, ReturnType, Stmt, Type};
pub fn generate_final_function(input: &mut syn::ItemFn) -> TokenStream2 {
append_tx_send_code(input);
let output: TokenStream2 = syn::parse(input.into_token_stream().into())
.expect("This is generated from valid rust code, it should stay that way.");
output
}
fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn {
let function_name_pascal = format_ident!(
"{}",
input
.sig
.ident
.clone()
.to_string()
.from_case(Case::Snake)
.to_case(Case::Pascal)
);
let tx_send = match &input.sig.output {
syn::ReturnType::Default => {
unreachable!("All functions should have a output of (Result<$type, rlua::Error>)");
}
syn::ReturnType::Type(_, ret_type) => {
let return_type = match *(ret_type.clone()) {
syn::Type::Path(path) => {
match path
.path
.segments
.first()
.expect("This is expected to be only one path segment")
.arguments
.to_owned()
{
syn::PathArguments::AngleBracketed(angled_path) => {
let angled_path = angled_path.args.to_owned();
let filtered_paths: Vec<_> = angled_path
.into_iter()
.filter(|generic_arg| {
if let GenericArgument::Type(generic_type) = generic_arg {
if let Type::Path(_) = generic_type {
true
} else {
false
}
} else {
false
}
})
.collect();
// There should only be two segments (the type is <String, rlua::Error>)
if filtered_paths.len() > 2 {
unreachable!("There should be no more than two filtered_output, but got: {:#?}", filtered_paths)
} else if filtered_paths.len() <= 0 {
unreachable!("There should be more than zero filtered_output, but got: {:#?}", filtered_paths)
}
if filtered_paths.len() == 2 {
// There is something else than mlua::Error
let gen_type = if let GenericArgument::Type(ret_type) =
filtered_paths
.first()
.expect("One path segment should exists")
.to_owned()
{
ret_type
} else {
unreachable!("These were filtered above.");
};
let return_type_as_type_prepared = quote! {-> #gen_type};
let return_type_as_return_type: ReturnType = syn::parse(
return_type_as_type_prepared.to_token_stream().into(),
)
.expect("This is valid.");
return_type_as_return_type
} else {
// There is only mlua::Error left
ReturnType::Default
}
}
_ => unimplemented!("Only for angled paths"),
}
}
_ => unimplemented!("Only for path types"),
};
let send_data = match return_type {
ReturnType::Default => {
quote! {
{
Event::CommandEvent(Command::#function_name_pascal, None)
}
}
}
ReturnType::Type(_, _) => {
quote! {
{
Event::CommandEvent(Command::#function_name_pascal(input.clone()), Some(callback_tx))
}
}
}
};
let output_return = match return_type {
ReturnType::Default => {
quote! {
{
return Ok(());
}
}
}
ReturnType::Type(_, _) => {
quote! {
{
if let Some(output) = callback_rx.recv().await {
callback_rx.close();
return Ok(output);
} else {
return Err(mlua::Error::ExternalError(Arc::new(Error::new(
ErrorKind::Other,
"Callback reciever dropped",
))));
}
}
}
}
};
quote! {
{
let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::<String>(256);
let tx:
core::cell::Ref<tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>> =
lua
.app_data_ref()
.expect("This exists, it was set before");
(*tx)
.try_send(#send_data)
.expect("This should work, as the reciever is not dropped");
#output_return
}
}
}
};
let tx_send_block: Block =
syn::parse(tx_send.into()).expect("This is a static string, it will always parse");
let tx_send_expr_block = ExprBlock {
attrs: vec![],
label: None,
block: tx_send_block,
};
let mut tx_send_stmt = vec![Stmt::Expr(Expr::Block(tx_send_expr_block), None)];
let mut new_stmts: Vec<Stmt> = Vec::with_capacity(input.block.stmts.len() + 1);
new_stmts.append(&mut tx_send_stmt);
new_stmts.append(&mut input.block.stmts);
input.block.stmts = new_stmts;
input
}

View File

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

View File

@ -1,105 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{parse_quote, ReturnType, Type};
fn generate_ci_function_exposure(field: &syn::Field) -> TokenStream2 {
let field_ident = field
.ident
.as_ref()
.expect("These are only the named field, thus they all should have a name.");
let function_name_ident = format_ident!("fun_{}", field_ident);
let function_name = format!("{}", field_ident);
quote! {
let #function_name_ident = lua.create_async_function(#field_ident).expect(
&format!(
"The function: `{}` should be defined",
#function_name
)
);
globals.set(#function_name, #function_name_ident).expect(
&format!(
"Setting a static global value ({}, fun_{}) should work",
#function_name,
#function_name
)
);
}
.into()
}
pub fn generate_generate_ci_function(input: &syn::DeriveInput) -> TokenStream2 {
let mut functions_to_generate: Vec<TokenStream2> = vec![];
let functions_to_export_in_lua: TokenStream2 = match &input.data {
syn::Data::Struct(input) => match &input.fields {
syn::Fields::Named(named_fields) => named_fields
.named
.iter()
.map(|field| -> TokenStream2 {
let input_type = match &field.ty {
syn::Type::BareFn(bare_fn) => {
if bare_fn.inputs.len() == 1 {
bare_fn.inputs.last().expect("The last element exists").ty.clone()
} else if bare_fn.inputs.len() == 0 {
let input_type: Type = parse_quote! {()};
input_type
} else {
panic!("The Function can only take on argument, or none");
}
}
_ => unimplemented!("This is only implemented for bare function types"),
};
let return_type = match &field.ty {
syn::Type::BareFn(function) => {
let return_path: &ReturnType = &function.output;
match return_path {
ReturnType::Default => None,
ReturnType::Type(_, return_type) => Some(return_type.to_owned()) }
}
_ => unimplemented!("This is only implemented for bare function types"),
};
let function_name = field
.ident
.as_ref()
.expect("These are only the named field, thus they all should have a name.");
if let Some(ret_type) = return_type {
functions_to_generate.push(quote! {
#[ci_command]
async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<#ret_type, mlua::Error> {
}
});
} else {
functions_to_generate.push(quote! {
#[ci_command]
async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<(), mlua::Error> {
}
});
}
generate_ci_function_exposure(field)
})
.collect(),
_ => unimplemented!("Only implemented for named fileds"),
},
_ => unimplemented!("Only implemented for structs"),
};
let functions_to_generate: TokenStream2 = functions_to_generate.into_iter().collect();
let gen = quote! {
pub fn generate_ci_functions(
lua: &mut mlua::Lua,
tx: tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>
)
{
lua.set_app_data(tx);
let globals = lua.globals();
#functions_to_export_in_lua
}
#functions_to_generate
};
gen.into()
}

View File

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

View File

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

View File

@ -1,57 +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,
}

View File

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

View File

@ -0,0 +1,63 @@
// Use `cargo expand app::command_interface` for an overview of the file contents
pub mod lua_command_manager;
use language_macros::ci_command_enum;
// TODO(@Soispha): Should these paths be moved to the proc macro?
// As they are not static, it could be easier for other people,
// if they stay here
use lua_command_manager::CommandTransferValue;
use mlua::LuaSerdeExt;
use crate::app::Event;
#[ci_command_enum]
struct Commands {
/// Returns the string given to it
// FIXME(@Soispha): This is a workaround because the default print prints to stdout,
// which is obviously not ideal
print: fn(String) -> String,
// Begin debug functions
/// Greets the user
greet: fn(String) -> String,
/// Returns a table of greeted users
greet_multiple: fn() -> Table,
// End debug functions
/// Closes the application
exit: fn(),
/// Shows the command line
command_line_show: fn(),
/// Hides the command line
command_line_hide: fn(),
/// Go to the next plane
cycle_planes: fn(),
/// Go to the previous plane
cycle_planes_rev: fn(),
/// 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<String>),
/// Send an error to the default error output
raise_error: fn(String),
/// Send output to the default output
/// This is mainly used to display the final
/// output of evaluated lua commands.
display_output: fn(String),
}

View File

@ -1,81 +1,145 @@
use crate::{
app::{command_interface::Command, events::event_types::EventStatus, status::State, App},
ui::central::InputPosition,
};
use anyhow::Result;
use cli_log::info;
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, status::State,
}, ui::central::InputPosition};
pub async fn handle(
app: &mut App<'_>,
command: &Command,
send_output: bool,
) -> Result<(EventStatus, String)> {
macro_rules! set_status_output {
output_callback: Option<oneshot::Sender<CommandTransferValue>>,
) -> Result<EventStatus> {
// 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.
//
// 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(CommandTransferValue::from($str))
.map_err(|e| Error::msg(format!("Failed to send command main output: `{}`", e)))?;
}
};
($str:expr, $($args:ident),+) => {
if send_output {
app.ui.set_command_output(&format!($str, $($args),+));
if let Some(sender) = output_callback {
sender
.send(CommandTransferValue::from(format!($str, $($args),+)))
.map_err(|e| Error::msg(format!("Failed to send command main output: `{}`", e)))?;
}
};
}
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::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();
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 => {
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) => {
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);
}
set_status_output!("Send message: `{}`", msg);
(EventStatus::Ok, "".to_owned())
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::Print(output) => {
// FIXME(@Soispha): This only works with strings, which is a clear downside to the
// original print function. Find a way to just use the original one
send_main_output!("{}", output);
EventStatus::Ok
}
Command::GreetMultiple => {
let mut table: Table = HashMap::new();
table.insert("UserName1".to_owned(), CommandTransferValue::Integer(2));
send_main_output!(table);
EventStatus::Ok
}
Command::Help(_) => todo!(),
Command::RaiseError(err) => {
send_error_output!(err);
EventStatus::Ok
}
})
}

View File

@ -1,38 +1,17 @@
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};
pub async fn handle(app: &mut App<'_>, command: String) -> Result<EventStatus> {
info!("Recieved ci command: `{command}`; executing..");
// This function is here mainly to reserve this spot for further processing of the lua command.
// TODO(@Soispha): Move the lua executor thread code from app to this module
pub async fn handle(
app: &mut App<'_>,
command: String,
) -> Result<EventStatus> {
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::<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);
app.lua.execute_code(command).await;
Ok(EventStatus::Ok)
}

View File

@ -10,7 +10,10 @@ use crate::{
ui::central,
};
pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
pub async fn handle_normal(
app: &mut App<'_>,
input_event: &CrosstermEvent,
) -> Result<EventStatus> {
match input_event {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
@ -135,22 +138,22 @@ 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

View File

@ -1,9 +1,12 @@
use matrix_sdk::deserialized_responses::SyncResponse;
use anyhow::Result;
use matrix_sdk::deserialized_responses::SyncResponse;
use crate::app::{events::event_types::EventStatus, App};
pub async fn handle<'a>(app: &mut App<'a>, sync: &SyncResponse) -> Result<EventStatus> {
pub async fn handle(
app: &mut App<'_>,
sync: &SyncResponse,
) -> Result<EventStatus> {
for (m_room_id, m_room) in sync.rooms.join.iter() {
let room = match app.status.get_room_mut(m_room_id) {
Some(r) => r,

View File

@ -1,9 +1,15 @@
use anyhow::{bail, Context, Result};
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
use crate::{app::{events::event_types::EventStatus, App}, ui::setup};
use crate::{
app::{events::event_types::EventStatus, App},
ui::setup,
};
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
pub async fn handle(
app: &mut App<'_>,
input_event: &CrosstermEvent,
) -> Result<EventStatus> {
let ui = match &mut app.ui.setup_ui {
Some(ui) => ui,
None => bail!("SetupUI instance not found"),

View File

@ -1,11 +1,15 @@
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;
use tokio::sync::oneshot;
use crate::app::{command_interface::Command, 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};
@ -15,45 +19,35 @@ use super::EventStatus;
pub enum Event {
InputEvent(CrosstermEvent),
MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse),
CommandEvent(Command, Option<Sender<String>>),
CommandEvent(Command, Option<oneshot::Sender<CommandTransferValue>>),
LuaCommand(String),
}
impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> {
trace!("Recieved event to handle: `{:#?}`", &self);
match &self {
Event::MatrixEvent(event) => matrix::handle(app, event)
match self {
Event::MatrixEvent(event) => matrix::handle(app, &event)
.await
.with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)),
Event::CommandEvent(event, callback_tx) => {
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)
.await
.with_context(|| format!("Failed to handle input event: `{:#?}`", event)),
State::Insert => main::handle_insert(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)
}),
},
}
}

View File

@ -2,23 +2,24 @@ 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 status::{State, Status};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::{
accounts::{Account, AccountsManager},
app::{command_interface::generate_ci_functions, events::event_types::Event},
app::{
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>,
@ -31,18 +32,11 @@ pub struct App<'ui> {
input_listener_killer: CancellationToken,
matrix_listener_killer: CancellationToken,
lua: Arc<Lua>,
lua: LuaCommandManager,
}
impl App<'_> {
pub fn new() -> Result<Self> {
fn set_up_lua(tx: mpsc::Sender<Event>) -> Arc<Lua> {
let mut lua = Lua::new();
generate_ci_functions(&mut lua, tx);
Arc::new(lua)
}
let path: &std::path::Path = Path::new("userdata/accounts.json");
let config = if path.exists() {
info!("Reading account config (userdata/accounts.json)");
@ -62,7 +56,7 @@ impl App<'_> {
input_listener_killer: CancellationToken::new(),
matrix_listener_killer: CancellationToken::new(),
lua: set_up_lua(tx),
lua: LuaCommandManager::new(tx),
})
}
@ -87,11 +81,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!(),
};
}

View File

@ -28,6 +28,19 @@ pub struct Room {
view_scroll: Option<usize>,
}
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<Client>,
rooms: IndexMap<String, Room>,
current_room_id: String,
status_messages: Vec<StatusMessage>,
}
impl fmt::Display for State {
@ -138,14 +152,40 @@ impl Status {
Self {
state: State::Normal,
account_name: "".to_string(),
account_user_id: "".to_string(),
client,
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<StatusMessage> {
&self.status_messages
}
pub fn account_name(&self) -> &String {
&self.account_name
}

17
src/cli.rs Normal file
View File

@ -0,0 +1,17 @@
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<Command>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Starts the main tui client
#[clap(value_parser)]
Start {},
}

View File

@ -1,13 +1,24 @@
mod accounts;
mod app;
mod ui;
mod accounts;
mod cli;
use clap::Parser;
use crate::cli::{Args, Command};
#[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(Command::Start {});
match command {
Command::Start {} => {
let mut app = app::App::new()?;
app.run().await?;
}
};
Ok(())
}

View File

@ -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<TextArea<'_>>,
mut message_compose: &mut TextArea<'_>,
) -> Vec<Color> {
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<CrosstermBackend<Stdout>>,
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(())
}

View File

@ -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}, widgets::{Paragraph, Block, Borders}, style::{Style, Color}};
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(())

View File

@ -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<StatusMessage>, colors: &Vec<Color>) -> 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)
}

View File

@ -2,3 +2,4 @@ pub mod messages;
pub mod room_info;
pub mod rooms;
pub mod status;
pub mod command_monitor;