From 3996f7c0d62aed437b3fa469fe54e5a4de41889d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 12 Mar 2025 12:51:35 +0100 Subject: [PATCH] feat(multiverse): Add a help screen --- labs/multiverse/src/help.rs | 42 +++++++ labs/multiverse/src/main.rs | 222 ++++++++++++++++++++++------------ labs/multiverse/src/status.rs | 37 +++--- 3 files changed, 207 insertions(+), 94 deletions(-) create mode 100644 labs/multiverse/src/help.rs diff --git a/labs/multiverse/src/help.rs b/labs/multiverse/src/help.rs new file mode 100644 index 000000000..261a014f9 --- /dev/null +++ b/labs/multiverse/src/help.rs @@ -0,0 +1,42 @@ +use ratatui::{ + prelude::*, + widgets::{Block, Borders, Cell, Clear, Padding, Row, Table, TableState}, +}; + +use crate::popup_area; + +pub struct HelpView {} + +impl HelpView { + pub fn new() -> Self { + Self {} + } +} + +impl Widget for &mut HelpView { + fn render(self, area: Rect, buf: &mut Buffer) { + let block = + Block::bordered().title(" Help Menu ").borders(Borders::ALL).padding(Padding::left(2)); + let area = popup_area(area, 80, 80); + Clear.render(area, buf); + + let rows = vec![ + Row::new(vec![Cell::from("F1"), Cell::from("Open Help")]), + Row::new(vec![Cell::from("s"), Cell::from("Resume syncing")]), + Row::new(vec![Cell::from("S"), Cell::from("Stop syncing")]), + Row::new(vec![Cell::from("Q"), Cell::from("Enable/disable the send queue")]), + Row::new(vec![Cell::from("M"), Cell::from("Send a message to the selected room")]), + Row::new(vec![ + Cell::from("L"), + Cell::from("Like the last message in the selected room"), + ]), + ]; + let widths = [Constraint::Length(5), Constraint::Length(5)]; + + let help_table = Table::new(rows, widths) + .block(block) + .widths(&[Constraint::Length(10), Constraint::Min(30)]); + + StatefulWidget::render(help_table, area, buf, &mut TableState::default()); + } +} diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index 3680894a6..00ca656a6 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -8,9 +8,10 @@ use std::{ use clap::Parser; use color_eyre::Result; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use events::EventsView; use futures_util::{pin_mut, StreamExt as _}; +use help::HelpView; use imbl::Vector; use layout::Flex; use linked_chunk::LinkedChunkView; @@ -38,11 +39,12 @@ use matrix_sdk_ui::{ use ratatui::{prelude::*, style::palette::tailwind, widgets::*}; use read_receipts::ReadReceipts; use status::Status; -use tokio::{runtime::Handle, spawn, task::JoinHandle}; +use tokio::{spawn, task::JoinHandle}; use tracing::{error, warn}; use tracing_subscriber::EnvFilter; mod events; +mod help; mod linked_chunk; mod read_receipts; mod room_list; @@ -73,6 +75,13 @@ struct Cli { proxy: Option, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum GlobalMode { + #[default] + Default, + Help, +} + /// Helper function to create a centered rect using up certain percentage of the /// available rect `r` fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect { @@ -149,6 +158,10 @@ struct App { details_mode: DetailsMode, current_pagination: Arc>>>, + + /// What popup are we showing that is covering the majority of the screen, + /// mainly used for help and settings screens. + global_mode: GlobalMode, } impl App { @@ -191,6 +204,7 @@ impl App { client, listen_task, status, + global_mode: GlobalMode::default(), details_mode: Default::default(), timelines, current_pagination: Default::default(), @@ -366,96 +380,137 @@ impl App { self.status.set_mode(mode); } - async fn render_loop(&mut self, mut terminal: Terminal) -> Result<()> { + fn set_global_mode(&mut self, mode: GlobalMode) { + self.global_mode = mode; + self.status.set_global_mode(mode); + } + + async fn handle_help_key_press(&mut self, key: KeyEvent) -> Result<()> { use KeyCode::*; + match (key.modifiers, key.code) { + (KeyModifiers::NONE, Char('q')) => self.set_global_mode(GlobalMode::Default), + _ => (), + } + + Ok(()) + } + + async fn handle_global_key_press(&mut self, key: KeyEvent) -> Result { + use KeyCode::*; + + match (key.modifiers, key.code) { + (KeyModifiers::NONE, F(1)) => self.set_global_mode(GlobalMode::Help), + + _ => (), + } + + if key.kind == KeyEventKind::Press && key.modifiers == KeyModifiers::NONE { + match key.code { + Char('q') | Esc => { + if self.global_mode != GlobalMode::Default { + self.set_global_mode(GlobalMode::Default); + } else { + return Ok(true); + } + } + + Char('j') | Down => { + self.room_list.next_room(); + } + + Char('k') | Up => { + self.room_list.previous_room(); + } + + Char('s') => self.sync_service.start().await, + Char('S') => self.sync_service.stop().await, + + Char('Q') => { + let q = self.client.send_queue(); + let enabled = q.is_enabled(); + q.set_enabled(!enabled).await; + } + + Char('M') => { + let selected = self.room_list.get_selected_room_id(); + + if let Some(sdk_timeline) = selected.and_then(|room_id| { + self.timelines + .lock() + .get(&room_id) + .map(|timeline| timeline.timeline.clone()) + }) { + match sdk_timeline + .send( + RoomMessageEventContent::text_plain(format!( + "hey {}", + MilliSecondsSinceUnixEpoch::now().get() + )) + .into(), + ) + .await + { + Ok(_) => { + self.status.set_message("message sent!".to_owned()); + } + Err(err) => { + self.status.set_message(format!("error when sending event: {err}")); + } + } + } else { + self.status.set_message("missing timeline for room".to_owned()); + }; + } + + Char('L') => self.toggle_reaction_to_latest_msg().await, + + Char('r') => { + if self.room_list.get_selected_room_id().is_some() { + self.set_mode(DetailsMode::ReadReceipts); + } + } + Char('t') => self.set_mode(DetailsMode::TimelineItems), + Char('e') => self.set_mode(DetailsMode::Events), + Char('l') => self.set_mode(DetailsMode::LinkedChunk), + + Char('b') + if self.details_mode == DetailsMode::TimelineItems + || self.details_mode == DetailsMode::LinkedChunk => + { + self.back_paginate(); + } + + Char('m') if self.details_mode == DetailsMode::ReadReceipts => { + self.room_list.mark_as_read().await + } + + _ => {} + } + } + + Ok(false) + } + + async fn render_loop(&mut self, mut terminal: Terminal) -> Result<()> { loop { terminal.draw(|f| f.render_widget(&mut *self, f.area()))?; if event::poll(Duration::from_millis(16))? { if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - match key.code { - Char('q') | Esc => return Ok(()), - - Char('j') | Down => { - self.room_list.next_room(); + match self.global_mode { + GlobalMode::Default => { + if self.handle_global_key_press(key).await? { + break; } - - Char('k') | Up => { - self.room_list.previous_room(); - } - - Char('s') => self.sync_service.start().await, - Char('S') => self.sync_service.stop().await, - - Char('Q') => { - let q = self.client.send_queue(); - let enabled = q.is_enabled(); - q.set_enabled(!enabled).await; - } - - Char('M') => { - let selected = self.room_list.get_selected_room_id(); - - if let Some(sdk_timeline) = selected.and_then(|room_id| { - self.timelines - .lock() - .get(&room_id) - .map(|timeline| timeline.timeline.clone()) - }) { - match sdk_timeline - .send( - RoomMessageEventContent::text_plain(format!( - "hey {}", - MilliSecondsSinceUnixEpoch::now().get() - )) - .into(), - ) - .await - { - Ok(_) => { - self.status.set_message("message sent!".to_owned()); - } - Err(err) => { - self.status.set_message(format!( - "error when sending event: {err}" - )); - } - } - } else { - self.status.set_message("missing timeline for room".to_owned()); - }; - } - - Char('L') => self.toggle_reaction_to_latest_msg().await, - - Char('r') => { - if self.room_list.get_selected_room_id().is_some() { - self.set_mode(DetailsMode::ReadReceipts); - } - } - Char('t') => self.set_mode(DetailsMode::TimelineItems), - Char('e') => self.set_mode(DetailsMode::Events), - Char('l') => self.set_mode(DetailsMode::LinkedChunk), - - Char('b') - if self.details_mode == DetailsMode::TimelineItems - || self.details_mode == DetailsMode::LinkedChunk => - { - self.back_paginate(); - } - - Char('m') if self.details_mode == DetailsMode::ReadReceipts => { - self.room_list.mark_as_read().await - } - - _ => {} } + GlobalMode::Help => self.handle_help_key_press(key).await?, } } } } + + Ok(()) } async fn run(&mut self, terminal: Terminal) -> Result<()> { @@ -497,6 +552,14 @@ impl Widget for &mut App { self.room_list.render(room_list_area, buf); self.render_right(rhs, buf); self.status.render(status_area, buf); + + match self.global_mode { + GlobalMode::Default => (), + GlobalMode::Help => { + let mut help_view = HelpView::new(); + help_view.render(area, buf); + } + } } } @@ -550,7 +613,6 @@ impl App { let room = rooms.get(&room_id); let mut read_receipts = ReadReceipts::new(room); - read_receipts.render(inner_area, buf); } diff --git a/labs/multiverse/src/status.rs b/labs/multiverse/src/status.rs index d0e3d7330..fe73a5117 100644 --- a/labs/multiverse/src/status.rs +++ b/labs/multiverse/src/status.rs @@ -17,7 +17,7 @@ use tokio::{ time::sleep, }; -use crate::DetailsMode; +use crate::{DetailsMode, GlobalMode}; const MESSAGE_DURATION: Duration = Duration::from_secs(4); @@ -30,6 +30,7 @@ pub struct Status { _receiver_task: JoinHandle<()>, mode: DetailsMode, + global_mode: GlobalMode, } pub struct StatusHandle { @@ -63,6 +64,7 @@ impl Status { _receiver_task: receiver_task, message_sender, mode: DetailsMode::default(), + global_mode: GlobalMode::default(), } } @@ -104,6 +106,10 @@ impl Status { self.mode = mode; } + pub(super) fn set_global_mode(&mut self, mode: GlobalMode) { + self.global_mode = mode; + } + pub fn handle(&self) -> StatusHandle { StatusHandle { message_sender: self.message_sender.clone() } } @@ -121,24 +127,27 @@ impl Widget for &mut Status { let content = if let Some(status_message) = status_message.as_deref() { status_message } else { - match self.mode { - DetailsMode::ReadReceipts => { - "\nUse j/k to move, s/S to start/stop the sync service, \ + match self.global_mode { + GlobalMode::Help => "Press q to exit the help screen", + GlobalMode::Default => match self.mode { + DetailsMode::ReadReceipts => { + "\nUse j/k to move, s/S to start/stop the sync service, \ m to mark as read, t to show the timeline, e to show events." - } - DetailsMode::TimelineItems => { - "\nUse j/k to move, s/S to start/stop the sync service, \ + } + DetailsMode::TimelineItems => { + "\nUse j/k to move, s/S to start/stop the sync service, \ r to show read receipts, e to show events, Q to enable/disable \ the send queue, M to send a message, L to like the last message." - } - DetailsMode::Events => { - "\nUse j/k to move, s/S to start/stop the sync service, r to show \ + } + DetailsMode::Events => { + "\nUse j/k to move, s/S to start/stop the sync service, r to show \ read receipts, t to show the timeline" - } - DetailsMode::LinkedChunk => { - "\nUse j/k to move, s/S to start/stop the sync service, r to show \ + } + DetailsMode::LinkedChunk => { + "\nUse j/k to move, s/S to start/stop the sync service, r to show \ read receipts, t to show the timeline, e to show events" - } + } + }, } };