From 3165905cd71198f555bbbe2a967a307e51eb5930 Mon Sep 17 00:00:00 2001 From: OJarrisonn Date: Sun, 17 Nov 2024 14:51:00 -0300 Subject: [PATCH] feat: patch cover colors This uses similar design for patch rendering. The cover of a patch is everything before the `---` line that separates the email part of a patch and the actual diff. Currently, only `bat` was configured as an option besides the `default` This also includes a config option for the cover renderer Signed-off-by: OJarrisonn --- src/app.rs | 30 ++++++++-- src/app/config.rs | 9 ++- src/app/cover_renderer.rs | 78 +++++++++++++++++++++++++ src/app/screens/edit_config.rs | 16 ++++- src/lore/lore_session.rs | 15 +++++ src/test_samples/app/config/config.json | 1 + 6 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 src/app/cover_renderer.rs diff --git a/src/app.rs b/src/app.rs index a6f7fb9..3bda297 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,6 +2,7 @@ use crate::log_on_error; 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, patch::Patch}; use patch_renderer::{render_patch_preview, PatchRenderer}; @@ -19,6 +20,7 @@ use std::collections::HashMap; use crate::utils; mod config; +pub mod cover_renderer; pub mod logging; pub mod patch_renderer; pub mod screens; @@ -140,18 +142,31 @@ impl App { let mut patches_preview: Vec = Vec::new(); for raw_patch in &raw_patches { let raw_patch = raw_patch.replace('\t', " "); - let patch_preview = - match render_patch_preview(&raw_patch, self.config.patch_renderer()) { + + let (raw_cover, raw_patch) = lore_session::split_cover(&raw_patch); + + 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"); + raw_cover.to_string() + } + }; + + 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", ); - raw_patch + raw_patch.to_string() } - } - .into_text()?; - patches_preview.push(patch_preview); + }; + + patches_preview + .push(format!("{}---\n{}", rendered_cover, rendered_patch).into_text()?); } self.patchset_details_and_actions_state = Some(PatchsetDetailsAndActionsState { representative_patch, @@ -254,6 +269,9 @@ impl App { if let Ok(patch_renderer) = edit_config.extract_patch_renderer() { self.config.set_patch_renderer(patch_renderer.into()) } + if let Ok(cover_renderer) = edit_config.extract_cover_renderer() { + self.config.set_cover_renderer(cover_renderer.into()) + } if let Ok(max_log_age) = edit_config.max_log_age() { self.config.set_max_log_age(max_log_age) } diff --git a/src/app/config.rs b/src/app/config.rs index c1b9b83..da624bf 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -7,7 +7,7 @@ use std::{ path::Path, }; -use super::patch_renderer::PatchRenderer; +use super::{cover_renderer::CoverRenderer, patch_renderer::PatchRenderer}; #[cfg(test)] mod tests; @@ -29,6 +29,8 @@ pub struct Config { data_dir: String, /// Renderer to use for patch previews patch_renderer: PatchRenderer, + /// Renderer to use for patchset covers + cover_renderer: CoverRenderer, /// Maximum age of a log file in days max_log_age: usize, } @@ -47,6 +49,7 @@ impl Config { logs_path: format!("{data_dir}/logs"), git_send_email_options: "--dry-run --suppress-cc=all".to_string(), patch_renderer: Default::default(), + cover_renderer: Default::default(), cache_dir, data_dir, max_log_age: 30, @@ -144,6 +147,10 @@ impl Config { self.patch_renderer = patch_renderer; } + pub fn set_cover_renderer(&mut self, cover_renderer: CoverRenderer) { + self.cover_renderer = cover_renderer; + } + pub fn set_max_log_age(&mut self, max_log_age: usize) { self.max_log_age = max_log_age; } diff --git a/src/app/cover_renderer.rs b/src/app/cover_renderer.rs new file mode 100644 index 0000000..fc30f26 --- /dev/null +++ b/src/app/cover_renderer.rs @@ -0,0 +1,78 @@ +use std::{ + fmt::Display, + io::Write, + process::{Command, Stdio}, +}; + +use serde::{Deserialize, Serialize}; + +use super::logging::Logger; + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)] +pub enum CoverRenderer { + #[default] + #[serde(rename = "default")] + Default, + #[serde(rename = "bat")] + Bat, +} + +impl From for CoverRenderer { + fn from(value: String) -> Self { + match value.as_str() { + "bat" => CoverRenderer::Bat, + _ => CoverRenderer::Default, + } + } +} + +impl From<&str> for CoverRenderer { + fn from(value: &str) -> Self { + match value { + "bat" => CoverRenderer::Bat, + _ => CoverRenderer::Default, + } + } +} + +impl Display for CoverRenderer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CoverRenderer::Default => write!(f, "default"), + CoverRenderer::Bat => write!(f, "bat"), + } + } +} + +pub fn render_cover(raw: &str, renderer: &CoverRenderer) -> color_eyre::Result { + let text = match renderer { + CoverRenderer::Default => Ok(raw.to_string()), + CoverRenderer::Bat => bat_cover_renderer(raw), + }?; + + Ok(text) +} + +/// Renders a .mbx cover using the `bat` command line tool. +/// +/// # Errors +/// +/// If bat isn't installed or if the command fails, an error will be returned. +fn bat_cover_renderer(patch: &str) -> color_eyre::Result { + let mut bat = Command::new("bat") + .arg("-pp") + .arg("-f") + .arg("-l") + .arg("mbx") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .map_err(|e| { + Logger::error(format!("Failed to spawn bat for cover preview: {}", e)); + e + })?; + + bat.stdin.as_mut().unwrap().write_all(patch.as_bytes())?; + let output = bat.wait_with_output()?; + Ok(String::from_utf8(output.stdout)?) +} diff --git a/src/app/screens/edit_config.rs b/src/app/screens/edit_config.rs index 4ed6524..94567ac 100644 --- a/src/app/screens/edit_config.rs +++ b/src/app/screens/edit_config.rs @@ -27,6 +27,10 @@ impl EditConfigState { EditableConfig::PatchRenderer, config.patch_renderer().to_string(), ); + config_buffer.insert( + EditableConfig::CoverRenderer, + config.cover_renderer().to_string(), + ); config_buffer.insert(EditableConfig::MaxLogAge, config.max_log_age().to_string()); EditConfigState { @@ -172,6 +176,11 @@ impl EditConfigState { Ok(patch_renderer) } + pub fn extract_cover_renderer(&mut self) -> Result { + let cover_renderer = self.extract_config_buffer_val(&EditableConfig::CoverRenderer); + Ok(cover_renderer) + } + /// Extracts the max log age from the config /// /// # Errors @@ -195,6 +204,7 @@ enum EditableConfig { DataDir, GitSendEmailOpt, PatchRenderer, + CoverRenderer, MaxLogAge, } @@ -208,7 +218,8 @@ impl TryFrom for EditableConfig { 2 => Ok(EditableConfig::DataDir), 3 => Ok(EditableConfig::GitSendEmailOpt), 4 => Ok(EditableConfig::PatchRenderer), - 5 => Ok(EditableConfig::MaxLogAge), + 5 => Ok(EditableConfig::CoverRenderer), + 6 => Ok(EditableConfig::MaxLogAge), _ => bail!("Invalid index {} for EditableConfig", value), // Handle out of bounds } } @@ -223,6 +234,9 @@ impl Display for EditableConfig { EditableConfig::PatchRenderer => { write!(f, "Patch Renderer (bat, delta, diff-so-fancy)") } + EditableConfig::CoverRenderer => { + write!(f, "Cover Renderer (bat)") + } EditableConfig::GitSendEmailOpt => write!(f, "`git send email` option"), EditableConfig::MaxLogAge => write!(f, "Max Log Age (0 = forever)"), } diff --git a/src/lore/lore_session.rs b/src/lore/lore_session.rs index 6399dbe..d975928 100644 --- a/src/lore/lore_session.rs +++ b/src/lore/lore_session.rs @@ -216,6 +216,21 @@ pub fn split_patchset(patchset_path_str: &str) -> Result, String> { Ok(patches) } +/// Takes the string that represents a patch and splits it into the cover and the actual diff. +/// +/// The cover is everything before the first "---" line. +pub fn split_cover(patch: &str) -> (&str, &str) { + let mut cover: &str = patch; + let mut diff: &str = ""; + + if let Some(cover_end) = patch.find("\n---\n") { + cover = &patch[..cover_end + 1]; + diff = &patch[cover_end + 5..]; + } + + (cover, diff) +} + fn extract_patches(mbox_path: &Path, patches: &mut Vec) { let mut current_patch: String = String::new(); let mut is_reading_patch: bool = false; diff --git a/src/test_samples/app/config/config.json b/src/test_samples/app/config/config.json index 1b4feb4..f6ba197 100644 --- a/src/test_samples/app/config/config.json +++ b/src/test_samples/app/config/config.json @@ -9,5 +9,6 @@ "cache_dir": "/cache_dir", "data_dir": "/data_dir", "patch_renderer": "default", + "cover_renderer": "default", "max_log_age": 42 }