Compare commits

..

12 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
54 changed files with 906 additions and 134 deletions
Generated
+1 -1
View File
@@ -1148,7 +1148,7 @@ dependencies = [
[[package]]
name = "managarr"
version = "0.1.5"
version = "0.2.0"
dependencies = [
"anyhow",
"assert_cmd",
+3 -3
View File
@@ -1,9 +1,9 @@
[package]
name = "managarr"
version = "0.1.5"
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"
+8 -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
```
@@ -244,6 +242,13 @@ tautulli:
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"
+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
@@ -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]
+6 -23
View File
@@ -18,7 +18,7 @@ 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;
@@ -46,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!(),
@@ -71,11 +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,
}
#[tokio::main]
@@ -131,7 +127,7 @@ async fn main() -> Result<()> {
std::thread::spawn(move || {
start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client)
});
start_ui(&app, !args.disable_terminal_size_checks).await?;
start_ui(&app).await?;
}
}
@@ -154,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()?;
@@ -247,7 +230,7 @@ fn build_network_client(config: &AppConfig) -> Client {
let mut client_builder = Client::builder();
if config.radarr.use_ssl {
let cert = add_cert_to_builder(config.radarr.ssl_cert_path.clone(), "Radarr");
let cert = create_cert(config.radarr.ssl_cert_path.clone(), "Radarr");
client_builder = client_builder.add_root_certificate(cert);
}
@@ -261,7 +244,7 @@ fn build_network_client(config: &AppConfig) -> Client {
}
}
fn add_cert_to_builder(cert_path: Option<String>, servarr_name: &str) -> Certificate {
fn create_cert(cert_path: Option<String>, servarr_name: &str) -> Certificate {
let err = |error: String| {
error!("{}", error);
eprintln!("error: {}", error.red());
+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));