mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-18 21:52:30 -04:00
feat(multiverse): Add a help screen
This commit is contained in:
42
labs/multiverse/src/help.rs
Normal file
42
labs/multiverse/src/help.rs
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<Url>,
|
||||
}
|
||||
|
||||
#[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<Mutex<Option<JoinHandle<()>>>>,
|
||||
|
||||
/// 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<impl Backend>) -> 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<bool> {
|
||||
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<impl Backend>) -> 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<impl Backend>) -> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user