shellshot 0.5.0

Transform your command-line output into clean, shareable images with a single command.
Documentation
use crate::{
    image_renderer::{ImageRendererError, canvas::Canvas, render_size::Size},
    theme::Theme,
    window_decoration::{no_decoration::NoDecoration, windows::Windows},
};
use ab_glyph::FontArc;
use clap::ValueEnum;
use termwiz::cell::Cell;

mod classic;
pub mod common;
mod no_decoration;
mod windows;

pub use classic::Classic;

/// Type of window decoration to apply around the rendered content
#[derive(Clone, Debug, ValueEnum)]
pub enum WindowDecorationType {
    /// Classic window decoration
    Classic,
    /// Windows-style window decoration
    Windows,
}

#[derive(Clone, Debug)]
pub struct WindowMetrics {
    pub padding: u32,
    pub border_width: u32,
    pub title_bar_height: u32,
}

#[derive(Debug, Clone)]
pub struct Fonts {
    pub regular: FontArc,
    pub bold: FontArc,
    pub italic: FontArc,
    pub bold_italic: FontArc,
}

pub trait WindowDecoration: std::fmt::Debug {
    fn build_command_line(&self, command: &str) -> Vec<Cell>;

    fn compute_metrics(&self, char_size: Size) -> WindowMetrics;

    fn font(&self) -> Result<Fonts, ImageRendererError>;

    fn draw_window(
        &self,
        canvas: &mut Canvas,
        metrics: &WindowMetrics,
        theme: &Theme,
    ) -> Result<(), ImageRendererError>;
}

pub fn create_window_decoration(
    decoration_type: Option<&WindowDecorationType>,
) -> Box<dyn WindowDecoration> {
    match decoration_type {
        Some(WindowDecorationType::Classic) => Box::new(Classic),
        Some(WindowDecorationType::Windows) => Box::new(Windows),
        None => Box::new(NoDecoration),
    }
}

#[cfg(test)]
mod tests {
    use ab_glyph::PxScale;

    use crate::image_renderer::render_size::calculate_char_size;

    use super::*;

    fn all_window_decorations() -> Vec<Option<WindowDecorationType>> {
        let mut types = WindowDecorationType::value_variants()
            .iter()
            .cloned()
            .map(Some)
            .collect::<Vec<_>>();
        types.push(None);
        types
    }

    #[test]
    fn test_all_window_decorations_command_line() {
        for decoration_type in all_window_decorations() {
            let window_decoration = create_window_decoration(decoration_type.as_ref());
            let command_line = window_decoration.build_command_line("echo test");

            assert!(
                !command_line.is_empty(),
                "Unexpected number of cells for {decoration_type:?}",
            );
        }
    }

    #[test]
    fn test_all_window_decorations_draw() {
        let canvas_width = 200;
        let canvas_height = 100;
        let scale = PxScale::from(1.0);
        let theme = Theme::default();

        for decoration_type in all_window_decorations() {
            let window_decoration = create_window_decoration(decoration_type.as_ref());

            let font = window_decoration.font().expect("Font should be available");

            let char_size = calculate_char_size(&font.regular, scale);
            let metrics = window_decoration.compute_metrics(char_size);

            let mut canvas = Canvas::new(canvas_width, canvas_height, font.clone(), scale)
                .expect("Failed to create Canvas");

            let result = window_decoration.draw_window(&mut canvas, &metrics, &theme);
            assert!(
                result.is_ok(),
                "draw_window failed for {decoration_type:?}: {result:?}",
            );
        }
    }
}