Render room list with new generic list

This commit is contained in:
Joscha 2022-07-05 19:37:03 +02:00
parent 8b7c58b702
commit 7f1dc020d3
3 changed files with 152 additions and 126 deletions

View file

@ -348,3 +348,24 @@ impl Time {
/// fixed format for the unique value. /// fixed format for the unique value.
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserId(pub String); pub struct UserId(pub String);
#[derive(Debug, PartialEq, Eq)]
pub enum SessionType {
Agent,
Account,
Bot,
}
impl UserId {
pub fn session_type(&self) -> Option<SessionType> {
if self.0.starts_with("agent:") {
Some(SessionType::Agent)
} else if self.0.starts_with("account:") {
Some(SessionType::Account)
} else if self.0.starts_with("bot:") {
Some(SessionType::Bot)
} else {
None
}
}
}

View file

@ -40,14 +40,18 @@ impl EuphRoom {
self.room = None; self.room = None;
} }
pub fn connected(&self) -> bool { pub async fn status(&self) -> Option<Option<Status>> {
if let Some(room) = &self.room { if let Some(room) = &self.room {
!room.stopped() room.status().await.ok()
} else { } else {
false None
} }
} }
pub fn stopped(&self) -> bool {
self.room.as_ref().map(|r| r.stopped()).unwrap_or(true)
}
pub fn retain(&mut self) { pub fn retain(&mut self) {
if let Some(room) = &self.room { if let Some(room) = &self.room {
if room.stopped() { if room.stopped() {

View file

@ -1,14 +1,20 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::iter;
use std::sync::Arc; use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use crossterm::style::{ContentStyle, Stylize};
use parking_lot::FairMutex; use parking_lot::FairMutex;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use toss::frame::{Frame, Pos, Size}; use toss::frame::{Frame, Pos, Size};
use toss::styled::Styled;
use toss::terminal::Terminal; use toss::terminal::Terminal;
use crate::euph::api::SessionType;
use crate::euph::{Joined, Status};
use crate::vault::Vault; use crate::vault::Vault;
use super::list::{List, Row};
use super::room::EuphRoom; use super::room::EuphRoom;
use super::{util, UiEvent}; use super::{util, UiEvent};
@ -34,10 +40,7 @@ pub struct Rooms {
vault: Vault, vault: Vault,
ui_event_tx: mpsc::UnboundedSender<UiEvent>, ui_event_tx: mpsc::UnboundedSender<UiEvent>,
/// Cursor position inside the room list. list: List<String>,
///
/// If there are any rooms, this should point to a valid room.
cursor: Option<Cursor>,
/// If set, a single room is displayed in full instead of the room list. /// If set, a single room is displayed in full instead of the room list.
focus: Option<String>, focus: Option<String>,
@ -50,7 +53,7 @@ impl Rooms {
Self { Self {
vault, vault,
ui_event_tx, ui_event_tx,
cursor: None, list: List::new(),
focus: None, focus: None,
euph_rooms: HashMap::new(), euph_rooms: HashMap::new(),
} }
@ -62,7 +65,7 @@ impl Rooms {
rooms.insert(room); rooms.insert(room);
} }
for (name, room) in &self.euph_rooms { for (name, room) in &self.euph_rooms {
if room.connected() { if !room.stopped() {
rooms.insert(name.clone()); rooms.insert(name.clone());
} }
} }
@ -71,50 +74,27 @@ impl Rooms {
rooms rooms
} }
fn make_cursor_consistent(&mut self, rooms: &[String], height: i32) { /// Remove rooms that are not running any more and can't be found in the db.
// Fix index if it's wrong ///
if rooms.is_empty() { /// These kinds of rooms are either
self.cursor = None; /// - failed connection attempts, or
} else if let Some(cursor) = &mut self.cursor { /// - rooms that were deleted from the db.
let max_index = rooms.len() - 1; async fn stabilize_rooms(&mut self) -> Vec<String> {
if cursor.index > max_index { let mut rooms = self.vault.euph_rooms().await;
cursor.index = max_index; let rooms_set = rooms.iter().map(|n| n as &str).collect::<HashSet<_>>();
} self.euph_rooms
} else { .retain(|n, r| !r.stopped() || rooms_set.contains(n as &str));
self.cursor = Some(Cursor::default());
}
// Fix line if it's wrong
if let Some(cursor) = &mut self.cursor {
cursor.line = cursor
.line
// Make sure the cursor is visible on screen
.clamp(0, height - 1)
// Make sure there is no free space below the room list:
// height - line <= len - index
// height - len + index <= line
.max(height - rooms.len() as i32 + cursor.index as i32)
// Make sure there is no free space above the room list:
// line <= index
.min(cursor.index as i32);
}
}
fn make_euph_rooms_consistent(&mut self, rooms: &[String]) {
let rooms = rooms
.iter()
.map(|n| n.to_string())
.collect::<HashSet<String>>();
self.euph_rooms.retain(|n, _| rooms.contains(n));
for room in self.euph_rooms.values_mut() { for room in self.euph_rooms.values_mut() {
room.retain(); room.retain();
} }
}
fn make_consistent(&mut self, rooms: &[String], height: i32) { for room in self.euph_rooms.keys() {
self.make_cursor_consistent(rooms, height); rooms.push(room.clone());
self.make_euph_rooms_consistent(rooms); }
rooms.sort_unstable();
rooms.dedup();
rooms
} }
pub async fn render(&mut self, frame: &mut Frame) { pub async fn render(&mut self, frame: &mut Frame) {
@ -128,37 +108,81 @@ impl Rooms {
} }
} }
async fn render_rooms(&mut self, frame: &mut Frame) { fn format_pbln(joined: &Joined) -> String {
let size = frame.size(); let mut p = 0_usize;
let mut b = 0_usize;
let rooms = self.rooms().await; let mut l = 0_usize;
self.make_consistent(&rooms, size.height.into()); let mut n = 0_usize;
for sess in iter::once(&joined.session).chain(joined.listing.values()) {
let cursor = self.cursor.unwrap_or_default(); match sess.id.session_type() {
for (index, room) in rooms.iter().enumerate() { Some(SessionType::Bot) if sess.name.is_empty() => n += 1,
let y = index as i32 - cursor.index as i32 + cursor.line; Some(SessionType::Bot) => b += 1,
_ if sess.name.is_empty() => l += 1,
let style = if index == cursor.index { _ => p += 1,
style::room_inverted()
} else {
style::room()
};
for x in 0..size.width {
frame.write(Pos::new(x.into(), y), (" ", style));
} }
let suffix = if let Some(room) = self.euph_rooms.get(room) {
if room.connected() {
"*"
} else {
""
}
} else {
""
};
let room_str = format!("&{room}{suffix}");
frame.write(Pos::new(0, y), (&room_str, style));
} }
// There must always be either one p, b, l or n since we're including
// ourselves.
let mut result = vec![];
if p > 0 {
result.push(format!("{p}p"));
}
if b > 0 {
result.push(format!("{b}b"));
}
if l > 0 {
result.push(format!("{l}l"));
}
if n > 0 {
result.push(format!("{n}n"));
}
result.join(" ")
}
fn format_status(status: &Option<Status>) -> String {
match status {
None => " (connecting)".to_string(),
Some(Status::Joining(j)) if j.bounce.is_some() => " (auth required)".to_string(),
Some(Status::Joining(_)) => " (joining)".to_string(),
Some(Status::Joined(j)) => format!(" ({})", Self::format_pbln(j)),
}
}
async fn render_rows(&self, rooms: Vec<String>) -> Vec<Row<String>> {
let mut rows: Vec<Row<String>> =
vec![Row::unsel(("Rooms", ContentStyle::default().bold()))];
if rooms.is_empty() {
rows.push(Row::unsel(("none", ContentStyle::default().dark_grey())))
}
for room in rooms {
let bg_style = ContentStyle::default();
let bg_sel_style = ContentStyle::default().black().on_white();
let room_style = ContentStyle::default().bold().blue();
let room_sel_style = ContentStyle::default().bold().black().on_white();
let mut normal = Styled::new((format!("&{room}"), room_style));
let mut selected = Styled::new((format!("&{room}"), room_sel_style));
if let Some(room) = self.euph_rooms.get(&room) {
if let Some(status) = room.status().await {
let status = Self::format_status(&status);
normal = normal.then((status.clone(), bg_style));
selected = selected.then((status, bg_sel_style));
}
};
rows.push(Row::sel(room, normal, bg_style, selected, bg_sel_style));
}
rows
}
async fn render_rooms(&mut self, frame: &mut Frame) {
let rooms = self.stabilize_rooms().await;
let rows = self.render_rows(rooms).await;
self.list.render(frame, Pos::ZERO, frame.size(), rows);
} }
pub async fn handle_key_event( pub async fn handle_key_event(
@ -180,70 +204,47 @@ impl Rooms {
.await; .await;
} }
} else { } else {
let rooms = self.rooms().await; let height = size.height as usize;
self.make_consistent(&rooms, size.height.into());
let rooms = self.stabilize_rooms().await;
let rows = self.render_rows(rooms).await;
match event.code { match event.code {
KeyCode::Enter => { KeyCode::Enter => {
if let Some(cursor) = self.cursor { if let Some(name) = self.list.cursor() {
if let Some(room) = rooms.get(cursor.index) { self.focus = Some(name.clone());
self.focus = Some(room.clone());
}
}
}
KeyCode::Char('j') => {
if let Some(cursor) = &mut self.cursor {
cursor.index = cursor.index.saturating_add(1);
cursor.line += 1;
}
}
KeyCode::Char('k') => {
if let Some(cursor) = &mut self.cursor {
cursor.index = cursor.index.saturating_sub(1);
cursor.line -= 1;
} }
} }
KeyCode::Char('j') | KeyCode::Down => self.list.move_cursor_down(height, &rows),
KeyCode::Char('k') | KeyCode::Up => self.list.move_cursor_up(height, &rows),
KeyCode::Char('J') => self.list.scroll_down(height, &rows), // TODO Replace by Ctrl+E and mouse scroll
KeyCode::Char('K') => self.list.scroll_up(height, &rows), // TODO Replace by Ctrl+Y and mouse scroll
KeyCode::Char('c') => { KeyCode::Char('c') => {
if let Some(cursor) = &self.cursor { if let Some(name) = self.list.cursor() {
if let Some(room) = rooms.get(cursor.index) { let room = self.euph_rooms.entry(name.clone()).or_insert_with(|| {
let room = room.clone(); EuphRoom::new(self.vault.euph(name.clone()), self.ui_event_tx.clone())
let actual_room = });
self.euph_rooms.entry(room.clone()).or_insert_with(|| { room.connect();
EuphRoom::new(
self.vault.euph(room.clone()),
self.ui_event_tx.clone(),
)
});
actual_room.connect();
}
} }
} }
KeyCode::Char('C') => { KeyCode::Char('C') => {
if let Some(room) = util::prompt(terminal, crossterm_lock) { if let Some(name) = util::prompt(terminal, crossterm_lock) {
let room = room.trim().to_string(); let name = name.trim().to_string();
let actual_room = let room = self.euph_rooms.entry(name.clone()).or_insert_with(|| {
self.euph_rooms.entry(room.clone()).or_insert_with(|| { EuphRoom::new(self.vault.euph(name), self.ui_event_tx.clone())
EuphRoom::new( });
self.vault.euph(room.clone()), room.connect();
self.ui_event_tx.clone(),
)
});
actual_room.connect();
} }
} }
KeyCode::Char('d') => { KeyCode::Char('d') => {
if let Some(cursor) = &self.cursor { if let Some(name) = self.list.cursor() {
if let Some(room) = rooms.get(cursor.index) { self.euph_rooms.remove(name);
self.euph_rooms.remove(room);
}
} }
} }
KeyCode::Char('D') => { KeyCode::Char('D') => {
if let Some(cursor) = &self.cursor { if let Some(name) = self.list.cursor() {
if let Some(room) = rooms.get(cursor.index) { self.euph_rooms.remove(name);
self.euph_rooms.remove(room); self.vault.euph(name.clone()).delete();
self.vault.euph(room.clone()).delete();
}
} }
} }
_ => {} _ => {}