From b6f2e17400010300472aa91f3aa211a91a2bce23 Mon Sep 17 00:00:00 2001 From: OJarrisonn Date: Tue, 11 Feb 2025 13:31:36 -0300 Subject: [PATCH 1/6] chore: add tokio Add `tokio` crate as a dependency. Signed-off-by: OJarrisonn --- Cargo.lock | 46 +++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f78fc30..0c689e65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -582,9 +582,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" @@ -771,6 +771,7 @@ dependencies = [ "serde-xml-rs", "serde_json", "thiserror", + "tokio", "ureq", "which", ] @@ -1084,6 +1085,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -1192,6 +1203,35 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/Cargo.toml b/Cargo.toml index 9711f937..c0a25c49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ chrono = "0.4.38" ansi-to-tui = "6.0.0" which = "6.0.3" ureq = { version = "=3.0.0-rc2", features = ["rustls"] } +tokio = { version = "1.43.0", features = ["full"] } # The profile that 'cargo dist' will build with [profile.dist] From 246c19cbdc2b589004d66364f962ca8ff8c7434f Mon Sep 17 00:00:00 2001 From: OJarrisonn Date: Tue, 11 Feb 2025 13:31:59 -0300 Subject: [PATCH 2/6] feat: implement logger actor Here i've created the implementation of the logger actor. This actor is responsible for logging messages. The `Logger` struct is a simple refactor of the old `app::logging::Logger` it is responsible for storing messages that will be later printed to the stdout and also to provide methods such as `spawn` `spawn` is responsible for creating a dedicated task that will handle all the messages received from all the transmitters. The `LoggerTx` is a transmitter that is responsible by sending messages down to the running actor. This is what should be used accross the code base to communicate with the actor. The `MockLoggerTx` is a mock version of this logger which does nothing at all. Finally but not least, the `LoggerActor` trait which unifies the interface of `LoggerTx` and `MockLoggerTx`. This permits the swaping of the loggers for testing scenarios. Signed-off-by: OJarrisonn --- src/app.rs | 69 +++-- src/app/cover_renderer.rs | 8 +- src/app/patch_renderer.rs | 22 +- src/cli.rs | 10 +- src/handler.rs | 18 +- src/handler/bookmarked.rs | 3 +- src/handler/details_actions.rs | 3 +- src/handler/edit_config.rs | 6 +- src/handler/latest.rs | 3 +- src/handler/mail_list.rs | 3 +- src/logger.rs | 503 +++++++++++++++++++++++++++++++++ src/main.rs | 20 +- src/ui.rs | 7 +- src/ui/details_actions.rs | 20 +- src/ui/edit_config.rs | 9 +- src/ui/latest.rs | 6 +- src/ui/mail_list.rs | 6 +- src/ui/navigation_bar.rs | 7 +- src/utils.rs | 8 +- 19 files changed, 628 insertions(+), 103 deletions(-) create mode 100644 src/logger.rs diff --git a/src/app.rs b/src/app.rs index 88251882..4d5270f3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,12 +1,11 @@ use crate::{ - log_on_error, + logger::LoggerActor, ui::popup::{info_popup::InfoPopUp, PopUp}, }; use ansi_to_tui::IntoText; use color_eyre::eyre::bail; use config::Config; use cover_renderer::render_cover; -use logging::Logger; use patch_hub::lore::{ lore_api_client::BlockingLoreAPIClient, lore_session, @@ -28,13 +27,12 @@ use crate::utils; mod config; pub mod cover_renderer; -pub mod logging; pub mod patch_renderer; pub mod screens; /// Type that represents the overall state of the application. It can be viewed /// as the **Model** component of `patch-hub`. -pub struct App { +pub struct App { /// The current active screen pub current_screen: CurrentScreen, /// Screen to navigate and select the mailing lists archived on Lore @@ -54,9 +52,10 @@ pub struct App { /// Client to handle Lore API requests and responses pub lore_api_client: BlockingLoreAPIClient, pub popup: Option>, + pub logger: Logger, } -impl App { +impl App { /// Creates a new instance of `App`. It dynamically loads configurations /// based on precedence (see [crate::app::Config::build]), app data /// (available mailing lists, bookmarked patchsets, reviewed patchsets), and @@ -65,7 +64,7 @@ impl App { /// # Returns /// /// `App` instance with loading configurations and app data. - pub fn new() -> color_eyre::Result { + pub fn new(logger: Logger) -> color_eyre::Result { let config: Config = Config::build(); config.create_dirs(); @@ -83,9 +82,8 @@ impl App { let lore_api_client = BlockingLoreAPIClient::default(); // Initialize the logger before the app starts - Logger::init_log_file(&config)?; - Logger::info("patch-hub started"); - logging::garbage_collector::collect_garbage(&config); + logger.info("patch-hub started"); + logger.collect_garbage(); Ok(App { current_screen: CurrentScreen::MailingListSelection, @@ -108,7 +106,8 @@ impl App { config, lore_api_client, popup: None, - }) + logger, + } } /// Initializes field [App::latest_patchsets], from currently selected @@ -163,15 +162,19 @@ impl App { screen => bail!(format!("Invalid screen passed as argument {screen:?}")), }; - let patchset_path: String = match log_on_error!(lore_session::download_patchset( - self.config.patchsets_cache_dir(), - &representative_patch, - )) { - Ok(result) => result, - Err(io_error) => bail!("{io_error}"), - }; + let patchset_path: String = + match self.logger.error_on_error(lore_session::download_patchset( + self.config.patchsets_cache_dir(), + &representative_patch, + )) { + Ok(result) => result, + Err(io_error) => bail!("{io_error}"), + }; - match log_on_error!(lore_session::split_patchset(&patchset_path)) { + match self + .logger + .error_on_error(lore_session::split_patchset(&patchset_path)) + { Ok(raw_patches) => { let mut patches_preview: Vec = Vec::new(); for raw_patch in &raw_patches { @@ -209,8 +212,10 @@ impl App { let rendered_cover = match render_cover(raw_cover, self.config.cover_renderer()) { Ok(render) => render, - Err(_) => { - Logger::error("Failed to render cover preview with external program"); + Err(e) => { + self.logger + .error("Failed to render cover preview with external program"); + self.logger.error(e); raw_cover.to_string() } }; @@ -218,10 +223,10 @@ impl App { let rendered_patch = match render_patch_preview(raw_patch, self.config.patch_renderer()) { Ok(render) => render, - Err(_) => { - Logger::error( - "Failed to render patch preview with external program", - ); + Err(e) => { + self.logger + .error("Failed to render patch preview with external program"); + self.logger.error(e); raw_patch.to_string() } }; @@ -395,30 +400,32 @@ impl App { let mut app_can_run = true; if !utils::binary_exists("b4") { - Logger::error("b4 is not installed, patchsets cannot be downloaded"); + self.logger + .error("b4 is not installed, patchsets cannot be downloaded"); app_can_run = false; } if !utils::binary_exists("git") { - Logger::warn("git is not installed, send-email won't work"); + self.logger + .warn("git is not installed, send-email won't work"); } match self.config.patch_renderer() { PatchRenderer::Bat => { if !utils::binary_exists("bat") { - Logger::warn("bat is not installed, patch rendering will fallback to default"); + self.logger + .warn("bat is not installed, patch rendering will fallback to default"); } } PatchRenderer::Delta => { if !utils::binary_exists("delta") { - Logger::warn( - "delta is not installed, patch rendering will fallback to default", - ); + self.logger + .warn("delta is not installed, patch rendering will fallback to default"); } } PatchRenderer::DiffSoFancy => { if !utils::binary_exists("diff-so-fancy") { - Logger::warn( + self.logger.warn( "diff-so-fancy is not installed, patch rendering will fallback to default", ); } diff --git a/src/app/cover_renderer.rs b/src/app/cover_renderer.rs index fc30f266..6a31f4de 100644 --- a/src/app/cover_renderer.rs +++ b/src/app/cover_renderer.rs @@ -4,10 +4,9 @@ use std::{ process::{Command, Stdio}, }; +use color_eyre::eyre::Context; use serde::{Deserialize, Serialize}; -use super::logging::Logger; - #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)] pub enum CoverRenderer { #[default] @@ -67,10 +66,7 @@ fn bat_cover_renderer(patch: &str) -> color_eyre::Result { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() - .map_err(|e| { - Logger::error(format!("Failed to spawn bat for cover preview: {}", e)); - e - })?; + .context("Failed to spawn bat for cover preview")?; bat.stdin.as_mut().unwrap().write_all(patch.as_bytes())?; let output = bat.wait_with_output()?; diff --git a/src/app/patch_renderer.rs b/src/app/patch_renderer.rs index 5a9efc41..67e5d667 100644 --- a/src/app/patch_renderer.rs +++ b/src/app/patch_renderer.rs @@ -4,11 +4,9 @@ use std::{ process::{Command, Stdio}, }; -use color_eyre::eyre::eyre; +use color_eyre::eyre::{eyre, Context}; use serde::{Deserialize, Serialize}; -use super::logging::Logger; - #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)] pub enum PatchRenderer { #[default] @@ -95,10 +93,7 @@ fn bat_patch_renderer(patch: &str) -> color_eyre::Result { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() - .map_err(|e| { - Logger::error(format!("Failed to spawn bat for patch preview: {}", e)); - e - })?; + .context("Failed to spawn bat for patch preview")?; bat.stdin .as_mut() @@ -127,10 +122,7 @@ fn delta_patch_renderer(patch: &str) -> color_eyre::Result { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() - .map_err(|e| { - Logger::error(format!("Failed to spawn delta for patch preview: {}", e)); - e - })?; + .context("Failed to spawn delta for patch preview")?; delta .stdin @@ -153,13 +145,7 @@ fn diff_so_fancy_renderer(patch: &str) -> color_eyre::Result { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() - .map_err(|e| { - Logger::error(format!( - "Failed to spawn diff-so-fancy for patch preview: {}", - e - )); - e - })?; + .context("Failed to spawn diff-so-fancy for patch preview")?; dsf.stdin .as_mut() diff --git a/src/cli.rs b/src/cli.rs index 60e2859b..4b7de001 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,10 +4,7 @@ use clap::Parser; use color_eyre::eyre::eyre; use ratatui::{prelude::Backend, Terminal}; -use crate::{ - app::{logging::Logger, App}, - utils, -}; +use crate::{app::App, logger::LoggerActor, utils}; #[derive(Debug, Parser)] #[command(version, about)] @@ -24,10 +21,11 @@ impl Cli { pub fn resolve( &self, terminal: Terminal, - app: &mut App, + app: &mut App, + logger: impl LoggerActor, ) -> ControlFlow, Terminal> { if self.show_configs { - Logger::info("Printing current configurations"); + logger.info("Printing current configurations"); drop(terminal); if let Err(err) = utils::restore() { return ControlFlow::Break(Err(eyre!(err))); diff --git a/src/handler.rs b/src/handler.rs index 97597e58..44dd6533 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -10,8 +10,9 @@ use std::{ }; use crate::{ - app::{logging::Logger, screens::CurrentScreen, App}, + app::{screens::CurrentScreen, App}, loading_screen, + logger::LoggerActor, ui::draw_ui, }; @@ -29,7 +30,7 @@ use ratatui::{ fn key_handling( mut terminal: Terminal, - app: &mut App, + app: &mut App, key: KeyEvent, ) -> color_eyre::Result>> where @@ -63,7 +64,10 @@ where Ok(ControlFlow::Continue(terminal)) } -fn logic_handling(mut terminal: Terminal, app: &mut App) -> color_eyre::Result> +fn logic_handling( + mut terminal: Terminal, + app: &mut App, +) -> color_eyre::Result> where B: Backend + Send + 'static, { @@ -102,12 +106,16 @@ where Ok(terminal) } -pub fn run_app(mut terminal: Terminal, mut app: App) -> color_eyre::Result<()> +pub fn run_app( + mut terminal: Terminal, + mut app: App, + logger: impl LoggerActor, +) -> color_eyre::Result<()> where B: Backend + Send + 'static, { if !app.check_external_deps() { - Logger::error("patch-hub cannot be executed because some dependencies are missing"); + logger.error("patch-hub cannot be executed because some dependencies are missing"); bail!("patch-hub cannot be executed because some dependencies are missing, check logs for more information"); } diff --git a/src/handler/bookmarked.rs b/src/handler/bookmarked.rs index 2896540e..3acd6bf5 100644 --- a/src/handler/bookmarked.rs +++ b/src/handler/bookmarked.rs @@ -3,6 +3,7 @@ use std::ops::ControlFlow; use crate::{ app::{screens::CurrentScreen, App}, loading_screen, + logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; use ratatui::{ @@ -12,7 +13,7 @@ use ratatui::{ }; pub fn handle_bookmarked_patchsets( - app: &mut App, + app: &mut App, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> diff --git a/src/handler/details_actions.rs b/src/handler/details_actions.rs index b7ca73a5..c0f1004d 100644 --- a/src/handler/details_actions.rs +++ b/src/handler/details_actions.rs @@ -2,6 +2,7 @@ use std::time::Duration; use crate::{ app::{screens::CurrentScreen, App}, + logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp}, utils, }; @@ -14,7 +15,7 @@ use ratatui::{ use super::wait_key_press; pub fn handle_patchset_details( - app: &mut App, + app: &mut App, key: KeyEvent, terminal: &mut Terminal, ) -> color_eyre::Result<()> { diff --git a/src/handler/edit_config.rs b/src/handler/edit_config.rs index 405f6d5d..218c3793 100644 --- a/src/handler/edit_config.rs +++ b/src/handler/edit_config.rs @@ -1,10 +1,14 @@ use crate::{ app::{screens::CurrentScreen, App}, + logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; use ratatui::crossterm::event::{KeyCode, KeyEvent}; -pub fn handle_edit_config(app: &mut App, key: KeyEvent) -> color_eyre::Result<()> { +pub fn handle_edit_config( + app: &mut App, + key: KeyEvent, +) -> color_eyre::Result<()> { if let Some(edit_config_state) = app.edit_config.as_mut() { match edit_config_state.is_editing() { true => match key.code { diff --git a/src/handler/latest.rs b/src/handler/latest.rs index 42102aac..688ecd4f 100644 --- a/src/handler/latest.rs +++ b/src/handler/latest.rs @@ -3,6 +3,7 @@ use std::ops::ControlFlow; use crate::{ app::{screens::CurrentScreen, App}, loading_screen, + logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; use ratatui::{ @@ -12,7 +13,7 @@ use ratatui::{ }; pub fn handle_latest_patchsets( - app: &mut App, + app: &mut App, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> diff --git a/src/handler/mail_list.rs b/src/handler/mail_list.rs index ecfd0396..a9f3e55f 100644 --- a/src/handler/mail_list.rs +++ b/src/handler/mail_list.rs @@ -3,6 +3,7 @@ use std::ops::ControlFlow; use crate::{ app::{screens::CurrentScreen, App}, loading_screen, + logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; use ratatui::{ @@ -12,7 +13,7 @@ use ratatui::{ }; pub fn handle_mailing_list_selection( - app: &mut App, + app: &mut App, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 00000000..90c70eff --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,503 @@ +use std::{fmt::Display, path::PathBuf}; + +use color_eyre::eyre::Context; +use tokio::{ + fs::{self, File, OpenOptions}, + io::AsyncWriteExt, + sync::mpsc::Sender, + task::JoinHandle, +}; + +/// Describes the log level of a message +/// +/// This is used to determine the severity of a log message so the logger +/// handles it accordingly to the verbosity level. +/// +/// The levels severity are: `Info` < `Warning` < `Error` +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum LogLevel { + /// The lowest level, dedicated to regular information that is not really + /// important + Info, + /// Mid level, used to indicate when something went wrong but it's not + /// critical + Warning, + /// The highest level, used to indicate critical errors. But not enought to + /// crash the program + Error, +} + +impl Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LogLevel::Info => write!(f, "INFO"), + LogLevel::Warning => write!(f, "WARN"), + LogLevel::Error => write!(f, "ERROR"), + } + } +} + +/// Describes a message to be logged +/// +/// Contains the message constent itself as a [`String`] and its [`LogLevel`] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct LogMessage { + level: LogLevel, + message: String, +} + +impl Display for LogMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}] {}", self.level, self.message) + } +} + +/// The Logger manages logging to [`stderr`] (log buffer) and a log file. +/// The messages are written to the log file immediatly, +/// but the messages to the `stderr` are written only after the TUI is closed, +/// so they are kept in memory. +/// +/// The logger also has a log level that can be set to filter the messages that +/// are written to the log file. +/// Only messages with a level equal or higher than the log level are written +/// to the log file. +/// +/// You're not supossed to use an instance of this directly, but use +/// [`LoggerTx`] instead by calling [`spawn`] as soon as this struct is built. +/// +/// The expected flow is: +/// - Instantiate the logger with [`build`] +/// - Spawn the actor with [`spawn`] +/// - Log with [`info`], [`warn`] or [`error`] +/// - Flush the log buffer to the stderr and finish the logger with [`flush`] +/// +/// [`Config`]: super::config::Config +/// [`info`]: LoggerTx::info +/// [`warn`]: LoggerTx::warn +/// [`error`]: LoggerTx::error +/// [`flush`]: LoggerTx::flush +/// [`stderr`]: std::io::stderr +/// [`spawn`]: Logger::spawn +/// [`build`]: Logger::build +#[derive(Debug)] +pub struct Logger { + log_dir: PathBuf, + log_file_path: PathBuf, + log_file: File, + latest_log_file: File, + logs_to_print: Vec, + print_level: LogLevel, // TODO: Add a log level configuration + max_age: usize, +} + +impl Logger { + /// Creates a new logger instance. The parameters are the [dir] where the + /// log files will be stored, [level] of log messages, and [max_age] of the + /// log files in days. + /// + /// You're supposed to call [`spawn`] immediately after this method to + /// transform the logger instance into an actor. + /// + /// # Errors + /// + /// If either the latest log file or the log file cannot be created, an + /// error is returned. + /// + /// [`level`]: LogLevel + /// [`flush`]: LoggerTx::flush + /// [`spawn`]: Logger::spawn + pub async fn build(dir: &str, level: LogLevel, max_age: usize) -> color_eyre::Result { + let log_dir = PathBuf::from(dir); + let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S"); + let log_file_path = log_dir.join(format!("patch-hub-{}.log", timestamp)); + + let log_file = OpenOptions::new() + .append(true) + .create(true) + .open(&log_file_path) + .await + .context("While building the logger")?; + + let latest_log_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(log_dir.join("latest.log")) + .await + .context("While building the logger")?; + + Ok(Self { + log_dir, + log_file_path, + log_file, + latest_log_file, + logs_to_print: Vec::new(), + print_level: level, + max_age, + }) + } + + /// Transforms the logger instance into an actor. This method returns a + /// [`LoggerTx`] and a [`JoinHandle`] that can be used to send commands to + /// the logger or await for it to finish (when a [`flush`] is performed, + /// for instance). + /// + /// The handling of the commandds received is done sequentially, so a + /// command is only processed once the previous one is finished. + /// + /// [`flush`]: LoggerTx::flush + pub fn spawn(mut self) -> (LoggerTx, JoinHandle<()>) { + let (tx, mut rx) = tokio::sync::mpsc::channel(100); + let handle = tokio::spawn(async move { + while let Some(command) = rx.recv().await { + match command { + Command::Log(msg) => { + self.log(msg).await; + } + Command::Flush => { + self.flush(); + rx.close(); + break; + } + Command::CollectGarbage => { + self.collect_garbage().await; + } + } + } + }); + + (LoggerTx(tx), handle) + } + + /// Given a [`LogMessage`] object, writes it to the current and latest log + /// files. If the message level is high enough, it is also stored in the log + /// buffer to be printed to [`stderr`] when a [`flush`] is performed. + /// + /// [`stderr`]: std::io::stderr + /// [`flush`]: LoggerTx::flush + async fn log(&mut self, message: LogMessage) { + self.log_file + .write_all(format!("{}\n", &message).as_bytes()) + .await + .expect("Failed to write to the current log file"); + + self.log_file + .flush() + .await + .expect("Failed to flush the current log file"); + + self.latest_log_file + .write_all(format!("{}\n", &message).as_bytes()) + .await + .expect("Failed to write to the latest log file"); + + self.latest_log_file + .flush() + .await + .expect("Failed to flush the latest log file"); + + if message.level >= self.print_level { + self.logs_to_print.push(message); + } + } + + /// Writes the log messages to the [`stderr`] if their level is equal or + /// higher than the print level set in the logger. + /// + /// **The logger is destroyed after this method is called.** + /// + /// [`stderr`]: std::io::stderr + fn flush(self) { + for message in &self.logs_to_print { + eprintln!("{}", message); + } + + if !self.logs_to_print.is_empty() { + eprintln!("Check the full log file: {}", self.log_file_path.display()); + } + } + + /// Runs the garbage collector to delete old log files. + /// + /// A log file is a file in the [`log_dir`] and it will be deleted if its + /// older than [`max_age`] days. + /// + /// [`log_dir`]: Logger::log_dir + /// [`max_age`]: Logger::max_age + async fn collect_garbage(&mut self) { + if self.max_age == 0 { + return; + } + + let now = std::time::SystemTime::now(); + + let Ok(mut logs) = fs::read_dir(&self.log_dir).await else { + self.log(LogMessage { + level: LogLevel::Error, + message: "Failed to read the logs directory during garbage collection".into(), + }) + .await; + return; + }; + + loop { + let log = logs.next_entry().await; + let Ok(log) = log else { + continue; + }; + + let Some(log) = log else { + break; + }; + + let filename = log.file_name(); + + if !filename.to_string_lossy().ends_with(".log") + || !filename.to_string_lossy().starts_with("patch-hub_") + { + continue; + } + + let Ok(Ok(created_date)) = log.metadata().await.map(|meta| meta.created()) else { + continue; + }; + let Ok(age) = now.duration_since(created_date) else { + continue; + }; + let age = age.as_secs() / 60 / 60 / 24; + + if age as usize > self.max_age && std::fs::remove_file(log.path()).is_err() { + self.log(LogMessage { + message: format!( + "Failed to remove the log file: {}", + log.path().to_string_lossy() + ), + level: LogLevel::Warning, + }) + .await; + } + } + } +} + +/// The possible commands to be handled by the logger actor. They will be +/// executed synchronously in the same order that they were sent through the +/// channel +#[derive(Debug, Clone, PartialEq, Eq)] +enum Command { + /// Logs the payload message + Log(LogMessage), + /// Flushes the logger by closing the log file, printing critical errors to + /// the stdout and destroying the logger instance + Flush, + /// Runs the log garbage collector deleting old files according with the + /// configured in the logger + CollectGarbage, +} + +/// The transmitter that sends messages down to a logger actor. This is what +/// you're supossed to use accross the code to log messages, instead of Logger. +/// Cloning it is cheap so do not feel afraid to pass it around. +/// +/// The transmitter is obtained by calling [`spawn`] on a [`Logger`] instance, +/// consuming it and creating a dedicated task for it. Use the methods of this +/// struct to interact with the logger. +/// +/// The intended usage is: +/// - Instantiate the logger with [`Logger::build`] +/// - Spawn the logger actor with [`Logger::spawn`] +/// - Use the methods of this struct to log messages +/// - Use the method [`flush`] to print the log messages to [`stderr`] and +/// finish the logger +/// +/// [`spawn`]: Logger::spawn +/// [`flush`]: LoggerTx::flush +/// [`stderr`]: std::io::stderr +#[derive(Debug, Clone)] +pub struct LoggerTx(Sender); + +/// A mock implementation of [`LoggerActor`] +/// This is version of [`LoggerTx`] that does nothing at all +#[derive(Debug, Clone, Copy)] +pub struct MockLoggerTx; + +/// The logger actor trait. This is useful so multiple different implementations +/// of the same actor can be used everywhere. +/// +/// The idea is that one always use this `LoggerExt` trait where a Logger actor +/// is needed +pub trait LoggerActor: Sized { + /// Helper to simplify the logging process. This method sends a + /// [`LogMessage`] to the logger. Will send the message in a new task so it + /// won't block the caller + /// + /// # Panics + /// If the logger was flushed + fn log(&self, message: String, level: LogLevel); + + /// Log a message with the `INFO` level + /// + /// # Panics + /// If the logger was flushed + fn info(&self, message: M); + + /// Log a message with the `WARNING` level + /// + /// # Panics + /// If the logger was flushed + fn warn(&self, message: M); + + /// Log a message with the `ERROR` level + fn error(&self, message: M); + + /// Log an info message if the result is an error + /// and return the result as is + /// + /// # Panics + /// If the logger was flushed + #[allow(dead_code)] + fn info_on_error(&self, result: Result) -> Result; + + /// Log an warning message if the result is an error + /// and return the result as is + /// + /// # Panics + /// If the logger was flushed + #[allow(dead_code)] + fn warn_on_error(&self, result: Result) -> Result; + + /// Log an error message if the result is an error + /// and return the result as is + /// + /// # Panics + /// If the logger was flushed + fn error_on_error(&self, result: Result) -> Result; + + /// Flushes the logger by printing its messages to [`stderr`] and closing + /// the log file. After this method is called, the logger is destroyed and + /// any attempt to use it will panic. + /// + /// # Panics + /// If called twice + /// + /// [`stderr`]: std::io::stderr + fn flush(self) -> JoinHandle<()>; + + /// Collects the garbage from the logs directory. Garbage logs are the ones + /// older than the [`max_age`] set during the logger [`build`]. + /// + /// # Panics + /// If called after a flush + /// + /// [`build`]: Logger::build + /// [`max_age`]: Logger::max_age + fn collect_garbage(&self) -> JoinHandle<()>; +} + +impl LoggerActor for LoggerTx { + fn log(&self, message: String, level: LogLevel) { + let sender = self.0.clone(); + + tokio::spawn(async move { + sender + .send(Command::Log(LogMessage { + level, + message: message.to_string(), + })) + .await + .expect("Attemp to use logger after a flush"); + }); + } + + fn info(&self, message: M) { + self.log(message.to_string(), LogLevel::Info); + } + + fn warn(&self, message: M) { + self.log(message.to_string(), LogLevel::Warning); + } + + fn error(&self, message: M) { + self.log(message.to_string(), LogLevel::Error); + } + + fn info_on_error(&self, result: Result) -> Result { + match result { + Ok(value) => Ok(value), + Err(err) => { + self.log(err.to_string(), LogLevel::Info); + Err(err) + } + } + } + + fn warn_on_error(&self, result: Result) -> Result { + match result { + Ok(value) => Ok(value), + Err(err) => { + self.log(err.to_string(), LogLevel::Warning); + Err(err) + } + } + } + + fn error_on_error(&self, result: Result) -> Result { + match result { + Ok(value) => Ok(value), + Err(err) => { + self.log(err.to_string(), LogLevel::Error); + Err(err) + } + } + } + + fn flush(self) -> JoinHandle<()> { + tokio::spawn(async move { + self.0 + .send(Command::Flush) + .await + .expect("Flushing a logger twice"); + }) + } + + fn collect_garbage(&self) -> JoinHandle<()> { + let sender = self.0.clone(); + + tokio::spawn(async move { + sender + .send(Command::CollectGarbage) + .await + .expect("Attemp to use logger after a flush"); + }) + } +} + +impl LoggerActor for MockLoggerTx { + fn log(&self, _message: String, _level: LogLevel) {} + + fn info(&self, _message: M) {} + + fn warn(&self, _message: M) {} + + fn error(&self, _message: M) {} + + fn info_on_error(&self, result: Result) -> Result { + result + } + + fn warn_on_error(&self, result: Result) -> Result { + result + } + + fn error_on_error(&self, result: Result) -> Result { + result + } + + fn flush(self) -> JoinHandle<()> { + tokio::spawn(async move {}) + } + + fn collect_garbage(&self) -> JoinHandle<()> { + tokio::spawn(async move {}) + } +} diff --git a/src/main.rs b/src/main.rs index d738c5bf..af74718d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,34 +1,38 @@ use std::ops::ControlFlow; use crate::app::App; -use app::logging::Logger; use clap::Parser; use cli::Cli; use handler::run_app; +use logger::{LogLevel, Logger, LoggerActor}; mod app; mod cli; mod handler; +mod logger; mod ui; mod utils; -fn main() -> color_eyre::Result<()> { +#[tokio::main] +async fn main() -> color_eyre::Result<()> { let args = Cli::parse(); - utils::install_hooks()?; + let (logger, _) = Logger::build("/tmp", LogLevel::Info, 0).await?.spawn(); + utils::install_hooks(logger.clone())?; + let mut terminal = utils::init()?; - let mut app = App::new()?; + let mut app = App::new(logger.clone())?; - match args.resolve(terminal, &mut app) { + match args.resolve(terminal, &mut app, logger.clone()) { ControlFlow::Break(b) => return b, ControlFlow::Continue(t) => terminal = t, } - run_app(terminal, app)?; + run_app(terminal, app, logger.clone())?; utils::restore()?; - Logger::info("patch-hub finished"); - Logger::flush(); + logger.info("patch-hub finished"); + let _ = logger.flush().await; Ok(()) } diff --git a/src/ui.rs b/src/ui.rs index a01d4c68..8308e0f4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,7 @@ -use crate::app::{screens::CurrentScreen, App}; +use crate::{ + app::{screens::CurrentScreen, App}, + logger::LoggerActor, +}; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Style, Stylize}, @@ -16,7 +19,7 @@ mod mail_list; mod navigation_bar; pub mod popup; -pub fn draw_ui(f: &mut Frame, app: &App) { +pub fn draw_ui(f: &mut Frame, app: &App) { // Clear the whole screen for sanitizing reasons f.render_widget(Clear, f.area()); diff --git a/src/ui/details_actions.rs b/src/ui/details_actions.rs index 92a4020d..03e84be6 100644 --- a/src/ui/details_actions.rs +++ b/src/ui/details_actions.rs @@ -6,9 +6,12 @@ use ratatui::{ Frame, }; -use crate::app::{ - screens::details_actions::{DetailsActions, PatchsetAction}, - App, +use crate::{ + app::{ + screens::details_actions::{DetailsActions, PatchsetAction}, + App, + }, + logger::LoggerActor, }; /// Returns a `Line` type that represents a line containing stats about reply @@ -47,7 +50,12 @@ fn review_trailers_details(details_actions: &DetailsActions) -> Line<'static> { ]) } -fn render_details_and_actions(f: &mut Frame, app: &App, details_chunk: Rect, actions_chunk: Rect) { +fn render_details_and_actions( + f: &mut Frame, + app: &App, + details_chunk: Rect, + actions_chunk: Rect, +) { let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); let mut staged_to_reply = String::new(); @@ -199,7 +207,7 @@ fn render_details_and_actions(f: &mut Frame, app: &App, details_chunk: Rect, act f.render_widget(patchset_actions, actions_chunk); } -fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { +fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); let preview_index = patchset_details_and_actions.preview_index; @@ -242,7 +250,7 @@ fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { f.render_widget(patch_preview, chunk); } -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); if patchset_details_and_actions.preview_fullscreen { diff --git a/src/ui/edit_config.rs b/src/ui/edit_config.rs index 88b5a57d..edc0fc00 100644 --- a/src/ui/edit_config.rs +++ b/src/ui/edit_config.rs @@ -6,10 +6,9 @@ use ratatui::{ Frame, }; -use crate::app::logging::Logger; -use crate::app::App; +use crate::{app::App, logger::LoggerActor}; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let edit_config = app.edit_config.as_ref().unwrap(); let mut constraints = Vec::new(); @@ -64,7 +63,7 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { } } -pub fn mode_footer_text(app: &App) -> Vec { +pub fn mode_footer_text(app: &App) -> Vec { let edit_config_state = app.edit_config.as_ref().unwrap(); vec![if edit_config_state.is_editing() { Span::styled("Editing...", Style::default().fg(Color::LightYellow)) @@ -73,7 +72,7 @@ pub fn mode_footer_text(app: &App) -> Vec { }] } -pub fn keys_hint(app: &App) -> Span { +pub fn keys_hint(app: &App) -> Span { let edit_config_state = app.edit_config.as_ref().unwrap(); match edit_config_state.is_editing() { true => Span::styled( diff --git a/src/ui/latest.rs b/src/ui/latest.rs index 4288543c..352b1593 100644 --- a/src/ui/latest.rs +++ b/src/ui/latest.rs @@ -1,4 +1,4 @@ -use crate::app::App; +use crate::{app::App, logger::LoggerActor}; use patch_hub::lore::patch::Patch; use ratatui::{ layout::Rect, @@ -8,7 +8,7 @@ use ratatui::{ Frame, }; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let page_number = app.latest_patchsets.as_ref().unwrap().page_number(); let patchset_index = app.latest_patchsets.as_ref().unwrap().patchset_index(); let mut list_items = Vec::::new(); @@ -67,7 +67,7 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { f.render_stateful_widget(list, chunk, &mut list_state); } -pub fn mode_footer_text(app: &App) -> Vec { +pub fn mode_footer_text(app: &App) -> Vec { vec![Span::styled( format!( "Latest Patchsets from {} (page {})", diff --git a/src/ui/mail_list.rs b/src/ui/mail_list.rs index 16686db1..3814d17a 100644 --- a/src/ui/mail_list.rs +++ b/src/ui/mail_list.rs @@ -6,9 +6,9 @@ use ratatui::{ Frame, }; -use crate::app::App; +use crate::{app::App, logger::LoggerActor}; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let highlighted_list_index = app.mailing_list_selection.highlighted_list_index; let mut list_items = Vec::::new(); @@ -50,7 +50,7 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { f.render_stateful_widget(list, chunk, &mut list_state); } -pub fn mode_footer_text(app: &App) -> Vec { +pub fn mode_footer_text(app: &App) -> Vec { let mut text_area = Span::default(); if app.mailing_list_selection.target_list.is_empty() { diff --git a/src/ui/navigation_bar.rs b/src/ui/navigation_bar.rs index b601bf97..225f00ca 100644 --- a/src/ui/navigation_bar.rs +++ b/src/ui/navigation_bar.rs @@ -1,5 +1,8 @@ use super::{bookmarked, details_actions, edit_config, latest, mail_list}; -use crate::app::{self, App}; +use crate::{ + app::{self, App}, + logger::LoggerActor, +}; use app::screens::CurrentScreen; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, @@ -8,7 +11,7 @@ use ratatui::{ Frame, }; -pub fn render(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render(f: &mut Frame, app: &App, chunk: Rect) { let mode_footer_text = match app.current_screen { CurrentScreen::MailingListSelection => mail_list::mode_footer_text(app), CurrentScreen::BookmarkedPatchsets => bookmarked::mode_footer_text(), diff --git a/src/utils.rs b/src/utils.rs index 120b4042..82df3ddd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,7 +13,7 @@ use ratatui::{ Terminal, }; -use crate::app::logging::Logger; +use crate::logger::{LoggerActor, LoggerTx}; /// A type alias for the terminal type used in this application pub type Tui = Terminal>; @@ -34,14 +34,16 @@ pub fn restore() -> io::Result<()> { /// This replaces the standard color_eyre panic and error hooks with hooks that /// restore the terminal before printing the panic or error. -pub fn install_hooks() -> color_eyre::Result<()> { +pub fn install_hooks(logger: LoggerTx) -> color_eyre::Result<()> { let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks(); // convert from a color_eyre PanicHook to a standard panic hook let panic_hook = panic_hook.into_panic_hook(); panic::set_hook(Box::new(move |panic_info| { restore().unwrap(); - Logger::flush(); + // TODO: await for the flush to finish + ::clone(&logger).flush(); + panic_hook(panic_info); })); From 7e4fe2c177787899953bfe899e1bf40d6a01e86e Mon Sep 17 00:00:00 2001 From: OJarrisonn Date: Tue, 11 Feb 2025 16:33:15 -0300 Subject: [PATCH 3/6] refactor: remove old logger implementation Removes the old logger implementation for cleanup purposes Signed-off-by: OJarrisonn --- src/app/logging/garbage_collector.rs | 50 ---------------------------- src/app/logging/log_on_error.rs | 27 --------------- 2 files changed, 77 deletions(-) delete mode 100644 src/app/logging/garbage_collector.rs delete mode 100644 src/app/logging/log_on_error.rs diff --git a/src/app/logging/garbage_collector.rs b/src/app/logging/garbage_collector.rs deleted file mode 100644 index e06892b4..00000000 --- a/src/app/logging/garbage_collector.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Log Garbage Collector -//! -//! This module is responsible for cleaning up the log files. - -use crate::app::config::Config; - -use super::Logger; - -/// Collects the garbage from the logs directory. -/// Will check for log files `patch-hub_*.log` and remove them if they are older than the `max_log_age` in the config. -pub fn collect_garbage(config: &Config) { - if config.max_log_age() == 0 { - return; - } - - let now = std::time::SystemTime::now(); - let logs_path = config.logs_path(); - let Ok(logs) = std::fs::read_dir(logs_path) else { - Logger::error("Failed to read the logs directory during garbage collection"); - return; - }; - - for log in logs { - let Ok(log) = log else { - continue; - }; - let filename = log.file_name(); - - if !filename.to_string_lossy().ends_with(".log") - || !filename.to_string_lossy().starts_with("patch-hub_") - { - continue; - } - - let Ok(Ok(created_date)) = log.metadata().map(|meta| meta.created()) else { - continue; - }; - let Ok(age) = now.duration_since(created_date) else { - continue; - }; - let age = age.as_secs() / 60 / 60 / 24; - - if age as usize > config.max_log_age() && std::fs::remove_file(log.path()).is_err() { - Logger::warn(format!( - "Failed to remove the log file: {}", - log.path().to_string_lossy() - )); - } - } -} diff --git a/src/app/logging/log_on_error.rs b/src/app/logging/log_on_error.rs deleted file mode 100644 index b156c33d..00000000 --- a/src/app/logging/log_on_error.rs +++ /dev/null @@ -1,27 +0,0 @@ -#[macro_export] -macro_rules! log_on_error { - ($result:expr) => { - log_on_error!($crate::app::logging::LogLevel::Error, $result) - }; - ($level:expr, $result:expr) => { - match $result { - Ok(_) => $result, - Err(ref error) => { - let error_message = - format!("Error executing {:?}: {}", stringify!($result), &error); - match $level { - $crate::app::logging::LogLevel::Info => { - Logger::info(error_message); - } - $crate::app::logging::LogLevel::Warning => { - Logger::warn(error_message); - } - $crate::app::logging::LogLevel::Error => { - Logger::error(error_message); - } - } - $result - } - } - }; -} From 5d126ec765b5b60a0b92db7f575052515b6ec209 Mon Sep 17 00:00:00 2001 From: OJarrisonn Date: Thu, 13 Mar 2025 16:13:32 -0300 Subject: [PATCH 4/6] test: adapt utils testing for async setup Adapts the `utils` testing module to support the new async setup function. Signed-off-by: OJarrisonn --- src/utils.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 82df3ddd..bb256a65 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -124,15 +124,21 @@ macro_rules! loading_screen { mod tests { use std::sync::Once; + use crate::logger::{LogLevel, Logger}; + use super::*; static INIT: Once = Once::new(); // Tests can be run in parallel, we don't want to override previously installed hooks - fn setup() { + async fn setup() -> color_eyre::Result<()> { + let (logger, _) = Logger::build("/tmp", LogLevel::Info, 0).await?.spawn(); + INIT.call_once(|| { - install_hooks().expect("Failed to install hooks"); - }) + install_hooks(logger).expect("Failed to install hooks"); + }); + + Ok(()) } #[test] @@ -143,14 +149,14 @@ mod tests { assert!(!super::binary_exists("there_is_no_way_this_binary_exists")); } - #[test] - fn test_install_hooks() { - setup(); + #[tokio::test] + async fn test_install_hooks() -> color_eyre::Result<()> { + setup().await } - #[test] - fn test_error_hook_works() { - setup(); + #[tokio::test] + async fn test_error_hook_works() -> color_eyre::Result<()> { + setup().await?; let result: color_eyre::Result<()> = Err(eyre::eyre!("Test error")); @@ -162,14 +168,17 @@ mod tests { let _ = format!("{:?}", e); } } + + Ok(()) } - #[test] - fn test_panic_hook() { - setup(); + #[tokio::test] + async fn test_panic_hook() -> color_eyre::Result<()> { + setup().await?; let result = std::panic::catch_unwind(|| std::panic!("Test panic")); assert!(result.is_err()); + Ok(()) } } From 6be9fa3739da36d092b9f82ab94c6fbd6c2da00c Mon Sep 17 00:00:00 2001 From: OJarrisonn Date: Fri, 14 Mar 2025 19:03:17 -0300 Subject: [PATCH 5/6] docs: document the `logger` field of `App` Adds a simple description to the `logger` field in the `App` struct Signed-off-by: OJarrisonn --- src/app.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.rs b/src/app.rs index 4d5270f3..6b64bc6d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -52,6 +52,7 @@ pub struct App { /// Client to handle Lore API requests and responses pub lore_api_client: BlockingLoreAPIClient, pub popup: Option>, + /// The logger actor instance that the application will use when logging pub logger: Logger, } From 197ac96aeabc2d576692936d292d515a35edd5bf Mon Sep 17 00:00:00 2001 From: OJarrisonn Date: Sat, 15 Mar 2025 10:20:34 -0300 Subject: [PATCH 6/6] fixup! feat: implement logger actor --- src/app.rs | 10 +- src/cli.rs | 6 +- src/handler.rs | 15 +- src/handler/bookmarked.rs | 3 +- src/handler/details_actions.rs | 3 +- src/handler/edit_config.rs | 6 +- src/handler/latest.rs | 3 +- src/handler/mail_list.rs | 3 +- src/logger.rs | 259 ++++++++++++++------------------- src/main.rs | 6 +- src/ui.rs | 7 +- src/ui/details_actions.rs | 20 +-- src/ui/edit_config.rs | 8 +- src/ui/latest.rs | 6 +- src/ui/mail_list.rs | 6 +- src/ui/navigation_bar.rs | 7 +- src/utils.rs | 10 +- 17 files changed, 154 insertions(+), 224 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6b64bc6d..2674c71e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,5 @@ use crate::{ - logger::LoggerActor, + logger::Logger, ui::popup::{info_popup::InfoPopUp, PopUp}, }; use ansi_to_tui::IntoText; @@ -32,7 +32,7 @@ pub mod screens; /// Type that represents the overall state of the application. It can be viewed /// as the **Model** component of `patch-hub`. -pub struct App { +pub struct App { /// The current active screen pub current_screen: CurrentScreen, /// Screen to navigate and select the mailing lists archived on Lore @@ -56,7 +56,7 @@ pub struct App { pub logger: Logger, } -impl App { +impl App { /// Creates a new instance of `App`. It dynamically loads configurations /// based on precedence (see [crate::app::Config::build]), app data /// (available mailing lists, bookmarked patchsets, reviewed patchsets), and @@ -65,7 +65,7 @@ impl App { /// # Returns /// /// `App` instance with loading configurations and app data. - pub fn new(logger: Logger) -> color_eyre::Result { + pub async fn new(logger: Logger) -> color_eyre::Result { let config: Config = Config::build(); config.create_dirs(); @@ -84,7 +84,7 @@ impl App { // Initialize the logger before the app starts logger.info("patch-hub started"); - logger.collect_garbage(); + logger.collect_garbage().await; Ok(App { current_screen: CurrentScreen::MailingListSelection, diff --git a/src/cli.rs b/src/cli.rs index 4b7de001..b90a6adf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use clap::Parser; use color_eyre::eyre::eyre; use ratatui::{prelude::Backend, Terminal}; -use crate::{app::App, logger::LoggerActor, utils}; +use crate::{app::App, logger::Logger, utils}; #[derive(Debug, Parser)] #[command(version, about)] @@ -21,8 +21,8 @@ impl Cli { pub fn resolve( &self, terminal: Terminal, - app: &mut App, - logger: impl LoggerActor, + app: &mut App, + logger: Logger, ) -> ControlFlow, Terminal> { if self.show_configs { logger.info("Printing current configurations"); diff --git a/src/handler.rs b/src/handler.rs index 44dd6533..4b161049 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -12,7 +12,7 @@ use std::{ use crate::{ app::{screens::CurrentScreen, App}, loading_screen, - logger::LoggerActor, + logger::Logger, ui::draw_ui, }; @@ -30,7 +30,7 @@ use ratatui::{ fn key_handling( mut terminal: Terminal, - app: &mut App, + app: &mut App, key: KeyEvent, ) -> color_eyre::Result>> where @@ -64,10 +64,7 @@ where Ok(ControlFlow::Continue(terminal)) } -fn logic_handling( - mut terminal: Terminal, - app: &mut App, -) -> color_eyre::Result> +fn logic_handling(mut terminal: Terminal, app: &mut App) -> color_eyre::Result> where B: Backend + Send + 'static, { @@ -106,11 +103,7 @@ where Ok(terminal) } -pub fn run_app( - mut terminal: Terminal, - mut app: App, - logger: impl LoggerActor, -) -> color_eyre::Result<()> +pub fn run_app(mut terminal: Terminal, mut app: App, logger: Logger) -> color_eyre::Result<()> where B: Backend + Send + 'static, { diff --git a/src/handler/bookmarked.rs b/src/handler/bookmarked.rs index 3acd6bf5..2896540e 100644 --- a/src/handler/bookmarked.rs +++ b/src/handler/bookmarked.rs @@ -3,7 +3,6 @@ use std::ops::ControlFlow; use crate::{ app::{screens::CurrentScreen, App}, loading_screen, - logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; use ratatui::{ @@ -13,7 +12,7 @@ use ratatui::{ }; pub fn handle_bookmarked_patchsets( - app: &mut App, + app: &mut App, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> diff --git a/src/handler/details_actions.rs b/src/handler/details_actions.rs index c0f1004d..b7ca73a5 100644 --- a/src/handler/details_actions.rs +++ b/src/handler/details_actions.rs @@ -2,7 +2,6 @@ use std::time::Duration; use crate::{ app::{screens::CurrentScreen, App}, - logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp}, utils, }; @@ -15,7 +14,7 @@ use ratatui::{ use super::wait_key_press; pub fn handle_patchset_details( - app: &mut App, + app: &mut App, key: KeyEvent, terminal: &mut Terminal, ) -> color_eyre::Result<()> { diff --git a/src/handler/edit_config.rs b/src/handler/edit_config.rs index 218c3793..405f6d5d 100644 --- a/src/handler/edit_config.rs +++ b/src/handler/edit_config.rs @@ -1,14 +1,10 @@ use crate::{ app::{screens::CurrentScreen, App}, - logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; use ratatui::crossterm::event::{KeyCode, KeyEvent}; -pub fn handle_edit_config( - app: &mut App, - key: KeyEvent, -) -> color_eyre::Result<()> { +pub fn handle_edit_config(app: &mut App, key: KeyEvent) -> color_eyre::Result<()> { if let Some(edit_config_state) = app.edit_config.as_mut() { match edit_config_state.is_editing() { true => match key.code { diff --git a/src/handler/latest.rs b/src/handler/latest.rs index 688ecd4f..42102aac 100644 --- a/src/handler/latest.rs +++ b/src/handler/latest.rs @@ -3,7 +3,6 @@ use std::ops::ControlFlow; use crate::{ app::{screens::CurrentScreen, App}, loading_screen, - logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; use ratatui::{ @@ -13,7 +12,7 @@ use ratatui::{ }; pub fn handle_latest_patchsets( - app: &mut App, + app: &mut App, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> diff --git a/src/handler/mail_list.rs b/src/handler/mail_list.rs index a9f3e55f..ecfd0396 100644 --- a/src/handler/mail_list.rs +++ b/src/handler/mail_list.rs @@ -3,7 +3,6 @@ use std::ops::ControlFlow; use crate::{ app::{screens::CurrentScreen, App}, loading_screen, - logger::LoggerActor, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; use ratatui::{ @@ -13,7 +12,7 @@ use ratatui::{ }; pub fn handle_mailing_list_selection( - app: &mut App, + app: &mut App, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> diff --git a/src/logger.rs b/src/logger.rs index 90c70eff..4352fc44 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -63,7 +63,7 @@ impl Display for LogMessage { /// to the log file. /// /// You're not supossed to use an instance of this directly, but use -/// [`LoggerTx`] instead by calling [`spawn`] as soon as this struct is built. +/// [`Logger`] instead by calling [`spawn`] as soon as this struct is built. /// /// The expected flow is: /// - Instantiate the logger with [`build`] @@ -72,15 +72,15 @@ impl Display for LogMessage { /// - Flush the log buffer to the stderr and finish the logger with [`flush`] /// /// [`Config`]: super::config::Config -/// [`info`]: LoggerTx::info -/// [`warn`]: LoggerTx::warn -/// [`error`]: LoggerTx::error -/// [`flush`]: LoggerTx::flush +/// [`info`]: Logger::info +/// [`warn`]: Logger::warn +/// [`error`]: Logger::error +/// [`flush`]: Logger::flush /// [`stderr`]: std::io::stderr -/// [`spawn`]: Logger::spawn -/// [`build`]: Logger::build +/// [`spawn`]: LoggerCore::spawn +/// [`build`]: LoggerCore::build #[derive(Debug)] -pub struct Logger { +pub struct LoggerCore { log_dir: PathBuf, log_file_path: PathBuf, log_file: File, @@ -90,7 +90,7 @@ pub struct Logger { max_age: usize, } -impl Logger { +impl LoggerCore { /// Creates a new logger instance. The parameters are the [dir] where the /// log files will be stored, [level] of log messages, and [max_age] of the /// log files in days. @@ -137,16 +137,16 @@ impl Logger { }) } - /// Transforms the logger instance into an actor. This method returns a - /// [`LoggerTx`] and a [`JoinHandle`] that can be used to send commands to - /// the logger or await for it to finish (when a [`flush`] is performed, - /// for instance). + /// Transforms the logger core instance into an actor. This method returns a + /// [`Logger`] and a [`JoinHandle`] that can be used to send commands to the + /// logger or await for it to finish (when a [`flush`] is performed, for + /// instance). /// /// The handling of the commandds received is done sequentially, so a /// command is only processed once the previous one is finished. /// - /// [`flush`]: LoggerTx::flush - pub fn spawn(mut self) -> (LoggerTx, JoinHandle<()>) { + /// [`flush`]: Logger::flush + pub fn spawn(mut self) -> (Logger, JoinHandle<()>) { let (tx, mut rx) = tokio::sync::mpsc::channel(100); let handle = tokio::spawn(async move { while let Some(command) = rx.recv().await { @@ -166,7 +166,7 @@ impl Logger { } }); - (LoggerTx(tx), handle) + (Logger::Default(tx), handle) } /// Given a [`LogMessage`] object, writes it to the current and latest log @@ -174,7 +174,7 @@ impl Logger { /// buffer to be printed to [`stderr`] when a [`flush`] is performed. /// /// [`stderr`]: std::io::stderr - /// [`flush`]: LoggerTx::flush + /// [`flush`]: Logger::flush async fn log(&mut self, message: LogMessage) { self.log_file .write_all(format!("{}\n", &message).as_bytes()) @@ -222,8 +222,8 @@ impl Logger { /// A log file is a file in the [`log_dir`] and it will be deleted if its /// older than [`max_age`] days. /// - /// [`log_dir`]: Logger::log_dir - /// [`max_age`]: Logger::max_age + /// [`log_dir`]: LoggerCore::log_dir + /// [`max_age`]: LoggerCore::max_age async fn collect_garbage(&mut self) { if self.max_age == 0 { return; @@ -284,7 +284,7 @@ impl Logger { /// executed synchronously in the same order that they were sent through the /// channel #[derive(Debug, Clone, PartialEq, Eq)] -enum Command { +pub enum Command { /// Logs the payload message Log(LogMessage), /// Flushes the logger by closing the log file, printing critical errors to @@ -296,107 +296,50 @@ enum Command { } /// The transmitter that sends messages down to a logger actor. This is what -/// you're supossed to use accross the code to log messages, instead of Logger. +/// you're supossed to use accross the code to log messages, not LoggerCore. /// Cloning it is cheap so do not feel afraid to pass it around. /// -/// The transmitter is obtained by calling [`spawn`] on a [`Logger`] instance, -/// consuming it and creating a dedicated task for it. Use the methods of this -/// struct to interact with the logger. +/// The transmitter is obtained by calling [`spawn`] on a [`LoggerCore`] +/// instance, consuming it and creating a dedicated task for it. Use the methods +/// of this struct to interact with the logger. /// /// The intended usage is: -/// - Instantiate the logger with [`Logger::build`] -/// - Spawn the logger actor with [`Logger::spawn`] +/// - Instantiate the logger with [`LoggerCore::build`] +/// - Spawn the logger actor with [`LoggerCore::spawn`] /// - Use the methods of this struct to log messages -/// - Use the method [`flush`] to print the log messages to [`stderr`] and -/// finish the logger +/// - Use the method [`flush`] to print the log messages to [`stderr`] +/// and finish the logger /// -/// [`spawn`]: Logger::spawn -/// [`flush`]: LoggerTx::flush +/// [`spawn`]: LoggerCore::spawn +/// [`flush`]: Logger::flush /// [`stderr`]: std::io::stderr #[derive(Debug, Clone)] -pub struct LoggerTx(Sender); +pub enum Logger { + /// The default version (produced by [`LoggerCore::spawn`]) + Default(Sender), + /// The mock version of this logger which won't do nothing at all + #[allow(dead_code)] + Mock, +} -/// A mock implementation of [`LoggerActor`] -/// This is version of [`LoggerTx`] that does nothing at all -#[derive(Debug, Clone, Copy)] -pub struct MockLoggerTx; +impl From for Logger { + fn from(value: LoggerCore) -> Self { + value.spawn().0 + } +} -/// The logger actor trait. This is useful so multiple different implementations -/// of the same actor can be used everywhere. -/// -/// The idea is that one always use this `LoggerExt` trait where a Logger actor -/// is needed -pub trait LoggerActor: Sized { +impl Logger { /// Helper to simplify the logging process. This method sends a /// [`LogMessage`] to the logger. Will send the message in a new task so it /// won't block the caller /// /// # Panics /// If the logger was flushed - fn log(&self, message: String, level: LogLevel); - - /// Log a message with the `INFO` level - /// - /// # Panics - /// If the logger was flushed - fn info(&self, message: M); - - /// Log a message with the `WARNING` level - /// - /// # Panics - /// If the logger was flushed - fn warn(&self, message: M); - - /// Log a message with the `ERROR` level - fn error(&self, message: M); - - /// Log an info message if the result is an error - /// and return the result as is - /// - /// # Panics - /// If the logger was flushed - #[allow(dead_code)] - fn info_on_error(&self, result: Result) -> Result; - - /// Log an warning message if the result is an error - /// and return the result as is - /// - /// # Panics - /// If the logger was flushed - #[allow(dead_code)] - fn warn_on_error(&self, result: Result) -> Result; - - /// Log an error message if the result is an error - /// and return the result as is - /// - /// # Panics - /// If the logger was flushed - fn error_on_error(&self, result: Result) -> Result; - - /// Flushes the logger by printing its messages to [`stderr`] and closing - /// the log file. After this method is called, the logger is destroyed and - /// any attempt to use it will panic. - /// - /// # Panics - /// If called twice - /// - /// [`stderr`]: std::io::stderr - fn flush(self) -> JoinHandle<()>; - - /// Collects the garbage from the logs directory. Garbage logs are the ones - /// older than the [`max_age`] set during the logger [`build`]. - /// - /// # Panics - /// If called after a flush - /// - /// [`build`]: Logger::build - /// [`max_age`]: Logger::max_age - fn collect_garbage(&self) -> JoinHandle<()>; -} - -impl LoggerActor for LoggerTx { fn log(&self, message: String, level: LogLevel) { - let sender = self.0.clone(); + let sender = match self { + Logger::Mock => return, + Logger::Default(sender) => sender.clone(), + }; tokio::spawn(async move { sender @@ -409,19 +352,34 @@ impl LoggerActor for LoggerTx { }); } - fn info(&self, message: M) { + /// Log a message with the `INFO` level + /// + /// # Panics + /// If the logger was flushed + pub fn info(&self, message: M) { self.log(message.to_string(), LogLevel::Info); } - fn warn(&self, message: M) { + /// Log a message with the `WARNING` level + /// + /// # Panics + /// If the logger was flushed + pub fn warn(&self, message: M) { self.log(message.to_string(), LogLevel::Warning); } - fn error(&self, message: M) { + /// Log a message with the `ERROR` level + pub fn error(&self, message: M) { self.log(message.to_string(), LogLevel::Error); } - fn info_on_error(&self, result: Result) -> Result { + /// Log an info message if the result is an error + /// and return the result as is + /// + /// # Panics + /// If the logger was flushed + #[allow(dead_code)] + pub fn info_on_error(&self, result: Result) -> Result { match result { Ok(value) => Ok(value), Err(err) => { @@ -431,7 +389,13 @@ impl LoggerActor for LoggerTx { } } - fn warn_on_error(&self, result: Result) -> Result { + /// Log an warning message if the result is an error + /// and return the result as is + /// + /// # Panics + /// If the logger was flushed + #[allow(dead_code)] + pub fn warn_on_error(&self, result: Result) -> Result { match result { Ok(value) => Ok(value), Err(err) => { @@ -441,7 +405,12 @@ impl LoggerActor for LoggerTx { } } - fn error_on_error(&self, result: Result) -> Result { + /// Log an error message if the result is an error + /// and return the result as is + /// + /// # Panics + /// If the logger was flushed + pub fn error_on_error(&self, result: Result) -> Result { match result { Ok(value) => Ok(value), Err(err) => { @@ -451,53 +420,43 @@ impl LoggerActor for LoggerTx { } } - fn flush(self) -> JoinHandle<()> { - tokio::spawn(async move { - self.0 - .send(Command::Flush) - .await - .expect("Flushing a logger twice"); - }) - } - - fn collect_garbage(&self) -> JoinHandle<()> { - let sender = self.0.clone(); + /// Flushes the logger by printing its messages to [`stderr`] and closing + /// the log file. After this method is called, the logger is destroyed and + /// any attempt to use it will panic. + /// + /// # Panics + /// If called twice + /// + /// [`stderr`]: std::io::stderr + pub fn flush(self) -> JoinHandle<()> { + let Self::Default(sender) = self else { + return tokio::spawn(async {}); + }; tokio::spawn(async move { sender - .send(Command::CollectGarbage) + .send(Command::Flush) .await - .expect("Attemp to use logger after a flush"); + .expect("Flushing a logger twice"); }) } -} - -impl LoggerActor for MockLoggerTx { - fn log(&self, _message: String, _level: LogLevel) {} - - fn info(&self, _message: M) {} - - fn warn(&self, _message: M) {} - - fn error(&self, _message: M) {} - - fn info_on_error(&self, result: Result) -> Result { - result - } - fn warn_on_error(&self, result: Result) -> Result { - result - } - - fn error_on_error(&self, result: Result) -> Result { - result - } - - fn flush(self) -> JoinHandle<()> { - tokio::spawn(async move {}) - } + /// Collects the garbage from the logs directory. Garbage logs are the ones + /// older than the [`max_age`] set during the logger [`build`]. + /// + /// # Panics + /// If called after a flush + /// + /// [`build`]: Logger::build + /// [`max_age`]: Logger::max_age + pub async fn collect_garbage(&self) { + let Self::Default(sender) = self else { + return; + }; - fn collect_garbage(&self) -> JoinHandle<()> { - tokio::spawn(async move {}) + sender + .send(Command::CollectGarbage) + .await + .expect("Attemp to use logger after a flush") } } diff --git a/src/main.rs b/src/main.rs index af74718d..0d181916 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use crate::app::App; use clap::Parser; use cli::Cli; use handler::run_app; -use logger::{LogLevel, Logger, LoggerActor}; +use logger::{LogLevel, LoggerCore}; mod app; mod cli; @@ -17,11 +17,11 @@ mod utils; async fn main() -> color_eyre::Result<()> { let args = Cli::parse(); - let (logger, _) = Logger::build("/tmp", LogLevel::Info, 0).await?.spawn(); + let (logger, _) = LoggerCore::build("/tmp", LogLevel::Info, 0).await?.spawn(); utils::install_hooks(logger.clone())?; let mut terminal = utils::init()?; - let mut app = App::new(logger.clone())?; + let mut app = App::new(logger.clone())?.await; match args.resolve(terminal, &mut app, logger.clone()) { ControlFlow::Break(b) => return b, diff --git a/src/ui.rs b/src/ui.rs index 8308e0f4..a01d4c68 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,4 @@ -use crate::{ - app::{screens::CurrentScreen, App}, - logger::LoggerActor, -}; +use crate::app::{screens::CurrentScreen, App}; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Style, Stylize}, @@ -19,7 +16,7 @@ mod mail_list; mod navigation_bar; pub mod popup; -pub fn draw_ui(f: &mut Frame, app: &App) { +pub fn draw_ui(f: &mut Frame, app: &App) { // Clear the whole screen for sanitizing reasons f.render_widget(Clear, f.area()); diff --git a/src/ui/details_actions.rs b/src/ui/details_actions.rs index 03e84be6..92a4020d 100644 --- a/src/ui/details_actions.rs +++ b/src/ui/details_actions.rs @@ -6,12 +6,9 @@ use ratatui::{ Frame, }; -use crate::{ - app::{ - screens::details_actions::{DetailsActions, PatchsetAction}, - App, - }, - logger::LoggerActor, +use crate::app::{ + screens::details_actions::{DetailsActions, PatchsetAction}, + App, }; /// Returns a `Line` type that represents a line containing stats about reply @@ -50,12 +47,7 @@ fn review_trailers_details(details_actions: &DetailsActions) -> Line<'static> { ]) } -fn render_details_and_actions( - f: &mut Frame, - app: &App, - details_chunk: Rect, - actions_chunk: Rect, -) { +fn render_details_and_actions(f: &mut Frame, app: &App, details_chunk: Rect, actions_chunk: Rect) { let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); let mut staged_to_reply = String::new(); @@ -207,7 +199,7 @@ fn render_details_and_actions( f.render_widget(patchset_actions, actions_chunk); } -fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { +fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); let preview_index = patchset_details_and_actions.preview_index; @@ -250,7 +242,7 @@ fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { f.render_widget(patch_preview, chunk); } -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); if patchset_details_and_actions.preview_fullscreen { diff --git a/src/ui/edit_config.rs b/src/ui/edit_config.rs index edc0fc00..12e6ef90 100644 --- a/src/ui/edit_config.rs +++ b/src/ui/edit_config.rs @@ -6,9 +6,9 @@ use ratatui::{ Frame, }; -use crate::{app::App, logger::LoggerActor}; +use crate::app::App; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let edit_config = app.edit_config.as_ref().unwrap(); let mut constraints = Vec::new(); @@ -63,7 +63,7 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { } } -pub fn mode_footer_text(app: &App) -> Vec { +pub fn mode_footer_text(app: &App) -> Vec { let edit_config_state = app.edit_config.as_ref().unwrap(); vec![if edit_config_state.is_editing() { Span::styled("Editing...", Style::default().fg(Color::LightYellow)) @@ -72,7 +72,7 @@ pub fn mode_footer_text(app: &App) -> Vec { }] } -pub fn keys_hint(app: &App) -> Span { +pub fn keys_hint(app: &App) -> Span { let edit_config_state = app.edit_config.as_ref().unwrap(); match edit_config_state.is_editing() { true => Span::styled( diff --git a/src/ui/latest.rs b/src/ui/latest.rs index 352b1593..4288543c 100644 --- a/src/ui/latest.rs +++ b/src/ui/latest.rs @@ -1,4 +1,4 @@ -use crate::{app::App, logger::LoggerActor}; +use crate::app::App; use patch_hub::lore::patch::Patch; use ratatui::{ layout::Rect, @@ -8,7 +8,7 @@ use ratatui::{ Frame, }; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let page_number = app.latest_patchsets.as_ref().unwrap().page_number(); let patchset_index = app.latest_patchsets.as_ref().unwrap().patchset_index(); let mut list_items = Vec::::new(); @@ -67,7 +67,7 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { f.render_stateful_widget(list, chunk, &mut list_state); } -pub fn mode_footer_text(app: &App) -> Vec { +pub fn mode_footer_text(app: &App) -> Vec { vec![Span::styled( format!( "Latest Patchsets from {} (page {})", diff --git a/src/ui/mail_list.rs b/src/ui/mail_list.rs index 3814d17a..16686db1 100644 --- a/src/ui/mail_list.rs +++ b/src/ui/mail_list.rs @@ -6,9 +6,9 @@ use ratatui::{ Frame, }; -use crate::{app::App, logger::LoggerActor}; +use crate::app::App; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let highlighted_list_index = app.mailing_list_selection.highlighted_list_index; let mut list_items = Vec::::new(); @@ -50,7 +50,7 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { f.render_stateful_widget(list, chunk, &mut list_state); } -pub fn mode_footer_text(app: &App) -> Vec { +pub fn mode_footer_text(app: &App) -> Vec { let mut text_area = Span::default(); if app.mailing_list_selection.target_list.is_empty() { diff --git a/src/ui/navigation_bar.rs b/src/ui/navigation_bar.rs index 225f00ca..b601bf97 100644 --- a/src/ui/navigation_bar.rs +++ b/src/ui/navigation_bar.rs @@ -1,8 +1,5 @@ use super::{bookmarked, details_actions, edit_config, latest, mail_list}; -use crate::{ - app::{self, App}, - logger::LoggerActor, -}; +use crate::app::{self, App}; use app::screens::CurrentScreen; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, @@ -11,7 +8,7 @@ use ratatui::{ Frame, }; -pub fn render(f: &mut Frame, app: &App, chunk: Rect) { +pub fn render(f: &mut Frame, app: &App, chunk: Rect) { let mode_footer_text = match app.current_screen { CurrentScreen::MailingListSelection => mail_list::mode_footer_text(app), CurrentScreen::BookmarkedPatchsets => bookmarked::mode_footer_text(), diff --git a/src/utils.rs b/src/utils.rs index bb256a65..25896d41 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,7 +13,7 @@ use ratatui::{ Terminal, }; -use crate::logger::{LoggerActor, LoggerTx}; +use crate::logger::Logger; /// A type alias for the terminal type used in this application pub type Tui = Terminal>; @@ -34,7 +34,7 @@ pub fn restore() -> io::Result<()> { /// This replaces the standard color_eyre panic and error hooks with hooks that /// restore the terminal before printing the panic or error. -pub fn install_hooks(logger: LoggerTx) -> color_eyre::Result<()> { +pub fn install_hooks(logger: Logger) -> color_eyre::Result<()> { let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks(); // convert from a color_eyre PanicHook to a standard panic hook @@ -42,7 +42,7 @@ pub fn install_hooks(logger: LoggerTx) -> color_eyre::Result<()> { panic::set_hook(Box::new(move |panic_info| { restore().unwrap(); // TODO: await for the flush to finish - ::clone(&logger).flush(); + ::clone(&logger).flush(); panic_hook(panic_info); })); @@ -124,7 +124,7 @@ macro_rules! loading_screen { mod tests { use std::sync::Once; - use crate::logger::{LogLevel, Logger}; + use crate::logger::{LogLevel, LoggerCore}; use super::*; @@ -132,7 +132,7 @@ mod tests { // Tests can be run in parallel, we don't want to override previously installed hooks async fn setup() -> color_eyre::Result<()> { - let (logger, _) = Logger::build("/tmp", LogLevel::Info, 0).await?.spawn(); + let (logger, _) = LoggerCore::build("/tmp", LogLevel::Info, 0).await?.spawn(); INIT.call_once(|| { install_hooks(logger).expect("Failed to install hooks");