Tweaked the key mappings so that it is now easier to change key mappings and update the corresponding UI elements as well

This commit is contained in:
2023-08-08 10:50:07 -06:00
parent 74011b9ab3
commit 5602fc4341
16 changed files with 469 additions and 206 deletions
+6 -2
View File
@@ -4,6 +4,7 @@ mod tests {
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::key_binding::{build_keymapping_string, SERVARR_KEYMAPPINGS};
use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::app::radarr::{ActiveRadarrBlock, RadarrData};
use crate::app::{App, Data, RadarrConfig, DEFAULT_ROUTE}; use crate::app::{App, Data, RadarrConfig, DEFAULT_ROUTE};
use crate::models::{HorizontallyScrollableText, Route, TabRoute}; use crate::models::{HorizontallyScrollableText, Route, TabRoute};
@@ -26,13 +27,16 @@ mod tests {
TabRoute { TabRoute {
title: "Radarr", title: "Radarr",
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: "<↑↓> scroll | ←→ change tab | <tab> change servarr | <q> quit ", help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_keymapping_string(&SERVARR_KEYMAPPINGS)
),
contextual_help: None, contextual_help: None,
}, },
TabRoute { TabRoute {
title: "Sonarr", title: "Sonarr",
route: Route::Sonarr, route: Route::Sonarr,
help: "<tab> change servarr | <q> quit ", help: format!("{} ", build_keymapping_string(&SERVARR_KEYMAPPINGS)),
contextual_help: None, contextual_help: None,
}, },
] ]
+49 -25
View File
@@ -1,5 +1,15 @@
use crate::event::Key; use crate::event::Key;
pub(in crate::app) type KeyMapping = (KeyBinding, &'static str);
pub fn build_keymapping_string(key_mappings: &[(KeyBinding, &str)]) -> String {
key_mappings
.iter()
.map(|(key_binding, desc)| format!("{} {}", key_binding.key, desc))
.collect::<Vec<String>>()
.join(" | ")
}
macro_rules! generate_keybindings { macro_rules! generate_keybindings {
($($field:ident),+) => { ($($field:ident),+) => {
pub struct KeyBindings { pub struct KeyBindings {
@@ -28,12 +38,14 @@ generate_keybindings! {
events, events,
home, home,
end, end,
tab,
delete, delete,
submit, submit,
quit, quit,
esc esc
} }
#[derive(Clone, Copy)]
pub struct KeyBinding { pub struct KeyBinding {
pub key: Key, pub key: Key,
pub desc: &'static str, pub desc: &'static str,
@@ -42,94 +54,106 @@ pub struct KeyBinding {
pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
add: KeyBinding { add: KeyBinding {
key: Key::Char('a'), key: Key::Char('a'),
desc: "Add", desc: "add",
}, },
up: KeyBinding { up: KeyBinding {
key: Key::Up, key: Key::Up,
desc: "Scroll up", desc: "up",
}, },
down: KeyBinding { down: KeyBinding {
key: Key::Down, key: Key::Down,
desc: "Scroll down", desc: "down",
}, },
left: KeyBinding { left: KeyBinding {
key: Key::Left, key: Key::Left,
desc: "Move left", desc: "left",
}, },
right: KeyBinding { right: KeyBinding {
key: Key::Right, key: Key::Right,
desc: "Move right", desc: "right",
}, },
backspace: KeyBinding { backspace: KeyBinding {
key: Key::Backspace, key: Key::Backspace,
desc: "Backspace", desc: "backspace",
}, },
search: KeyBinding { search: KeyBinding {
key: Key::Char('s'), key: Key::Char('s'),
desc: "Search", desc: "search",
}, },
settings: KeyBinding { settings: KeyBinding {
key: Key::Char('s'), key: Key::Char('s'),
desc: "Settings", desc: "settings",
}, },
filter: KeyBinding { filter: KeyBinding {
key: Key::Char('f'), key: Key::Char('f'),
desc: "Filter", desc: "filter",
}, },
sort: KeyBinding { sort: KeyBinding {
key: Key::Char('o'), key: Key::Char('o'),
desc: "Sort", desc: "sort",
}, },
edit: KeyBinding { edit: KeyBinding {
key: Key::Char('e'), key: Key::Char('e'),
desc: "Edit", desc: "edit",
}, },
events: KeyBinding { events: KeyBinding {
key: Key::Char('e'), key: Key::Char('e'),
desc: "Events", desc: "events",
}, },
logs: KeyBinding { logs: KeyBinding {
key: Key::Char('l'), key: Key::Char('l'),
desc: "Logs", desc: "logs",
}, },
tasks: KeyBinding { tasks: KeyBinding {
key: Key::Char('t'), key: Key::Char('t'),
desc: "Tasks", desc: "tasks",
}, },
restrictions: KeyBinding { restrictions: KeyBinding {
key: Key::Char('t'), key: Key::Char('R'),
desc: "Restrictions", desc: "restrictions",
}, },
refresh: KeyBinding { refresh: KeyBinding {
key: Key::Char('r'), key: Key::Ctrl('r'),
desc: "Refresh", desc: "refresh",
}, },
update: KeyBinding { update: KeyBinding {
key: Key::Char('u'), key: Key::Char('u'),
desc: "Update", desc: "update",
}, },
home: KeyBinding { home: KeyBinding {
key: Key::Home, key: Key::Home,
desc: "Home", desc: "home",
}, },
end: KeyBinding { end: KeyBinding {
key: Key::End, key: Key::End,
desc: "End", desc: "end",
},
tab: KeyBinding {
key: Key::Tab,
desc: "tab",
}, },
delete: KeyBinding { delete: KeyBinding {
key: Key::Delete, key: Key::Delete,
desc: "Delete selected item", desc: "delete",
}, },
submit: KeyBinding { submit: KeyBinding {
key: Key::Enter, key: Key::Enter,
desc: "Select", desc: "submit",
}, },
quit: KeyBinding { quit: KeyBinding {
key: Key::Char('q'), key: Key::Char('q'),
desc: "Quit", desc: "quit",
}, },
esc: KeyBinding { esc: KeyBinding {
key: Key::Esc, key: Key::Esc,
desc: "Exit current menu", desc: "close",
}, },
}; };
pub static SERVARR_KEYMAPPINGS: [KeyMapping; 2] = [
(DEFAULT_KEYBINDINGS.tab, "change servarr"),
(DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc),
];
pub static BARE_POPUP_KEY_MAPPINGS: [KeyMapping; 1] =
[(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)];
+18
View File
@@ -0,0 +1,18 @@
#[cfg(test)]
mod test {
use crate::app::key_binding::{build_keymapping_string, DEFAULT_KEYBINDINGS};
use pretty_assertions::assert_str_eq;
#[test]
fn test_build_keymapping_string() {
let test_keys_array = [
(DEFAULT_KEYBINDINGS.add, "add"),
(DEFAULT_KEYBINDINGS.delete, "delete"),
];
assert_str_eq!(
build_keymapping_string(&test_keys_array),
"<a> add | <del> delete"
);
}
}
+8 -2
View File
@@ -9,10 +9,13 @@ use crate::app::radarr::{ActiveRadarrBlock, RadarrData};
use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState}; use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
use crate::network::NetworkEvent; use crate::network::NetworkEvent;
use self::key_binding::{build_keymapping_string, SERVARR_KEYMAPPINGS};
#[cfg(test)] #[cfg(test)]
#[path = "app_tests.rs"] #[path = "app_tests.rs"]
mod app_tests; mod app_tests;
pub mod key_binding; pub mod key_binding;
mod key_binding_tests;
pub mod radarr; pub mod radarr;
const DEFAULT_ROUTE: Route = Route::Radarr(ActiveRadarrBlock::Movies, None); const DEFAULT_ROUTE: Route = Route::Radarr(ActiveRadarrBlock::Movies, None);
@@ -137,13 +140,16 @@ impl<'a> Default for App<'a> {
TabRoute { TabRoute {
title: "Radarr", title: "Radarr",
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: "<↑↓> scroll | ←→ change tab | <tab> change servarr | <q> quit ", help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_keymapping_string(&SERVARR_KEYMAPPINGS)
),
contextual_help: None, contextual_help: None,
}, },
TabRoute { TabRoute {
title: "Sonarr", title: "Sonarr",
route: Route::Sonarr, route: Route::Sonarr,
help: "<tab> change servarr | <q> quit ", help: format!("{} ", build_keymapping_string(&SERVARR_KEYMAPPINGS)),
contextual_help: None, contextual_help: None,
}, },
]), ]),
+31 -19
View File
@@ -15,6 +15,16 @@ use crate::models::{
}; };
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use self::radarr_key_mappings::{
COLLECTIONS_KEY_MAPPINGS, DOWNLOADS_KEY_MAPPINGS, INDEXERS_KEY_MAPPINGS, LIBRARY_KEY_MAPPINGS,
MANUAL_MOVIE_SEARCH_CONTEXTUAL_KEY_MAPPINGS, MANUAL_MOVIE_SEARCH_KEY_MAPPINGS,
MOVIE_DETAILS_KEY_MAPPINGS, ROOT_FOLDERS_KEY_MAPPINGS, SYSTEM_KEY_MAPPINGS,
};
use super::key_binding::build_keymapping_string;
pub mod radarr_key_mappings;
#[cfg(test)] #[cfg(test)]
#[path = "radarr_tests.rs"] #[path = "radarr_tests.rs"]
mod radarr_tests; mod radarr_tests;
@@ -302,76 +312,78 @@ impl<'a> Default for RadarrData<'a> {
TabRoute { TabRoute {
title: "Library", title: "Library",
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: "", help: String::new(),
contextual_help: Some("<a> add | <e> edit | <del> delete | <s> search | <f> filter | <r> refresh | <u> update all | <enter> details | <esc> cancel filter"), contextual_help: Some(build_keymapping_string(&LIBRARY_KEY_MAPPINGS)),
}, },
TabRoute { TabRoute {
title: "Downloads", title: "Downloads",
route: ActiveRadarrBlock::Downloads.into(), route: ActiveRadarrBlock::Downloads.into(),
help: "", help: String::new(),
contextual_help: Some("<r> refresh | <del> delete"), contextual_help: Some(build_keymapping_string(&DOWNLOADS_KEY_MAPPINGS)),
}, },
TabRoute { TabRoute {
title: "Collections", title: "Collections",
route: ActiveRadarrBlock::Collections.into(), route: ActiveRadarrBlock::Collections.into(),
help: "", help: String::new(),
contextual_help: Some("<s> search | <e> edit | <f> filter | <r> refresh | <u> update all | <enter> details | <esc> cancel filter"), contextual_help: Some(build_keymapping_string(&COLLECTIONS_KEY_MAPPINGS)),
}, },
TabRoute { TabRoute {
title: "Root Folders", title: "Root Folders",
route: ActiveRadarrBlock::RootFolders.into(), route: ActiveRadarrBlock::RootFolders.into(),
help: "", help: String::new(),
contextual_help: Some("<a> add | <del> delete | <r> refresh"), contextual_help: Some(build_keymapping_string(&ROOT_FOLDERS_KEY_MAPPINGS)),
}, },
TabRoute { TabRoute {
title: "Indexers", title: "Indexers",
route: ActiveRadarrBlock::Indexers.into(), route: ActiveRadarrBlock::Indexers.into(),
help: "", help: String::new(),
contextual_help: Some("<a> add | <enter> edit | <s> settings | <t> restrictions | <del> delete | <r> refresh"), contextual_help: Some(build_keymapping_string(&INDEXERS_KEY_MAPPINGS)),
}, },
TabRoute { TabRoute {
title: "System", title: "System",
route: ActiveRadarrBlock::System.into(), route: ActiveRadarrBlock::System.into(),
help: "", help: String::new(),
contextual_help: Some("<t> open tasks | <e> open events | <l> open logs | <u> open updates | <r> refresh"), contextual_help: Some(build_keymapping_string(&SYSTEM_KEY_MAPPINGS)),
}, },
]), ]),
movie_info_tabs: TabState::new(vec![ movie_info_tabs: TabState::new(vec![
TabRoute { TabRoute {
title: "Details", title: "Details",
route: ActiveRadarrBlock::MovieDetails.into(), route: ActiveRadarrBlock::MovieDetails.into(),
help: "<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close", help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS),
contextual_help: None, contextual_help: None,
}, },
TabRoute { TabRoute {
title: "History", title: "History",
route: ActiveRadarrBlock::MovieHistory.into(), route: ActiveRadarrBlock::MovieHistory.into(),
help: "<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close", help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS),
contextual_help: None, contextual_help: None,
}, },
TabRoute { TabRoute {
title: "File", title: "File",
route: ActiveRadarrBlock::FileInfo.into(), route: ActiveRadarrBlock::FileInfo.into(),
help: "<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close", help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS),
contextual_help: None, contextual_help: None,
}, },
TabRoute { TabRoute {
title: "Cast", title: "Cast",
route: ActiveRadarrBlock::Cast.into(), route: ActiveRadarrBlock::Cast.into(),
help: "<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close", help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS),
contextual_help: None, contextual_help: None,
}, },
TabRoute { TabRoute {
title: "Crew", title: "Crew",
route: ActiveRadarrBlock::Crew.into(), route: ActiveRadarrBlock::Crew.into(),
help: "<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close", help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS),
contextual_help: None, contextual_help: None,
}, },
TabRoute { TabRoute {
title: "Manual Search", title: "Manual Search",
route: ActiveRadarrBlock::ManualSearch.into(), route: ActiveRadarrBlock::ManualSearch.into(),
help: "<r> refresh | <u> update | <e> edit | <o> sort | <s> auto search | <esc> close", help: build_keymapping_string(&MANUAL_MOVIE_SEARCH_KEY_MAPPINGS),
contextual_help: Some("<enter> details"), contextual_help: Some(build_keymapping_string(
&MANUAL_MOVIE_SEARCH_CONTEXTUAL_KEY_MAPPINGS,
)),
}, },
]), ]),
} }
+116
View File
@@ -0,0 +1,116 @@
use crate::app::key_binding::{KeyMapping, DEFAULT_KEYBINDINGS};
pub static LIBRARY_KEY_MAPPINGS: [KeyMapping; 9] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.update, "update all"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static DOWNLOADS_KEY_MAPPINGS: [KeyMapping; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
];
pub static COLLECTIONS_KEY_MAPPINGS: [KeyMapping; 7] = [
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.update, "update all"),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static ROOT_FOLDERS_KEY_MAPPINGS: [KeyMapping; 3] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static INDEXERS_KEY_MAPPINGS: [KeyMapping; 6] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(
DEFAULT_KEYBINDINGS.settings,
DEFAULT_KEYBINDINGS.settings.desc,
),
(
DEFAULT_KEYBINDINGS.restrictions,
DEFAULT_KEYBINDINGS.restrictions.desc,
),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static SYSTEM_KEY_MAPPINGS: [KeyMapping; 5] = [
(DEFAULT_KEYBINDINGS.tasks, "open tasks"),
(DEFAULT_KEYBINDINGS.events, "open events"),
(DEFAULT_KEYBINDINGS.logs, "open logs"),
(DEFAULT_KEYBINDINGS.update, "open updates"),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static MOVIE_DETAILS_KEY_MAPPINGS: [KeyMapping; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.search, "auto search"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static MANUAL_MOVIE_SEARCH_KEY_MAPPINGS: [KeyMapping; 6] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, "auto search"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static MANUAL_MOVIE_SEARCH_CONTEXTUAL_KEY_MAPPINGS: [KeyMapping; 1] =
[(DEFAULT_KEYBINDINGS.submit, "details")];
pub static ADD_MOVIE_SEARCH_RESULTS_KEY_MAPPINGS: [KeyMapping; 2] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static SYSTEM_TASKS_KEY_MAPPINGS: [KeyMapping; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static COLLECTION_DETAILS_KEY_MAPPINGS: [KeyMapping; 2] = [
(DEFAULT_KEYBINDINGS.submit, "show overview/add movie"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
@@ -8,6 +8,13 @@ mod tests {
use serde_json::Number; use serde_json::Number;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::build_keymapping_string;
use crate::app::radarr::radarr_key_mappings::{
COLLECTIONS_KEY_MAPPINGS, DOWNLOADS_KEY_MAPPINGS, INDEXERS_KEY_MAPPINGS,
LIBRARY_KEY_MAPPINGS, MANUAL_MOVIE_SEARCH_CONTEXTUAL_KEY_MAPPINGS,
MANUAL_MOVIE_SEARCH_KEY_MAPPINGS, MOVIE_DETAILS_KEY_MAPPINGS, ROOT_FOLDERS_KEY_MAPPINGS,
SYSTEM_KEY_MAPPINGS,
};
use crate::app::radarr::radarr_test_utils::utils::create_test_radarr_data; use crate::app::radarr::radarr_test_utils::utils::create_test_radarr_data;
use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::app::radarr::{ActiveRadarrBlock, RadarrData};
use crate::models::radarr_models::{ use crate::models::radarr_models::{
@@ -316,8 +323,10 @@ mod tests {
ActiveRadarrBlock::Movies.into() ActiveRadarrBlock::Movies.into()
); );
assert!(radarr_data.main_tabs.tabs[0].help.is_empty()); assert!(radarr_data.main_tabs.tabs[0].help.is_empty());
assert_eq!(radarr_data.main_tabs.tabs[0].contextual_help, assert_eq!(
Some("<a> add | <e> edit | <del> delete | <s> search | <f> filter | <r> refresh | <u> update all | <enter> details | <esc> cancel filter")); radarr_data.main_tabs.tabs[0].contextual_help,
Some(build_keymapping_string(&LIBRARY_KEY_MAPPINGS))
);
assert_str_eq!(radarr_data.main_tabs.tabs[1].title, "Downloads"); assert_str_eq!(radarr_data.main_tabs.tabs[1].title, "Downloads");
assert_eq!( assert_eq!(
@@ -327,7 +336,7 @@ mod tests {
assert!(radarr_data.main_tabs.tabs[1].help.is_empty()); assert!(radarr_data.main_tabs.tabs[1].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[1].contextual_help, radarr_data.main_tabs.tabs[1].contextual_help,
Some("<r> refresh | <del> delete") Some(build_keymapping_string(&DOWNLOADS_KEY_MAPPINGS))
); );
assert_str_eq!(radarr_data.main_tabs.tabs[2].title, "Collections"); assert_str_eq!(radarr_data.main_tabs.tabs[2].title, "Collections");
@@ -336,8 +345,10 @@ mod tests {
ActiveRadarrBlock::Collections.into() ActiveRadarrBlock::Collections.into()
); );
assert!(radarr_data.main_tabs.tabs[2].help.is_empty()); assert!(radarr_data.main_tabs.tabs[2].help.is_empty());
assert_eq!(radarr_data.main_tabs.tabs[2].contextual_help, assert_eq!(
Some("<s> search | <e> edit | <f> filter | <r> refresh | <u> update all | <enter> details | <esc> cancel filter")); radarr_data.main_tabs.tabs[2].contextual_help,
Some(build_keymapping_string(&COLLECTIONS_KEY_MAPPINGS))
);
assert_str_eq!(radarr_data.main_tabs.tabs[3].title, "Root Folders"); assert_str_eq!(radarr_data.main_tabs.tabs[3].title, "Root Folders");
assert_eq!( assert_eq!(
@@ -347,7 +358,7 @@ mod tests {
assert!(radarr_data.main_tabs.tabs[3].help.is_empty()); assert!(radarr_data.main_tabs.tabs[3].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[3].contextual_help, radarr_data.main_tabs.tabs[3].contextual_help,
Some("<a> add | <del> delete | <r> refresh") Some(build_keymapping_string(&ROOT_FOLDERS_KEY_MAPPINGS))
); );
assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Indexers"); assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Indexers");
@@ -358,9 +369,7 @@ mod tests {
assert!(radarr_data.main_tabs.tabs[4].help.is_empty()); assert!(radarr_data.main_tabs.tabs[4].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[4].contextual_help, radarr_data.main_tabs.tabs[4].contextual_help,
Some( Some(build_keymapping_string(&INDEXERS_KEY_MAPPINGS))
"<a> add | <enter> edit | <s> settings | <t> restrictions | <del> delete | <r> refresh"
)
); );
assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System"); assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System");
@@ -371,7 +380,7 @@ mod tests {
assert!(radarr_data.main_tabs.tabs[5].help.is_empty()); assert!(radarr_data.main_tabs.tabs[5].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[5].contextual_help, radarr_data.main_tabs.tabs[5].contextual_help,
Some("<t> open tasks | <e> open events | <l> open logs | <u> open updates | <r> refresh") Some(build_keymapping_string(&SYSTEM_KEY_MAPPINGS))
); );
assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6); assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6);
@@ -383,7 +392,7 @@ mod tests {
); );
assert_str_eq!( assert_str_eq!(
radarr_data.movie_info_tabs.tabs[0].help, radarr_data.movie_info_tabs.tabs[0].help,
"<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close" build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS)
); );
assert!(radarr_data.movie_info_tabs.tabs[0] assert!(radarr_data.movie_info_tabs.tabs[0]
.contextual_help .contextual_help
@@ -396,7 +405,7 @@ mod tests {
); );
assert_str_eq!( assert_str_eq!(
radarr_data.movie_info_tabs.tabs[1].help, radarr_data.movie_info_tabs.tabs[1].help,
"<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close" build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS)
); );
assert!(radarr_data.movie_info_tabs.tabs[1] assert!(radarr_data.movie_info_tabs.tabs[1]
.contextual_help .contextual_help
@@ -409,7 +418,7 @@ mod tests {
); );
assert_str_eq!( assert_str_eq!(
radarr_data.movie_info_tabs.tabs[2].help, radarr_data.movie_info_tabs.tabs[2].help,
"<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close" build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS)
); );
assert!(radarr_data.movie_info_tabs.tabs[2] assert!(radarr_data.movie_info_tabs.tabs[2]
.contextual_help .contextual_help
@@ -422,7 +431,7 @@ mod tests {
); );
assert_str_eq!( assert_str_eq!(
radarr_data.movie_info_tabs.tabs[3].help, radarr_data.movie_info_tabs.tabs[3].help,
"<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close" build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS)
); );
assert!(radarr_data.movie_info_tabs.tabs[3] assert!(radarr_data.movie_info_tabs.tabs[3]
.contextual_help .contextual_help
@@ -435,7 +444,7 @@ mod tests {
); );
assert_str_eq!( assert_str_eq!(
radarr_data.movie_info_tabs.tabs[4].help, radarr_data.movie_info_tabs.tabs[4].help,
"<r> refresh | <u> update | <e> edit | <s> auto search | <esc> close" build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS)
); );
assert!(radarr_data.movie_info_tabs.tabs[4] assert!(radarr_data.movie_info_tabs.tabs[4]
.contextual_help .contextual_help
@@ -448,11 +457,13 @@ mod tests {
); );
assert_str_eq!( assert_str_eq!(
radarr_data.movie_info_tabs.tabs[5].help, radarr_data.movie_info_tabs.tabs[5].help,
"<r> refresh | <u> update | <e> edit | <o> sort | <s> auto search | <esc> close" build_keymapping_string(&MANUAL_MOVIE_SEARCH_KEY_MAPPINGS)
); );
assert_eq!( assert_eq!(
radarr_data.movie_info_tabs.tabs[5].contextual_help, radarr_data.movie_info_tabs.tabs[5].contextual_help,
Some("<enter> details") Some(build_keymapping_string(
&MANUAL_MOVIE_SEARCH_CONTEXTUAL_KEY_MAPPINGS
))
); );
} }
} }
+24 -2
View File
@@ -1,13 +1,13 @@
use std::fmt; use std::fmt;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[cfg(test)] #[cfg(test)]
#[path = "key_tests.rs"] #[path = "key_tests.rs"]
mod key_tests; mod key_tests;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Key { pub enum Key {
Up, Up,
Down, Down,
@@ -18,7 +18,9 @@ pub enum Key {
Backspace, Backspace,
Home, Home,
End, End,
Tab,
Delete, Delete,
Ctrl(char),
Char(char), Char(char),
Unknown, Unknown,
} }
@@ -27,6 +29,18 @@ impl Display for Key {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match *self {
Key::Char(c) => write!(f, "<{}>", c), Key::Char(c) => write!(f, "<{}>", c),
Key::Ctrl(c) => write!(f, "<Ctrl-{}>", c),
Key::Up => write!(f, "<↑>"),
Key::Down => write!(f, "<↓>"),
Key::Left => write!(f, "<←>"),
Key::Right => write!(f, "<→>"),
Key::Enter => write!(f, "<enter>"),
Key::Esc => write!(f, "<esc>"),
Key::Backspace => write!(f, "<backspace>"),
Key::Home => write!(f, "<home>"),
Key::End => write!(f, "<end>"),
Key::Tab => write!(f, "<tab>"),
Key::Delete => write!(f, "<del>"),
_ => write!(f, "<{:?}>", self), _ => write!(f, "<{:?}>", self),
} }
} }
@@ -61,6 +75,9 @@ impl From<KeyEvent> for Key {
KeyEvent { KeyEvent {
code: KeyCode::End, .. code: KeyCode::End, ..
} => Key::End, } => Key::End,
KeyEvent {
code: KeyCode::Tab, ..
} => Key::Tab,
KeyEvent { KeyEvent {
code: KeyCode::Delete, code: KeyCode::Delete,
.. ..
@@ -72,6 +89,11 @@ impl From<KeyEvent> for Key {
KeyEvent { KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
} => Key::Esc, } => Key::Esc,
KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::CONTROL,
..
} => Key::Ctrl(c),
KeyEvent { KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char(c),
.. ..
+36 -9
View File
@@ -1,18 +1,27 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use crate::event::key::Key; use crate::event::key::Key;
#[test] #[rstest]
fn test_key_formatter() { #[case(Key::Up, "")]
assert_str_eq!(format!("{}", Key::Esc), "<Esc>"); #[case(Key::Down, "")]
} #[case(Key::Left, "")]
#[case(Key::Right, "")]
#[test] #[case(Key::Enter, "enter")]
fn test_key_formatter_char() { #[case(Key::Esc, "esc")]
assert_str_eq!(format!("{}", Key::Char('q')), "<q>"); #[case(Key::Backspace, "backspace")]
#[case(Key::Home, "home")]
#[case(Key::End, "end")]
#[case(Key::Tab, "tab")]
#[case(Key::Delete, "del")]
#[case(Key::Char('q'), "q")]
#[case(Key::Ctrl('q'), "Ctrl-q")]
fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) {
assert_str_eq!(format!("{}", key), format!("<{}>", expected_str));
} }
#[test] #[test]
@@ -53,6 +62,11 @@ mod tests {
assert_eq!(Key::from(KeyEvent::from(KeyCode::End)), Key::End); assert_eq!(Key::from(KeyEvent::from(KeyCode::End)), Key::End);
} }
#[test]
fn test_key_from_tab() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Tab)), Key::Tab);
}
#[test] #[test]
fn test_key_from_delete() { fn test_key_from_delete() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Delete)), Key::Delete); assert_eq!(Key::from(KeyEvent::from(KeyCode::Delete)), Key::Delete);
@@ -76,6 +90,19 @@ mod tests {
) )
} }
#[test]
fn test_key_from_ctrl() {
assert_eq!(
Key::from(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}),
Key::Ctrl('c')
);
}
#[test] #[test]
fn test_key_from_unknown() { fn test_key_from_unknown() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Pause)), Key::Unknown); assert_eq!(Key::from(KeyEvent::from(KeyCode::Pause)), Key::Unknown);
+7 -7
View File
@@ -284,12 +284,12 @@ impl HorizontallyScrollableText {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct TabRoute { pub struct TabRoute {
pub title: &'static str, pub title: &'static str,
pub route: Route, pub route: Route,
pub help: &'static str, pub help: String,
pub contextual_help: Option<&'static str>, pub contextual_help: Option<String>,
} }
pub struct TabState { pub struct TabState {
@@ -313,12 +313,12 @@ impl TabState {
&self.tabs[self.index].route &self.tabs[self.index].route
} }
pub fn get_active_tab_help(&self) -> &'static str { pub fn get_active_tab_help(&self) -> &str {
self.tabs[self.index].help &self.tabs[self.index].help
} }
pub fn get_active_tab_contextual_help(&self) -> Option<&'static str> { pub fn get_active_tab_contextual_help(&self) -> Option<String> {
self.tabs[self.index].contextual_help self.tabs[self.index].contextual_help.clone()
} }
pub fn next(&mut self) { pub fn next(&mut self) {
+6 -6
View File
@@ -533,7 +533,7 @@ mod tests {
#[test] #[test]
fn test_tab_state_get_active_tab_help() { fn test_tab_state_get_active_tab_help() {
let tabs = create_test_tab_routes(); let tabs = create_test_tab_routes();
let second_tab_help = tabs[1].help; let second_tab_help = tabs[1].help.clone();
let tab_state = TabState { tabs, index: 1 }; let tab_state = TabState { tabs, index: 1 };
let tab_help = tab_state.get_active_tab_help(); let tab_help = tab_state.get_active_tab_help();
@@ -544,7 +544,7 @@ mod tests {
#[test] #[test]
fn test_tab_state_get_active_tab_contextual_help() { fn test_tab_state_get_active_tab_contextual_help() {
let tabs = create_test_tab_routes(); let tabs = create_test_tab_routes();
let second_tab_contextual_help = tabs[1].contextual_help.unwrap(); let second_tab_contextual_help = tabs[1].contextual_help.clone().unwrap();
let tab_state = TabState { tabs, index: 1 }; let tab_state = TabState { tabs, index: 1 };
let tab_contextual_help = tab_state.get_active_tab_contextual_help(); let tab_contextual_help = tab_state.get_active_tab_contextual_help();
@@ -648,14 +648,14 @@ mod tests {
TabRoute { TabRoute {
title: "Test 1", title: "Test 1",
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: "Help for Test 1", help: "Help for Test 1".to_owned(),
contextual_help: Some("Contextual Help for Test 1"), contextual_help: Some("Contextual Help for Test 1".to_owned()),
}, },
TabRoute { TabRoute {
title: "Test 2", title: "Test 2",
route: ActiveRadarrBlock::Collections.into(), route: ActiveRadarrBlock::Collections.into(),
help: "Help for Test 2", help: "Help for Test 2".to_owned(),
contextual_help: Some("Contextual Help for Test 2"), contextual_help: Some("Contextual Help for Test 2".to_owned()),
}, },
] ]
} }
+3 -3
View File
@@ -294,7 +294,7 @@ pub struct TableProps<'a, T> {
pub content: &'a mut StatefulTable<T>, pub content: &'a mut StatefulTable<T>,
pub table_headers: Vec<&'a str>, pub table_headers: Vec<&'a str>,
pub constraints: Vec<Constraint>, pub constraints: Vec<Constraint>,
pub help: Option<&'static str>, pub help: Option<String>,
} }
pub struct ListProps<'a, T> { pub struct ListProps<'a, T> {
@@ -302,7 +302,7 @@ pub struct ListProps<'a, T> {
pub title: &'static str, pub title: &'static str,
pub is_loading: bool, pub is_loading: bool,
pub is_popup: bool, pub is_popup: bool,
pub help: Option<&'static str>, pub help: Option<String>,
} }
fn draw_table<'a, B, T, F>( fn draw_table<'a, B, T, F>(
@@ -618,7 +618,7 @@ pub fn draw_list_box<'a, B: Backend, T>(
fn draw_help_and_get_content_rect<B: Backend>( fn draw_help_and_get_content_rect<B: Backend>(
f: &mut Frame<'_, B>, f: &mut Frame<'_, B>,
area: Rect, area: Rect,
help: Option<&str>, help: Option<String>,
) -> Rect { ) -> Rect {
if let Some(help_string) = help { if let Some(help_string) = help {
let chunks = let chunks =
@@ -4,6 +4,8 @@ use tui::text::Text;
use tui::widgets::{Cell, Paragraph, Row, Wrap}; use tui::widgets::{Cell, Paragraph, Row, Wrap};
use tui::Frame; use tui::Frame;
use crate::app::key_binding::{build_keymapping_string, BARE_POPUP_KEY_MAPPINGS};
use crate::app::radarr::radarr_key_mappings::COLLECTION_DETAILS_KEY_MAPPINGS;
use crate::app::radarr::{ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS}; use crate::app::radarr::{ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS};
use crate::app::App; use crate::app::App;
use crate::models::radarr_models::CollectionMovie; use crate::models::radarr_models::CollectionMovie;
@@ -102,8 +104,10 @@ pub fn draw_collection_details<B: Backend>(
.current_selection() .current_selection()
.clone() .clone()
}; };
let mut help_text = let mut help_text = Text::from(format!(
Text::from("<↑↓> scroll table | <enter> show overview/add movie | <esc> close"); "<↑↓> scroll table | {}",
build_keymapping_string(&COLLECTION_DETAILS_KEY_MAPPINGS)
));
help_text.patch_style(style_help()); help_text.patch_style(style_help());
let monitored = if collection_selection.monitored { let monitored = if collection_selection.monitored {
"Yes" "Yes"
@@ -257,7 +261,7 @@ fn draw_movie_overview<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, cont
.overview, .overview,
); );
overview.patch_style(style_default()); overview.patch_style(style_default());
let mut help_text = Text::from("<esc> close"); let mut help_text = Text::from(build_keymapping_string(&BARE_POPUP_KEY_MAPPINGS));
help_text.patch_style(style_help()); help_text.patch_style(style_help());
let paragraph = Paragraph::new(overview) let paragraph = Paragraph::new(overview)
+6 -2
View File
@@ -4,6 +4,8 @@ use tui::text::Text;
use tui::widgets::{Cell, ListItem, Paragraph, Row}; use tui::widgets::{Cell, ListItem, Paragraph, Row};
use tui::Frame; use tui::Frame;
use crate::app::key_binding::{build_keymapping_string, BARE_POPUP_KEY_MAPPINGS};
use crate::app::radarr::radarr_key_mappings::ADD_MOVIE_SEARCH_RESULTS_KEY_MAPPINGS;
use crate::app::radarr::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS}; use crate::app::radarr::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
use crate::models::radarr_models::AddMovieSearchResult; use crate::models::radarr_models::AddMovieSearchResult;
use crate::models::Route; use crate::models::Route;
@@ -142,7 +144,7 @@ fn draw_add_movie_search<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, ar
); );
f.render_widget(layout_block(), chunks[1]); f.render_widget(layout_block(), chunks[1]);
let mut help_text = Text::from("<esc> close"); let mut help_text = Text::from(build_keymapping_string(&BARE_POPUP_KEY_MAPPINGS));
help_text.patch_style(style_help()); help_text.patch_style(style_help());
let help_paragraph = Paragraph::new(help_text) let help_paragraph = Paragraph::new(help_text)
.block(borderless_block()) .block(borderless_block())
@@ -161,7 +163,9 @@ fn draw_add_movie_search<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, ar
| ActiveRadarrBlock::AddMovieSelectRootFolder | ActiveRadarrBlock::AddMovieSelectRootFolder
| ActiveRadarrBlock::AddMovieAlreadyInLibrary | ActiveRadarrBlock::AddMovieAlreadyInLibrary
| ActiveRadarrBlock::AddMovieTagsInput => { | ActiveRadarrBlock::AddMovieTagsInput => {
let mut help_text = Text::from("<enter> details | <esc> edit search"); let mut help_text = Text::from(build_keymapping_string(
&ADD_MOVIE_SEARCH_RESULTS_KEY_MAPPINGS,
));
help_text.patch_style(style_help()); help_text.patch_style(style_help());
let help_paragraph = Paragraph::new(help_text) let help_paragraph = Paragraph::new(help_text)
.block(borderless_block()) .block(borderless_block())
+19 -4
View File
@@ -4,6 +4,8 @@ use tui::text::{Span, Text};
use tui::widgets::{Cell, ListItem, Paragraph, Row}; use tui::widgets::{Cell, ListItem, Paragraph, Row};
use tui::Frame; use tui::Frame;
use crate::app::key_binding::{build_keymapping_string, BARE_POPUP_KEY_MAPPINGS};
use crate::app::radarr::radarr_key_mappings::SYSTEM_TASKS_KEY_MAPPINGS;
use crate::app::radarr::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::app::radarr::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::app::App; use crate::app::App;
use crate::models::Route; use crate::models::Route;
@@ -84,7 +86,10 @@ fn draw_logs_popup<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Re
title: "Log Details", title: "Log Details",
is_loading: app.is_loading, is_loading: app.is_loading,
is_popup: true, is_popup: true,
help: Some("<↑↓←→> scroll | <esc> close"), help: Some(format!(
"<↑↓←→> scroll | {}",
build_keymapping_string(&BARE_POPUP_KEY_MAPPINGS)
)),
}, },
); );
} }
@@ -93,8 +98,11 @@ fn draw_tasks_popup<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, area: R
let tasks_popup_table = |f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect| { let tasks_popup_table = |f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect| {
f.render_widget(title_block("Tasks"), area); f.render_widget(title_block("Tasks"), area);
let context_area = let context_area = draw_help_and_get_content_rect(
draw_help_and_get_content_rect(f, area, Some("<enter> start task | <esc> close")); f,
area,
Some(build_keymapping_string(&SYSTEM_TASKS_KEY_MAPPINGS)),
);
draw_table( draw_table(
f, f,
@@ -150,7 +158,14 @@ fn draw_start_task_prompt<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, p
fn draw_updates_popup<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { fn draw_updates_popup<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) {
f.render_widget(title_block("Updates"), area); f.render_widget(title_block("Updates"), area);
let content_rect = draw_help_and_get_content_rect(f, area, Some("<↑↓> scroll | <esc> close")); let content_rect = draw_help_and_get_content_rect(
f,
area,
Some(format!(
"<↑↓> scroll | {}",
build_keymapping_string(&BARE_POPUP_KEY_MAPPINGS)
)),
);
let updates = app.data.radarr_data.updates.get_text(); let updates = app.data.radarr_data.updates.get_text();
let block = borderless_block(); let block = borderless_block();