diff --git a/Cargo.lock b/Cargo.lock index cfbc774..0448a4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,15 +17,6 @@ version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "atty" version = "0.2.14" @@ -91,12 +82,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - [[package]] name = "cc" version = "1.0.73" @@ -204,16 +189,11 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "cove-core", - "crossterm 0.22.1", + "crossterm", "futures", - "palette", "thiserror", "tokio", - "tokio-tungstenite", "toss", - "tui", - "unicode-width", ] [[package]] @@ -225,23 +205,6 @@ dependencies = [ "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]] name = "crossterm" version = "0.23.2" @@ -250,9 +213,10 @@ checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" dependencies = [ "bitflags", "crossterm_winapi", + "futures-core", "libc", - "mio 0.8.3", - "parking_lot 0.12.0", + "mio", + "parking_lot", "signal-hook", "signal-hook-mio", "winapi", @@ -309,15 +273,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "find-crate" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" -dependencies = [ - "toml", -] - [[package]] name = "fnv" version = "1.0.7" @@ -518,15 +473,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "itoa" version = "1.0.2" @@ -585,19 +531,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "mio" version = "0.8.3" @@ -610,33 +543,6 @@ dependencies = [ "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]] name = "num_cpus" version = "1.13.1" @@ -671,41 +577,6 @@ version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "parking_lot" version = "0.12.0" @@ -713,21 +584,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", -] - -[[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", + "parking_lot_core", ] [[package]] @@ -749,50 +606,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "pin-project-lite" version = "0.2.9" @@ -835,12 +648,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" version = "1.0.39" @@ -1090,8 +897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio 0.7.14", - "mio 0.8.3", + "mio", "signal-hook", ] @@ -1104,12 +910,6 @@ dependencies = [ "libc", ] -[[package]] -name = "siphasher" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" - [[package]] name = "slab" version = "0.4.6" @@ -1214,10 +1014,10 @@ dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.3", + "mio", "num_cpus", "once_cell", - "parking_lot 0.12.0", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1274,38 +1074,17 @@ dependencies = [ "webpki", ] -[[package]] -name = "toml" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" -dependencies = [ - "serde", -] - [[package]] name = "toss" 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 = [ - "crossterm 0.23.2", + "crossterm", "unicode-linebreak", "unicode-segmentation", "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]] name = "tungstenite" version = "0.16.0" diff --git a/cove-tui/Cargo.toml b/cove-tui/Cargo.toml index 92ebb15..3115fee 100644 --- a/cove-tui/Cargo.toml +++ b/cove-tui/Cargo.toml @@ -4,19 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] -anyhow = "1.0.53" -clap = { version = "3.1.0", features = ["derive"] } -cove-core = { path = "../cove-core" } -crossterm = { version = "0.22.1", features = ["event-stream"] } +anyhow = "1.0.57" +clap = { version = "3.1.18", features = ["derive"] } +crossterm = { version = "0.23.2", features = ["event-stream"] } futures = "0.3.21" -palette = "0.6.0" -# serde_json = "1.0.78" -thiserror = "1.0.30" -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" +thiserror = "1.0.31" +tokio = { version = "1.18.2", features = ["full"] } +toss = { git = "https://github.com/Garmelon/toss.git", rev = "33264b4aec27066e6abb7cc7d15bd680b43fcd5a" } diff --git a/cove-tui/src/client.rs b/cove-tui/src/client.rs deleted file mode 100644 index bbd7d92..0000000 --- a/cove-tui/src/client.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod cove; diff --git a/cove-tui/src/client/cove.rs b/cove-tui/src/client/cove.rs deleted file mode 100644 index 990e7f7..0000000 --- a/cove-tui/src/client/cove.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod conn; -pub mod room; diff --git a/cove-tui/src/client/cove/conn.rs b/cove-tui/src/client/cove/conn.rs deleted file mode 100644 index 7d4f4a2..0000000 --- a/cove-tui/src/client/cove/conn.rs +++ /dev/null @@ -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, -} - -impl Present { - fn session_map(sessions: &[Session]) -> HashMap { - 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), - 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, - 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>, - ev_tx: UnboundedSender, -} - -impl CoveConn { - // TODO Disallow modification via this MutexGuard - pub async fn state(&self) -> MutexGuard<'_, State> { - self.state.lock().await - } - - async fn cmd(&self, cmd: C) -> Result - where - C: Into, - Rpl: TryInto, - { - 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 { 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, -) -> (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) -} diff --git a/cove-tui/src/client/cove/room.rs b/cove-tui/src/client/cove/room.rs deleted file mode 100644 index 1fb9fdc..0000000 --- a/cove-tui/src/client/cove/room.rs +++ /dev/null @@ -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, -} - -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>, - /// 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, -} - -impl CoveRoom { - /// This method uses [`tokio::spawn`] and must thus be called in the context - /// of a tokio runtime. - pub fn new( - config: &'static Config, - name: String, - event_sender: UnboundedSender, - 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( - name: String, - mut ev_rx: UnboundedReceiver, - ev_tx: UnboundedSender, - 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>, 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; - } - } -} diff --git a/cove-tui/src/config.rs b/cove-tui/src/config.rs deleted file mode 100644 index 38955f3..0000000 --- a/cove-tui/src/config.rs +++ /dev/null @@ -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), - } - } -} diff --git a/cove-tui/src/main.rs b/cove-tui/src/main.rs index c9d60a2..1162517 100644 --- a/cove-tui/src/main.rs +++ b/cove-tui/src/main.rs @@ -1,46 +1,13 @@ -// TODO Make as few things async as necessary - #![warn(clippy::use_self)] -pub mod client; -mod config; -mod never; mod ui; -use std::io; - -use config::Config; -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::execute; -use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; -use tui::backend::CrosstermBackend; -use tui::Terminal; +use toss::terminal::Terminal; use ui::Ui; #[tokio::main] async fn main() -> anyhow::Result<()> { - let config = Box::leak(Box::new(Config::load())); - - 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?; - + let mut terminal = Terminal::new()?; + Ui::run(&mut terminal).await?; Ok(()) } diff --git a/cove-tui/src/never.rs b/cove-tui/src/never.rs deleted file mode 100644 index dffbe50..0000000 --- a/cove-tui/src/never.rs +++ /dev/null @@ -1,2 +0,0 @@ -// TODO Replace with `!` when it is stabilised -pub enum Never {} diff --git a/cove-tui/src/ui.rs b/cove-tui/src/ui.rs index 5e17461..17f8dc2 100644 --- a/cove-tui/src/ui.rs +++ b/cove-tui/src/ui.rs @@ -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::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 tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; -use tui::backend::CrosstermBackend; -use tui::layout::{Constraint, Direction, Layout, Rect}; -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; - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum RoomId { - Cove(String), -} +use toss::frame::{Frame, Pos}; +use toss::terminal::{Redraw, Terminal}; #[derive(Debug)] pub enum UiEvent { + Redraw, Term(Event), - Cove(String, CoveEvent), -} - -impl UiEvent { - fn cove(room: &str, event: CoveEvent) -> Self { - Self::Cove(room.to_string(), event) - } } enum EventHandleResult { @@ -55,49 +20,23 @@ enum EventHandleResult { } pub struct Ui { - config: &'static Config, event_tx: UnboundedSender, - - cove_rooms: HashMap, - room: Option, - - rooms_pane: PaneInfo, - users_pane: PaneInfo, - - overlay: Option, - - last_area: Rect, } impl Ui { - fn new(config: &'static Config, event_tx: UnboundedSender) -> Self { - Self { - config, - event_tx, - - cove_rooms: HashMap::new(), - room: None, - - rooms_pane: PaneInfo::default(), - users_pane: PaneInfo::default(), - - overlay: None, - - last_area: Rect::default(), - } + fn new(event_tx: UnboundedSender) -> Self { + Self { event_tx } } - pub async fn run( - config: &'static Config, - terminal: &mut Terminal, - ) -> anyhow::Result<()> { - let (event_tx, mut event_rx) = mpsc::unbounded_channel(); - let mut ui = Self::new(config, event_tx.clone()); + pub async fn run(terminal: &mut Terminal) -> anyhow::Result<()> { + let (event_tx, event_rx) = mpsc::unbounded_channel(); + let mut ui = Self::new(event_tx.clone()); - tokio::select! { - e = ui.run_main(terminal, &mut event_rx) => e, + let result = tokio::select! { + e = ui.run_main(terminal, event_tx.clone(), event_rx) => e, e = Self::shovel_crossterm_events(event_tx) => e, - } + }; + result } async fn shovel_crossterm_events(tx: UnboundedSender) -> anyhow::Result<()> { @@ -111,25 +50,17 @@ impl Ui { async fn run_main( &mut self, - terminal: &mut Terminal, - event_rx: &mut UnboundedReceiver, + terminal: &mut Terminal, + event_tx: UnboundedSender, + mut event_rx: UnboundedReceiver, ) -> anyhow::Result<()> { loop { // 1. Render current state terminal.autoresize()?; - - let mut frame = terminal.get_frame(); - self.last_area = frame.size(); - 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(); + self.render(terminal.frame()).await?; + if terminal.present()? == Redraw::Required { + event_tx.send(UiEvent::Redraw); + } // 2. Handle events (in batches) let mut event = match event_rx.recv().await { @@ -138,10 +69,10 @@ impl Ui { }; loop { let result = match event { + UiEvent::Redraw => EventHandleResult::Continue, 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::Resize(_, _)) => EventHandleResult::Continue, - UiEvent::Cove(name, event) => self.handle_cove_event(name, event).await?, }; match result { EventHandleResult::Continue => {} @@ -156,259 +87,22 @@ impl Ui { } } - async fn render(&mut self, frame: &mut Frame<'_, Backend>) -> anyhow::Result<()> { - let entire_area = frame.size(); - 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); - } - } - } + async fn render(&mut self, frame: &mut Frame) -> anyhow::Result<()> { + frame.write(Pos::new(0, 0), "Hello world!", ContentStyle::default()); 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 { - if let Some(result) = self.handle_key_event_for_overlay(event).await { - return result; - } - - 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; + match event.code { + KeyCode::Char('Q') => return EventHandleResult::Stop, + _ => {} } EventHandleResult::Continue } - async fn handle_key_event_for_overlay(&mut self, event: KeyEvent) -> Option { - 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 { - 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 { - 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 { - 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) } - - async fn handle_cove_event( - &mut self, - name: String, - event: CoveEvent, - ) -> anyhow::Result { - 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 { - 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)); - } - } } diff --git a/cove-tui/src/ui/cove.rs b/cove-tui/src/ui/cove.rs deleted file mode 100644 index d0a8e0d..0000000 --- a/cove-tui/src/ui/cove.rs +++ /dev/null @@ -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(&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(&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(&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(&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(&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(()) - } - } - } -} diff --git a/cove-tui/src/ui/cove/body.rs b/cove-tui/src/ui/cove/body.rs deleted file mode 100644 index 1c234cb..0000000 --- a/cove-tui/src/ui/cove/body.rs +++ /dev/null @@ -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, - }, - 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) { - match self { - Self::ChooseNick { prev_error, .. } => *prev_error = error, - _ => { - *self = Self::ChooseNick { - nick: TextLineState::default(), - prev_error: error, - } - } - } - } - - pub async fn render(&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 { - 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, - } - } -} diff --git a/cove-tui/src/ui/cove/users.rs b/cove-tui/src/ui/cove/users.rs deleted file mode 100644 index 2927a23..0000000 --- a/cove-tui/src/ui/cove/users.rs +++ /dev/null @@ -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, -} - -impl CoveUsers { - pub fn new(present: &Present) -> Self { - let mut users: Vec = iter::once(&present.session) - .chain(present.others.values()) - .map(<&Session as Into>::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::>() - .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); - } -} diff --git a/cove-tui/src/ui/input.rs b/cove-tui/src/ui/input.rs deleted file mode 100644 index b957f97..0000000 --- a/cove-tui/src/ui/input.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crossterm::event::KeyEvent; - -pub trait EventHandler { - type Reaction; - - fn handle_key(&mut self, event: KeyEvent) -> Option; - - // TODO Add method to show currently accepted keys for F1 help -} diff --git a/cove-tui/src/ui/layout.rs b/cove-tui/src/ui/layout.rs deleted file mode 100644 index 20eeeff..0000000 --- a/cove-tui/src/ui/layout.rs +++ /dev/null @@ -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 - } -} diff --git a/cove-tui/src/ui/overlays.rs b/cove-tui/src/ui/overlays.rs deleted file mode 100644 index 46030c8..0000000 --- a/cove-tui/src/ui/overlays.rs +++ /dev/null @@ -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), -} diff --git a/cove-tui/src/ui/overlays/switch_room.rs b/cove-tui/src/ui/overlays/switch_room.rs deleted file mode 100644 index 32fd5e0..0000000 --- a/cove-tui/src/ui/overlays/switch_room.rs +++ /dev/null @@ -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 { - 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() - } -} diff --git a/cove-tui/src/ui/pane.rs b/cove-tui/src/ui/pane.rs deleted file mode 100644 index 7807097..0000000 --- a/cove-tui/src/ui/pane.rs +++ /dev/null @@ -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, - } - } -} diff --git a/cove-tui/src/ui/room.rs b/cove-tui/src/ui/room.rs deleted file mode 100644 index ff128c2..0000000 --- a/cove-tui/src/ui/room.rs +++ /dev/null @@ -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, - }, - Messages, - FatalError(String), -} - -impl Main { - fn choose_nick() -> Self { - Self::ChooseNick { - nick: TextLineState::default(), - prev_error: None, - } - } - - fn fatal(s: S) -> Self { - Self::FatalError(s.to_string()) - } -} - -pub struct RoomInfo { - name: String, - room: Arc>, - main: Main, -} - -impl RoomInfo { - pub fn new(name: String, room: Arc>) -> 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(&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(&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(&mut self, frame: &mut Frame<'_, B>, area: Rect) { - if let Some(present) = self.room.lock().await.present() { - frame.render_widget(Users::new(present), area); - } - } -} diff --git a/cove-tui/src/ui/rooms.rs b/cove-tui/src/ui/rooms.rs deleted file mode 100644 index 9b34dbd..0000000 --- a/cove-tui/src/ui/rooms.rs +++ /dev/null @@ -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, - selected: Option, -} - -impl Rooms { - pub fn new(cove_rooms: &HashMap) -> Self { - let mut rooms = cove_rooms - .iter() - .map(|(name, _)| RoomInfo { name: name.clone() }) - .collect::>(); - 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); - } -} diff --git a/cove-tui/src/ui/styles.rs b/cove-tui/src/ui/styles.rs deleted file mode 100644 index 0925d06..0000000 --- a/cove-tui/src/ui/styles.rs +++ /dev/null @@ -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) -} diff --git a/cove-tui/src/ui/textline.rs b/cove-tui/src/ui/textline.rs deleted file mode 100644 index 12d6ddf..0000000 --- a/cove-tui/src/ui/textline.rs +++ /dev/null @@ -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::(); - 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 { - 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, - } - } -}