diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index d015d0c..e93ca6e 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -4,6 +4,7 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use tokio::sync::mpsc; + use crate::app::key_binding::{build_keymapping_string, SERVARR_KEYMAPPINGS}; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::app::{App, Data, RadarrConfig, DEFAULT_ROUTE}; use crate::models::{HorizontallyScrollableText, Route, TabRoute}; @@ -26,13 +27,16 @@ mod tests { TabRoute { title: "Radarr", route: ActiveRadarrBlock::Movies.into(), - help: "<↑↓> scroll | ←→ change tab | change servarr | quit ", + help: format!( + "<↑↓> scroll | ←→ change tab | {} ", + build_keymapping_string(&SERVARR_KEYMAPPINGS) + ), contextual_help: None, }, TabRoute { title: "Sonarr", route: Route::Sonarr, - help: " change servarr | quit ", + help: format!("{} ", build_keymapping_string(&SERVARR_KEYMAPPINGS)), contextual_help: None, }, ] diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index 475ec56..09dce37 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -1,5 +1,15 @@ 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::>() + .join(" | ") +} + macro_rules! generate_keybindings { ($($field:ident),+) => { pub struct KeyBindings { @@ -28,12 +38,14 @@ generate_keybindings! { events, home, end, + tab, delete, submit, quit, esc } +#[derive(Clone, Copy)] pub struct KeyBinding { pub key: Key, pub desc: &'static str, @@ -42,94 +54,106 @@ pub struct KeyBinding { pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { add: KeyBinding { key: Key::Char('a'), - desc: "Add", + desc: "add", }, up: KeyBinding { key: Key::Up, - desc: "Scroll up", + desc: "up", }, down: KeyBinding { key: Key::Down, - desc: "Scroll down", + desc: "down", }, left: KeyBinding { key: Key::Left, - desc: "Move left", + desc: "left", }, right: KeyBinding { key: Key::Right, - desc: "Move right", + desc: "right", }, backspace: KeyBinding { key: Key::Backspace, - desc: "Backspace", + desc: "backspace", }, search: KeyBinding { key: Key::Char('s'), - desc: "Search", + desc: "search", }, settings: KeyBinding { key: Key::Char('s'), - desc: "Settings", + desc: "settings", }, filter: KeyBinding { key: Key::Char('f'), - desc: "Filter", + desc: "filter", }, sort: KeyBinding { key: Key::Char('o'), - desc: "Sort", + desc: "sort", }, edit: KeyBinding { key: Key::Char('e'), - desc: "Edit", + desc: "edit", }, events: KeyBinding { key: Key::Char('e'), - desc: "Events", + desc: "events", }, logs: KeyBinding { key: Key::Char('l'), - desc: "Logs", + desc: "logs", }, tasks: KeyBinding { key: Key::Char('t'), - desc: "Tasks", + desc: "tasks", }, restrictions: KeyBinding { - key: Key::Char('t'), - desc: "Restrictions", + key: Key::Char('R'), + desc: "restrictions", }, refresh: KeyBinding { - key: Key::Char('r'), - desc: "Refresh", + key: Key::Ctrl('r'), + desc: "refresh", }, update: KeyBinding { key: Key::Char('u'), - desc: "Update", + desc: "update", }, home: KeyBinding { key: Key::Home, - desc: "Home", + desc: "home", }, end: KeyBinding { key: Key::End, - desc: "End", + desc: "end", + }, + tab: KeyBinding { + key: Key::Tab, + desc: "tab", }, delete: KeyBinding { key: Key::Delete, - desc: "Delete selected item", + desc: "delete", }, submit: KeyBinding { key: Key::Enter, - desc: "Select", + desc: "submit", }, quit: KeyBinding { key: Key::Char('q'), - desc: "Quit", + desc: "quit", }, esc: KeyBinding { 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)]; diff --git a/src/app/key_binding_tests.rs b/src/app/key_binding_tests.rs new file mode 100644 index 0000000..e2c604e --- /dev/null +++ b/src/app/key_binding_tests.rs @@ -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), + " add | delete" + ); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 4a0ddc7..a30fe56 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -9,10 +9,13 @@ use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState}; use crate::network::NetworkEvent; +use self::key_binding::{build_keymapping_string, SERVARR_KEYMAPPINGS}; + #[cfg(test)] #[path = "app_tests.rs"] mod app_tests; pub mod key_binding; +mod key_binding_tests; pub mod radarr; const DEFAULT_ROUTE: Route = Route::Radarr(ActiveRadarrBlock::Movies, None); @@ -137,13 +140,16 @@ impl<'a> Default for App<'a> { TabRoute { title: "Radarr", route: ActiveRadarrBlock::Movies.into(), - help: "<↑↓> scroll | ←→ change tab | change servarr | quit ", + help: format!( + "<↑↓> scroll | ←→ change tab | {} ", + build_keymapping_string(&SERVARR_KEYMAPPINGS) + ), contextual_help: None, }, TabRoute { title: "Sonarr", route: Route::Sonarr, - help: " change servarr | quit ", + help: format!("{} ", build_keymapping_string(&SERVARR_KEYMAPPINGS)), contextual_help: None, }, ]), diff --git a/src/app/radarr.rs b/src/app/radarr/mod.rs similarity index 80% rename from src/app/radarr.rs rename to src/app/radarr/mod.rs index 72aa4d9..cccdd76 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr/mod.rs @@ -15,6 +15,16 @@ use crate::models::{ }; 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)] #[path = "radarr_tests.rs"] mod radarr_tests; @@ -251,130 +261,132 @@ impl<'a> RadarrData<'a> { impl<'a> Default for RadarrData<'a> { fn default() -> RadarrData<'a> { RadarrData { - root_folders: StatefulTable::default(), - disk_space_vec: Vec::new(), - version: String::default(), - start_time: DateTime::default(), - movies: StatefulTable::default(), - add_searched_movies: StatefulTable::default(), - monitor_list: StatefulList::default(), - minimum_availability_list: StatefulList::default(), - quality_profile_list: StatefulList::default(), - root_folder_list: StatefulList::default(), - selected_block: BlockSelectionState::default(), - filtered_movies: StatefulTable::default(), - downloads: StatefulTable::default(), - indexers: StatefulTable::default(), - indexer_settings: None, - quality_profile_map: BiMap::default(), - tags_map: BiMap::default(), - file_details: String::default(), - audio_details: String::default(), - video_details: String::default(), - movie_details: ScrollableText::default(), - movie_history: StatefulTable::default(), - movie_cast: StatefulTable::default(), - movie_crew: StatefulTable::default(), - movie_releases: StatefulTable::default(), - movie_releases_sort: StatefulList::default(), - collections: StatefulTable::default(), - filtered_collections: StatefulTable::default(), - collection_movies: StatefulTable::default(), - logs: StatefulList::default(), - log_details: StatefulList::default(), - tasks: StatefulTable::default(), - queued_events: StatefulTable::default(), - updates: ScrollableText::default(), - prompt_confirm_action: None, - search: HorizontallyScrollableText::default(), - filter: HorizontallyScrollableText::default(), - edit_path: HorizontallyScrollableText::default(), - edit_tags: HorizontallyScrollableText::default(), - edit_monitored: None, - edit_search_on_add: None, - sort_ascending: None, - is_searching: false, - is_filtering: false, - prompt_confirm: false, - delete_movie_files: false, - add_list_exclusion: false, - main_tabs: TabState::new(vec![ - TabRoute { - title: "Library", - route: ActiveRadarrBlock::Movies.into(), - help: "", - contextual_help: Some(" add | edit | delete | search | filter | refresh | update all | details | cancel filter"), - }, - TabRoute { - title: "Downloads", - route: ActiveRadarrBlock::Downloads.into(), - help: "", - contextual_help: Some(" refresh | delete"), - }, - TabRoute { - title: "Collections", - route: ActiveRadarrBlock::Collections.into(), - help: "", - contextual_help: Some(" search | edit | filter | refresh | update all | details | cancel filter"), - }, - TabRoute { - title: "Root Folders", - route: ActiveRadarrBlock::RootFolders.into(), - help: "", - contextual_help: Some(" add | delete | refresh"), - }, - TabRoute { - title: "Indexers", - route: ActiveRadarrBlock::Indexers.into(), - help: "", - contextual_help: Some(" add | edit | settings | restrictions | delete | refresh"), - }, - TabRoute { - title: "System", - route: ActiveRadarrBlock::System.into(), - help: "", - contextual_help: Some(" open tasks | open events | open logs | open updates | refresh"), - }, - ]), - movie_info_tabs: TabState::new(vec![ - TabRoute { - title: "Details", - route: ActiveRadarrBlock::MovieDetails.into(), - help: " refresh | update | edit | auto search | close", - contextual_help: None, - }, - TabRoute { - title: "History", - route: ActiveRadarrBlock::MovieHistory.into(), - help: " refresh | update | edit | auto search | close", - contextual_help: None, - }, - TabRoute { - title: "File", - route: ActiveRadarrBlock::FileInfo.into(), - help: " refresh | update | edit | auto search | close", - contextual_help: None, - }, - TabRoute { - title: "Cast", - route: ActiveRadarrBlock::Cast.into(), - help: " refresh | update | edit | auto search | close", - contextual_help: None, - }, - TabRoute { - title: "Crew", - route: ActiveRadarrBlock::Crew.into(), - help: " refresh | update | edit | auto search | close", - contextual_help: None, - }, - TabRoute { - title: "Manual Search", - route: ActiveRadarrBlock::ManualSearch.into(), - help: " refresh | update | edit | sort | auto search | close", - contextual_help: Some(" details"), - }, - ]), - } + root_folders: StatefulTable::default(), + disk_space_vec: Vec::new(), + version: String::default(), + start_time: DateTime::default(), + movies: StatefulTable::default(), + add_searched_movies: StatefulTable::default(), + monitor_list: StatefulList::default(), + minimum_availability_list: StatefulList::default(), + quality_profile_list: StatefulList::default(), + root_folder_list: StatefulList::default(), + selected_block: BlockSelectionState::default(), + filtered_movies: StatefulTable::default(), + downloads: StatefulTable::default(), + indexers: StatefulTable::default(), + indexer_settings: None, + quality_profile_map: BiMap::default(), + tags_map: BiMap::default(), + file_details: String::default(), + audio_details: String::default(), + video_details: String::default(), + movie_details: ScrollableText::default(), + movie_history: StatefulTable::default(), + movie_cast: StatefulTable::default(), + movie_crew: StatefulTable::default(), + movie_releases: StatefulTable::default(), + movie_releases_sort: StatefulList::default(), + collections: StatefulTable::default(), + filtered_collections: StatefulTable::default(), + collection_movies: StatefulTable::default(), + logs: StatefulList::default(), + log_details: StatefulList::default(), + tasks: StatefulTable::default(), + queued_events: StatefulTable::default(), + updates: ScrollableText::default(), + prompt_confirm_action: None, + search: HorizontallyScrollableText::default(), + filter: HorizontallyScrollableText::default(), + edit_path: HorizontallyScrollableText::default(), + edit_tags: HorizontallyScrollableText::default(), + edit_monitored: None, + edit_search_on_add: None, + sort_ascending: None, + is_searching: false, + is_filtering: false, + prompt_confirm: false, + delete_movie_files: false, + add_list_exclusion: false, + main_tabs: TabState::new(vec![ + TabRoute { + title: "Library", + route: ActiveRadarrBlock::Movies.into(), + help: String::new(), + contextual_help: Some(build_keymapping_string(&LIBRARY_KEY_MAPPINGS)), + }, + TabRoute { + title: "Downloads", + route: ActiveRadarrBlock::Downloads.into(), + help: String::new(), + contextual_help: Some(build_keymapping_string(&DOWNLOADS_KEY_MAPPINGS)), + }, + TabRoute { + title: "Collections", + route: ActiveRadarrBlock::Collections.into(), + help: String::new(), + contextual_help: Some(build_keymapping_string(&COLLECTIONS_KEY_MAPPINGS)), + }, + TabRoute { + title: "Root Folders", + route: ActiveRadarrBlock::RootFolders.into(), + help: String::new(), + contextual_help: Some(build_keymapping_string(&ROOT_FOLDERS_KEY_MAPPINGS)), + }, + TabRoute { + title: "Indexers", + route: ActiveRadarrBlock::Indexers.into(), + help: String::new(), + contextual_help: Some(build_keymapping_string(&INDEXERS_KEY_MAPPINGS)), + }, + TabRoute { + title: "System", + route: ActiveRadarrBlock::System.into(), + help: String::new(), + contextual_help: Some(build_keymapping_string(&SYSTEM_KEY_MAPPINGS)), + }, + ]), + movie_info_tabs: TabState::new(vec![ + TabRoute { + title: "Details", + route: ActiveRadarrBlock::MovieDetails.into(), + help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS), + contextual_help: None, + }, + TabRoute { + title: "History", + route: ActiveRadarrBlock::MovieHistory.into(), + help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS), + contextual_help: None, + }, + TabRoute { + title: "File", + route: ActiveRadarrBlock::FileInfo.into(), + help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS), + contextual_help: None, + }, + TabRoute { + title: "Cast", + route: ActiveRadarrBlock::Cast.into(), + help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS), + contextual_help: None, + }, + TabRoute { + title: "Crew", + route: ActiveRadarrBlock::Crew.into(), + help: build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS), + contextual_help: None, + }, + TabRoute { + title: "Manual Search", + route: ActiveRadarrBlock::ManualSearch.into(), + help: build_keymapping_string(&MANUAL_MOVIE_SEARCH_KEY_MAPPINGS), + contextual_help: Some(build_keymapping_string( + &MANUAL_MOVIE_SEARCH_CONTEXTUAL_KEY_MAPPINGS, + )), + }, + ]), + } } } diff --git a/src/app/radarr/radarr_key_mappings.rs b/src/app/radarr/radarr_key_mappings.rs new file mode 100644 index 0000000..2f98835 --- /dev/null +++ b/src/app/radarr/radarr_key_mappings.rs @@ -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), +]; diff --git a/src/app/radarr_test_utils.rs b/src/app/radarr/radarr_test_utils.rs similarity index 100% rename from src/app/radarr_test_utils.rs rename to src/app/radarr/radarr_test_utils.rs diff --git a/src/app/radarr_tests.rs b/src/app/radarr/radarr_tests.rs similarity index 96% rename from src/app/radarr_tests.rs rename to src/app/radarr/radarr_tests.rs index dce036f..cf77dbd 100644 --- a/src/app/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -8,6 +8,13 @@ mod tests { use serde_json::Number; 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::{ActiveRadarrBlock, RadarrData}; use crate::models::radarr_models::{ @@ -316,8 +323,10 @@ mod tests { ActiveRadarrBlock::Movies.into() ); assert!(radarr_data.main_tabs.tabs[0].help.is_empty()); - assert_eq!(radarr_data.main_tabs.tabs[0].contextual_help, - Some(" add | edit | delete | search | filter | refresh | update all | details | cancel filter")); + assert_eq!( + 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_eq!( @@ -327,7 +336,7 @@ mod tests { assert!(radarr_data.main_tabs.tabs[1].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[1].contextual_help, - Some(" refresh | delete") + Some(build_keymapping_string(&DOWNLOADS_KEY_MAPPINGS)) ); assert_str_eq!(radarr_data.main_tabs.tabs[2].title, "Collections"); @@ -336,8 +345,10 @@ mod tests { ActiveRadarrBlock::Collections.into() ); assert!(radarr_data.main_tabs.tabs[2].help.is_empty()); - assert_eq!(radarr_data.main_tabs.tabs[2].contextual_help, - Some(" search | edit | filter | refresh | update all | details | cancel filter")); + assert_eq!( + 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_eq!( @@ -347,7 +358,7 @@ mod tests { assert!(radarr_data.main_tabs.tabs[3].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[3].contextual_help, - Some(" add | delete | refresh") + Some(build_keymapping_string(&ROOT_FOLDERS_KEY_MAPPINGS)) ); 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_eq!( radarr_data.main_tabs.tabs[4].contextual_help, - Some( - " add | edit | settings | restrictions | delete | refresh" - ) + Some(build_keymapping_string(&INDEXERS_KEY_MAPPINGS)) ); 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_eq!( radarr_data.main_tabs.tabs[5].contextual_help, - Some(" open tasks | open events | open logs | open updates | refresh") + Some(build_keymapping_string(&SYSTEM_KEY_MAPPINGS)) ); assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6); @@ -383,7 +392,7 @@ mod tests { ); assert_str_eq!( radarr_data.movie_info_tabs.tabs[0].help, - " refresh | update | edit | auto search | close" + build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS) ); assert!(radarr_data.movie_info_tabs.tabs[0] .contextual_help @@ -396,7 +405,7 @@ mod tests { ); assert_str_eq!( radarr_data.movie_info_tabs.tabs[1].help, - " refresh | update | edit | auto search | close" + build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS) ); assert!(radarr_data.movie_info_tabs.tabs[1] .contextual_help @@ -409,7 +418,7 @@ mod tests { ); assert_str_eq!( radarr_data.movie_info_tabs.tabs[2].help, - " refresh | update | edit | auto search | close" + build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS) ); assert!(radarr_data.movie_info_tabs.tabs[2] .contextual_help @@ -422,7 +431,7 @@ mod tests { ); assert_str_eq!( radarr_data.movie_info_tabs.tabs[3].help, - " refresh | update | edit | auto search | close" + build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS) ); assert!(radarr_data.movie_info_tabs.tabs[3] .contextual_help @@ -435,7 +444,7 @@ mod tests { ); assert_str_eq!( radarr_data.movie_info_tabs.tabs[4].help, - " refresh | update | edit | auto search | close" + build_keymapping_string(&MOVIE_DETAILS_KEY_MAPPINGS) ); assert!(radarr_data.movie_info_tabs.tabs[4] .contextual_help @@ -448,11 +457,13 @@ mod tests { ); assert_str_eq!( radarr_data.movie_info_tabs.tabs[5].help, - " refresh | update | edit | sort | auto search | close" + build_keymapping_string(&MANUAL_MOVIE_SEARCH_KEY_MAPPINGS) ); assert_eq!( radarr_data.movie_info_tabs.tabs[5].contextual_help, - Some(" details") + Some(build_keymapping_string( + &MANUAL_MOVIE_SEARCH_CONTEXTUAL_KEY_MAPPINGS + )) ); } } diff --git a/src/event/key.rs b/src/event/key.rs index 60b5f31..cc464a5 100644 --- a/src/event/key.rs +++ b/src/event/key.rs @@ -1,13 +1,13 @@ use std::fmt; use std::fmt::{Display, Formatter}; -use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; #[cfg(test)] #[path = "key_tests.rs"] mod key_tests; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Key { Up, Down, @@ -18,7 +18,9 @@ pub enum Key { Backspace, Home, End, + Tab, Delete, + Ctrl(char), Char(char), Unknown, } @@ -27,6 +29,18 @@ impl Display for Key { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { Key::Char(c) => write!(f, "<{}>", c), + Key::Ctrl(c) => write!(f, "", c), + Key::Up => write!(f, "<↑>"), + Key::Down => write!(f, "<↓>"), + Key::Left => write!(f, "<←>"), + Key::Right => write!(f, "<→>"), + Key::Enter => write!(f, ""), + Key::Esc => write!(f, ""), + Key::Backspace => write!(f, ""), + Key::Home => write!(f, ""), + Key::End => write!(f, ""), + Key::Tab => write!(f, ""), + Key::Delete => write!(f, ""), _ => write!(f, "<{:?}>", self), } } @@ -61,6 +75,9 @@ impl From for Key { KeyEvent { code: KeyCode::End, .. } => Key::End, + KeyEvent { + code: KeyCode::Tab, .. + } => Key::Tab, KeyEvent { code: KeyCode::Delete, .. @@ -72,6 +89,11 @@ impl From for Key { KeyEvent { code: KeyCode::Esc, .. } => Key::Esc, + KeyEvent { + code: KeyCode::Char(c), + modifiers: KeyModifiers::CONTROL, + .. + } => Key::Ctrl(c), KeyEvent { code: KeyCode::Char(c), .. diff --git a/src/event/key_tests.rs b/src/event/key_tests.rs index 6ec0f5a..bbe037a 100644 --- a/src/event/key_tests.rs +++ b/src/event/key_tests.rs @@ -1,18 +1,27 @@ #[cfg(test)] mod tests { - use crossterm::event::{KeyCode, KeyEvent}; + use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}; use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; use crate::event::key::Key; - #[test] - fn test_key_formatter() { - assert_str_eq!(format!("{}", Key::Esc), ""); - } - - #[test] - fn test_key_formatter_char() { - assert_str_eq!(format!("{}", Key::Char('q')), ""); + #[rstest] + #[case(Key::Up, "↑")] + #[case(Key::Down, "↓")] + #[case(Key::Left, "←")] + #[case(Key::Right, "→")] + #[case(Key::Enter, "enter")] + #[case(Key::Esc, "esc")] + #[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] @@ -53,6 +62,11 @@ mod tests { 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] fn test_key_from_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] fn test_key_from_unknown() { assert_eq!(Key::from(KeyEvent::from(KeyCode::Pause)), Key::Unknown); diff --git a/src/models/mod.rs b/src/models/mod.rs index 77bdcd6..883fcf0 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -284,12 +284,12 @@ impl HorizontallyScrollableText { } } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub struct TabRoute { pub title: &'static str, pub route: Route, - pub help: &'static str, - pub contextual_help: Option<&'static str>, + pub help: String, + pub contextual_help: Option, } pub struct TabState { @@ -313,12 +313,12 @@ impl TabState { &self.tabs[self.index].route } - pub fn get_active_tab_help(&self) -> &'static str { - self.tabs[self.index].help + pub fn get_active_tab_help(&self) -> &str { + &self.tabs[self.index].help } - pub fn get_active_tab_contextual_help(&self) -> Option<&'static str> { - self.tabs[self.index].contextual_help + pub fn get_active_tab_contextual_help(&self) -> Option { + self.tabs[self.index].contextual_help.clone() } pub fn next(&mut self) { diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index 7554d70..881f610 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -533,7 +533,7 @@ mod tests { #[test] fn test_tab_state_get_active_tab_help() { 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_help = tab_state.get_active_tab_help(); @@ -544,7 +544,7 @@ mod tests { #[test] fn test_tab_state_get_active_tab_contextual_help() { 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_contextual_help = tab_state.get_active_tab_contextual_help(); @@ -648,14 +648,14 @@ mod tests { TabRoute { title: "Test 1", route: ActiveRadarrBlock::Movies.into(), - help: "Help for Test 1", - contextual_help: Some("Contextual Help for Test 1"), + help: "Help for Test 1".to_owned(), + contextual_help: Some("Contextual Help for Test 1".to_owned()), }, TabRoute { title: "Test 2", route: ActiveRadarrBlock::Collections.into(), - help: "Help for Test 2", - contextual_help: Some("Contextual Help for Test 2"), + help: "Help for Test 2".to_owned(), + contextual_help: Some("Contextual Help for Test 2".to_owned()), }, ] } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f5a257d..e387eaa 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -294,7 +294,7 @@ pub struct TableProps<'a, T> { pub content: &'a mut StatefulTable, pub table_headers: Vec<&'a str>, pub constraints: Vec, - pub help: Option<&'static str>, + pub help: Option, } pub struct ListProps<'a, T> { @@ -302,7 +302,7 @@ pub struct ListProps<'a, T> { pub title: &'static str, pub is_loading: bool, pub is_popup: bool, - pub help: Option<&'static str>, + pub help: Option, } 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( f: &mut Frame<'_, B>, area: Rect, - help: Option<&str>, + help: Option, ) -> Rect { if let Some(help_string) = help { let chunks = diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index 0e69298..9ef7681 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -4,6 +4,8 @@ use tui::text::Text; use tui::widgets::{Cell, Paragraph, Row, Wrap}; 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::App; use crate::models::radarr_models::CollectionMovie; @@ -102,8 +104,10 @@ pub fn draw_collection_details( .current_selection() .clone() }; - let mut help_text = - Text::from("<↑↓> scroll table | show overview/add movie | close"); + let mut help_text = Text::from(format!( + "<↑↓> scroll table | {}", + build_keymapping_string(&COLLECTION_DETAILS_KEY_MAPPINGS) + )); help_text.patch_style(style_help()); let monitored = if collection_selection.monitored { "Yes" @@ -257,7 +261,7 @@ fn draw_movie_overview(f: &mut Frame<'_, B>, app: &mut App<'_>, cont .overview, ); overview.patch_style(style_default()); - let mut help_text = Text::from(" close"); + let mut help_text = Text::from(build_keymapping_string(&BARE_POPUP_KEY_MAPPINGS)); help_text.patch_style(style_help()); let paragraph = Paragraph::new(overview) diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index a364ed8..b03e747 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -4,6 +4,8 @@ use tui::text::Text; use tui::widgets::{Cell, ListItem, Paragraph, Row}; 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::models::radarr_models::AddMovieSearchResult; use crate::models::Route; @@ -142,7 +144,7 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App<'_>, ar ); f.render_widget(layout_block(), chunks[1]); - let mut help_text = Text::from(" close"); + let mut help_text = Text::from(build_keymapping_string(&BARE_POPUP_KEY_MAPPINGS)); help_text.patch_style(style_help()); let help_paragraph = Paragraph::new(help_text) .block(borderless_block()) @@ -161,7 +163,9 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App<'_>, ar | ActiveRadarrBlock::AddMovieSelectRootFolder | ActiveRadarrBlock::AddMovieAlreadyInLibrary | ActiveRadarrBlock::AddMovieTagsInput => { - let mut help_text = Text::from(" details | edit search"); + let mut help_text = Text::from(build_keymapping_string( + &ADD_MOVIE_SEARCH_RESULTS_KEY_MAPPINGS, + )); help_text.patch_style(style_help()); let help_paragraph = Paragraph::new(help_text) .block(borderless_block()) diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index 8de9c60..364f89f 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -4,6 +4,8 @@ use tui::text::{Span, Text}; use tui::widgets::{Cell, ListItem, Paragraph, Row}; 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::App; use crate::models::Route; @@ -84,7 +86,10 @@ fn draw_logs_popup(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Re title: "Log Details", is_loading: app.is_loading, is_popup: true, - help: Some("<↑↓←→> scroll | close"), + help: Some(format!( + "<↑↓←→> scroll | {}", + build_keymapping_string(&BARE_POPUP_KEY_MAPPINGS) + )), }, ); } @@ -93,8 +98,11 @@ fn draw_tasks_popup(f: &mut Frame<'_, B>, app: &mut App<'_>, area: R let tasks_popup_table = |f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect| { f.render_widget(title_block("Tasks"), area); - let context_area = - draw_help_and_get_content_rect(f, area, Some(" start task | close")); + let context_area = draw_help_and_get_content_rect( + f, + area, + Some(build_keymapping_string(&SYSTEM_TASKS_KEY_MAPPINGS)), + ); draw_table( f, @@ -150,7 +158,14 @@ fn draw_start_task_prompt(f: &mut Frame<'_, B>, app: &mut App<'_>, p fn draw_updates_popup(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { f.render_widget(title_block("Updates"), area); - let content_rect = draw_help_and_get_content_rect(f, area, Some("<↑↓> scroll | 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 block = borderless_block();