diff --git a/src/ui/room.rs b/src/ui/room.rs index 99d7f3b..c19c147 100644 --- a/src/ui/room.rs +++ b/src/ui/room.rs @@ -1,21 +1,29 @@ +use std::iter; use std::sync::Arc; use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::style::{Color, ContentStyle, Stylize}; use parking_lot::FairMutex; use tokio::sync::mpsc; use toss::frame::{Frame, Pos, Size}; +use toss::styled::Styled; use toss::terminal::Terminal; -use crate::euph::{self, Status}; +use crate::euph::api::{SessionType, SessionView}; +use crate::euph::{self, Joined, Status}; use crate::vault::{EuphMsg, EuphVault}; use super::chat::Chat; +use super::list::{List, Row}; use super::{util, UiEvent}; pub struct EuphRoom { ui_event_tx: mpsc::UnboundedSender, room: Option, chat: Chat, + + nick_list_width: u16, + nick_list: List, } impl EuphRoom { @@ -24,6 +32,8 @@ impl EuphRoom { ui_event_tx, room: None, chat: Chat::new(vault), + nick_list_width: 24, + nick_list: List::new(), } } @@ -61,53 +71,188 @@ impl EuphRoom { } pub async fn render(&mut self, frame: &mut Frame) { - let size = frame.size(); - - let chat_pos = Pos::new(0, 2); - let chat_size = Size { - height: size.height - 2, - ..size - }; - self.chat.render(frame, chat_pos, chat_size).await; - - let room = self.chat.store().room(); - let status = if let Some(room) = &self.room { - room.status().await.ok() - } else { - None - }; - Self::render_top_bar(frame, room, status); + let status = self.status().await; + match &status { + Some(Some(Status::Joined(joined))) => { + self.render_with_nick_list(frame, &status, joined).await + } + _ => self.render_without_nick_list(frame, &status).await, + } } - fn render_top_bar(frame: &mut Frame, room: &str, status: Option>) { - // Clear area in case something accidentally wrote on it already + async fn render_without_nick_list( + &mut self, + frame: &mut Frame, + status: &Option>, + ) { let size = frame.size(); - for x in 0..size.width as i32 { - frame.write(Pos::new(x, 0), " "); - frame.write(Pos::new(x, 1), "─"); - } - // Write status - let status = match status { - None => format!("&{room}, archive"), - Some(None) => format!("&{room}, connecting..."), - Some(Some(Status::Joining(j))) => { - if j.bounce.is_none() { - format!("&{room}, joining...") - } else { - format!("&{room}, auth required") - } - } + // Position of horizontal line between status and chat + let hsplit = 1_i32; + + let status_pos = Pos::new(0, 0); + // let status_size = Size::new(size.width, 1); + + let chat_pos = Pos::new(0, hsplit + 1); + let chat_size = Size::new(size.width, size.height.saturating_sub(hsplit as u16 + 1)); + + self.chat.render(frame, chat_pos, chat_size).await; + self.render_status(frame, status_pos, status); + Self::render_hsplit(frame, hsplit); + } + + async fn render_with_nick_list( + &mut self, + frame: &mut Frame, + status: &Option>, + joined: &Joined, + ) { + let size = frame.size(); + + // Position of vertical line between main part and nick list + let vsplit = size.width.saturating_sub(self.nick_list_width + 1) as i32; + // Position of horizontal line between status and chat + let hsplit = 1_i32; + + let status_pos = Pos::new(0, 0); + // let status_size = Size::new(vsplit as u16, 1); + + let chat_pos = Pos::new(0, hsplit + 1); + let chat_size = Size::new(vsplit as u16, size.height.saturating_sub(hsplit as u16 + 1)); + + let nick_list_pos = Pos::new(vsplit + 1, 0); + let nick_list_size = Size::new(self.nick_list_width, size.height); + + self.chat.render(frame, chat_pos, chat_size).await; + self.render_status(frame, status_pos, status); + self.render_nick_list(frame, nick_list_pos, nick_list_size, joined); + Self::render_vsplit_hsplit(frame, vsplit, hsplit); + } + + fn render_status(&self, frame: &mut Frame, pos: Pos, status: &Option>) { + let room = self.chat.store().room(); + let room_style = ContentStyle::default().bold().blue(); + let mut info = Styled::new((format!("&{room}"), room_style)); + info = match status { + None => info.then(", archive"), + Some(None) => info.then(", connecting..."), + Some(Some(Status::Joining(j))) if j.bounce.is_some() => info.then(", auth required"), + Some(Some(Status::Joining(_))) => info.then(", joining..."), Some(Some(Status::Joined(j))) => { let nick = &j.session.name; if nick.is_empty() { - format!("&{room}, present without nick") + info.then(", present without nick") } else { - format!("&{room}, present as {nick}",) + let nick_style = euph::nick_style(nick); + info.then(", present as ").then((nick, nick_style)) } } }; - frame.write(Pos::new(0, 0), status); + frame.write(pos, info); + } + + fn render_row(session: &SessionView) -> Row { + if session.name.is_empty() { + let name = "lurk"; + let style = ContentStyle::default().grey(); + let style_inv = ContentStyle::default().black().on_grey(); + Row::sel( + session.session_id.clone(), + Styled::new((name, style)), + style, + Styled::new((name, style_inv)), + style_inv, + ) + } else { + let name = &session.name; + let (r, g, b) = euph::nick_color(name); + let color = Color::Rgb { r, g, b }; + let style = ContentStyle::default().bold().with(color); + let style_inv = ContentStyle::default().bold().black().on(color); + Row::sel( + session.session_id.clone(), + Styled::new((name, style)), + style, + Styled::new((name, style_inv)), + style_inv, + ) + } + } + + fn render_section(rows: &mut Vec>, name: &str, sessions: &[&SessionView]) { + if sessions.is_empty() { + return; + } + + let heading_style = ContentStyle::new().bold(); + + if !rows.is_empty() { + rows.push(Row::unsel("")); + } + + let row = Styled::new((name, heading_style)).then(format!(" ({})", sessions.len())); + rows.push(Row::unsel(row)); + + for sess in sessions { + rows.push(Self::render_row(sess)); + } + } + + fn render_rows(joined: &Joined) -> Vec> { + let mut people = vec![]; + let mut bots = vec![]; + let mut lurkers = vec![]; + let mut nurkers = vec![]; + + for sess in iter::once(&joined.session).chain(joined.listing.values()) { + match sess.id.session_type() { + Some(SessionType::Bot) if sess.name.is_empty() => nurkers.push(sess), + Some(SessionType::Bot) => bots.push(sess), + _ if sess.name.is_empty() => lurkers.push(sess), + _ => people.push(sess), + } + } + + people.sort_unstable_by_key(|s| (&s.name, &s.session_id)); + bots.sort_unstable_by_key(|s| (&s.name, &s.session_id)); + lurkers.sort_unstable_by_key(|s| &s.session_id); + nurkers.sort_unstable_by_key(|s| &s.session_id); + + let mut rows: Vec> = vec![]; + Self::render_section(&mut rows, "People", &people); + Self::render_section(&mut rows, "Bots", &bots); + Self::render_section(&mut rows, "Lurkers", &lurkers); + Self::render_section(&mut rows, "Nurkers", &nurkers); + rows + } + + fn render_nick_list(&mut self, frame: &mut Frame, pos: Pos, size: Size, joined: &Joined) { + // Clear area in case there's overdraw from the chat or status + for y in pos.y..(pos.y + size.height as i32) { + for x in pos.x..(pos.x + size.width as i32) { + frame.write(Pos::new(x, y), " "); + } + } + + let rows = Self::render_rows(joined); + self.nick_list.render(frame, pos, size, rows); + } + + fn render_hsplit(frame: &mut Frame, hsplit: i32) { + for x in 0..frame.size().width as i32 { + frame.write(Pos::new(x, hsplit), "─"); + } + } + + fn render_vsplit_hsplit(frame: &mut Frame, vsplit: i32, hsplit: i32) { + for x in 0..vsplit { + frame.write(Pos::new(x, hsplit), "─"); + } + + for y in 0..frame.size().height as i32 { + let symbol = if y == hsplit { "┤" } else { "│" }; + frame.write(Pos::new(vsplit, y), symbol); + } } pub async fn handle_key_event(