Merge: PR-11 (commands) -> master

This commit is contained in:
antifallobst 2023-07-21 18:57:03 +00:00
commit dcf87f257d
30 changed files with 1556 additions and 924 deletions

247
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"] }

View File

@ -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": {

View File

@ -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

View File

@ -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"] }

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_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>) -> 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()
}

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;
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<String, rlua::Error> {
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,
}

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 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);
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!(),
})
}

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 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<EventStatus> {
@ -11,39 +15,48 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => {
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<E
}
};
}
ui::MainInputPosition::Rooms => {
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<E
_ => (),
};
}
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<E
_ => (),
};
}
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<E
"There can only be one line in the buffer, as we collect it on enter being inputted"
)
.to_owned();
let output = app.handle_ci_event(&cli_event).await?;
// delete the old text:
// 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.tx
.send(Event::LuaCommand(ci_event))
.await
.context("Failed to send lua command to internal event stream")?;
}
_ => {
app.ui

View File

@ -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;

View File

@ -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<EventStatus> {
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() {
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<E
};
}
input => 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 {

View File

@ -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<Sender<String>>),
LuaCommand(String),
}
impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
trace!("Recieved event to handle: `{:#?}`", &self);
match &self {
Event::MatrixEvent(event) => matrix::handle(app, event)
.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)),

View File

@ -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<event_types::Event>,
channel_rx: mpsc::Receiver<event_types::Event>,
tx: mpsc::Sender<Event>,
rx: mpsc::Receiver<Event>,
input_listener_killer: CancellationToken,
matrix_listener_killer: CancellationToken,
lua: Lua,
lua: Arc<Lua>,
}
impl App<'_> {
pub fn new() -> Result<Self> {
fn set_up_lua() -> Lua {
let lua = Lua::new();
fn set_up_lua(tx: mpsc::Sender<Event>) -> Arc<Lua> {
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<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<()> {
// 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<Event> {
&self.channel_tx
}
}

View File

@ -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,

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 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<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> {
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<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(())
}
}