Delete lots of stuff
This commit is contained in:
parent
742e7725ab
commit
00c905eff5
22 changed files with 46 additions and 2076 deletions
241
Cargo.lock
generated
241
Cargo.lock
generated
|
|
@ -17,15 +17,6 @@ version = "1.0.57"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "approx"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
|
|
@ -91,12 +82,6 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
|
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cassowary"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.73"
|
version = "1.0.73"
|
||||||
|
|
@ -204,16 +189,11 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"cove-core",
|
"crossterm",
|
||||||
"crossterm 0.22.1",
|
|
||||||
"futures",
|
"futures",
|
||||||
"palette",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
|
||||||
"toss",
|
"toss",
|
||||||
"tui",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -225,23 +205,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm"
|
|
||||||
version = "0.22.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"crossterm_winapi",
|
|
||||||
"futures-core",
|
|
||||||
"libc",
|
|
||||||
"mio 0.7.14",
|
|
||||||
"parking_lot 0.11.2",
|
|
||||||
"signal-hook",
|
|
||||||
"signal-hook-mio",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.23.2"
|
version = "0.23.2"
|
||||||
|
|
@ -250,9 +213,10 @@ checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
|
"futures-core",
|
||||||
"libc",
|
"libc",
|
||||||
"mio 0.8.3",
|
"mio",
|
||||||
"parking_lot 0.12.0",
|
"parking_lot",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-mio",
|
"signal-hook-mio",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
|
@ -309,15 +273,6 @@ dependencies = [
|
||||||
"termcolor",
|
"termcolor",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "find-crate"
|
|
||||||
version = "0.6.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
|
|
||||||
dependencies = [
|
|
||||||
"toml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
|
@ -518,15 +473,6 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "instant"
|
|
||||||
version = "0.1.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -585,19 +531,6 @@ version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mio"
|
|
||||||
version = "0.7.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"miow",
|
|
||||||
"ntapi",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
@ -610,33 +543,6 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miow"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ntapi"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
|
|
@ -671,41 +577,6 @@ version = "6.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
|
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "palette"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f9735f7e1e51a3f740bacd5dc2724b61a7806f23597a8736e679f38ee3435d18"
|
|
||||||
dependencies = [
|
|
||||||
"approx",
|
|
||||||
"num-traits",
|
|
||||||
"palette_derive",
|
|
||||||
"phf",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "palette_derive"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7799c3053ea8a6d8a1193c7ba42f534e7863cf52e378a7f90406f4a645d33bad"
|
|
||||||
dependencies = [
|
|
||||||
"find-crate",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.11.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
|
||||||
dependencies = [
|
|
||||||
"instant",
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core 0.8.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|
@ -713,21 +584,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
|
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"parking_lot_core 0.9.3",
|
"parking_lot_core",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.8.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"instant",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"winapi",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -749,50 +606,6 @@ version = "2.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37"
|
|
||||||
dependencies = [
|
|
||||||
"phf_macros",
|
|
||||||
"phf_shared",
|
|
||||||
"proc-macro-hack",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf_generator"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082"
|
|
||||||
dependencies = [
|
|
||||||
"phf_shared",
|
|
||||||
"rand",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf_macros"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86"
|
|
||||||
dependencies = [
|
|
||||||
"phf_generator",
|
|
||||||
"phf_shared",
|
|
||||||
"proc-macro-hack",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf_shared"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9"
|
|
||||||
dependencies = [
|
|
||||||
"siphasher",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
|
@ -835,12 +648,6 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-hack"
|
|
||||||
version = "0.5.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.39"
|
version = "1.0.39"
|
||||||
|
|
@ -1090,8 +897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio 0.7.14",
|
"mio",
|
||||||
"mio 0.8.3",
|
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1104,12 +910,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "siphasher"
|
|
||||||
version = "0.3.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
|
@ -1214,10 +1014,10 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio 0.8.3",
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot 0.12.0",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
|
@ -1274,38 +1074,17 @@ dependencies = [
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml"
|
|
||||||
version = "0.5.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toss"
|
name = "toss"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/Garmelon/toss.git?rev=8a6b0f83edfa39617256725c263b01906eac037d#8a6b0f83edfa39617256725c263b01906eac037d"
|
source = "git+https://github.com/Garmelon/toss.git?rev=33264b4aec27066e6abb7cc7d15bd680b43fcd5a#33264b4aec27066e6abb7cc7d15bd680b43fcd5a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm 0.23.2",
|
"crossterm",
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tui"
|
|
||||||
version = "0.17.0"
|
|
||||||
source = "git+https://github.com/Garmelon/tui-rs.git?rev=07952dc#07952dc5b98885347cc224ac3ea91d8e6329bb1a"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cassowary",
|
|
||||||
"crossterm 0.22.1",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,10 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.53"
|
anyhow = "1.0.57"
|
||||||
clap = { version = "3.1.0", features = ["derive"] }
|
clap = { version = "3.1.18", features = ["derive"] }
|
||||||
cove-core = { path = "../cove-core" }
|
crossterm = { version = "0.23.2", features = ["event-stream"] }
|
||||||
crossterm = { version = "0.22.1", features = ["event-stream"] }
|
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
palette = "0.6.0"
|
thiserror = "1.0.31"
|
||||||
# serde_json = "1.0.78"
|
tokio = { version = "1.18.2", features = ["full"] }
|
||||||
thiserror = "1.0.30"
|
toss = { git = "https://github.com/Garmelon/toss.git", rev = "33264b4aec27066e6abb7cc7d15bd680b43fcd5a" }
|
||||||
tokio = { version = "1.16.1", features = ["full"] }
|
|
||||||
# tokio-stream = "0.1.8"
|
|
||||||
tokio-tungstenite = { version = "0.16.1", features = [
|
|
||||||
"rustls-tls-native-roots",
|
|
||||||
] }
|
|
||||||
toss = { git = "https://github.com/Garmelon/toss.git", rev = "8a6b0f83edfa39617256725c263b01906eac037d" }
|
|
||||||
tui = { git = "https://github.com/Garmelon/tui-rs.git", rev = "07952dc" }
|
|
||||||
unicode-width = "0.1.9"
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
pub mod cove;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod conn;
|
|
||||||
pub mod room;
|
|
||||||
|
|
@ -1,392 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use cove_core::conn::{self, ConnMaintenance, ConnRx, ConnTx};
|
|
||||||
use cove_core::packets::{
|
|
||||||
Cmd, IdentifyCmd, IdentifyRpl, JoinNtf, NickNtf, NickRpl, Ntf, Packet, PartNtf, RoomCmd,
|
|
||||||
RoomRpl, Rpl, SendNtf, SendRpl, WhoRpl,
|
|
||||||
};
|
|
||||||
use cove_core::replies::Replies;
|
|
||||||
use cove_core::{replies, Session, SessionId};
|
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
|
||||||
use tokio::sync::{Mutex, MutexGuard};
|
|
||||||
|
|
||||||
// TODO Split into "interacting" and "maintenance" parts?
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("{0}")]
|
|
||||||
CouldNotConnect(conn::Error),
|
|
||||||
#[error("{0}")]
|
|
||||||
Conn(#[from] conn::Error),
|
|
||||||
#[error("{0}")]
|
|
||||||
Reply(#[from] replies::Error),
|
|
||||||
#[error("invalid room: {0}")]
|
|
||||||
InvalidRoom(String),
|
|
||||||
#[error("invalid identity: {0}")]
|
|
||||||
InvalidIdentity(String),
|
|
||||||
#[error("maintenance aborted")]
|
|
||||||
MaintenanceAborted,
|
|
||||||
#[error("not connected")]
|
|
||||||
NotConnected,
|
|
||||||
#[error("incorrect reply type")]
|
|
||||||
IncorrectReplyType,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Event {
|
|
||||||
StateChanged,
|
|
||||||
IdentificationRequired,
|
|
||||||
// TODO Add events for joining, parting, sending, ...
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Present {
|
|
||||||
pub session: Session,
|
|
||||||
pub others: HashMap<SessionId, Session>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Present {
|
|
||||||
fn session_map(sessions: &[Session]) -> HashMap<SessionId, Session> {
|
|
||||||
sessions
|
|
||||||
.iter()
|
|
||||||
.map(|session| (session.id, session.clone()))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new(session: &Session, others: &[Session]) -> Self {
|
|
||||||
Self {
|
|
||||||
session: session.clone(),
|
|
||||||
others: Self::session_map(others),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, session: &Session, others: &[Session]) {
|
|
||||||
self.session = session.clone();
|
|
||||||
self.others = Self::session_map(others);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_session(&mut self, session: &Session) {
|
|
||||||
self.session = session.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn join(&mut self, who: Session) {
|
|
||||||
self.others.insert(who.id, who);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nick(&mut self, who: Session) {
|
|
||||||
self.others.insert(who.id, who);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn part(&mut self, who: Session) {
|
|
||||||
self.others.remove(&who.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Status {
|
|
||||||
ChoosingRoom,
|
|
||||||
Identifying,
|
|
||||||
IdRequired(Option<String>),
|
|
||||||
Present(Present),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Status {
|
|
||||||
fn present(&self) -> Option<&Present> {
|
|
||||||
match self {
|
|
||||||
Status::Present(present) => Some(present),
|
|
||||||
Status::ChoosingRoom | Status::Identifying | Status::IdRequired(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn present_mut(&mut self) -> Option<&mut Present> {
|
|
||||||
match self {
|
|
||||||
Status::Present(present) => Some(present),
|
|
||||||
Status::ChoosingRoom | Status::Identifying | Status::IdRequired(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Connected {
|
|
||||||
tx: ConnTx,
|
|
||||||
next_id: u64,
|
|
||||||
replies: Replies<u64, Rpl>,
|
|
||||||
status: Status,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connected {
|
|
||||||
fn new(tx: ConnTx, timeout: Duration) -> Self {
|
|
||||||
Self {
|
|
||||||
tx,
|
|
||||||
next_id: 0,
|
|
||||||
replies: Replies::new(timeout),
|
|
||||||
status: Status::ChoosingRoom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn status(&self) -> &Status {
|
|
||||||
&self.status
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn present(&self) -> Option<&Present> {
|
|
||||||
self.status.present()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The warning about enum variant sizes shouldn't matter since a connection will
|
|
||||||
// spend most its time in the Connected state anyways.
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
pub enum State {
|
|
||||||
Connecting,
|
|
||||||
Connected(Connected),
|
|
||||||
// TODO Include reason for stop
|
|
||||||
Stopped,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
pub fn connected(&self) -> Option<&Connected> {
|
|
||||||
match self {
|
|
||||||
Self::Connected(connected) => Some(connected),
|
|
||||||
Self::Connecting | Self::Stopped => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connected_mut(&mut self) -> Option<&mut Connected> {
|
|
||||||
match self {
|
|
||||||
Self::Connected(connected) => Some(connected),
|
|
||||||
Self::Connecting | Self::Stopped => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn present(&self) -> Option<&Present> {
|
|
||||||
self.connected()?.present()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct CoveConn {
|
|
||||||
state: Arc<Mutex<State>>,
|
|
||||||
ev_tx: UnboundedSender<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoveConn {
|
|
||||||
// TODO Disallow modification via this MutexGuard
|
|
||||||
pub async fn state(&self) -> MutexGuard<'_, State> {
|
|
||||||
self.state.lock().await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn cmd<C, R>(&self, cmd: C) -> Result<R, Error>
|
|
||||||
where
|
|
||||||
C: Into<Cmd>,
|
|
||||||
Rpl: TryInto<R>,
|
|
||||||
{
|
|
||||||
let pending_reply = {
|
|
||||||
let mut state = self.state.lock().await;
|
|
||||||
let mut connected = state.connected_mut().ok_or(Error::NotConnected)?;
|
|
||||||
|
|
||||||
let id = connected.next_id;
|
|
||||||
connected.next_id += 1;
|
|
||||||
|
|
||||||
let pending_reply = connected.replies.wait_for(id);
|
|
||||||
connected.tx.send(&Packet::cmd(id, cmd.into()))?;
|
|
||||||
pending_reply
|
|
||||||
};
|
|
||||||
|
|
||||||
let rpl = pending_reply.get().await?;
|
|
||||||
let rpl_value = rpl.try_into().map_err(|_| Error::IncorrectReplyType)?;
|
|
||||||
Ok(rpl_value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to identify with a nick and identity. Does nothing if the room
|
|
||||||
/// doesn't require verification.
|
|
||||||
///
|
|
||||||
/// This method is intended to be called whenever a CoveConn user suspects
|
|
||||||
/// identification to be necessary. It has little overhead.
|
|
||||||
pub async fn identify(&self, nick: &str, identity: &str) {
|
|
||||||
{
|
|
||||||
let mut state = self.state.lock().await;
|
|
||||||
if let Some(connected) = state.connected_mut() {
|
|
||||||
if let Status::IdRequired(_) = connected.status {
|
|
||||||
connected.status = Status::Identifying;
|
|
||||||
let _ = self.ev_tx.send(Event::StateChanged);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let conn = self.clone();
|
|
||||||
let nick = nick.to_string();
|
|
||||||
let identity = identity.to_string();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
// There's no need for a second locking block, or for us to see the
|
|
||||||
// result of this command. CoveConnMt::run will set the connection's
|
|
||||||
// status as appropriate.
|
|
||||||
conn.cmd::<IdentifyCmd, IdentifyRpl>(IdentifyCmd { nick, identity })
|
|
||||||
.await
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maintenance for a [`CoveConn`].
|
|
||||||
pub struct CoveConnMt {
|
|
||||||
url: String,
|
|
||||||
room: String,
|
|
||||||
timeout: Duration,
|
|
||||||
conn: CoveConn,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoveConnMt {
|
|
||||||
pub async fn run(self) -> Result<(), Error> {
|
|
||||||
let (tx, rx, mt) = match Self::connect(&self.url, self.timeout).await {
|
|
||||||
Ok(conn) => conn,
|
|
||||||
Err(e) => {
|
|
||||||
*self.conn.state.lock().await = State::Stopped;
|
|
||||||
let _ = self.conn.ev_tx.send(Event::StateChanged);
|
|
||||||
return Err(Error::CouldNotConnect(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
*self.conn.state.lock().await = State::Connected(Connected::new(tx, self.timeout));
|
|
||||||
let _ = self.conn.ev_tx.send(Event::StateChanged);
|
|
||||||
|
|
||||||
tokio::spawn(Self::join_room(self.conn.clone(), self.room));
|
|
||||||
let result = tokio::select! {
|
|
||||||
result = Self::recv(&self.conn, rx) => result,
|
|
||||||
_ = mt.perform() => Err(Error::MaintenanceAborted),
|
|
||||||
};
|
|
||||||
|
|
||||||
*self.conn.state.lock().await = State::Stopped;
|
|
||||||
let _ = self.conn.ev_tx.send(Event::StateChanged);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect(
|
|
||||||
url: &str,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> Result<(ConnTx, ConnRx, ConnMaintenance), conn::Error> {
|
|
||||||
let stream = tokio_tungstenite::connect_async(url).await?.0;
|
|
||||||
let conn = conn::new(stream, timeout);
|
|
||||||
Ok(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn join_room(conn: CoveConn, name: String) -> Result<(), Error> {
|
|
||||||
let _: RoomRpl = conn.cmd(RoomCmd { name }).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recv(conn: &CoveConn, mut rx: ConnRx) -> Result<(), Error> {
|
|
||||||
while let Some(packet) = rx.recv().await? {
|
|
||||||
match packet {
|
|
||||||
Packet::Cmd { .. } => {} // Ignore commands, the server shouldn't send any
|
|
||||||
Packet::Rpl { id, rpl } => Self::on_rpl(conn, id, rpl).await?,
|
|
||||||
Packet::Ntf { ntf } => Self::on_ntf(conn, ntf).await?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_rpl(conn: &CoveConn, id: u64, rpl: Rpl) -> Result<(), Error> {
|
|
||||||
let mut state = conn.state.lock().await;
|
|
||||||
let connected = match state.connected_mut() {
|
|
||||||
Some(connected) => connected,
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
match &rpl {
|
|
||||||
Rpl::Room(RoomRpl::Success) => {
|
|
||||||
connected.status = Status::IdRequired(None);
|
|
||||||
let _ = conn.ev_tx.send(Event::IdentificationRequired);
|
|
||||||
}
|
|
||||||
Rpl::Room(RoomRpl::InvalidRoom { reason }) => {
|
|
||||||
return Err(Error::InvalidRoom(reason.clone()))
|
|
||||||
}
|
|
||||||
Rpl::Identify(IdentifyRpl::Success { you, others, .. }) => {
|
|
||||||
connected.status = Status::Present(Present::new(you, others));
|
|
||||||
let _ = conn.ev_tx.send(Event::StateChanged);
|
|
||||||
}
|
|
||||||
Rpl::Identify(IdentifyRpl::InvalidNick { reason }) => {
|
|
||||||
connected.status = Status::IdRequired(Some(reason.clone()));
|
|
||||||
let _ = conn.ev_tx.send(Event::IdentificationRequired);
|
|
||||||
}
|
|
||||||
Rpl::Identify(IdentifyRpl::InvalidIdentity { reason }) => {
|
|
||||||
return Err(Error::InvalidIdentity(reason.clone()))
|
|
||||||
}
|
|
||||||
Rpl::Nick(NickRpl::Success { you }) => {
|
|
||||||
if let Some(present) = connected.status.present_mut() {
|
|
||||||
present.update_session(you);
|
|
||||||
let _ = conn.ev_tx.send(Event::StateChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Rpl::Nick(NickRpl::InvalidNick { reason: _ }) => {}
|
|
||||||
Rpl::Send(SendRpl::Success { message }) => {
|
|
||||||
// TODO Add message to message store or send an event
|
|
||||||
}
|
|
||||||
Rpl::Send(SendRpl::InvalidContent { reason: _ }) => {}
|
|
||||||
Rpl::Who(WhoRpl { you, others }) => {
|
|
||||||
if let Some(present) = connected.status.present_mut() {
|
|
||||||
present.update(you, others);
|
|
||||||
let _ = conn.ev_tx.send(Event::StateChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connected.replies.complete(&id, rpl);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_ntf(conn: &CoveConn, ntf: Ntf) -> Result<(), Error> {
|
|
||||||
let mut state = conn.state.lock().await;
|
|
||||||
let connected = match state.connected_mut() {
|
|
||||||
Some(connected) => connected,
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
match ntf {
|
|
||||||
Ntf::Join(JoinNtf { who }) => {
|
|
||||||
if let Some(present) = connected.status.present_mut() {
|
|
||||||
present.join(who);
|
|
||||||
let _ = conn.ev_tx.send(Event::StateChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ntf::Nick(NickNtf { who }) => {
|
|
||||||
if let Some(present) = connected.status.present_mut() {
|
|
||||||
present.nick(who);
|
|
||||||
let _ = conn.ev_tx.send(Event::StateChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ntf::Part(PartNtf { who }) => {
|
|
||||||
if let Some(present) = connected.status.present_mut() {
|
|
||||||
present.part(who);
|
|
||||||
let _ = conn.ev_tx.send(Event::StateChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ntf::Send(SendNtf { message }) => {
|
|
||||||
// TODO Add message to message store or send an event
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
url: String,
|
|
||||||
room: String,
|
|
||||||
timeout: Duration,
|
|
||||||
ev_tx: UnboundedSender<Event>,
|
|
||||||
) -> (CoveConn, CoveConnMt) {
|
|
||||||
let conn = CoveConn {
|
|
||||||
state: Arc::new(Mutex::new(State::Connecting)),
|
|
||||||
ev_tx,
|
|
||||||
};
|
|
||||||
let mt = CoveConnMt {
|
|
||||||
url,
|
|
||||||
room,
|
|
||||||
timeout,
|
|
||||||
conn,
|
|
||||||
};
|
|
||||||
(mt.conn.clone(), mt)
|
|
||||||
}
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
|
||||||
use tokio::sync::oneshot::{self, Sender};
|
|
||||||
use tokio::sync::{Mutex, MutexGuard};
|
|
||||||
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::never::Never;
|
|
||||||
|
|
||||||
use super::conn::{self, CoveConn, CoveConnMt, Event};
|
|
||||||
|
|
||||||
struct ConnConfig {
|
|
||||||
url: String,
|
|
||||||
room: String,
|
|
||||||
timeout: Duration,
|
|
||||||
ev_tx: UnboundedSender<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnConfig {
|
|
||||||
fn new_conn(&self) -> (CoveConn, CoveConnMt) {
|
|
||||||
conn::new(
|
|
||||||
self.url.clone(),
|
|
||||||
self.room.clone(),
|
|
||||||
self.timeout,
|
|
||||||
self.ev_tx.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CoveRoom {
|
|
||||||
name: String,
|
|
||||||
conn: Arc<Mutex<CoveConn>>,
|
|
||||||
/// Once this is dropped, all other room-related tasks, connections and
|
|
||||||
/// values are cleaned up. It is never used to send actual values.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
dead_mans_switch: Sender<Never>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoveRoom {
|
|
||||||
/// This method uses [`tokio::spawn`] and must thus be called in the context
|
|
||||||
/// of a tokio runtime.
|
|
||||||
pub fn new<E, F>(
|
|
||||||
config: &'static Config,
|
|
||||||
name: String,
|
|
||||||
event_sender: UnboundedSender<E>,
|
|
||||||
convert_event: F,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
E: Send + 'static,
|
|
||||||
F: Fn(&str, Event) -> E + Send + 'static,
|
|
||||||
{
|
|
||||||
let (ev_tx, ev_rx) = mpsc::unbounded_channel();
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
|
|
||||||
let conf = ConnConfig {
|
|
||||||
ev_tx,
|
|
||||||
url: config.cove_url.to_string(),
|
|
||||||
room: name.clone(),
|
|
||||||
timeout: config.timeout,
|
|
||||||
};
|
|
||||||
let (conn, mt) = conf.new_conn();
|
|
||||||
|
|
||||||
let room = Self {
|
|
||||||
name: name.clone(),
|
|
||||||
conn: Arc::new(Mutex::new(conn)),
|
|
||||||
dead_mans_switch: tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spawned separately because otherwise, the last few elements before a
|
|
||||||
// connection is closed might not get shoveled.
|
|
||||||
tokio::spawn(Self::shovel_events(
|
|
||||||
name,
|
|
||||||
ev_rx,
|
|
||||||
event_sender,
|
|
||||||
convert_event,
|
|
||||||
));
|
|
||||||
|
|
||||||
let conn_clone = room.conn.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::select! {
|
|
||||||
_ = rx => {} // Watch dead man's switch
|
|
||||||
_ = Self::run(conn_clone, mt, conf) => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
room
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn identify(&self, nick: &str, identity: &str) {
|
|
||||||
self.conn().await.identify(nick, identity).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Disallow modification via this MutexGuard
|
|
||||||
pub async fn conn(&self) -> MutexGuard<'_, CoveConn> {
|
|
||||||
self.conn.lock().await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn shovel_events<E>(
|
|
||||||
name: String,
|
|
||||||
mut ev_rx: UnboundedReceiver<Event>,
|
|
||||||
ev_tx: UnboundedSender<E>,
|
|
||||||
convert_event: impl Fn(&str, Event) -> E,
|
|
||||||
) {
|
|
||||||
while let Some(event) = ev_rx.recv().await {
|
|
||||||
let event = convert_event(&name, event);
|
|
||||||
if ev_tx.send(event).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Background task to connect to a room and stay connected.
|
|
||||||
async fn run(conn: Arc<Mutex<CoveConn>>, mut mt: CoveConnMt, conf: ConnConfig) {
|
|
||||||
// We have successfully connected to the url before. Errors while
|
|
||||||
// connecting are probably not our fault and we should try again later.
|
|
||||||
let mut url_exists = false;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match mt.run().await {
|
|
||||||
Err(conn::Error::CouldNotConnect(_)) if url_exists => {
|
|
||||||
// TODO Exponential backoff?
|
|
||||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
|
||||||
}
|
|
||||||
// TODO Note these errors somewhere in the room state
|
|
||||||
Err(conn::Error::CouldNotConnect(_)) => return,
|
|
||||||
Err(conn::Error::InvalidRoom(_)) => return,
|
|
||||||
Err(conn::Error::InvalidIdentity(_)) => return,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
url_exists = true;
|
|
||||||
|
|
||||||
// TODO Clean up with restructuring assignments later?
|
|
||||||
let (new_conn, new_mt) = conf.new_conn();
|
|
||||||
*conn.lock().await = new_conn;
|
|
||||||
mt = new_mt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
pub struct Args {
|
|
||||||
#[clap(long, default_value_t = String::from("wss://plugh.de/cove/"))]
|
|
||||||
cove_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Config {
|
|
||||||
pub cove_url: String,
|
|
||||||
pub cove_identity: String,
|
|
||||||
pub timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn load() -> Self {
|
|
||||||
let args = Args::parse();
|
|
||||||
Self {
|
|
||||||
cove_url: args.cove_url,
|
|
||||||
// TODO Load identity from file oslt
|
|
||||||
cove_identity: format!("{:?}", Instant::now()),
|
|
||||||
timeout: Duration::from_secs(10),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +1,13 @@
|
||||||
// TODO Make as few things async as necessary
|
|
||||||
|
|
||||||
#![warn(clippy::use_self)]
|
#![warn(clippy::use_self)]
|
||||||
|
|
||||||
pub mod client;
|
|
||||||
mod config;
|
|
||||||
mod never;
|
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use std::io;
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use config::Config;
|
|
||||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
|
||||||
use crossterm::execute;
|
|
||||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
|
||||||
use tui::backend::CrosstermBackend;
|
|
||||||
use tui::Terminal;
|
|
||||||
use ui::Ui;
|
use ui::Ui;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let config = Box::leak(Box::new(Config::load()));
|
let mut terminal = Terminal::new()?;
|
||||||
|
Ui::run(&mut terminal).await?;
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
|
|
||||||
|
|
||||||
crossterm::terminal::enable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
EnterAlternateScreen,
|
|
||||||
EnableMouseCapture
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Defer error handling so the terminal always gets restored properly
|
|
||||||
let result = Ui::run(config, &mut terminal).await;
|
|
||||||
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
|
||||||
|
|
||||||
result?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
// TODO Replace with `!` when it is stabilised
|
|
||||||
pub enum Never {}
|
|
||||||
|
|
@ -1,52 +1,17 @@
|
||||||
mod cove;
|
|
||||||
mod input;
|
|
||||||
mod layout;
|
|
||||||
mod overlays;
|
|
||||||
mod pane;
|
|
||||||
mod rooms;
|
|
||||||
mod styles;
|
|
||||||
mod textline;
|
|
||||||
|
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::Stdout;
|
|
||||||
|
|
||||||
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
|
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, MouseEvent};
|
||||||
|
use crossterm::style::ContentStyle;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use tokio::sync::mpsc::error::TryRecvError;
|
use tokio::sync::mpsc::error::TryRecvError;
|
||||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||||
use tui::backend::CrosstermBackend;
|
use toss::frame::{Frame, Pos};
|
||||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
use toss::terminal::{Redraw, Terminal};
|
||||||
use tui::{Frame, Terminal};
|
|
||||||
|
|
||||||
use crate::client::cove::conn::Event as CoveEvent;
|
|
||||||
use crate::client::cove::room::CoveRoom;
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::ui::overlays::OverlayReaction;
|
|
||||||
|
|
||||||
use self::cove::CoveUi;
|
|
||||||
use self::input::EventHandler;
|
|
||||||
use self::overlays::{Overlay, SwitchRoom, SwitchRoomState};
|
|
||||||
use self::pane::PaneInfo;
|
|
||||||
use self::rooms::Rooms;
|
|
||||||
|
|
||||||
pub type Backend = CrosstermBackend<Stdout>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum RoomId {
|
|
||||||
Cove(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UiEvent {
|
pub enum UiEvent {
|
||||||
|
Redraw,
|
||||||
Term(Event),
|
Term(Event),
|
||||||
Cove(String, CoveEvent),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiEvent {
|
|
||||||
fn cove(room: &str, event: CoveEvent) -> Self {
|
|
||||||
Self::Cove(room.to_string(), event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EventHandleResult {
|
enum EventHandleResult {
|
||||||
|
|
@ -55,49 +20,23 @@ enum EventHandleResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Ui {
|
pub struct Ui {
|
||||||
config: &'static Config,
|
|
||||||
event_tx: UnboundedSender<UiEvent>,
|
event_tx: UnboundedSender<UiEvent>,
|
||||||
|
|
||||||
cove_rooms: HashMap<String, CoveUi>,
|
|
||||||
room: Option<RoomId>,
|
|
||||||
|
|
||||||
rooms_pane: PaneInfo,
|
|
||||||
users_pane: PaneInfo,
|
|
||||||
|
|
||||||
overlay: Option<Overlay>,
|
|
||||||
|
|
||||||
last_area: Rect,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ui {
|
impl Ui {
|
||||||
fn new(config: &'static Config, event_tx: UnboundedSender<UiEvent>) -> Self {
|
fn new(event_tx: UnboundedSender<UiEvent>) -> Self {
|
||||||
Self {
|
Self { event_tx }
|
||||||
config,
|
|
||||||
event_tx,
|
|
||||||
|
|
||||||
cove_rooms: HashMap::new(),
|
|
||||||
room: None,
|
|
||||||
|
|
||||||
rooms_pane: PaneInfo::default(),
|
|
||||||
users_pane: PaneInfo::default(),
|
|
||||||
|
|
||||||
overlay: None,
|
|
||||||
|
|
||||||
last_area: Rect::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(terminal: &mut Terminal) -> anyhow::Result<()> {
|
||||||
config: &'static Config,
|
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||||
terminal: &mut Terminal<Backend>,
|
let mut ui = Self::new(event_tx.clone());
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
|
||||||
let mut ui = Self::new(config, event_tx.clone());
|
|
||||||
|
|
||||||
tokio::select! {
|
let result = tokio::select! {
|
||||||
e = ui.run_main(terminal, &mut event_rx) => e,
|
e = ui.run_main(terminal, event_tx.clone(), event_rx) => e,
|
||||||
e = Self::shovel_crossterm_events(event_tx) => e,
|
e = Self::shovel_crossterm_events(event_tx) => e,
|
||||||
}
|
};
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shovel_crossterm_events(tx: UnboundedSender<UiEvent>) -> anyhow::Result<()> {
|
async fn shovel_crossterm_events(tx: UnboundedSender<UiEvent>) -> anyhow::Result<()> {
|
||||||
|
|
@ -111,25 +50,17 @@ impl Ui {
|
||||||
|
|
||||||
async fn run_main(
|
async fn run_main(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal<Backend>,
|
terminal: &mut Terminal,
|
||||||
event_rx: &mut UnboundedReceiver<UiEvent>,
|
event_tx: UnboundedSender<UiEvent>,
|
||||||
|
mut event_rx: UnboundedReceiver<UiEvent>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
loop {
|
loop {
|
||||||
// 1. Render current state
|
// 1. Render current state
|
||||||
terminal.autoresize()?;
|
terminal.autoresize()?;
|
||||||
|
self.render(terminal.frame()).await?;
|
||||||
let mut frame = terminal.get_frame();
|
if terminal.present()? == Redraw::Required {
|
||||||
self.last_area = frame.size();
|
event_tx.send(UiEvent::Redraw);
|
||||||
self.render(&mut frame).await?;
|
}
|
||||||
|
|
||||||
// Do a little dance to please the borrow checker
|
|
||||||
let cursor = frame.cursor();
|
|
||||||
drop(frame);
|
|
||||||
|
|
||||||
terminal.flush()?;
|
|
||||||
terminal.set_cursor_opt(cursor)?; // Must happen after flush
|
|
||||||
terminal.flush_backend()?;
|
|
||||||
terminal.swap_buffers();
|
|
||||||
|
|
||||||
// 2. Handle events (in batches)
|
// 2. Handle events (in batches)
|
||||||
let mut event = match event_rx.recv().await {
|
let mut event = match event_rx.recv().await {
|
||||||
|
|
@ -138,10 +69,10 @@ impl Ui {
|
||||||
};
|
};
|
||||||
loop {
|
loop {
|
||||||
let result = match event {
|
let result = match event {
|
||||||
|
UiEvent::Redraw => EventHandleResult::Continue,
|
||||||
UiEvent::Term(Event::Key(event)) => self.handle_key_event(event).await,
|
UiEvent::Term(Event::Key(event)) => self.handle_key_event(event).await,
|
||||||
UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?,
|
UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?,
|
||||||
UiEvent::Term(Event::Resize(_, _)) => EventHandleResult::Continue,
|
UiEvent::Term(Event::Resize(_, _)) => EventHandleResult::Continue,
|
||||||
UiEvent::Cove(name, event) => self.handle_cove_event(name, event).await?,
|
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
EventHandleResult::Continue => {}
|
EventHandleResult::Continue => {}
|
||||||
|
|
@ -156,259 +87,22 @@ impl Ui {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render(&mut self, frame: &mut Frame<'_, Backend>) -> anyhow::Result<()> {
|
async fn render(&mut self, frame: &mut Frame) -> anyhow::Result<()> {
|
||||||
let entire_area = frame.size();
|
frame.write(Pos::new(0, 0), "Hello world!", ContentStyle::default());
|
||||||
let areas = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(self.rooms_pane.width()),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(self.users_pane.width()),
|
|
||||||
])
|
|
||||||
.split(entire_area);
|
|
||||||
let rooms_pane_area = areas[0];
|
|
||||||
let rooms_pane_border = areas[1];
|
|
||||||
let main_pane_area = areas[2];
|
|
||||||
let users_pane_border = areas[3];
|
|
||||||
let users_pane_area = areas[4];
|
|
||||||
|
|
||||||
// Main pane and users pane
|
|
||||||
self.render_room(frame, main_pane_area, users_pane_area)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Rooms pane
|
|
||||||
let mut rooms = Rooms::new(&self.cove_rooms);
|
|
||||||
if let Some(RoomId::Cove(name)) = &self.room {
|
|
||||||
rooms = rooms.select(name);
|
|
||||||
}
|
|
||||||
frame.render_widget(rooms, rooms_pane_area);
|
|
||||||
|
|
||||||
// Pane borders and width
|
|
||||||
self.rooms_pane.restrict_width(rooms_pane_area.width);
|
|
||||||
frame.render_widget(self.rooms_pane.border(), rooms_pane_border);
|
|
||||||
self.users_pane.restrict_width(users_pane_area.width);
|
|
||||||
frame.render_widget(self.users_pane.border(), users_pane_border);
|
|
||||||
|
|
||||||
// Overlay
|
|
||||||
if let Some(overlay) = &mut self.overlay {
|
|
||||||
match overlay {
|
|
||||||
Overlay::SwitchRoom(state) => {
|
|
||||||
frame.render_stateful_widget(SwitchRoom, entire_area, state);
|
|
||||||
let (x, y) = state.last_cursor_pos();
|
|
||||||
frame.set_cursor(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_room(
|
|
||||||
&mut self,
|
|
||||||
frame: &mut Frame<'_, Backend>,
|
|
||||||
main_pane_area: Rect,
|
|
||||||
users_pane_area: Rect,
|
|
||||||
) {
|
|
||||||
match &self.room {
|
|
||||||
Some(RoomId::Cove(name)) => {
|
|
||||||
if let Some(ui) = self.cove_rooms.get_mut(name) {
|
|
||||||
ui.render_main(frame, main_pane_area).await;
|
|
||||||
ui.render_users(frame, users_pane_area).await;
|
|
||||||
} else {
|
|
||||||
self.room = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// TODO Render welcome screen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_key_event(&mut self, event: KeyEvent) -> EventHandleResult {
|
async fn handle_key_event(&mut self, event: KeyEvent) -> EventHandleResult {
|
||||||
if let Some(result) = self.handle_key_event_for_overlay(event).await {
|
match event.code {
|
||||||
return result;
|
KeyCode::Char('Q') => return EventHandleResult::Stop,
|
||||||
}
|
_ => {}
|
||||||
|
|
||||||
if let Some(result) = self.handle_key_event_for_main_panel(event).await {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(result) = self.handle_key_event_for_ui(event).await {
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EventHandleResult::Continue
|
EventHandleResult::Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_key_event_for_overlay(&mut self, event: KeyEvent) -> Option<EventHandleResult> {
|
|
||||||
if let Some(overlay) = &mut self.overlay {
|
|
||||||
let reaction = match overlay {
|
|
||||||
Overlay::SwitchRoom(state) => state.handle_key(event),
|
|
||||||
};
|
|
||||||
match reaction {
|
|
||||||
Some(OverlayReaction::Handled) => {}
|
|
||||||
Some(OverlayReaction::Close) => self.overlay = None,
|
|
||||||
Some(OverlayReaction::SwitchRoom(id)) => {
|
|
||||||
self.overlay = None;
|
|
||||||
self.switch_to_room(id);
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
Some(EventHandleResult::Continue)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_key_event_for_main_panel(
|
|
||||||
&mut self,
|
|
||||||
event: KeyEvent,
|
|
||||||
) -> Option<EventHandleResult> {
|
|
||||||
match &self.room {
|
|
||||||
Some(RoomId::Cove(name)) => {
|
|
||||||
if let Some(ui) = self.cove_rooms.get_mut(name) {
|
|
||||||
ui.handle_key(event).await;
|
|
||||||
Some(EventHandleResult::Continue)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_key_event_for_ui(&mut self, event: KeyEvent) -> Option<EventHandleResult> {
|
|
||||||
match event.code {
|
|
||||||
KeyCode::Char('Q') => Some(EventHandleResult::Stop),
|
|
||||||
KeyCode::Char('s') => {
|
|
||||||
self.overlay = Some(Overlay::SwitchRoom(SwitchRoomState::default()));
|
|
||||||
Some(EventHandleResult::Continue)
|
|
||||||
}
|
|
||||||
KeyCode::Char('J') => {
|
|
||||||
self.switch_to_next_room();
|
|
||||||
Some(EventHandleResult::Continue)
|
|
||||||
}
|
|
||||||
KeyCode::Char('K') => {
|
|
||||||
self.switch_to_prev_room();
|
|
||||||
Some(EventHandleResult::Continue)
|
|
||||||
}
|
|
||||||
KeyCode::Char('D') => {
|
|
||||||
self.remove_current_room();
|
|
||||||
Some(EventHandleResult::Continue)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_mouse_event(&mut self, event: MouseEvent) -> anyhow::Result<EventHandleResult> {
|
async fn handle_mouse_event(&mut self, event: MouseEvent) -> anyhow::Result<EventHandleResult> {
|
||||||
let rooms_width = event.column;
|
|
||||||
let users_width = self.last_area.width - event.column - 1;
|
|
||||||
let rooms_hover = rooms_width == self.rooms_pane.width();
|
|
||||||
let users_hover = users_width == self.users_pane.width();
|
|
||||||
match event.kind {
|
|
||||||
MouseEventKind::Moved => {
|
|
||||||
self.rooms_pane.hover(rooms_hover);
|
|
||||||
self.users_pane.hover(users_hover);
|
|
||||||
}
|
|
||||||
MouseEventKind::Down(_) => {
|
|
||||||
self.rooms_pane.drag(rooms_hover);
|
|
||||||
self.users_pane.drag(users_hover);
|
|
||||||
}
|
|
||||||
MouseEventKind::Up(_) => {
|
|
||||||
self.rooms_pane.drag(false);
|
|
||||||
self.users_pane.drag(false);
|
|
||||||
}
|
|
||||||
MouseEventKind::Drag(_) => {
|
|
||||||
self.rooms_pane.drag_to(rooms_width);
|
|
||||||
self.users_pane.drag_to(users_width);
|
|
||||||
}
|
|
||||||
// MouseEventKind::ScrollDown => todo!(),
|
|
||||||
// MouseEventKind::ScrollUp => todo!(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(EventHandleResult::Continue)
|
Ok(EventHandleResult::Continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_cove_event(
|
|
||||||
&mut self,
|
|
||||||
name: String,
|
|
||||||
event: CoveEvent,
|
|
||||||
) -> anyhow::Result<EventHandleResult> {
|
|
||||||
match event {
|
|
||||||
CoveEvent::StateChanged => {}
|
|
||||||
CoveEvent::IdentificationRequired => {
|
|
||||||
// TODO Send identification if default nick is set in config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(EventHandleResult::Continue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn switch_to_room(&mut self, id: RoomId) {
|
|
||||||
match &id {
|
|
||||||
RoomId::Cove(name) => {
|
|
||||||
if let Entry::Vacant(entry) = self.cove_rooms.entry(name.clone()) {
|
|
||||||
let room = CoveRoom::new(
|
|
||||||
self.config,
|
|
||||||
name.clone(),
|
|
||||||
self.event_tx.clone(),
|
|
||||||
UiEvent::cove,
|
|
||||||
);
|
|
||||||
entry.insert(CoveUi::new(room));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.room = Some(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rooms_in_order(&self) -> Vec<RoomId> {
|
|
||||||
let mut rooms = vec![];
|
|
||||||
rooms.extend(self.cove_rooms.keys().cloned().map(RoomId::Cove));
|
|
||||||
rooms.sort();
|
|
||||||
rooms
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_room_index(&self, rooms: &[RoomId]) -> Option<(usize, RoomId)> {
|
|
||||||
let id = self.room.clone()?;
|
|
||||||
let index = rooms.iter().position(|room| room == &id)?;
|
|
||||||
Some((index, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_room_index(&mut self, rooms: &[RoomId], index: usize) {
|
|
||||||
if rooms.is_empty() {
|
|
||||||
self.room = None;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = rooms[index % rooms.len()].clone();
|
|
||||||
self.room = Some(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn switch_to_next_room(&mut self) {
|
|
||||||
let rooms = self.rooms_in_order();
|
|
||||||
if let Some((index, _)) = self.get_room_index(&rooms) {
|
|
||||||
self.set_room_index(&rooms, index + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn switch_to_prev_room(&mut self) {
|
|
||||||
let rooms = self.rooms_in_order();
|
|
||||||
if let Some((index, _)) = self.get_room_index(&rooms) {
|
|
||||||
self.set_room_index(&rooms, index + rooms.len() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_current_room(&mut self) {
|
|
||||||
let rooms = self.rooms_in_order();
|
|
||||||
if let Some((index, id)) = self.get_room_index(&rooms) {
|
|
||||||
match id {
|
|
||||||
RoomId::Cove(name) => self.cove_rooms.remove(&name),
|
|
||||||
};
|
|
||||||
|
|
||||||
let rooms = self.rooms_in_order();
|
|
||||||
let max_index = if rooms.is_empty() { 0 } else { rooms.len() - 1 };
|
|
||||||
self.set_room_index(&rooms, index.min(max_index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
mod body;
|
|
||||||
mod users;
|
|
||||||
|
|
||||||
use crossterm::event::KeyEvent;
|
|
||||||
use tui::backend::Backend;
|
|
||||||
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
|
||||||
use tui::text::Span;
|
|
||||||
use tui::widgets::{Block, BorderType, Borders, Paragraph};
|
|
||||||
use tui::Frame;
|
|
||||||
|
|
||||||
use crate::client::cove::room::CoveRoom;
|
|
||||||
|
|
||||||
use self::body::{Body, Reaction};
|
|
||||||
use self::users::CoveUsers;
|
|
||||||
|
|
||||||
use super::input::EventHandler;
|
|
||||||
use super::styles;
|
|
||||||
|
|
||||||
pub struct CoveUi {
|
|
||||||
room: CoveRoom,
|
|
||||||
body: Body,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoveUi {
|
|
||||||
pub fn new(room: CoveRoom) -> Self {
|
|
||||||
Self {
|
|
||||||
room,
|
|
||||||
body: Body::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.room.name()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render_main<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
let areas = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
let title_area = areas[0];
|
|
||||||
let separator_area = areas[1];
|
|
||||||
let body_area = areas[2];
|
|
||||||
|
|
||||||
self.render_title(frame, title_area).await;
|
|
||||||
self.render_separator(frame, separator_area).await;
|
|
||||||
self.render_body(frame, body_area).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn render_title<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
// TODO Show current nick as well, if applicable
|
|
||||||
let room_name = Paragraph::new(Span::styled(
|
|
||||||
format!("&{}", self.name()),
|
|
||||||
styles::selected_room(),
|
|
||||||
))
|
|
||||||
.alignment(Alignment::Center);
|
|
||||||
frame.render_widget(room_name, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn render_separator<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
let separator = Block::default()
|
|
||||||
.borders(Borders::BOTTOM)
|
|
||||||
.border_type(BorderType::Double);
|
|
||||||
frame.render_widget(separator, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn render_body<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
self.body.update(&self.room).await;
|
|
||||||
self.body.render(frame, area).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render_users<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
if let Some(present) = self.room.conn().await.state().await.present() {
|
|
||||||
frame.render_widget(CoveUsers::new(present), area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_key(&mut self, event: KeyEvent) -> Option<()> {
|
|
||||||
match self.body.handle_key(event)? {
|
|
||||||
Reaction::Handled => Some(()),
|
|
||||||
Reaction::Identify(nick) => {
|
|
||||||
self.room.identify(&nick, &nick).await;
|
|
||||||
Some(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use tui::backend::Backend;
|
|
||||||
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
|
||||||
use tui::text::Span;
|
|
||||||
use tui::widgets::Paragraph;
|
|
||||||
use tui::Frame;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
use crate::client::cove::conn::{State, Status};
|
|
||||||
use crate::client::cove::room::CoveRoom;
|
|
||||||
use crate::ui::input::EventHandler;
|
|
||||||
use crate::ui::textline::{TextLine, TextLineState};
|
|
||||||
use crate::ui::{layout, styles};
|
|
||||||
|
|
||||||
pub enum Body {
|
|
||||||
Empty,
|
|
||||||
Connecting,
|
|
||||||
ChoosingRoom,
|
|
||||||
Identifying,
|
|
||||||
ChooseNick {
|
|
||||||
nick: TextLineState,
|
|
||||||
prev_error: Option<String>,
|
|
||||||
},
|
|
||||||
Present,
|
|
||||||
Stopped, // TODO Display reason for stoppage
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Body {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Body {
|
|
||||||
pub async fn update(&mut self, room: &CoveRoom) {
|
|
||||||
match &*room.conn().await.state().await {
|
|
||||||
State::Connecting => *self = Self::Connecting,
|
|
||||||
State::Connected(conn) => match conn.status() {
|
|
||||||
Status::ChoosingRoom => *self = Self::ChoosingRoom,
|
|
||||||
Status::Identifying => *self = Self::Identifying,
|
|
||||||
Status::IdRequired(error) => self.choose_nick(error.clone()),
|
|
||||||
Status::Present(_) => *self = Self::Present,
|
|
||||||
},
|
|
||||||
State::Stopped => *self = Self::Stopped,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn choose_nick(&mut self, error: Option<String>) {
|
|
||||||
match self {
|
|
||||||
Self::ChooseNick { prev_error, .. } => *prev_error = error,
|
|
||||||
_ => {
|
|
||||||
*self = Self::ChooseNick {
|
|
||||||
nick: TextLineState::default(),
|
|
||||||
prev_error: error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
match self {
|
|
||||||
Body::Empty => todo!(),
|
|
||||||
Body::Connecting => {
|
|
||||||
let text = "Connecting...";
|
|
||||||
let area = layout::centered(text.width() as u16, 1, area);
|
|
||||||
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
|
|
||||||
}
|
|
||||||
Body::ChoosingRoom => {
|
|
||||||
let text = "Entering room...";
|
|
||||||
let area = layout::centered(text.width() as u16, 1, area);
|
|
||||||
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
|
|
||||||
}
|
|
||||||
Body::Identifying => {
|
|
||||||
let text = "Identifying...";
|
|
||||||
let area = layout::centered(text.width() as u16, 1, area);
|
|
||||||
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
|
|
||||||
}
|
|
||||||
Body::ChooseNick {
|
|
||||||
nick,
|
|
||||||
prev_error: None,
|
|
||||||
} => {
|
|
||||||
let area = layout::centered_v(2, area);
|
|
||||||
let areas = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Length(1), Constraint::Length(1)])
|
|
||||||
.split(area);
|
|
||||||
let title_area = areas[0];
|
|
||||||
let text_area = areas[1];
|
|
||||||
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(Span::styled("Choose a nick:", styles::title()))
|
|
||||||
.alignment(Alignment::Center),
|
|
||||||
title_area,
|
|
||||||
);
|
|
||||||
frame.render_stateful_widget(TextLine, layout::centered(50, 1, text_area), nick);
|
|
||||||
}
|
|
||||||
Body::ChooseNick {
|
|
||||||
nick,
|
|
||||||
prev_error: Some(error),
|
|
||||||
} => {
|
|
||||||
let area = layout::centered_v(3, area);
|
|
||||||
let areas = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
let title_area = areas[0];
|
|
||||||
let text_area = areas[1];
|
|
||||||
let error_area = areas[2];
|
|
||||||
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(Span::styled("Choose a nick:", styles::title()))
|
|
||||||
.alignment(Alignment::Center),
|
|
||||||
title_area,
|
|
||||||
);
|
|
||||||
frame.render_stateful_widget(TextLine, layout::centered(50, 1, text_area), nick);
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(Span::styled(error as &str, styles::error()))
|
|
||||||
.alignment(Alignment::Center),
|
|
||||||
error_area,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Body::Present => {
|
|
||||||
let text = "Present";
|
|
||||||
let area = layout::centered(text.width() as u16, 1, area);
|
|
||||||
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
|
|
||||||
}
|
|
||||||
Body::Stopped => {
|
|
||||||
let text = "Stopped";
|
|
||||||
let area = layout::centered(text.width() as u16, 1, area);
|
|
||||||
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Reaction {
|
|
||||||
Handled,
|
|
||||||
Identify(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventHandler for Body {
|
|
||||||
type Reaction = Reaction;
|
|
||||||
|
|
||||||
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction> {
|
|
||||||
match self {
|
|
||||||
Body::ChooseNick { nick, .. } => {
|
|
||||||
if event.code == KeyCode::Enter {
|
|
||||||
Some(Reaction::Identify(nick.content().to_string()))
|
|
||||||
} else {
|
|
||||||
nick.handle_key(event).and(Some(Reaction::Handled))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Body::Present => None, // TODO Implement
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::iter;
|
|
||||||
|
|
||||||
use cove_core::{Identity, Session};
|
|
||||||
use tui::buffer::Buffer;
|
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::text::{Span, Spans};
|
|
||||||
use tui::widgets::{Paragraph, Widget};
|
|
||||||
|
|
||||||
use crate::client::cove::conn::Present;
|
|
||||||
use crate::ui::styles;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct UserInfo {
|
|
||||||
nick: String,
|
|
||||||
identity: Identity,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Session> for UserInfo {
|
|
||||||
fn from(s: &Session) -> Self {
|
|
||||||
Self {
|
|
||||||
nick: s.nick.clone(),
|
|
||||||
identity: s.identity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CoveUsers {
|
|
||||||
users: Vec<UserInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoveUsers {
|
|
||||||
pub fn new(present: &Present) -> Self {
|
|
||||||
let mut users: Vec<UserInfo> = iter::once(&present.session)
|
|
||||||
.chain(present.others.values())
|
|
||||||
.map(<&Session as Into<UserInfo>>::into)
|
|
||||||
.collect();
|
|
||||||
users.sort();
|
|
||||||
Self { users }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for CoveUsers {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let sessions = self.users.len();
|
|
||||||
let identities = self
|
|
||||||
.users
|
|
||||||
.iter()
|
|
||||||
.map(|i| i.identity)
|
|
||||||
.collect::<HashSet<_>>()
|
|
||||||
.len();
|
|
||||||
let title = format!("Users ({identities}/{sessions})");
|
|
||||||
|
|
||||||
let mut lines = vec![Spans::from(Span::styled(title, styles::title()))];
|
|
||||||
for user in self.users {
|
|
||||||
// TODO Colour users based on identity
|
|
||||||
lines.push(Spans::from(Span::from(user.nick)));
|
|
||||||
}
|
|
||||||
Paragraph::new(lines).render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
use crossterm::event::KeyEvent;
|
|
||||||
|
|
||||||
pub trait EventHandler {
|
|
||||||
type Reaction;
|
|
||||||
|
|
||||||
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction>;
|
|
||||||
|
|
||||||
// TODO Add method to show currently accepted keys for F1 help
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use tui::layout::Rect;
|
|
||||||
|
|
||||||
pub fn centered(width: u16, height: u16, area: Rect) -> Rect {
|
|
||||||
let width = width.min(area.width);
|
|
||||||
let height = height.min(area.height);
|
|
||||||
let dx = (area.width - width) / 2;
|
|
||||||
let dy = (area.height - height) / 2;
|
|
||||||
Rect {
|
|
||||||
x: area.x + dx,
|
|
||||||
y: area.y + dy,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn centered_v(height: u16, area: Rect) -> Rect {
|
|
||||||
let height = height.min(area.height);
|
|
||||||
let dy = (area.height - height) / 2;
|
|
||||||
Rect {
|
|
||||||
y: area.y + dy,
|
|
||||||
height,
|
|
||||||
..area
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
mod switch_room;
|
|
||||||
|
|
||||||
pub use switch_room::*;
|
|
||||||
|
|
||||||
use super::RoomId;
|
|
||||||
|
|
||||||
pub enum Overlay {
|
|
||||||
SwitchRoom(SwitchRoomState),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum OverlayReaction {
|
|
||||||
Handled,
|
|
||||||
Close,
|
|
||||||
SwitchRoom(RoomId),
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use tui::buffer::Buffer;
|
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::widgets::{Block, Borders, Clear, StatefulWidget, Widget};
|
|
||||||
|
|
||||||
use crate::ui::input::EventHandler;
|
|
||||||
use crate::ui::textline::{TextLine, TextLineReaction, TextLineState};
|
|
||||||
use crate::ui::{layout, RoomId};
|
|
||||||
|
|
||||||
use super::OverlayReaction;
|
|
||||||
|
|
||||||
pub struct SwitchRoom;
|
|
||||||
|
|
||||||
impl StatefulWidget for SwitchRoom {
|
|
||||||
type State = SwitchRoomState;
|
|
||||||
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
|
||||||
let area = layout::centered(50, 3, area);
|
|
||||||
Clear.render(area, buf);
|
|
||||||
|
|
||||||
let block = Block::default().title("Join room").borders(Borders::ALL);
|
|
||||||
let inner_area = block.inner(area);
|
|
||||||
block.render(area, buf);
|
|
||||||
|
|
||||||
TextLine.render(inner_area, buf, &mut state.room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct SwitchRoomState {
|
|
||||||
room: TextLineState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventHandler for SwitchRoomState {
|
|
||||||
type Reaction = OverlayReaction;
|
|
||||||
|
|
||||||
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction> {
|
|
||||||
if event.code == KeyCode::Enter {
|
|
||||||
let name = self.room.content().trim();
|
|
||||||
if name.is_empty() {
|
|
||||||
return Some(Self::Reaction::Handled);
|
|
||||||
}
|
|
||||||
let id = RoomId::Cove(name.to_string());
|
|
||||||
return Some(Self::Reaction::SwitchRoom(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.room.handle_key(event).map(|r| match r {
|
|
||||||
TextLineReaction::Handled => Self::Reaction::Handled,
|
|
||||||
TextLineReaction::Close => Self::Reaction::Close,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SwitchRoomState {
|
|
||||||
pub fn last_cursor_pos(&self) -> (u16, u16) {
|
|
||||||
self.room.last_cursor_pos()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
use tui::buffer::Buffer;
|
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::style::{Modifier, Style};
|
|
||||||
use tui::widgets::{Block, Borders, Widget};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PaneInfo {
|
|
||||||
width: u16,
|
|
||||||
hovering: bool,
|
|
||||||
dragging: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PaneInfo {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
width: 24,
|
|
||||||
hovering: false,
|
|
||||||
dragging: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaneInfo {
|
|
||||||
pub fn width(&self) -> u16 {
|
|
||||||
self.width
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restrict_width(&mut self, width: u16) {
|
|
||||||
self.width = self.width.min(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hover(&mut self, active: bool) {
|
|
||||||
self.hovering = active;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn drag(&mut self, active: bool) {
|
|
||||||
self.dragging = active;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn drag_to(&mut self, width: u16) {
|
|
||||||
if self.dragging {
|
|
||||||
self.width = width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rendering the pane's border (not part of the pane's area)
|
|
||||||
|
|
||||||
struct Border {
|
|
||||||
hovering: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Border {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let mut block = Block::default().borders(Borders::LEFT);
|
|
||||||
if self.hovering {
|
|
||||||
block = block.style(Style::default().add_modifier(Modifier::REVERSED));
|
|
||||||
}
|
|
||||||
block.render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaneInfo {
|
|
||||||
pub fn border(&self) -> impl Widget {
|
|
||||||
Border {
|
|
||||||
hovering: self.hovering,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
mod users;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tui::backend::Backend;
|
|
||||||
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
|
||||||
use tui::style::Style;
|
|
||||||
use tui::text::{Span, Spans, Text};
|
|
||||||
use tui::widgets::{Block, BorderType, Borders, Paragraph};
|
|
||||||
use tui::Frame;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
use crate::room::{Room, Status};
|
|
||||||
|
|
||||||
use self::users::Users;
|
|
||||||
|
|
||||||
use super::textline::{TextLine, TextLineState};
|
|
||||||
use super::{layout, styles};
|
|
||||||
|
|
||||||
enum Main {
|
|
||||||
Empty,
|
|
||||||
Connecting,
|
|
||||||
Identifying,
|
|
||||||
ChooseNick {
|
|
||||||
nick: TextLineState,
|
|
||||||
prev_error: Option<String>,
|
|
||||||
},
|
|
||||||
Messages,
|
|
||||||
FatalError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Main {
|
|
||||||
fn choose_nick() -> Self {
|
|
||||||
Self::ChooseNick {
|
|
||||||
nick: TextLineState::default(),
|
|
||||||
prev_error: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fatal<S: ToString>(s: S) -> Self {
|
|
||||||
Self::FatalError(s.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RoomInfo {
|
|
||||||
name: String,
|
|
||||||
room: Arc<Mutex<Room>>,
|
|
||||||
main: Main,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RoomInfo {
|
|
||||||
pub fn new(name: String, room: Arc<Mutex<Room>>) -> Self {
|
|
||||||
Self {
|
|
||||||
name,
|
|
||||||
room,
|
|
||||||
main: Main::Empty,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn align_main(&mut self) {
|
|
||||||
let room = self.room.lock().await;
|
|
||||||
match room.status() {
|
|
||||||
Status::Nominal if room.connected() && room.present().is_some() => {
|
|
||||||
if !matches!(self.main, Main::Messages) {
|
|
||||||
self.main = Main::Messages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Status::Nominal if room.connected() => self.main = Main::Connecting,
|
|
||||||
Status::Nominal => self.main = Main::Identifying,
|
|
||||||
Status::NickRequired => self.main = Main::choose_nick(),
|
|
||||||
Status::CouldNotConnect => self.main = Main::fatal("Could not connect to room"),
|
|
||||||
Status::InvalidRoom(err) => self.main = Main::fatal(format!("Invalid room:\n{err}")),
|
|
||||||
Status::InvalidNick(err) => {
|
|
||||||
if let Main::ChooseNick { prev_error, .. } = &mut self.main {
|
|
||||||
*prev_error = Some(err.clone());
|
|
||||||
} else {
|
|
||||||
self.main = Main::choose_nick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Status::InvalidIdentity(err) => {
|
|
||||||
self.main = Main::fatal(format!("Invalid identity:\n{err}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render_main<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
self.align_main().await;
|
|
||||||
|
|
||||||
let areas = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
let room_name_area = areas[0];
|
|
||||||
let separator_area = areas[1];
|
|
||||||
let main_area = areas[2];
|
|
||||||
|
|
||||||
// Room name at the top
|
|
||||||
let room_name = Paragraph::new(Span::styled(
|
|
||||||
format!("&{}", self.name()),
|
|
||||||
styles::selected_room(),
|
|
||||||
))
|
|
||||||
.alignment(Alignment::Center);
|
|
||||||
frame.render_widget(room_name, room_name_area);
|
|
||||||
let separator = Block::default()
|
|
||||||
.borders(Borders::BOTTOM)
|
|
||||||
.border_type(BorderType::Double);
|
|
||||||
frame.render_widget(separator, separator_area);
|
|
||||||
|
|
||||||
// Main below
|
|
||||||
self.render_main_inner(frame, main_area).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn render_main_inner<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
match &mut self.main {
|
|
||||||
Main::Empty => {}
|
|
||||||
Main::Connecting => {
|
|
||||||
let text = "Connecting...";
|
|
||||||
let area = layout::centered(text.width() as u16, 1, area);
|
|
||||||
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
|
|
||||||
}
|
|
||||||
Main::Identifying => {
|
|
||||||
let text = "Identifying...";
|
|
||||||
let area = layout::centered(text.width() as u16, 1, area);
|
|
||||||
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
|
|
||||||
}
|
|
||||||
Main::ChooseNick {
|
|
||||||
nick,
|
|
||||||
prev_error: None,
|
|
||||||
} => {
|
|
||||||
let area = layout::centered(50, 2, area);
|
|
||||||
let top = Rect { height: 1, ..area };
|
|
||||||
let bot = Rect {
|
|
||||||
y: top.y + 1,
|
|
||||||
..top
|
|
||||||
};
|
|
||||||
let text = "Choose a nick:";
|
|
||||||
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), top);
|
|
||||||
frame.render_stateful_widget(TextLine, bot, nick);
|
|
||||||
}
|
|
||||||
Main::ChooseNick { nick, prev_error } => {
|
|
||||||
let width = prev_error
|
|
||||||
.as_ref()
|
|
||||||
.map(|e| e.width() as u16)
|
|
||||||
.unwrap_or(0)
|
|
||||||
.max(50);
|
|
||||||
let height = if prev_error.is_some() { 5 } else { 2 };
|
|
||||||
let area = layout::centered(width, height, area);
|
|
||||||
let top = Rect {
|
|
||||||
height: height - 1,
|
|
||||||
..area
|
|
||||||
};
|
|
||||||
let bot = Rect {
|
|
||||||
y: area.bottom() - 1,
|
|
||||||
height: 1,
|
|
||||||
..area
|
|
||||||
};
|
|
||||||
let mut lines = vec![];
|
|
||||||
if let Some(err) = &prev_error {
|
|
||||||
lines.push(Spans::from(Span::styled("Error:", styles::title())));
|
|
||||||
lines.push(Spans::from(Span::styled(err, styles::error())));
|
|
||||||
lines.push(Spans::from(""));
|
|
||||||
}
|
|
||||||
lines.push(Spans::from(Span::styled("Choose a nick:", styles::title())));
|
|
||||||
frame.render_widget(Paragraph::new(lines), top);
|
|
||||||
frame.render_stateful_widget(TextLine, bot, nick);
|
|
||||||
}
|
|
||||||
Main::Messages => {
|
|
||||||
// TODO Actually render messages
|
|
||||||
frame.render_widget(Paragraph::new("TODO: Messages"), area);
|
|
||||||
}
|
|
||||||
Main::FatalError(err) => {
|
|
||||||
let title = "Fatal error:";
|
|
||||||
let width = (err.width() as u16).max(title.width() as u16);
|
|
||||||
let area = layout::centered(width, 2, area);
|
|
||||||
let pg = Paragraph::new(vec![
|
|
||||||
Spans::from(Span::styled(title, styles::title())),
|
|
||||||
Spans::from(Span::styled(err as &str, styles::error())),
|
|
||||||
])
|
|
||||||
.alignment(Alignment::Center);
|
|
||||||
frame.render_widget(pg, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render_users<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
|
||||||
if let Some(present) = self.room.lock().await.present() {
|
|
||||||
frame.render_widget(Users::new(present), area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use tui::buffer::Buffer;
|
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::text::{Span, Spans};
|
|
||||||
use tui::widgets::{Paragraph, Widget};
|
|
||||||
|
|
||||||
use super::cove::CoveUi;
|
|
||||||
use super::styles;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct RoomInfo {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Rooms {
|
|
||||||
rooms: Vec<RoomInfo>,
|
|
||||||
selected: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rooms {
|
|
||||||
pub fn new(cove_rooms: &HashMap<String, CoveUi>) -> Self {
|
|
||||||
let mut rooms = cove_rooms
|
|
||||||
.iter()
|
|
||||||
.map(|(name, _)| RoomInfo { name: name.clone() })
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
rooms.sort();
|
|
||||||
Self {
|
|
||||||
rooms,
|
|
||||||
selected: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select(mut self, name: &str) -> Self {
|
|
||||||
for (i, room) in self.rooms.iter().enumerate() {
|
|
||||||
if room.name == name {
|
|
||||||
self.selected = Some(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Rooms {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let title = if let Some(selected) = self.selected {
|
|
||||||
format!("Rooms ({}/{})", selected + 1, self.rooms.len())
|
|
||||||
} else {
|
|
||||||
format!("Rooms ({})", self.rooms.len())
|
|
||||||
};
|
|
||||||
let mut lines = vec![Spans::from(Span::styled(title, styles::title()))];
|
|
||||||
for (i, room) in self.rooms.iter().enumerate() {
|
|
||||||
let name = format!("&{}", room.name);
|
|
||||||
if Some(i) == self.selected {
|
|
||||||
lines.push(Spans::from(vec![
|
|
||||||
Span::raw("\n>"),
|
|
||||||
Span::styled(name, styles::selected_room()),
|
|
||||||
]));
|
|
||||||
} else {
|
|
||||||
lines.push(Spans::from(vec![
|
|
||||||
Span::raw("\n "),
|
|
||||||
Span::styled(name, styles::room()),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Paragraph::new(lines).render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
use tui::style::{Color, Modifier, Style};
|
|
||||||
|
|
||||||
pub fn title() -> Style {
|
|
||||||
Style::default().add_modifier(Modifier::BOLD)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error() -> Style {
|
|
||||||
Style::default().fg(Color::Red)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn room() -> Style {
|
|
||||||
Style::default().fg(Color::LightBlue)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_room() -> Style {
|
|
||||||
room().add_modifier(Modifier::BOLD)
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
use std::cmp;
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use tui::buffer::Buffer;
|
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::widgets::{Paragraph, StatefulWidget, Widget};
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
use super::input::EventHandler;
|
|
||||||
|
|
||||||
/// A simple single-line text box.
|
|
||||||
pub struct TextLine;
|
|
||||||
|
|
||||||
impl StatefulWidget for TextLine {
|
|
||||||
type State = TextLineState;
|
|
||||||
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
|
||||||
Paragraph::new(&state.content as &str).render(area, buf);
|
|
||||||
|
|
||||||
// Determine cursor position
|
|
||||||
let prefix = state.content.chars().take(state.cursor).collect::<String>();
|
|
||||||
let position = prefix.width() as u16;
|
|
||||||
let x = area.x + position.min(area.width);
|
|
||||||
state.last_cursor_pos = (x, area.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State for [`TextLine`].
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct TextLineState {
|
|
||||||
content: String,
|
|
||||||
cursor: usize,
|
|
||||||
last_cursor_pos: (u16, u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextLineState {
|
|
||||||
pub fn content(&self) -> &str {
|
|
||||||
&self.content
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The cursor's position from when the widget was last rendered.
|
|
||||||
pub fn last_cursor_pos(&self) -> (u16, u16) {
|
|
||||||
self.last_cursor_pos
|
|
||||||
}
|
|
||||||
|
|
||||||
fn chars(&self) -> usize {
|
|
||||||
self.content.chars().count()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_start(&mut self) {
|
|
||||||
self.cursor = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_left(&mut self) {
|
|
||||||
if self.cursor > 0 {
|
|
||||||
self.cursor -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_right(&mut self) {
|
|
||||||
self.cursor = cmp::min(self.cursor + 1, self.chars());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_end(&mut self) {
|
|
||||||
self.cursor = self.chars();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cursor_byte_offset(&self) -> usize {
|
|
||||||
self.content
|
|
||||||
.char_indices()
|
|
||||||
.nth(self.cursor)
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
.unwrap_or_else(|| self.content.len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum TextLineReaction {
|
|
||||||
Handled,
|
|
||||||
Close,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventHandler for TextLineState {
|
|
||||||
type Reaction = TextLineReaction;
|
|
||||||
|
|
||||||
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction> {
|
|
||||||
match event.code {
|
|
||||||
KeyCode::Backspace if self.cursor > 0 => {
|
|
||||||
self.move_cursor_left();
|
|
||||||
self.content.remove(self.cursor_byte_offset());
|
|
||||||
Some(TextLineReaction::Handled)
|
|
||||||
}
|
|
||||||
KeyCode::Left => {
|
|
||||||
self.move_cursor_left();
|
|
||||||
Some(TextLineReaction::Handled)
|
|
||||||
}
|
|
||||||
KeyCode::Right => {
|
|
||||||
self.move_cursor_right();
|
|
||||||
Some(TextLineReaction::Handled)
|
|
||||||
}
|
|
||||||
KeyCode::Home => {
|
|
||||||
self.move_cursor_start();
|
|
||||||
Some(TextLineReaction::Handled)
|
|
||||||
}
|
|
||||||
KeyCode::End => {
|
|
||||||
self.move_cursor_end();
|
|
||||||
Some(TextLineReaction::Handled)
|
|
||||||
}
|
|
||||||
KeyCode::Delete if self.cursor < self.chars() => {
|
|
||||||
self.content.remove(self.cursor_byte_offset());
|
|
||||||
Some(TextLineReaction::Handled)
|
|
||||||
}
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
self.content.insert(self.cursor_byte_offset(), c);
|
|
||||||
self.move_cursor_right();
|
|
||||||
Some(TextLineReaction::Handled)
|
|
||||||
}
|
|
||||||
KeyCode::Esc => Some(TextLineReaction::Close),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue