diff --git a/cove-tui/src/client/cove/conn.rs b/cove-tui/src/client/cove/conn.rs index e27c2c3..7d4f4a2 100644 --- a/cove-tui/src/client/cove/conn.rs +++ b/cove-tui/src/client/cove/conn.rs @@ -122,6 +122,10 @@ impl Connected { } } + pub fn status(&self) -> &Status { + &self.status + } + pub fn present(&self) -> Option<&Present> { self.status.present() } diff --git a/cove-tui/src/client/cove/room.rs b/cove-tui/src/client/cove/room.rs index 236dbf3..eb7a27f 100644 --- a/cove-tui/src/client/cove/room.rs +++ b/cove-tui/src/client/cove/room.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use std::time::Duration; -use tokio::runtime::Runtime; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tokio::sync::oneshot::{self, Sender}; use tokio::sync::{Mutex, MutexGuard}; @@ -15,7 +14,7 @@ struct ConnConfig { url: String, room: String, timeout: Duration, - ev_tx: UnboundedSender, + ev_tx: UnboundedSender, } impl ConnConfig { @@ -68,11 +67,19 @@ impl CoveRoom { 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::shovel_events(name, ev_rx, event_sender, convert_event) => {} _ = Self::run(conn_clone, mt, conf) => {} } }); @@ -91,7 +98,7 @@ impl CoveRoom { async fn shovel_events( name: String, - mut ev_rx: UnboundedReceiver, + mut ev_rx: UnboundedReceiver, ev_tx: UnboundedSender, convert_event: impl Fn(&str, Event) -> E, ) { @@ -115,6 +122,7 @@ impl CoveRoom { // 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, diff --git a/cove-tui/src/main.rs b/cove-tui/src/main.rs index 7518e77..c9d60a2 100644 --- a/cove-tui/src/main.rs +++ b/cove-tui/src/main.rs @@ -1,3 +1,5 @@ +// TODO Make as few things async as necessary + #![warn(clippy::use_self)] pub mod client; diff --git a/cove-tui/src/ui/cove.rs b/cove-tui/src/ui/cove.rs index 27f6350..e739cf6 100644 --- a/cove-tui/src/ui/cove.rs +++ b/cove-tui/src/ui/cove.rs @@ -1,3 +1,4 @@ +mod body; mod users; use crossterm::event::KeyEvent; @@ -9,6 +10,7 @@ use tui::Frame; use crate::client::cove::room::CoveRoom; +use self::body::Body; use self::users::CoveUsers; use super::input::EventHandler; @@ -16,11 +18,15 @@ use super::styles; pub struct CoveUi { room: CoveRoom, + body: Body, } impl CoveUi { pub fn new(room: CoveRoom) -> Self { - Self { room } + Self { + room, + body: Body::default(), + } } fn name(&self) -> &str { @@ -63,7 +69,8 @@ impl CoveUi { } async fn render_body(&mut self, frame: &mut Frame<'_, B>, area: Rect) { - // TODO Implement + 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) { diff --git a/cove-tui/src/ui/cove/body.rs b/cove-tui/src/ui/cove/body.rs new file mode 100644 index 0000000..1ce7986 --- /dev/null +++ b/cove-tui/src/ui/cove/body.rs @@ -0,0 +1,133 @@ +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::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())), + title_area, + ); + frame.render_stateful_widget(TextLine, 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())), + title_area, + ); + frame.render_stateful_widget(TextLine, text_area, nick); + frame.render_widget( + Paragraph::new(Span::styled(error as &str, styles::error())), + 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); + } + } + } +} diff --git a/cove-tui/src/ui/layout.rs b/cove-tui/src/ui/layout.rs index a0fc103..20eeeff 100644 --- a/cove-tui/src/ui/layout.rs +++ b/cove-tui/src/ui/layout.rs @@ -12,3 +12,13 @@ pub fn centered(width: u16, height: u16, area: Rect) -> Rect { 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 + } +}