#[cfg(test)] #[macro_use] extern crate assertables; use anyhow::Result; use clap::{CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version}; use clap_complete::generate; use crossterm::execute; use crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, }; use log::{debug, error, warn}; use network::NetworkTrait; use ratatui::Terminal; use ratatui::backend::CrosstermBackend; use reqwest::Client; use std::panic::PanicHookInfo; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::{io, panic, process}; use tokio::select; use tokio::sync::mpsc::Receiver; use tokio::sync::{Mutex, mpsc}; use tokio_util::sync::CancellationToken; use utils::{ build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs, }; use crate::app::{App, log_and_print_error}; use crate::cli::Command; use crate::event::Key; use crate::event::input_event::{Events, InputEvent}; use crate::network::{Network, NetworkEvent}; use crate::ui::theme::{Theme, ThemeDefinitionsWrapper}; use crate::ui::{THEME, ui}; use crate::utils::load_theme_config; mod app; mod cli; mod event; mod handlers; mod logos; mod models; mod network; mod ui; mod utils; #[derive(Debug, Parser)] #[command( name = crate_name!(), author = crate_authors!(), version = crate_version!(), about = crate_description!(), help_template = "\ {before-help}{name} {version} {author-with-newline} {about-with-newline} {usage-heading} {usage} {all-args}{after-help} " )] struct Cli { #[command(subcommand)] command: Option, #[arg( long, global = true, env = "MANAGARR_DISABLE_SPINNER", help = "Disable the spinner (can sometimes make parsing output challenging)" )] disable_spinner: bool, #[arg( long, global = true, value_parser, env = "MANAGARR_CONFIG_FILE", help = "The Managarr configuration file to use" )] config_file: Option, #[arg( long, global = true, value_parser, env = "MANAGARR_THEMES_FILE", help = "The Managarr themes file to use" )] themes_file: Option, #[arg( long, global = true, value_parser, env = "MANAGARR_THEME", help = "The name of the Managarr theme to use" )] theme: Option, #[arg( long, global = true, help = "For multi-instance configurations, you need to specify the name of the instance configuration that you want to use. This is useful when you have multiple instances of the same Servarr defined in your config file. By default, if left empty, the first configured Servarr instance listed in the config file will be used." )] servarr_name: Option, } #[tokio::main] async fn main() -> Result<()> { log4rs::init_config(utils::init_logging_config())?; panic::set_hook(Box::new(|info| { panic_hook(info); })); let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); let args = Cli::parse(); let mut config = if let Some(ref config_file) = args.config_file { load_config(config_file.to_str().expect("Invalid config file specified"))? } else { confy::load("managarr", "config")? }; let theme_name = config.theme.clone(); let spinner_disabled = args.disable_spinner; debug!("Managarr loaded using config: {config:?}"); config.validate(); config.post_process_initialization(); 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(); ctrlc::set_handler(move || { ctrlc_cancellation_token.cancel(); r.store(false, Ordering::SeqCst); process::exit(1); }) .expect("Error setting Ctrl-C handler"); let app = Arc::new(Mutex::new(App::new( sync_network_tx, config.clone(), cancellation_token.clone(), ))); match args.command { Some(command) => match command { Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) => { if spinner_disabled { start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await; } else { start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await; } } Command::Completions { shell } => { let mut cli = Cli::command(); generate(shell, &mut cli, "managarr", &mut io::stdout()) } Command::TailLogs { no_color } => tail_logs(no_color).await?, }, None => { let app_nw = Arc::clone(&app); std::thread::spawn(move || { start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client) }); start_ui( &app, &args.themes_file, args.theme.unwrap_or(theme_name.unwrap_or_default()), ) .await?; } } Ok(()) } #[tokio::main] async fn start_networking( mut network_rx: Receiver, app: &Arc>>, cancellation_token: CancellationToken, client: Client, ) { let mut network = Network::new(app, cancellation_token, client); loop { select! { Some(network_event) = network_rx.recv() => { if let Err(e) = network.handle_network_event(network_event).await { error!("Encountered an error handling network event: {e:?}"); } } _ = network.cancellation_token.cancelled() => { warn!("Clearing network channel"); while network_rx.try_recv().is_ok() { // Discard the message } network.reset_cancellation_token().await; } } } } async fn start_ui( app: &Arc>>, themes_file_arg: &Option, theme_name: String, ) -> Result<()> { let theme_definitions_wrapper = if let Some(theme_file) = themes_file_arg { load_theme_config(theme_file.to_str().expect("Invalid theme file specified"))? } else { confy::load("managarr", "themes").unwrap_or_else(|_| ThemeDefinitionsWrapper::default()) }; let theme = if !theme_name.is_empty() { let theme_definition = theme_definitions_wrapper .theme_definitions .iter() .find(|t| t.name == theme_name); if theme_definition.is_none() { log_and_print_error(format!("The specified theme was not found: {theme_name}")); process::exit(1); } theme_definition.unwrap().theme } else { debug!("No theme specified, using default theme"); Theme::default() }; debug!("Managarr loaded using theme: {theme:?}"); theme.validate(); THEME.set(theme); let mut stdout = io::stdout(); enable_raw_mode()?; execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; terminal.hide_cursor()?; let input_events = Events::new(); loop { let mut app = app.lock().await; terminal.draw(|f| ui(f, &mut app))?; match input_events.next()? { Some(InputEvent::KeyEvent(key)) => { if key == Key::Char('q') && !app.ignore_special_keys_for_textbox_input { break; } handlers::handle_events(key, &mut app); } Some(InputEvent::Tick) => app.on_tick().await, _ => {} } } terminal.show_cursor()?; disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; Ok(()) } #[cfg(debug_assertions)] fn panic_hook(info: &PanicHookInfo<'_>) { 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 '{msg}', {location}\n\r{stacktrace}" )), ) .unwrap(); } #[cfg(not(debug_assertions))] fn panic_hook(info: &PanicHookInfo<'_>) { use human_panic::{handle_dump, metadata, print_msg}; let meta = metadata!(); 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"); }