feat(multiverse): Settings view

This commit is contained in:
Damir Jelić
2025-03-27 15:49:34 +01:00
parent 226229d63b
commit 036d14e9e3
7 changed files with 325 additions and 55 deletions

View File

@@ -31,10 +31,7 @@ use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
use tokio::{spawn, task::JoinHandle};
use tracing::{error, warn};
use tracing_subscriber::EnvFilter;
use widgets::{
recovery::{RecoveryView, RecoveryViewState},
room_view::RoomView,
};
use widgets::{room_view::RoomView, settings::SettingsView};
use crate::widgets::{
help::HelpView,
@@ -74,8 +71,8 @@ pub enum GlobalMode {
Default,
/// Mode where we have opened the help screen.
Help,
/// Mode where we have opened the recovery screen.
Recovery { state: RecoveryViewState },
/// Mode where we have opened the settings screen.
Settings { view: SettingsView },
}
/// Helper function to create a centered rect using up certain percentage of the
@@ -316,8 +313,8 @@ impl App {
match (key.modifiers, key.code) {
(KeyModifiers::NONE, F(1)) => self.set_global_mode(GlobalMode::Help),
(KeyModifiers::NONE, F(10)) => self.set_global_mode(GlobalMode::Recovery {
state: RecoveryViewState::new(self.client.clone()),
(KeyModifiers::NONE, F(10)) => self.set_global_mode(GlobalMode::Settings {
view: SettingsView::new(self.client.clone(), self.sync_service.clone()),
}),
(KeyModifiers::CONTROL, Char('j') | Down) => {
@@ -343,32 +340,14 @@ impl App {
_ => self.room_view.handle_key_press(key).await,
}
// TODO: Remove the remaining keys.
if key.kind == KeyEventKind::Press && key.modifiers == KeyModifiers::NONE {
match key.code {
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;
}
_ => {}
}
}
Ok(false)
}
fn on_tick(&mut self) {
match &mut self.state.global_mode {
GlobalMode::Help | GlobalMode::Default => {}
GlobalMode::Recovery { state } => {
state.on_tick();
GlobalMode::Settings { view } => {
view.on_tick();
}
}
}
@@ -393,8 +372,8 @@ impl App {
}
_ => (),
},
GlobalMode::Recovery { state } => {
if state.handle_key_press(key) {
GlobalMode::Settings { view } => {
if view.handle_key_press(key).await {
self.set_global_mode(GlobalMode::Default);
}
}
@@ -453,9 +432,8 @@ impl Widget for &mut App {
match &mut self.state.global_mode {
GlobalMode::Default => {}
GlobalMode::Recovery { state } => {
let mut recovery_view = RecoveryView::new();
recovery_view.render(area, buf, state);
GlobalMode::Settings { view } => {
view.render(area, buf);
}
GlobalMode::Help => {
let mut help_view = HelpView::new();

View File

@@ -2,4 +2,5 @@ pub mod help;
pub mod recovery;
pub mod room_list;
pub mod room_view;
pub mod settings;
pub mod status;

View File

@@ -80,7 +80,7 @@ impl DefaultRecoveryView {
match self.mode {
Mode::Default => match key.code {
KeyCode::Esc => Yes,
KeyCode::Esc | KeyCode::Char('q') => Yes,
KeyCode::Char('j') | KeyCode::Down => {
self.state.select_next();
No

View File

@@ -1,9 +1,8 @@
use crossterm::event::{KeyCode, KeyEvent};
use layout::Flex;
use matrix_sdk::{encryption::recovery::RecoveryState, Client};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Padding},
widgets::{Block, Borders, Padding},
};
use recovering::RecoveringView;
use throbber_widgets_tui::{Throbber, ThrobberState};
@@ -104,7 +103,7 @@ impl RecoveryViewState {
match &mut self.mode {
Mode::Unknown => match (key.modifiers, key.code) {
(_, Esc) => true,
(_, Esc | Char('q')) => true,
_ => false,
},
Mode::Incomplete { view } => match view.handle_key(key) {
@@ -163,23 +162,6 @@ impl StatefulWidget for &mut RecoveryView {
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
state.update_state();
// Create a centered popout with 8 lines and 70 rows. This size is picked so the
// recovery view looks a bit like a settings screen.
let vertical =
Layout::vertical([Constraint::Fill(1), Constraint::Length(10), Constraint::Fill(1)])
.flex(Flex::Center);
let horizontal =
Layout::horizontal([Constraint::Fill(1), Constraint::Min(50), Constraint::Fill(1)])
.flex(Flex::Center);
let [_, area, _] = vertical.areas(area);
let [_, area, _] = horizontal.areas(area);
// Clear that part of the screen so we can draw our bounding block and other
// widgets inside the block.
Clear.render(area, buf);
// Render our block, mainly for the border.
let block = Block::bordered()
.title(Line::from("Encryption").centered())

View File

@@ -0,0 +1,104 @@
use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent};
use matrix_sdk::Client;
use matrix_sdk_ui::sync_service::{self, SyncService};
use ratatui::{
prelude::*,
widgets::{HighlightSpacing, *},
};
// TODO: This replicates a lot of the logic the details view has, we should make
// a generic tab popout widget to share a bit of logic here.
enum MenuEntries {
Sync = 0,
SendQueue = 1,
}
impl From<usize> for MenuEntries {
fn from(value: usize) -> Self {
match value {
0 => MenuEntries::Sync,
1 => MenuEntries::SendQueue,
_ => unreachable!("The developer settings view has only 2 options"),
}
}
}
pub struct DeveloperSettingsView {
client: Client,
sync_service: Arc<SyncService>,
state: ListState,
}
impl DeveloperSettingsView {
pub fn new(client: Client, sync_service: Arc<SyncService>) -> Self {
Self { client, sync_service, state: ListState::default() }
}
pub async fn handle_key_press(&mut self, key: KeyEvent) {
use MenuEntries::*;
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
self.state.select_next();
}
KeyCode::Char('k') | KeyCode::Up => {
self.state.select_previous();
}
KeyCode::Enter | KeyCode::Char(' ') => {
if let Some(selected) = self.state.selected() {
match selected.into() {
Sync => {
let sync_service = &self.sync_service;
match sync_service.state().get() {
sync_service::State::Running => sync_service.stop().await,
sync_service::State::Idle
| sync_service::State::Terminated
| sync_service::State::Error
| sync_service::State::Offline => sync_service.start().await,
}
}
SendQueue => {
let send_queue = self.client.send_queue();
let enabled = send_queue.is_enabled();
send_queue.set_enabled(!enabled).await
}
}
}
}
_ => (),
}
}
}
impl Widget for &mut DeveloperSettingsView {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let sync_item = match self.sync_service.state().get() {
sync_service::State::Running => ListItem::new("Sync [x]"),
sync_service::State::Idle
| sync_service::State::Terminated
| sync_service::State::Error
| sync_service::State::Offline => ListItem::new("Sync [ ]"),
};
let send_queue_item = if self.client.send_queue().is_enabled() {
ListItem::new("Send Queue [x]")
} else {
ListItem::new("Send Queue [ ]")
};
let list = List::new(vec![sync_item, send_queue_item])
.highlight_symbol("> ")
.highlight_spacing(HighlightSpacing::Always);
StatefulWidget::render(list, area, buf, &mut self.state);
}
}

View File

@@ -0,0 +1,205 @@
use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent};
use developer::DeveloperSettingsView;
use matrix_sdk::Client;
use matrix_sdk_ui::sync_service::SyncService;
use ratatui::{prelude::*, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
use style::palette::tailwind;
use super::recovery::{RecoveryView, RecoveryViewState};
use crate::popup_area;
mod developer;
// TODO: This replicates a lot of the logic the details view has, we should make
// a generic tab popout widget to share a bit of logic here.
#[derive(Clone, Copy, Default, Display, FromRepr, EnumIter)]
enum SelectedTab {
/// Show the developer settings we have.
#[default]
Developer,
/// Show the encryption settings
Encryption,
}
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current
/// tab.
fn previous(self) -> Self {
let current_index: usize = self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self)
}
/// Cycle to the next tab, if we're at the last tab we return the first and
/// default tab.
fn cycle_next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(SelectedTab::default())
}
/// Cycle to the previous tab, if we're at the first tab we return the last
/// tab.
fn cycle_prev(self) -> Self {
let current_index = self as usize;
if current_index == 0 {
Self::iter().last().expect("We should always have a last element in our enum")
} else {
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self)
}
}
/// Return tab's name as a styled `Line`
fn title(self) -> Line<'static> {
format!(" {self} ").fg(tailwind::SLATE.c200).bg(self.palette().c900).into()
}
const fn palette(&self) -> tailwind::Palette {
match self {
Self::Developer => tailwind::BLUE,
Self::Encryption => tailwind::EMERALD,
}
}
}
pub struct SettingsView {
selected_tab: SelectedTab,
developer_settings_view: DeveloperSettingsView,
recovery_view_state: RecoveryViewState,
}
impl SettingsView {
pub fn new(client: Client, sync_service: Arc<SyncService>) -> Self {
let recovery_view_state = RecoveryViewState::new(client.clone());
let developer_settings_view = DeveloperSettingsView::new(client, sync_service);
Self { selected_tab: SelectedTab::default(), recovery_view_state, developer_settings_view }
}
pub async fn handle_key_press(&mut self, event: KeyEvent) -> bool {
use KeyCode::*;
match event.code {
Char('l') | Right => {
self.next_tab();
false
}
Tab => {
self.cycle_next_tab();
false
}
BackTab => {
self.cycle_prev_tab();
false
}
Char('h') | Left => {
self.previous_tab();
false
}
Char('q') | Esc => match self.selected_tab {
SelectedTab::Developer => true,
SelectedTab::Encryption => self.recovery_view_state.handle_key_press(event),
},
_ => match self.selected_tab {
SelectedTab::Developer => {
self.developer_settings_view.handle_key_press(event).await;
false
}
SelectedTab::Encryption => self.recovery_view_state.handle_key_press(event),
},
}
}
pub fn on_tick(&mut self) {
self.recovery_view_state.on_tick();
}
fn cycle_next_tab(&mut self) {
self.selected_tab = self.selected_tab.cycle_next();
}
fn cycle_prev_tab(&mut self) {
self.selected_tab = self.selected_tab.cycle_prev();
}
fn next_tab(&mut self) {
self.selected_tab = self.selected_tab.next();
}
fn previous_tab(&mut self) {
self.selected_tab = self.selected_tab.previous();
}
}
impl<'a> Widget for &'a mut SettingsView {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
use Constraint::{Length, Min};
let area = popup_area(area, 40, 30);
Clear.render(area, buf);
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
let [header_area, inner_area, footer_area] = vertical.areas(area);
let horizontal = Layout::horizontal([Min(0), Length(20)]);
let [tab_title_area, title_area] = horizontal.areas(header_area);
"Settings".bold().render(title_area, buf);
Block::bordered()
.border_set(symbols::border::PROPORTIONAL_TALL)
.padding(Padding::horizontal(1))
.border_style(tailwind::BLUE.c700)
.render(inner_area, buf);
let titles = SelectedTab::iter().map(SelectedTab::title);
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
let selected_tab_index = self.selected_tab as usize;
let tabs_area = inner_area.inner(Margin::new(1, 1));
Tabs::new(titles)
.highlight_style(highlight_style)
.select(selected_tab_index)
.padding("", "")
.divider(" ")
.render(tab_title_area, buf);
match self.selected_tab {
SelectedTab::Developer => {
self.developer_settings_view.render(tabs_area, buf);
}
SelectedTab::Encryption => {
let mut view = RecoveryView::new();
view.render(tabs_area, buf, &mut self.recovery_view_state);
}
}
Line::raw("◄ ► to change tab | Press q to exit the settings screen")
.centered()
.render(footer_area, buf);
}
}

View File

@@ -122,7 +122,7 @@ impl StatefulWidget for &mut Status {
match global_mode {
GlobalMode::Help => "Press q to exit the help screen",
GlobalMode::Recovery { .. } => "Press ESC to exit the recovery screen",
GlobalMode::Settings { .. } => "Press ESC to exit the settings screen",
GlobalMode::Default => "Press F1 to show the help screen",
}
};