Show current room state
This commit is contained in:
parent
f34bf63be4
commit
04d17179a0
6 changed files with 202 additions and 33 deletions
|
|
@ -9,6 +9,7 @@ use cove_core::packets::{
|
||||||
use cove_core::{Session, SessionId};
|
use cove_core::{Session, SessionId};
|
||||||
use tokio::sync::oneshot::{self, Sender};
|
use tokio::sync::oneshot::{self, Sender};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use tui::widgets::StatefulWidget;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::never::Never;
|
use crate::never::Never;
|
||||||
|
|
@ -41,18 +42,15 @@ pub struct Present {
|
||||||
pub others: HashMap<SessionId, Session>,
|
pub others: HashMap<SessionId, Session>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Status {
|
pub enum Status {
|
||||||
/// No action required by the UI.
|
/// No action required by the UI.
|
||||||
Nominal,
|
Nominal,
|
||||||
/// User must enter a nick.
|
/// User must enter a nick.
|
||||||
NickRequired,
|
NickRequired,
|
||||||
/// Identifying to the server. No action required by the UI.
|
|
||||||
Identifying,
|
|
||||||
CouldNotConnect,
|
CouldNotConnect,
|
||||||
InvalidRoom(String),
|
InvalidRoom(String),
|
||||||
InvalidNick(String),
|
InvalidNick(String),
|
||||||
InvalidIdentity(String),
|
InvalidIdentity(String),
|
||||||
InvalidContent(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
|
|
@ -95,6 +93,14 @@ impl Room {
|
||||||
room
|
room
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn status(&self) -> &Status {
|
||||||
|
&self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connected(&self) -> bool {
|
||||||
|
self.connected.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn present(&self) -> Option<&Present> {
|
pub fn present(&self) -> Option<&Present> {
|
||||||
self.present.as_ref()
|
self.present.as_ref()
|
||||||
}
|
}
|
||||||
|
|
@ -209,9 +215,7 @@ impl Room {
|
||||||
Rpl::Send(SendRpl::Success { message }) => {
|
Rpl::Send(SendRpl::Success { message }) => {
|
||||||
// TODO Send message to store
|
// TODO Send message to store
|
||||||
}
|
}
|
||||||
Rpl::Send(SendRpl::InvalidContent { reason }) => {
|
Rpl::Send(SendRpl::InvalidContent { reason }) => {}
|
||||||
self.status = Status::InvalidContent(reason.clone());
|
|
||||||
}
|
|
||||||
Rpl::Who(WhoRpl { you, others }) => {
|
Rpl::Who(WhoRpl { you, others }) => {
|
||||||
if let Some(present) = &mut self.present {
|
if let Some(present) = &mut self.present {
|
||||||
present.session = you.clone();
|
present.session = you.clone();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ mod pane;
|
||||||
mod room;
|
mod room;
|
||||||
mod rooms;
|
mod rooms;
|
||||||
mod textline;
|
mod textline;
|
||||||
|
mod styles;
|
||||||
|
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
|
||||||
|
|
@ -4,41 +4,191 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tui::backend::Backend;
|
use tui::backend::Backend;
|
||||||
use tui::layout::Rect;
|
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||||
use tui::widgets::{Block, BorderType, Borders};
|
use tui::style::Style;
|
||||||
|
use tui::text::{Span, Spans, Text};
|
||||||
|
use tui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||||
use tui::Frame;
|
use tui::Frame;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::room::Room;
|
use crate::room::{Room, Status};
|
||||||
|
|
||||||
use self::users::Users;
|
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 {
|
pub struct RoomInfo {
|
||||||
name: String,
|
name: String,
|
||||||
room: Arc<Mutex<Room>>,
|
room: Arc<Mutex<Room>>,
|
||||||
|
main: Main,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoomInfo {
|
impl RoomInfo {
|
||||||
pub fn new(name: String, room: Arc<Mutex<Room>>) -> Self {
|
pub fn new(name: String, room: Arc<Mutex<Room>>) -> Self {
|
||||||
Self { name, room }
|
Self {
|
||||||
|
name,
|
||||||
|
room,
|
||||||
|
main: Main::Empty,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
&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) {
|
pub async fn render_main<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
||||||
// TODO Implement
|
self.align_main().await;
|
||||||
frame.render_widget(
|
|
||||||
Block::default()
|
let areas = Layout::default()
|
||||||
.borders(Borders::TOP)
|
.direction(Direction::Vertical)
|
||||||
.border_type(BorderType::Double),
|
.constraints([
|
||||||
Rect {
|
Constraint::Length(1),
|
||||||
x: area.x,
|
Constraint::Length(1),
|
||||||
y: area.y + 1,
|
Constraint::Min(0),
|
||||||
width: area.width,
|
])
|
||||||
height: 1,
|
.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 = "Connecing...";
|
||||||
|
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) {
|
pub async fn render_users<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use tui::text::{Span, Spans};
|
||||||
use tui::widgets::{Paragraph, Widget};
|
use tui::widgets::{Paragraph, Widget};
|
||||||
|
|
||||||
use crate::room::Present;
|
use crate::room::Present;
|
||||||
|
use crate::ui::styles;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct UserInfo {
|
struct UserInfo {
|
||||||
|
|
@ -42,8 +43,6 @@ impl Users {
|
||||||
|
|
||||||
impl Widget for Users {
|
impl Widget for Users {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let title_style = Style::default().add_modifier(Modifier::BOLD);
|
|
||||||
|
|
||||||
let sessions = self.users.len();
|
let sessions = self.users.len();
|
||||||
let identities = self
|
let identities = self
|
||||||
.users
|
.users
|
||||||
|
|
@ -53,7 +52,7 @@ impl Widget for Users {
|
||||||
.len();
|
.len();
|
||||||
let title = format!("Users ({identities}/{sessions})");
|
let title = format!("Users ({identities}/{sessions})");
|
||||||
|
|
||||||
let mut lines = vec![Spans::from(Span::styled(title, title_style))];
|
let mut lines = vec![Spans::from(Span::styled(title, styles::title()))];
|
||||||
for user in self.users {
|
for user in self.users {
|
||||||
// TODO Colour users based on identity
|
// TODO Colour users based on identity
|
||||||
lines.push(Spans::from(Span::from(user.nick)));
|
lines.push(Spans::from(Span::from(user.nick)));
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ use tui::widgets::{Paragraph, Widget};
|
||||||
|
|
||||||
use crate::room::Room;
|
use crate::room::Room;
|
||||||
|
|
||||||
|
use super::styles;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct RoomInfo {
|
struct RoomInfo {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -45,27 +47,23 @@ impl Rooms {
|
||||||
|
|
||||||
impl Widget for Rooms {
|
impl Widget for Rooms {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let title_style = Style::default().add_modifier(Modifier::BOLD);
|
|
||||||
let room_style = Style::default().fg(Color::LightBlue);
|
|
||||||
let selected_room_style = room_style.add_modifier(Modifier::BOLD);
|
|
||||||
|
|
||||||
let title = if let Some(selected) = self.selected {
|
let title = if let Some(selected) = self.selected {
|
||||||
format!("Rooms ({}/{})", selected + 1, self.rooms.len())
|
format!("Rooms ({}/{})", selected + 1, self.rooms.len())
|
||||||
} else {
|
} else {
|
||||||
format!("Rooms ({})", self.rooms.len())
|
format!("Rooms ({})", self.rooms.len())
|
||||||
};
|
};
|
||||||
let mut lines = vec![Spans::from(Span::styled(title, title_style))];
|
let mut lines = vec![Spans::from(Span::styled(title, styles::title()))];
|
||||||
for (i, room) in self.rooms.iter().enumerate() {
|
for (i, room) in self.rooms.iter().enumerate() {
|
||||||
let name = format!("&{}", room.name);
|
let name = format!("&{}", room.name);
|
||||||
if Some(i) == self.selected {
|
if Some(i) == self.selected {
|
||||||
lines.push(Spans::from(vec![
|
lines.push(Spans::from(vec![
|
||||||
Span::raw("\n>"),
|
Span::raw("\n>"),
|
||||||
Span::styled(name, selected_room_style),
|
Span::styled(name, styles::selected_room()),
|
||||||
]));
|
]));
|
||||||
} else {
|
} else {
|
||||||
lines.push(Spans::from(vec![
|
lines.push(Spans::from(vec![
|
||||||
Span::raw("\n "),
|
Span::raw("\n "),
|
||||||
Span::styled(name, room_style),
|
Span::styled(name, styles::room()),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
cove-tui/src/ui/styles.rs
Normal file
17
cove-tui/src/ui/styles.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue