From 168aa25f48fa476cd33aaae284e01ff2f8e32e57 Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:59:19 +0200 Subject: [PATCH] refactor: move help popup logic into config (#107) --- rm-config/src/keymap/mod.rs | 79 +++++++++-- rm-main/src/tui/global_popups/help.rs | 181 ++++++++------------------ 2 files changed, 120 insertions(+), 140 deletions(-) diff --git a/rm-config/src/keymap/mod.rs b/rm-config/src/keymap/mod.rs index 4114548..0f3522c 100644 --- a/rm-config/src/keymap/mod.rs +++ b/rm-config/src/keymap/mod.rs @@ -1,10 +1,14 @@ pub mod actions; use std::{ - collections::HashMap, io::ErrorKind, marker::PhantomData, path::PathBuf, sync::OnceLock, + collections::{BTreeMap, HashMap}, + io::ErrorKind, + marker::PhantomData, + path::PathBuf, + sync::OnceLock, }; -use actions::search_tab::SearchAction; +use actions::{search_tab::SearchAction, UserAction}; use anyhow::{Context, Result}; use crossterm::event::{KeyCode, KeyModifiers as CrosstermKeyModifiers}; use serde::{ @@ -34,14 +38,69 @@ pub struct KeymapConfig { } #[derive(Serialize, Deserialize, Clone)] -pub struct KeybindsHolder> { +pub struct KeybindsHolder + UserAction> { pub keybindings: Vec>, - #[serde(skip)] - pub help_repr: Vec<(String, &'static str)>, +} + +impl + Ord + UserAction> KeybindsHolder { + const KEYS_DELIMITER: &'static str = ", "; + + pub fn get_help_repr(&self) -> Vec<(String, &'static str)> { + let mut keys: BTreeMap<&T, Vec> = BTreeMap::new(); + for keybinding in &self.keybindings { + if !keybinding.show_in_help { + continue; + } + + keys.entry(&keybinding.action) + .or_insert_with(Vec::new) + .push(keybinding.keycode_string()); + } + + let mut new_keys = vec![]; + + for (action, keycodes) in keys { + new_keys.push((action, keycodes)); + } + + let mut res = vec![]; + let mut skip_next_loop = false; + + for (idx, (action, keycodes)) in new_keys.iter().enumerate() { + if skip_next_loop { + skip_next_loop = false; + continue; + } + + if let Some((next_action, next_keycodes)) = new_keys.get(idx + 1) { + if action.is_mergable_with(next_action) { + skip_next_loop = true; + let keys = format!( + "{} / {}", + keycodes.join(Self::KEYS_DELIMITER), + next_keycodes.join(Self::KEYS_DELIMITER) + ); + + let desc = action + .merged_desc(next_action) + .expect("keys checked for mergability before"); + + res.push((keys, desc)); + continue; + } + } + + let keycode_string = keycodes.join(Self::KEYS_DELIMITER); + let desc = action.desc(); + res.push((keycode_string, desc)); + } + + res + } } #[derive(Serialize, Clone)] -pub struct Keybinding { +pub struct Keybinding { pub on: KeyCode, #[serde(default)] pub modifier: KeyModifier, @@ -49,7 +108,7 @@ pub struct Keybinding { pub show_in_help: bool, } -impl> Keybinding { +impl + UserAction> Keybinding { pub fn keycode_string(&self) -> String { let key = match self.on { KeyCode::Backspace => "Backspace".into(), @@ -95,7 +154,7 @@ impl> Keybinding { } } -impl Keybinding { +impl Keybinding { fn new(on: KeyCode, action: T, modifier: Option, show_in_help: bool) -> Self { Self { on, @@ -106,7 +165,7 @@ impl Keybinding { } } -impl<'de, T: Deserialize<'de>> Deserialize<'de> for Keybinding { +impl<'de, T: Deserialize<'de> + UserAction> Deserialize<'de> for Keybinding { fn deserialize(deserializer: D) -> std::prelude::v1::Result where D: serde::Deserializer<'de>, @@ -124,7 +183,7 @@ impl<'de, T: Deserialize<'de>> Deserialize<'de> for Keybinding { phantom: PhantomData, } - impl<'de, T: Deserialize<'de>> Visitor<'de> for KeybindingVisitor { + impl<'de, T: Deserialize<'de> + UserAction> Visitor<'de> for KeybindingVisitor { type Value = Keybinding; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/rm-main/src/tui/global_popups/help.rs b/rm-main/src/tui/global_popups/help.rs index 2a97f46..49d6bac 100644 --- a/rm-main/src/tui/global_popups/help.rs +++ b/rm-main/src/tui/global_popups/help.rs @@ -1,14 +1,9 @@ -use std::collections::BTreeMap; - use ratatui::{ prelude::*, widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, }; -use rm_config::{ - keymap::{actions::UserAction, Keybinding}, - CONFIG, -}; +use rm_config::CONFIG; use rm_shared::action::Action; use crate::tui::{ @@ -26,11 +21,14 @@ macro_rules! add_line { }; } -const KEYS_DELIMITER: &str = ", "; - pub struct HelpPopup { ctx: app::Ctx, scroll: Option, + global_keys: Vec<(String, &'static str)>, + torrent_keys: Vec<(String, &'static str)>, + search_keys: Vec<(String, &'static str)>, + max_key_len: usize, + max_line_len: usize, } struct Scroll { @@ -51,103 +49,42 @@ impl Scroll { impl HelpPopup { pub fn new(ctx: app::Ctx) -> Self { - Self { ctx, scroll: None } - } - - fn get_keybindings + UserAction + Ord>( - keybindings: &[Keybinding], - max_keycode_len: &mut usize, - max_line_len: &mut usize, - ) -> Vec<(String, &'static str)> { - let mut keys: BTreeMap<&T, Vec> = BTreeMap::new(); - - for keybinding in keybindings { - if !keybinding.show_in_help { - continue; - } - - let keycode = keybinding.keycode_string(); - if keycode.len() > *max_keycode_len { - *max_keycode_len = keycode.chars().count(); - } - - keys.entry(&keybinding.action) - .or_insert_with(Vec::new) - .push(keybinding.keycode_string()); - } - - for keycodes in keys.values() { - let mut keycodes_total_len = 0; - let delimiter_len = if keycodes.len() >= 2 { - (keycodes.len() - 1) * 3 - } else { - 0 - }; - - for keycode in keycodes { - keycodes_total_len += keycode.chars().count(); - } - - if keycodes_total_len + delimiter_len > *max_keycode_len { - *max_keycode_len = keycodes_total_len + delimiter_len; - } - } - - let mut new_keys = vec![]; - - for (action, keycodes) in keys { - new_keys.push((action, keycodes)); - } - - let mut res = vec![]; - let mut skip_next_loop = false; - for (idx, (action, keycodes)) in new_keys.iter().enumerate() { - if skip_next_loop { - skip_next_loop = false; - continue; - } - - if let Some(next_key) = new_keys.get(idx + 1) { - if action.is_mergable_with(next_key.0) { - skip_next_loop = true; - let keys = format!( - "{} / {}", - keycodes.join(KEYS_DELIMITER), - next_key.1.join(KEYS_DELIMITER) - ); - - if keys.chars().count() > *max_keycode_len { - *max_keycode_len = keys.chars().count(); - } - - let desc = action - .merged_desc(next_key.0) - .expect("keys checked for mergability before"); - - let line_len = keys.chars().count() + desc.chars().count() + 3; - if line_len > *max_line_len { - *max_line_len = line_len; - } + let mut max_key_len = 0; + let mut max_line_len = 0; + let global_keys = CONFIG.keybindings.general.get_help_repr(); + let torrent_keys = CONFIG.keybindings.torrents_tab.get_help_repr(); + let search_keys = CONFIG.keybindings.search_tab.get_help_repr(); - res.push((keys, desc)); + let mut calc_max_lens = |keys: &[(String, &'static str)]| { + for (keycode, desc) in keys { + let key_len = keycode.chars().count(); + let desc_len = desc.chars().count(); + let line_len = key_len + desc_len + 3; + if key_len > max_key_len { + max_key_len = key_len; + } - continue; + if line_len > max_line_len { + max_line_len = line_len; } } + }; - let keycode_string = keycodes.join(KEYS_DELIMITER); - if keycode_string.chars().count() > *max_keycode_len { - *max_keycode_len = keycode_string.chars().count(); - } - let desc = action.desc(); - let line_len = keycode_string.chars().count() + desc.chars().count() + 3; - if line_len > *max_line_len { - *max_line_len = line_len; - } - res.push((keycode_string, desc)); - } + calc_max_lens(&global_keys); + calc_max_lens(&torrent_keys); + calc_max_lens(&search_keys); - res + debug_assert!(max_key_len > 0); + debug_assert!(max_line_len > 0); + Self { + ctx, + scroll: None, + global_keys, + torrent_keys, + search_keys, + max_key_len, + max_line_len, + } } fn scroll_down(&mut self) -> ComponentAction { @@ -209,54 +146,38 @@ impl Component for HelpPopup { let block = popup_block_with_close_highlight(" Help "); - let mut to_pad = 0; - let mut max_line_len = 0; - let mut global_keys = Self::get_keybindings( - &CONFIG.keybindings.general.keybindings, - &mut to_pad, - &mut max_line_len, - ); - let mut torrents_keys = Self::get_keybindings( - &CONFIG.keybindings.torrents_tab.keybindings, - &mut to_pad, - &mut max_line_len, - ); - let mut search_keys = Self::get_keybindings( - &CONFIG.keybindings.search_tab.keybindings, - &mut to_pad, - &mut max_line_len, - ); - - debug_assert!(to_pad > 0); - debug_assert!(max_line_len > 0); - let to_pad_additionally = (text_rect .width - .saturating_sub(max_line_len.try_into().unwrap()) + .saturating_sub(self.max_line_len.try_into().unwrap()) / 2) .saturating_sub(6); - to_pad += usize::from(to_pad_additionally); + let pad_amount = usize::from(to_pad_additionally) + self.max_key_len; - let pad_keys = |keys: &mut Vec<(String, &'static str)>| { + let padded_keys = |keys: &Vec<(String, &'static str)>| -> Vec<(String, &'static str)> { + let mut new_keys = vec![]; for key in keys { - let mut how_much_to_pad = to_pad.saturating_sub(key.0.chars().count()); + let mut keycode = key.0.clone(); + let mut how_much_to_pad = pad_amount.saturating_sub(key.0.chars().count()); while how_much_to_pad > 0 { - key.0.insert(0, ' '); + keycode.insert(0, ' '); how_much_to_pad -= 1; } + new_keys.push((keycode, key.1)); } + new_keys }; - pad_keys(&mut global_keys); - pad_keys(&mut torrents_keys); - pad_keys(&mut search_keys); + let global_keys = padded_keys(&mut self.global_keys); + let torrent_keys = padded_keys(&mut self.torrent_keys); + let search_keys = padded_keys(&mut self.search_keys); let mut lines = vec![]; let insert_keys = |lines: &mut Vec, keys: Vec<(String, &'static str)>| { + lines.push(Line::default()); for (keycode, desc) in keys { - add_line!(lines, keycode, desc); + add_line!(lines, keycode, *desc); } lines.push(Line::default()); }; @@ -279,7 +200,7 @@ impl Component for HelpPopup { .centered(), ); - insert_keys(&mut lines, torrents_keys); + insert_keys(&mut lines, torrent_keys); lines.push( Line::from(vec![Span::styled(