From 2ecc5140a999ff642098e8f6276e803d19728b36 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sun, 23 Jun 2024 10:10:27 +0200 Subject: [PATCH 01/16] start implementing config --- Cargo.lock | 11 ++ Cargo.toml | 3 +- rm-config/Cargo.toml | 2 + rm-config/src/keymap.rs | 155 ++++++++++++++++++ rm-config/src/lib.rs | 74 ++++----- rm-config/src/utils.rs | 28 ++++ rm-main/Cargo.toml | 1 + rm-main/src/app.rs | 11 +- rm-main/src/main.rs | 1 - rm-main/src/transmission/action.rs | 11 +- rm-main/src/transmission/fetchers.rs | 2 +- rm-main/src/tui.rs | 10 +- rm-main/src/ui/components/mod.rs | 2 +- rm-main/src/ui/components/tabs.rs | 3 +- rm-main/src/ui/global_popups/error.rs | 8 +- rm-main/src/ui/global_popups/help.rs | 2 +- rm-main/src/ui/global_popups/mod.rs | 3 +- rm-main/src/ui/mod.rs | 14 +- rm-main/src/ui/tabs/search.rs | 2 +- rm-main/src/ui/tabs/torrents/input_manager.rs | 3 +- rm-main/src/ui/tabs/torrents/mod.rs | 2 +- rm-main/src/ui/tabs/torrents/popups/files.rs | 2 +- rm-main/src/ui/tabs/torrents/popups/mod.rs | 3 +- rm-main/src/ui/tabs/torrents/popups/stats.rs | 2 +- rm-main/src/ui/tabs/torrents/task_manager.rs | 3 +- .../src/ui/tabs/torrents/tasks/add_magnet.rs | 2 +- .../ui/tabs/torrents/tasks/delete_torrent.rs | 2 +- rm-main/src/ui/tabs/torrents/tasks/filter.rs | 2 +- rm-shared/Cargo.toml | 13 ++ {rm-main => rm-shared}/src/action.rs | 39 +++-- rm-shared/src/event.rs | 9 + rm-shared/src/lib.rs | 2 + 32 files changed, 327 insertions(+), 100 deletions(-) create mode 100644 rm-config/src/keymap.rs create mode 100644 rm-config/src/utils.rs create mode 100644 rm-shared/Cargo.toml rename {rm-main => rm-shared}/src/action.rs (76%) create mode 100644 rm-shared/src/event.rs create mode 100644 rm-shared/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 61b9ac5..5b46f11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,6 +276,7 @@ dependencies = [ "libc", "mio", "parking_lot", + "serde", "signal-hook", "signal-hook-mio", "winapi", @@ -1245,13 +1246,22 @@ name = "rm-config" version = "0.3.3" dependencies = [ "anyhow", + "crossterm", "ratatui", + "rm-shared", "serde", "toml", "url", "xdg", ] +[[package]] +name = "rm-shared" +version = "0.3.3" +dependencies = [ + "crossterm", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1349,6 +1359,7 @@ dependencies = [ "magnetease", "ratatui", "rm-config", + "rm-shared", "serde", "tokio", "tokio-util", diff --git a/Cargo.toml b/Cargo.toml index e508dfa..7be944a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ license = "GPL-3.0-or-later" [workspace.dependencies] rm-config = { version = "0.3", path = "rm-config" } +rm-shared = { version = "0.3", path = "rm-shared" } magnetease = "0.1" anyhow = "1" @@ -33,7 +34,7 @@ tokio-util = "0.7" futures = "0.3" # TUI -crossterm = { version = "0.27", features = ["event-stream"] } +crossterm = { version = "0.27", features = ["event-stream", "serde"] } ratatui = { version = "0.26", features = ["serde"] } tui-input = "0.8" tui-tree-widget = "0.20" diff --git a/rm-config/Cargo.toml b/rm-config/Cargo.toml index a519736..f91ee6d 100644 --- a/rm-config/Cargo.toml +++ b/rm-config/Cargo.toml @@ -11,11 +11,13 @@ homepage.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +rm-shared.workspace = true xdg.workspace = true toml.workspace = true serde.workspace = true anyhow.workspace = true url.workspace = true ratatui.workspace = true +crossterm.workspace = true diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs new file mode 100644 index 0000000..6184522 --- /dev/null +++ b/rm-config/src/keymap.rs @@ -0,0 +1,155 @@ +use std::collections::HashMap; + +use anyhow::Result; +use crossterm::event::{KeyCode, KeyModifiers}; +use serde::{Deserialize, Serialize}; +use toml::Table; + +use crate::{utils, KEYMAP_CONFIG_FILENAME}; +use rm_shared::action::Action; + +#[derive(Serialize, Deserialize)] +pub struct Keymap { + general: General, + torrents_tab: TorrentsTab, +} + +#[derive(Serialize, Deserialize)] +struct General> { + keybindings: Vec>, +} + +#[derive(Serialize, Deserialize, Debug)] +enum GeneralAction { + ShowHelp, + Quit, + SoftQuit, + SwitchToTorrents, + SwitchToSearch, + Left, + Right, + Down, + Up, + Search, + SwitchFocus, + Confirm, + PageDown, + PageUp, + Home, + End, +} + +impl From for Action { + fn from(value: GeneralAction) -> Self { + match value { + GeneralAction::ShowHelp => Action::ShowHelp, + GeneralAction::Quit => Action::Quit, + GeneralAction::SoftQuit => Action::SoftQuit, + GeneralAction::SwitchToTorrents => Action::ChangeTab(1), + GeneralAction::SwitchToSearch => Action::ChangeTab(2), + GeneralAction::Left => Action::Left, + GeneralAction::Right => Action::Right, + GeneralAction::Down => Action::Down, + GeneralAction::Up => Action::Up, + GeneralAction::Search => Action::Search, + GeneralAction::SwitchFocus => Action::ChangeFocus, + GeneralAction::Confirm => Action::Confirm, + GeneralAction::PageDown => Action::ScrollDownPage, + GeneralAction::PageUp => Action::ScrollUpPage, + GeneralAction::Home => Action::Home, + GeneralAction::End => Action::End, + } + } +} + +#[derive(Serialize, Deserialize)] +struct TorrentsTab> { + keybindings: Vec>, +} + +#[derive(Serialize, Deserialize, Debug)] +enum TorrentsAction { + AddMagnet, + Pause, + DeleteWithFiles, + DeleteWithoutFiles, + ShowFiles, + ShowStats, +} + +impl From for Action { + fn from(value: TorrentsAction) -> Self { + match value { + TorrentsAction::AddMagnet => Action::AddMagnet, + TorrentsAction::Pause => Action::Pause, + TorrentsAction::DeleteWithFiles => Action::DeleteWithFiles, + TorrentsAction::DeleteWithoutFiles => Action::DeleteWithoutFiles, + TorrentsAction::ShowFiles => Action::ShowFiles, + TorrentsAction::ShowStats => Action::ShowStats, + } + } +} + +#[derive(Serialize, Deserialize)] +struct Keybinding> { + on: KeyCode, + #[serde(default)] + modifier: KeyModifier, + action: T, +} + +#[derive(Serialize, Deserialize, Hash)] +enum KeyModifier { + None, + Ctrl, + Shift, +} + +impl From for KeyModifiers { + fn from(value: KeyModifier) -> Self { + match value { + KeyModifier::None => KeyModifiers::NONE, + KeyModifier::Ctrl => KeyModifiers::CONTROL, + KeyModifier::Shift => KeyModifiers::SHIFT, + } + } +} + +impl Default for KeyModifier { + fn default() -> Self { + Self::None + } +} + +impl Keymap { + pub fn init() -> Result { + let table = { + if let Ok(table) = utils::fetch_config_table(KEYMAP_CONFIG_FILENAME) { + table + } else { + todo!(); + } + }; + + Self::table_to_keymap(&table) + } + + pub fn to_hashmap(self) -> HashMap<(KeyCode, KeyModifiers), Action> { + let mut hashmap = HashMap::new(); + for keybinding in self.general.keybindings { + let hash_value = (keybinding.on, keybinding.modifier.into()); + hashmap.insert(hash_value, keybinding.action.into()); + } + for keybinding in self.torrents_tab.keybindings { + let hash_value = (keybinding.on, keybinding.modifier.into()); + hashmap.insert(hash_value, keybinding.action.into()); + } + hashmap + } + + fn table_to_keymap(table: &Table) -> Result { + let config_string = table.to_string(); + let config = toml::from_str(&config_string)?; + Ok(config) + } +} diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index b510760..ddfae80 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -1,20 +1,25 @@ -use std::{ - fs::File, - io::{Read, Write}, - path::PathBuf, - sync::OnceLock, -}; +mod keymap; +mod utils; + +use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; use anyhow::{bail, Context, Result}; +use crossterm::event::{KeyCode, KeyModifiers}; use ratatui::style::Color; +use rm_shared::action::Action; use serde::{Deserialize, Serialize}; use toml::Table; use xdg::BaseDirectories; -#[derive(Debug, Serialize, Deserialize)] +use crate::utils::put_config; +use keymap::Keymap; + +#[derive(Serialize, Deserialize)] pub struct Config { pub connection: Connection, pub general: General, + #[serde(skip)] + pub keymap: Option>, } #[derive(Debug, Serialize, Deserialize)] @@ -45,41 +50,34 @@ pub struct Connection { const DEFAULT_CONFIG: &str = include_str!("../defaults/config.toml"); static XDG_DIRS: OnceLock = OnceLock::new(); static CONFIG_PATH: OnceLock = OnceLock::new(); +pub const MAIN_CONFIG_FILENAME: &str = "config.toml"; +pub const KEYMAP_CONFIG_FILENAME: &str = "keymap.toml"; + +pub fn xdg_dirs() -> &'static BaseDirectories { + XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("rustmission").unwrap()) +} + +pub fn get_config_path(filename: &str) -> &'static PathBuf { + CONFIG_PATH.get_or_init(|| xdg_dirs().place_config_file(filename).unwrap()) +} impl Config { pub fn init() -> Result { - let Ok(table) = Self::table_from_home() else { - Self::put_default_conf_in_home()?; + let Ok(table) = utils::fetch_config_table(MAIN_CONFIG_FILENAME) else { + put_config(DEFAULT_CONFIG, MAIN_CONFIG_FILENAME)?; // TODO: check if the user really changed the config. println!( "Update {:?} and start rustmission again", - Self::get_config_path() + get_config_path(MAIN_CONFIG_FILENAME) ); std::process::exit(0); }; Self::table_config_verify(&table)?; - Self::table_to_config(&table) - } - - fn table_from_home() -> Result { - let xdg_dirs = xdg::BaseDirectories::with_prefix("rustmission")?; - let config_path = xdg_dirs - .find_config_file("config.toml") - .ok_or_else(|| anyhow::anyhow!("config.toml not found"))?; - - let mut config_buf = String::new(); - let mut config_file = File::open(config_path).unwrap(); - config_file.read_to_string(&mut config_buf).unwrap(); - Ok(toml::from_str(&config_buf)?) - } - - fn put_default_conf_in_home() -> Result
{ - let config_path = Self::get_config_path(); - let mut config_file = File::create(config_path)?; - config_file.write_all(DEFAULT_CONFIG.as_bytes())?; - Ok(toml::from_str(DEFAULT_CONFIG)?) + let mut config = Self::table_to_config(&table)?; + config.keymap = Some(Keymap::init().unwrap().to_hashmap()); + Ok(config) } fn table_to_config(table: &Table) -> Result { @@ -99,31 +97,19 @@ impl Config { .with_context(|| { format!( "no url given in: {}", - Self::get_config_path().to_str().unwrap() + get_config_path(MAIN_CONFIG_FILENAME).to_str().unwrap() ) })?; url::Url::parse(url).with_context(|| { format!( "invalid url '{url}' in {}", - Self::get_config_path().to_str().unwrap() + get_config_path(MAIN_CONFIG_FILENAME).to_str().unwrap() ) })?; Ok(()) } - - fn get_xdg_dirs() -> &'static BaseDirectories { - XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("rustmission").unwrap()) - } - - pub fn get_config_path() -> &'static PathBuf { - CONFIG_PATH.get_or_init(|| { - Self::get_xdg_dirs() - .place_config_file("config.toml") - .unwrap() - }) - } } #[cfg(test)] diff --git a/rm-config/src/utils.rs b/rm-config/src/utils.rs new file mode 100644 index 0000000..a029181 --- /dev/null +++ b/rm-config/src/utils.rs @@ -0,0 +1,28 @@ +use std::{ + fs::File, + io::{Read, Write}, +}; + +use anyhow::Result; +use toml::Table; + +use crate::get_config_path; + +pub fn fetch_config_table(config_name: &str) -> Result
{ + let config_path = crate::xdg_dirs() + .find_config_file(config_name) + .ok_or_else(|| anyhow::anyhow!("{} not found", config_name))?; + + let mut config_buf = String::new(); + let mut config_file = File::open(config_path).unwrap(); + config_file.read_to_string(&mut config_buf).unwrap(); + + Ok(toml::from_str(&config_buf)?) +} + +pub fn put_config(content: &str, filename: &str) -> Result
{ + let config_path = get_config_path(filename); + let mut config_file = File::create(config_path)?; + config_file.write_all(content.as_bytes())?; + Ok(toml::from_str(content)?) +} diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index bf320bb..16b109a 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" [dependencies] rm-config.workspace = true +rm-shared.workspace = true magnetease.workspace = true anyhow.workspace = true serde.workspace = true diff --git a/rm-main/src/app.rs b/rm-main/src/app.rs index 89f8620..dfdc0a9 100644 --- a/rm-main/src/app.rs +++ b/rm-main/src/app.rs @@ -1,8 +1,10 @@ use rm_config::Config; +use rm_shared::action::event_to_action; +use rm_shared::action::Action; +use rm_shared::action::Mode; use std::sync::Arc; use crate::{ - action::{event_to_action, Action, Mode}, transmission::{self, TorrentAction}, tui::Tui, ui::{components::Component, MainWindow}, @@ -44,9 +46,10 @@ impl Ctx { }); } Err(e) => { - let config_path = Config::get_config_path().to_str().unwrap(); + let config_path = rm_config::get_config_path(rm_config::MAIN_CONFIG_FILENAME); return Err(Error::msg(format!( - "{e}\nIs the connection info in {config_path} correct?" + "{e}\nIs the connection info in {:?} correct?", + config_path ))); } } @@ -108,7 +111,7 @@ impl App { tokio::select! { event = tui_event => { - if let Some(action) = event_to_action(self.mode, event.unwrap()) { + if let Some(action) = event_to_action(self.mode, event.unwrap(), self.ctx.config.keymap.as_ref().unwrap()) { if let Some(action) = self.update(action).await { self.ctx.action_tx.send(action).unwrap(); } diff --git a/rm-main/src/main.rs b/rm-main/src/main.rs index 7168fa1..a55d156 100644 --- a/rm-main/src/main.rs +++ b/rm-main/src/main.rs @@ -1,4 +1,3 @@ -mod action; pub mod app; mod cli; pub mod transmission; diff --git a/rm-main/src/transmission/action.rs b/rm-main/src/transmission/action.rs index 6d49760..781ffc9 100644 --- a/rm-main/src/transmission/action.rs +++ b/rm-main/src/transmission/action.rs @@ -5,7 +5,9 @@ use transmission_rpc::types::{ Id, SessionGet, Torrent, TorrentAction as RPCAction, TorrentAddArgs, TorrentSetArgs, }; -use crate::{action::Action, app, ui::global_popups::ErrorPopup}; +use crate::app; +use rm_shared::action::Action; +use rm_shared::action::ErrorMessage; #[derive(Debug)] pub enum TorrentAction { @@ -37,8 +39,11 @@ pub async fn action_handler(ctx: app::Ctx, mut trans_rx: UnboundedReceiver { diff --git a/rm-main/src/transmission/fetchers.rs b/rm-main/src/transmission/fetchers.rs index db98d4e..b6ff5ff 100644 --- a/rm-main/src/transmission/fetchers.rs +++ b/rm-main/src/transmission/fetchers.rs @@ -6,10 +6,10 @@ use std::{ use transmission_rpc::types::{FreeSpace, SessionStats, TorrentGetField}; use crate::{ - action::Action, app, ui::tabs::torrents::{rustmission_torrent::RustmissionTorrent, table_manager::TableManager}, }; +use rm_shared::action::Action; pub async fn stats(ctx: app::Ctx, stats: Arc>>) { loop { diff --git a/rm-main/src/tui.rs b/rm-main/src/tui.rs index 31a80ec..492e2bb 100644 --- a/rm-main/src/tui.rs +++ b/rm-main/src/tui.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::Result; use crossterm::{ cursor, - event::{Event as CrosstermEvent, KeyEvent, KeyEventKind}, + event::{Event as CrosstermEvent, KeyEventKind}, terminal::{EnterAlternateScreen, LeaveAlternateScreen}, }; use futures::{FutureExt, StreamExt}; @@ -14,13 +14,7 @@ use tokio::{ }; use tokio_util::sync::CancellationToken; -#[derive(Clone, Debug)] -pub enum Event { - Quit, - Error, - Render, - Key(KeyEvent), -} +use rm_shared::event::Event; pub struct Tui { pub terminal: ratatui::Terminal>, diff --git a/rm-main/src/ui/components/mod.rs b/rm-main/src/ui/components/mod.rs index 39ee924..60212ed 100644 --- a/rm-main/src/ui/components/mod.rs +++ b/rm-main/src/ui/components/mod.rs @@ -4,7 +4,7 @@ pub mod tabs; use ratatui::prelude::*; use ratatui::Frame; -use crate::action::Action; +use rm_shared::action::Action; pub use tabs::TabComponent; pub trait Component { diff --git a/rm-main/src/ui/components/tabs.rs b/rm-main/src/ui/components/tabs.rs index 0a56e85..293d4a4 100644 --- a/rm-main/src/ui/components/tabs.rs +++ b/rm-main/src/ui/components/tabs.rs @@ -1,4 +1,5 @@ -use crate::{action::Action, app}; +use crate::app; +use rm_shared::action::Action; use super::Component; use ratatui::{layout::Flex, prelude::*, widgets::Tabs}; diff --git a/rm-main/src/ui/global_popups/error.rs b/rm-main/src/ui/global_popups/error.rs index bedeca3..8963299 100644 --- a/rm-main/src/ui/global_popups/error.rs +++ b/rm-main/src/ui/global_popups/error.rs @@ -3,10 +3,8 @@ use ratatui::{ widgets::{Block, Clear, Paragraph, Wrap}, }; -use crate::{ - action::Action, - ui::{centered_rect, components::Component}, -}; +use crate::ui::{centered_rect, components::Component}; +use rm_shared::action::Action; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ErrorPopup { @@ -16,7 +14,7 @@ pub struct ErrorPopup { } impl ErrorPopup { - pub fn new(title: &'static str, message: String) -> Self { + pub fn new(title: &str, message: String) -> Self { Self { title: title.to_owned(), message, diff --git a/rm-main/src/ui/global_popups/help.rs b/rm-main/src/ui/global_popups/help.rs index 2b06f52..fdbacc9 100644 --- a/rm-main/src/ui/global_popups/help.rs +++ b/rm-main/src/ui/global_popups/help.rs @@ -7,10 +7,10 @@ use ratatui::{ }; use crate::{ - action::Action, app, ui::{centered_rect, components::Component}, }; +use rm_shared::action::Action; macro_rules! add_line { ($lines:expr, $key:expr, $description:expr) => { diff --git a/rm-main/src/ui/global_popups/mod.rs b/rm-main/src/ui/global_popups/mod.rs index 91cf956..2ea2007 100644 --- a/rm-main/src/ui/global_popups/mod.rs +++ b/rm-main/src/ui/global_popups/mod.rs @@ -6,7 +6,8 @@ use ratatui::prelude::*; pub use error::ErrorPopup; pub use help::HelpPopup; -use crate::{action::Action, app}; +use crate::app; +use rm_shared::action::Action; use super::components::Component; diff --git a/rm-main/src/ui/mod.rs b/rm-main/src/ui/mod.rs index 3db2bde..43954ab 100644 --- a/rm-main/src/ui/mod.rs +++ b/rm-main/src/ui/mod.rs @@ -2,16 +2,15 @@ pub mod components; pub mod global_popups; pub mod tabs; -use crate::ui::tabs::torrents::TorrentsTab; +use crate::ui::{global_popups::ErrorPopup, tabs::torrents::TorrentsTab}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::prelude::*; use tui_input::InputRequest; -use crate::{ - action::Action, - app::{self}, -}; +use rm_shared::action::Action; + +use crate::app::{self}; use self::{ components::{tabs::CurrentTab, Component, TabComponent}, @@ -45,8 +44,9 @@ impl Component for MainWindow { use Action as A; match action { - A::Error(e_popup) => { - self.global_popup_manager.error_popup = Some(*e_popup); + A::Error(error) => { + let error_popup = ErrorPopup::new(&error.title, error.message); + self.global_popup_manager.error_popup = Some(error_popup); Some(A::Render) } A::ShowHelp => self.global_popup_manager.handle_actions(action), diff --git a/rm-main/src/ui/tabs/search.rs b/rm-main/src/ui/tabs/search.rs index 927207a..c89d44e 100644 --- a/rm-main/src/ui/tabs/search.rs +++ b/rm-main/src/ui/tabs/search.rs @@ -14,7 +14,6 @@ use tokio::sync::mpsc::{self, UnboundedSender}; use tui_input::Input; use crate::{ - action::Action, app, transmission::TorrentAction, ui::{ @@ -23,6 +22,7 @@ use crate::{ }, utils::bytes_to_human_format, }; +use rm_shared::action::Action; #[derive(Clone, Copy, PartialEq, Eq)] enum SearchFocus { diff --git a/rm-main/src/ui/tabs/torrents/input_manager.rs b/rm-main/src/ui/tabs/torrents/input_manager.rs index 93ff5ae..ba8cdd8 100644 --- a/rm-main/src/ui/tabs/torrents/input_manager.rs +++ b/rm-main/src/ui/tabs/torrents/input_manager.rs @@ -4,7 +4,8 @@ use ratatui::{ }; use tui_input::{Input, InputRequest}; -use crate::{action::Action, app, ui::components::Component}; +use crate::{app, ui::components::Component}; +use rm_shared::action::Action; pub struct InputManager { input: Input, diff --git a/rm-main/src/ui/tabs/torrents/mod.rs b/rm-main/src/ui/tabs/torrents/mod.rs index 9e83925..46582ec 100644 --- a/rm-main/src/ui/tabs/torrents/mod.rs +++ b/rm-main/src/ui/tabs/torrents/mod.rs @@ -15,10 +15,10 @@ use ratatui::prelude::*; use ratatui::widgets::{Row, Table}; use transmission_rpc::types::TorrentStatus; -use crate::action::Action; use crate::ui::components::table::GenericTable; use crate::ui::components::Component; use crate::{app, transmission}; +use rm_shared::action::Action; use self::bottom_stats::BottomStats; use self::popups::files::FilesPopup; diff --git a/rm-main/src/ui/tabs/torrents/popups/files.rs b/rm-main/src/ui/tabs/torrents/popups/files.rs index 5a9a2ef..c399d91 100644 --- a/rm-main/src/ui/tabs/torrents/popups/files.rs +++ b/rm-main/src/ui/tabs/torrents/popups/files.rs @@ -15,11 +15,11 @@ use transmission_rpc::types::{Id, Torrent, TorrentSetArgs}; use tui_tree_widget::{Tree, TreeItem, TreeState}; use crate::{ - action::Action, app, transmission::TorrentAction, ui::{centered_rect, components::Component}, }; +use rm_shared::action::Action; pub struct FilesPopup { ctx: app::Ctx, diff --git a/rm-main/src/ui/tabs/torrents/popups/mod.rs b/rm-main/src/ui/tabs/torrents/popups/mod.rs index 6b784d6..398f57e 100644 --- a/rm-main/src/ui/tabs/torrents/popups/mod.rs +++ b/rm-main/src/ui/tabs/torrents/popups/mod.rs @@ -1,5 +1,6 @@ use self::{files::FilesPopup, stats::StatisticsPopup}; -use crate::{action::Action, ui::components::Component}; +use crate::ui::components::Component; +use rm_shared::action::Action; use ratatui::prelude::*; diff --git a/rm-main/src/ui/tabs/torrents/popups/stats.rs b/rm-main/src/ui/tabs/torrents/popups/stats.rs index db846f7..92fbe79 100644 --- a/rm-main/src/ui/tabs/torrents/popups/stats.rs +++ b/rm-main/src/ui/tabs/torrents/popups/stats.rs @@ -8,11 +8,11 @@ use ratatui::{ use transmission_rpc::types::SessionStats; use crate::{ - action::Action, app, ui::{centered_rect, components::Component}, utils::bytes_to_human_format, }; +use rm_shared::action::Action; pub struct StatisticsPopup { stats: SessionStats, diff --git a/rm-main/src/ui/tabs/torrents/task_manager.rs b/rm-main/src/ui/tabs/torrents/task_manager.rs index cbe4888..55b8d91 100644 --- a/rm-main/src/ui/tabs/torrents/task_manager.rs +++ b/rm-main/src/ui/tabs/torrents/task_manager.rs @@ -2,7 +2,8 @@ use std::sync::{Arc, Mutex}; use ratatui::prelude::*; -use crate::{action::Action, app, ui::components::Component}; +use crate::{app, ui::components::Component}; +use rm_shared::action::Action; use super::{ tasks::{ diff --git a/rm-main/src/ui/tabs/torrents/tasks/add_magnet.rs b/rm-main/src/ui/tabs/torrents/tasks/add_magnet.rs index 4840752..bbffbe8 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/add_magnet.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/add_magnet.rs @@ -2,11 +2,11 @@ use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; use crate::{ - action::Action, app, transmission::TorrentAction, ui::{components::Component, tabs::torrents::input_manager::InputManager, to_input_request}, }; +use rm_shared::action::Action; pub struct AddMagnetBar { input_magnet_mgr: InputManager, diff --git a/rm-main/src/ui/tabs/torrents/tasks/delete_torrent.rs b/rm-main/src/ui/tabs/torrents/tasks/delete_torrent.rs index 5a1dc59..0d5cb28 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/delete_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/delete_torrent.rs @@ -3,11 +3,11 @@ use ratatui::prelude::*; use transmission_rpc::types::Id; use crate::{ - action::Action, app, transmission::TorrentAction, ui::{components::Component, tabs::torrents::input_manager::InputManager, to_input_request}, }; +use rm_shared::action::Action; pub struct DeleteBar { torrents_to_delete: Vec, diff --git a/rm-main/src/ui/tabs/torrents/tasks/filter.rs b/rm-main/src/ui/tabs/torrents/tasks/filter.rs index f58cecc..8854a65 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/filter.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/filter.rs @@ -4,7 +4,6 @@ use crossterm::event::KeyCode; use ratatui::prelude::*; use crate::{ - action::Action, app, ui::{ components::Component, @@ -12,6 +11,7 @@ use crate::{ to_input_request, }, }; +use rm_shared::action::Action; pub struct FilterBar { input: InputManager, diff --git a/rm-shared/Cargo.toml b/rm-shared/Cargo.toml new file mode 100644 index 0000000..66fa6de --- /dev/null +++ b/rm-shared/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rm-shared" +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +crossterm.workspace = true diff --git a/rm-main/src/action.rs b/rm-shared/src/action.rs similarity index 76% rename from rm-main/src/action.rs rename to rm-shared/src/action.rs index 07a5d05..bd66552 100644 --- a/rm-main/src/action.rs +++ b/rm-shared/src/action.rs @@ -1,9 +1,10 @@ -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use std::collections::HashMap; -use crate::{tui::Event, ui::global_popups::ErrorPopup}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crate::event::Event; #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum Action { +pub enum Action { HardQuit, Quit, SoftQuit, @@ -31,7 +32,13 @@ pub(crate) enum Action { AddMagnet, ChangeTab(u8), Input(KeyEvent), - Error(Box), + Error(Box), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ErrorMessage { + pub title: String, + pub message: String, } impl Action { @@ -54,7 +61,11 @@ pub enum Mode { Normal, } -pub fn event_to_action(mode: Mode, event: Event) -> Option { +pub fn event_to_action( + mode: Mode, + event: Event, + keymap: &HashMap<(KeyCode, KeyModifiers), Action>, +) -> Option { use Action as A; // Handle CTRL+C first @@ -71,18 +82,22 @@ pub fn event_to_action(mode: Mode, event: Event) -> Option { Event::Error => todo!(), Event::Render => Some(A::Render), Event::Key(key) if mode == Mode::Input => Some(A::Input(key)), - Event::Key(key) => key_event_to_action(key), + Event::Key(key) => key_event_to_action(key, keymap), } } -fn key_event_to_action(key: KeyEvent) -> Option { +fn key_event_to_action( + key: KeyEvent, + keymap: &HashMap<(KeyCode, KeyModifiers), Action>, +) -> Option { use Action as A; - match (key.modifiers, key.code) { - (KeyModifiers::CONTROL, KeyCode::Char('d')) => Some(A::ScrollDownPage), - (KeyModifiers::CONTROL, KeyCode::Char('u')) => Some(A::ScrollUpPage), - (_, keycode) => keycode_to_action(keycode), - } + keymap.get(&(key.code, key.modifiers)).cloned() + // match (key.modifiers, key.code) { + // (KeyModifiers::CONTROL, KeyCode::Char('d')) => Some(A::ScrollDownPage), + // (KeyModifiers::CONTROL, KeyCode::Char('u')) => Some(A::ScrollUpPage), + // (_, keycode) => keycode_to_action(keycode), + // } } fn keycode_to_action(key: KeyCode) -> Option { diff --git a/rm-shared/src/event.rs b/rm-shared/src/event.rs new file mode 100644 index 0000000..f1d22d8 --- /dev/null +++ b/rm-shared/src/event.rs @@ -0,0 +1,9 @@ +use crossterm::event::KeyEvent; + +#[derive(Clone, Debug)] +pub enum Event { + Quit, + Error, + Render, + Key(KeyEvent), +} diff --git a/rm-shared/src/lib.rs b/rm-shared/src/lib.rs new file mode 100644 index 0000000..9aeb93a --- /dev/null +++ b/rm-shared/src/lib.rs @@ -0,0 +1,2 @@ +pub mod action; +pub mod event; From 07f341401b171ef6459febaaea2ef0c905200ebd Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sun, 23 Jun 2024 18:19:38 +0200 Subject: [PATCH 02/16] impl deserialize for keybinding --- rm-config/src/keymap.rs | 127 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 3 deletions(-) diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 6184522..8b5cc3c 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -1,8 +1,11 @@ -use std::collections::HashMap; +use std::{collections::HashMap, marker::PhantomData}; use anyhow::Result; use crossterm::event::{KeyCode, KeyModifiers}; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{self, SeqAccess, Visitor}, + Deserialize, Serialize, +}; use toml::Table; use crate::{utils, KEYMAP_CONFIG_FILENAME}; @@ -90,7 +93,7 @@ impl From for Action { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize)] struct Keybinding> { on: KeyCode, #[serde(default)] @@ -98,6 +101,123 @@ struct Keybinding> { action: T, } +impl> Keybinding { + fn new(on: KeyCode, action: T, modifier: Option) -> Self { + Self { + on, + modifier: modifier.unwrap_or(KeyModifier::None), + action, + } + } +} + +impl<'de, T: Into + Deserialize<'de>> Deserialize<'de> for Keybinding { + fn deserialize(deserializer: D) -> std::prelude::v1::Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field { + On, + Modifier, + Action, + } + + struct KeybindingVisitor { + phantom: PhantomData, + } + + impl<'de, T: Into + Deserialize<'de>> Visitor<'de> for KeybindingVisitor { + type Value = Keybinding; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("struct Keybinding") + } + + fn visit_map(self, mut map: A) -> std::prelude::v1::Result + where + A: serde::de::MapAccess<'de>, + { + let mut on = None; + let mut modifier = None; + let mut action = None; + while let Some(key) = map.next_key()? { + match key { + Field::On => { + if on.is_some() { + return Err(de::Error::duplicate_field("on")); + } + let key = map.next_value::()?; + + if key.len() == 1 { + on = Some(KeyCode::Char(key.chars().next().unwrap())); + } else if key.starts_with('F') && (key.len() == 2 || key.len() == 3) { + let which_f = key[1..].parse::().map_err(|_| { + de::Error::invalid_value( + de::Unexpected::Str(&key), + &"something_correct", + ) + })?; + on = Some(KeyCode::F(which_f)); + } else { + on = { + match key.to_lowercase().as_str() { + "enter" => Some(KeyCode::Enter), + "esc" => Some(KeyCode::Esc), + "up" => Some(KeyCode::Up), + "down" => Some(KeyCode::Down), + "left" => Some(KeyCode::Left), + "right" => Some(KeyCode::Right), + "home" => Some(KeyCode::Home), + "end" => Some(KeyCode::End), + "pageup" => Some(KeyCode::PageUp), + "pagedown" => Some(KeyCode::PageDown), + "tab" => Some(KeyCode::Tab), + "backspace" => Some(KeyCode::Backspace), + "delete" => Some(KeyCode::Delete), + + _ => { + return Err(de::Error::invalid_value( + de::Unexpected::Str(&key), + &"something correct", + )) + } + } + }; + } + } + Field::Modifier => { + if modifier.is_some() { + return Err(de::Error::duplicate_field("modifier")); + } + modifier = Some(map.next_value()); + } + Field::Action => { + if action.is_some() { + return Err(de::Error::duplicate_field("action")); + } + action = Some(map.next_value()); + } + } + } + let on = on.ok_or_else(|| de::Error::missing_field("on"))?; + let action = action.ok_or_else(|| de::Error::missing_field("action"))??; + Ok(Keybinding::new(on, action, modifier.transpose().unwrap())) + } + } + + const FIELDS: &[&str] = &["on", "modifier", "action"]; + deserializer.deserialize_struct( + "Keybinding", + FIELDS, + KeybindingVisitor { + phantom: PhantomData::default(), + }, + ) + } +} + #[derive(Serialize, Deserialize, Hash)] enum KeyModifier { None, @@ -124,6 +244,7 @@ impl Default for KeyModifier { impl Keymap { pub fn init() -> Result { let table = { + // TODO: handle errors or there will be hell to pay if let Ok(table) = utils::fetch_config_table(KEYMAP_CONFIG_FILENAME) { table } else { From c19cd778276abd268f635b81c6b2ffeb8b7b736a Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Mon, 24 Jun 2024 17:41:00 +0200 Subject: [PATCH 03/16] refactor(config): restructure --- Cargo.lock | 1 + Cargo.toml | 2 +- rm-config/src/keymap.rs | 19 ++-- rm-config/src/lib.rs | 154 +++++------------------------- rm-config/src/main_config.rs | 60 ++++++++++++ rm-config/src/utils.rs | 20 +++- rm-main/src/app.rs | 4 +- rm-main/src/transmission/utils.rs | 3 +- 8 files changed, 117 insertions(+), 146 deletions(-) create mode 100644 rm-config/src/main_config.rs diff --git a/Cargo.lock b/Cargo.lock index 5b46f11..b93b294 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1921,6 +1921,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7be944a..15b445f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ fuzzy-matcher = "0.3.7" clap = { version = "4.5.6", features = ["derive"] } base64 = "0.22" xdg = "2.5" -url = "2.5" +url = { version = "2.5", features = ["serde"] } toml = "0.8" # Async diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 8b5cc3c..738670d 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -1,18 +1,18 @@ -use std::{collections::HashMap, marker::PhantomData}; +use std::{collections::HashMap, marker::PhantomData, path::PathBuf, sync::OnceLock}; use anyhow::Result; use crossterm::event::{KeyCode, KeyModifiers}; use serde::{ - de::{self, SeqAccess, Visitor}, + de::{self, Visitor}, Deserialize, Serialize, }; use toml::Table; -use crate::{utils, KEYMAP_CONFIG_FILENAME}; +use crate::utils; use rm_shared::action::Action; #[derive(Serialize, Deserialize)] -pub struct Keymap { +pub(crate) struct KeymapConfig { general: General, torrents_tab: TorrentsTab, } @@ -241,11 +241,13 @@ impl Default for KeyModifier { } } -impl Keymap { +impl KeymapConfig { + pub const FILENAME: &'static str = "keymap.toml"; + pub fn init() -> Result { let table = { // TODO: handle errors or there will be hell to pay - if let Ok(table) = utils::fetch_config_table(KEYMAP_CONFIG_FILENAME) { + if let Ok(table) = utils::fetch_config(Self::FILENAME) { table } else { todo!(); @@ -273,4 +275,9 @@ impl Keymap { let config = toml::from_str(&config_string)?; Ok(config) } + + pub fn path() -> &'static PathBuf { + static PATH: OnceLock = OnceLock::new(); + PATH.get_or_init(|| utils::get_config_path(Self::FILENAME)) + } } diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index ddfae80..6c3ec57 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -1,149 +1,43 @@ mod keymap; +mod main_config; mod utils; -use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; +use std::{collections::HashMap, path::PathBuf}; -use anyhow::{bail, Context, Result}; +use anyhow::Result; use crossterm::event::{KeyCode, KeyModifiers}; -use ratatui::style::Color; -use rm_shared::action::Action; -use serde::{Deserialize, Serialize}; -use toml::Table; -use xdg::BaseDirectories; +use keymap::KeymapConfig; +use main_config::MainConfig; -use crate::utils::put_config; -use keymap::Keymap; +use rm_shared::action::Action; -#[derive(Serialize, Deserialize)] pub struct Config { - pub connection: Connection, - pub general: General, - #[serde(skip)] - pub keymap: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct General { - #[serde(default)] - pub auto_hide: bool, - #[serde(default = "default_accent_color")] - pub accent_color: Color, - #[serde(default = "default_beginner_mode")] - pub beginner_mode: bool, -} - -fn default_accent_color() -> Color { - Color::LightMagenta -} - -fn default_beginner_mode() -> bool { - true + pub general: main_config::General, + pub connection: main_config::Connection, + pub keymap: HashMap<(KeyCode, KeyModifiers), Action>, + pub directories: Directories, } -#[derive(Debug, Serialize, Deserialize)] -pub struct Connection { - pub username: Option, - pub password: Option, - pub url: String, -} - -const DEFAULT_CONFIG: &str = include_str!("../defaults/config.toml"); -static XDG_DIRS: OnceLock = OnceLock::new(); -static CONFIG_PATH: OnceLock = OnceLock::new(); -pub const MAIN_CONFIG_FILENAME: &str = "config.toml"; -pub const KEYMAP_CONFIG_FILENAME: &str = "keymap.toml"; - -pub fn xdg_dirs() -> &'static BaseDirectories { - XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("rustmission").unwrap()) -} - -pub fn get_config_path(filename: &str) -> &'static PathBuf { - CONFIG_PATH.get_or_init(|| xdg_dirs().place_config_file(filename).unwrap()) +pub struct Directories { + pub main_path: &'static PathBuf, + pub keymap_path: &'static PathBuf, } impl Config { pub fn init() -> Result { - let Ok(table) = utils::fetch_config_table(MAIN_CONFIG_FILENAME) else { - put_config(DEFAULT_CONFIG, MAIN_CONFIG_FILENAME)?; - // TODO: check if the user really changed the config. - println!( - "Update {:?} and start rustmission again", - get_config_path(MAIN_CONFIG_FILENAME) - ); - std::process::exit(0); - }; + let main_config = MainConfig::init()?; + let keybindings = KeymapConfig::init()?; - Self::table_config_verify(&table)?; - - let mut config = Self::table_to_config(&table)?; - config.keymap = Some(Keymap::init().unwrap().to_hashmap()); - Ok(config) - } - - fn table_to_config(table: &Table) -> Result { - let config_string = table.to_string(); - let config: Self = toml::from_str(&config_string)?; - Ok(config) - } - - fn table_config_verify(table: &Table) -> Result<()> { - let Some(connection_table) = table.get("connection").unwrap().as_table() else { - bail!("expected connection table") + let directories = Directories { + main_path: MainConfig::path(), + keymap_path: KeymapConfig::path(), }; - let url = connection_table - .get("url") - .and_then(|url| url.as_str()) - .with_context(|| { - format!( - "no url given in: {}", - get_config_path(MAIN_CONFIG_FILENAME).to_str().unwrap() - ) - })?; - - url::Url::parse(url).with_context(|| { - format!( - "invalid url '{url}' in {}", - get_config_path(MAIN_CONFIG_FILENAME).to_str().unwrap() - ) - })?; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn invalid_config() -> Table { - toml::toml! { - [connection] - username = "username" - password = "password" - auto_hide = "dfgoij" - url = "bad_url" - } - } - - fn valid_config() -> Table { - toml::toml! { - [connection] - username = "username" - password = "password" - url = "http://192.168.1.1/transmission/rpc" - } - } - - #[test] - fn validates_properly() { - let valid_config = valid_config(); - assert!(Config::table_config_verify(&valid_config).is_ok()); - } - - #[test] - fn invalidates_properly() { - let invalid_config = invalid_config(); - assert!(Config::table_config_verify(&invalid_config).is_err()); + Ok(Self { + general: main_config.general, + connection: main_config.connection, + keymap: keybindings.to_hashmap(), + directories, + }) } } diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs new file mode 100644 index 0000000..cf72597 --- /dev/null +++ b/rm-config/src/main_config.rs @@ -0,0 +1,60 @@ +use std::{path::PathBuf, sync::OnceLock}; + +use anyhow::Result; +use ratatui::style::Color; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::utils::{self, put_config}; + +#[derive(Serialize, Deserialize)] +pub struct MainConfig { + pub general: General, + pub connection: Connection, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct General { + #[serde(default)] + pub auto_hide: bool, + #[serde(default = "default_accent_color")] + pub accent_color: Color, + #[serde(default = "default_beginner_mode")] + pub beginner_mode: bool, +} + +fn default_accent_color() -> Color { + Color::LightMagenta +} + +fn default_beginner_mode() -> bool { + true +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Connection { + pub username: Option, + pub password: Option, + pub url: Url, +} + +impl MainConfig { + pub(crate) const FILENAME: &'static str = "config.toml"; + const DEFAULT_CONFIG: &'static str = include_str!("../defaults/config.toml"); + + pub(crate) fn init() -> Result { + let Ok(config) = utils::fetch_config(Self::FILENAME) else { + put_config(Self::DEFAULT_CONFIG, Self::FILENAME)?; + // TODO: check if the user really changed the config. + println!("Update {:?} and start rustmission again", Self::path()); + std::process::exit(0); + }; + + Ok(config) + } + + pub(crate) fn path() -> &'static PathBuf { + static PATH: OnceLock = OnceLock::new(); + PATH.get_or_init(|| utils::get_config_path(Self::FILENAME)) + } +} diff --git a/rm-config/src/utils.rs b/rm-config/src/utils.rs index a029181..40d53a1 100644 --- a/rm-config/src/utils.rs +++ b/rm-config/src/utils.rs @@ -1,15 +1,25 @@ use std::{ fs::File, io::{Read, Write}, + path::PathBuf, + sync::OnceLock, }; use anyhow::Result; -use toml::Table; +use serde::de::DeserializeOwned; +use xdg::BaseDirectories; -use crate::get_config_path; +pub fn xdg_dirs() -> &'static BaseDirectories { + static XDG_DIRS: OnceLock = OnceLock::new(); + XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("rustmission").unwrap()) +} + +pub fn get_config_path(filename: &str) -> PathBuf { + xdg_dirs().place_config_file(filename).unwrap() +} -pub fn fetch_config_table(config_name: &str) -> Result
{ - let config_path = crate::xdg_dirs() +pub fn fetch_config(config_name: &str) -> Result { + let config_path = xdg_dirs() .find_config_file(config_name) .ok_or_else(|| anyhow::anyhow!("{} not found", config_name))?; @@ -20,7 +30,7 @@ pub fn fetch_config_table(config_name: &str) -> Result
{ Ok(toml::from_str(&config_buf)?) } -pub fn put_config(content: &str, filename: &str) -> Result
{ +pub fn put_config(content: &str, filename: &str) -> Result { let config_path = get_config_path(filename); let mut config_file = File::create(config_path)?; config_file.write_all(content.as_bytes())?; diff --git a/rm-main/src/app.rs b/rm-main/src/app.rs index dfdc0a9..d9d5281 100644 --- a/rm-main/src/app.rs +++ b/rm-main/src/app.rs @@ -46,7 +46,7 @@ impl Ctx { }); } Err(e) => { - let config_path = rm_config::get_config_path(rm_config::MAIN_CONFIG_FILENAME); + let config_path = config.directories.main_path; return Err(Error::msg(format!( "{e}\nIs the connection info in {:?} correct?", config_path @@ -111,7 +111,7 @@ impl App { tokio::select! { event = tui_event => { - if let Some(action) = event_to_action(self.mode, event.unwrap(), self.ctx.config.keymap.as_ref().unwrap()) { + if let Some(action) = event_to_action(self.mode, event.unwrap(), &self.ctx.config.keymap) { if let Some(action) = self.update(action).await { self.ctx.action_tx.send(action).unwrap(); } diff --git a/rm-main/src/transmission/utils.rs b/rm-main/src/transmission/utils.rs index 27fc0ef..f8d9b0f 100644 --- a/rm-main/src/transmission/utils.rs +++ b/rm-main/src/transmission/utils.rs @@ -14,9 +14,8 @@ pub fn client_from_config(config: &Config) -> TransClient { .as_ref() .unwrap_or(&"".to_string()) .clone(); - let url = config.connection.url.parse().unwrap(); let auth = BasicAuth { user, password }; - TransClient::with_auth(url, auth) + TransClient::with_auth(config.connection.url.clone(), auth) } From 813667d4944ebd4a16cc6af66398852359b4f8f6 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Tue, 25 Jun 2024 13:38:51 +0200 Subject: [PATCH 04/16] fix headers_hide --- rm-config/src/main_config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index cf72597..31967b5 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -21,6 +21,8 @@ pub struct General { pub accent_color: Color, #[serde(default = "default_beginner_mode")] pub beginner_mode: bool, + #[serde(default)] + pub headers_hide: bool, } fn default_accent_color() -> Color { From 0680e22e52efacb2fdf171a8838842fc9007e705 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Tue, 25 Jun 2024 14:29:39 +0200 Subject: [PATCH 05/16] use indexmap --- Cargo.lock | 2 ++ Cargo.toml | 1 + rm-config/Cargo.toml | 1 + rm-config/src/keymap.rs | 13 +++++++------ rm-config/src/lib.rs | 7 ++++--- rm-shared/Cargo.toml | 1 + rm-shared/src/action.rs | 7 +++---- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b93b294..94ad68d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1247,6 +1247,7 @@ version = "0.3.3" dependencies = [ "anyhow", "crossterm", + "indexmap", "ratatui", "rm-shared", "serde", @@ -1260,6 +1261,7 @@ name = "rm-shared" version = "0.3.3" dependencies = [ "crossterm", + "indexmap", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 15b445f..f1fea1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ base64 = "0.22" xdg = "2.5" url = { version = "2.5", features = ["serde"] } toml = "0.8" +indexmap = "2" # Async tokio = { version = "1", features = ["macros", "sync"] } diff --git a/rm-config/Cargo.toml b/rm-config/Cargo.toml index f91ee6d..77f4d8e 100644 --- a/rm-config/Cargo.toml +++ b/rm-config/Cargo.toml @@ -19,5 +19,6 @@ anyhow.workspace = true url.workspace = true ratatui.workspace = true crossterm.workspace = true +indexmap.workspace = true diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 738670d..9710d15 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -1,7 +1,8 @@ -use std::{collections::HashMap, marker::PhantomData, path::PathBuf, sync::OnceLock}; +use std::{marker::PhantomData, path::PathBuf, sync::OnceLock}; use anyhow::Result; use crossterm::event::{KeyCode, KeyModifiers}; +use indexmap::IndexMap; use serde::{ de::{self, Visitor}, Deserialize, Serialize, @@ -257,17 +258,17 @@ impl KeymapConfig { Self::table_to_keymap(&table) } - pub fn to_hashmap(self) -> HashMap<(KeyCode, KeyModifiers), Action> { - let mut hashmap = HashMap::new(); + pub fn to_map(self) -> IndexMap<(KeyCode, KeyModifiers), Action> { + let mut map = IndexMap::new(); for keybinding in self.general.keybindings { let hash_value = (keybinding.on, keybinding.modifier.into()); - hashmap.insert(hash_value, keybinding.action.into()); + map.insert(hash_value, keybinding.action.into()); } for keybinding in self.torrents_tab.keybindings { let hash_value = (keybinding.on, keybinding.modifier.into()); - hashmap.insert(hash_value, keybinding.action.into()); + map.insert(hash_value, keybinding.action.into()); } - hashmap + map } fn table_to_keymap(table: &Table) -> Result { diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index 6c3ec57..525b3fc 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -2,7 +2,8 @@ mod keymap; mod main_config; mod utils; -use std::{collections::HashMap, path::PathBuf}; +use indexmap::IndexMap; +use std::path::PathBuf; use anyhow::Result; use crossterm::event::{KeyCode, KeyModifiers}; @@ -14,7 +15,7 @@ use rm_shared::action::Action; pub struct Config { pub general: main_config::General, pub connection: main_config::Connection, - pub keymap: HashMap<(KeyCode, KeyModifiers), Action>, + pub keymap: IndexMap<(KeyCode, KeyModifiers), Action>, pub directories: Directories, } @@ -36,7 +37,7 @@ impl Config { Ok(Self { general: main_config.general, connection: main_config.connection, - keymap: keybindings.to_hashmap(), + keymap: keybindings.to_map(), directories, }) } diff --git a/rm-shared/Cargo.toml b/rm-shared/Cargo.toml index 66fa6de..18341c4 100644 --- a/rm-shared/Cargo.toml +++ b/rm-shared/Cargo.toml @@ -11,3 +11,4 @@ license.workspace = true [dependencies] crossterm.workspace = true +indexmap.workspace = true diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index bd66552..9772cd8 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -1,6 +1,5 @@ -use std::collections::HashMap; - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use indexmap::IndexMap; use crate::event::Event; #[derive(Debug, Clone, PartialEq, Eq)] @@ -64,7 +63,7 @@ pub enum Mode { pub fn event_to_action( mode: Mode, event: Event, - keymap: &HashMap<(KeyCode, KeyModifiers), Action>, + keymap: &IndexMap<(KeyCode, KeyModifiers), Action>, ) -> Option { use Action as A; @@ -88,7 +87,7 @@ pub fn event_to_action( fn key_event_to_action( key: KeyEvent, - keymap: &HashMap<(KeyCode, KeyModifiers), Action>, + keymap: &IndexMap<(KeyCode, KeyModifiers), Action>, ) -> Option { use Action as A; From 5f7941e3e510d0234734670ca06f47c64d5f011d Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Tue, 25 Jun 2024 14:51:54 +0200 Subject: [PATCH 06/16] get rid of crate's event --- rm-main/src/tui.rs | 29 +++++++++++++++-------------- rm-shared/src/action.rs | 7 ++----- rm-shared/src/event.rs | 9 --------- rm-shared/src/lib.rs | 1 - 4 files changed, 17 insertions(+), 29 deletions(-) delete mode 100644 rm-shared/src/event.rs diff --git a/rm-main/src/tui.rs b/rm-main/src/tui.rs index 492e2bb..7149f03 100644 --- a/rm-main/src/tui.rs +++ b/rm-main/src/tui.rs @@ -1,9 +1,9 @@ -use std::time::Duration; +use std::{io, time::Duration}; use anyhow::Result; use crossterm::{ cursor, - event::{Event as CrosstermEvent, KeyEventKind}, + event::{Event, KeyEventKind}, terminal::{EnterAlternateScreen, LeaveAlternateScreen}, }; use futures::{FutureExt, StreamExt}; @@ -14,11 +14,9 @@ use tokio::{ }; use tokio_util::sync::CancellationToken; -use rm_shared::event::Event; - pub struct Tui { pub terminal: ratatui::Terminal>, - pub task: JoinHandle<()>, + pub task: JoinHandle>, pub cancellation_token: CancellationToken, pub event_rx: UnboundedReceiver, pub event_tx: UnboundedSender, @@ -29,7 +27,7 @@ impl Tui { let terminal = ratatui::Terminal::new(Backend::new(std::io::stdout()))?; let (event_tx, event_rx) = mpsc::unbounded_channel(); let cancellation_token = CancellationToken::new(); - let task = tokio::spawn(async {}); + let task = tokio::spawn(async { Ok(()) }); Ok(Self { terminal, task, @@ -39,7 +37,7 @@ impl Tui { }) } - pub fn start(&mut self) { + pub fn start(&mut self) -> Result<()> { self.cancel(); self.cancellation_token = CancellationToken::new(); let cancellation_token = self.cancellation_token.clone(); @@ -51,26 +49,29 @@ impl Tui { let crossterm_event = reader.next().fuse(); tokio::select! { _ = cancellation_token.cancelled() => break, - event = crossterm_event => Self::handle_crossterm_event(event, &event_tx), + event = crossterm_event => Self::handle_crossterm_event::(event, &event_tx)?, } } + Ok(()) }); + Ok(()) } fn handle_crossterm_event( - event: Option>, + event: Option>, event_tx: &UnboundedSender, - ) { + ) -> Result<()> { match event { - Some(Ok(CrosstermEvent::Key(key))) => { + Some(Ok(Event::Key(key))) => { if key.kind == KeyEventKind::Press { event_tx.send(Event::Key(key)).unwrap(); } } - Some(Ok(CrosstermEvent::Resize(_, _))) => event_tx.send(Event::Render).unwrap(), - Some(Err(_)) => event_tx.send(Event::Error).unwrap(), + Some(Ok(Event::Resize(x, y))) => event_tx.send(Event::Resize(x, y)).unwrap(), + Some(Err(e)) => Err(e)?, _ => (), } + Ok(()) } pub(crate) fn stop(&self) { @@ -91,7 +92,7 @@ impl Tui { pub(crate) fn enter(&mut self) -> Result<()> { crossterm::terminal::enable_raw_mode()?; crossterm::execute!(std::io::stdout(), EnterAlternateScreen, cursor::Hide)?; - self.start(); + self.start()?; Ok(()) } diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index 9772cd8..afbee34 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -1,7 +1,6 @@ -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use indexmap::IndexMap; -use crate::event::Event; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Action { HardQuit, @@ -77,11 +76,9 @@ pub fn event_to_action( } match event { - Event::Quit => Some(A::Quit), - Event::Error => todo!(), - Event::Render => Some(A::Render), Event::Key(key) if mode == Mode::Input => Some(A::Input(key)), Event::Key(key) => key_event_to_action(key, keymap), + _ => None, } } diff --git a/rm-shared/src/event.rs b/rm-shared/src/event.rs deleted file mode 100644 index f1d22d8..0000000 --- a/rm-shared/src/event.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crossterm::event::KeyEvent; - -#[derive(Clone, Debug)] -pub enum Event { - Quit, - Error, - Render, - Key(KeyEvent), -} diff --git a/rm-shared/src/lib.rs b/rm-shared/src/lib.rs index 9aeb93a..e9a6726 100644 --- a/rm-shared/src/lib.rs +++ b/rm-shared/src/lib.rs @@ -1,2 +1 @@ pub mod action; -pub mod event; From 9ca2f274b13a537c14a1555e39e6a3979e67c48e Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Wed, 26 Jun 2024 21:43:06 +0200 Subject: [PATCH 07/16] fix help --- Cargo.lock | 2 - Cargo.toml | 1 - rm-config/Cargo.toml | 1 - rm-config/src/keymap.rs | 159 +++++++++++++++++++++------ rm-config/src/lib.rs | 9 +- rm-main/src/ui/global_popups/help.rs | 51 +++++---- rm-shared/Cargo.toml | 1 - rm-shared/src/action.rs | 52 +-------- 8 files changed, 164 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94ad68d..b93b294 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1247,7 +1247,6 @@ version = "0.3.3" dependencies = [ "anyhow", "crossterm", - "indexmap", "ratatui", "rm-shared", "serde", @@ -1261,7 +1260,6 @@ name = "rm-shared" version = "0.3.3" dependencies = [ "crossterm", - "indexmap", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f1fea1b..15b445f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ base64 = "0.22" xdg = "2.5" url = { version = "2.5", features = ["serde"] } toml = "0.8" -indexmap = "2" # Async tokio = { version = "1", features = ["macros", "sync"] } diff --git a/rm-config/Cargo.toml b/rm-config/Cargo.toml index 77f4d8e..f91ee6d 100644 --- a/rm-config/Cargo.toml +++ b/rm-config/Cargo.toml @@ -19,6 +19,5 @@ anyhow.workspace = true url.workspace = true ratatui.workspace = true crossterm.workspace = true -indexmap.workspace = true diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 9710d15..262ef80 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -1,8 +1,7 @@ -use std::{marker::PhantomData, path::PathBuf, sync::OnceLock}; +use std::{collections::HashMap, marker::PhantomData, path::PathBuf, sync::OnceLock}; use anyhow::Result; use crossterm::event::{KeyCode, KeyModifiers}; -use indexmap::IndexMap; use serde::{ de::{self, Visitor}, Deserialize, Serialize, @@ -12,19 +11,19 @@ use toml::Table; use crate::utils; use rm_shared::action::Action; -#[derive(Serialize, Deserialize)] -pub(crate) struct KeymapConfig { - general: General, - torrents_tab: TorrentsTab, +#[derive(Serialize, Deserialize, Clone)] +pub struct KeymapConfig { + pub general: General, + pub torrents_tab: TorrentsTab, } -#[derive(Serialize, Deserialize)] -struct General> { - keybindings: Vec>, +#[derive(Serialize, Deserialize, Clone)] +pub struct General> { + pub keybindings: Vec>, } -#[derive(Serialize, Deserialize, Debug)] -enum GeneralAction { +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum GeneralAction { ShowHelp, Quit, SoftQuit, @@ -37,10 +36,37 @@ enum GeneralAction { Search, SwitchFocus, Confirm, - PageDown, - PageUp, - Home, - End, + ScrollPageDown, + ScrollPageUp, + GoToBeginning, + GoToEnd, +} + +pub trait UserAction: Into { + fn desc(&self) -> &'static str; +} + +impl UserAction for GeneralAction { + fn desc(&self) -> &'static str { + match self { + GeneralAction::ShowHelp => "toggle help", + GeneralAction::Quit => "quit Rustmission / a popup", + GeneralAction::SoftQuit => "close a popup / task", + GeneralAction::SwitchToTorrents => "switch to torrents tab", + GeneralAction::SwitchToSearch => "switch to search tab", + GeneralAction::Left => "switch to tab left", + GeneralAction::Right => "switch to tab right", + GeneralAction::Down => "move down", + GeneralAction::Up => "move up", + GeneralAction::Search => "search", + GeneralAction::SwitchFocus => "switch focus", + GeneralAction::Confirm => "confirm", + GeneralAction::ScrollPageDown => "scroll page down", + GeneralAction::ScrollPageUp => "scroll page up", + GeneralAction::GoToBeginning => "scroll to the beginning", + GeneralAction::GoToEnd => "scroll to the end", + } + } } impl From for Action { @@ -58,21 +84,21 @@ impl From for Action { GeneralAction::Search => Action::Search, GeneralAction::SwitchFocus => Action::ChangeFocus, GeneralAction::Confirm => Action::Confirm, - GeneralAction::PageDown => Action::ScrollDownPage, - GeneralAction::PageUp => Action::ScrollUpPage, - GeneralAction::Home => Action::Home, - GeneralAction::End => Action::End, + GeneralAction::ScrollPageDown => Action::ScrollDownPage, + GeneralAction::ScrollPageUp => Action::ScrollUpPage, + GeneralAction::GoToBeginning => Action::Home, + GeneralAction::GoToEnd => Action::End, } } } -#[derive(Serialize, Deserialize)] -struct TorrentsTab> { - keybindings: Vec>, +#[derive(Serialize, Deserialize, Clone)] +pub struct TorrentsTab> { + pub keybindings: Vec>, } -#[derive(Serialize, Deserialize, Debug)] -enum TorrentsAction { +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum TorrentsAction { AddMagnet, Pause, DeleteWithFiles, @@ -81,6 +107,19 @@ enum TorrentsAction { ShowStats, } +impl UserAction for TorrentsAction { + fn desc(&self) -> &'static str { + match self { + TorrentsAction::AddMagnet => "add a magnet", + TorrentsAction::Pause => "pause/unpause", + TorrentsAction::DeleteWithFiles => "delete with files", + TorrentsAction::DeleteWithoutFiles => "delete without files", + TorrentsAction::ShowFiles => "show files", + TorrentsAction::ShowStats => "show statistics", + } + } +} + impl From for Action { fn from(value: TorrentsAction) -> Self { match value { @@ -94,12 +133,52 @@ impl From for Action { } } -#[derive(Serialize)] -struct Keybinding> { - on: KeyCode, +#[derive(Serialize, Clone)] +pub struct Keybinding> { + pub on: KeyCode, #[serde(default)] - modifier: KeyModifier, - action: T, + pub modifier: KeyModifier, + pub action: T, +} + +impl> Keybinding { + pub fn keycode_string(&self) -> String { + let key = match self.on { + KeyCode::Backspace => "Backspace".into(), + KeyCode::Enter => "Enter".into(), + KeyCode::Left => "".into(), + KeyCode::Right => "".into(), + KeyCode::Up => "".into(), + KeyCode::Down => "".into(), + KeyCode::Home => "Home".into(), + KeyCode::End => "End".into(), + KeyCode::PageUp => "PageUp".into(), + KeyCode::PageDown => "PageDown".into(), + KeyCode::Tab => "Tab".into(), + KeyCode::BackTab => todo!(), + KeyCode::Delete => todo!(), + KeyCode::Insert => "Insert".into(), + KeyCode::F(i) => format!("F{i}"), + KeyCode::Char(c) => c.into(), + KeyCode::Null => todo!(), + KeyCode::Esc => "Esc".into(), + KeyCode::CapsLock => todo!(), + KeyCode::ScrollLock => todo!(), + KeyCode::NumLock => todo!(), + KeyCode::PrintScreen => todo!(), + KeyCode::Pause => todo!(), + KeyCode::Menu => todo!(), + KeyCode::KeypadBegin => todo!(), + KeyCode::Media(_) => todo!(), + KeyCode::Modifier(_) => todo!(), + }; + + if !self.modifier.is_none() { + format!("{}-{key}", self.modifier.to_str()) + } else { + key + } + } } impl> Keybinding { @@ -219,13 +298,27 @@ impl<'de, T: Into + Deserialize<'de>> Deserialize<'de> for Keybinding } } -#[derive(Serialize, Deserialize, Hash)] -enum KeyModifier { +#[derive(Serialize, Deserialize, Hash, Clone, Copy, PartialEq, Eq)] +pub enum KeyModifier { None, Ctrl, Shift, } +impl KeyModifier { + fn to_str(self) -> &'static str { + match self { + KeyModifier::None => "", + KeyModifier::Ctrl => "CTRL", + KeyModifier::Shift => "SHIFT", + } + } + + fn is_none(self) -> bool { + self == KeyModifier::None + } +} + impl From for KeyModifiers { fn from(value: KeyModifier) -> Self { match value { @@ -258,8 +351,8 @@ impl KeymapConfig { Self::table_to_keymap(&table) } - pub fn to_map(self) -> IndexMap<(KeyCode, KeyModifiers), Action> { - let mut map = IndexMap::new(); + pub fn to_map(self) -> HashMap<(KeyCode, KeyModifiers), Action> { + let mut map = HashMap::new(); for keybinding in self.general.keybindings { let hash_value = (keybinding.on, keybinding.modifier.into()); map.insert(hash_value, keybinding.action.into()); diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index 525b3fc..1cc39e1 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -1,9 +1,8 @@ -mod keymap; +pub mod keymap; mod main_config; mod utils; -use indexmap::IndexMap; -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use anyhow::Result; use crossterm::event::{KeyCode, KeyModifiers}; @@ -15,7 +14,8 @@ use rm_shared::action::Action; pub struct Config { pub general: main_config::General, pub connection: main_config::Connection, - pub keymap: IndexMap<(KeyCode, KeyModifiers), Action>, + pub keymap: HashMap<(KeyCode, KeyModifiers), Action>, + pub keybindings: KeymapConfig, pub directories: Directories, } @@ -37,6 +37,7 @@ impl Config { Ok(Self { general: main_config.general, connection: main_config.connection, + keybindings: keybindings.clone(), keymap: keybindings.to_map(), directories, }) diff --git a/rm-main/src/ui/global_popups/help.rs b/rm-main/src/ui/global_popups/help.rs index fdbacc9..2debed4 100644 --- a/rm-main/src/ui/global_popups/help.rs +++ b/rm-main/src/ui/global_popups/help.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use ratatui::{ prelude::*, widgets::{ @@ -10,6 +12,7 @@ use crate::{ app, ui::{centered_rect, components::Component}, }; +use rm_config::keymap::{GeneralAction, TorrentsAction, UserAction}; use rm_shared::action::Action; macro_rules! add_line { @@ -67,22 +70,19 @@ impl Component for HelpPopup { )]) .centered()]; - add_line!(lines, "? / F1", "show/hide help"); - add_line!(lines, "q", "quit Rustmission / a popup"); - add_line!(lines, "ESC", "close a popup / task"); - add_line!(lines, "1", "switch to torrents tab"); - add_line!(lines, "2", "switch to search tab"); - add_line!(lines, "h / ←", "switch to tab left of current tab"); - add_line!(lines, "l / →", "switch to tab right of current tab"); - add_line!(lines, "j / ↓", "move down"); - add_line!(lines, "k / ↑", "move up"); - add_line!(lines, "/", "search or filter"); - add_line!(lines, "TAB", "switch focus"); - add_line!(lines, "Enter", "confirm"); - add_line!(lines, "CTRL-d", "scroll page down"); - add_line!(lines, "CTRL-u", "scroll page up"); - add_line!(lines, "Home", "scroll to the beginning"); - add_line!(lines, "End", "scroll to the end"); + let mut general_keys: BTreeMap> = BTreeMap::new(); + + for keybinding in &self.ctx.config.keybindings.general.keybindings { + general_keys + .entry(keybinding.action) + .or_insert_with(Vec::new) + .push(keybinding.keycode_string()); + } + + for (action, keycodes) in general_keys { + let keycode_string = keycodes.join(" / "); + add_line!(lines, keycode_string, action.desc()); + } lines.push( Line::from(vec![Span::styled( @@ -92,12 +92,19 @@ impl Component for HelpPopup { .centered(), ); - add_line!(lines, "a", "add a magnet url"); - add_line!(lines, "p", "pause/unpause a torrent"); - add_line!(lines, "d", "delete a torrent without files"); - add_line!(lines, "D", "delete a torrent with files"); - add_line!(lines, "f", "show files of a torrent"); - add_line!(lines, "s", "show statistics"); + let mut torrent_keys: BTreeMap> = BTreeMap::new(); + + for keybinding in &self.ctx.config.keybindings.torrents_tab.keybindings { + torrent_keys + .entry(keybinding.action) + .or_insert_with(Vec::new) + .push(keybinding.keycode_string()); + } + + for (action, keycodes) in torrent_keys { + let keycode_string = keycodes.join(" / "); + add_line!(lines, keycode_string, action.desc()); + } let help_text = Text::from(lines); let help_paragraph = Paragraph::new(help_text); diff --git a/rm-shared/Cargo.toml b/rm-shared/Cargo.toml index 18341c4..66fa6de 100644 --- a/rm-shared/Cargo.toml +++ b/rm-shared/Cargo.toml @@ -11,4 +11,3 @@ license.workspace = true [dependencies] crossterm.workspace = true -indexmap.workspace = true diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index afbee34..c3ee590 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -1,5 +1,6 @@ +use std::collections::HashMap; + use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; -use indexmap::IndexMap; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Action { @@ -62,7 +63,7 @@ pub enum Mode { pub fn event_to_action( mode: Mode, event: Event, - keymap: &IndexMap<(KeyCode, KeyModifiers), Action>, + keymap: &HashMap<(KeyCode, KeyModifiers), Action>, ) -> Option { use Action as A; @@ -77,52 +78,7 @@ pub fn event_to_action( match event { Event::Key(key) if mode == Mode::Input => Some(A::Input(key)), - Event::Key(key) => key_event_to_action(key, keymap), - _ => None, - } -} - -fn key_event_to_action( - key: KeyEvent, - keymap: &IndexMap<(KeyCode, KeyModifiers), Action>, -) -> Option { - use Action as A; - - keymap.get(&(key.code, key.modifiers)).cloned() - // match (key.modifiers, key.code) { - // (KeyModifiers::CONTROL, KeyCode::Char('d')) => Some(A::ScrollDownPage), - // (KeyModifiers::CONTROL, KeyCode::Char('u')) => Some(A::ScrollUpPage), - // (_, keycode) => keycode_to_action(keycode), - // } -} - -fn keycode_to_action(key: KeyCode) -> Option { - use Action as A; - match key { - KeyCode::Char('q') | KeyCode::Char('Q') => Some(A::Quit), - KeyCode::Esc => Some(A::SoftQuit), - KeyCode::Tab => Some(A::ChangeFocus), - KeyCode::Home => Some(A::Home), - KeyCode::End => Some(A::End), - KeyCode::PageUp => Some(A::ScrollUpPage), - KeyCode::PageDown => Some(A::ScrollDownPage), - KeyCode::Char('j') | KeyCode::Down => Some(A::Down), - KeyCode::Char('k') | KeyCode::Up => Some(A::Up), - KeyCode::Char('h') | KeyCode::Left => Some(A::Left), - KeyCode::Char('l') | KeyCode::Right => Some(A::Right), - KeyCode::Char('?') | KeyCode::F(1) => Some(A::ShowHelp), - KeyCode::Char('s') => Some(A::ShowStats), - KeyCode::Char('f') => Some(A::ShowFiles), - KeyCode::Char('/') => Some(A::Search), - KeyCode::Char('a') => Some(A::AddMagnet), - KeyCode::Char('p') => Some(A::Pause), - KeyCode::Char('d') => Some(A::DeleteWithoutFiles), - KeyCode::Char('D') => Some(A::DeleteWithFiles), - KeyCode::Char(' ') => Some(A::Space), - KeyCode::Char(n @ '1'..='9') => { - Some(A::ChangeTab(n.to_digit(10).expect("This is ok") as u8)) - } - KeyCode::Enter => Some(A::Confirm), + Event::Key(key) => keymap.get(&(key.code, key.modifiers)).cloned(), _ => None, } } From af6f856e75c24fea6941cad1a617f82718dab173 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Wed, 26 Jun 2024 22:05:38 +0200 Subject: [PATCH 08/16] refactor writing to lines in help --- rm-main/src/ui/global_popups/help.rs | 51 +++++++++++++--------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/rm-main/src/ui/global_popups/help.rs b/rm-main/src/ui/global_popups/help.rs index 2debed4..fa2883d 100644 --- a/rm-main/src/ui/global_popups/help.rs +++ b/rm-main/src/ui/global_popups/help.rs @@ -12,7 +12,7 @@ use crate::{ app, ui::{centered_rect, components::Component}, }; -use rm_config::keymap::{GeneralAction, TorrentsAction, UserAction}; +use rm_config::keymap::{Keybinding, UserAction}; use rm_shared::action::Action; macro_rules! add_line { @@ -33,6 +33,24 @@ impl HelpPopup { pub const fn new(ctx: app::Ctx) -> Self { Self { ctx } } + + fn write_keybindings + UserAction + Ord>( + keybindings: &[Keybinding], + lines: &mut Vec, + ) { + let mut keys = BTreeMap::new(); + + for keybinding in keybindings { + keys.entry(&keybinding.action) + .or_insert_with(Vec::new) + .push(keybinding.keycode_string()); + } + + for (action, keycodes) in keys { + let keycode_string = keycodes.join(" / "); + add_line!(lines, keycode_string, action.desc()); + } + } } impl Component for HelpPopup { @@ -70,19 +88,7 @@ impl Component for HelpPopup { )]) .centered()]; - let mut general_keys: BTreeMap> = BTreeMap::new(); - - for keybinding in &self.ctx.config.keybindings.general.keybindings { - general_keys - .entry(keybinding.action) - .or_insert_with(Vec::new) - .push(keybinding.keycode_string()); - } - - for (action, keycodes) in general_keys { - let keycode_string = keycodes.join(" / "); - add_line!(lines, keycode_string, action.desc()); - } + Self::write_keybindings(&self.ctx.config.keybindings.general.keybindings, &mut lines); lines.push( Line::from(vec![Span::styled( @@ -92,19 +98,10 @@ impl Component for HelpPopup { .centered(), ); - let mut torrent_keys: BTreeMap> = BTreeMap::new(); - - for keybinding in &self.ctx.config.keybindings.torrents_tab.keybindings { - torrent_keys - .entry(keybinding.action) - .or_insert_with(Vec::new) - .push(keybinding.keycode_string()); - } - - for (action, keycodes) in torrent_keys { - let keycode_string = keycodes.join(" / "); - add_line!(lines, keycode_string, action.desc()); - } + Self::write_keybindings( + &self.ctx.config.keybindings.torrents_tab.keybindings, + &mut lines, + ); let help_text = Text::from(lines); let help_paragraph = Paragraph::new(help_text); From 0a238c1a2e21cff6f9fdf2a2249fdbd78df64c7f Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Thu, 27 Jun 2024 12:48:32 +0200 Subject: [PATCH 09/16] default config --- Cargo.lock | 1 + Cargo.toml | 1 + rm-config/Cargo.toml | 3 +-- rm-config/defaults/keymap.toml | 45 ++++++++++++++++++++++++++++++++++ rm-config/src/keymap.rs | 41 +++++++++++++++++-------------- rm-config/src/utils.rs | 30 ++++++++++++++++------- 6 files changed, 92 insertions(+), 29 deletions(-) create mode 100644 rm-config/defaults/keymap.toml diff --git a/Cargo.lock b/Cargo.lock index b93b294..e2cc16b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1250,6 +1250,7 @@ dependencies = [ "ratatui", "rm-shared", "serde", + "thiserror", "toml", "url", "xdg", diff --git a/Cargo.toml b/Cargo.toml index 15b445f..8857e40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ base64 = "0.22" xdg = "2.5" url = { version = "2.5", features = ["serde"] } toml = "0.8" +thiserror = "1" # Async tokio = { version = "1", features = ["macros", "sync"] } diff --git a/rm-config/Cargo.toml b/rm-config/Cargo.toml index f91ee6d..e1ab5f9 100644 --- a/rm-config/Cargo.toml +++ b/rm-config/Cargo.toml @@ -19,5 +19,4 @@ anyhow.workspace = true url.workspace = true ratatui.workspace = true crossterm.workspace = true - - +thiserror.workspace = true diff --git a/rm-config/defaults/keymap.toml b/rm-config/defaults/keymap.toml new file mode 100644 index 0000000..9d44063 --- /dev/null +++ b/rm-config/defaults/keymap.toml @@ -0,0 +1,45 @@ +[general] +keybindings = [ + { on = "?", action = "ShowHelp" }, + { on = "F1", action = "ShowHelp" }, + + { on = "q", action = "Quit" }, + { on = "Esc", action = "SoftQuit" }, + { on = "Enter", action = "Confirm" }, + { on = "Tab", action = "SwitchFocus" }, + { on = "/", action = "Search" }, + + { on = "1", action = "SwitchToTorrents" }, + { on = "2", action = "SwitchToSearch" }, + + { on = "Home", action = "GoToBeginning" }, + { on = "End", action = "GoToEnd" }, + { on = "PageUp", action = "ScrollPageUp" }, + { on = "PageDown", action = "ScrollPageDown" }, + + { modifier = "Ctrl", on = "u", action = "ScrollPageUp" }, + { modifier = "Ctrl", on = "d", action = "ScrollPageDown" }, + + # Arrows + { on = "Left", action = "Left" }, + { on = "Right", action = "Right" }, + { on = "Up", action = "Up"}, + { on = "Down", action = "Down" }, + + # Vi + { on = "h", action = "Left" }, + { on = "l", action = "Right" }, + { on = "k", action = "Up" }, + { on = "j", action = "Down" }, +] + +[torrents_tab] +keybindings = [ + { on = "a", action = "AddMagnet" }, + { on = "p", action = "Pause" }, + { on = "f", action = "ShowFiles" }, + { on = "s", action = "ShowStats" }, + + { on = "d", action = "DeleteWithoutFiles" }, + { on = "D", action = "DeleteWithFiles" }, +] diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 262ef80..a7c19e4 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -1,4 +1,6 @@ -use std::{collections::HashMap, marker::PhantomData, path::PathBuf, sync::OnceLock}; +use std::{ + collections::HashMap, io::ErrorKind, marker::PhantomData, path::PathBuf, sync::OnceLock, +}; use anyhow::Result; use crossterm::event::{KeyCode, KeyModifiers}; @@ -6,7 +8,6 @@ use serde::{ de::{self, Visitor}, Deserialize, Serialize, }; -use toml::Table; use crate::utils; use rm_shared::action::Action; @@ -303,6 +304,9 @@ pub enum KeyModifier { None, Ctrl, Shift, + Alt, + Super, + Meta, } impl KeyModifier { @@ -311,6 +315,9 @@ impl KeyModifier { KeyModifier::None => "", KeyModifier::Ctrl => "CTRL", KeyModifier::Shift => "SHIFT", + KeyModifier::Alt => "ALT", + KeyModifier::Super => "SUPER", + KeyModifier::Meta => "META", } } @@ -325,6 +332,9 @@ impl From for KeyModifiers { KeyModifier::None => KeyModifiers::NONE, KeyModifier::Ctrl => KeyModifiers::CONTROL, KeyModifier::Shift => KeyModifiers::SHIFT, + KeyModifier::Alt => KeyModifiers::ALT, + KeyModifier::Super => KeyModifiers::SUPER, + KeyModifier::Meta => KeyModifiers::META, } } } @@ -337,18 +347,19 @@ impl Default for KeyModifier { impl KeymapConfig { pub const FILENAME: &'static str = "keymap.toml"; + const DEFAULT_CONFIG: &'static str = include_str!("../defaults/keymap.toml"); pub fn init() -> Result { - let table = { - // TODO: handle errors or there will be hell to pay - if let Ok(table) = utils::fetch_config(Self::FILENAME) { - table - } else { - todo!(); - } - }; - - Self::table_to_keymap(&table) + match utils::fetch_config::(Self::FILENAME) { + Ok(config) => return Ok(config), + Err(e) => match e { + utils::ConfigFetchingError::Io(e) if e.kind() == ErrorKind::NotFound => { + return Ok(utils::put_config(Self::DEFAULT_CONFIG, Self::FILENAME)?) + } + utils::ConfigFetchingError::Toml(_) => anyhow::bail!(e), + _ => anyhow::bail!(e), + }, + } } pub fn to_map(self) -> HashMap<(KeyCode, KeyModifiers), Action> { @@ -364,12 +375,6 @@ impl KeymapConfig { map } - fn table_to_keymap(table: &Table) -> Result { - let config_string = table.to_string(); - let config = toml::from_str(&config_string)?; - Ok(config) - } - pub fn path() -> &'static PathBuf { static PATH: OnceLock = OnceLock::new(); PATH.get_or_init(|| utils::get_config_path(Self::FILENAME)) diff --git a/rm-config/src/utils.rs b/rm-config/src/utils.rs index 40d53a1..102afde 100644 --- a/rm-config/src/utils.rs +++ b/rm-config/src/utils.rs @@ -1,14 +1,23 @@ use std::{ fs::File, - io::{Read, Write}, + io::{self, Read, Write}, path::PathBuf, sync::OnceLock, }; use anyhow::Result; use serde::de::DeserializeOwned; +use thiserror::Error; use xdg::BaseDirectories; +#[derive(Error, Debug)] +pub enum ConfigFetchingError { + #[error(transparent)] + Io(#[from] io::Error), + #[error(transparent)] + Toml(#[from] toml::de::Error), +} + pub fn xdg_dirs() -> &'static BaseDirectories { static XDG_DIRS: OnceLock = OnceLock::new(); XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("rustmission").unwrap()) @@ -18,21 +27,24 @@ pub fn get_config_path(filename: &str) -> PathBuf { xdg_dirs().place_config_file(filename).unwrap() } -pub fn fetch_config(config_name: &str) -> Result { - let config_path = xdg_dirs() - .find_config_file(config_name) - .ok_or_else(|| anyhow::anyhow!("{} not found", config_name))?; +pub fn fetch_config(config_name: &str) -> Result { + let config_path = xdg_dirs().find_config_file(config_name).ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, format!("{config_name} not found")) + })?; let mut config_buf = String::new(); - let mut config_file = File::open(config_path).unwrap(); - config_file.read_to_string(&mut config_buf).unwrap(); + let mut config_file = File::open(config_path)?; + config_file.read_to_string(&mut config_buf)?; Ok(toml::from_str(&config_buf)?) } -pub fn put_config(content: &str, filename: &str) -> Result { +pub fn put_config( + content: &'static str, + filename: &str, +) -> Result { let config_path = get_config_path(filename); let mut config_file = File::create(config_path)?; config_file.write_all(content.as_bytes())?; - Ok(toml::from_str(content)?) + Ok(toml::from_str(content).expect("default configs are correct")) } From ad1736e73c06a2535ecf8a0e88ed33c951210fe6 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Fri, 28 Jun 2024 16:41:19 +0200 Subject: [PATCH 10/16] fix uppercase letters --- rm-config/src/keymap.rs | 14 +++++++++++++- rm-shared/src/action.rs | 9 ++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index a7c19e4..99aad77 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -284,7 +284,19 @@ impl<'de, T: Into + Deserialize<'de>> Deserialize<'de> for Keybinding } let on = on.ok_or_else(|| de::Error::missing_field("on"))?; let action = action.ok_or_else(|| de::Error::missing_field("action"))??; - Ok(Keybinding::new(on, action, modifier.transpose().unwrap())) + let modifier = modifier.transpose().unwrap(); + + if modifier.is_some() { + if let KeyCode::Char(char) = on { + if char.is_uppercase() { + return Err(de::Error::custom( + "you can't have a modifier with an uppercase letter, sorry", + )); + } + } + } + + Ok(Keybinding::new(on, action, modifier)) } } diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index c3ee590..a4c873d 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -78,7 +78,14 @@ pub fn event_to_action( match event { Event::Key(key) if mode == Mode::Input => Some(A::Input(key)), - Event::Key(key) => keymap.get(&(key.code, key.modifiers)).cloned(), + Event::Key(key) => { + if let KeyCode::Char(e) = key.code { + if e.is_uppercase() { + return keymap.get(&(key.code, KeyModifiers::NONE)).cloned(); + } + } + keymap.get(&(key.code, key.modifiers)).cloned() + } _ => None, } } From ddd683b58011135b7088d282746f43a359f0ce6f Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Fri, 28 Jun 2024 17:15:12 +0200 Subject: [PATCH 11/16] fix space --- rm-config/defaults/keymap.toml | 1 + rm-config/src/keymap.rs | 11 ++++++++++- rm-main/src/ui/tabs/torrents/popups/files.rs | 2 +- rm-shared/src/action.rs | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/rm-config/defaults/keymap.toml b/rm-config/defaults/keymap.toml index 9d44063..f8e52d7 100644 --- a/rm-config/defaults/keymap.toml +++ b/rm-config/defaults/keymap.toml @@ -6,6 +6,7 @@ keybindings = [ { on = "q", action = "Quit" }, { on = "Esc", action = "SoftQuit" }, { on = "Enter", action = "Confirm" }, + { on = " ", action = "Select" }, { on = "Tab", action = "SwitchFocus" }, { on = "/", action = "Search" }, diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 99aad77..1e71477 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -37,6 +37,7 @@ pub enum GeneralAction { Search, SwitchFocus, Confirm, + Select, ScrollPageDown, ScrollPageUp, GoToBeginning, @@ -62,6 +63,7 @@ impl UserAction for GeneralAction { GeneralAction::Search => "search", GeneralAction::SwitchFocus => "switch focus", GeneralAction::Confirm => "confirm", + GeneralAction::Select => "select", GeneralAction::ScrollPageDown => "scroll page down", GeneralAction::ScrollPageUp => "scroll page up", GeneralAction::GoToBeginning => "scroll to the beginning", @@ -85,6 +87,7 @@ impl From for Action { GeneralAction::Search => Action::Search, GeneralAction::SwitchFocus => Action::ChangeFocus, GeneralAction::Confirm => Action::Confirm, + GeneralAction::Select => Action::Select, GeneralAction::ScrollPageDown => Action::ScrollDownPage, GeneralAction::ScrollPageUp => Action::ScrollUpPage, GeneralAction::GoToBeginning => Action::Home, @@ -160,7 +163,13 @@ impl> Keybinding { KeyCode::Delete => todo!(), KeyCode::Insert => "Insert".into(), KeyCode::F(i) => format!("F{i}"), - KeyCode::Char(c) => c.into(), + KeyCode::Char(c) => { + if c == ' ' { + "Space".into() + } else { + c.into() + } + } KeyCode::Null => todo!(), KeyCode::Esc => "Esc".into(), KeyCode::CapsLock => todo!(), diff --git a/rm-main/src/ui/tabs/torrents/popups/files.rs b/rm-main/src/ui/tabs/torrents/popups/files.rs index ed47154..7806fce 100644 --- a/rm-main/src/ui/tabs/torrents/popups/files.rs +++ b/rm-main/src/ui/tabs/torrents/popups/files.rs @@ -111,7 +111,7 @@ impl Component for FilesPopup { Some(A::Render) } (A::Confirm, CurrentFocus::CloseButton) => Some(A::Quit), - (A::Space | A::Confirm, CurrentFocus::Files) => { + (A::Select | A::Confirm, CurrentFocus::Files) => { if let Some(torrent) = &mut *self.torrent.lock().unwrap() { let wanted_ids = torrent.wanted.as_mut().unwrap(); diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index a4c873d..915a93e 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -17,7 +17,7 @@ pub enum Action { Home, End, Confirm, - Space, + Select, ShowHelp, ShowStats, ShowFiles, From 0e22142f1627288168cfa11eb7bfa5c9b3bfd159 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sat, 29 Jun 2024 15:12:37 +0200 Subject: [PATCH 12/16] make help key in the bottom reflect reality --- rm-config/src/keymap.rs | 56 +++++++++++++------ rm-config/src/lib.rs | 7 +-- rm-main/src/app.rs | 2 +- rm-main/src/ui/tabs/torrents/tasks/default.rs | 9 ++- 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 1e71477..82ebbdc 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -14,12 +14,14 @@ use rm_shared::action::Action; #[derive(Serialize, Deserialize, Clone)] pub struct KeymapConfig { - pub general: General, - pub torrents_tab: TorrentsTab, + pub general: KeybindsHolder, + pub torrents_tab: KeybindsHolder, + #[serde(skip)] + pub keymap: HashMap<(KeyCode, KeyModifiers), Action>, } #[derive(Serialize, Deserialize, Clone)] -pub struct General> { +pub struct KeybindsHolder> { pub keybindings: Vec>, } @@ -96,11 +98,6 @@ impl From for Action { } } -#[derive(Serialize, Deserialize, Clone)] -pub struct TorrentsTab> { - pub keybindings: Vec>, -} - #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum TorrentsAction { AddMagnet, @@ -372,10 +369,16 @@ impl KeymapConfig { pub fn init() -> Result { match utils::fetch_config::(Self::FILENAME) { - Ok(config) => return Ok(config), + Ok(mut keymap_config) => { + keymap_config.populate_hashmap(); + return Ok(keymap_config); + } Err(e) => match e { utils::ConfigFetchingError::Io(e) if e.kind() == ErrorKind::NotFound => { - return Ok(utils::put_config(Self::DEFAULT_CONFIG, Self::FILENAME)?) + let mut keymap_config = + utils::put_config::(Self::DEFAULT_CONFIG, Self::FILENAME)?; + keymap_config.populate_hashmap(); + return Ok(keymap_config); } utils::ConfigFetchingError::Toml(_) => anyhow::bail!(e), _ => anyhow::bail!(e), @@ -383,17 +386,36 @@ impl KeymapConfig { } } - pub fn to_map(self) -> HashMap<(KeyCode, KeyModifiers), Action> { - let mut map = HashMap::new(); - for keybinding in self.general.keybindings { + pub fn get_keys_for_action(&self, action: Action) -> Option { + let mut keys = vec![]; + + for keybinding in &self.general.keybindings { + if action == keybinding.action.into() { + keys.push(keybinding.keycode_string()); + } + } + for keybinding in &self.torrents_tab.keybindings { + if action == keybinding.action.into() { + keys.push(keybinding.keycode_string()); + } + } + + if keys.is_empty() { + return None; + } else { + Some(keys.join("/")) + } + } + + fn populate_hashmap(&mut self) { + for keybinding in &self.general.keybindings { let hash_value = (keybinding.on, keybinding.modifier.into()); - map.insert(hash_value, keybinding.action.into()); + self.keymap.insert(hash_value, keybinding.action.into()); } - for keybinding in self.torrents_tab.keybindings { + for keybinding in &self.torrents_tab.keybindings { let hash_value = (keybinding.on, keybinding.modifier.into()); - map.insert(hash_value, keybinding.action.into()); + self.keymap.insert(hash_value, keybinding.action.into()); } - map } pub fn path() -> &'static PathBuf { diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index 1cc39e1..a2e3a8b 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -2,19 +2,15 @@ pub mod keymap; mod main_config; mod utils; -use std::{collections::HashMap, path::PathBuf}; +use std::path::PathBuf; use anyhow::Result; -use crossterm::event::{KeyCode, KeyModifiers}; use keymap::KeymapConfig; use main_config::MainConfig; -use rm_shared::action::Action; - pub struct Config { pub general: main_config::General, pub connection: main_config::Connection, - pub keymap: HashMap<(KeyCode, KeyModifiers), Action>, pub keybindings: KeymapConfig, pub directories: Directories, } @@ -38,7 +34,6 @@ impl Config { general: main_config.general, connection: main_config.connection, keybindings: keybindings.clone(), - keymap: keybindings.to_map(), directories, }) } diff --git a/rm-main/src/app.rs b/rm-main/src/app.rs index d9d5281..4464295 100644 --- a/rm-main/src/app.rs +++ b/rm-main/src/app.rs @@ -111,7 +111,7 @@ impl App { tokio::select! { event = tui_event => { - if let Some(action) = event_to_action(self.mode, event.unwrap(), &self.ctx.config.keymap) { + if let Some(action) = event_to_action(self.mode, event.unwrap(), &self.ctx.config.keybindings.keymap) { if let Some(action) = self.update(action).await { self.ctx.action_tx.send(action).unwrap(); } diff --git a/rm-main/src/ui/tabs/torrents/tasks/default.rs b/rm-main/src/ui/tabs/torrents/tasks/default.rs index 201e26b..f508350 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/default.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/default.rs @@ -15,7 +15,14 @@ impl DefaultBar { impl Component for DefaultBar { fn render(&mut self, f: &mut ratatui::Frame<'_>, rect: Rect) { if self.ctx.config.general.beginner_mode { - f.render_widget("F1 - help", rect) + if let Some(keys) = self + .ctx + .config + .keybindings + .get_keys_for_action(rm_shared::action::Action::ShowHelp) + { + f.render_widget(format!("󰘥 {keys} - help"), rect) + } } } } From 90b6ffe3544057aa96d6775fbbe7b0ff2852a4e1 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sun, 30 Jun 2024 11:39:36 +0200 Subject: [PATCH 13/16] rename SoftQuit to Close --- rm-config/defaults/keymap.toml | 2 +- rm-config/src/keymap.rs | 6 +++--- rm-main/src/ui/global_popups/error.rs | 2 +- rm-main/src/ui/global_popups/help.rs | 4 ++-- rm-main/src/ui/tabs/torrents/popups/stats.rs | 2 +- rm-shared/src/action.rs | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/rm-config/defaults/keymap.toml b/rm-config/defaults/keymap.toml index f8e52d7..26272db 100644 --- a/rm-config/defaults/keymap.toml +++ b/rm-config/defaults/keymap.toml @@ -4,7 +4,7 @@ keybindings = [ { on = "F1", action = "ShowHelp" }, { on = "q", action = "Quit" }, - { on = "Esc", action = "SoftQuit" }, + { on = "Esc", action = "Close" }, { on = "Enter", action = "Confirm" }, { on = " ", action = "Select" }, { on = "Tab", action = "SwitchFocus" }, diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 82ebbdc..b904e7e 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -29,7 +29,7 @@ pub struct KeybindsHolder> { pub enum GeneralAction { ShowHelp, Quit, - SoftQuit, + Close, SwitchToTorrents, SwitchToSearch, Left, @@ -55,7 +55,7 @@ impl UserAction for GeneralAction { match self { GeneralAction::ShowHelp => "toggle help", GeneralAction::Quit => "quit Rustmission / a popup", - GeneralAction::SoftQuit => "close a popup / task", + GeneralAction::Close => "close a popup / task", GeneralAction::SwitchToTorrents => "switch to torrents tab", GeneralAction::SwitchToSearch => "switch to search tab", GeneralAction::Left => "switch to tab left", @@ -79,7 +79,7 @@ impl From for Action { match value { GeneralAction::ShowHelp => Action::ShowHelp, GeneralAction::Quit => Action::Quit, - GeneralAction::SoftQuit => Action::SoftQuit, + GeneralAction::Close => Action::Close, GeneralAction::SwitchToTorrents => Action::ChangeTab(1), GeneralAction::SwitchToSearch => Action::ChangeTab(2), GeneralAction::Left => Action::Left, diff --git a/rm-main/src/ui/global_popups/error.rs b/rm-main/src/ui/global_popups/error.rs index 8963299..faf0ec1 100644 --- a/rm-main/src/ui/global_popups/error.rs +++ b/rm-main/src/ui/global_popups/error.rs @@ -26,7 +26,7 @@ impl Component for ErrorPopup { fn handle_actions(&mut self, action: Action) -> Option { match action { _ if action.is_soft_quit() => Some(action), - Action::Confirm => Some(Action::SoftQuit), + Action::Confirm => Some(Action::Close), _ => None, } } diff --git a/rm-main/src/ui/global_popups/help.rs b/rm-main/src/ui/global_popups/help.rs index fa2883d..d891ac4 100644 --- a/rm-main/src/ui/global_popups/help.rs +++ b/rm-main/src/ui/global_popups/help.rs @@ -56,8 +56,8 @@ impl HelpPopup { impl Component for HelpPopup { fn handle_actions(&mut self, action: Action) -> Option { match action { - action if action.is_soft_quit() => Some(Action::SoftQuit), - Action::Confirm | Action::ShowHelp => Some(Action::SoftQuit), + action if action.is_soft_quit() => Some(Action::Close), + Action::Confirm | Action::ShowHelp => Some(Action::Close), _ => None, } } diff --git a/rm-main/src/ui/tabs/torrents/popups/stats.rs b/rm-main/src/ui/tabs/torrents/popups/stats.rs index 92fbe79..3c02123 100644 --- a/rm-main/src/ui/tabs/torrents/popups/stats.rs +++ b/rm-main/src/ui/tabs/torrents/popups/stats.rs @@ -30,7 +30,7 @@ impl Component for StatisticsPopup { use Action as A; match action { _ if action.is_soft_quit() => Some(action), - A::Confirm => Some(Action::SoftQuit), + A::Confirm => Some(Action::Close), _ => None, } } diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index 915a93e..647033f 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -6,7 +6,7 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; pub enum Action { HardQuit, Quit, - SoftQuit, + Close, Render, Up, Down, @@ -50,7 +50,7 @@ impl Action { } pub fn is_soft_quit(&self) -> bool { - self.is_quit() || *self == Self::SoftQuit + self.is_quit() || *self == Self::Close } } From 6a9af8b179fffdf59ee8a59a8fd761b9111a36ca Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sun, 30 Jun 2024 11:44:19 +0200 Subject: [PATCH 14/16] rename keymodifiers to crosstermkeymodifiers --- rm-config/src/keymap.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index b904e7e..6b32604 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -3,7 +3,7 @@ use std::{ }; use anyhow::Result; -use crossterm::event::{KeyCode, KeyModifiers}; +use crossterm::event::{KeyCode, KeyModifiers as CrosstermKeyModifiers}; use serde::{ de::{self, Visitor}, Deserialize, Serialize, @@ -17,7 +17,7 @@ pub struct KeymapConfig { pub general: KeybindsHolder, pub torrents_tab: KeybindsHolder, #[serde(skip)] - pub keymap: HashMap<(KeyCode, KeyModifiers), Action>, + pub keymap: HashMap<(KeyCode, CrosstermKeyModifiers), Action>, } #[derive(Serialize, Deserialize, Clone)] @@ -344,15 +344,15 @@ impl KeyModifier { } } -impl From for KeyModifiers { +impl From for CrosstermKeyModifiers { fn from(value: KeyModifier) -> Self { match value { - KeyModifier::None => KeyModifiers::NONE, - KeyModifier::Ctrl => KeyModifiers::CONTROL, - KeyModifier::Shift => KeyModifiers::SHIFT, - KeyModifier::Alt => KeyModifiers::ALT, - KeyModifier::Super => KeyModifiers::SUPER, - KeyModifier::Meta => KeyModifiers::META, + KeyModifier::None => CrosstermKeyModifiers::NONE, + KeyModifier::Ctrl => CrosstermKeyModifiers::CONTROL, + KeyModifier::Shift => CrosstermKeyModifiers::SHIFT, + KeyModifier::Alt => CrosstermKeyModifiers::ALT, + KeyModifier::Super => CrosstermKeyModifiers::SUPER, + KeyModifier::Meta => CrosstermKeyModifiers::META, } } } From 047fc80d83205695979368184668433d1df7351d Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sun, 30 Jun 2024 11:45:16 +0200 Subject: [PATCH 15/16] merge 2 match branches --- rm-config/src/keymap.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap.rs index 6b32604..012cf46 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap.rs @@ -380,7 +380,6 @@ impl KeymapConfig { keymap_config.populate_hashmap(); return Ok(keymap_config); } - utils::ConfigFetchingError::Toml(_) => anyhow::bail!(e), _ => anyhow::bail!(e), }, } From a9f16558aad91caffda22c3661782102eea42ed3 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sun, 30 Jun 2024 12:40:53 +0200 Subject: [PATCH 16/16] restructure keymap config --- rm-config/src/keymap/actions/general.rs | 73 ++++++++++++ rm-config/src/keymap/actions/mod.rs | 8 ++ rm-config/src/keymap/actions/torrents_tab.rs | 40 +++++++ rm-config/src/{keymap.rs => keymap/mod.rs} | 115 +------------------ rm-main/src/ui/global_popups/help.rs | 2 +- 5 files changed, 127 insertions(+), 111 deletions(-) create mode 100644 rm-config/src/keymap/actions/general.rs create mode 100644 rm-config/src/keymap/actions/mod.rs create mode 100644 rm-config/src/keymap/actions/torrents_tab.rs rename rm-config/src/{keymap.rs => keymap/mod.rs} (74%) diff --git a/rm-config/src/keymap/actions/general.rs b/rm-config/src/keymap/actions/general.rs new file mode 100644 index 0000000..de3bff7 --- /dev/null +++ b/rm-config/src/keymap/actions/general.rs @@ -0,0 +1,73 @@ +use rm_shared::action::Action; +use serde::{Deserialize, Serialize}; + +use super::UserAction; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum GeneralAction { + ShowHelp, + Quit, + Close, + SwitchToTorrents, + SwitchToSearch, + Left, + Right, + Down, + Up, + Search, + SwitchFocus, + Confirm, + Select, + ScrollPageDown, + ScrollPageUp, + GoToBeginning, + GoToEnd, +} + +impl UserAction for GeneralAction { + fn desc(&self) -> &'static str { + match self { + GeneralAction::ShowHelp => "toggle help", + GeneralAction::Quit => "quit Rustmission / a popup", + GeneralAction::Close => "close a popup / task", + GeneralAction::SwitchToTorrents => "switch to torrents tab", + GeneralAction::SwitchToSearch => "switch to search tab", + GeneralAction::Left => "switch to tab left", + GeneralAction::Right => "switch to tab right", + GeneralAction::Down => "move down", + GeneralAction::Up => "move up", + GeneralAction::Search => "search", + GeneralAction::SwitchFocus => "switch focus", + GeneralAction::Confirm => "confirm", + GeneralAction::Select => "select", + GeneralAction::ScrollPageDown => "scroll page down", + GeneralAction::ScrollPageUp => "scroll page up", + GeneralAction::GoToBeginning => "scroll to the beginning", + GeneralAction::GoToEnd => "scroll to the end", + } + } +} + +impl From for Action { + fn from(value: GeneralAction) -> Self { + match value { + GeneralAction::ShowHelp => Action::ShowHelp, + GeneralAction::Quit => Action::Quit, + GeneralAction::Close => Action::Close, + GeneralAction::SwitchToTorrents => Action::ChangeTab(1), + GeneralAction::SwitchToSearch => Action::ChangeTab(2), + GeneralAction::Left => Action::Left, + GeneralAction::Right => Action::Right, + GeneralAction::Down => Action::Down, + GeneralAction::Up => Action::Up, + GeneralAction::Search => Action::Search, + GeneralAction::SwitchFocus => Action::ChangeFocus, + GeneralAction::Confirm => Action::Confirm, + GeneralAction::Select => Action::Select, + GeneralAction::ScrollPageDown => Action::ScrollDownPage, + GeneralAction::ScrollPageUp => Action::ScrollUpPage, + GeneralAction::GoToBeginning => Action::Home, + GeneralAction::GoToEnd => Action::End, + } + } +} diff --git a/rm-config/src/keymap/actions/mod.rs b/rm-config/src/keymap/actions/mod.rs new file mode 100644 index 0000000..76131db --- /dev/null +++ b/rm-config/src/keymap/actions/mod.rs @@ -0,0 +1,8 @@ +use rm_shared::action::Action; + +pub mod general; +pub mod torrents_tab; + +pub trait UserAction: Into { + fn desc(&self) -> &'static str; +} diff --git a/rm-config/src/keymap/actions/torrents_tab.rs b/rm-config/src/keymap/actions/torrents_tab.rs new file mode 100644 index 0000000..c1b39a7 --- /dev/null +++ b/rm-config/src/keymap/actions/torrents_tab.rs @@ -0,0 +1,40 @@ +use rm_shared::action::Action; +use serde::{Deserialize, Serialize}; + +use super::UserAction; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum TorrentsAction { + AddMagnet, + Pause, + DeleteWithFiles, + DeleteWithoutFiles, + ShowFiles, + ShowStats, +} + +impl UserAction for TorrentsAction { + fn desc(&self) -> &'static str { + match self { + TorrentsAction::AddMagnet => "add a magnet", + TorrentsAction::Pause => "pause/unpause", + TorrentsAction::DeleteWithFiles => "delete with files", + TorrentsAction::DeleteWithoutFiles => "delete without files", + TorrentsAction::ShowFiles => "show files", + TorrentsAction::ShowStats => "show statistics", + } + } +} + +impl From for Action { + fn from(value: TorrentsAction) -> Self { + match value { + TorrentsAction::AddMagnet => Action::AddMagnet, + TorrentsAction::Pause => Action::Pause, + TorrentsAction::DeleteWithFiles => Action::DeleteWithFiles, + TorrentsAction::DeleteWithoutFiles => Action::DeleteWithoutFiles, + TorrentsAction::ShowFiles => Action::ShowFiles, + TorrentsAction::ShowStats => Action::ShowStats, + } + } +} diff --git a/rm-config/src/keymap.rs b/rm-config/src/keymap/mod.rs similarity index 74% rename from rm-config/src/keymap.rs rename to rm-config/src/keymap/mod.rs index 012cf46..7f8526b 100644 --- a/rm-config/src/keymap.rs +++ b/rm-config/src/keymap/mod.rs @@ -1,3 +1,5 @@ +pub mod actions; + use std::{ collections::HashMap, io::ErrorKind, marker::PhantomData, path::PathBuf, sync::OnceLock, }; @@ -12,6 +14,8 @@ use serde::{ use crate::utils; use rm_shared::action::Action; +use self::actions::{general::GeneralAction, torrents_tab::TorrentsAction}; + #[derive(Serialize, Deserialize, Clone)] pub struct KeymapConfig { pub general: KeybindsHolder, @@ -25,115 +29,6 @@ pub struct KeybindsHolder> { pub keybindings: Vec>, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum GeneralAction { - ShowHelp, - Quit, - Close, - SwitchToTorrents, - SwitchToSearch, - Left, - Right, - Down, - Up, - Search, - SwitchFocus, - Confirm, - Select, - ScrollPageDown, - ScrollPageUp, - GoToBeginning, - GoToEnd, -} - -pub trait UserAction: Into { - fn desc(&self) -> &'static str; -} - -impl UserAction for GeneralAction { - fn desc(&self) -> &'static str { - match self { - GeneralAction::ShowHelp => "toggle help", - GeneralAction::Quit => "quit Rustmission / a popup", - GeneralAction::Close => "close a popup / task", - GeneralAction::SwitchToTorrents => "switch to torrents tab", - GeneralAction::SwitchToSearch => "switch to search tab", - GeneralAction::Left => "switch to tab left", - GeneralAction::Right => "switch to tab right", - GeneralAction::Down => "move down", - GeneralAction::Up => "move up", - GeneralAction::Search => "search", - GeneralAction::SwitchFocus => "switch focus", - GeneralAction::Confirm => "confirm", - GeneralAction::Select => "select", - GeneralAction::ScrollPageDown => "scroll page down", - GeneralAction::ScrollPageUp => "scroll page up", - GeneralAction::GoToBeginning => "scroll to the beginning", - GeneralAction::GoToEnd => "scroll to the end", - } - } -} - -impl From for Action { - fn from(value: GeneralAction) -> Self { - match value { - GeneralAction::ShowHelp => Action::ShowHelp, - GeneralAction::Quit => Action::Quit, - GeneralAction::Close => Action::Close, - GeneralAction::SwitchToTorrents => Action::ChangeTab(1), - GeneralAction::SwitchToSearch => Action::ChangeTab(2), - GeneralAction::Left => Action::Left, - GeneralAction::Right => Action::Right, - GeneralAction::Down => Action::Down, - GeneralAction::Up => Action::Up, - GeneralAction::Search => Action::Search, - GeneralAction::SwitchFocus => Action::ChangeFocus, - GeneralAction::Confirm => Action::Confirm, - GeneralAction::Select => Action::Select, - GeneralAction::ScrollPageDown => Action::ScrollDownPage, - GeneralAction::ScrollPageUp => Action::ScrollUpPage, - GeneralAction::GoToBeginning => Action::Home, - GeneralAction::GoToEnd => Action::End, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum TorrentsAction { - AddMagnet, - Pause, - DeleteWithFiles, - DeleteWithoutFiles, - ShowFiles, - ShowStats, -} - -impl UserAction for TorrentsAction { - fn desc(&self) -> &'static str { - match self { - TorrentsAction::AddMagnet => "add a magnet", - TorrentsAction::Pause => "pause/unpause", - TorrentsAction::DeleteWithFiles => "delete with files", - TorrentsAction::DeleteWithoutFiles => "delete without files", - TorrentsAction::ShowFiles => "show files", - TorrentsAction::ShowStats => "show statistics", - } - } -} - -impl From for Action { - fn from(value: TorrentsAction) -> Self { - match value { - TorrentsAction::AddMagnet => Action::AddMagnet, - TorrentsAction::Pause => Action::Pause, - TorrentsAction::DeleteWithFiles => Action::DeleteWithFiles, - TorrentsAction::DeleteWithoutFiles => Action::DeleteWithoutFiles, - TorrentsAction::ShowFiles => Action::ShowFiles, - TorrentsAction::ShowStats => Action::ShowStats, - } - } -} - #[derive(Serialize, Clone)] pub struct Keybinding> { pub on: KeyCode, @@ -365,7 +260,7 @@ impl Default for KeyModifier { impl KeymapConfig { pub const FILENAME: &'static str = "keymap.toml"; - const DEFAULT_CONFIG: &'static str = include_str!("../defaults/keymap.toml"); + const DEFAULT_CONFIG: &'static str = include_str!("../../defaults/keymap.toml"); pub fn init() -> Result { match utils::fetch_config::(Self::FILENAME) { diff --git a/rm-main/src/ui/global_popups/help.rs b/rm-main/src/ui/global_popups/help.rs index d891ac4..71d003b 100644 --- a/rm-main/src/ui/global_popups/help.rs +++ b/rm-main/src/ui/global_popups/help.rs @@ -12,7 +12,7 @@ use crate::{ app, ui::{centered_rect, components::Component}, }; -use rm_config::keymap::{Keybinding, UserAction}; +use rm_config::keymap::{actions::UserAction, Keybinding}; use rm_shared::action::Action; macro_rules! add_line {