Delete lots of stuff

This commit is contained in:
Joscha 2022-06-08 15:03:37 +02:00
parent 742e7725ab
commit 00c905eff5
22 changed files with 46 additions and 2076 deletions

241
Cargo.lock generated
View file

@ -17,15 +17,6 @@ version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -91,12 +82,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.73" version = "1.0.73"
@ -204,16 +189,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"cove-core", "crossterm",
"crossterm 0.22.1",
"futures", "futures",
"palette",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-tungstenite",
"toss", "toss",
"tui",
"unicode-width",
] ]
[[package]] [[package]]
@ -225,23 +205,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossterm"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
dependencies = [
"bitflags",
"crossterm_winapi",
"futures-core",
"libc",
"mio 0.7.14",
"parking_lot 0.11.2",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.23.2" version = "0.23.2"
@ -250,9 +213,10 @@ checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossterm_winapi", "crossterm_winapi",
"futures-core",
"libc", "libc",
"mio 0.8.3", "mio",
"parking_lot 0.12.0", "parking_lot",
"signal-hook", "signal-hook",
"signal-hook-mio", "signal-hook-mio",
"winapi", "winapi",
@ -309,15 +273,6 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "find-crate"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
dependencies = [
"toml",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -518,15 +473,6 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.2" version = "1.0.2"
@ -585,19 +531,6 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "mio"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [
"libc",
"log",
"miow",
"ntapi",
"winapi",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.3" version = "0.8.3"
@ -610,33 +543,6 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "miow"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
]
[[package]]
name = "ntapi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
dependencies = [
"winapi",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.1" version = "1.13.1"
@ -671,41 +577,6 @@ version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
[[package]]
name = "palette"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9735f7e1e51a3f740bacd5dc2724b61a7806f23597a8736e679f38ee3435d18"
dependencies = [
"approx",
"num-traits",
"palette_derive",
"phf",
]
[[package]]
name = "palette_derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7799c3053ea8a6d8a1193c7ba42f534e7863cf52e378a7f90406f4a645d33bad"
dependencies = [
"find-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.5",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.0" version = "0.12.0"
@ -713,21 +584,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
dependencies = [ dependencies = [
"lock_api", "lock_api",
"parking_lot_core 0.9.3", "parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
] ]
[[package]] [[package]]
@ -749,50 +606,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "phf"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37"
dependencies = [
"phf_macros",
"phf_shared",
"proc-macro-hack",
]
[[package]]
name = "phf_generator"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_macros"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "phf_shared"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.9" version = "0.2.9"
@ -835,12 +648,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.39" version = "1.0.39"
@ -1090,8 +897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [ dependencies = [
"libc", "libc",
"mio 0.7.14", "mio",
"mio 0.8.3",
"signal-hook", "signal-hook",
] ]
@ -1104,12 +910,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.6" version = "0.4.6"
@ -1214,10 +1014,10 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"memchr", "memchr",
"mio 0.8.3", "mio",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"parking_lot 0.12.0", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
@ -1274,38 +1074,17 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toss" name = "toss"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Garmelon/toss.git?rev=8a6b0f83edfa39617256725c263b01906eac037d#8a6b0f83edfa39617256725c263b01906eac037d" source = "git+https://github.com/Garmelon/toss.git?rev=33264b4aec27066e6abb7cc7d15bd680b43fcd5a#33264b4aec27066e6abb7cc7d15bd680b43fcd5a"
dependencies = [ dependencies = [
"crossterm 0.23.2", "crossterm",
"unicode-linebreak", "unicode-linebreak",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
] ]
[[package]]
name = "tui"
version = "0.17.0"
source = "git+https://github.com/Garmelon/tui-rs.git?rev=07952dc#07952dc5b98885347cc224ac3ea91d8e6329bb1a"
dependencies = [
"bitflags",
"cassowary",
"crossterm 0.22.1",
"unicode-segmentation",
"unicode-width",
]
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.16.0" version = "0.16.0"

View file

@ -4,19 +4,10 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.53" anyhow = "1.0.57"
clap = { version = "3.1.0", features = ["derive"] } clap = { version = "3.1.18", features = ["derive"] }
cove-core = { path = "../cove-core" } crossterm = { version = "0.23.2", features = ["event-stream"] }
crossterm = { version = "0.22.1", features = ["event-stream"] }
futures = "0.3.21" futures = "0.3.21"
palette = "0.6.0" thiserror = "1.0.31"
# serde_json = "1.0.78" tokio = { version = "1.18.2", features = ["full"] }
thiserror = "1.0.30" toss = { git = "https://github.com/Garmelon/toss.git", rev = "33264b4aec27066e6abb7cc7d15bd680b43fcd5a" }
tokio = { version = "1.16.1", features = ["full"] }
# tokio-stream = "0.1.8"
tokio-tungstenite = { version = "0.16.1", features = [
"rustls-tls-native-roots",
] }
toss = { git = "https://github.com/Garmelon/toss.git", rev = "8a6b0f83edfa39617256725c263b01906eac037d" }
tui = { git = "https://github.com/Garmelon/tui-rs.git", rev = "07952dc" }
unicode-width = "0.1.9"

View file

@ -1 +0,0 @@
pub mod cove;

View file

@ -1,2 +0,0 @@
pub mod conn;
pub mod room;

View file

@ -1,392 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use cove_core::conn::{self, ConnMaintenance, ConnRx, ConnTx};
use cove_core::packets::{
Cmd, IdentifyCmd, IdentifyRpl, JoinNtf, NickNtf, NickRpl, Ntf, Packet, PartNtf, RoomCmd,
RoomRpl, Rpl, SendNtf, SendRpl, WhoRpl,
};
use cove_core::replies::Replies;
use cove_core::{replies, Session, SessionId};
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::{Mutex, MutexGuard};
// TODO Split into "interacting" and "maintenance" parts?
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
CouldNotConnect(conn::Error),
#[error("{0}")]
Conn(#[from] conn::Error),
#[error("{0}")]
Reply(#[from] replies::Error),
#[error("invalid room: {0}")]
InvalidRoom(String),
#[error("invalid identity: {0}")]
InvalidIdentity(String),
#[error("maintenance aborted")]
MaintenanceAborted,
#[error("not connected")]
NotConnected,
#[error("incorrect reply type")]
IncorrectReplyType,
}
#[derive(Debug)]
pub enum Event {
StateChanged,
IdentificationRequired,
// TODO Add events for joining, parting, sending, ...
}
pub struct Present {
pub session: Session,
pub others: HashMap<SessionId, Session>,
}
impl Present {
fn session_map(sessions: &[Session]) -> HashMap<SessionId, Session> {
sessions
.iter()
.map(|session| (session.id, session.clone()))
.collect()
}
fn new(session: &Session, others: &[Session]) -> Self {
Self {
session: session.clone(),
others: Self::session_map(others),
}
}
fn update(&mut self, session: &Session, others: &[Session]) {
self.session = session.clone();
self.others = Self::session_map(others);
}
fn update_session(&mut self, session: &Session) {
self.session = session.clone();
}
fn join(&mut self, who: Session) {
self.others.insert(who.id, who);
}
fn nick(&mut self, who: Session) {
self.others.insert(who.id, who);
}
fn part(&mut self, who: Session) {
self.others.remove(&who.id);
}
}
pub enum Status {
ChoosingRoom,
Identifying,
IdRequired(Option<String>),
Present(Present),
}
impl Status {
fn present(&self) -> Option<&Present> {
match self {
Status::Present(present) => Some(present),
Status::ChoosingRoom | Status::Identifying | Status::IdRequired(_) => None,
}
}
fn present_mut(&mut self) -> Option<&mut Present> {
match self {
Status::Present(present) => Some(present),
Status::ChoosingRoom | Status::Identifying | Status::IdRequired(_) => None,
}
}
}
pub struct Connected {
tx: ConnTx,
next_id: u64,
replies: Replies<u64, Rpl>,
status: Status,
}
impl Connected {
fn new(tx: ConnTx, timeout: Duration) -> Self {
Self {
tx,
next_id: 0,
replies: Replies::new(timeout),
status: Status::ChoosingRoom,
}
}
pub fn status(&self) -> &Status {
&self.status
}
pub fn present(&self) -> Option<&Present> {
self.status.present()
}
}
// The warning about enum variant sizes shouldn't matter since a connection will
// spend most its time in the Connected state anyways.
#[allow(clippy::large_enum_variant)]
pub enum State {
Connecting,
Connected(Connected),
// TODO Include reason for stop
Stopped,
}
impl State {
pub fn connected(&self) -> Option<&Connected> {
match self {
Self::Connected(connected) => Some(connected),
Self::Connecting | Self::Stopped => None,
}
}
pub fn connected_mut(&mut self) -> Option<&mut Connected> {
match self {
Self::Connected(connected) => Some(connected),
Self::Connecting | Self::Stopped => None,
}
}
pub fn present(&self) -> Option<&Present> {
self.connected()?.present()
}
}
#[derive(Clone)]
pub struct CoveConn {
state: Arc<Mutex<State>>,
ev_tx: UnboundedSender<Event>,
}
impl CoveConn {
// TODO Disallow modification via this MutexGuard
pub async fn state(&self) -> MutexGuard<'_, State> {
self.state.lock().await
}
async fn cmd<C, R>(&self, cmd: C) -> Result<R, Error>
where
C: Into<Cmd>,
Rpl: TryInto<R>,
{
let pending_reply = {
let mut state = self.state.lock().await;
let mut connected = state.connected_mut().ok_or(Error::NotConnected)?;
let id = connected.next_id;
connected.next_id += 1;
let pending_reply = connected.replies.wait_for(id);
connected.tx.send(&Packet::cmd(id, cmd.into()))?;
pending_reply
};
let rpl = pending_reply.get().await?;
let rpl_value = rpl.try_into().map_err(|_| Error::IncorrectReplyType)?;
Ok(rpl_value)
}
/// Attempt to identify with a nick and identity. Does nothing if the room
/// doesn't require verification.
///
/// This method is intended to be called whenever a CoveConn user suspects
/// identification to be necessary. It has little overhead.
pub async fn identify(&self, nick: &str, identity: &str) {
{
let mut state = self.state.lock().await;
if let Some(connected) = state.connected_mut() {
if let Status::IdRequired(_) = connected.status {
connected.status = Status::Identifying;
let _ = self.ev_tx.send(Event::StateChanged);
} else {
return;
}
} else {
return;
}
}
let conn = self.clone();
let nick = nick.to_string();
let identity = identity.to_string();
tokio::spawn(async move {
// There's no need for a second locking block, or for us to see the
// result of this command. CoveConnMt::run will set the connection's
// status as appropriate.
conn.cmd::<IdentifyCmd, IdentifyRpl>(IdentifyCmd { nick, identity })
.await
});
}
}
/// Maintenance for a [`CoveConn`].
pub struct CoveConnMt {
url: String,
room: String,
timeout: Duration,
conn: CoveConn,
}
impl CoveConnMt {
pub async fn run(self) -> Result<(), Error> {
let (tx, rx, mt) = match Self::connect(&self.url, self.timeout).await {
Ok(conn) => conn,
Err(e) => {
*self.conn.state.lock().await = State::Stopped;
let _ = self.conn.ev_tx.send(Event::StateChanged);
return Err(Error::CouldNotConnect(e));
}
};
*self.conn.state.lock().await = State::Connected(Connected::new(tx, self.timeout));
let _ = self.conn.ev_tx.send(Event::StateChanged);
tokio::spawn(Self::join_room(self.conn.clone(), self.room));
let result = tokio::select! {
result = Self::recv(&self.conn, rx) => result,
_ = mt.perform() => Err(Error::MaintenanceAborted),
};
*self.conn.state.lock().await = State::Stopped;
let _ = self.conn.ev_tx.send(Event::StateChanged);
result
}
async fn connect(
url: &str,
timeout: Duration,
) -> Result<(ConnTx, ConnRx, ConnMaintenance), conn::Error> {
let stream = tokio_tungstenite::connect_async(url).await?.0;
let conn = conn::new(stream, timeout);
Ok(conn)
}
async fn join_room(conn: CoveConn, name: String) -> Result<(), Error> {
let _: RoomRpl = conn.cmd(RoomCmd { name }).await?;
Ok(())
}
async fn recv(conn: &CoveConn, mut rx: ConnRx) -> Result<(), Error> {
while let Some(packet) = rx.recv().await? {
match packet {
Packet::Cmd { .. } => {} // Ignore commands, the server shouldn't send any
Packet::Rpl { id, rpl } => Self::on_rpl(conn, id, rpl).await?,
Packet::Ntf { ntf } => Self::on_ntf(conn, ntf).await?,
}
}
Ok(())
}
async fn on_rpl(conn: &CoveConn, id: u64, rpl: Rpl) -> Result<(), Error> {
let mut state = conn.state.lock().await;
let connected = match state.connected_mut() {
Some(connected) => connected,
None => return Ok(()),
};
match &rpl {
Rpl::Room(RoomRpl::Success) => {
connected.status = Status::IdRequired(None);
let _ = conn.ev_tx.send(Event::IdentificationRequired);
}
Rpl::Room(RoomRpl::InvalidRoom { reason }) => {
return Err(Error::InvalidRoom(reason.clone()))
}
Rpl::Identify(IdentifyRpl::Success { you, others, .. }) => {
connected.status = Status::Present(Present::new(you, others));
let _ = conn.ev_tx.send(Event::StateChanged);
}
Rpl::Identify(IdentifyRpl::InvalidNick { reason }) => {
connected.status = Status::IdRequired(Some(reason.clone()));
let _ = conn.ev_tx.send(Event::IdentificationRequired);
}
Rpl::Identify(IdentifyRpl::InvalidIdentity { reason }) => {
return Err(Error::InvalidIdentity(reason.clone()))
}
Rpl::Nick(NickRpl::Success { you }) => {
if let Some(present) = connected.status.present_mut() {
present.update_session(you);
let _ = conn.ev_tx.send(Event::StateChanged);
}
}
Rpl::Nick(NickRpl::InvalidNick { reason: _ }) => {}
Rpl::Send(SendRpl::Success { message }) => {
// TODO Add message to message store or send an event
}
Rpl::Send(SendRpl::InvalidContent { reason: _ }) => {}
Rpl::Who(WhoRpl { you, others }) => {
if let Some(present) = connected.status.present_mut() {
present.update(you, others);
let _ = conn.ev_tx.send(Event::StateChanged);
}
}
}
connected.replies.complete(&id, rpl);
Ok(())
}
async fn on_ntf(conn: &CoveConn, ntf: Ntf) -> Result<(), Error> {
let mut state = conn.state.lock().await;
let connected = match state.connected_mut() {
Some(connected) => connected,
None => return Ok(()),
};
match ntf {
Ntf::Join(JoinNtf { who }) => {
if let Some(present) = connected.status.present_mut() {
present.join(who);
let _ = conn.ev_tx.send(Event::StateChanged);
}
}
Ntf::Nick(NickNtf { who }) => {
if let Some(present) = connected.status.present_mut() {
present.nick(who);
let _ = conn.ev_tx.send(Event::StateChanged);
}
}
Ntf::Part(PartNtf { who }) => {
if let Some(present) = connected.status.present_mut() {
present.part(who);
let _ = conn.ev_tx.send(Event::StateChanged);
}
}
Ntf::Send(SendNtf { message }) => {
// TODO Add message to message store or send an event
}
}
Ok(())
}
}
pub fn new(
url: String,
room: String,
timeout: Duration,
ev_tx: UnboundedSender<Event>,
) -> (CoveConn, CoveConnMt) {
let conn = CoveConn {
state: Arc::new(Mutex::new(State::Connecting)),
ev_tx,
};
let mt = CoveConnMt {
url,
room,
timeout,
conn,
};
(mt.conn.clone(), mt)
}

View file

@ -1,144 +0,0 @@
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::sync::oneshot::{self, Sender};
use tokio::sync::{Mutex, MutexGuard};
use crate::config::Config;
use crate::never::Never;
use super::conn::{self, CoveConn, CoveConnMt, Event};
struct ConnConfig {
url: String,
room: String,
timeout: Duration,
ev_tx: UnboundedSender<Event>,
}
impl ConnConfig {
fn new_conn(&self) -> (CoveConn, CoveConnMt) {
conn::new(
self.url.clone(),
self.room.clone(),
self.timeout,
self.ev_tx.clone(),
)
}
}
pub struct CoveRoom {
name: String,
conn: Arc<Mutex<CoveConn>>,
/// Once this is dropped, all other room-related tasks, connections and
/// values are cleaned up. It is never used to send actual values.
#[allow(dead_code)]
dead_mans_switch: Sender<Never>,
}
impl CoveRoom {
/// This method uses [`tokio::spawn`] and must thus be called in the context
/// of a tokio runtime.
pub fn new<E, F>(
config: &'static Config,
name: String,
event_sender: UnboundedSender<E>,
convert_event: F,
) -> Self
where
E: Send + 'static,
F: Fn(&str, Event) -> E + Send + 'static,
{
let (ev_tx, ev_rx) = mpsc::unbounded_channel();
let (tx, rx) = oneshot::channel();
let conf = ConnConfig {
ev_tx,
url: config.cove_url.to_string(),
room: name.clone(),
timeout: config.timeout,
};
let (conn, mt) = conf.new_conn();
let room = Self {
name: name.clone(),
conn: Arc::new(Mutex::new(conn)),
dead_mans_switch: tx,
};
// Spawned separately because otherwise, the last few elements before a
// connection is closed might not get shoveled.
tokio::spawn(Self::shovel_events(
name,
ev_rx,
event_sender,
convert_event,
));
let conn_clone = room.conn.clone();
tokio::spawn(async move {
tokio::select! {
_ = rx => {} // Watch dead man's switch
_ = Self::run(conn_clone, mt, conf) => {}
}
});
room
}
pub fn name(&self) -> &str {
&self.name
}
pub async fn identify(&self, nick: &str, identity: &str) {
self.conn().await.identify(nick, identity).await;
}
// TODO Disallow modification via this MutexGuard
pub async fn conn(&self) -> MutexGuard<'_, CoveConn> {
self.conn.lock().await
}
async fn shovel_events<E>(
name: String,
mut ev_rx: UnboundedReceiver<Event>,
ev_tx: UnboundedSender<E>,
convert_event: impl Fn(&str, Event) -> E,
) {
while let Some(event) = ev_rx.recv().await {
let event = convert_event(&name, event);
if ev_tx.send(event).is_err() {
break;
}
}
}
/// Background task to connect to a room and stay connected.
async fn run(conn: Arc<Mutex<CoveConn>>, mut mt: CoveConnMt, conf: ConnConfig) {
// We have successfully connected to the url before. Errors while
// connecting are probably not our fault and we should try again later.
let mut url_exists = false;
loop {
match mt.run().await {
Err(conn::Error::CouldNotConnect(_)) if url_exists => {
// TODO Exponential backoff?
tokio::time::sleep(Duration::from_secs(10)).await;
}
// TODO Note these errors somewhere in the room state
Err(conn::Error::CouldNotConnect(_)) => return,
Err(conn::Error::InvalidRoom(_)) => return,
Err(conn::Error::InvalidIdentity(_)) => return,
_ => {}
}
url_exists = true;
// TODO Clean up with restructuring assignments later?
let (new_conn, new_mt) = conf.new_conn();
*conn.lock().await = new_conn;
mt = new_mt;
}
}
}

View file

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

View file

@ -1,46 +1,13 @@
// TODO Make as few things async as necessary
#![warn(clippy::use_self)] #![warn(clippy::use_self)]
pub mod client;
mod config;
mod never;
mod ui; mod ui;
use std::io; use toss::terminal::Terminal;
use config::Config;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use tui::backend::CrosstermBackend;
use tui::Terminal;
use ui::Ui; use ui::Ui;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let config = Box::leak(Box::new(Config::load())); let mut terminal = Terminal::new()?;
Ui::run(&mut terminal).await?;
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
crossterm::terminal::enable_raw_mode()?;
execute!(
terminal.backend_mut(),
EnterAlternateScreen,
EnableMouseCapture
)?;
// Defer error handling so the terminal always gets restored properly
let result = Ui::run(config, &mut terminal).await;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
crossterm::terminal::disable_raw_mode()?;
result?;
Ok(()) Ok(())
} }

View file

@ -1,2 +0,0 @@
// TODO Replace with `!` when it is stabilised
pub enum Never {}

View file

@ -1,52 +1,17 @@
mod cove;
mod input;
mod layout;
mod overlays;
mod pane;
mod rooms;
mod styles;
mod textline;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::io::Stdout;
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, MouseEvent};
use crossterm::style::ContentStyle;
use futures::StreamExt; use futures::StreamExt;
use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tui::backend::CrosstermBackend; use toss::frame::{Frame, Pos};
use tui::layout::{Constraint, Direction, Layout, Rect}; use toss::terminal::{Redraw, Terminal};
use tui::{Frame, Terminal};
use crate::client::cove::conn::Event as CoveEvent;
use crate::client::cove::room::CoveRoom;
use crate::config::Config;
use crate::ui::overlays::OverlayReaction;
use self::cove::CoveUi;
use self::input::EventHandler;
use self::overlays::{Overlay, SwitchRoom, SwitchRoomState};
use self::pane::PaneInfo;
use self::rooms::Rooms;
pub type Backend = CrosstermBackend<Stdout>;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum RoomId {
Cove(String),
}
#[derive(Debug)] #[derive(Debug)]
pub enum UiEvent { pub enum UiEvent {
Redraw,
Term(Event), Term(Event),
Cove(String, CoveEvent),
}
impl UiEvent {
fn cove(room: &str, event: CoveEvent) -> Self {
Self::Cove(room.to_string(), event)
}
} }
enum EventHandleResult { enum EventHandleResult {
@ -55,49 +20,23 @@ enum EventHandleResult {
} }
pub struct Ui { pub struct Ui {
config: &'static Config,
event_tx: UnboundedSender<UiEvent>, event_tx: UnboundedSender<UiEvent>,
cove_rooms: HashMap<String, CoveUi>,
room: Option<RoomId>,
rooms_pane: PaneInfo,
users_pane: PaneInfo,
overlay: Option<Overlay>,
last_area: Rect,
} }
impl Ui { impl Ui {
fn new(config: &'static Config, event_tx: UnboundedSender<UiEvent>) -> Self { fn new(event_tx: UnboundedSender<UiEvent>) -> Self {
Self { Self { event_tx }
config,
event_tx,
cove_rooms: HashMap::new(),
room: None,
rooms_pane: PaneInfo::default(),
users_pane: PaneInfo::default(),
overlay: None,
last_area: Rect::default(),
}
} }
pub async fn run( pub async fn run(terminal: &mut Terminal) -> anyhow::Result<()> {
config: &'static Config, let (event_tx, event_rx) = mpsc::unbounded_channel();
terminal: &mut Terminal<Backend>, let mut ui = Self::new(event_tx.clone());
) -> anyhow::Result<()> {
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
let mut ui = Self::new(config, event_tx.clone());
tokio::select! { let result = tokio::select! {
e = ui.run_main(terminal, &mut event_rx) => e, e = ui.run_main(terminal, event_tx.clone(), event_rx) => e,
e = Self::shovel_crossterm_events(event_tx) => e, e = Self::shovel_crossterm_events(event_tx) => e,
} };
result
} }
async fn shovel_crossterm_events(tx: UnboundedSender<UiEvent>) -> anyhow::Result<()> { async fn shovel_crossterm_events(tx: UnboundedSender<UiEvent>) -> anyhow::Result<()> {
@ -111,25 +50,17 @@ impl Ui {
async fn run_main( async fn run_main(
&mut self, &mut self,
terminal: &mut Terminal<Backend>, terminal: &mut Terminal,
event_rx: &mut UnboundedReceiver<UiEvent>, event_tx: UnboundedSender<UiEvent>,
mut event_rx: UnboundedReceiver<UiEvent>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
loop { loop {
// 1. Render current state // 1. Render current state
terminal.autoresize()?; terminal.autoresize()?;
self.render(terminal.frame()).await?;
let mut frame = terminal.get_frame(); if terminal.present()? == Redraw::Required {
self.last_area = frame.size(); event_tx.send(UiEvent::Redraw);
self.render(&mut frame).await?; }
// Do a little dance to please the borrow checker
let cursor = frame.cursor();
drop(frame);
terminal.flush()?;
terminal.set_cursor_opt(cursor)?; // Must happen after flush
terminal.flush_backend()?;
terminal.swap_buffers();
// 2. Handle events (in batches) // 2. Handle events (in batches)
let mut event = match event_rx.recv().await { let mut event = match event_rx.recv().await {
@ -138,10 +69,10 @@ impl Ui {
}; };
loop { loop {
let result = match event { let result = match event {
UiEvent::Redraw => EventHandleResult::Continue,
UiEvent::Term(Event::Key(event)) => self.handle_key_event(event).await, UiEvent::Term(Event::Key(event)) => self.handle_key_event(event).await,
UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?, UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?,
UiEvent::Term(Event::Resize(_, _)) => EventHandleResult::Continue, UiEvent::Term(Event::Resize(_, _)) => EventHandleResult::Continue,
UiEvent::Cove(name, event) => self.handle_cove_event(name, event).await?,
}; };
match result { match result {
EventHandleResult::Continue => {} EventHandleResult::Continue => {}
@ -156,259 +87,22 @@ impl Ui {
} }
} }
async fn render(&mut self, frame: &mut Frame<'_, Backend>) -> anyhow::Result<()> { async fn render(&mut self, frame: &mut Frame) -> anyhow::Result<()> {
let entire_area = frame.size(); frame.write(Pos::new(0, 0), "Hello world!", ContentStyle::default());
let areas = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(self.rooms_pane.width()),
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
Constraint::Length(self.users_pane.width()),
])
.split(entire_area);
let rooms_pane_area = areas[0];
let rooms_pane_border = areas[1];
let main_pane_area = areas[2];
let users_pane_border = areas[3];
let users_pane_area = areas[4];
// Main pane and users pane
self.render_room(frame, main_pane_area, users_pane_area)
.await;
// Rooms pane
let mut rooms = Rooms::new(&self.cove_rooms);
if let Some(RoomId::Cove(name)) = &self.room {
rooms = rooms.select(name);
}
frame.render_widget(rooms, rooms_pane_area);
// Pane borders and width
self.rooms_pane.restrict_width(rooms_pane_area.width);
frame.render_widget(self.rooms_pane.border(), rooms_pane_border);
self.users_pane.restrict_width(users_pane_area.width);
frame.render_widget(self.users_pane.border(), users_pane_border);
// Overlay
if let Some(overlay) = &mut self.overlay {
match overlay {
Overlay::SwitchRoom(state) => {
frame.render_stateful_widget(SwitchRoom, entire_area, state);
let (x, y) = state.last_cursor_pos();
frame.set_cursor(x, y);
}
}
}
Ok(()) Ok(())
} }
async fn render_room(
&mut self,
frame: &mut Frame<'_, Backend>,
main_pane_area: Rect,
users_pane_area: Rect,
) {
match &self.room {
Some(RoomId::Cove(name)) => {
if let Some(ui) = self.cove_rooms.get_mut(name) {
ui.render_main(frame, main_pane_area).await;
ui.render_users(frame, users_pane_area).await;
} else {
self.room = None;
}
}
None => {
// TODO Render welcome screen
}
}
}
async fn handle_key_event(&mut self, event: KeyEvent) -> EventHandleResult { async fn handle_key_event(&mut self, event: KeyEvent) -> EventHandleResult {
if let Some(result) = self.handle_key_event_for_overlay(event).await { match event.code {
return result; KeyCode::Char('Q') => return EventHandleResult::Stop,
} _ => {}
if let Some(result) = self.handle_key_event_for_main_panel(event).await {
return result;
}
if let Some(result) = self.handle_key_event_for_ui(event).await {
return result;
} }
EventHandleResult::Continue EventHandleResult::Continue
} }
async fn handle_key_event_for_overlay(&mut self, event: KeyEvent) -> Option<EventHandleResult> {
if let Some(overlay) = &mut self.overlay {
let reaction = match overlay {
Overlay::SwitchRoom(state) => state.handle_key(event),
};
match reaction {
Some(OverlayReaction::Handled) => {}
Some(OverlayReaction::Close) => self.overlay = None,
Some(OverlayReaction::SwitchRoom(id)) => {
self.overlay = None;
self.switch_to_room(id);
}
None => {}
}
Some(EventHandleResult::Continue)
} else {
None
}
}
async fn handle_key_event_for_main_panel(
&mut self,
event: KeyEvent,
) -> Option<EventHandleResult> {
match &self.room {
Some(RoomId::Cove(name)) => {
if let Some(ui) = self.cove_rooms.get_mut(name) {
ui.handle_key(event).await;
Some(EventHandleResult::Continue)
} else {
None
}
}
None => None,
}
}
async fn handle_key_event_for_ui(&mut self, event: KeyEvent) -> Option<EventHandleResult> {
match event.code {
KeyCode::Char('Q') => Some(EventHandleResult::Stop),
KeyCode::Char('s') => {
self.overlay = Some(Overlay::SwitchRoom(SwitchRoomState::default()));
Some(EventHandleResult::Continue)
}
KeyCode::Char('J') => {
self.switch_to_next_room();
Some(EventHandleResult::Continue)
}
KeyCode::Char('K') => {
self.switch_to_prev_room();
Some(EventHandleResult::Continue)
}
KeyCode::Char('D') => {
self.remove_current_room();
Some(EventHandleResult::Continue)
}
_ => None,
}
}
async fn handle_mouse_event(&mut self, event: MouseEvent) -> anyhow::Result<EventHandleResult> { async fn handle_mouse_event(&mut self, event: MouseEvent) -> anyhow::Result<EventHandleResult> {
let rooms_width = event.column;
let users_width = self.last_area.width - event.column - 1;
let rooms_hover = rooms_width == self.rooms_pane.width();
let users_hover = users_width == self.users_pane.width();
match event.kind {
MouseEventKind::Moved => {
self.rooms_pane.hover(rooms_hover);
self.users_pane.hover(users_hover);
}
MouseEventKind::Down(_) => {
self.rooms_pane.drag(rooms_hover);
self.users_pane.drag(users_hover);
}
MouseEventKind::Up(_) => {
self.rooms_pane.drag(false);
self.users_pane.drag(false);
}
MouseEventKind::Drag(_) => {
self.rooms_pane.drag_to(rooms_width);
self.users_pane.drag_to(users_width);
}
// MouseEventKind::ScrollDown => todo!(),
// MouseEventKind::ScrollUp => todo!(),
_ => {}
}
Ok(EventHandleResult::Continue) Ok(EventHandleResult::Continue)
} }
async fn handle_cove_event(
&mut self,
name: String,
event: CoveEvent,
) -> anyhow::Result<EventHandleResult> {
match event {
CoveEvent::StateChanged => {}
CoveEvent::IdentificationRequired => {
// TODO Send identification if default nick is set in config
}
}
Ok(EventHandleResult::Continue)
}
fn switch_to_room(&mut self, id: RoomId) {
match &id {
RoomId::Cove(name) => {
if let Entry::Vacant(entry) = self.cove_rooms.entry(name.clone()) {
let room = CoveRoom::new(
self.config,
name.clone(),
self.event_tx.clone(),
UiEvent::cove,
);
entry.insert(CoveUi::new(room));
}
}
}
self.room = Some(id);
}
fn rooms_in_order(&self) -> Vec<RoomId> {
let mut rooms = vec![];
rooms.extend(self.cove_rooms.keys().cloned().map(RoomId::Cove));
rooms.sort();
rooms
}
fn get_room_index(&self, rooms: &[RoomId]) -> Option<(usize, RoomId)> {
let id = self.room.clone()?;
let index = rooms.iter().position(|room| room == &id)?;
Some((index, id))
}
fn set_room_index(&mut self, rooms: &[RoomId], index: usize) {
if rooms.is_empty() {
self.room = None;
return;
}
let id = rooms[index % rooms.len()].clone();
self.room = Some(id);
}
fn switch_to_next_room(&mut self) {
let rooms = self.rooms_in_order();
if let Some((index, _)) = self.get_room_index(&rooms) {
self.set_room_index(&rooms, index + 1);
}
}
fn switch_to_prev_room(&mut self) {
let rooms = self.rooms_in_order();
if let Some((index, _)) = self.get_room_index(&rooms) {
self.set_room_index(&rooms, index + rooms.len() - 1);
}
}
fn remove_current_room(&mut self) {
let rooms = self.rooms_in_order();
if let Some((index, id)) = self.get_room_index(&rooms) {
match id {
RoomId::Cove(name) => self.cove_rooms.remove(&name),
};
let rooms = self.rooms_in_order();
let max_index = if rooms.is_empty() { 0 } else { rooms.len() - 1 };
self.set_room_index(&rooms, index.min(max_index));
}
}
} }

View file

@ -1,91 +0,0 @@
mod body;
mod users;
use crossterm::event::KeyEvent;
use tui::backend::Backend;
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use tui::text::Span;
use tui::widgets::{Block, BorderType, Borders, Paragraph};
use tui::Frame;
use crate::client::cove::room::CoveRoom;
use self::body::{Body, Reaction};
use self::users::CoveUsers;
use super::input::EventHandler;
use super::styles;
pub struct CoveUi {
room: CoveRoom,
body: Body,
}
impl CoveUi {
pub fn new(room: CoveRoom) -> Self {
Self {
room,
body: Body::default(),
}
}
fn name(&self) -> &str {
self.room.name()
}
pub async fn render_main<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(area);
let title_area = areas[0];
let separator_area = areas[1];
let body_area = areas[2];
self.render_title(frame, title_area).await;
self.render_separator(frame, separator_area).await;
self.render_body(frame, body_area).await;
}
async fn render_title<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
// TODO Show current nick as well, if applicable
let room_name = Paragraph::new(Span::styled(
format!("&{}", self.name()),
styles::selected_room(),
))
.alignment(Alignment::Center);
frame.render_widget(room_name, area);
}
async fn render_separator<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
let separator = Block::default()
.borders(Borders::BOTTOM)
.border_type(BorderType::Double);
frame.render_widget(separator, area);
}
async fn render_body<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
self.body.update(&self.room).await;
self.body.render(frame, area).await
}
pub async fn render_users<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
if let Some(present) = self.room.conn().await.state().await.present() {
frame.render_widget(CoveUsers::new(present), area);
}
}
pub async fn handle_key(&mut self, event: KeyEvent) -> Option<()> {
match self.body.handle_key(event)? {
Reaction::Handled => Some(()),
Reaction::Identify(nick) => {
self.room.identify(&nick, &nick).await;
Some(())
}
}
}
}

View file

@ -1,161 +0,0 @@
use crossterm::event::{KeyCode, KeyEvent};
use tui::backend::Backend;
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use tui::text::Span;
use tui::widgets::Paragraph;
use tui::Frame;
use unicode_width::UnicodeWidthStr;
use crate::client::cove::conn::{State, Status};
use crate::client::cove::room::CoveRoom;
use crate::ui::input::EventHandler;
use crate::ui::textline::{TextLine, TextLineState};
use crate::ui::{layout, styles};
pub enum Body {
Empty,
Connecting,
ChoosingRoom,
Identifying,
ChooseNick {
nick: TextLineState,
prev_error: Option<String>,
},
Present,
Stopped, // TODO Display reason for stoppage
}
impl Default for Body {
fn default() -> Self {
Self::Empty
}
}
impl Body {
pub async fn update(&mut self, room: &CoveRoom) {
match &*room.conn().await.state().await {
State::Connecting => *self = Self::Connecting,
State::Connected(conn) => match conn.status() {
Status::ChoosingRoom => *self = Self::ChoosingRoom,
Status::Identifying => *self = Self::Identifying,
Status::IdRequired(error) => self.choose_nick(error.clone()),
Status::Present(_) => *self = Self::Present,
},
State::Stopped => *self = Self::Stopped,
}
}
fn choose_nick(&mut self, error: Option<String>) {
match self {
Self::ChooseNick { prev_error, .. } => *prev_error = error,
_ => {
*self = Self::ChooseNick {
nick: TextLineState::default(),
prev_error: error,
}
}
}
}
pub async fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
match self {
Body::Empty => todo!(),
Body::Connecting => {
let text = "Connecting...";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
Body::ChoosingRoom => {
let text = "Entering room...";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
Body::Identifying => {
let text = "Identifying...";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
Body::ChooseNick {
nick,
prev_error: None,
} => {
let area = layout::centered_v(2, area);
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Length(1)])
.split(area);
let title_area = areas[0];
let text_area = areas[1];
frame.render_widget(
Paragraph::new(Span::styled("Choose a nick:", styles::title()))
.alignment(Alignment::Center),
title_area,
);
frame.render_stateful_widget(TextLine, layout::centered(50, 1, text_area), nick);
}
Body::ChooseNick {
nick,
prev_error: Some(error),
} => {
let area = layout::centered_v(3, area);
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(area);
let title_area = areas[0];
let text_area = areas[1];
let error_area = areas[2];
frame.render_widget(
Paragraph::new(Span::styled("Choose a nick:", styles::title()))
.alignment(Alignment::Center),
title_area,
);
frame.render_stateful_widget(TextLine, layout::centered(50, 1, text_area), nick);
frame.render_widget(
Paragraph::new(Span::styled(error as &str, styles::error()))
.alignment(Alignment::Center),
error_area,
);
}
Body::Present => {
let text = "Present";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
Body::Stopped => {
let text = "Stopped";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
}
}
}
pub enum Reaction {
Handled,
Identify(String),
}
impl EventHandler for Body {
type Reaction = Reaction;
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction> {
match self {
Body::ChooseNick { nick, .. } => {
if event.code == KeyCode::Enter {
Some(Reaction::Identify(nick.content().to_string()))
} else {
nick.handle_key(event).and(Some(Reaction::Handled))
}
}
Body::Present => None, // TODO Implement
_ => None,
}
}
}

View file

@ -1,61 +0,0 @@
use std::collections::HashSet;
use std::iter;
use cove_core::{Identity, Session};
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::text::{Span, Spans};
use tui::widgets::{Paragraph, Widget};
use crate::client::cove::conn::Present;
use crate::ui::styles;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct UserInfo {
nick: String,
identity: Identity,
}
impl From<&Session> for UserInfo {
fn from(s: &Session) -> Self {
Self {
nick: s.nick.clone(),
identity: s.identity,
}
}
}
pub struct CoveUsers {
users: Vec<UserInfo>,
}
impl CoveUsers {
pub fn new(present: &Present) -> Self {
let mut users: Vec<UserInfo> = iter::once(&present.session)
.chain(present.others.values())
.map(<&Session as Into<UserInfo>>::into)
.collect();
users.sort();
Self { users }
}
}
impl Widget for CoveUsers {
fn render(self, area: Rect, buf: &mut Buffer) {
let sessions = self.users.len();
let identities = self
.users
.iter()
.map(|i| i.identity)
.collect::<HashSet<_>>()
.len();
let title = format!("Users ({identities}/{sessions})");
let mut lines = vec![Spans::from(Span::styled(title, styles::title()))];
for user in self.users {
// TODO Colour users based on identity
lines.push(Spans::from(Span::from(user.nick)));
}
Paragraph::new(lines).render(area, buf);
}
}

View file

@ -1,9 +0,0 @@
use crossterm::event::KeyEvent;
pub trait EventHandler {
type Reaction;
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction>;
// TODO Add method to show currently accepted keys for F1 help
}

View file

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

View file

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

View file

@ -1,58 +0,0 @@
use crossterm::event::{KeyCode, KeyEvent};
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::widgets::{Block, Borders, Clear, StatefulWidget, Widget};
use crate::ui::input::EventHandler;
use crate::ui::textline::{TextLine, TextLineReaction, TextLineState};
use crate::ui::{layout, RoomId};
use super::OverlayReaction;
pub struct SwitchRoom;
impl StatefulWidget for SwitchRoom {
type State = SwitchRoomState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let area = layout::centered(50, 3, area);
Clear.render(area, buf);
let block = Block::default().title("Join room").borders(Borders::ALL);
let inner_area = block.inner(area);
block.render(area, buf);
TextLine.render(inner_area, buf, &mut state.room);
}
}
#[derive(Debug, Default)]
pub struct SwitchRoomState {
room: TextLineState,
}
impl EventHandler for SwitchRoomState {
type Reaction = OverlayReaction;
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction> {
if event.code == KeyCode::Enter {
let name = self.room.content().trim();
if name.is_empty() {
return Some(Self::Reaction::Handled);
}
let id = RoomId::Cove(name.to_string());
return Some(Self::Reaction::SwitchRoom(id));
}
self.room.handle_key(event).map(|r| match r {
TextLineReaction::Handled => Self::Reaction::Handled,
TextLineReaction::Close => Self::Reaction::Close,
})
}
}
impl SwitchRoomState {
pub fn last_cursor_pos(&self) -> (u16, u16) {
self.room.last_cursor_pos()
}
}

View file

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

View file

@ -1,199 +0,0 @@
mod users;
use std::sync::Arc;
use tokio::sync::Mutex;
use tui::backend::Backend;
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use tui::style::Style;
use tui::text::{Span, Spans, Text};
use tui::widgets::{Block, BorderType, Borders, Paragraph};
use tui::Frame;
use unicode_width::UnicodeWidthStr;
use crate::room::{Room, Status};
use self::users::Users;
use super::textline::{TextLine, TextLineState};
use super::{layout, styles};
enum Main {
Empty,
Connecting,
Identifying,
ChooseNick {
nick: TextLineState,
prev_error: Option<String>,
},
Messages,
FatalError(String),
}
impl Main {
fn choose_nick() -> Self {
Self::ChooseNick {
nick: TextLineState::default(),
prev_error: None,
}
}
fn fatal<S: ToString>(s: S) -> Self {
Self::FatalError(s.to_string())
}
}
pub struct RoomInfo {
name: String,
room: Arc<Mutex<Room>>,
main: Main,
}
impl RoomInfo {
pub fn new(name: String, room: Arc<Mutex<Room>>) -> Self {
Self {
name,
room,
main: Main::Empty,
}
}
pub fn name(&self) -> &str {
&self.name
}
async fn align_main(&mut self) {
let room = self.room.lock().await;
match room.status() {
Status::Nominal if room.connected() && room.present().is_some() => {
if !matches!(self.main, Main::Messages) {
self.main = Main::Messages;
}
}
Status::Nominal if room.connected() => self.main = Main::Connecting,
Status::Nominal => self.main = Main::Identifying,
Status::NickRequired => self.main = Main::choose_nick(),
Status::CouldNotConnect => self.main = Main::fatal("Could not connect to room"),
Status::InvalidRoom(err) => self.main = Main::fatal(format!("Invalid room:\n{err}")),
Status::InvalidNick(err) => {
if let Main::ChooseNick { prev_error, .. } = &mut self.main {
*prev_error = Some(err.clone());
} else {
self.main = Main::choose_nick();
}
}
Status::InvalidIdentity(err) => {
self.main = Main::fatal(format!("Invalid identity:\n{err}"))
}
}
}
pub async fn render_main<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
self.align_main().await;
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(area);
let room_name_area = areas[0];
let separator_area = areas[1];
let main_area = areas[2];
// Room name at the top
let room_name = Paragraph::new(Span::styled(
format!("&{}", self.name()),
styles::selected_room(),
))
.alignment(Alignment::Center);
frame.render_widget(room_name, room_name_area);
let separator = Block::default()
.borders(Borders::BOTTOM)
.border_type(BorderType::Double);
frame.render_widget(separator, separator_area);
// Main below
self.render_main_inner(frame, main_area).await;
}
async fn render_main_inner<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
match &mut self.main {
Main::Empty => {}
Main::Connecting => {
let text = "Connecting...";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
Main::Identifying => {
let text = "Identifying...";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
Main::ChooseNick {
nick,
prev_error: None,
} => {
let area = layout::centered(50, 2, area);
let top = Rect { height: 1, ..area };
let bot = Rect {
y: top.y + 1,
..top
};
let text = "Choose a nick:";
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), top);
frame.render_stateful_widget(TextLine, bot, nick);
}
Main::ChooseNick { nick, prev_error } => {
let width = prev_error
.as_ref()
.map(|e| e.width() as u16)
.unwrap_or(0)
.max(50);
let height = if prev_error.is_some() { 5 } else { 2 };
let area = layout::centered(width, height, area);
let top = Rect {
height: height - 1,
..area
};
let bot = Rect {
y: area.bottom() - 1,
height: 1,
..area
};
let mut lines = vec![];
if let Some(err) = &prev_error {
lines.push(Spans::from(Span::styled("Error:", styles::title())));
lines.push(Spans::from(Span::styled(err, styles::error())));
lines.push(Spans::from(""));
}
lines.push(Spans::from(Span::styled("Choose a nick:", styles::title())));
frame.render_widget(Paragraph::new(lines), top);
frame.render_stateful_widget(TextLine, bot, nick);
}
Main::Messages => {
// TODO Actually render messages
frame.render_widget(Paragraph::new("TODO: Messages"), area);
}
Main::FatalError(err) => {
let title = "Fatal error:";
let width = (err.width() as u16).max(title.width() as u16);
let area = layout::centered(width, 2, area);
let pg = Paragraph::new(vec![
Spans::from(Span::styled(title, styles::title())),
Spans::from(Span::styled(err as &str, styles::error())),
])
.alignment(Alignment::Center);
frame.render_widget(pg, area);
}
}
}
pub async fn render_users<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
if let Some(present) = self.room.lock().await.present() {
frame.render_widget(Users::new(present), area);
}
}
}

View file

@ -1,68 +0,0 @@
use std::collections::HashMap;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::text::{Span, Spans};
use tui::widgets::{Paragraph, Widget};
use super::cove::CoveUi;
use super::styles;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct RoomInfo {
name: String,
}
pub struct Rooms {
rooms: Vec<RoomInfo>,
selected: Option<usize>,
}
impl Rooms {
pub fn new(cove_rooms: &HashMap<String, CoveUi>) -> Self {
let mut rooms = cove_rooms
.iter()
.map(|(name, _)| RoomInfo { name: name.clone() })
.collect::<Vec<_>>();
rooms.sort();
Self {
rooms,
selected: None,
}
}
pub fn select(mut self, name: &str) -> Self {
for (i, room) in self.rooms.iter().enumerate() {
if room.name == name {
self.selected = Some(i);
}
}
self
}
}
impl Widget for Rooms {
fn render(self, area: Rect, buf: &mut Buffer) {
let title = if let Some(selected) = self.selected {
format!("Rooms ({}/{})", selected + 1, self.rooms.len())
} else {
format!("Rooms ({})", self.rooms.len())
};
let mut lines = vec![Spans::from(Span::styled(title, styles::title()))];
for (i, room) in self.rooms.iter().enumerate() {
let name = format!("&{}", room.name);
if Some(i) == self.selected {
lines.push(Spans::from(vec![
Span::raw("\n>"),
Span::styled(name, styles::selected_room()),
]));
} else {
lines.push(Spans::from(vec![
Span::raw("\n "),
Span::styled(name, styles::room()),
]));
}
}
Paragraph::new(lines).render(area, buf);
}
}

View file

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

View file

@ -1,121 +0,0 @@
use std::cmp;
use crossterm::event::{KeyCode, KeyEvent};
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::widgets::{Paragraph, StatefulWidget, Widget};
use unicode_width::UnicodeWidthStr;
use super::input::EventHandler;
/// A simple single-line text box.
pub struct TextLine;
impl StatefulWidget for TextLine {
type State = TextLineState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Paragraph::new(&state.content as &str).render(area, buf);
// Determine cursor position
let prefix = state.content.chars().take(state.cursor).collect::<String>();
let position = prefix.width() as u16;
let x = area.x + position.min(area.width);
state.last_cursor_pos = (x, area.y);
}
}
/// State for [`TextLine`].
#[derive(Debug, Default)]
pub struct TextLineState {
content: String,
cursor: usize,
last_cursor_pos: (u16, u16),
}
impl TextLineState {
pub fn content(&self) -> &str {
&self.content
}
/// The cursor's position from when the widget was last rendered.
pub fn last_cursor_pos(&self) -> (u16, u16) {
self.last_cursor_pos
}
fn chars(&self) -> usize {
self.content.chars().count()
}
fn move_cursor_start(&mut self) {
self.cursor = 0;
}
fn move_cursor_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
fn move_cursor_right(&mut self) {
self.cursor = cmp::min(self.cursor + 1, self.chars());
}
fn move_cursor_end(&mut self) {
self.cursor = self.chars();
}
fn cursor_byte_offset(&self) -> usize {
self.content
.char_indices()
.nth(self.cursor)
.map(|(i, _)| i)
.unwrap_or_else(|| self.content.len())
}
}
pub enum TextLineReaction {
Handled,
Close,
}
impl EventHandler for TextLineState {
type Reaction = TextLineReaction;
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction> {
match event.code {
KeyCode::Backspace if self.cursor > 0 => {
self.move_cursor_left();
self.content.remove(self.cursor_byte_offset());
Some(TextLineReaction::Handled)
}
KeyCode::Left => {
self.move_cursor_left();
Some(TextLineReaction::Handled)
}
KeyCode::Right => {
self.move_cursor_right();
Some(TextLineReaction::Handled)
}
KeyCode::Home => {
self.move_cursor_start();
Some(TextLineReaction::Handled)
}
KeyCode::End => {
self.move_cursor_end();
Some(TextLineReaction::Handled)
}
KeyCode::Delete if self.cursor < self.chars() => {
self.content.remove(self.cursor_byte_offset());
Some(TextLineReaction::Handled)
}
KeyCode::Char(c) => {
self.content.insert(self.cursor_byte_offset(), c);
self.move_cursor_right();
Some(TextLineReaction::Handled)
}
KeyCode::Esc => Some(TextLineReaction::Close),
_ => None,
}
}
}