Compare commits
2 Commits
fdcf493478
...
mumble-tui
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b6e81a21d | |||
| 801a182e7c |
Generated
+262
-13
@@ -607,6 +607,12 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
@@ -839,6 +845,20 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa 1.0.15",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.9.0"
|
||||
@@ -1193,6 +1213,32 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"crossterm_winapi",
|
||||
"futures-core",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
@@ -1321,8 +1367,18 @@ version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.21.3",
|
||||
"darling_macro 0.21.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"darling_macro 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1338,13 +1394,37 @@ dependencies = [
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.21.3",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
@@ -2221,7 +2301,7 @@ version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.21.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
@@ -2369,6 +2449,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
@@ -2672,7 +2758,7 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"rustix 1.1.2",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
@@ -3050,6 +3136,17 @@ dependencies = [
|
||||
"ahash 0.8.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
@@ -3058,7 +3155,7 @@ checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3442,6 +3539,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.19.0"
|
||||
@@ -3460,6 +3566,19 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.21"
|
||||
@@ -3763,6 +3882,12 @@ dependencies = [
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
@@ -3909,6 +4034,15 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
@@ -4075,7 +4209,7 @@ version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"rustix 1.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4154,6 +4288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -4235,7 +4370,6 @@ dependencies = [
|
||||
"dasp_ring_buffer",
|
||||
"deep_filter",
|
||||
"dioxus-asset-resolver",
|
||||
"dioxus-signals",
|
||||
"etcetera",
|
||||
"futures",
|
||||
"futures-channel",
|
||||
@@ -4314,6 +4448,25 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mumble-web2-tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"color-eyre",
|
||||
"crossterm",
|
||||
"dioxus-core",
|
||||
"dioxus-signals",
|
||||
"futures",
|
||||
"futures-channel",
|
||||
"generational-box",
|
||||
"mumble-web2-client",
|
||||
"mumble-web2-common",
|
||||
"ratatui",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
@@ -5627,6 +5780,27 @@ dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cassowary",
|
||||
"compact_str 0.8.1",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"lru",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.5.2"
|
||||
@@ -6007,6 +6181,19 @@ dependencies = [
|
||||
"transpose",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.2"
|
||||
@@ -6016,7 +6203,7 @@ dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -6187,7 +6374,7 @@ dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"compact_str",
|
||||
"compact_str 0.9.0",
|
||||
"eyre",
|
||||
"futures-util",
|
||||
"http",
|
||||
@@ -6746,6 +6933,17 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
@@ -6964,6 +7162,34 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subsecond"
|
||||
version = "0.7.3"
|
||||
@@ -7158,7 +7384,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"rustix 1.1.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -7853,6 +8079,29 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
@@ -8735,7 +8984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"rustix",
|
||||
"rustix 1.1.2",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
|
||||
@@ -8752,7 +9001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
"rustix 1.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["client", "common", "gui", "proxy"]
|
||||
members = ["client", "common", "gui", "proxy", "tui"]
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
|
||||
+1
-1
@@ -66,7 +66,6 @@ etcetera = { version = "0.10.0", optional = true }
|
||||
|
||||
# Base Dependencies
|
||||
# ================
|
||||
dioxus-signals = "0.7.2"
|
||||
manganis = "0.7.2"
|
||||
once_cell = "1.19.0"
|
||||
asynchronous-codec = { workspace = true }
|
||||
@@ -114,6 +113,7 @@ tract-onnx = "=0.12.4"
|
||||
tract-pulse = "=0.12.4"
|
||||
|
||||
[features]
|
||||
embed-denoiser = []
|
||||
web = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
|
||||
+17
-9
@@ -1,8 +1,8 @@
|
||||
use dioxus_signals::{ReadableExt as _, Signal};
|
||||
use mime_guess::Mime;
|
||||
use mumble_web2_common::ProxyOverrides;
|
||||
use ordermap::OrderSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
pub type ChannelId = u32;
|
||||
@@ -197,19 +197,27 @@ impl ServerState {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub status: Signal<ConnectionState>,
|
||||
pub server: Signal<ServerState>,
|
||||
pub audio: Signal<AudioSettings>,
|
||||
pub trait Reactivity {
|
||||
type Signal<T>;
|
||||
|
||||
fn new<T: 'static>(value: T) -> Self::Signal<T>;
|
||||
fn read<T: 'static>(signal: &Self::Signal<T>) -> impl Deref<Target = T>;
|
||||
fn write<T: 'static>(signal: &Self::Signal<T>) -> impl DerefMut<Target = T>;
|
||||
}
|
||||
|
||||
impl fmt::Debug for State {
|
||||
pub struct State<R: Reactivity> {
|
||||
pub status: R::Signal<ConnectionState>,
|
||||
pub server: R::Signal<ServerState>,
|
||||
pub audio: R::Signal<AudioSettings>,
|
||||
}
|
||||
|
||||
impl<R: Reactivity> fmt::Debug for State<R> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("State")
|
||||
.field("status", &self.status.read())
|
||||
.field("server", &self.server.read())
|
||||
.field("status", &*R::read(&self.status))
|
||||
.field("server", &*R::read(&self.server))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedState = Arc<State>;
|
||||
pub type SharedState<R> = Arc<State<R>>;
|
||||
|
||||
+24
-6
@@ -1,15 +1,34 @@
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
|
||||
use df::tract::{DfParams, DfTract, RuntimeParams};
|
||||
use dioxus_asset_resolver::read_asset_bytes;
|
||||
use manganis::{asset, Asset};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::imp::SpawnHandle;
|
||||
|
||||
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
|
||||
#[cfg(not(feature = "embed-denoiser"))]
|
||||
async fn denoiser_model_bytes() -> color_eyre::Result<Cow<'static, [u8]>> {
|
||||
use color_eyre::eyre::eyre;
|
||||
use manganis::{asset, Asset};
|
||||
|
||||
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
|
||||
let bytes = dioxus_asset_resolver::read_asset_bytes(&DF_MODEL.to_string())
|
||||
.await
|
||||
.map_err(|err| eyre!("could not read denoising model: {err}"))?;
|
||||
Ok(Cow::Owned(bytes))
|
||||
}
|
||||
|
||||
#[cfg(feature = "embed-denoiser")]
|
||||
async fn denoiser_model_bytes() -> color_eyre::Result<Cow<'static, [u8]>> {
|
||||
static DF_MODEL: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/assets/DeepFilterNet3_ll_onnx.tar.gz"
|
||||
));
|
||||
Ok(Cow::Borrowed(DF_MODEL))
|
||||
}
|
||||
|
||||
// TODO: make this user configurable.
|
||||
static DEFAULT_NOISE_FLOOR: f32 = 0.001;
|
||||
// 200ms hold at 48kHz sample rate
|
||||
@@ -44,12 +63,11 @@ fn with_denoising_model<O>(spawn: &SpawnHandle, func: impl FnOnce(&mut DfTract)
|
||||
let cell = Arc::new(AtomicCell::new(None));
|
||||
let cell_task = cell.clone();
|
||||
*state = DenoisingModelState::Downloading(cell);
|
||||
let model = DF_MODEL.to_string();
|
||||
spawn.spawn(async move {
|
||||
let model_bytes = match read_asset_bytes(&model).await {
|
||||
let model_bytes = match denoiser_model_bytes().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
error!("could not read denoising model from \"{model}\": {e:?}");
|
||||
error!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::app::{Command, SharedState};
|
||||
use crate::Reactivity;
|
||||
use color_eyre::eyre::Error;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use mumble_protocol::control::ClientControlCodec;
|
||||
@@ -74,7 +75,7 @@ pub async fn network_connect(
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
state: SharedState,
|
||||
state: SharedState<impl Reactivity>,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::app::{Command, SharedState};
|
||||
use crate::Reactivity;
|
||||
use color_eyre::eyre::Error;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
@@ -28,7 +29,7 @@ impl super::PlatformInterface for DesktopPlatform {
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
state: SharedState,
|
||||
state: SharedState<impl Reactivity>,
|
||||
) -> Result<(), Error> {
|
||||
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::app::{Command, SharedState};
|
||||
use crate::Reactivity;
|
||||
use color_eyre::eyre::Error;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
@@ -24,7 +25,7 @@ impl super::PlatformInterface for MobilePlatform {
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
state: SharedState,
|
||||
state: SharedState<impl Reactivity>,
|
||||
) -> Result<(), Error> {
|
||||
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
use crate::app::{Command, SharedState};
|
||||
use crate::effects::AudioProcessor;
|
||||
use crate::Reactivity;
|
||||
use color_eyre::eyre::Error;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
@@ -83,7 +84,7 @@ pub trait PlatformInterface {
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
proxy_overrides: &ProxyOverrides,
|
||||
state: SharedState,
|
||||
state: SharedState<impl Reactivity>,
|
||||
) -> impl Future<Output = Result<(), Error>>;
|
||||
|
||||
/// Get server status (user count, version, etc.) for the given address.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// Stub implementation of the platform interface, so that we can
|
||||
/// `cargo check` without any --feature flags.
|
||||
use crate::{app::SharedState, effects::AudioProcessor};
|
||||
use crate::{app::SharedState, effects::AudioProcessor, Reactivity};
|
||||
use color_eyre::eyre::Error;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
@@ -25,7 +25,7 @@ impl super::PlatformInterface for StubPlatform {
|
||||
_username: String,
|
||||
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
||||
_overrides: &ProxyOverrides,
|
||||
_state: SharedState,
|
||||
_state: SharedState<impl Reactivity>,
|
||||
) -> impl Future<Output = Result<(), Error>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::app::{Command, SharedState};
|
||||
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
||||
use crate::Reactivity;
|
||||
use color_eyre::eyre::{bail, eyre, Error};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
@@ -112,7 +113,7 @@ impl super::PlatformInterface for WebPlatform {
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
state: SharedState,
|
||||
state: SharedState<impl Reactivity>,
|
||||
) -> Result<(), Error> {
|
||||
network_connect(address, username, event_rx, overrides, state).await
|
||||
}
|
||||
@@ -442,7 +443,7 @@ pub async fn network_connect(
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
state: SharedState,
|
||||
state: SharedState<impl Reactivity>,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
|
||||
+26
-24
@@ -3,11 +3,10 @@ use crate::AudioSettings;
|
||||
use crate::Chat;
|
||||
use crate::Command;
|
||||
use crate::ConnectionState;
|
||||
use crate::Reactivity;
|
||||
use asynchronous_codec::FramedRead;
|
||||
use asynchronous_codec::FramedWrite;
|
||||
use color_eyre::eyre::{bail, Error};
|
||||
use dioxus_signals::ReadableExt as _;
|
||||
use dioxus_signals::WritableExt as _;
|
||||
use futures::select;
|
||||
use futures::AsyncRead;
|
||||
use futures::AsyncWrite;
|
||||
@@ -36,7 +35,10 @@ use crate::imp::{
|
||||
Platform, PlatformInterface as _,
|
||||
};
|
||||
|
||||
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
|
||||
pub async fn network_entrypoint<X: Reactivity>(
|
||||
mut event_rx: UnboundedReceiver<Command>,
|
||||
state: SharedState<X>,
|
||||
) {
|
||||
loop {
|
||||
let Some(Command::Connect {
|
||||
address,
|
||||
@@ -47,16 +49,16 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state:
|
||||
panic!("did not receive connect command")
|
||||
};
|
||||
|
||||
*state.server.write_unchecked() = Default::default();
|
||||
*state.status.write_unchecked() = ConnectionState::Connecting;
|
||||
*X::write(&state.server) = Default::default();
|
||||
*X::write(&state.status) = ConnectionState::Connecting;
|
||||
if let Err(error) =
|
||||
Platform::network_connect(address, username, &mut event_rx, &config, state.clone())
|
||||
.await
|
||||
{
|
||||
error!("could not connect {:?}", error);
|
||||
*state.status.write_unchecked() = ConnectionState::Failed(error.to_string());
|
||||
*X::write(&state.status) = ConnectionState::Failed(error.to_string());
|
||||
} else {
|
||||
*state.status.write_unchecked() = ConnectionState::Disconnected;
|
||||
*X::write(&state.status) = ConnectionState::Disconnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,14 +78,14 @@ pub(crate) async fn sender_loop<W: AsyncWrite + Unpin + 'static>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
||||
pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static, X: Reactivity>(
|
||||
username: String,
|
||||
state: SharedState,
|
||||
state: SharedState<X>,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
mut outgoing: UnboundedSender<ControlPacket<Serverbound>>,
|
||||
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
||||
) -> Result<(), Error> {
|
||||
let audio_settings = state.audio.read().clone();
|
||||
let audio_settings = X::read(&state.audio).clone();
|
||||
|
||||
// Get version packet
|
||||
let version = match reader.next().await {
|
||||
@@ -190,14 +192,14 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn accept_command(
|
||||
fn accept_command<X: Reactivity>(
|
||||
command: Command,
|
||||
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
||||
audio: &mut AudioSystem,
|
||||
state: &State,
|
||||
state: &State<X>,
|
||||
) -> Result<(), Error> {
|
||||
use Command::*;
|
||||
let Some(session) = state.server.read().session else {
|
||||
let Some(session) = X::read(&state.server).session else {
|
||||
bail!("no session id")
|
||||
};
|
||||
|
||||
@@ -220,7 +222,7 @@ fn accept_command(
|
||||
};
|
||||
|
||||
{
|
||||
let mut server = state.server.write_unchecked();
|
||||
let mut server = X::write(&state.server);
|
||||
let Some(me) = server.session else {
|
||||
bail!("not signed in with a session id")
|
||||
};
|
||||
@@ -261,7 +263,7 @@ fn accept_command(
|
||||
};
|
||||
|
||||
{
|
||||
let mut server = state.server.write_unchecked();
|
||||
let mut server = X::write(&state.server);
|
||||
let Some(me) = server.session else {
|
||||
bail!("not signed in with a session id")
|
||||
};
|
||||
@@ -304,11 +306,11 @@ fn accept_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn accept_packet(
|
||||
fn accept_packet<X: Reactivity>(
|
||||
msg: ControlPacket<mumble_protocol::Clientbound>,
|
||||
audio_context: &mut AudioSystem,
|
||||
player_map: &mut HashMap<u32, AudioPlayer>,
|
||||
state: &State,
|
||||
state: &State<X>,
|
||||
) -> Result<(), Error> {
|
||||
match msg {
|
||||
ControlPacket::UDPTunnel(u) => {
|
||||
@@ -345,15 +347,15 @@ fn accept_packet(
|
||||
}
|
||||
}
|
||||
ControlPacket::ChannelState(u) => {
|
||||
let mut server = state.server.write_unchecked();
|
||||
let mut server = X::write(&state.server);
|
||||
server.channels_state.update_from_channel_state(&u);
|
||||
}
|
||||
ControlPacket::ChannelRemove(u) => {
|
||||
let mut server = state.server.write_unchecked();
|
||||
let mut server = X::write(&state.server);
|
||||
server.channels_state.update_from_channel_remove(&u);
|
||||
}
|
||||
ControlPacket::UserState(u) => {
|
||||
let mut server = state.server.write_unchecked();
|
||||
let mut server = X::write(&state.server);
|
||||
let server = &mut *server;
|
||||
let id = u.get_session();
|
||||
|
||||
@@ -397,7 +399,7 @@ fn accept_packet(
|
||||
}
|
||||
}
|
||||
ControlPacket::UserRemove(u) => {
|
||||
let mut server = state.server.write_unchecked();
|
||||
let mut server = X::write(&state.server);
|
||||
let id = u.get_session();
|
||||
if let Some(state) = server.users.remove(&id) {
|
||||
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||
@@ -406,7 +408,7 @@ fn accept_packet(
|
||||
}
|
||||
}
|
||||
ControlPacket::TextMessage(u) => {
|
||||
let mut server = state.server.write_unchecked();
|
||||
let mut server = X::write(&state.server);
|
||||
if u.has_message() {
|
||||
let text = u.get_message().to_string();
|
||||
server.chat.push(Chat {
|
||||
@@ -421,8 +423,8 @@ fn accept_packet(
|
||||
}
|
||||
}
|
||||
ControlPacket::ServerSync(u) => {
|
||||
*state.status.write_unchecked() = ConnectionState::Connected;
|
||||
let mut server = state.server.write_unchecked();
|
||||
*X::write(&state.status) = ConnectionState::Connected;
|
||||
let mut server = X::write(&state.server);
|
||||
if u.has_welcome_text() {
|
||||
let text = u.get_welcome_text().to_string();
|
||||
server.chat.push(Chat {
|
||||
|
||||
+24
-6
@@ -3,15 +3,34 @@
|
||||
use dioxus::prelude::*;
|
||||
use mumble_web2_client::{
|
||||
network_entrypoint, reqwest, AudioSettings, ChannelId, Command, ConfigSystem,
|
||||
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, ServerState,
|
||||
SharedState, State, UserId, UserState, VERSION,
|
||||
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, UserId,
|
||||
UserState, VERSION,
|
||||
};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{fmt, sync::Arc};
|
||||
use Command::*;
|
||||
use ConnectionState::*;
|
||||
|
||||
pub struct DioxusReactivity;
|
||||
|
||||
impl mumble_web2_client::Reactivity for DioxusReactivity {
|
||||
type Signal<T> = Signal<T>;
|
||||
|
||||
fn new<T: 'static>(value: T) -> Signal<T> {
|
||||
Signal::new(value)
|
||||
}
|
||||
|
||||
fn read<T: 'static>(signal: &Signal<T>) -> impl std::ops::Deref<Target = T> {
|
||||
signal.read_unchecked()
|
||||
}
|
||||
|
||||
fn write<T: 'static>(signal: &Signal<T>) -> impl std::ops::DerefMut<Target = T> {
|
||||
signal.write_unchecked()
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedState = mumble_web2_client::SharedState<DioxusReactivity>;
|
||||
pub type State = mumble_web2_client::State<DioxusReactivity>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UserIcon {
|
||||
Normal,
|
||||
@@ -519,8 +538,7 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
async move {
|
||||
let client = reqwest::Client::new();
|
||||
loop {
|
||||
*last_status.write_unchecked() =
|
||||
Some(Platform::get_status(&client, &addr).await);
|
||||
*last_status.write_unchecked() = Some(Platform::get_status(&client, &addr).await);
|
||||
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "mumble-web2-tui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
mumble-web2-client = { version = "0.1.0", path = "../client", features = ["desktop", "embed-denoiser"] }
|
||||
mumble-web2-common = { version = "0.1.0", path = "../common" }
|
||||
ratatui = "0.29"
|
||||
crossterm = { version = "0.28", features = ["event-stream"] }
|
||||
tokio = { version = "^1.41.1", features = ["rt", "macros"] }
|
||||
futures-channel = "^0.3.30"
|
||||
futures = "^0.3.30"
|
||||
dioxus-signals = "0.7.2"
|
||||
dioxus-core = "0.7.2"
|
||||
generational-box = "0.7.2"
|
||||
color-eyre = "^0.6.3"
|
||||
tracing-subscriber = { version = "^0.3.18", features = ["env-filter"] }
|
||||
tracing = "^0.1.40"
|
||||
+775
@@ -0,0 +1,775 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use dioxus_core::with_owner;
|
||||
use futures_channel::mpsc;
|
||||
use generational_box::Owner;
|
||||
use mumble_web2_client::{
|
||||
network_entrypoint, AudioSettings, ChannelId, Command, ConfigSystem,
|
||||
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, ServerState,
|
||||
UserState,
|
||||
};
|
||||
use mumble_web2_common::ProxyOverrides;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct RefCellReactivity;
|
||||
|
||||
impl mumble_web2_client::Reactivity for RefCellReactivity {
|
||||
type Signal<T> = RefCell<T>;
|
||||
|
||||
fn new<T: 'static>(value: T) -> Self::Signal<T> {
|
||||
RefCell::new(value)
|
||||
}
|
||||
|
||||
fn read<T: 'static>(signal: &Self::Signal<T>) -> impl std::ops::Deref<Target = T> {
|
||||
signal.borrow()
|
||||
}
|
||||
|
||||
fn write<T: 'static>(signal: &Self::Signal<T>) -> impl std::ops::DerefMut<Target = T> {
|
||||
signal.borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub type State = mumble_web2_client::State<RefCellReactivity>;
|
||||
pub type SharedState = mumble_web2_client::SharedState<RefCellReactivity>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App state (TUI-local, not shared with client)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Focus {
|
||||
Address,
|
||||
Username,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Pane {
|
||||
Channels,
|
||||
Chat,
|
||||
}
|
||||
|
||||
struct App {
|
||||
state: SharedState,
|
||||
tx: mpsc::UnboundedSender<Command>,
|
||||
config: ConfigSystem,
|
||||
overrides: ProxyOverrides,
|
||||
|
||||
// Login fields
|
||||
address: String,
|
||||
username: String,
|
||||
login_focus: Focus,
|
||||
|
||||
// Server view
|
||||
active_pane: Pane,
|
||||
chat_input: String,
|
||||
chat_focused: bool,
|
||||
channel_list: Vec<(ChannelId, u16)>, // (id, depth) - flattened tree for navigation
|
||||
channel_cursor: usize,
|
||||
|
||||
should_quit: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(
|
||||
state: SharedState,
|
||||
tx: mpsc::UnboundedSender<Command>,
|
||||
config: ConfigSystem,
|
||||
overrides: ProxyOverrides,
|
||||
) -> Self {
|
||||
let address = config
|
||||
.config_get::<String>("server_url")
|
||||
.or_else(|| overrides.proxy_url.clone())
|
||||
.unwrap_or_default();
|
||||
let username = config.config_get::<String>("username").unwrap_or_default();
|
||||
Self {
|
||||
state,
|
||||
tx,
|
||||
config,
|
||||
overrides,
|
||||
address,
|
||||
username,
|
||||
login_focus: Focus::Username,
|
||||
active_pane: Pane::Channels,
|
||||
chat_input: String::new(),
|
||||
chat_focused: false,
|
||||
channel_list: Vec::new(),
|
||||
channel_cursor: 0,
|
||||
should_quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn send(&self, cmd: Command) {
|
||||
let _ = self.tx.unbounded_send(cmd);
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
matches!(&*self.state.status.borrow(), ConnectionState::Connected)
|
||||
}
|
||||
|
||||
// Build a flat list of (channel_id, depth) by walking the tree.
|
||||
fn rebuild_channel_list(&mut self) {
|
||||
self.channel_list.clear();
|
||||
let server = self.state.server.borrow();
|
||||
// Find root channels (no parent)
|
||||
let mut roots: Vec<ChannelId> = server
|
||||
.channels_state
|
||||
.channels
|
||||
.iter()
|
||||
.filter(|(_, ch)| ch.parent.is_none())
|
||||
.map(|(&id, _)| id)
|
||||
.collect();
|
||||
roots.sort();
|
||||
for root in roots {
|
||||
Self::walk_channel(&mut self.channel_list, &server, root, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_channel(
|
||||
list: &mut Vec<(ChannelId, u16)>,
|
||||
server: &ServerState,
|
||||
id: ChannelId,
|
||||
depth: u16,
|
||||
) {
|
||||
list.push((id, depth));
|
||||
let Some(ch) = server.channels_state.channels.get(&id) else {
|
||||
return;
|
||||
};
|
||||
for &child in ch.children.iter() {
|
||||
Self::walk_channel(list, server, child, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// User icon helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn user_indicator(user: &UserState) -> &'static str {
|
||||
if user.deaf || user.self_deaf {
|
||||
"D"
|
||||
} else if user.mute || user.self_mute {
|
||||
"M"
|
||||
} else if user.suppress {
|
||||
"S"
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
}
|
||||
|
||||
fn user_style(user: &UserState) -> Style {
|
||||
if user.deaf || user.self_deaf {
|
||||
Style::default().fg(Color::Red)
|
||||
} else if user.mute || user.self_mute || user.suppress {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else {
|
||||
Style::default().fg(Color::Green)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn draw(frame: &mut Frame, app: &mut App) {
|
||||
if app.is_connected() {
|
||||
draw_server(frame, app);
|
||||
} else {
|
||||
draw_login(frame, app);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_login(frame: &mut Frame, app: &App) {
|
||||
let area = frame.area();
|
||||
|
||||
// Center a box
|
||||
let vert = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(10),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(area);
|
||||
let horiz = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(50),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(vert[1]);
|
||||
let box_area = horiz[1];
|
||||
|
||||
let block = Block::default()
|
||||
.title(" Mumble Web 2 ")
|
||||
.borders(Borders::ALL);
|
||||
let inner = block.inner(box_area);
|
||||
frame.render_widget(Clear, box_area);
|
||||
frame.render_widget(block, box_area);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1), // address label
|
||||
Constraint::Length(1), // address input
|
||||
Constraint::Length(1), // spacer
|
||||
Constraint::Length(1), // username label
|
||||
Constraint::Length(1), // username input
|
||||
Constraint::Length(1), // spacer
|
||||
Constraint::Length(1), // status / button hint
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(inner);
|
||||
|
||||
let status = &*app.state.status.borrow();
|
||||
|
||||
// Address
|
||||
if app.overrides.any_server {
|
||||
let label_style = if app.login_focus == Focus::Address {
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new("Server Address:").style(label_style),
|
||||
chunks[0],
|
||||
);
|
||||
let input_style = if app.login_focus == Focus::Address {
|
||||
Style::default().fg(Color::White)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!("> {}", app.address)).style(input_style),
|
||||
chunks[1],
|
||||
);
|
||||
}
|
||||
|
||||
// Username
|
||||
let label_style = if app.login_focus == Focus::Username {
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
frame.render_widget(Paragraph::new("Username:").style(label_style), chunks[3]);
|
||||
let input_style = if app.login_focus == Focus::Username {
|
||||
Style::default().fg(Color::White)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!("> {}", app.username)).style(input_style),
|
||||
chunks[4],
|
||||
);
|
||||
|
||||
// Status line
|
||||
let status_line = match status {
|
||||
ConnectionState::Disconnected => Line::from(Span::styled(
|
||||
"[Enter] Connect",
|
||||
Style::default().fg(Color::Green),
|
||||
)),
|
||||
ConnectionState::Connecting => Line::from(Span::styled(
|
||||
"Connecting...",
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
ConnectionState::Failed(msg) => Line::from(vec![
|
||||
Span::styled("Failed: ", Style::default().fg(Color::Red)),
|
||||
Span::raw(msg.clone()),
|
||||
Span::styled(" [Enter] Retry", Style::default().fg(Color::Green)),
|
||||
]),
|
||||
ConnectionState::Connected => unreachable!(),
|
||||
};
|
||||
frame.render_widget(Paragraph::new(status_line), chunks[6]);
|
||||
}
|
||||
|
||||
fn draw_server(frame: &mut Frame, app: &mut App) {
|
||||
app.rebuild_channel_list();
|
||||
let server = app.state.server.borrow();
|
||||
let audio = app.state.audio.borrow();
|
||||
|
||||
// Main layout: channels left, chat right, controls bottom
|
||||
let vert = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(3)])
|
||||
.split(frame.area());
|
||||
|
||||
let horiz = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(35), Constraint::Percentage(65)])
|
||||
.split(vert[0]);
|
||||
|
||||
// --- Channel tree ---
|
||||
let chan_block = Block::default()
|
||||
.title(" Channels ")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(if app.active_pane == Pane::Channels && !app.chat_focused {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
});
|
||||
|
||||
let mut items: Vec<ListItem> = Vec::new();
|
||||
for (i, &(ch_id, depth)) in app.channel_list.iter().enumerate() {
|
||||
let Some(ch) = server.channels_state.channels.get(&ch_id) else {
|
||||
continue;
|
||||
};
|
||||
let indent = " ".repeat(depth as usize);
|
||||
let marker = if ch.children.is_empty() { " " } else { "▾ " };
|
||||
let is_selected = i == app.channel_cursor;
|
||||
|
||||
let style = if is_selected {
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
let prefix = if is_selected { ">" } else { " " };
|
||||
|
||||
// Channel name line
|
||||
let mut lines = vec![Line::from(Span::styled(
|
||||
format!("{prefix}{indent}{marker}{}", ch.name),
|
||||
style,
|
||||
))];
|
||||
|
||||
// Users in this channel
|
||||
for &uid in ch.users.iter() {
|
||||
if let Some(user) = server.users.get(&uid) {
|
||||
let is_self = server.session == Some(uid);
|
||||
let ind = user_indicator(user);
|
||||
let u_style = if is_self {
|
||||
user_style(user).add_modifier(Modifier::UNDERLINED)
|
||||
} else {
|
||||
user_style(user)
|
||||
};
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {indent} [{ind}] {}", user.name),
|
||||
u_style,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
items.push(ListItem::new(lines));
|
||||
}
|
||||
|
||||
let channel_list = List::new(items).block(chan_block);
|
||||
frame.render_widget(channel_list, horiz[0]);
|
||||
|
||||
// --- Chat panel ---
|
||||
let chat_area = horiz[1];
|
||||
let chat_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(3)])
|
||||
.split(chat_area);
|
||||
|
||||
let chat_block = Block::default()
|
||||
.title(" Chat ")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(if app.active_pane == Pane::Chat && !app.chat_focused {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
});
|
||||
|
||||
let chat_lines: Vec<Line> = server
|
||||
.chat
|
||||
.iter()
|
||||
.map(|msg| {
|
||||
let sender = msg
|
||||
.sender
|
||||
.and_then(|uid| server.users.get(&uid))
|
||||
.map(|u| u.name.as_str())
|
||||
.unwrap_or("server");
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
format!("{sender}: "),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(&msg.raw),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Show last N lines that fit
|
||||
let chat_inner_height = chat_block.inner(chat_layout[0]).height as usize;
|
||||
let skip = chat_lines.len().saturating_sub(chat_inner_height);
|
||||
let visible_lines: Vec<Line> = chat_lines.into_iter().skip(skip).collect();
|
||||
|
||||
let chat_widget = Paragraph::new(visible_lines)
|
||||
.block(chat_block)
|
||||
.wrap(Wrap { trim: false });
|
||||
frame.render_widget(chat_widget, chat_layout[0]);
|
||||
|
||||
// Chat input
|
||||
let input_block = Block::default()
|
||||
.title(if app.chat_focused {
|
||||
" Input (Esc to cancel) "
|
||||
} else {
|
||||
" [t] to type "
|
||||
})
|
||||
.borders(Borders::ALL)
|
||||
.border_style(if app.chat_focused {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default()
|
||||
});
|
||||
let input_widget = Paragraph::new(app.chat_input.as_str()).block(input_block);
|
||||
frame.render_widget(input_widget, chat_layout[1]);
|
||||
|
||||
// --- Controls bar ---
|
||||
let this_user = server.this_user();
|
||||
let (self_mute, mute, suppress, self_deaf, deaf) = this_user
|
||||
.map(|u| (u.self_mute, u.mute, u.suppress, u.self_deaf, u.deaf))
|
||||
.unwrap_or_default();
|
||||
|
||||
let muted = mute || suppress || self_mute;
|
||||
let deafened = deaf || self_deaf;
|
||||
|
||||
let status_text = match &*app.state.status.borrow() {
|
||||
ConnectionState::Connected => "Connected",
|
||||
ConnectionState::Connecting => "Connecting",
|
||||
ConnectionState::Disconnected => "Disconnected",
|
||||
ConnectionState::Failed(_) => "Failed",
|
||||
};
|
||||
|
||||
let current_channel = this_user
|
||||
.and_then(|u| server.channels_state.channels.get(&u.channel))
|
||||
.map(|ch| ch.name.as_str())
|
||||
.unwrap_or("?");
|
||||
|
||||
let controls = Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" {status_text} "),
|
||||
Style::default().fg(Color::Green),
|
||||
),
|
||||
Span::styled(
|
||||
format!("#{current_channel} "),
|
||||
Style::default().fg(Color::White),
|
||||
),
|
||||
Span::raw("│ "),
|
||||
Span::styled(
|
||||
if muted { "[m]ute ✓ " } else { "[m]ute " },
|
||||
if muted {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else {
|
||||
Style::default()
|
||||
},
|
||||
),
|
||||
Span::styled(
|
||||
if deafened { "[d]eaf ✓ " } else { "[d]eaf " },
|
||||
if deafened {
|
||||
Style::default().fg(Color::Red)
|
||||
} else {
|
||||
Style::default()
|
||||
},
|
||||
),
|
||||
Span::styled(
|
||||
if audio.denoise {
|
||||
"[n]oise ✓ "
|
||||
} else {
|
||||
"[n]oise "
|
||||
},
|
||||
if audio.denoise {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
},
|
||||
),
|
||||
Span::raw("│ "),
|
||||
Span::styled("[q]uit", Style::default().fg(Color::DarkGray)),
|
||||
]);
|
||||
|
||||
let controls_block = Block::default().borders(Borders::ALL);
|
||||
let controls_widget = Paragraph::new(controls).block(controls_block);
|
||||
frame.render_widget(controls_widget, vert[1]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn handle_login_key(app: &mut App, code: KeyCode) {
|
||||
match code {
|
||||
KeyCode::Tab | KeyCode::BackTab => {
|
||||
app.login_focus = match app.login_focus {
|
||||
Focus::Address => Focus::Username,
|
||||
Focus::Username => {
|
||||
if app.overrides.any_server {
|
||||
Focus::Address
|
||||
} else {
|
||||
Focus::Username
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let status = &*app.state.status.borrow();
|
||||
if matches!(
|
||||
status,
|
||||
ConnectionState::Disconnected | ConnectionState::Failed(_)
|
||||
) {
|
||||
app.config.config_set::<String>("username", &app.username);
|
||||
if app.overrides.any_server {
|
||||
app.config.config_set::<String>("server_url", &app.address);
|
||||
}
|
||||
app.send(Command::Connect {
|
||||
address: app.address.clone(),
|
||||
username: app.username.clone(),
|
||||
config: app.overrides.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let field = match app.login_focus {
|
||||
Focus::Address => &mut app.address,
|
||||
Focus::Username => &mut app.username,
|
||||
};
|
||||
field.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
let field = match app.login_focus {
|
||||
Focus::Address => &mut app.address,
|
||||
Focus::Username => &mut app.username,
|
||||
};
|
||||
field.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.should_quit = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_server_key(app: &mut App, code: KeyCode) {
|
||||
if app.chat_focused {
|
||||
match code {
|
||||
KeyCode::Esc => {
|
||||
app.chat_focused = false;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if !app.chat_input.is_empty() {
|
||||
let server = app.state.server.borrow();
|
||||
if let Some(user) = server.this_user() {
|
||||
let channels = vec![user.channel];
|
||||
let markdown = std::mem::take(&mut app.chat_input);
|
||||
drop(server);
|
||||
app.send(Command::SendChat { markdown, channels });
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.chat_input.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.chat_input.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match code {
|
||||
KeyCode::Char('q') => {
|
||||
app.send(Command::Disconnect);
|
||||
app.should_quit = true;
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
let server = app.state.server.borrow();
|
||||
if let Some(user) = server.this_user() {
|
||||
if !user.mute && !user.suppress {
|
||||
let new_mute = !user.self_mute;
|
||||
drop(server);
|
||||
app.send(Command::SetMute { mute: new_mute });
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
let server = app.state.server.borrow();
|
||||
if let Some(user) = server.this_user() {
|
||||
if !user.deaf {
|
||||
let new_deaf = !user.self_deaf;
|
||||
drop(server);
|
||||
app.send(Command::SetDeaf { deaf: new_deaf });
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('n') => {
|
||||
let audio = app.state.audio.borrow().clone();
|
||||
let new_denoise = !audio.denoise;
|
||||
*app.state.audio.borrow_mut() = AudioSettings {
|
||||
denoise: new_denoise,
|
||||
};
|
||||
app.send(Command::UpdateAudioSettings(AudioSettings {
|
||||
denoise: new_denoise,
|
||||
}));
|
||||
app.config.config_set::<bool>("denoise", &new_denoise);
|
||||
}
|
||||
KeyCode::Char('t') => {
|
||||
app.chat_focused = true;
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
app.active_pane = match app.active_pane {
|
||||
Pane::Channels => Pane::Chat,
|
||||
Pane::Chat => Pane::Channels,
|
||||
};
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
if !app.channel_list.is_empty() {
|
||||
app.channel_cursor = (app.channel_cursor + 1).min(app.channel_list.len() - 1);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up => {
|
||||
app.channel_cursor = app.channel_cursor.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if let Some(&(ch_id, _)) = app.channel_list.get(app.channel_cursor) {
|
||||
let server = app.state.server.borrow();
|
||||
if let Some(uid) = server.session {
|
||||
drop(server);
|
||||
app.send(Command::EnterChannel {
|
||||
channel: ch_id,
|
||||
user: uid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(app: &mut App, ev: Event) {
|
||||
let Event::Key(key) = ev else { return };
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl-C always quits
|
||||
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
app.send(Command::Disconnect);
|
||||
app.should_quit = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if app.is_connected() {
|
||||
handle_server_key(app, key.code);
|
||||
} else {
|
||||
handle_login_key(app, key.code);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn init_file_logging() -> color_eyre::Result<()> {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
|
||||
let log_path = std::env::var("MUMBLE_TUI_LOG")
|
||||
.unwrap_or_else(|_| std::env::temp_dir().join("mumble-tui.log").to_string_lossy().into_owned());
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)?;
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_ansi(false)
|
||||
.with_env_filter(env_filter)
|
||||
.with_writer(file)
|
||||
.init();
|
||||
|
||||
eprintln!("logging to {log_path}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
init_file_logging()?;
|
||||
|
||||
// Use a single-threaded runtime since dioxus Signals are !Send.
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let local = tokio::task::LocalSet::new();
|
||||
|
||||
local.block_on(&rt, async {
|
||||
let config = ConfigSystem::new()?;
|
||||
let overrides = Platform::load_proxy_overrides().await.unwrap_or_default();
|
||||
|
||||
let state = SharedState::new(State {
|
||||
status: RefCell::new(ConnectionState::Disconnected),
|
||||
server: RefCell::new(Default::default()),
|
||||
audio: RefCell::new(AudioSettings {
|
||||
denoise: config.config_get::<bool>("denoise").unwrap_or(true),
|
||||
}),
|
||||
});
|
||||
|
||||
let (tx, rx) = mpsc::unbounded::<Command>();
|
||||
|
||||
// Spawn the network loop on the local task set (not Send-bound).
|
||||
let net_state = state.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
network_entrypoint(rx, net_state).await;
|
||||
});
|
||||
|
||||
// Setup terminal
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
let mut stdout = std::io::stdout();
|
||||
crossterm::execute!(
|
||||
stdout,
|
||||
crossterm::terminal::EnterAlternateScreen,
|
||||
crossterm::event::EnableMouseCapture
|
||||
)?;
|
||||
let backend = ratatui::backend::CrosstermBackend::new(stdout);
|
||||
let mut terminal = ratatui::Terminal::new(backend)?;
|
||||
|
||||
let mut app = App::new(state, tx, config, overrides);
|
||||
|
||||
// Event loop
|
||||
loop {
|
||||
terminal.draw(|f| draw(f, &mut app))?;
|
||||
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
|
||||
// Poll with a short timeout so we re-render when state changes.
|
||||
// Yield to the tokio runtime between polls so network tasks can progress.
|
||||
if crossterm::event::poll(std::time::Duration::from_millis(16))? {
|
||||
let ev = crossterm::event::read()?;
|
||||
handle_event(&mut app, ev);
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
// Restore terminal
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
crossterm::execute!(
|
||||
terminal.backend_mut(),
|
||||
crossterm::terminal::LeaveAlternateScreen,
|
||||
crossterm::event::DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user