Skip to content

Commit

Permalink
refactor: move help popup logic into config (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
micielski authored Aug 27, 2024
1 parent 9fe9306 commit 168aa25
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 140 deletions.
79 changes: 69 additions & 10 deletions rm-config/src/keymap/mod.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -34,22 +38,77 @@ pub struct KeymapConfig {
}

#[derive(Serialize, Deserialize, Clone)]
pub struct KeybindsHolder<T: Into<Action>> {
pub struct KeybindsHolder<T: Into<Action> + UserAction> {
pub keybindings: Vec<Keybinding<T>>,
#[serde(skip)]
pub help_repr: Vec<(String, &'static str)>,
}

impl<T: Into<Action> + Ord + UserAction> KeybindsHolder<T> {
const KEYS_DELIMITER: &'static str = ", ";

pub fn get_help_repr(&self) -> Vec<(String, &'static str)> {
let mut keys: BTreeMap<&T, Vec<String>> = 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<T> {
pub struct Keybinding<T: UserAction> {
pub on: KeyCode,
#[serde(default)]
pub modifier: KeyModifier,
pub action: T,
pub show_in_help: bool,
}

impl<T: Into<Action>> Keybinding<T> {
impl<T: Into<Action> + UserAction> Keybinding<T> {
pub fn keycode_string(&self) -> String {
let key = match self.on {
KeyCode::Backspace => "Backspace".into(),
Expand Down Expand Up @@ -95,7 +154,7 @@ impl<T: Into<Action>> Keybinding<T> {
}
}

impl<T> Keybinding<T> {
impl<T: UserAction> Keybinding<T> {
fn new(on: KeyCode, action: T, modifier: Option<KeyModifier>, show_in_help: bool) -> Self {
Self {
on,
Expand All @@ -106,7 +165,7 @@ impl<T> Keybinding<T> {
}
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for Keybinding<T> {
impl<'de, T: Deserialize<'de> + UserAction> Deserialize<'de> for Keybinding<T> {
fn deserialize<D>(deserializer: D) -> std::prelude::v1::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
Expand All @@ -124,7 +183,7 @@ impl<'de, T: Deserialize<'de>> Deserialize<'de> for Keybinding<T> {
phantom: PhantomData<T>,
}

impl<'de, T: Deserialize<'de>> Visitor<'de> for KeybindingVisitor<T> {
impl<'de, T: Deserialize<'de> + UserAction> Visitor<'de> for KeybindingVisitor<T> {
type Value = Keybinding<T>;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
Expand Down
181 changes: 51 additions & 130 deletions rm-main/src/tui/global_popups/help.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -26,11 +21,14 @@ macro_rules! add_line {
};
}

const KEYS_DELIMITER: &str = ", ";

pub struct HelpPopup {
ctx: app::Ctx,
scroll: Option<Scroll>,
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 {
Expand All @@ -51,103 +49,42 @@ impl Scroll {

impl HelpPopup {
pub fn new(ctx: app::Ctx) -> Self {
Self { ctx, scroll: None }
}

fn get_keybindings<T: Into<Action> + UserAction + Ord>(
keybindings: &[Keybinding<T>],
max_keycode_len: &mut usize,
max_line_len: &mut usize,
) -> Vec<(String, &'static str)> {
let mut keys: BTreeMap<&T, Vec<String>> = 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 {
Expand Down Expand Up @@ -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<Line>, 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());
};
Expand All @@ -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(
Expand Down

0 comments on commit 168aa25

Please sign in to comment.