Compare commits

...

20 Commits

Author SHA1 Message Date
antifallobst dcf87f257d Merge: PR-11 (commands) -> master 2023-07-21 18:57:03 +00:00
antifallobst eac2eb2a7c Merge branch 'master' into commands 2023-07-21 18:55:25 +00:00
Benedikt Peetz a6d176b6e9
Fix(event_handlers/ci_output): Remove because not needed anymore 2023-07-20 21:50:30 +02:00
Benedikt Peetz 20c751fd7f
Fix(treewide): Update codebase to new lua_macros api 2023-07-20 21:49:56 +02:00
Benedikt Peetz c243c90cab
Fix(lua_macros): Rework to support new command focused layout
The internal commands are wrapped by a lua api, which allows to write
the whole thing a little bit more language agnostic.
2023-07-20 21:43:41 +02:00
Benedikt Peetz 27e3ff228c
Build(flake): Make it easier to switch to nightly 2023-07-20 21:39:38 +02:00
Benedikt Peetz 2a2c173683
Fix(command_interface): Use comments to generate the help function
The generation part is yet to be done, but the comments itself are
supposed to be the way of documentation.
2023-07-19 06:32:19 +02:00
Benedikt Peetz fbd1672d03
Style(treewide): Reorder imports and modules 2023-07-19 06:28:48 +02:00
Benedikt Peetz 734328787e
Fix(ui_widgets): Add missing lifetimes 2023-07-18 08:12:33 +02:00
Benedikt Peetz 14333944dc
Fix(handles): Add command handling over the internal event stream 2023-07-18 08:11:21 +02:00
Benedikt Peetz a413171ffe
Refactor(transmitter): Go back to plain tx,rx channels 2023-07-18 08:07:21 +02:00
Benedikt Peetz 6412650686
Fix(lua_macros): Add sender trough app_data 2023-07-18 08:03:16 +02:00
Benedikt Peetz c3a2b2d566
Fix(lua): Switch to mlua library, as it's better than rlua 2023-07-17 07:27:51 +02:00
Benedikt Peetz 91ea3f65ea
Refactor(lua_macros): Remove dead code 2023-07-17 07:23:36 +02:00
Benedikt Peetz fc880d47d2
Build(Lock_files): Update 2023-07-17 06:08:32 +02:00
Benedikt Peetz 866ec7c277
Refactor(ci_command_handling): Move to the event handlers 2023-07-17 00:17:53 +02:00
Benedikt Peetz 49818e0bfe
Refactor(app): Add a transmitter 2023-07-17 00:16:47 +02:00
Benedikt Peetz ba225e29df
Fix(treewide): Use the new lua_macros macros 2023-07-16 14:19:50 +02:00
Benedikt Peetz 3e8722433d
Fix(lua_macros): Expand to generate the required types and functions 2023-07-16 14:01:48 +02:00
Benedikt Peetz 8f9a2a3f22
Refactor(ui): Split into multiple files 2023-07-15 12:29:35 +02:00
30 changed files with 1556 additions and 924 deletions

247
Cargo.lock generated
View File

@ -75,9 +75,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.71" version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
[[package]] [[package]]
name = "anymap2" name = "anymap2"
@ -138,9 +138,9 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -149,9 +149,9 @@ version = "0.1.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -415,6 +415,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.3" version = "0.9.3"
@ -547,8 +556,8 @@ checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [ dependencies = [
"fnv", "fnv",
"ident_case", "ident_case",
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"strsim", "strsim",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -560,18 +569,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote 1.0.29", "quote 1.0.31",
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.4.0" version = "5.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"hashbrown 0.12.3", "hashbrown 0.14.0",
"lock_api", "lock_api",
"once_cell", "once_cell",
"parking_lot_core 0.9.8", "parking_lot_core 0.9.8",
@ -602,8 +611,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -649,9 +658,9 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -696,9 +705,9 @@ dependencies = [
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
@ -848,9 +857,9 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -1213,9 +1222,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.8" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
@ -1291,9 +1300,10 @@ dependencies = [
name = "lua_macros" name = "lua_macros"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "convert_case",
"quote 1.0.29", "proc-macro2 1.0.66",
"syn 2.0.25", "quote 1.0.31",
"syn 2.0.26",
] ]
[[package]] [[package]]
@ -1523,6 +1533,23 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "mlua"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07366ed2cd22a3b000aed076e2b68896fb46f06f1f5786c5962da73c0af01577"
dependencies = [
"bstr",
"cc",
"futures-core",
"futures-task",
"futures-util",
"num-traits",
"once_cell",
"pkg-config",
"rustc-hash",
]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.11" version = "0.2.11"
@ -1602,9 +1629,9 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -1717,9 +1744,9 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -1788,9 +1815,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.64" version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1822,8 +1849,8 @@ checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools", "itertools",
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -1838,11 +1865,11 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.29" version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
] ]
[[package]] [[package]]
@ -2063,9 +2090,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf" checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -2074,9 +2101,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.7.3" version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
@ -2115,30 +2142,6 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "rlua"
version = "0.19.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d33e5ba15c3d43178f283ed5863d4531e292fc0e56fb773f3bea45f18e3a42a"
dependencies = [
"bitflags",
"bstr",
"libc",
"num-traits",
"rlua-lua54-sys",
]
[[package]]
name = "rlua-lua54-sys"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aafabafe1895cb4a2be81a56d7ff3d46bf4b5d2f9cfdbea2ed404cdabe96474"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.7.4" version = "0.7.4"
@ -2230,8 +2233,8 @@ checksum = "0f82e91eb61cd86d9287303133ee55b54618eccb75a522cc22a42c15f5bda340"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"proc-macro-crate", "proc-macro-crate",
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"ruma-identifiers-validation", "ruma-identifiers-validation",
"serde", "serde",
"syn 1.0.109", "syn 1.0.109",
@ -2244,6 +2247,12 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.37.23" version = "0.37.23"
@ -2260,9 +2269,9 @@ dependencies = [
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.14" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]] [[package]]
name = "schannel" name = "schannel"
@ -2304,38 +2313,38 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.169" version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd51c3db8f9500d531e6c12dd0fd4ad13d133e9117f5aebac3cdbb8b6d9824b0" checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_bytes" name = "serde_bytes"
version = "0.11.11" version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a" checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.169" version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27738cfea0d944ab72c3ed01f3d5f23ec4322af8a1431e40ce630e4c01ea74fd" checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.100" version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -2380,9 +2389,9 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.15" version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3"
dependencies = [ dependencies = [
"libc", "libc",
"signal-hook-registry", "signal-hook-registry",
@ -2493,19 +2502,19 @@ version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.25" version = "2.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"unicode-ident", "unicode-ident",
] ]
@ -2538,9 +2547,9 @@ version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -2593,9 +2602,9 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -2639,9 +2648,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.19.12" version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [ dependencies = [
"indexmap 2.0.0", "indexmap 2.0.0",
"toml_datetime", "toml_datetime",
@ -2672,9 +2681,9 @@ version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -2696,7 +2705,7 @@ dependencies = [
"indexmap 2.0.0", "indexmap 2.0.0",
"lua_macros", "lua_macros",
"matrix-sdk", "matrix-sdk",
"rlua", "mlua",
"serde", "serde",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -2747,9 +2756,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.10" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
@ -2912,9 +2921,9 @@ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell", "once_cell",
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2936,7 +2945,7 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [ dependencies = [
"quote 1.0.29", "quote 1.0.31",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
] ]
@ -2946,9 +2955,9 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3089,9 +3098,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.4.9" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529" checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -3132,7 +3141,7 @@ version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [ dependencies = [
"proc-macro2 1.0.64", "proc-macro2 1.0.66",
"quote 1.0.29", "quote 1.0.31",
"syn 2.0.25", "syn 2.0.26",
] ]

View File

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

View File

@ -50,11 +50,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1687709756, "lastModified": 1689068808,
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=", "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7", "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -65,11 +65,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1688829822, "lastModified": 1689449371,
"narHash": "sha256-hv56yK1fPHPt7SU2DboxBtdSbIuv9nym7Dss7Cn2jic=", "narHash": "sha256-sK3Oi8uEFrFPL83wKPV6w0+96NrmwqIpw9YFffMifVg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ed6afb10dfdfc97b6bcf0703f1bad8118e9e961b", "rev": "29bcead8405cfe4c00085843eb372cc43837bb9d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -98,11 +98,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1688870171, "lastModified": 1689561325,
"narHash": "sha256-8tD8fheWPa7TaJoxzcU3iHkCrQQpOpdMN+HYqgZ1N5A=", "narHash": "sha256-+UABrHUXtWJSc9mM7oEKPIYQEhTzUVVNy2IPG9Lfrj0=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "5a932f10ac4bd59047d6e8b5780750ec76ea988a", "rev": "d8a38aea13c67dc2ce10cff93eb274dcf455753f",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -45,16 +45,20 @@
overlays = [(import rust-overlay)]; overlays = [(import rust-overlay)];
}; };
#rust-nightly = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default); nightly = true;
rust-stable = pkgs.rust-bin.stable.latest.default; rust =
if nightly
then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)
else pkgs.rust-bin.stable.latest.default;
craneLib = (crane.mkLib pkgs).overrideToolchain rust-stable; craneLib = (crane.mkLib pkgs).overrideToolchain rust;
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
pkg-config pkg-config
]; ];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
openssl openssl
lua54Packages.stdlib
]; ];
craneBuild = craneLib.buildPackage { craneBuild = craneLib.buildPackage {
@ -78,7 +82,7 @@
statix statix
ltex-ls ltex-ls
rust-stable rust
rust-analyzer rust-analyzer
cargo-edit cargo-edit
cargo-expand cargo-expand

View File

@ -4,9 +4,10 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[lib] [lib]
crate_type = ["proc-macro"] proc-macro = true
[dependencies] [dependencies]
convert_case = "0.6.0"
proc-macro2 = "1.0.64" proc-macro2 = "1.0.64"
quote = "1.0.29" quote = "1.0.29"
syn = "2.0.25" syn = { version = "2.0.25", features = ["extra-traits", "full", "parsing"] }

View File

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

View File

@ -0,0 +1,173 @@
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

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

View File

@ -0,0 +1,105 @@
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

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

View File

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

View File

@ -1,25 +0,0 @@
use anyhow::Result;
use tokio::sync::mpsc;
use super::events::event_types::Event;
#[derive(Debug, Clone)]
pub enum Command {
// Closes the application
Exit,
CommandLineShow,
CommandLineHide,
CyclePlanes,
CyclePlanesRev,
// sends a message to the current room
RoomMessageSend(String),
}
pub async fn execute(channel: &mpsc::Sender<Event>, command: Command) -> Result<()> {
let event = Event::CommandEvent(command);
channel.send(event).await?;
Ok(())
}

View File

@ -1,13 +1,49 @@
use lua_macros::generate_ci_functions; // FIXME: This file needs documentation with examples of how the proc macros work.
use rlua::Context; // for now use `cargo expand app::command_interface` for an overview
// This struct is here to gurantee, that all functions actually end up in the lua context. use std::{io::{Error, ErrorKind}, sync::Arc};
// I. e. rust should throw a compile error, when one field is added, but not a matching function.
#[generate_ci_functions()]
struct Commands<'lua> {
greet: Function<'lua>,
}
fn greet(context: Context, name: String) -> Result<String, rlua::Error> { use lua_macros::{ci_command, turn_struct_to_ci_command_enum};
Ok(format!("Name is {}", name))
use crate::app::event_types::Event;
/// This struct is here to guarantee, that all functions actually end up in the lua context.
/// I.e. Rust should throw a compile error, when one field is added, but not a matching function.
///
/// What it does:
/// - Generates a `generate_ci_functions` function, which wraps the specified rust in functions
/// in lua and exports them to the globals in the context provided as argument.
/// - Generates a Commands enum, which contains every Camel cased version of the fields.
///
/// Every command specified here should have a function named $command_name, where $command_name is the snake cased name of the field.
///
/// This function is exported to the lua context, thus it's signature must be:
/// ```rust
/// fn $command_name(context: Context, input_string: String) -> Result<$return_type, rlua::Error> {}
/// ```
/// where $return_type is the type returned by the function (the only supported ones are right now
/// `String` and `()`).
#[turn_struct_to_ci_command_enum]
struct Commands {
/// Greets the user
greet: fn(String) -> String,
/// Closes the application
//#[expose(lua)]
exit: fn(),
/// Shows the command line
command_line_show: fn(),
/// Hides the command line
command_line_hide: fn(),
/// Go to the next plane
cycle_planes: fn(),
/// Go to the previous plane
cycle_planes_rev: fn(),
/// Send a message to the current room
/// The send message is interpreted literally.
room_message_send: fn(String) -> String,
} }

View File

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

View File

@ -0,0 +1,38 @@
use std::{sync::Arc, time::Duration};
use anyhow::{Context, Result};
use cli_log::{debug, info};
use tokio::{task, time::timeout};
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..");
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);
Ok(EventStatus::Ok)
}

View File

@ -1,9 +1,13 @@
use anyhow::Result; use anyhow::{Context, Result};
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers};
use crate::{ use crate::{
app::{command, command::Command, events::event_types::EventStatus, App}, app::{
ui, command_interface::Command,
events::event_types::{Event, EventStatus},
App,
},
ui::central,
}; };
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> { pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
@ -11,39 +15,48 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
}) => { }) => {
command::execute(app.channel_tx(), Command::Exit).await?; app.tx
.send(Event::CommandEvent(Command::Exit, None))
.await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
}) => { }) => {
command::execute(app.channel_tx(), Command::CyclePlanes).await?; app.tx
.send(Event::CommandEvent(Command::CyclePlanes, None))
.await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::BackTab, code: KeyCode::BackTab,
.. ..
}) => { }) => {
command::execute(app.channel_tx(), Command::CyclePlanesRev).await?; app.tx
.send(Event::CommandEvent(Command::CyclePlanesRev, None))
.await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Char('c'), code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
.. ..
}) => { }) => {
command::execute(app.channel_tx(), Command::CommandLineShow).await?; app.tx
.send(Event::CommandEvent(Command::CommandLineShow, None))
.await?;
} }
input => match app.ui.input_position() { input => match app.ui.input_position() {
ui::MainInputPosition::MessageCompose => { central::InputPosition::MessageCompose => {
match input { match input {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
modifiers: KeyModifiers::ALT, modifiers: KeyModifiers::ALT,
.. ..
}) => { }) => {
command::execute( app.tx
app.channel_tx(), .send(Event::CommandEvent(
Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")), Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")),
) None,
.await?; ))
.await?;
app.ui.message_compose_clear(); app.ui.message_compose_clear();
} }
_ => { _ => {
@ -53,7 +66,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
} }
}; };
} }
ui::MainInputPosition::Rooms => { central::InputPosition::Rooms => {
match input { match input {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Up, .. code: KeyCode::Up, ..
@ -91,7 +104,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
_ => (), _ => (),
}; };
} }
ui::MainInputPosition::Messages => { central::InputPosition::Messages => {
match input { match input {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Up, .. code: KeyCode::Up, ..
@ -136,14 +149,14 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
_ => (), _ => (),
}; };
} }
ui::MainInputPosition::CLI => { central::InputPosition::CLI => {
if let Some(_) = app.ui.cli { if let Some(_) = app.ui.cli {
match input { match input {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
}) => { }) => {
let cli_event = app.ui let ci_event = app.ui
.cli .cli
.as_mut() .as_mut()
.expect("This is already checked") .expect("This is already checked")
@ -153,16 +166,10 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
"There can only be one line in the buffer, as we collect it on enter being inputted" "There can only be one line in the buffer, as we collect it on enter being inputted"
) )
.to_owned(); .to_owned();
let output = app.handle_ci_event(&cli_event).await?; app.tx
.send(Event::LuaCommand(ci_event))
// delete the old text: .await
.context("Failed to send lua command to internal event stream")?;
// We can use a mutable borrow now, as we should only need one
let cli = app.ui.cli.as_mut().expect("Checked above");
cli.move_cursor(tui_textarea::CursorMove::Jump(0, 0));
cli.delete_str(0, cli_event.chars().count());
assert!(cli.is_empty());
cli.insert_str(output);
} }
_ => { _ => {
app.ui app.ui

View File

@ -1,4 +1,11 @@
pub mod command; // input events
pub mod main;
pub mod matrix;
pub mod setup; pub mod setup;
pub mod main;
// matrix
pub mod matrix;
// ci
pub mod ci_output;
pub mod command;
pub mod lua_command;

View File

@ -1,10 +1,7 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent}; use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
use crate::{ use crate::{app::{events::event_types::EventStatus, App}, ui::setup};
app::{events::event_types::EventStatus, App},
ui,
};
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 { let ui = match &mut app.ui.setup_ui {
@ -32,7 +29,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
.. ..
}) => { }) => {
match ui.input_position() { match ui.input_position() {
ui::SetupInputPosition::Ok => { setup::InputPosition::Ok => {
let homeserver = ui.homeserver.lines()[0].clone(); let homeserver = ui.homeserver.lines()[0].clone();
let username = ui.username.lines()[0].clone(); let username = ui.username.lines()[0].clone();
let password = ui.password_data.lines()[0].clone(); let password = ui.password_data.lines()[0].clone();
@ -46,13 +43,13 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
}; };
} }
input => match ui.input_position() { input => match ui.input_position() {
ui::SetupInputPosition::Homeserver => { setup::InputPosition::Homeserver => {
ui.homeserver.input(input.to_owned()); ui.homeserver.input(input.to_owned());
} }
ui::SetupInputPosition::Username => { setup::InputPosition::Username => {
ui.username.input(input.to_owned()); ui.username.input(input.to_owned());
} }
ui::SetupInputPosition::Password => { setup::InputPosition::Password => {
let textarea_input = tui_textarea::Input::from(input.to_owned()); let textarea_input = tui_textarea::Input::from(input.to_owned());
ui.password_data.input(textarea_input.clone()); ui.password_data.input(textarea_input.clone());
match textarea_input.key { match textarea_input.key {

View File

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

View File

@ -1,45 +1,46 @@
pub mod command;
pub mod command_interface; pub mod command_interface;
pub mod events; pub mod events;
pub mod status; pub mod status;
use std::path::Path; use std::{path::Path, sync::Arc};
use accounts::{Account, AccountsManager};
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use cli_log::info; use cli_log::info;
use matrix_sdk::Client; use matrix_sdk::Client;
use rlua::Lua; use mlua::Lua;
use status::{State, Status}; use status::{State, Status};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{accounts, app::command_interface::generate_ci_functions, ui}; use crate::{
accounts::{Account, AccountsManager},
app::{command_interface::generate_ci_functions, events::event_types::Event},
ui::{central, setup},
};
use self::events::event_types::{self, Event}; use self::events::event_types;
pub struct App<'ui> { pub struct App<'ui> {
ui: ui::UI<'ui>, ui: central::UI<'ui>,
accounts_manager: accounts::AccountsManager, accounts_manager: AccountsManager,
status: Status, status: Status,
channel_tx: mpsc::Sender<event_types::Event>, tx: mpsc::Sender<Event>,
channel_rx: mpsc::Receiver<event_types::Event>, rx: mpsc::Receiver<Event>,
input_listener_killer: CancellationToken, input_listener_killer: CancellationToken,
matrix_listener_killer: CancellationToken, matrix_listener_killer: CancellationToken,
lua: Lua, lua: Arc<Lua>,
} }
impl App<'_> { impl App<'_> {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
fn set_up_lua() -> Lua { fn set_up_lua(tx: mpsc::Sender<Event>) -> Arc<Lua> {
let lua = Lua::new(); let mut lua = Lua::new();
lua.context(|mut lua_context| { generate_ci_functions(&mut lua, tx);
generate_ci_functions(&mut lua_context); Arc::new(lua)
});
lua
} }
let path: &std::path::Path = Path::new("userdata/accounts.json"); let path: &std::path::Path = Path::new("userdata/accounts.json");
@ -50,41 +51,25 @@ impl App<'_> {
None None
}; };
let (channel_tx, channel_rx) = mpsc::channel(256); let (tx, rx) = mpsc::channel(256);
Ok(Self { Ok(Self {
ui: ui::UI::new()?, ui: central::UI::new()?,
accounts_manager: AccountsManager::new(config)?, accounts_manager: AccountsManager::new(config)?,
status: Status::new(None), status: Status::new(None),
channel_tx, tx: tx.clone(),
channel_rx, rx,
input_listener_killer: CancellationToken::new(), input_listener_killer: CancellationToken::new(),
matrix_listener_killer: CancellationToken::new(), matrix_listener_killer: CancellationToken::new(),
lua: set_up_lua(), lua: set_up_lua(tx),
}) })
} }
pub async fn handle_ci_event(&self, event: &str) -> Result<String> {
info!("Recieved ci event: `{event}`; executing..");
// TODO: Should the ci support more than strings?
let output = self.lua.context(|context| -> Result<String> {
let output = context
.load(&event)
.eval::<String>()
.with_context(|| format!("Failed to execute: `{event}`"))?;
info!("Function evaluated to: `{output}`");
Ok(output)
})?;
Ok(output)
}
pub async fn run(&mut self) -> Result<()> { pub async fn run(&mut self) -> Result<()> {
// Spawn input event listener // Spawn input event listener
tokio::task::spawn(events::poll_input_events( tokio::task::spawn(events::poll_input_events(
self.channel_tx.clone(), self.tx.clone(),
self.input_listener_killer.clone(), self.input_listener_killer.clone(),
)); ));
@ -100,10 +85,7 @@ impl App<'_> {
self.status.set_state(State::Main); self.status.set_state(State::Main);
self.ui.update(&self.status).await?; self.ui.update(&self.status).await?;
let event: event_types::Event = match self.channel_rx.recv().await { let event = self.rx.recv().await.context("Failed to get next event")?;
Some(e) => e,
None => return Err(Error::msg("Event channel has no senders")),
};
match event.handle(self).await? { match event.handle(self).await? {
event_types::EventStatus::Ok => (), event_types::EventStatus::Ok => (),
@ -117,16 +99,13 @@ impl App<'_> {
} }
async fn setup(&mut self) -> Result<()> { async fn setup(&mut self) -> Result<()> {
self.ui.setup_ui = Some(ui::SetupUI::new()); self.ui.setup_ui = Some(setup::UI::new());
loop { loop {
self.status.set_state(State::Setup); self.status.set_state(State::Setup);
self.ui.update_setup().await?; self.ui.update_setup().await?;
let event: event_types::Event = match self.channel_rx.recv().await { let event = self.rx.recv().await.context("Failed to get next event")?;
Some(e) => e,
None => return Err(Error::msg("Event channel has no senders")),
};
match event.handle(self).await? { match event.handle(self).await? {
event_types::EventStatus::Ok => (), event_types::EventStatus::Ok => (),
@ -150,7 +129,7 @@ impl App<'_> {
// Spawn Matrix Event Listener // Spawn Matrix Event Listener
tokio::task::spawn(events::poll_matrix_events( tokio::task::spawn(events::poll_matrix_events(
self.channel_tx.clone(), self.tx.clone(),
self.matrix_listener_killer.clone(), self.matrix_listener_killer.clone(),
client.clone(), client.clone(),
)); ));
@ -204,8 +183,4 @@ impl App<'_> {
pub fn client(&self) -> Option<&Client> { pub fn client(&self) -> Option<&Client> {
self.accounts_manager.client() self.accounts_manager.client()
} }
pub fn channel_tx(&self) -> &mpsc::Sender<Event> {
&self.channel_tx
}
} }

View File

@ -1,5 +1,5 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use cli_log::{warn, info}; use cli_log::warn;
use indexmap::IndexMap; use indexmap::IndexMap;
use matrix_sdk::{ use matrix_sdk::{
room::MessagesOptions, room::MessagesOptions,

165
src/ui/central/mod.rs Normal file
View File

@ -0,0 +1,165 @@
pub mod update;
use std::io::Stdout;
use anyhow::{bail, Context, Result};
use cli_log::{info, warn};
use crossterm::{
event::DisableMouseCapture,
execute,
terminal::{disable_raw_mode, LeaveAlternateScreen},
};
use tui::{
backend::CrosstermBackend,
widgets::{Block, Borders, ListState},
Terminal,
};
use tui_textarea::TextArea;
use crate::ui::terminal_prepare;
use super::setup;
pub use update::*;
#[derive(Clone, Copy, PartialEq)]
pub enum InputPosition {
Status,
Rooms,
Messages,
MessageCompose,
RoomInfo,
CLI,
}
pub struct UI<'a> {
terminal: Terminal<CrosstermBackend<Stdout>>,
input_position: InputPosition,
pub rooms_state: ListState,
pub message_compose: TextArea<'a>,
pub cli: Option<TextArea<'a>>,
pub setup_ui: Option<setup::UI<'a>>,
}
impl Drop for UI<'_> {
fn drop(&mut self) {
info!("Destructing UI");
disable_raw_mode().expect("While destructing UI -> Failed to disable raw mode");
execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
).expect("While destructing UI -> Failed execute backend commands (LeaveAlternateScreen and DisableMouseCapture)");
self.terminal
.show_cursor()
.expect("While destructing UI -> Failed to re-enable cursor");
}
}
impl UI<'_> {
pub fn new() -> Result<Self> {
let stdout = terminal_prepare().context("Falied to prepare terminal")?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let mut message_compose = TextArea::default();
message_compose.set_block(
Block::default()
.title("Message Compose (send: <Alt>+<Enter>)")
.borders(Borders::ALL),
);
info!("Initialized UI");
Ok(Self {
terminal,
input_position: InputPosition::Rooms,
rooms_state: ListState::default(),
message_compose,
cli: None,
setup_ui: None,
})
}
pub fn cycle_main_input_position(&mut self) {
self.input_position = match self.input_position {
InputPosition::Status => InputPosition::Rooms,
InputPosition::Rooms => InputPosition::Messages,
InputPosition::Messages => InputPosition::MessageCompose,
InputPosition::MessageCompose => InputPosition::RoomInfo,
InputPosition::RoomInfo => match self.cli {
Some(_) => InputPosition::CLI,
None => InputPosition::Status,
},
InputPosition::CLI => InputPosition::Status,
};
}
pub fn cycle_main_input_position_rev(&mut self) {
self.input_position = match self.input_position {
InputPosition::Status => match self.cli {
Some(_) => InputPosition::CLI,
None => InputPosition::RoomInfo,
},
InputPosition::Rooms => InputPosition::Status,
InputPosition::Messages => InputPosition::Rooms,
InputPosition::MessageCompose => InputPosition::Messages,
InputPosition::RoomInfo => InputPosition::MessageCompose,
InputPosition::CLI => InputPosition::RoomInfo,
};
}
pub fn input_position(&self) -> &InputPosition {
&self.input_position
}
pub fn message_compose_clear(&mut self) {
self.message_compose = TextArea::default();
self.message_compose.set_block(
Block::default()
.title("Message Compose (send: <Alt>+<Enter>)")
.borders(Borders::ALL),
);
}
pub fn set_command_output(&mut self, output: &str) {
info!("Setting output to: `{}`", output);
if let Some(_) = self.cli {
let cli = Some(TextArea::from([output]));
self.cli = cli;
} else {
warn!("Failed to set output");
}
}
pub fn cli_enable(&mut self) {
self.input_position = InputPosition::CLI;
if self.cli.is_some() {
return;
}
let mut cli = TextArea::default();
cli.set_block(Block::default().borders(Borders::ALL));
self.cli = Some(cli);
}
pub fn cli_disable(&mut self) {
if self.input_position == InputPosition::CLI {
self.cycle_main_input_position();
}
self.cli = None;
}
pub async fn update_setup(&mut self) -> Result<()> {
let ui = match &mut self.setup_ui {
Some(c) => c,
None => bail!("SetupUI instance not found"),
};
ui.update(&mut self.terminal).await?;
Ok(())
}
}

View File

@ -0,0 +1,167 @@
use std::cmp;
use anyhow::{Context, Result};
use tui::{
layout::{Constraint, Direction, Layout},
style::Color,
};
use crate::{
app::status::Status,
ui::{textarea_activate, textarea_inactivate},
};
use self::widgets::{messages, room_info, rooms, status};
use super::{InputPosition, UI};
pub mod widgets;
impl UI<'_> {
pub async fn update(&mut self, status: &Status) -> Result<()> {
let chunks = match self.cli {
Some(_) => Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(10), Constraint::Length(3)].as_ref())
.split(self.terminal.size()?),
None => vec![self.terminal.size()?],
};
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Length(32),
Constraint::Min(16),
Constraint::Length(32),
]
.as_ref(),
)
.split(chunks[0]);
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
.split(main_chunks[0]);
let middle_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Min(4),
Constraint::Length(cmp::min(2 + self.message_compose.lines().len() as u16, 8)),
]
.as_ref(),
)
.split(main_chunks[1]);
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([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,
]
}
};
// initiate the widgets
let status_panel = status::init(status, &colors);
let rooms_panel = rooms::init(status, &colors);
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);
// render the widgets
self.terminal.draw(|frame| {
frame.render_widget(status_panel, left_chunks[0]);
frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state);
frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_state);
frame.render_widget(self.message_compose.widget(), middle_chunks[1]);
match &self.cli {
Some(cli) => frame.render_widget(cli.widget(), chunks[1]),
None => (),
};
frame.render_widget(room_info_panel, right_chunks[0]);
})?;
Ok(())
}
}

View File

@ -0,0 +1,109 @@
use anyhow::{Context, Result};
use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent};
use tui::{
layout::Corner,
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, ListState},
};
use crate::{app::status::Room, ui::central::InputPosition};
pub fn init<'a>(room: Option<&'a Room>, colors: &Vec<Color>) -> Result<(List<'a>, ListState)> {
let content = match room {
Some(room) => get_content_from_room(room).context("Failed to get content from room")?,
None => vec![ListItem::new(Text::styled(
"No room selected!",
Style::default().fg(Color::Red),
))],
};
let mut messages_state = ListState::default();
if let Some(room) = room {
messages_state.select(room.view_scroll());
}
Ok((
List::new(content)
.block(
Block::default()
.title("Messages")
.borders(Borders::ALL)
.style(Style::default().fg(colors[InputPosition::Messages as usize])),
)
.start_corner(Corner::BottomLeft)
.highlight_symbol(">")
.highlight_style(
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::BOLD),
),
messages_state,
))
}
fn get_content_from_room(room: &Room) -> Result<Vec<ListItem>> {
let results: Vec<Result<ListItem>> = room
.timeline()
.iter()
.rev()
.map(|event| filter_event(event).context("Failed to filter event"))
.collect();
let mut output = Vec::with_capacity(results.len());
for result in results {
output.push(result?);
}
Ok(output)
}
fn filter_event<'a>(event: &AnyTimelineEvent) -> Result<ListItem<'a>> {
match event {
// Message Like Events
AnyTimelineEvent::MessageLike(message_like_event) => {
let (content, color) = match &message_like_event {
AnyMessageLikeEvent::RoomMessage(room_message_event) => {
let message_content = &room_message_event
.as_original()
.context("Failed to get inner original message_event")?
.content
.body();
(message_content.to_string(), Color::White)
}
_ => (
format!(
"~~~ not supported message like event: {} ~~~",
message_like_event.event_type().to_string()
),
Color::Red,
),
};
let mut text = Text::styled(
message_like_event.sender().to_string(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
text.extend(Text::styled(
content.to_string(),
Style::default().fg(color),
));
Ok(ListItem::new(text))
}
// State Events
AnyTimelineEvent::State(state) => Ok(ListItem::new(vec![Spans::from(vec![
Span::styled(
state.sender().to_string(),
Style::default().fg(Color::DarkGray),
),
Span::styled(": ", Style::default().fg(Color::DarkGray)),
Span::styled(
state.event_type().to_string(),
Style::default().fg(Color::DarkGray),
),
])])),
}
}

View File

@ -0,0 +1,4 @@
pub mod messages;
pub mod room_info;
pub mod rooms;
pub mod status;

View File

@ -0,0 +1,36 @@
use tui::{
style::{Color, Style},
text::Text,
widgets::{Block, Borders, Paragraph}, layout::Alignment,
};
use crate::{app::status::Room, ui::central::InputPosition};
pub fn init<'a>(room: Option<&'a Room>, colors: &Vec<Color>) -> Paragraph<'a> {
let mut room_info_content = Text::default();
if let Some(room) = room {
room_info_content.extend(Text::styled(room.name(), Style::default().fg(Color::Cyan)));
if room.encrypted() {
room_info_content.extend(Text::styled("Encrypted", Style::default().fg(Color::Green)));
} else {
room_info_content.extend(Text::styled(
"Not Encrypted!",
Style::default().fg(Color::Red),
));
}
} else {
room_info_content.extend(Text::styled(
"No room selected!",
Style::default().fg(Color::Red),
));
}
Paragraph::new(room_info_content)
.block(
Block::default()
.title("Room Info")
.borders(Borders::ALL)
.style(Style::default().fg(colors[InputPosition::RoomInfo as usize])),
)
.alignment(Alignment::Center)
}

View File

@ -0,0 +1,29 @@
use tui::{
style::{Color, Modifier, Style},
text::Span,
widgets::{Borders, List, ListItem, Block},
};
use crate::{app::status::Status, ui::central::InputPosition};
pub fn init<'a>(status: &'a Status, colors: &Vec<Color>) -> List<'a> {
let rooms_content: Vec<_> = status
.rooms()
.iter()
.map(|(_, room)| ListItem::new(Span::styled(room.name(), Style::default())))
.collect();
List::new(rooms_content)
.block(
Block::default()
.title("Rooms (navigate: arrow keys)")
.borders(Borders::ALL)
.style(Style::default().fg(colors[InputPosition::Rooms as usize])),
)
.style(Style::default().fg(Color::DarkGray))
.highlight_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">")
}

View File

@ -0,0 +1,30 @@
use tui::{
layout::Alignment,
style::{Color, Modifier, Style},
text::Text,
widgets::{Block, Borders, Paragraph},
};
use crate::{app::status::Status, ui::central::InputPosition};
pub fn init<'a>(status: &'a Status, colors: &Vec<Color>) -> Paragraph<'a> {
let mut status_content = Text::styled(
status.account_name(),
Style::default().add_modifier(Modifier::BOLD),
);
status_content.extend(Text::styled(status.account_user_id(), Style::default()));
status_content.extend(Text::styled(
"settings",
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED),
));
Paragraph::new(status_content)
.block(
Block::default()
.title("Status")
.borders(Borders::ALL)
.style(Style::default().fg(colors[InputPosition::Status as usize])),
)
.alignment(Alignment::Left)
}

View File

@ -1,64 +1,23 @@
use std::{cmp, io, io::Stdout}; pub mod central;
pub mod setup;
use anyhow::{Error, Result}; use std::{io, io::Stdout};
use anyhow::{Context, Result};
use cli_log::info; use cli_log::info;
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture}, event::EnableMouseCapture,
execute, execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{enable_raw_mode, EnterAlternateScreen},
}; };
use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent};
use tui::{ use tui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans, Text}, widgets::{Block, Borders},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
Terminal,
}; };
use tui_textarea::TextArea; use tui_textarea::TextArea;
use crate::app::status::Status;
#[derive(Clone, Copy)]
pub enum SetupInputPosition {
Homeserver,
Username,
Password,
Ok,
}
#[derive(Clone, Copy, PartialEq)]
pub enum MainInputPosition {
Status,
Rooms,
Messages,
MessageCompose,
RoomInfo,
CLI,
}
pub struct SetupUI<'a> {
input_position: SetupInputPosition,
pub homeserver: TextArea<'a>,
pub username: TextArea<'a>,
pub password: TextArea<'a>,
pub password_data: TextArea<'a>,
}
pub struct UI<'a> {
terminal: Terminal<CrosstermBackend<Stdout>>,
input_position: MainInputPosition,
pub rooms_state: ListState,
pub message_compose: TextArea<'a>,
pub cli: Option<TextArea<'a>>,
pub setup_ui: Option<SetupUI<'a>>,
}
fn terminal_prepare() -> Result<Stdout> { fn terminal_prepare() -> Result<Stdout> {
enable_raw_mode()?; enable_raw_mode().context("Failed to enable raw mode")?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
info!("Prepared terminal"); info!("Prepared terminal");
@ -84,553 +43,3 @@ pub fn textarea_inactivate(textarea: &mut TextArea) {
.unwrap_or_else(|| Block::default().borders(Borders::ALL)); .unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default().fg(Color::DarkGray))); textarea.set_block(b.style(Style::default().fg(Color::DarkGray)));
} }
impl Drop for UI<'_> {
fn drop(&mut self) {
info!("Destructing UI");
disable_raw_mode().expect("While destructing UI -> Failed to disable raw mode");
execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
).expect("While destructing UI -> Failed execute backend commands (LeaveAlternateScreen and DisableMouseCapture)");
self.terminal
.show_cursor()
.expect("While destructing UI -> Failed to re-enable cursor");
}
}
impl SetupUI<'_> {
pub fn new() -> Self {
let mut homeserver = TextArea::new(vec!["https://matrix.org".to_string()]);
let mut username = TextArea::default();
let mut password = TextArea::default();
let password_data = TextArea::default();
homeserver.set_block(Block::default().title("Homeserver").borders(Borders::ALL));
username.set_block(Block::default().title("Username").borders(Borders::ALL));
password.set_block(Block::default().title("Password").borders(Borders::ALL));
textarea_activate(&mut homeserver);
textarea_inactivate(&mut username);
textarea_inactivate(&mut password);
Self {
input_position: SetupInputPosition::Homeserver,
homeserver,
username,
password,
password_data,
}
}
pub fn cycle_input_position(&mut self) {
self.input_position = match self.input_position {
SetupInputPosition::Homeserver => {
textarea_inactivate(&mut self.homeserver);
textarea_activate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Username
}
SetupInputPosition::Username => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_activate(&mut self.password);
SetupInputPosition::Password
}
SetupInputPosition::Password => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Ok
}
SetupInputPosition::Ok => {
textarea_activate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Homeserver
}
};
}
pub fn cycle_input_position_rev(&mut self) {
self.input_position = match self.input_position {
SetupInputPosition::Homeserver => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Ok
}
SetupInputPosition::Username => {
textarea_activate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Homeserver
}
SetupInputPosition::Password => {
textarea_inactivate(&mut self.homeserver);
textarea_activate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Username
}
SetupInputPosition::Ok => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_activate(&mut self.password);
SetupInputPosition::Password
}
};
}
pub fn input_position(&self) -> &SetupInputPosition {
&self.input_position
}
pub async fn update(
&'_ mut self,
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<()> {
let mut strings: Vec<String> = Vec::new();
strings.resize(3, "".to_string());
let content_ok = match self.input_position {
SetupInputPosition::Ok => {
Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED))
}
_ => Span::styled("OK", Style::default().fg(Color::DarkGray)),
};
let block = Block::default().title("Login").borders(Borders::ALL);
let ok = Paragraph::new(content_ok).alignment(Alignment::Center);
// define a 32 * 6 chunk in the middle of the screen
let mut chunk = terminal.size()?;
chunk.x = (chunk.width / 2) - 16;
chunk.y = (chunk.height / 2) - 5;
chunk.height = 12;
chunk.width = 32;
let mut split_chunk = chunk.clone();
split_chunk.x += 1;
split_chunk.y += 1;
split_chunk.height -= 1;
split_chunk.width -= 2;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // 0. Homserver:
Constraint::Length(3), // 1. Username:
Constraint::Length(3), // 2. Password:
Constraint::Length(1), // 3. OK
]
.as_ref(),
)
.split(split_chunk);
terminal.draw(|frame| {
frame.render_widget(block.clone(), chunk);
frame.render_widget(self.homeserver.widget(), chunks[0]);
frame.render_widget(self.username.widget(), chunks[1]);
frame.render_widget(self.password.widget(), chunks[2]);
frame.render_widget(ok.clone(), chunks[3]);
})?;
Ok(())
}
}
impl UI<'_> {
pub fn new() -> Result<Self> {
let stdout = terminal_prepare()?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let mut message_compose = TextArea::default();
message_compose.set_block(
Block::default()
.title("Message Compose (send: <Alt>+<Enter>)")
.borders(Borders::ALL),
);
info!("Initialized UI");
Ok(Self {
terminal,
input_position: MainInputPosition::Rooms,
rooms_state: ListState::default(),
message_compose,
cli: None,
setup_ui: None,
})
}
pub fn cycle_main_input_position(&mut self) {
self.input_position = match self.input_position {
MainInputPosition::Status => MainInputPosition::Rooms,
MainInputPosition::Rooms => MainInputPosition::Messages,
MainInputPosition::Messages => MainInputPosition::MessageCompose,
MainInputPosition::MessageCompose => MainInputPosition::RoomInfo,
MainInputPosition::RoomInfo => match self.cli {
Some(_) => MainInputPosition::CLI,
None => MainInputPosition::Status,
},
MainInputPosition::CLI => MainInputPosition::Status,
};
}
pub fn cycle_main_input_position_rev(&mut self) {
self.input_position = match self.input_position {
MainInputPosition::Status => match self.cli {
Some(_) => MainInputPosition::CLI,
None => MainInputPosition::RoomInfo,
},
MainInputPosition::Rooms => MainInputPosition::Status,
MainInputPosition::Messages => MainInputPosition::Rooms,
MainInputPosition::MessageCompose => MainInputPosition::Messages,
MainInputPosition::RoomInfo => MainInputPosition::MessageCompose,
MainInputPosition::CLI => MainInputPosition::RoomInfo,
};
}
pub fn input_position(&self) -> &MainInputPosition {
&self.input_position
}
pub fn message_compose_clear(&mut self) {
self.message_compose = TextArea::default();
self.message_compose.set_block(
Block::default()
.title("Message Compose (send: <Alt>+<Enter>)")
.borders(Borders::ALL),
);
}
pub fn cli_enable(&mut self) {
self.input_position = MainInputPosition::CLI;
if self.cli.is_some() {
return;
}
let mut cli = TextArea::default();
cli.set_block(Block::default().borders(Borders::ALL));
self.cli = Some(cli);
}
pub fn cli_disable(&mut self) {
if self.input_position == MainInputPosition::CLI {
self.cycle_main_input_position();
}
self.cli = None;
}
pub async fn update(&mut self, status: &Status) -> Result<()> {
let chunks = match self.cli {
Some(_) => Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(10), Constraint::Length(3)].as_ref())
.split(self.terminal.size()?),
None => vec![self.terminal.size()?],
};
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Length(32),
Constraint::Min(16),
Constraint::Length(32),
]
.as_ref(),
)
.split(chunks[0]);
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
.split(main_chunks[0]);
let middle_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Min(4),
Constraint::Length(cmp::min(2 + self.message_compose.lines().len() as u16, 8)),
]
.as_ref(),
)
.split(main_chunks[1]);
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(4)].as_ref())
.split(main_chunks[2]);
let mut status_content = Text::styled(
status.account_name(),
Style::default().add_modifier(Modifier::BOLD),
);
status_content.extend(Text::styled(status.account_user_id(), Style::default()));
status_content.extend(Text::styled(
"settings",
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED),
));
let rooms_content = status
.rooms()
.iter()
.map(|(_, room)| ListItem::new(Span::styled(room.name(), Style::default())))
.collect::<Vec<_>>();
let messages_content = match status.room() {
Some(r) => {
r.timeline()
.iter()
.rev()
.map(|event| {
match event {
// Message Like Events
AnyTimelineEvent::MessageLike(message_like_event) => {
let (content, color) = match &message_like_event {
AnyMessageLikeEvent::RoomMessage(room_message_event) => {
let message_content = &room_message_event
.as_original()
.unwrap()
.content
.body();
(message_content.to_string(), Color::White)
}
_ => (
format!(
"~~~ not supported message like event: {} ~~~",
message_like_event.event_type().to_string()
),
Color::Red,
),
};
let mut text = Text::styled(
message_like_event.sender().to_string(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
text.extend(Text::styled(
content.to_string(),
Style::default().fg(color),
));
ListItem::new(text)
}
// State Events
AnyTimelineEvent::State(state) => {
ListItem::new(vec![Spans::from(vec![
Span::styled(
state.sender().to_string(),
Style::default().fg(Color::DarkGray),
),
Span::styled(": ", Style::default().fg(Color::DarkGray)),
Span::styled(
state.event_type().to_string(),
Style::default().fg(Color::DarkGray),
),
])])
}
}
})
.collect::<Vec<_>>()
}
None => {
vec![ListItem::new(Text::styled(
"No room selected!",
Style::default().fg(Color::Red),
))]
}
};
let mut messages_state = ListState::default();
let mut room_info_content = Text::default();
if let Some(room) = status.room() {
messages_state.select(room.view_scroll());
room_info_content.extend(Text::styled(room.name(), Style::default().fg(Color::Cyan)));
if room.encrypted() {
room_info_content
.extend(Text::styled("Encrypted", Style::default().fg(Color::Green)));
} else {
room_info_content.extend(Text::styled(
"Not Encrypted!",
Style::default().fg(Color::Red),
));
}
} else {
room_info_content.extend(Text::styled(
"No room selected!",
Style::default().fg(Color::Red),
));
}
// calculate to widgets colors, based of which widget is currently selected
let colors = match self.input_position {
MainInputPosition::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,
]
}
MainInputPosition::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,
]
}
MainInputPosition::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,
]
}
MainInputPosition::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,
]
}
MainInputPosition::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,
]
}
MainInputPosition::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,
]
}
};
// initiate the widgets
let status_panel = Paragraph::new(status_content)
.block(
Block::default()
.title("Status")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Status as usize])),
)
.alignment(Alignment::Left);
let rooms_panel = List::new(rooms_content)
.block(
Block::default()
.title("Rooms (navigate: arrow keys)")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize])),
)
.style(Style::default().fg(Color::DarkGray))
.highlight_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">");
let messages_panel = List::new(messages_content)
.block(
Block::default()
.title("Messages")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Messages as usize])),
)
.start_corner(Corner::BottomLeft)
.highlight_symbol(">")
.highlight_style(
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::BOLD),
);
let room_info_panel = Paragraph::new(room_info_content)
.block(
Block::default()
.title("Room Info")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize])),
)
.alignment(Alignment::Center);
// render the widgets
self.terminal.draw(|frame| {
frame.render_widget(status_panel, left_chunks[0]);
frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state);
frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_state);
frame.render_widget(self.message_compose.widget(), middle_chunks[1]);
match &self.cli {
Some(cli) => frame.render_widget(cli.widget(), chunks[1]),
None => (),
};
frame.render_widget(room_info_panel, right_chunks[0]);
})?;
Ok(())
}
pub async fn update_setup(&mut self) -> Result<()> {
let ui = match &mut self.setup_ui {
Some(c) => c,
None => return Err(Error::msg("SetupUI instance not found")),
};
ui.update(&mut self.terminal).await?;
Ok(())
}
}

172
src/ui/setup.rs Normal file
View File

@ -0,0 +1,172 @@
use std::io::Stdout;
use anyhow::Result;
use tui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Alignment},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Paragraph},
Terminal,
};
use tui_textarea::TextArea;
use crate::ui::{textarea_activate, textarea_inactivate};
pub struct UI<'a> {
input_position: InputPosition,
pub homeserver: TextArea<'a>,
pub username: TextArea<'a>,
pub password: TextArea<'a>,
pub password_data: TextArea<'a>,
}
#[derive(Clone, Copy)]
pub enum InputPosition {
Homeserver,
Username,
Password,
Ok,
}
impl UI<'_> {
pub fn new() -> Self {
let mut homeserver = TextArea::new(vec!["https://matrix.org".to_string()]);
let mut username = TextArea::default();
let mut password = TextArea::default();
let password_data = TextArea::default();
homeserver.set_block(Block::default().title("Homeserver").borders(Borders::ALL));
username.set_block(Block::default().title("Username").borders(Borders::ALL));
password.set_block(Block::default().title("Password").borders(Borders::ALL));
textarea_activate(&mut homeserver);
textarea_inactivate(&mut username);
textarea_inactivate(&mut password);
Self {
input_position: InputPosition::Homeserver,
homeserver,
username,
password,
password_data,
}
}
pub fn cycle_input_position(&mut self) {
self.input_position = match self.input_position {
InputPosition::Homeserver => {
textarea_inactivate(&mut self.homeserver);
textarea_activate(&mut self.username);
textarea_inactivate(&mut self.password);
InputPosition::Username
}
InputPosition::Username => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_activate(&mut self.password);
InputPosition::Password
}
InputPosition::Password => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
InputPosition::Ok
}
InputPosition::Ok => {
textarea_activate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
InputPosition::Homeserver
}
};
}
pub fn cycle_input_position_rev(&mut self) {
self.input_position = match self.input_position {
InputPosition::Homeserver => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
InputPosition::Ok
}
InputPosition::Username => {
textarea_activate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
InputPosition::Homeserver
}
InputPosition::Password => {
textarea_inactivate(&mut self.homeserver);
textarea_activate(&mut self.username);
textarea_inactivate(&mut self.password);
InputPosition::Username
}
InputPosition::Ok => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_activate(&mut self.password);
InputPosition::Password
}
};
}
pub fn input_position(&self) -> &InputPosition {
&self.input_position
}
pub async fn update(
&mut self,
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<()> {
let strings: Vec<String> = vec!["".to_owned(); 3];
let content_ok = match self.input_position {
InputPosition::Ok => {
Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED))
}
_ => Span::styled("OK", Style::default().fg(Color::DarkGray)),
};
let block = Block::default().title("Login").borders(Borders::ALL);
let ok = Paragraph::new(content_ok).alignment(Alignment::Center);
// define a 32 * 6 chunk in the middle of the screen
let mut chunk = terminal.size()?;
chunk.x = (chunk.width / 2) - 16;
chunk.y = (chunk.height / 2) - 5;
chunk.height = 12;
chunk.width = 32;
let mut split_chunk = chunk.clone();
split_chunk.x += 1;
split_chunk.y += 1;
split_chunk.height -= 1;
split_chunk.width -= 2;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // 0. Homserver:
Constraint::Length(3), // 1. Username:
Constraint::Length(3), // 2. Password:
Constraint::Length(1), // 3. OK
]
.as_ref(),
)
.split(split_chunk);
terminal.draw(|frame| {
frame.render_widget(block.clone(), chunk);
frame.render_widget(self.homeserver.widget(), chunks[0]);
frame.render_widget(self.username.widget(), chunks[1]);
frame.render_widget(self.password.widget(), chunks[2]);
frame.render_widget(ok.clone(), chunks[3]);
})?;
Ok(())
}
}