feat: Refactor all keybinding tips into a dynamically changing menu that can be invoked via '?' [#32]
This commit is contained in:
@@ -6,13 +6,20 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::context_clues::SERVARR_CONTEXT_CLUES;
|
||||
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
|
||||
use crate::app::radarr::radarr_context_clues::{
|
||||
LIBRARY_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::handle_events;
|
||||
use crate::handlers::{handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::handlers::{handle_events, populate_keymapping_table};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::Route;
|
||||
|
||||
@@ -82,6 +89,82 @@ mod tests {
|
||||
assert!(app.cancellation_token.is_cancelled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_populate_keybindings_table_on_help_button_press() {
|
||||
let mut app = App::test_default();
|
||||
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
|
||||
.collect::<Vec<_>>();
|
||||
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
|
||||
|
||||
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
|
||||
|
||||
assert!(app.keymapping_table.is_some());
|
||||
assert_eq!(
|
||||
expected_keybinding_items,
|
||||
app.keymapping_table.unwrap().items
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_ignore_help_button_when_ignore_special_keys_for_textbox_input_is_true() {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = true;
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
|
||||
|
||||
assert!(app.keymapping_table.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_empties_keybindings_table_on_help_button_press_when_keybindings_table_is_already_populated(
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
|
||||
.collect::<Vec<_>>();
|
||||
let mut stateful_table = StatefulTable::default();
|
||||
stateful_table.set_items(keybinding_items);
|
||||
app.keymapping_table = Some(stateful_table);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
|
||||
|
||||
assert!(app.keymapping_table.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_shows_keymapping_popup_when_keymapping_table_is_populated() {
|
||||
let mut app = App::test_default();
|
||||
let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
|
||||
.collect::<Vec<_>>();
|
||||
let mut stateful_table = StatefulTable::default();
|
||||
stateful_table.set_items(keybinding_items);
|
||||
app.keymapping_table = Some(stateful_table);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
let expected_selection = KeybindingItem {
|
||||
key: SERVARR_CONTEXT_CLUES[1].0.key.to_string(),
|
||||
alt_key: SERVARR_CONTEXT_CLUES[1]
|
||||
.0
|
||||
.alt
|
||||
.map_or(String::new(), |k| k.to_string()),
|
||||
desc: SERVARR_CONTEXT_CLUES[1].1.to_string(),
|
||||
};
|
||||
|
||||
handle_events(DEFAULT_KEYBINDINGS.down.key, &mut app);
|
||||
|
||||
assert!(app.keymapping_table.is_some());
|
||||
assert_eq!(
|
||||
&expected_selection,
|
||||
app.keymapping_table.unwrap().current_selection()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
@@ -113,4 +196,95 @@ mod tests {
|
||||
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_populate_keymapping_table_global_options() {
|
||||
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
|
||||
.iter()
|
||||
.map(|(key, desc)| {
|
||||
let (key, alt_key) = if key.alt.is_some() {
|
||||
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
|
||||
} else {
|
||||
(key.key.to_string(), String::new())
|
||||
};
|
||||
KeybindingItem {
|
||||
key,
|
||||
alt_key,
|
||||
desc: desc.to_string(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
|
||||
|
||||
populate_keymapping_table(&mut app);
|
||||
|
||||
assert!(app.keymapping_table.is_some());
|
||||
assert_eq!(
|
||||
expected_keybinding_items,
|
||||
app.keymapping_table.unwrap().items
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_populate_keymapping_table_populates_servarr_specific_tab_info_before_global_options() {
|
||||
let mut expected_keybinding_items = LIBRARY_CONTEXT_CLUES
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
|
||||
.collect::<Vec<_>>();
|
||||
expected_keybinding_items.extend(
|
||||
SERVARR_CONTEXT_CLUES
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
|
||||
);
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data = RadarrData::default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
populate_keymapping_table(&mut app);
|
||||
|
||||
assert!(app.keymapping_table.is_some());
|
||||
assert_eq!(
|
||||
expected_keybinding_items,
|
||||
app.keymapping_table.unwrap().items
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_populate_keymapping_table_populates_delegated_servarr_context_provider_options_before_global_options(
|
||||
) {
|
||||
let mut expected_keybinding_items = MOVIE_DETAILS_CONTEXT_CLUES
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
|
||||
.collect::<Vec<_>>();
|
||||
expected_keybinding_items.extend(
|
||||
SERVARR_CONTEXT_CLUES
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
|
||||
);
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data = RadarrData::default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
|
||||
|
||||
populate_keymapping_table(&mut app);
|
||||
|
||||
assert!(app.keymapping_table.is_some());
|
||||
assert_eq!(
|
||||
expected_keybinding_items,
|
||||
app.keymapping_table.unwrap().items
|
||||
);
|
||||
}
|
||||
|
||||
fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem {
|
||||
let (key, alt_key) = if key.alt.is_some() {
|
||||
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
|
||||
} else {
|
||||
(key.key.to_string(), String::new())
|
||||
};
|
||||
KeybindingItem {
|
||||
key,
|
||||
alt_key,
|
||||
desc: desc.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handle_table_events;
|
||||
use crate::handlers::table_handler::TableHandlingConfig;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "keybinding_handler_tests.rs"]
|
||||
mod keybinding_handler_tests;
|
||||
|
||||
pub(super) struct KeybindingHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
}
|
||||
|
||||
impl KeybindingHandler<'_, '_> {
|
||||
handle_table_events!(
|
||||
self,
|
||||
keybindings,
|
||||
self.app.keymapping_table.as_mut().unwrap(),
|
||||
KeybindingItem
|
||||
);
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let keybinding_table_handling_config = TableHandlingConfig::new(self.app.get_current_route());
|
||||
|
||||
if !self.handle_keybindings_table_events(keybinding_table_handling_config) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(_active_block: ActiveKeybindingBlock) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
_active_block: ActiveKeybindingBlock,
|
||||
_context: Option<ActiveKeybindingBlock>,
|
||||
) -> KeybindingHandler<'a, 'b> {
|
||||
KeybindingHandler { key, app }
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.app.keymapping_table.is_some()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {}
|
||||
|
||||
fn handle_submit(&mut self) {}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
self.app.keymapping_table = None;
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::KeybindingHandler;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use rstest::rstest;
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[test]
|
||||
fn test_esc_empties_keymapping_table() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
|
||||
KeybindingHandler::new(ESC_KEY, &mut app, ActiveKeybindingBlock::Help, None).handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
||||
assert!(app.keymapping_table.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keybinding_handler_accepts() {
|
||||
assert!(KeybindingHandler::accepts(ActiveKeybindingBlock::Help));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keybinding_handler_not_ready_when_keybinding_is_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = KeybindingHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveKeybindingBlock::Help,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_keybinding_handler_ready_when_keymapping_table_is_not_empty(
|
||||
#[values(true, false)] is_loading: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.keymapping_table = Some(StatefulTable::default());
|
||||
app.is_loading = is_loading;
|
||||
|
||||
let handler = KeybindingHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveKeybindingBlock::Help,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
use radarr_handlers::RadarrHandler;
|
||||
use sonarr_handlers::SonarrHandler;
|
||||
|
||||
use crate::app::context_clues::{
|
||||
ContextClueProvider, ServarrContextClueProvider, SERVARR_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::KeyBinding;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::keybinding_handler::KeybindingHandler;
|
||||
use crate::matches_key;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{HorizontallyScrollableText, Route};
|
||||
|
||||
mod keybinding_handler;
|
||||
mod radarr_handlers;
|
||||
mod sonarr_handlers;
|
||||
|
||||
@@ -97,8 +106,17 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
||||
app.server_tabs.previous();
|
||||
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
|
||||
app.cancellation_token.cancel();
|
||||
} else if matches_key!(help, key) && !app.ignore_special_keys_for_textbox_input {
|
||||
if app.keymapping_table.is_none() {
|
||||
populate_keymapping_table(app);
|
||||
} else {
|
||||
app.keymapping_table = None;
|
||||
}
|
||||
} else {
|
||||
match app.get_current_route() {
|
||||
_ if app.keymapping_table.is_some() => {
|
||||
KeybindingHandler::new(key, app, ActiveKeybindingBlock::Help, None).handle();
|
||||
}
|
||||
Route::Radarr(active_radarr_block, context) => {
|
||||
RadarrHandler::new(key, app, active_radarr_block, context).handle()
|
||||
}
|
||||
@@ -110,6 +128,48 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_keymapping_table(app: &mut App<'_>) {
|
||||
let context_clue_to_keybinding_item = |key: &KeyBinding, desc: &&str| {
|
||||
let (key, alt_key) = if key.alt.is_some() {
|
||||
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
|
||||
} else {
|
||||
(key.key.to_string(), String::new())
|
||||
};
|
||||
KeybindingItem {
|
||||
key,
|
||||
alt_key,
|
||||
desc: desc.to_string(),
|
||||
}
|
||||
};
|
||||
let mut keybindings = Vec::new();
|
||||
let global_keybindings = Vec::from(SERVARR_CONTEXT_CLUES)
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(contextual_help) = app.server_tabs.get_active_route_contextual_help() {
|
||||
keybindings.extend(
|
||||
contextual_help
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(contextual_help) = ServarrContextClueProvider::get_context_clues(app) {
|
||||
keybindings.extend(
|
||||
contextual_help
|
||||
.iter()
|
||||
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
|
||||
);
|
||||
}
|
||||
|
||||
keybindings.extend(global_keybindings);
|
||||
|
||||
let mut table = StatefulTable::default();
|
||||
table.set_items(keybindings);
|
||||
app.keymapping_table = Some(table);
|
||||
}
|
||||
|
||||
fn handle_clear_errors(app: &mut App<'_>) {
|
||||
if !app.error.text.is_empty() {
|
||||
app.error = HorizontallyScrollableText::default();
|
||||
|
||||
Reference in New Issue
Block a user