Compare commits

...

15 Commits

Author SHA1 Message Date
1ff31b1bd9 Updated codecov config to also ignore the ui directory entirely, since it's not very conducive to tests as it is 2024-11-03 16:05:17 -07:00
ce4cbd8652 Updated codecov config to ignore test files 2024-11-03 16:01:49 -07:00
346d95f8ec Added Codecov config 2024-11-03 15:57:18 -07:00
85ea05e3c8 Incremented minor release since removing the --disable-terminal-size-checks flag is a breaking change, but the application is not yet ready for a 1.0 release. 2024-11-03 15:42:59 -07:00
93d78701ce fix:fixed divide by zero panic when download size is unknown 2024-11-03 15:36:26 -07:00
8d7cb63c7a Remove the terminal size checks since they've caused so many issues since their introduction 2024-11-03 15:33:08 -07:00
c8c7d00517 Added environment variables section to the README for added visibility into the feature 2024-11-03 15:06:24 -07:00
Alex Clarke
9402ad3f3b Merge pull request #9 from tangowithfoxtrot/add-env-var-config-options
Add environment variables for --config and terminal size check args
2024-11-03 14:43:50 -07:00
tangowithfoxtrot
ea9a9070ce Merge branch 'rc/v0.1.6-address-community-comments' into add-env-var-config-options 2024-11-03 13:40:53 -08:00
a0fe51c57b Added help that's always visible for modals with new shortcuts for accepting all modals, or closing all modals without the need of seeing the UI 2024-11-03 14:25:33 -07:00
tangowithfoxtrot
9326428141 feat: allow configuration via env vars 2024-11-03 11:20:15 -08:00
Alex Clarke
c1da8592b4 Merge pull request #7 from Dark-Alex-17/release-plz-2024-11-03T00-33-10Z
chore: release v0.1.5
2024-11-02 18:37:24 -06:00
github-actions[bot]
aa43219c29 chore: release v0.1.5 2024-11-03 00:33:11 +00:00
f6f477b124 Added HTTPS support for all Servarrs 2024-11-02 18:32:44 -06:00
Alex Clarke
76f22e7434 Merge pull request #5 from Dark-Alex-17/release-plz-2024-11-01T19-06-52Z
chore: release v0.1.4
2024-11-01 13:13:03 -06:00
62 changed files with 1128 additions and 254 deletions
+6
View File
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03
### Other
- Added HTTPS support for all Servarrs
## [0.1.4](https://github.com/Dark-Alex-17/managarr/compare/v0.1.3...v0.1.4) - 2024-11-01
### Other
Generated
+1 -1
View File
@@ -1148,7 +1148,7 @@ dependencies = [
[[package]]
name = "managarr"
version = "0.1.4"
version = "0.2.0"
dependencies = [
"anyhow",
"assert_cmd",
+3 -3
View File
@@ -1,9 +1,9 @@
[package]
name = "managarr"
version = "0.1.4"
version = "0.2.0"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"]
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
documentation = "https://github.com/Dark-Alex-17/managarr"
repository = "https://github.com/Dark-Alex-17/managarr"
homepage = "https://github.com/Dark-Alex-17/managarr"
@@ -38,7 +38,7 @@ tokio = { version = "1.36.0", features = ["full"] }
tokio-util = "0.7.8"
ratatui = { version = "0.28.0", features = ["all-widgets"] }
urlencoding = "2.1.2"
clap = { version = "4.5.20", features = ["derive", "cargo"] }
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
clap_complete = "4.5.33"
itertools = "0.13.0"
ctrlc = "3.4.5"
+16 -3
View File
@@ -113,7 +113,7 @@ To see all available commands, simply run `managarr --help`:
```shell
$ managarr --help
managarr 0.1.3
managarr 0.1.5
Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs
@@ -127,7 +127,6 @@ Commands:
Options:
--config <CONFIG> The Managarr configuration file to use
--disable-terminal-size-checks Disable the terminal size checks
-h, --help Print help
-V, --version Print version
```
@@ -159,7 +158,6 @@ Commands:
Options:
--config <CONFIG> The Managarr configuration file to use
--disable-terminal-size-checks Disable the terminal size checks
-h, --help Print help
```
@@ -206,6 +204,8 @@ radarr:
host: 127.0.0.1
port: 7878
api_token: someApiToken1234567890
use_ssl: true
ssl_cert_path: /path/to/radarr.crt
sonarr:
host: 127.0.0.1
port: 8989
@@ -214,28 +214,41 @@ readarr:
host: 127.0.0.1
port: 8787
api_token: someApiToken1234567890
use_ssl: false
lidarr:
host: 127.0.0.1
port: 8686
api_token: someApiToken1234567890
use_ssl: false
whisparr:
host: 127.0.0.1
port: 6969
api_token: someApiToken1234567890
use_ssl: false
bazarr:
host: 127.0.0.1
port: 6767
api_token: someApiToken1234567890
use_ssl: false
prowlarr:
host: 127.0.0.1
port: 9696
api_token: someApiToken1234567890
use_ssl: false
tautulli:
host: 127.0.0.1
port: 8181
api_token: someApiToken1234567890
use_ssl: false
```
## Environment Variables
Managarr supports using environment variables on startup so you don't have to always specify certain flags:
| Variable | Description | Equivalent Flag |
| --------------------------------------- | -------------------------------- | -------------------------------- |
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
## Track My Progress for the Beta release (With Sonarr Support!)
Progress for the beta release can be followed on my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr)
with all items tagged `Beta`.
+6
View File
@@ -0,0 +1,6 @@
coverage:
range: "80..100"
ignore:
- "**/*_tests.rs"
- "src/ui"
+2
View File
@@ -224,5 +224,7 @@ mod tests {
assert_str_eq!(radarr_config.host, "localhost");
assert_eq!(radarr_config.port, Some(7878));
assert!(radarr_config.api_token.is_empty());
assert!(!radarr_config.use_ssl);
assert_eq!(radarr_config.ssl_cert_path, None);
}
}
+5
View File
@@ -33,6 +33,7 @@ generate_keybindings! {
tab,
delete,
submit,
confirm,
quit,
esc
}
@@ -140,6 +141,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
key: Key::Enter,
desc: "submit",
},
confirm: KeyBinding {
key: Key::Ctrl('s'),
desc: "submit",
},
quit: KeyBinding {
key: Key::Char('q'),
desc: "quit",
+1
View File
@@ -31,6 +31,7 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.tab, Key::Tab, "tab")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")]
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")]
#[case(DEFAULT_KEYBINDINGS.quit, Key::Char('q'), "quit")]
#[case(DEFAULT_KEYBINDINGS.esc, Key::Esc, "close")]
fn test_default_key_bindings_and_descriptions(
+5
View File
@@ -176,6 +176,9 @@ pub struct RadarrConfig {
pub host: String,
pub port: Option<u16>,
pub api_token: String,
#[serde(default)]
pub use_ssl: bool,
pub ssl_cert_path: Option<String>,
}
impl Default for RadarrConfig {
@@ -184,6 +187,8 @@ impl Default for RadarrConfig {
host: "localhost".to_string(),
port: Some(7878),
api_token: "".to_string(),
use_ssl: false,
ssl_cert_path: None,
}
}
}
+5
View File
@@ -120,6 +120,11 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.confirm, "submit"),
(DEFAULT_KEYBINDINGS.esc, "cancel"),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
+18 -2
View File
@@ -5,8 +5,8 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
};
@@ -349,6 +349,22 @@ mod tests {
assert_eq!(add_movie_search_results_context_clues_iter.next(), None);
}
#[test]
fn test_confirmation_prompt_context_clues() {
let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter();
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm);
assert_str_eq!(*description, "submit");
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel");
assert_eq!(confirmation_prompt_context_clues_iter.next(), None);
}
#[test]
fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
@@ -584,6 +584,9 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::network::radarr_network::RadarrEvent;
use super::*;
@@ -716,6 +719,43 @@ mod tests {
assert!(app.data.radarr_data.blocklist.sort.is_none());
assert!(!app.data.radarr_data.blocklist.sort_asc);
}
#[rstest]
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
RadarrEvent::DeleteBlocklistItem(None)
)]
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::BlocklistClearAllItemsPrompt,
RadarrEvent::ClearBlocklist
)]
fn test_blocklist_prompt_confirm(
#[case] base_route: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent,
) {
let mut app = App::default();
app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&prompt_block,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
}
}
#[test]
+20 -2
View File
@@ -182,8 +182,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::Blocklist {
match self.key {
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
self.app.should_refresh = true;
}
@@ -204,7 +204,25 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
}
_ => (),
},
ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteBlocklistItem(None));
self.app.pop_navigation_stack();
}
}
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -1040,6 +1040,7 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_COLLECTION_SELECTION_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
use crate::test_edit_collection_key;
use super::*;
@@ -1509,6 +1510,36 @@ mod tests {
assert!(app.data.radarr_data.collections.sort.is_none());
assert!(!app.data.radarr_data.collections.sort_asc);
}
#[test]
fn test_update_all_collections_prompt_confirm_confirm() {
let mut app = App::default();
app
.data
.radarr_data
.collections
.set_items(vec![Collection::default()]);
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
CollectionsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::UpdateAllCollectionsPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::UpdateCollections)
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
);
}
}
#[rstest]
@@ -291,19 +291,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput {
handle_text_box_keys!(
self,
key,
self
.app
.data
.radarr_data
.edit_collection_modal
.as_mut()
.unwrap()
.path
)
match self.active_radarr_block {
ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.radarr_data
.edit_collection_modal
.as_mut()
.unwrap()
.path
)
}
ActiveRadarrBlock::EditCollectionPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditCollectionConfirmPrompt
&& *key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -871,7 +871,15 @@ mod tests {
mod test_handle_key_char {
use super::*;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::{
models::{
servarr_data::radarr::{
modals::EditCollectionModal, radarr_data::EDIT_COLLECTION_SELECTION_BLOCKS,
},
BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
#[test]
fn test_edit_collection_root_folder_path_input_backspace() {
@@ -927,6 +935,39 @@ mod tests {
"h"
);
}
#[test]
fn test_edit_collection_confirm_prompt_prompt_confirmation_confirm() {
let mut app = App::default();
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditCollection(None))
);
assert!(app.should_refresh);
}
}
#[test]
@@ -385,6 +385,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
.unwrap()
)
}
ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -349,6 +349,9 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::network::radarr_network::RadarrEvent;
use super::*;
@@ -450,6 +453,47 @@ mod tests {
);
assert!(!app.should_refresh);
}
#[rstest]
#[case(
ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::DeleteDownloadPrompt,
RadarrEvent::DeleteDownload(None)
)]
#[case(
ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::UpdateDownloadsPrompt,
RadarrEvent::UpdateDownloads
)]
fn test_downloads_prompt_confirm_submit(
#[case] base_route: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent,
) {
let mut app = App::default();
app
.data
.radarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&prompt_block,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
}
}
#[test]
+19 -2
View File
@@ -119,8 +119,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
match self.key {
match self.active_radarr_block {
ActiveRadarrBlock::Downloads => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.update.key => {
self
.app
@@ -130,7 +130,24 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::DeleteDownloadPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None));
self.app.pop_navigation_stack();
}
}
ActiveRadarrBlock::UpdateDownloadsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -429,6 +429,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
.tags
);
}
ActiveRadarrBlock::EditIndexerPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditIndexerConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -1282,6 +1282,9 @@ mod tests {
mod test_handle_key_char {
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
use pretty_assertions::assert_str_eq;
use super::*;
@@ -1560,6 +1563,37 @@ mod tests {
"h"
);
}
#[test]
fn test_edit_indexer_prompt_prompt_confirmation_confirm() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1);
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::EditIndexerPrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert!(app.data.radarr_data.edit_indexer_modal.is_some());
assert!(app.should_refresh);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditIndexer(None))
);
}
}
#[test]
@@ -241,19 +241,35 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
}
fn handle_char_key_event(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.indexer_settings
.as_mut()
.unwrap()
.whitelisted_hardcoded_subs
)
match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.indexer_settings
.as_mut()
.unwrap()
.whitelisted_hardcoded_subs
)
}
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::IndexerSettingsConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::EditAllIndexerSettings(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -851,7 +851,13 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::assert_str_eq;
use crate::models::radarr_models::IndexerSettings;
use crate::{
models::{
radarr_models::IndexerSettings,
servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
use super::*;
@@ -909,6 +915,37 @@ mod tests {
"h"
);
}
#[test]
fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditAllIndexerSettings(None))
);
assert!(app.data.radarr_data.indexer_settings.is_some());
assert!(app.should_refresh);
}
}
#[test]
@@ -464,7 +464,10 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use crate::models::servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS;
use crate::{
models::servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
network::radarr_network::RadarrEvent,
};
use super::*;
@@ -696,6 +699,33 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
}
#[test]
fn test_delete_indexer_prompt_confirm() {
let mut app = App::default();
app
.data
.radarr_data
.indexers
.set_items(vec![Indexer::default()]);
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::DeleteIndexerPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteIndexer(None))
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
}
}
#[rstest]
+11 -2
View File
@@ -166,8 +166,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::Indexers {
match self.key {
match self.active_radarr_block {
ActiveRadarrBlock::Indexers => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.add.key => {
self
.app
@@ -194,7 +194,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
}
_ => (),
},
ActiveRadarrBlock::DeleteIndexerPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(None));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
}
@@ -461,6 +461,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
.tags
)
}
ActiveRadarrBlock::AddMoviePrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::AddMovieConfirmPrompt
&& *key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -1494,7 +1494,13 @@ mod tests {
mod test_handle_key_char {
use super::*;
use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::{
models::{
servarr_data::radarr::{modals::AddMovieModal, radarr_data::ADD_MOVIE_SELECTION_BLOCKS},
BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
#[test]
fn test_add_movie_search_input_backspace() {
@@ -1588,6 +1594,35 @@ mod tests {
"h"
);
}
#[test]
fn test_add_movie_confirm_prompt_prompt_confirmation_confirm() {
let mut app = App::default();
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into());
app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1);
AddMovieHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::AddMoviePrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::AddMovie(None))
);
assert!(app.data.radarr_data.add_movie_modal.is_some());
}
}
#[test]
@@ -1,3 +1,4 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -100,5 +101,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
}
}
fn handle_char_key_event(&mut self) {}
fn handle_char_key_event(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::DeleteMoviePrompt
&& self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::DeleteMovieConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
}
@@ -250,6 +250,51 @@ mod tests {
}
}
mod test_handle_key_char {
use crate::{
models::{
servarr_data::radarr::radarr_data::DELETE_MOVIE_SELECTION_BLOCKS, BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
use super::*;
#[test]
fn test_delete_movie_confirm_prompt_prompt_confirm() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into());
app.data.radarr_data.delete_movie_files = true;
app.data.radarr_data.add_list_exclusion = true;
app.data.radarr_data.selected_block =
BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(DELETE_MOVIE_SELECTION_BLOCKS.len() - 1);
DeleteMovieHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::DeleteMoviePrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteMovie(None))
);
assert!(app.should_refresh);
assert!(app.data.radarr_data.prompt_confirm);
assert!(app.data.radarr_data.delete_movie_files);
assert!(app.data.radarr_data.add_list_exclusion);
}
}
#[test]
fn test_delete_movie_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
@@ -327,6 +327,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
.tags
)
}
ActiveRadarrBlock::EditMoviePrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditMovieConfirmPrompt
&& *key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(None));
self.app.should_refresh = true;
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -940,7 +940,16 @@ mod tests {
mod test_handle_key_char {
use super::*;
use crate::models::servarr_data::radarr::modals::EditMovieModal;
use crate::{
models::{
servarr_data::radarr::{
modals::EditMovieModal,
radarr_data::{EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS},
},
BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
#[test]
fn test_edit_movie_path_input_backspace() {
@@ -1051,6 +1060,36 @@ mod tests {
"h"
);
}
#[test]
fn test_edit_movie_confirm_prompt_prompt_confirm() {
let mut app = App::default();
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default());
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into());
app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
EditMovieHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::EditMoviePrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditMovie(None))
);
assert!(app.data.radarr_data.edit_movie_modal.is_some());
assert!(app.should_refresh);
}
}
#[test]
@@ -996,6 +996,7 @@ mod tests {
RadarrData, EDIT_MOVIE_SELECTION_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
use crate::test_edit_movie_key;
use super::*;
@@ -1452,6 +1453,33 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert!(app.data.radarr_data.movies.sort.is_none());
}
#[test]
fn test_update_all_movies_prompt_confirm() {
let mut app = App::default();
app
.data
.radarr_data
.movies
.set_items(vec![Movie::default()]);
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into());
LibraryHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::UpdateAllMoviesPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::UpdateAllMovies)
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
}
}
#[rstest]
@@ -386,6 +386,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
self.app.data.radarr_data.movies.filter.as_mut().unwrap()
)
}
ActiveRadarrBlock::UpdateAllMoviesPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -464,6 +464,32 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
}
_ => (),
},
ActiveRadarrBlock::AutomaticallySearchMoviePrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::TriggerAutomaticSearch(None));
self.app.pop_navigation_stack();
}
}
ActiveRadarrBlock::UpdateAndScanPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None));
self.app.pop_navigation_stack();
}
}
ActiveRadarrBlock::ManualSearchConfirmPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DownloadRelease(None));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -1468,6 +1468,7 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_MOVIE_SELECTION_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
use crate::test_edit_movie_key;
use super::*;
@@ -1780,6 +1781,50 @@ mod tests {
assert_eq!(app.get_current_route(), &active_radarr_block.into());
assert!(app.is_routing);
}
#[rstest]
#[case(
ActiveRadarrBlock::AutomaticallySearchMoviePrompt,
RadarrEvent::TriggerAutomaticSearch(None)
)]
#[case(
ActiveRadarrBlock::UpdateAndScanPrompt,
RadarrEvent::UpdateAndScan(None)
)]
#[case(
ActiveRadarrBlock::ManualSearchConfirmPrompt,
RadarrEvent::DownloadRelease(None)
)]
fn test_movie_info_prompt_confirm(
#[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent,
) {
let mut app = App::default();
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
movie_details: ScrollableText::with_string("test".to_owned()),
..MovieDetailsModal::default()
});
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
app.push_navigation_stack(prompt_block.into());
MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&prompt_block,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::MovieDetails.into()
);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
}
}
#[test]
@@ -180,6 +180,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
self.app.data.radarr_data.edit_root_folder.as_mut().unwrap()
)
}
ActiveRadarrBlock::DeleteRootFolderPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteRootFolder(None));
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
@@ -554,6 +554,8 @@ mod tests {
mod test_handle_key_char {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::network::radarr_network::RadarrEvent;
use super::*;
#[test]
@@ -706,6 +708,36 @@ mod tests {
"h"
);
}
#[test]
fn test_delete_root_folder_prompt_confirm() {
let mut app = App::default();
app
.data
.radarr_data
.root_folders
.set_items(vec![RootFolder::default()]);
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into());
RootFoldersHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::DeleteRootFolderPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteRootFolder(None))
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::RootFolders.into()
);
}
}
#[test]
@@ -168,5 +168,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
{
self.app.should_refresh = true;
}
if self.active_radarr_block == &ActiveRadarrBlock::SystemTaskStartConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(None));
self.app.pop_navigation_stack();
}
}
}
@@ -858,6 +858,8 @@ mod tests {
mod test_handle_key_char {
use rstest::rstest;
use crate::network::radarr_network::RadarrEvent;
use super::*;
#[rstest]
@@ -912,6 +914,32 @@ mod tests {
assert_eq!(app.get_current_route(), &active_radarr_block.into());
assert!(!app.should_refresh);
}
#[test]
fn test_system_tasks_start_task_prompt_confirm() {
let mut app = App::default();
app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned());
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into());
SystemDetailsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::SystemTaskStartConfirmPrompt,
&None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::StartTask(None))
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::SystemTasks.into()
);
}
}
#[test]
+67 -32
View File
@@ -1,6 +1,6 @@
#![warn(rust_2018_idioms)]
use std::fs::File;
use std::fs::{self, File};
use std::io::BufReader;
use std::panic::PanicHookInfo;
use std::path::PathBuf;
@@ -18,12 +18,13 @@ use clap_complete::generate;
use colored::Colorize;
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, size, EnterAlternateScreen, LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use log::error;
use network::NetworkTrait;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use reqwest::{Certificate, Client};
use tokio::sync::mpsc::Receiver;
use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken;
@@ -45,9 +46,6 @@ mod network;
mod ui;
mod utils;
static MIN_TERM_WIDTH: u16 = 205;
static MIN_TERM_HEIGHT: u16 = 40;
#[derive(Debug, Parser)]
#[command(
name = crate_name!(),
@@ -70,18 +68,10 @@ struct Cli {
long,
global = true,
value_parser,
env = "MANAGARR_CONFIG_FILE",
help = "The Managarr configuration file to use"
)]
config: Option<PathBuf>,
#[arg(long, global = true, help = "Disable the terminal size checks")]
disable_terminal_size_checks: bool,
}
fn load_config(path: &str) -> Result<AppConfig> {
let file = File::open(path).map_err(|e| anyhow!(e))?;
let reader = BufReader::new(file);
let config = serde_yaml::from_reader(reader)?;
Ok(config)
}
#[tokio::main]
@@ -98,6 +88,7 @@ async fn main() -> Result<()> {
} else {
confy::load("managarr", "config")?
};
let reqwest_client = build_network_client(&config);
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
let cancellation_token = CancellationToken::new();
let ctrlc_cancellation_token = cancellation_token.clone();
@@ -119,7 +110,7 @@ async fn main() -> Result<()> {
Some(command) => match command {
Command::Radarr(_) => {
let app_nw = Arc::clone(&app);
let mut network = Network::new(&app_nw, cancellation_token);
let mut network = Network::new(&app_nw, cancellation_token, reqwest_client);
if let Err(e) = cli::handle_command(&app, command, &mut network).await {
eprintln!("error: {}", e.to_string().red());
@@ -133,8 +124,10 @@ async fn main() -> Result<()> {
},
None => {
let app_nw = Arc::clone(&app);
std::thread::spawn(move || start_networking(sync_network_rx, &app_nw, cancellation_token));
start_ui(&app, !args.disable_terminal_size_checks).await?;
std::thread::spawn(move || {
start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client)
});
start_ui(&app).await?;
}
}
@@ -146,8 +139,9 @@ async fn start_networking(
mut network_rx: Receiver<NetworkEvent>,
app: &Arc<Mutex<App<'_>>>,
cancellation_token: CancellationToken,
client: Client,
) {
let mut network = Network::new(app, cancellation_token);
let mut network = Network::new(app, cancellation_token, client);
while let Some(network_event) = network_rx.recv().await {
if let Err(e) = network.handle_network_event(network_event).await {
@@ -156,20 +150,7 @@ async fn start_networking(
}
}
async fn start_ui(app: &Arc<Mutex<App<'_>>>, check_terminal_size: bool) -> Result<()> {
if check_terminal_size {
let (width, height) = size()?;
if width < MIN_TERM_WIDTH || height < MIN_TERM_HEIGHT {
return Err(anyhow!(
"Terminal too small. Minimum size required: {}x{}; current terminal size: {}x{}",
MIN_TERM_WIDTH,
MIN_TERM_HEIGHT,
width,
height
));
}
}
async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> {
let mut stdout = io::stdout();
enable_raw_mode()?;
@@ -238,6 +219,60 @@ fn panic_hook(info: &PanicHookInfo<'_>) {
.unwrap();
}
fn load_config(path: &str) -> Result<AppConfig> {
let file = File::open(path).map_err(|e| anyhow!(e))?;
let reader = BufReader::new(file);
let config = serde_yaml::from_reader(reader)?;
Ok(config)
}
fn build_network_client(config: &AppConfig) -> Client {
let mut client_builder = Client::builder();
if config.radarr.use_ssl {
let cert = create_cert(config.radarr.ssl_cert_path.clone(), "Radarr");
client_builder = client_builder.add_root_certificate(cert);
}
match client_builder.build() {
Ok(client) => client,
Err(e) => {
error!("Unable to create reqwest client: {}", e);
eprintln!("error: {}", e.to_string().red());
process::exit(1);
}
}
}
fn create_cert(cert_path: Option<String>, servarr_name: &str) -> Certificate {
let err = |error: String| {
error!("{}", error);
eprintln!("error: {}", error.red());
process::exit(1);
};
if cert_path.is_none() {
err(format!(
"A {} cert path is required when 'use_ssl' is 'true'",
servarr_name
));
}
match fs::read(cert_path.unwrap()) {
Ok(cert) => match Certificate::from_pem(&cert) {
Ok(certificate) => certificate,
Err(_) => err(format!(
"Unable to read the specified {} SSL certificate",
servarr_name
)),
},
Err(_) => err(format!(
"Unable to open specified {} SSL certificate",
servarr_name
)),
}
}
#[cfg(not(debug_assertions))]
fn panic_hook(info: &PanicHookInfo<'_>) {
use human_panic::{handle_dump, print_msg, Metadata};
+4
View File
@@ -291,6 +291,10 @@ pub struct HostConfig {
pub instance_name: Option<HorizontallyScrollableText>,
pub application_url: Option<HorizontallyScrollableText>,
pub enable_ssl: bool,
#[serde(deserialize_with = "super::from_i64")]
pub ssl_port: i64,
pub ssl_cert_path: Option<String>,
pub ssl_cert_password: Option<String>,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
+6 -2
View File
@@ -62,9 +62,13 @@ impl<'a, 'b> NetworkTrait for Network<'a, 'b> {
}
impl<'a, 'b> Network<'a, 'b> {
pub fn new(app: &'a Arc<Mutex<App<'b>>>, cancellation_token: CancellationToken) -> Self {
pub fn new(
app: &'a Arc<Mutex<App<'b>>>,
cancellation_token: CancellationToken,
client: Client,
) -> Self {
Network {
client: Client::new(),
client,
app,
cancellation_token,
}
+13 -10
View File
@@ -6,6 +6,7 @@ mod tests {
use mockito::{Mock, Server, ServerGuard};
use pretty_assertions::assert_str_eq;
use reqwest::Client;
use rstest::rstest;
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, Mutex};
@@ -37,10 +38,12 @@ mod tests {
host,
api_token: String::new(),
port,
use_ssl: false,
ssl_cert_path: None,
};
app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let _ = network
.handle_network_event(RadarrEvent::HealthCheck.into())
@@ -64,7 +67,7 @@ mod tests {
.create_async()
.await;
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let _ = network
.handle_request::<Test, ()>(
@@ -90,7 +93,7 @@ mod tests {
#[values(RequestMethod::Get, RequestMethod::Post)] request_method: RequestMethod,
) {
let (async_server, app_arc, server) = mock_api(request_method, 200, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -122,7 +125,7 @@ mod tests {
#[values(RequestMethod::Get, RequestMethod::Post)] request_method: RequestMethod,
) {
let (async_server, app_arc, server) = mock_api(request_method, 400, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let mut test_result = String::new();
let resp = network
@@ -160,7 +163,7 @@ mod tests {
cancellation_token.clone(),
)));
app_arc.lock().await.is_loading = true;
let mut network = Network::new(&app_arc, cancellation_token);
let mut network = Network::new(&app_arc, cancellation_token, Client::new());
network.cancellation_token.cancel();
let resp = network
@@ -194,7 +197,7 @@ mod tests {
.create_async()
.await;
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -226,7 +229,7 @@ mod tests {
#[tokio::test]
async fn test_handle_request_failure_to_send_request() {
let app_arc = Arc::new(Mutex::new(App::default()));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -266,7 +269,7 @@ mod tests {
request_method: RequestMethod,
) {
let (async_server, app_arc, server) = mock_api(request_method, 404, true).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -296,7 +299,7 @@ mod tests {
#[tokio::test]
async fn test_handle_request_non_success_code_empty_response_body() {
let (async_server, app_arc, server) = mock_api(RequestMethod::Post, 404, false).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let resp = network
.handle_request::<(), Test>(
@@ -354,7 +357,7 @@ mod tests {
async_server = async_server.create_async().await;
let app_arc = Arc::new(Mutex::new(App::default()));
let network = Network::new(&app_arc, CancellationToken::new());
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
network
.call_api(RequestProps {
+7 -1
View File
@@ -2262,8 +2262,14 @@ impl<'a, 'b> Network<'a, 'b> {
host,
port,
api_token,
use_ssl,
..
} = &app.config.radarr;
let uri = format!("http://{host}:{}/api/v3{resource}", port.unwrap_or(7878));
let protocol = if *use_ssl { "https" } else { "http" };
let uri = format!(
"{protocol}://{host}:{}/api/v3{resource}",
port.unwrap_or(7878)
);
RequestProps {
uri,
+110 -98
View File
@@ -6,6 +6,7 @@ mod test {
use chrono::{DateTime, Utc};
use mockito::{Matcher, Mock, Server, ServerGuard};
use pretty_assertions::{assert_eq, assert_str_eq};
use reqwest::Client;
use rstest::rstest;
use serde_json::{json, Number, Value};
use strum::IntoEnumIterator;
@@ -236,7 +237,7 @@ mod test {
RadarrEvent::HealthCheck.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let _ = network.handle_radarr_event(RadarrEvent::HealthCheck).await;
@@ -262,7 +263,7 @@ mod test {
RadarrEvent::GetOverview.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let disk_space_vec = vec![
DiskSpace {
free_space: 1111,
@@ -301,7 +302,7 @@ mod test {
RadarrEvent::GetStatus.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap())
as DateTime<Utc>;
@@ -385,7 +386,7 @@ mod test {
.movies
.sorting(vec![title_sort_option]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Movies(movies) = network
.handle_radarr_event(RadarrEvent::GetMovies)
@@ -440,7 +441,7 @@ mod test {
.radarr_data
.movies
.sorting(vec![title_sort_option]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetMovies)
@@ -493,7 +494,7 @@ mod test {
.movies
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Releases(releases_vec) = network
.handle_radarr_event(RadarrEvent::GetReleases(None))
@@ -551,7 +552,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetReleases(None))
@@ -610,7 +611,7 @@ mod test {
)
.await;
app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::AddMovieSearchResults(add_movie_search_results) = network
.handle_radarr_event(RadarrEvent::SearchNewMovie(None))
@@ -676,7 +677,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::AddMovieSearchResults(add_movie_search_results) = network
.handle_radarr_event(RadarrEvent::SearchNewMovie(Some("test term".into())))
@@ -711,7 +712,7 @@ mod test {
task_name: TaskName::default(),
..Task::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::StartTask(None))
@@ -736,7 +737,7 @@ mod test {
RadarrEvent::StartTask(None).resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::StartTask(Some(TaskName::default())))
@@ -757,7 +758,7 @@ mod test {
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Get, None, Some(json!([])), None, &resource).await;
app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::SearchNewMovie(None))
@@ -804,10 +805,12 @@ mod test {
host,
port,
api_token: "test1234".to_owned(),
use_ssl: false,
ssl_cert_path: None,
};
app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app));
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::SearchNewMovie(None))
@@ -886,7 +889,7 @@ mod test {
.radarr_data
.indexers
.set_items(vec![indexer()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::TestIndexer(None))
@@ -954,7 +957,7 @@ mod test {
.radarr_data
.indexers
.set_items(vec![indexer()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::TestIndexer(None))
@@ -1015,7 +1018,7 @@ mod test {
.with_body("{}")
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Value(value) = network
.handle_radarr_event(RadarrEvent::TestIndexer(Some(1)))
@@ -1092,7 +1095,7 @@ mod test {
.radarr_data
.indexers
.set_items(indexers);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::IndexerTestResults(results) = network
.handle_radarr_event(RadarrEvent::TestAllIndexers)
@@ -1143,7 +1146,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(None))
@@ -1166,7 +1169,7 @@ mod test {
RadarrEvent::TriggerAutomaticSearch(None).resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(Some(1)))
@@ -1196,7 +1199,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateAndScan(None))
@@ -1219,7 +1222,7 @@ mod test {
RadarrEvent::UpdateAndScan(None).resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateAndScan(Some(1)))
@@ -1242,7 +1245,7 @@ mod test {
RadarrEvent::UpdateAllMovies.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateAllMovies)
@@ -1264,7 +1267,7 @@ mod test {
RadarrEvent::UpdateDownloads.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateDownloads)
@@ -1286,7 +1289,7 @@ mod test {
RadarrEvent::UpdateCollections.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::UpdateCollections)
@@ -1317,7 +1320,7 @@ mod test {
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.quality_profile_map =
BiMap::from_iter([(2222, "HD - 1080p".to_owned())]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Movie(movie) = network
.handle_radarr_event(RadarrEvent::GetMovieDetails(None))
@@ -1402,7 +1405,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Movie(movie) = network
.handle_radarr_event(RadarrEvent::GetMovieDetails(Some(1)))
@@ -1456,7 +1459,7 @@ mod test {
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.quality_profile_map =
BiMap::from_iter([(2222, "HD - 1080p".to_owned())]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetMovieDetails(None))
@@ -1530,7 +1533,7 @@ mod test {
.movies
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::MovieHistoryItems(history) = network
.handle_radarr_event(RadarrEvent::GetMovieHistory(None))
@@ -1578,7 +1581,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::MovieHistoryItems(history) = network
.handle_radarr_event(RadarrEvent::GetMovieHistory(Some(1)))
@@ -1618,7 +1621,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetMovieHistory(None))
@@ -1767,7 +1770,7 @@ mod test {
.blocklist
.sorting(vec![blocklist_sort_option]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::BlocklistResponse(blocklist) = network
.handle_radarr_event(RadarrEvent::GetBlocklist)
@@ -1888,7 +1891,7 @@ mod test {
.radarr_data
.blocklist
.sorting(vec![blocklist_sort_option]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetBlocklist)
@@ -2011,7 +2014,7 @@ mod test {
.collections
.sorting(vec![collection_sort_option]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Collections(collections) = network
.handle_radarr_event(RadarrEvent::GetCollections)
@@ -2118,7 +2121,7 @@ mod test {
.radarr_data
.collections
.sorting(vec![collection_sort_option]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetCollections)
@@ -2162,7 +2165,7 @@ mod test {
RadarrEvent::GetDownloads.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::DownloadsResponse(downloads) = network
.handle_radarr_event(RadarrEvent::GetDownloads)
@@ -2186,7 +2189,10 @@ mod test {
"urlBase": "some.test.site/radarr",
"instanceName": "Radarr",
"applicationUrl": "https://some.test.site:7878/radarr",
"enableSsl": true
"enableSsl": true,
"sslPort": 9898,
"sslCertPath": "/app/radarr.pfx",
"sslCertPassword": "test"
});
let response: HostConfig = serde_json::from_value(host_config_response.clone()).unwrap();
let (async_server, app_arc, _server) = mock_radarr_api(
@@ -2197,7 +2203,7 @@ mod test {
RadarrEvent::GetHostConfig.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::HostConfig(host_config) = network
.handle_radarr_event(RadarrEvent::GetHostConfig)
@@ -2250,7 +2256,7 @@ mod test {
RadarrEvent::GetIndexers.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Indexers(indexers) = network
.handle_radarr_event(RadarrEvent::GetIndexers)
@@ -2289,7 +2295,7 @@ mod test {
RadarrEvent::GetAllIndexerSettings.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::IndexerSettings(settings) = network
.handle_radarr_event(RadarrEvent::GetAllIndexerSettings)
@@ -2327,7 +2333,7 @@ mod test {
)
.await;
app_arc.lock().await.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetAllIndexerSettings)
@@ -2374,7 +2380,7 @@ mod test {
RadarrEvent::GetQueuedEvents.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::QueueEvents(events) = network
.handle_radarr_event(RadarrEvent::GetQueuedEvents)
@@ -2435,7 +2441,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::LogResponse(logs) = network
.handle_radarr_event(RadarrEvent::GetLogs(None))
@@ -2505,7 +2511,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::LogResponse(logs) = network
.handle_radarr_event(RadarrEvent::GetLogs(Some(1000)))
@@ -2546,7 +2552,7 @@ mod test {
RadarrEvent::GetQualityProfiles.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::QualityProfiles(quality_profiles) = network
.handle_radarr_event(RadarrEvent::GetQualityProfiles)
@@ -2577,7 +2583,7 @@ mod test {
RadarrEvent::GetTags.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Tags(tags) = network
.handle_radarr_event(RadarrEvent::GetTags)
@@ -2639,7 +2645,7 @@ mod test {
RadarrEvent::GetTasks.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Tasks(tasks) = network
.handle_radarr_event(RadarrEvent::GetTasks)
@@ -2732,7 +2738,7 @@ mod test {
RadarrEvent::GetUpdates.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Updates(updates) = network
.handle_radarr_event(RadarrEvent::GetUpdates)
@@ -2762,7 +2768,7 @@ mod test {
.await;
app_arc.lock().await.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Tag(tag) = network
.handle_radarr_event(RadarrEvent::AddTag("testing".to_owned()))
@@ -2787,7 +2793,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteTag(1).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteTag(1))
@@ -2814,7 +2820,7 @@ mod test {
RadarrEvent::GetRootFolders.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::RootFolders(root_folders) = network
.handle_radarr_event(RadarrEvent::GetRootFolders)
@@ -2850,7 +2856,7 @@ mod test {
RadarrEvent::GetSecurityConfig.resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::SecurityConfig(security_config) = network
.handle_radarr_event(RadarrEvent::GetSecurityConfig)
@@ -2898,7 +2904,7 @@ mod test {
.movies
.set_items(vec![movie()]);
app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Credits(credits) = network
.handle_radarr_event(RadarrEvent::GetMovieCredits(None))
@@ -2943,7 +2949,7 @@ mod test {
&resource,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let RadarrSerdeable::Credits(credits) = network
.handle_radarr_event(RadarrEvent::GetMovieCredits(Some(1)))
@@ -2989,7 +2995,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::GetMovieCredits(None))
@@ -3018,7 +3024,7 @@ mod test {
app.data.radarr_data.delete_movie_files = true;
app.data.radarr_data.add_list_exclusion = true;
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteMovie(None))
@@ -3038,7 +3044,7 @@ mod test {
);
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let delete_movie_params = DeleteMovieParams {
id: 1,
delete_movie_files: true,
@@ -3087,7 +3093,7 @@ mod test {
.radarr_data
.blocklist
.set_items(blocklist_items);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::ClearBlocklist)
@@ -3109,7 +3115,7 @@ mod test {
.radarr_data
.blocklist
.set_items(vec![blocklist_item()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteBlocklistItem(None))
@@ -3124,7 +3130,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem(None).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteBlocklistItem(Some(1)))
@@ -3146,7 +3152,7 @@ mod test {
.radarr_data
.downloads
.set_items(vec![download_record()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteDownload(None))
@@ -3161,7 +3167,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteDownload(None).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteDownload(Some(1)))
@@ -3183,7 +3189,7 @@ mod test {
.radarr_data
.indexers
.set_items(vec![indexer()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteIndexer(None))
@@ -3198,7 +3204,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteIndexer(None).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteIndexer(Some(1)))
@@ -3220,7 +3226,7 @@ mod test {
.radarr_data
.root_folders
.set_items(vec![root_folder()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteRootFolder(None))
@@ -3235,7 +3241,7 @@ mod test {
let resource = format!("{}/1", RadarrEvent::DeleteRootFolder(None).resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DeleteRootFolder(Some(1)))
@@ -3319,7 +3325,7 @@ mod test {
app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
}
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddMovie(None))
@@ -3372,7 +3378,7 @@ mod test {
},
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddMovie(Some(body)))
@@ -3458,7 +3464,7 @@ mod test {
add_searched_movies.scroll_to_bottom();
app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddMovie(None))
@@ -3502,7 +3508,7 @@ mod test {
.await;
app_arc.lock().await.data.radarr_data.edit_root_folder = Some("/nfs/test".into());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddRootFolder(None))
@@ -3532,7 +3538,7 @@ mod test {
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::AddRootFolder(Some("/test/test".to_owned())))
@@ -3572,7 +3578,7 @@ mod test {
.await;
app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings());
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditAllIndexerSettings(None))
@@ -3611,7 +3617,7 @@ mod test {
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditAllIndexerSettings(
@@ -3709,7 +3715,7 @@ mod test {
app.data.radarr_data.quality_profile_map =
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditCollection(None))
@@ -3794,7 +3800,7 @@ mod test {
root_folder_path: Some("/nfs/Test Path".to_owned()),
search_on_add: Some(false),
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditCollection(Some(edit_collection_params)))
@@ -3873,7 +3879,7 @@ mod test {
collection_id: 123,
..EditCollectionParams::default()
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditCollection(Some(edit_collection_params)))
@@ -3969,7 +3975,7 @@ mod test {
app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal);
app.data.radarr_data.indexers.set_items(vec![indexer()]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(None))
@@ -4070,7 +4076,7 @@ mod test {
);
app.data.radarr_data.indexers.set_items(vec![indexer]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(None))
@@ -4185,7 +4191,7 @@ mod test {
);
app.data.radarr_data.indexers.set_items(vec![indexer]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(None))
@@ -4280,7 +4286,7 @@ mod test {
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)))
@@ -4340,7 +4346,7 @@ mod test {
.match_body(Matcher::Json(indexer_details_json))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)))
@@ -4425,7 +4431,7 @@ mod test {
.match_body(Matcher::Json(expected_edit_indexer_body))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)))
@@ -4488,7 +4494,7 @@ mod test {
app.data.radarr_data.quality_profile_map =
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditMovie(None))
@@ -4539,7 +4545,7 @@ mod test {
tags: Some(vec![1, 2]),
..EditMovieParams::default()
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params)))
@@ -4576,7 +4582,7 @@ mod test {
movie_id: 1,
..EditMovieParams::default()
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params)))
@@ -4617,7 +4623,7 @@ mod test {
clear_tags: true,
..EditMovieParams::default()
};
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params)))
@@ -4654,7 +4660,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![movie()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_radarr_event(RadarrEvent::DownloadRelease(None))
@@ -4678,7 +4684,7 @@ mod test {
RadarrEvent::DownloadRelease(None).resource(),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let params = ReleaseDownloadBody {
guid: "1234".to_owned(),
indexer_id: 2,
@@ -4705,7 +4711,7 @@ mod test {
(3, "hi".to_owned()),
]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(
network.extract_and_add_tag_ids_vec(tags).await,
@@ -4733,7 +4739,7 @@ mod test {
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let tag_ids_vec = network.extract_and_add_tag_ids_vec(tags).await;
@@ -4762,7 +4768,7 @@ mod test {
id: 1,
..Movie::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(network.extract_movie_id().await, 1);
}
@@ -4776,7 +4782,7 @@ mod test {
..Movie::default()
}]);
app_arc.lock().await.data.radarr_data.movies = filtered_movies;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(network.extract_movie_id().await, 1);
}
@@ -4794,7 +4800,7 @@ mod test {
id: 1,
..Collection::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(network.extract_collection_id().await, 1);
}
@@ -4808,7 +4814,7 @@ mod test {
..Collection::default()
}]);
app_arc.lock().await.data.radarr_data.collections = filtered_collections;
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_eq!(network.extract_collection_id().await, 1);
}
@@ -4826,7 +4832,7 @@ mod test {
id: 1,
..Movie::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_str_eq!(
network.append_movie_id_param("/test", None).await,
@@ -4847,7 +4853,7 @@ mod test {
id: 1,
..Movie::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert_str_eq!(
network.append_movie_id_param("/test", Some(11)).await,
@@ -4858,7 +4864,7 @@ mod test {
#[tokio::test]
async fn test_radarr_request_props_from_default_radarr_config() {
let app_arc = Arc::new(Mutex::new(App::default()));
let network = Network::new(&app_arc, CancellationToken::new());
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let request_props = network
.radarr_request_props_from("/test", RequestMethod::Get, None::<()>)
@@ -4873,6 +4879,8 @@ mod test {
host: "192.168.0.123".to_owned(),
port: Some(8080),
api_token: "testToken1234".to_owned(),
use_ssl: false,
ssl_cert_path: None,
};
}
@@ -4884,14 +4892,16 @@ mod test {
host: "192.168.0.123".to_owned(),
port: Some(8080),
api_token: api_token.clone(),
use_ssl: true,
ssl_cert_path: None,
};
let network = Network::new(&app_arc, CancellationToken::new());
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let request_props = network
.radarr_request_props_from("/test", RequestMethod::Get, None::<()>)
.await;
assert_str_eq!(request_props.uri, "http://192.168.0.123:8080/api/v3/test");
assert_str_eq!(request_props.uri, "https://192.168.0.123:8080/api/v3/test");
assert_eq!(request_props.method, RequestMethod::Get);
assert_eq!(request_props.body, None);
assert_str_eq!(request_props.api_token, api_token);
@@ -4987,6 +4997,8 @@ mod test {
host,
port,
api_token: "test1234".to_owned(),
use_ssl: false,
ssl_cert_path: None,
};
app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app));
+4 -4
View File
@@ -1,6 +1,6 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text};
use ratatui::widgets::Clear;
@@ -83,7 +83,7 @@ fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.select(app.server_tabs.index);
let help = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Right);
.right_aligned();
f.render_widget(tabs, tabs_area);
f.render_widget(help, help_area);
@@ -170,7 +170,7 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -
.select(tab_state.index);
let help = Paragraph::new(Text::from(tab_state.get_active_tab_help().help()))
.block(borderless_block())
.alignment(Alignment::Right);
.right_aligned();
f.render_widget(tabs, tabs_area);
f.render_widget(help, help_area);
@@ -197,7 +197,7 @@ pub fn draw_input_box_popup(
let help = Paragraph::new("<esc> cancel")
.help()
.alignment(Alignment::Center)
.centered()
.block(borderless_block());
f.render_widget(help, help_area);
}
@@ -259,7 +259,7 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.wrap(Wrap { trim: false });
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
.centered();
f.render_widget(paragraph, paragraph_area);
f.render_widget(help_paragraph, help_area);
@@ -1,9 +1,12 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::ListItem;
use ratatui::text::Text;
use ratatui::widgets::{ListItem, Paragraph};
use ratatui::Frame;
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{
@@ -110,7 +113,7 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
let selected_minimum_availability = minimum_availability_list.current_selection();
let selected_quality_profile = quality_profile_list.current_selection();
let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, root_folder_area, search_on_add_area, _, buttons_area] =
let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, root_folder_area, search_on_add_area, _, buttons_area, help_area] =
Layout::vertical([
Constraint::Length(6),
Constraint::Length(3),
@@ -118,8 +121,9 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
@@ -127,6 +131,8 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area);
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
let prompt_paragraph = layout_paragraph_borderless(&collection_overview);
let monitored_checkbox = Checkbox::new("Monitored")
.highlighted(selected_block == &ActiveRadarrBlock::EditCollectionToggleMonitored)
@@ -169,6 +175,7 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
f.render_widget(search_on_add_checkbox, search_on_add_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
}
fn draw_edit_collection_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
+17 -8
View File
@@ -1,5 +1,7 @@
use std::sync::atomic::Ordering;
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::Route;
@@ -14,6 +16,8 @@ use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::popup::Size;
use crate::ui::{draw_popup_over, DrawUi};
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::Paragraph;
use ratatui::Frame;
#[cfg(test)]
@@ -50,32 +54,36 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let highlight_yes_no = selected_block == &ActiveRadarrBlock::EditIndexerConfirmPrompt;
let edit_indexer_modal_option = &app.data.radarr_data.edit_indexer_modal;
let protocol = &app.data.radarr_data.indexers.current_selection().protocol;
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
if edit_indexer_modal_option.is_some() {
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
let [settings_area, buttons_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(3)])
.margin(1)
.areas(area);
let [settings_area, _, buttons_area, help_area] = Layout::vertical([
Constraint::Length(15),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let [left_side_area, right_side_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.margin(1)
.areas(settings_area);
let [name_area, rss_area, auto_search_area, interactive_search_area, _] = Layout::vertical([
let [name_area, rss_area, auto_search_area, interactive_search_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
])
.areas(left_side_area);
let [url_area, api_key_area, seed_ratio_area, tags_area, _] = Layout::vertical([
let [url_area, api_key_area, seed_ratio_area, tags_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
])
.areas(right_side_area);
@@ -161,6 +169,7 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(interactive_search_checkbox, interactive_search_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
}
} else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
@@ -1,8 +1,12 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
@@ -52,33 +56,37 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
let selected_block = app.data.radarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == &ActiveRadarrBlock::IndexerSettingsConfirmPrompt;
let indexer_settings_option = &app.data.radarr_data.indexer_settings;
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
if indexer_settings_option.is_some() {
let indexer_settings = indexer_settings_option.as_ref().unwrap();
let [settings_area, buttons_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(3)])
.margin(1)
.areas(area);
let [settings_area, _, buttons_area, help_area] = Layout::vertical([
Constraint::Length(15),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let [left_side_area, right_side_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.margin(1)
.areas(settings_area);
let [min_age_area, retention_area, max_size_area, prefer_flags_area, _] = Layout::vertical([
let [min_age_area, retention_area, max_size_area, prefer_flags_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
])
.areas(left_side_area);
let [availability_delay_area, rss_sync_interval_area, whitelisted_sub_tags_area, allow_hardcoded_subs_area, _] =
let [availability_delay_area, rss_sync_interval_area, whitelisted_sub_tags_area, allow_hardcoded_subs_area] =
Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
])
.areas(right_side_area);
@@ -162,6 +170,7 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
f.render_widget(allow_hardcoded_subs_checkbox, allow_hardcoded_subs_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
} else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
+13 -7
View File
@@ -1,12 +1,14 @@
use std::sync::atomic::Ordering;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
use ratatui::Frame;
use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES};
use crate::app::radarr::radarr_context_clues::ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES;
use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
};
use crate::models::radarr_models::AddMovieSearchResult;
use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
@@ -209,7 +211,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
.centered();
search_box.show_cursor(f, search_box_area);
f.render_widget(layout_block(), results_area);
@@ -220,7 +222,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
.centered();
let error_message = Message::new("No movies found matching your query!");
let error_message_popup = Popup::new(error_message).size(Size::Message);
@@ -240,7 +242,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Text::from(build_context_clue_string(&ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
.centered();
let search_results_table = ManagarrTable::new(
app.data.radarr_data.add_searched_movies.as_mut(),
search_results_row_mapping,
@@ -369,7 +371,7 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(title_block_centered(&title), area);
let [paragraph_area, root_folder_area, monitor_area, min_availability_area, quality_profile_area, tags_area, _, buttons_area] =
let [paragraph_area, root_folder_area, monitor_area, min_availability_area, quality_profile_area, tags_area, _, buttons_area, help_area] =
Layout::vertical([
Constraint::Length(6),
Constraint::Length(3),
@@ -377,14 +379,18 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let prompt_paragraph = layout_paragraph_borderless(&prompt);
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
f.render_widget(prompt_paragraph, paragraph_area);
f.render_widget(help_paragraph, help_area);
let [add_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
+10 -3
View File
@@ -2,9 +2,12 @@ use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::Layout;
use ratatui::widgets::ListItem;
use ratatui::text::Text;
use ratatui::widgets::{ListItem, Paragraph};
use ratatui::Frame;
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{
@@ -113,7 +116,7 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
let selected_minimum_availability = minimum_availability_list.current_selection();
let selected_quality_profile = quality_profile_list.current_selection();
let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, path_area, tags_area, _, buttons_area] =
let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, path_area, tags_area, _, buttons_area, help_area] =
Layout::vertical([
Constraint::Length(6),
Constraint::Length(3),
@@ -121,8 +124,9 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
@@ -130,6 +134,8 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area);
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
let prompt_paragraph = layout_paragraph_borderless(&movie_overview);
let monitored_checkbox = Checkbox::new("Monitored")
.checked(monitored.unwrap_or_default())
@@ -181,6 +187,7 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
f.render_widget(quality_profile_drop_down_button, quality_profile_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
}
fn draw_edit_movie_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
+2 -2
View File
@@ -1,6 +1,6 @@
use std::iter;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
@@ -510,7 +510,7 @@ fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) {
let content_paragraph = Paragraph::new(lines_vec)
.block(borderless_block())
.wrap(Wrap { trim: false })
.alignment(Alignment::Left);
.left_aligned();
let confirmation_prompt = ConfirmationPrompt::new()
.title(title)
.prompt(&prompt)
+7 -3
View File
@@ -1,7 +1,7 @@
use std::iter;
use chrono::{Duration, Utc};
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::Stylize;
use ratatui::text::Text;
use ratatui::widgets::{Paragraph, Row};
@@ -192,7 +192,11 @@ fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
size,
..
} = &downloads_vec[i];
let percent = 1f64 - (*sizeleft as f64 / *size as f64);
let percent = if *size == 0 {
0.0
} else {
1f64 - (*sizeleft as f64 / *size as f64)
};
let download_gauge = line_gauge_with_title(title, percent);
f.render_widget(download_gauge, download_item_areas[i]);
@@ -244,6 +248,6 @@ fn draw_radarr_logo(f: &mut Frame<'_>, area: Rect) {
let logo = Paragraph::new(logo_text)
.light_yellow()
.block(layout_block().default())
.alignment(Alignment::Center);
.centered();
f.render_widget(logo, area);
}
+2 -2
View File
@@ -1,7 +1,7 @@
use std::ops::Sub;
use chrono::Utc;
use ratatui::layout::{Alignment, Layout};
use ratatui::layout::Layout;
use ratatui::style::Style;
use ratatui::text::{Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row};
@@ -204,7 +204,7 @@ fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
);
let help_paragraph = Paragraph::new(help_text)
.block(layout_block_top_border())
.alignment(Alignment::Left);
.left_aligned();
f.render_widget(help_paragraph, area);
}
+1 -1
View File
@@ -43,7 +43,7 @@ pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> {
.primary()
.bold()
.wrap(Wrap { trim: false })
.alignment(Alignment::Center)
.centered()
}
pub fn borderless_block<'a>() -> Block<'a> {
+5 -5
View File
@@ -1,7 +1,7 @@
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{layout_block, style_block_highlight};
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::prelude::{Style, Text, Widget};
use ratatui::style::Styled;
use ratatui::widgets::Paragraph;
@@ -58,11 +58,11 @@ impl<'a> Button<'a> {
if let Some(icon) = self.icon {
layout_block().style(style).render(area, buf);
Paragraph::new(Text::from(self.title))
.alignment(Alignment::Left)
.left_aligned()
.style(style)
.render(title_area, buf);
Paragraph::new(Text::from(format!("{icon} ")))
.alignment(Alignment::Right)
.right_aligned()
.style(style)
.render(icon_area, buf);
}
@@ -72,7 +72,7 @@ impl<'a> Button<'a> {
let [label_area, button_area] =
Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area);
let label_paragraph = Paragraph::new(Text::from(format!("\n{}: ", self.label.unwrap())))
.alignment(Alignment::Right)
.right_aligned()
.primary();
if self.icon.is_some() {
@@ -87,7 +87,7 @@ impl<'a> Button<'a> {
fn render_button(self, area: Rect, buf: &mut Buffer) {
Paragraph::new(Text::from(self.title))
.block(layout_block())
.alignment(Alignment::Center)
.centered()
.style(style_block_highlight(self.is_selected))
.render(area, buf);
}
+3 -3
View File
@@ -1,7 +1,7 @@
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{borderless_block, layout_block, style_block_highlight};
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::Text;
use ratatui::style::Stylize;
use ratatui::widgets::{Paragraph, Widget};
@@ -47,13 +47,13 @@ impl<'a> Checkbox<'a> {
Paragraph::new(Text::from(format!("\n{}: ", self.label)))
.block(borderless_block())
.alignment(Alignment::Right)
.right_aligned()
.primary()
.render(label_area, buf);
Paragraph::new(Text::from(check))
.block(layout_block())
.alignment(Alignment::Center)
.centered()
.style(style_block_highlight(self.is_highlighted).bold())
.render(checkbox_box_area, buf);
}
+27 -8
View File
@@ -1,9 +1,12 @@
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{layout_paragraph_borderless, title_block_centered};
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::{Paragraph, Widget};
use std::iter;
@@ -64,12 +67,16 @@ impl<'a> ConfirmationPrompt<'a> {
fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) {
title_block_centered(self.title).render(area, buf);
let help_text =
Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
if let Some(checkboxes) = self.checkboxes {
let mut constraints = vec![
Constraint::Length(4),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
];
constraints.splice(
1..1,
@@ -81,6 +88,7 @@ impl<'a> ConfirmationPrompt<'a> {
.areas(chunks[checkboxes.len() + 2]);
layout_paragraph_borderless(self.prompt).render(chunks[0], buf);
help_paragraph.render(chunks[checkboxes.len() + 3], buf);
checkboxes
.into_iter()
@@ -102,26 +110,37 @@ impl<'a> ConfirmationPrompt<'a> {
fn render_confirmation_prompt(self, area: Rect, buf: &mut Buffer) {
title_block_centered(self.title).render(area, buf);
let help_text =
Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text).centered();
let [prompt_area, buttons_area] = if let Some(content_paragraph) = self.content {
let [prompt_area, content_area, _, buttons_area] = Layout::vertical([
let [prompt_area, content_area, _, buttons_area, help_area] = Layout::vertical([
Constraint::Length(4),
Constraint::Length(7),
Constraint::Fill(0),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
content_paragraph.render(content_area, buf);
help_paragraph.render(help_area, buf);
[prompt_area, buttons_area]
} else {
let [prompt_area, buttons_area] =
Layout::vertical([Constraint::Percentage(72), Constraint::Length(3)])
.margin(1)
.flex(Flex::SpaceBetween)
.areas(area);
let [prompt_area, buttons_area, _, help_area] = Layout::vertical([
Constraint::Percentage(72),
Constraint::Length(3),
Constraint::Fill(0),
Constraint::Min(1),
])
.margin(1)
.flex(Flex::SpaceBetween)
.areas(area);
help_paragraph.render(help_area, buf);
[prompt_area, buttons_area]
};
+2 -2
View File
@@ -1,5 +1,5 @@
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Layout, Position, Rect};
use ratatui::layout::{Constraint, Layout, Position, Rect};
use ratatui::prelude::Text;
use ratatui::style::{Style, Styled, Stylize};
use ratatui::widgets::{Block, Paragraph, Widget};
@@ -114,7 +114,7 @@ impl<'a> InputBox<'a> {
Paragraph::new(Text::from(format!("\n{label}: ")))
.block(borderless_block())
.alignment(Alignment::Right)
.right_aligned()
.primary()
.render(label_area, buf);
input_box_paragraph.render(text_box_area, buf);
+3 -3
View File
@@ -1,7 +1,7 @@
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{background_block, centered_rect, layout_block_top_border};
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::Text;
use ratatui::widgets::{Block, Clear, Paragraph, Widget};
@@ -27,7 +27,7 @@ impl Size {
pub fn to_percent(&self) -> (u16, u16) {
match self {
Size::SmallPrompt => (20, 20),
Size::Prompt => (35, 35),
Size::Prompt => (37, 37),
Size::LargePrompt => (70, 45),
Size::Message => (25, 8),
Size::NarrowMessage => (50, 20),
@@ -100,7 +100,7 @@ impl<'a, T: Widget> Popup<'a, T> {
Paragraph::new(Text::from(format!(" {footer}").help()))
.block(layout_block_top_border())
.alignment(Alignment::Left)
.left_aligned()
.render(help_footer_area, buf);
content_area
+1 -1
View File
@@ -7,7 +7,7 @@ mod tests {
#[test]
fn test_dimensions_to_percent() {
assert_eq!(Size::SmallPrompt.to_percent(), (20, 20));
assert_eq!(Size::Prompt.to_percent(), (35, 35));
assert_eq!(Size::Prompt.to_percent(), (37, 37));
assert_eq!(Size::LargePrompt.to_percent(), (70, 45));
assert_eq!(Size::Message.to_percent(), (25, 8));
assert_eq!(Size::NarrowMessage.to_percent(), (50, 20));