diff --git a/Cargo.toml b/Cargo.toml index 1031e38..e8878e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.1.11" +version = "0.0.12" authors = ["Alex Clarke "] description = "A TUI for managing *arr servers" keywords = ["managarr", "tui-rs", "dashboard", "servarr"] @@ -12,11 +12,13 @@ edition = "2021" [dependencies] anyhow = "1.0.68" +backtrace = "0.3.67" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.0.30", features = ["help", "usage", "error-context", "derive"] } confy = { version = "0.5.1", default_features = false, features = ["yaml_conf"] } crossterm = "0.26.1" derivative = "2.2.0" +human-panic = "1.1.3" indoc = "2.0.0" log = "0.4.17" log4rs = { version = "1.2.0", features = ["file_appender"] } diff --git a/src/main.rs b/src/main.rs index bd36d6b..3529d45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ -extern crate core; +#![warn(rust_2018_idioms)] -use std::io; +use std::panic::PanicInfo; use std::sync::Arc; +use std::{io, panic}; use anyhow::Result; use clap::Parser; @@ -36,6 +37,9 @@ struct Cli {} #[tokio::main] async fn main() -> Result<()> { log4rs::init_config(utils::init_logging_config())?; + panic::set_hook(Box::new(|info| { + panic_hook(info); + })); Cli::parse(); let config = confy::load("managarr", "config")?; @@ -105,3 +109,48 @@ async fn start_ui(app: &Arc>) -> Result<()> { Ok(()) } + +#[cfg(debug_assertions)] +fn panic_hook(info: &PanicInfo<'_>) { + use backtrace::Backtrace; + use crossterm::style::Print; + + let location = info.location().unwrap(); + + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match info.payload().downcast_ref::() { + Some(s) => &s[..], + None => "Box", + }, + }; + + let stacktrace: String = format!("{:?}", Backtrace::new()).replace('\n', "\n\r"); + + disable_raw_mode().unwrap(); + execute!( + io::stdout(), + LeaveAlternateScreen, + Print(format!( + "thread '' panicked at '{}', {}\n\r{}", + msg, location, stacktrace + )), + ) + .unwrap(); +} + +#[cfg(not(debug_assertions))] +fn panic_hook(info: &PanicInfo<'_>) { + use human_panic::{handle_dump, print_msg, Metadata}; + + let meta = Metadata { + version: env!("CARGO_PKG_VERSION").into(), + name: env!("CARGO_PKG_NAME").into(), + authors: env!("CARGO_PKG_AUTHORS").replace(":", ", ").into(), + homepage: env!("CARGO_PKG_HOMEPAGE").into(), + }; + let file_path = handle_dump(&meta, info); + disable_raw_mode().unwrap(); + execute!(io::stdout(), LeaveAlternateScreen).unwrap(); + print_msg(file_path, &meta).expect("human-panic: printing error message to console failed"); +} diff --git a/src/network/mod.rs b/src/network/mod.rs index 8fb9fbb..4d944fe 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -43,7 +43,7 @@ impl<'a> Network<'a> { pub async fn handle_request( &self, request_props: RequestProps, - mut app_update_fn: impl FnMut(R, MutexGuard), + mut app_update_fn: impl FnMut(R, MutexGuard<'_, App>), ) where T: Serialize + Default + Debug, R: DeserializeOwned, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c70244d..a599041 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -13,12 +13,12 @@ use tui::Frame; use crate::app::App; use crate::models::{Route, StatefulList, StatefulTable, TabState}; use crate::ui::utils::{ - borderless_block, centered_rect, horizontal_chunks, horizontal_chunks_with_margin, layout_block, - layout_block_top_border, layout_button_paragraph, layout_button_paragraph_borderless, - layout_paragraph_borderless, logo_block, show_cursor, style_block_highlight, style_default, - style_default_bold, style_failure, style_help, style_highlight, style_primary, style_secondary, - style_system_function, title_block, title_block_centered, vertical_chunks, - vertical_chunks_with_margin, + background_block, borderless_block, centered_rect, horizontal_chunks, + horizontal_chunks_with_margin, layout_block, layout_block_top_border, layout_button_paragraph, + layout_button_paragraph_borderless, layout_paragraph_borderless, logo_block, show_cursor, + style_block_highlight, style_default, style_default_bold, style_failure, style_help, + style_highlight, style_primary, style_secondary, style_system_function, title_block, + title_block_centered, vertical_chunks, vertical_chunks_with_margin, }; mod radarr_ui; @@ -26,7 +26,8 @@ mod utils; static HIGHLIGHT_SYMBOL: &str = "=> "; -pub fn ui(f: &mut Frame, app: &mut App) { +pub fn ui(f: &mut Frame<'_, B>, app: &mut App) { + f.render_widget(background_block(), f.size()); let main_chunks = if !app.error.text.is_empty() { let chunks = vertical_chunks_with_margin( vec![ @@ -115,6 +116,7 @@ pub fn draw_popup( ) { let popup_area = centered_rect(percent_x, percent_y, f.size()); f.render_widget(Clear, popup_area); + f.render_widget(background_block(), popup_area); popup_fn(f, app, popup_area); } @@ -188,6 +190,31 @@ fn draw_context_row(f: &mut Frame<'_, B>, app: &App, area: Rect) { } } +pub fn draw_error_popup_over( + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, + message: &str, + background_fn: fn(&mut Frame<'_, B>, &mut App, Rect), +) { + background_fn(f, app, area); + draw_error_popup(f, message); +} + +pub fn draw_error_popup(f: &mut Frame<'_, B>, message: &str) { + let prompt_area = centered_rect(25, 8, f.size()); + f.render_widget(Clear, prompt_area); + f.render_widget(background_block(), prompt_area); + + let error_message = Paragraph::new(Text::from(message)) + .block(title_block_centered("Error").style(style_failure())) + .style(style_failure().add_modifier(Modifier::BOLD)) + .wrap(Wrap { trim: false }) + .alignment(Alignment::Center); + + f.render_widget(error_message, prompt_area); +} + fn draw_tabs<'a, B: Backend>( f: &mut Frame<'_, B>, area: Rect, @@ -234,7 +261,7 @@ pub struct TableProps<'a, T> { fn draw_table<'a, B, T, F>( f: &mut Frame<'_, B>, content_area: Rect, - block: Block, + block: Block<'_>, table_props: TableProps<'a, T>, row_mapper: F, is_loading: bool, @@ -302,30 +329,6 @@ pub fn loading(f: &mut Frame<'_, B>, block: Block<'_>, area: Rect, i } } -pub fn draw_error_popup_over( - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, - message: &str, - background_fn: fn(&mut Frame<'_, B>, &mut App, Rect), -) { - background_fn(f, app, area); - draw_error_popup(f, message); -} - -pub fn draw_error_popup(f: &mut Frame<'_, B>, message: &str) { - let prompt_area = centered_rect(25, 8, f.size()); - f.render_widget(Clear, prompt_area); - - let error_message = Paragraph::new(Text::from(message)) - .block(title_block_centered("Error").style(style_failure())) - .style(style_failure().add_modifier(Modifier::BOLD)) - .wrap(Wrap { trim: false }) - .alignment(Alignment::Center); - - f.render_widget(error_message, prompt_area); -} - pub fn draw_prompt_box( f: &mut Frame<'_, B>, prompt_area: Rect, @@ -341,7 +344,7 @@ pub fn draw_prompt_box_with_content( prompt_area: Rect, title: &str, prompt: &str, - content: Option, + content: Option>, yes_no_value: &bool, ) { f.render_widget(title_block_centered(title), prompt_area); diff --git a/src/ui/radarr_ui/movie_details_ui.rs b/src/ui/radarr_ui/movie_details_ui.rs index 055dac5..2e2ef4b 100644 --- a/src/ui/radarr_ui/movie_details_ui.rs +++ b/src/ui/radarr_ui/movie_details_ui.rs @@ -204,7 +204,7 @@ fn draw_movie_details(f: &mut Frame<'_, B>, app: &App, content_area: spans_info_default(title, split[1..].join(":")) }) - .collect::>(), + .collect::>>(), ); text.patch_style(determine_style_from_download_status(download_status)); @@ -508,7 +508,7 @@ fn draw_manual_search_confirm_prompt( .unwrap_or_default() .iter() .map(|item| Spans::from(vec![Span::styled(format!("• {}", item), style_primary())])) - .collect::>(); + .collect::>>(); spans_vec.append(&mut rejections_spans); let content_paragraph = Paragraph::new(spans_vec) diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 43f04eb..9e7cea7 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -5,6 +5,23 @@ use tui::text::{Span, Spans, Text}; use tui::widgets::{Block, BorderType, Borders, LineGauge, Paragraph, Wrap}; use tui::{symbols, Frame}; +pub const COLOR_TEAL: Color = Color::Rgb(35, 50, 55); +// pub const COLOR_CYAN: Color = Color::Rgb(0, 230, 230); +pub const COLOR_CYAN: Color = Color::Cyan; +// pub const COLOR_LIGHT_BLUE: Color = Color::Rgb(138, 196, 255); +pub const COLOR_LIGHT_BLUE: Color = Color::LightBlue; +// pub const COLOR_YELLOW: Color = Color::Rgb(249, 229, 113); +pub const COLOR_YELLOW: Color = Color::Yellow; +// pub const COLOR_GREEN: Color = Color::Rgb(72, 213, 150); +pub const COLOR_GREEN: Color = Color::Green; +// pub const COLOR_RED: Color = Color::Rgb(249, 140, 164); +pub const COLOR_RED: Color = Color::Red; +// pub const COLOR_ORANGE: Color = Color::Rgb(255, 170, 66); +// pub const COLOR_WHITE: Color = Color::Rgb(255, 255, 255); +pub const COLOR_WHITE: Color = Color::White; +// pub const COLOR_MAGENTA: Color = Color::Rgb(139, 0, 139); +pub const COLOR_MAGENTA: Color = Color::Magenta; + pub fn horizontal_chunks(constraints: Vec, area: Rect) -> Vec { layout_with_constraints(constraints) .direction(Direction::Horizontal) @@ -45,6 +62,10 @@ fn layout_with_constraints(constraints: Vec) -> Layout { )) } +pub fn background_block<'a>() -> Block<'a> { + Block::default().style(Style::default().bg(COLOR_TEAL).fg(COLOR_WHITE)) +} + pub fn layout_block<'a>() -> Block<'a> { Block::default() .borders(Borders::ALL) @@ -67,7 +88,11 @@ pub fn layout_block_bottom_border<'a>() -> Block<'a> { Block::default().borders(Borders::BOTTOM) } -pub fn layout_button_paragraph(is_selected: bool, label: &str, alignment: Alignment) -> Paragraph { +pub fn layout_button_paragraph( + is_selected: bool, + label: &str, + alignment: Alignment, +) -> Paragraph<'_> { Paragraph::new(Text::from(label)) .block(layout_block()) .alignment(alignment) @@ -78,14 +103,14 @@ pub fn layout_button_paragraph_borderless( is_selected: bool, label: &str, alignment: Alignment, -) -> Paragraph { +) -> Paragraph<'_> { Paragraph::new(Text::from(label)) .block(borderless_block()) .alignment(alignment) .style(style_block_highlight(is_selected)) } -pub fn layout_paragraph_borderless(string: &str) -> Paragraph { +pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> { Paragraph::new(Text::from(string)) .block(borderless_block()) .style(style_primary().add_modifier(Modifier::BOLD)) @@ -131,7 +156,7 @@ pub fn style_highlight() -> Style { } pub fn style_default() -> Style { - Style::default().fg(Color::White) + Style::default().fg(COLOR_WHITE) } pub fn style_default_bold() -> Style { @@ -139,35 +164,35 @@ pub fn style_default_bold() -> Style { } pub fn style_primary() -> Style { - Style::default().fg(Color::Cyan) + Style::default().fg(COLOR_CYAN) } pub fn style_secondary() -> Style { - Style::default().fg(Color::Yellow) + Style::default().fg(COLOR_YELLOW) } pub fn style_system_function() -> Style { - Style::default().fg(Color::Yellow) + Style::default().fg(COLOR_YELLOW) } pub fn style_unmonitored() -> Style { - Style::default().fg(Color::Rgb(91, 87, 87)) + Style::default().fg(COLOR_WHITE) } pub fn style_success() -> Style { - Style::default().fg(Color::Green) + Style::default().fg(COLOR_GREEN) } pub fn style_warning() -> Style { - Style::default().fg(Color::Magenta) + Style::default().fg(COLOR_MAGENTA) } pub fn style_failure() -> Style { - Style::default().fg(Color::Red) + Style::default().fg(COLOR_RED) } pub fn style_help() -> Style { - Style::default().fg(Color::LightBlue) + Style::default().fg(COLOR_LIGHT_BLUE) } pub fn style_block_highlight(is_selected: bool) -> Style { @@ -223,19 +248,19 @@ pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { .split(popup_layout[1])[1] } -pub fn line_gauge_with_title(title: &str, ratio: f64) -> LineGauge { +pub fn line_gauge_with_title(title: &str, ratio: f64) -> LineGauge<'_> { LineGauge::default() .block(Block::default().title(title)) - .gauge_style(Style::default().fg(Color::Cyan)) + .gauge_style(Style::default().fg(COLOR_CYAN)) .line_set(symbols::line::THICK) .ratio(ratio) .label(Spans::from(format!("{:.0}%", ratio * 100.0))) } -pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge { +pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> { LineGauge::default() .block(Block::default()) - .gauge_style(Style::default().fg(Color::Cyan)) + .gauge_style(Style::default().fg(COLOR_CYAN)) .line_set(symbols::line::THICK) .ratio(ratio) .label(Spans::from(format!("{}: {:.0}%", title, ratio * 100.0))) @@ -264,8 +289,8 @@ mod test { layout_with_constraints, logo_block, spans_info_default, spans_info_primary, spans_info_with_style, style_block_highlight, style_bold, style_default, style_default_bold, style_failure, style_help, style_highlight, style_primary, style_secondary, style_success, - style_system_function, style_warning, title_block, title_block_centered, title_style, - vertical_chunks, vertical_chunks_with_margin, + style_system_function, style_unmonitored, style_warning, title_block, title_block_centered, + title_style, vertical_chunks, vertical_chunks_with_margin, }; #[test] @@ -498,6 +523,11 @@ mod test { ); } + #[test] + fn test_style_unmonitored() { + assert_eq!(style_unmonitored(), Style::default().fg(Color::White)); + } + #[test] fn test_style_default() { assert_eq!(style_default(), Style::default().fg(Color::White));