diff --git a/Cargo.lock b/Cargo.lock index 1c4c9f4..c5ca408 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "anymap2" @@ -138,9 +138,9 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -149,9 +149,9 @@ version = "0.1.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -415,6 +415,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "core-foundation" version = "0.9.3" @@ -547,8 +556,8 @@ checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.31", "strsim", "syn 1.0.109", ] @@ -560,18 +569,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core", - "quote 1.0.29", + "quote 1.0.31", "syn 1.0.109", ] [[package]] name = "dashmap" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" dependencies = [ "cfg-if", - "hashbrown 0.12.3", + "hashbrown 0.14.0", "lock_api", "once_cell", "parking_lot_core 0.9.8", @@ -602,8 +611,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" dependencies = [ "darling", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.31", "syn 1.0.109", ] @@ -649,9 +658,9 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -696,9 +705,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" @@ -848,9 +857,9 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -1213,9 +1222,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" @@ -1291,9 +1300,10 @@ dependencies = [ name = "lua_macros" version = "0.1.0" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "convert_case", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -1523,6 +1533,23 @@ dependencies = [ "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]] name = "native-tls" version = "0.2.11" @@ -1602,9 +1629,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -1717,9 +1744,9 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -1788,9 +1815,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -1822,8 +1849,8 @@ checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", "itertools", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.31", "syn 1.0.109", ] @@ -1838,11 +1865,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" dependencies = [ - "proc-macro2 1.0.64", + "proc-macro2 1.0.66", ] [[package]] @@ -2063,9 +2090,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" dependencies = [ "aho-corasick", "memchr", @@ -2074,9 +2101,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "reqwest" @@ -2115,30 +2142,6 @@ dependencies = [ "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]] name = "ruma" version = "0.7.4" @@ -2230,8 +2233,8 @@ checksum = "0f82e91eb61cd86d9287303133ee55b54618eccb75a522cc22a42c15f5bda340" dependencies = [ "once_cell", "proc-macro-crate", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.31", "ruma-identifiers-validation", "serde", "syn 1.0.109", @@ -2244,6 +2247,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.37.23" @@ -2260,9 +2269,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schannel" @@ -2304,38 +2313,38 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.169" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd51c3db8f9500d531e6c12dd0fd4ad13d133e9117f5aebac3cdbb8b6d9824b0" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.169" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27738cfea0d944ab72c3ed01f3d5f23ec4322af8a1431e40ce630e4c01ea74fd" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] name = "serde_json" -version = "1.0.100" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" dependencies = [ "itoa", "ryu", @@ -2380,9 +2389,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3" dependencies = [ "libc", "signal-hook-registry", @@ -2493,19 +2502,19 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.31", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.25" +version = "2.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.31", "unicode-ident", ] @@ -2538,9 +2547,9 @@ version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -2593,9 +2602,9 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -2639,9 +2648,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.12" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ "indexmap 2.0.0", "toml_datetime", @@ -2672,9 +2681,9 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] [[package]] @@ -2696,7 +2705,7 @@ dependencies = [ "indexmap 2.0.0", "lua_macros", "matrix-sdk", - "rlua", + "mlua", "serde", "tokio", "tokio-util", @@ -2747,9 +2756,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -2912,9 +2921,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", "wasm-bindgen-shared", ] @@ -2936,7 +2945,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote 1.0.29", + "quote 1.0.31", "wasm-bindgen-macro-support", ] @@ -2946,9 +2955,9 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3089,9 +3098,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.9" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529" +checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" dependencies = [ "memchr", ] @@ -3132,7 +3141,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", ] diff --git a/Cargo.toml b/Cargo.toml index 55e4b6d..0d6971f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,4 @@ tokio-util = "0.7" serde = "1.0" cli-log = "2.0" indexmap = "2.0.0" -rlua = "0.19.7" +mlua = { version = "0.8.9", features = ["lua54", "async", "send"] } diff --git a/flake.lock b/flake.lock index f639470..f952431 100644 --- a/flake.lock +++ b/flake.lock @@ -50,11 +50,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1687709756, - "narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=", + "lastModified": 1689068808, + "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", "owner": "numtide", "repo": "flake-utils", - "rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7", + "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", "type": "github" }, "original": { @@ -65,11 +65,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1688829822, - "narHash": "sha256-hv56yK1fPHPt7SU2DboxBtdSbIuv9nym7Dss7Cn2jic=", + "lastModified": 1689449371, + "narHash": "sha256-sK3Oi8uEFrFPL83wKPV6w0+96NrmwqIpw9YFffMifVg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ed6afb10dfdfc97b6bcf0703f1bad8118e9e961b", + "rev": "29bcead8405cfe4c00085843eb372cc43837bb9d", "type": "github" }, "original": { @@ -98,11 +98,11 @@ ] }, "locked": { - "lastModified": 1688870171, - "narHash": "sha256-8tD8fheWPa7TaJoxzcU3iHkCrQQpOpdMN+HYqgZ1N5A=", + "lastModified": 1689561325, + "narHash": "sha256-+UABrHUXtWJSc9mM7oEKPIYQEhTzUVVNy2IPG9Lfrj0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "5a932f10ac4bd59047d6e8b5780750ec76ea988a", + "rev": "d8a38aea13c67dc2ce10cff93eb274dcf455753f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 72bee03..9ce72bf 100644 --- a/flake.nix +++ b/flake.nix @@ -45,16 +45,20 @@ overlays = [(import rust-overlay)]; }; - #rust-nightly = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default); - rust-stable = pkgs.rust-bin.stable.latest.default; + nightly = true; + 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; [ pkg-config ]; buildInputs = with pkgs; [ openssl + lua54Packages.stdlib ]; craneBuild = craneLib.buildPackage { @@ -78,7 +82,7 @@ statix ltex-ls - rust-stable + rust rust-analyzer cargo-edit cargo-expand diff --git a/lua_macros/Cargo.toml b/lua_macros/Cargo.toml index bf1583b..cb754e2 100644 --- a/lua_macros/Cargo.toml +++ b/lua_macros/Cargo.toml @@ -4,9 +4,10 @@ version = "0.1.0" edition = "2021" [lib] -crate_type = ["proc-macro"] +proc-macro = true [dependencies] +convert_case = "0.6.0" proc-macro2 = "1.0.64" quote = "1.0.29" -syn = "2.0.25" +syn = { version = "2.0.25", features = ["extra-traits", "full", "parsing"] } diff --git a/lua_macros/src/lib.rs b/lua_macros/src/lib.rs index 777665e..871dd3f 100644 --- a/lua_macros/src/lib.rs +++ b/lua_macros/src/lib.rs @@ -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_macro2::TokenStream as TokenStream2; +use proc_macro2::{Span, TokenStream as TokenStream2}; 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] -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 // that we can manipulate - let input = syn::parse(input) - .expect("This should always be valid rust code, as it's extracted from direct code"); + let mut input: DeriveInput = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code"); + + let mut named_fields = match &input.data { + syn::Data::Struct(input) => match &input.fields { + syn::Fields::Named(named_fields) => named_fields, + _ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"), + }, + _ => unimplemented!("The macro only works for structs"), + } + .to_owned(); + + let attr_parsed = parse_quote! { + /// This is a help function + }; + + named_fields.named.push(syn::Field { + attrs: vec![attr_parsed], + // attrs: attr_parser + // .parse("#[doc = r\"This is a help function\"]".to_token_stream().into()) + // .expect("See reason for other one"), + vis: Visibility::Inherited, + mutability: FieldMutability::None, + ident: Some(format_ident!("help")), + colon_token: Some(Token![:](Span::call_site())), + ty: parse_str("fn(Option) -> String").expect("This is static and valid rust code"), + }); + + match &mut input.data { + syn::Data::Struct(input) => input.fields = syn::Fields::Named(named_fields.clone()), + _ => unreachable!("This was a DataStruct before"), + }; // Build the trait implementation - 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 { - let input_tokens: TokenStream2 = match &input.data { - syn::Data::Struct(input) => match &input.fields { - syn::Fields::Named(named_fields) => named_fields - .named - .iter() - .map(|field| -> TokenStream2 { - 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() +#[proc_macro_attribute] +pub fn ci_command(_attrs: TokenStream, input: TokenStream) -> TokenStream { + let mut input: ItemFn = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code"); + let output_function = generate_final_function(&mut input); + output_function.into() } diff --git a/lua_macros/src/mark_as_ci_command.rs b/lua_macros/src/mark_as_ci_command.rs new file mode 100644 index 0000000..76e2fc1 --- /dev/null +++ b/lua_macros/src/mark_as_ci_command.rs @@ -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 ) + if filtered_paths.len() > 2 { + unreachable!("There should be no more than two filtered_output, but got: {:#?}", filtered_paths) + } else if filtered_paths.len() <= 0 { + unreachable!("There should be more than zero filtered_output, but got: {:#?}", filtered_paths) + } + + if filtered_paths.len() == 2 { + // There is something else than mlua::Error + let gen_type = if let GenericArgument::Type(ret_type) = + filtered_paths + .first() + .expect("One path segment should exists") + .to_owned() + { + ret_type + } else { + unreachable!("These were filtered above."); + }; + let return_type_as_type_prepared = quote! {-> #gen_type}; + + let return_type_as_return_type: ReturnType = syn::parse( + return_type_as_type_prepared.to_token_stream().into(), + ) + .expect("This is valid."); + return_type_as_return_type + } else { + // There is only mlua::Error left + ReturnType::Default + } + } + _ => unimplemented!("Only for angled paths"), + } + } + _ => unimplemented!("Only for path types"), + }; + + let send_data = match return_type { + ReturnType::Default => { + quote! { + { + Event::CommandEvent(Command::#function_name_pascal, None) + } + } + } + ReturnType::Type(_, _) => { + quote! { + { + Event::CommandEvent(Command::#function_name_pascal(input.clone()), Some(callback_tx)) + } + } + } + }; + + let output_return = match return_type { + ReturnType::Default => { + quote! { + { + return Ok(()); + } + } + } + ReturnType::Type(_, _) => { + quote! { + { + if let Some(output) = callback_rx.recv().await { + callback_rx.close(); + return Ok(output); + } else { + return Err(mlua::Error::ExternalError(Arc::new(Error::new( + ErrorKind::Other, + "Callback reciever dropped", + )))); + } + } + } + } + }; + + quote! { + { + let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::(256); + + let tx: + core::cell::Ref> = + lua + .app_data_ref() + .expect("This exists, it was set before"); + + (*tx) + .try_send(#send_data) + .expect("This should work, as the reciever is not dropped"); + + #output_return + } + } + } + }; + + let tx_send_block: Block = + syn::parse(tx_send.into()).expect("This is a static string, it will always parse"); + + let tx_send_expr_block = ExprBlock { + attrs: vec![], + label: None, + block: tx_send_block, + }; + let mut tx_send_stmt = vec![Stmt::Expr(Expr::Block(tx_send_expr_block), None)]; + + let mut new_stmts: Vec = Vec::with_capacity(input.block.stmts.len() + 1); + new_stmts.append(&mut tx_send_stmt); + new_stmts.append(&mut input.block.stmts); + input.block.stmts = new_stmts; + input +} diff --git a/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs b/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs new file mode 100644 index 0000000..8e5fd7e --- /dev/null +++ b/lua_macros/src/struct_to_ci_enum/generate_command_enum.rs @@ -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 = if return_path.len() == 1 { + Some( + return_path + .last() + .expect("The last element exists") + .ty + .clone(), + ) + } else if return_path.len() == 0 { + None + } else { + panic!("The Function can only take on argument, or none"); + }; + input_type + } + _ => unimplemented!("This is only implemented for bare function types"), + }; + + let enum_variant_name = format_ident!( + "{}", + field_ident + .to_string() + .from_case(Case::Snake) + .to_case(Case::Pascal) + ); + if enum_variant_type.is_some() { + quote! { + #enum_variant_name (#enum_variant_type), + } + .into() + } else { + quote! { + #enum_variant_name, + } + } + }) + .collect(); + + let gen = quote! { + #[derive(Debug)] + pub enum Command { + #input_tokens + } + }; + gen.into() +} diff --git a/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs b/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs new file mode 100644 index 0000000..3c690ec --- /dev/null +++ b/lua_macros/src/struct_to_ci_enum/generate_generate_ci_function.rs @@ -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 = vec![]; + + let functions_to_export_in_lua: TokenStream2 = match &input.data { + syn::Data::Struct(input) => match &input.fields { + syn::Fields::Named(named_fields) => named_fields + .named + .iter() + .map(|field| -> TokenStream2 { + let input_type = match &field.ty { + syn::Type::BareFn(bare_fn) => { + if bare_fn.inputs.len() == 1 { + bare_fn.inputs.last().expect("The last element exists").ty.clone() + } else if bare_fn.inputs.len() == 0 { + let input_type: Type = parse_quote! {()}; + input_type + } else { + panic!("The Function can only take on argument, or none"); + } + } + _ => unimplemented!("This is only implemented for bare function types"), + }; + let return_type = match &field.ty { + syn::Type::BareFn(function) => { + let return_path: &ReturnType = &function.output; + match return_path { + ReturnType::Default => None, + ReturnType::Type(_, return_type) => Some(return_type.to_owned()) } + } + _ => unimplemented!("This is only implemented for bare function types"), + }; + + let function_name = field + .ident + .as_ref() + .expect("These are only the named field, thus they all should have a name."); + + if let Some(ret_type) = return_type { + functions_to_generate.push(quote! { + #[ci_command] + async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<#ret_type, mlua::Error> { + } + }); + } else { + functions_to_generate.push(quote! { + #[ci_command] + async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<(), mlua::Error> { + } + }); + } + generate_ci_function_exposure(field) + }) + .collect(), + + _ => unimplemented!("Only implemented for named fileds"), + }, + _ => unimplemented!("Only implemented for structs"), + }; + + let functions_to_generate: TokenStream2 = functions_to_generate.into_iter().collect(); + let gen = quote! { + pub fn generate_ci_functions( + lua: &mut mlua::Lua, + tx: tokio::sync::mpsc::Sender + ) + { + lua.set_app_data(tx); + let globals = lua.globals(); + #functions_to_export_in_lua + } + #functions_to_generate + }; + gen.into() +} diff --git a/lua_macros/src/struct_to_ci_enum/generate_help_function.rs b/lua_macros/src/struct_to_ci_enum/generate_help_function.rs new file mode 100644 index 0000000..2987272 --- /dev/null +++ b/lua_macros/src/struct_to_ci_enum/generate_help_function.rs @@ -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 = field + .attrs + .iter() + .filter_map(|attr| { + if attr.path().is_ident("doc") { + let help_text = attr + .meta + .require_name_value() + .expect("This is a named value type, because all doc comments work this way") + .value + .clone(); + Some(help_text.into_token_stream().into()) + } else { + return None; + } + }) + .collect(); + if attrs_with_doc.len() == 0 { + // TODO there should be a better panic function, than the generic one + panic!( + "The command named: `{}`, does not provide a help message", + field.ident.as_ref().expect("These are all named") + ); + } else { + let help_text_for_one_command_combined: TokenStream2 = attrs_with_doc.into_iter().collect(); + return help_text_for_one_command_combined; + } + }) + .collect(); + + quote! { + #[ci_command] + async fn help( + lua: &mlua::Lua, + input_str: Option + ) -> Result { + // TODO add a way to filter the help based on the input + + let output = "These functions exist:\n"; + output.push_str(#combined_help_text); + Ok(output) + } + } +} diff --git a/lua_macros/src/struct_to_ci_enum/mod.rs b/lua_macros/src/struct_to_ci_enum/mod.rs new file mode 100644 index 0000000..4d93f98 --- /dev/null +++ b/lua_macros/src/struct_to_ci_enum/mod.rs @@ -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::*; diff --git a/src/app/command.rs b/src/app/command.rs deleted file mode 100644 index 6342bf9..0000000 --- a/src/app/command.rs +++ /dev/null @@ -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, command: Command) -> Result<()> { - let event = Event::CommandEvent(command); - channel.send(event).await?; - Ok(()) -} diff --git a/src/app/command_interface.rs b/src/app/command_interface.rs index e38e91e..6f74395 100644 --- a/src/app/command_interface.rs +++ b/src/app/command_interface.rs @@ -1,13 +1,49 @@ -use lua_macros::generate_ci_functions; -use rlua::Context; +// FIXME: This file needs documentation with examples of how the proc macros work. +// 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. -// 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>, -} +use std::{io::{Error, ErrorKind}, sync::Arc}; -fn greet(context: Context, name: String) -> Result { - Ok(format!("Name is {}", name)) +use lua_macros::{ci_command, turn_struct_to_ci_command_enum}; + +use crate::app::event_types::Event; +/// This struct is here to guarantee, that all functions actually end up in the lua context. +/// I.e. Rust should throw a compile error, when one field is added, but not a matching function. +/// +/// What it does: +/// - Generates a `generate_ci_functions` function, which wraps the specified rust in functions +/// in lua and exports them to the globals in the context provided as argument. +/// - Generates a Commands enum, which contains every Camel cased version of the fields. +/// +/// Every command specified here should have a function named $command_name, where $command_name is the snake cased name of the field. +/// +/// This function is exported to the lua context, thus it's signature must be: +/// ```rust +/// fn $command_name(context: Context, input_string: String) -> Result<$return_type, rlua::Error> {} +/// ``` +/// where $return_type is the type returned by the function (the only supported ones are right now +/// `String` and `()`). + +#[turn_struct_to_ci_command_enum] +struct Commands { + /// Greets the user + greet: fn(String) -> String, + + /// Closes the application + //#[expose(lua)] + exit: fn(), + + /// Shows the command line + command_line_show: fn(), + + /// Hides the command line + command_line_hide: fn(), + + /// Go to the next plane + cycle_planes: fn(), + /// Go to the previous plane + cycle_planes_rev: fn(), + + /// Send a message to the current room + /// The send message is interpreted literally. + room_message_send: fn(String) -> String, } diff --git a/src/app/events/event_types/event/handlers/command.rs b/src/app/events/event_types/event/handlers/command.rs index 0c67ed2..f058bd6 100644 --- a/src/app/events/event_types/event/handlers/command.rs +++ b/src/app/events/event_types/event/handlers/command.rs @@ -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 cli_log::info; -pub async fn handle(app: &mut App<'_>, command: &Command) -> Result { +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); Ok(match command { - Command::Exit => EventStatus::Terminate, + Command::Exit => ( + EventStatus::Terminate, + "Terminated the application".to_owned(), + ), Command::CommandLineShow => { app.ui.cli_enable(); - EventStatus::Ok + set_status_output!("CLI online"); + (EventStatus::Ok, "".to_owned()) } Command::CommandLineHide => { app.ui.cli_disable(); - EventStatus::Ok + set_status_output!("CLI offline"); + (EventStatus::Ok, "".to_owned()) } Command::CyclePlanes => { app.ui.cycle_main_input_position(); - EventStatus::Ok + set_status_output!("Switched main input position"); + (EventStatus::Ok, "".to_owned()) } Command::CyclePlanesRev => { app.ui.cycle_main_input_position_rev(); - EventStatus::Ok + set_status_output!("Switched main input position; reversed"); + (EventStatus::Ok, "".to_owned()) } Command::RoomMessageSend(msg) => { if let Some(room) = app.status.room_mut() { 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!(), }) } diff --git a/src/app/events/event_types/event/handlers/lua_command.rs b/src/app/events/event_types/event/handlers/lua_command.rs new file mode 100644 index 0000000..e22833d --- /dev/null +++ b/src/app/events/event_types/event/handlers/lua_command.rs @@ -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 { + 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::() + .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) +} diff --git a/src/app/events/event_types/event/handlers/main.rs b/src/app/events/event_types/event/handlers/main.rs index 8e3bdd1..5e076bc 100644 --- a/src/app/events/event_types/event/handlers/main.rs +++ b/src/app/events/event_types/event/handlers/main.rs @@ -1,9 +1,13 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers}; use crate::{ - app::{command, command::Command, events::event_types::EventStatus, App}, - ui, + app::{ + command_interface::Command, + events::event_types::{Event, EventStatus}, + App, + }, + ui::central, }; pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { @@ -11,39 +15,48 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { - command::execute(app.channel_tx(), Command::Exit).await?; + app.tx + .send(Event::CommandEvent(Command::Exit, None)) + .await?; } CrosstermEvent::Key(KeyEvent { code: KeyCode::Tab, .. }) => { - command::execute(app.channel_tx(), Command::CyclePlanes).await?; + app.tx + .send(Event::CommandEvent(Command::CyclePlanes, None)) + .await?; } CrosstermEvent::Key(KeyEvent { code: KeyCode::BackTab, .. }) => { - command::execute(app.channel_tx(), Command::CyclePlanesRev).await?; + app.tx + .send(Event::CommandEvent(Command::CyclePlanesRev, None)) + .await?; } CrosstermEvent::Key(KeyEvent { code: KeyCode::Char('c'), 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() { - ui::MainInputPosition::MessageCompose => { + central::InputPosition::MessageCompose => { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::ALT, .. }) => { - command::execute( - app.channel_tx(), - Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")), - ) - .await?; + app.tx + .send(Event::CommandEvent( + Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")), + None, + )) + .await?; app.ui.message_compose_clear(); } _ => { @@ -53,7 +66,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { + central::InputPosition::Rooms => { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Up, .. @@ -91,7 +104,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result (), }; } - ui::MainInputPosition::Messages => { + central::InputPosition::Messages => { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Up, .. @@ -136,14 +149,14 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result (), }; } - ui::MainInputPosition::CLI => { + central::InputPosition::CLI => { if let Some(_) = app.ui.cli { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, .. }) => { - let cli_event = app.ui + let ci_event = app.ui .cli .as_mut() .expect("This is already checked") @@ -153,16 +166,10 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { app.ui diff --git a/src/app/events/event_types/event/handlers/mod.rs b/src/app/events/event_types/event/handlers/mod.rs index d3fe26c..98b60ad 100644 --- a/src/app/events/event_types/event/handlers/mod.rs +++ b/src/app/events/event_types/event/handlers/mod.rs @@ -1,4 +1,11 @@ -pub mod command; -pub mod main; -pub mod matrix; +// input events pub mod setup; +pub mod main; + +// matrix +pub mod matrix; + +// ci +pub mod ci_output; +pub mod command; +pub mod lua_command; diff --git a/src/app/events/event_types/event/handlers/setup.rs b/src/app/events/event_types/event/handlers/setup.rs index 38cafb2..8e22128 100644 --- a/src/app/events/event_types/event/handlers/setup.rs +++ b/src/app/events/event_types/event/handlers/setup.rs @@ -1,10 +1,7 @@ use anyhow::{bail, Context, Result}; use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent}; -use crate::{ - app::{events::event_types::EventStatus, App}, - ui, -}; +use crate::{app::{events::event_types::EventStatus, App}, ui::setup}; pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { let ui = match &mut app.ui.setup_ui { @@ -32,7 +29,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { match ui.input_position() { - ui::SetupInputPosition::Ok => { + setup::InputPosition::Ok => { let homeserver = ui.homeserver.lines()[0].clone(); let username = ui.username.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 match ui.input_position() { - ui::SetupInputPosition::Homeserver => { + setup::InputPosition::Homeserver => { ui.homeserver.input(input.to_owned()); } - ui::SetupInputPosition::Username => { + setup::InputPosition::Username => { ui.username.input(input.to_owned()); } - ui::SetupInputPosition::Password => { + setup::InputPosition::Password => { let textarea_input = tui_textarea::Input::from(input.to_owned()); ui.password_data.input(textarea_input.clone()); match textarea_input.key { diff --git a/src/app/events/event_types/event/mod.rs b/src/app/events/event_types/event/mod.rs index ffdad06..65cc9ec 100644 --- a/src/app/events/event_types/event/mod.rs +++ b/src/app/events/event_types/event/mod.rs @@ -1,11 +1,13 @@ mod handlers; use anyhow::{Context, Result}; +use cli_log::{info, trace}; 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; @@ -13,22 +15,39 @@ use super::EventStatus; pub enum Event { InputEvent(CrosstermEvent), MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse), - CommandEvent(Command), + CommandEvent(Command, Option>), + LuaCommand(String), } impl Event { pub async fn handle(&self, app: &mut App<'_>) -> Result { + trace!("Recieved event to handle: `{:#?}`", &self); match &self { Event::MatrixEvent(event) => matrix::handle(app, event) .await .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 - .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() { - 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) .await .with_context(|| format!("Failed to handle input event: `{:#?}`", event)), diff --git a/src/app/mod.rs b/src/app/mod.rs index 10652c9..e43f46a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,45 +1,46 @@ -pub mod command; pub mod command_interface; pub mod events; pub mod status; -use std::path::Path; +use std::{path::Path, sync::Arc}; -use accounts::{Account, AccountsManager}; use anyhow::{Context, Error, Result}; use cli_log::info; use matrix_sdk::Client; -use rlua::Lua; +use mlua::Lua; use status::{State, Status}; use tokio::sync::mpsc; 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> { - ui: ui::UI<'ui>, - accounts_manager: accounts::AccountsManager, + ui: central::UI<'ui>, + accounts_manager: AccountsManager, status: Status, - channel_tx: mpsc::Sender, - channel_rx: mpsc::Receiver, + tx: mpsc::Sender, + rx: mpsc::Receiver, + input_listener_killer: CancellationToken, matrix_listener_killer: CancellationToken, - lua: Lua, + lua: Arc, } impl App<'_> { pub fn new() -> Result { - fn set_up_lua() -> Lua { - let lua = Lua::new(); + fn set_up_lua(tx: mpsc::Sender) -> Arc { + let mut lua = Lua::new(); - lua.context(|mut lua_context| { - generate_ci_functions(&mut lua_context); - }); - lua + generate_ci_functions(&mut lua, tx); + Arc::new(lua) } let path: &std::path::Path = Path::new("userdata/accounts.json"); @@ -50,41 +51,25 @@ impl App<'_> { None }; - let (channel_tx, channel_rx) = mpsc::channel(256); - + let (tx, rx) = mpsc::channel(256); Ok(Self { - ui: ui::UI::new()?, + ui: central::UI::new()?, accounts_manager: AccountsManager::new(config)?, status: Status::new(None), - channel_tx, - channel_rx, + tx: tx.clone(), + rx, input_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 { - info!("Recieved ci event: `{event}`; executing.."); - - // TODO: Should the ci support more than strings? - let output = self.lua.context(|context| -> Result { - let output = context - .load(&event) - .eval::() - .with_context(|| format!("Failed to execute: `{event}`"))?; - info!("Function evaluated to: `{output}`"); - Ok(output) - })?; - Ok(output) - } - pub async fn run(&mut self) -> Result<()> { // Spawn input event listener tokio::task::spawn(events::poll_input_events( - self.channel_tx.clone(), + self.tx.clone(), self.input_listener_killer.clone(), )); @@ -100,10 +85,7 @@ impl App<'_> { self.status.set_state(State::Main); self.ui.update(&self.status).await?; - let event: event_types::Event = match self.channel_rx.recv().await { - Some(e) => e, - None => return Err(Error::msg("Event channel has no senders")), - }; + let event = self.rx.recv().await.context("Failed to get next event")?; match event.handle(self).await? { event_types::EventStatus::Ok => (), @@ -117,16 +99,13 @@ impl App<'_> { } async fn setup(&mut self) -> Result<()> { - self.ui.setup_ui = Some(ui::SetupUI::new()); + self.ui.setup_ui = Some(setup::UI::new()); loop { self.status.set_state(State::Setup); self.ui.update_setup().await?; - let event: event_types::Event = match self.channel_rx.recv().await { - Some(e) => e, - None => return Err(Error::msg("Event channel has no senders")), - }; + let event = self.rx.recv().await.context("Failed to get next event")?; match event.handle(self).await? { event_types::EventStatus::Ok => (), @@ -150,7 +129,7 @@ impl App<'_> { // Spawn Matrix Event Listener tokio::task::spawn(events::poll_matrix_events( - self.channel_tx.clone(), + self.tx.clone(), self.matrix_listener_killer.clone(), client.clone(), )); @@ -204,8 +183,4 @@ impl App<'_> { pub fn client(&self) -> Option<&Client> { self.accounts_manager.client() } - - pub fn channel_tx(&self) -> &mpsc::Sender { - &self.channel_tx - } } diff --git a/src/app/status.rs b/src/app/status.rs index 430f8a8..2a858f9 100644 --- a/src/app/status.rs +++ b/src/app/status.rs @@ -1,5 +1,5 @@ use anyhow::{Error, Result}; -use cli_log::{warn, info}; +use cli_log::warn; use indexmap::IndexMap; use matrix_sdk::{ room::MessagesOptions, diff --git a/src/ui/central/mod.rs b/src/ui/central/mod.rs new file mode 100644 index 0000000..882584d --- /dev/null +++ b/src/ui/central/mod.rs @@ -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>, + input_position: InputPosition, + pub rooms_state: ListState, + pub message_compose: TextArea<'a>, + pub cli: Option>, + + pub setup_ui: Option>, +} + +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 { + 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: +)") + .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: +)") + .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(()) + } +} diff --git a/src/ui/central/update/mod.rs b/src/ui/central/update/mod.rs new file mode 100644 index 0000000..412cb8c --- /dev/null +++ b/src/ui/central/update/mod.rs @@ -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(()) + } +} diff --git a/src/ui/central/update/widgets/messages.rs b/src/ui/central/update/widgets/messages.rs new file mode 100644 index 0000000..9a4f64d --- /dev/null +++ b/src/ui/central/update/widgets/messages.rs @@ -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) -> 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> { + let results: Vec> = 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> { + 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), + ), + ])])), + } +} diff --git a/src/ui/central/update/widgets/mod.rs b/src/ui/central/update/widgets/mod.rs new file mode 100644 index 0000000..850c27f --- /dev/null +++ b/src/ui/central/update/widgets/mod.rs @@ -0,0 +1,4 @@ +pub mod messages; +pub mod room_info; +pub mod rooms; +pub mod status; diff --git a/src/ui/central/update/widgets/room_info.rs b/src/ui/central/update/widgets/room_info.rs new file mode 100644 index 0000000..3563c55 --- /dev/null +++ b/src/ui/central/update/widgets/room_info.rs @@ -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) -> 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) +} diff --git a/src/ui/central/update/widgets/rooms.rs b/src/ui/central/update/widgets/rooms.rs new file mode 100644 index 0000000..f981e96 --- /dev/null +++ b/src/ui/central/update/widgets/rooms.rs @@ -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) -> 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(">") +} diff --git a/src/ui/central/update/widgets/status.rs b/src/ui/central/update/widgets/status.rs new file mode 100644 index 0000000..0d87034 --- /dev/null +++ b/src/ui/central/update/widgets/status.rs @@ -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) -> 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) +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f4d627f..28ebea2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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 crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, + event::EnableMouseCapture, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{enable_raw_mode, EnterAlternateScreen}, }; -use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent}; use tui::{ - backend::CrosstermBackend, - layout::{Alignment, Constraint, Corner, Direction, Layout}, style::{Color, Modifier, Style}, - text::{Span, Spans, Text}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, - Terminal, + widgets::{Block, Borders}, }; 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>, - input_position: MainInputPosition, - pub rooms_state: ListState, - pub message_compose: TextArea<'a>, - pub cli: Option>, - - pub setup_ui: Option>, -} - fn terminal_prepare() -> Result { - enable_raw_mode()?; + enable_raw_mode().context("Failed to enable raw mode")?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; info!("Prepared terminal"); @@ -84,553 +43,3 @@ pub fn textarea_inactivate(textarea: &mut TextArea) { .unwrap_or_else(|| Block::default().borders(Borders::ALL)); 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>, - ) -> Result<()> { - let mut strings: Vec = 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 { - 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: +)") - .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: +)") - .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::>(); - - 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::>() - } - 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(()) - } -} diff --git a/src/ui/setup.rs b/src/ui/setup.rs new file mode 100644 index 0000000..da339b2 --- /dev/null +++ b/src/ui/setup.rs @@ -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>, + ) -> Result<()> { + let strings: Vec = 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(()) + } +}