eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Background image manager.
//!
//! Handles loading, resizing, and blending of background images
//! for a "glassmorphism" effect in the terminal.
//!
//! Requires the `background-images` feature to be enabled.

#[cfg(feature = "background-images")]
use image::{DynamicImage, GenericImageView, imageops::FilterType};
use ratatui::style::Color;
use std::path::Path;
use std::sync::{Arc, Mutex};
use super::terminal_detect::{TerminalType, GraphicsCapability};
#[cfg(feature = "background-images")]
use super::iterm2_renderer::ITerm2ImageRenderer;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderMode {
    /// Cell-based color rendering (default, cross-platform)
    CellBased,
    /// High-resolution inline images (iTerm2, Kitty)
    HighRes,
}

/// Manage background image rendering.
pub struct BackgroundManager {
    #[cfg(feature = "background-images")]
    /// Original loaded image
    source_image: Option<DynamicImage>,
    #[cfg(feature = "background-images")]
    /// Resized image matching terminal dimensions
    resized_buffer: Option<image::RgbaImage>,
    /// Last known terminal size
    last_size: (u16, u16),
    /// Opacity (0.0 to 1.0)
    opacity: f32,
    /// Rendering mode
    render_mode: RenderMode,
    /// Detected terminal type
    terminal_type: TerminalType,
}

impl BackgroundManager {
    /// Create a new background manager with automatic terminal detection.
    pub fn new() -> Self {
        let terminal_type = TerminalType::detect();
        let capability = terminal_type.graphics_capability();
        
        let render_mode = match capability {
            GraphicsCapability::ITerm2Inline | GraphicsCapability::KittyGraphics => RenderMode::HighRes,
            GraphicsCapability::CellBased => RenderMode::CellBased,
        };
        
        tracing::info!(
            "Background renderer initialized: terminal={}, mode={:?}",
            terminal_type.name(),
            render_mode
        );
        
        Self {
            #[cfg(feature = "background-images")]
            source_image: None,
            #[cfg(feature = "background-images")]
            resized_buffer: None,
            last_size: (0, 0),
            opacity: 0.3,
            render_mode,
            terminal_type,
        }
    }
    
    /// Get the current render mode.
    pub fn render_mode(&self) -> RenderMode {
        self.render_mode
    }
    
    /// Force a specific render mode (for testing or user override).
    pub fn set_render_mode(&mut self, mode: RenderMode) {
        self.render_mode = mode;
        tracing::info!("Render mode manually set to: {:?}", mode);
    }

    /// Load an image from path or bundled preset.
    /// Supports Paths or "bundled:1", "bundled:2", "bundled:3"
    /// 
    /// Returns an error if the `background-images` feature is not enabled.
    #[cfg(feature = "background-images")]
    pub fn load(&mut self, path_str: &str) -> Result<(), String> {
        let path_str = path_str.trim();
        
        if path_str.starts_with("bundled:") {
            let id = path_str.trim_start_matches("bundled:");
            let bytes = match id {
                "1" => Some(include_bytes!("../../assets/backgrounds/007fe2a95b352fbaae9fc481abbe6209.jpg") as &[u8]),
                "2" => Some(include_bytes!("../../assets/backgrounds/2d15d3e8ffbf35daaa11130845e12599.jpg") as &[u8]),
                "3" => Some(include_bytes!("../../assets/backgrounds/a6b7358705c30ed4e0e5482670446394.jpg") as &[u8]),
                _ => None,
            };
            
            if let Some(data) = bytes {
                tracing::info!("Loading bundled background: {}", id);
                match image::load_from_memory(data) {
                    Ok(img) => {
                        self.source_image = Some(img);
                        self.last_size = (0, 0);
                        return Ok(());
                    }
                    Err(e) => return Err(format!("Failed to load bundled image {}: {}", id, e)),
                }
            } else {
                return Err(format!("Unknown bundled image id: {}", id));
            }
        }
    
        tracing::info!("Loading background image from path: {}", path_str);
        match image::open(path_str) {
            Ok(img) => {
                self.source_image = Some(img);
                // Force resize on next render
                self.last_size = (0, 0);
                Ok(())
            }
            Err(e) => Err(format!("Failed to load image: {}", e)),
        }
    }
    
    /// Load an image (no-op when `background-images` feature is disabled).
    #[cfg(not(feature = "background-images"))]
    pub fn load(&mut self, _path_str: &str) -> Result<(), String> {
        Err("Background images are not supported. Enable the 'background-images' feature.".to_string())
    }

    /// Set opacity.
    pub fn set_opacity(&mut self, opacity: f32) {
        self.opacity = opacity.clamp(0.0, 1.0);
    }

    /// Update resize buffer if needed.
    #[cfg(feature = "background-images")]
    pub fn resize_if_needed(&mut self, width: u16, height: u16) {
        if (width, height) == self.last_size {
            return;
        }

        if let Some(img) = &self.source_image {
            tracing::info!("Resizing background to {}x{}", width, height);
            // In terminals, a character is roughly 1x2 pixels.
            // But we are mapping 1 pixel to 1 cell for background color.
            // So we just resize strictly to width x height.
            let resized = img.resize_exact(width as u32, height as u32, FilterType::Triangle);
            self.resized_buffer = Some(resized.to_rgba8());
            self.last_size = (width, height);
        }
    }
    
    /// Update resize buffer if needed (no-op when feature disabled).
    #[cfg(not(feature = "background-images"))]
    pub fn resize_if_needed(&mut self, _width: u16, _height: u16) {
        // No-op when background images are disabled
    }

    /// Helper to convert Color to RGB tuple.
    fn to_rgb(color: Color) -> (u8, u8, u8) {
        match color {
            Color::Rgb(r, g, b) => (r, g, b),
            Color::Black => (0, 0, 0),
            Color::Red => (170, 0, 0),
            Color::Green => (0, 170, 0),
            Color::Yellow => (170, 85, 0),
            Color::Blue => (0, 0, 170),
            Color::Magenta => (170, 0, 170),
            Color::Cyan => (0, 170, 170),
            Color::Gray => (170, 170, 170),
            Color::DarkGray => (85, 85, 85),
            Color::LightRed => (255, 85, 85),
            Color::LightGreen => (85, 255, 85),
            Color::LightYellow => (255, 255, 85),
            Color::LightBlue => (85, 85, 255),
            Color::LightMagenta => (255, 85, 255),
            Color::LightCyan => (85, 255, 255),
            Color::White => (255, 255, 255),
            // Reset usually implies terminal default. We assume dark terminal (0,0,0).
            Color::Reset => (0, 0, 0),
            // Fallback for Indexed (approximate or just black)
            Color::Indexed(_) => (0, 0, 0),
        }
    }

    /// Apply background blending to a color at specific coordinates.
    #[cfg(feature = "background-images")]
    pub fn blend(&self, x: u16, y: u16, _fg: Color, bg: Color) -> Color {
        // We now blend regardless of the background color to support themes like Nord
        // that set a specific background color (#2E3440).
        
        if let Some(buffer) = &self.resized_buffer {
            if x < self.last_size.0 && y < self.last_size.1 {
                if let Some(pixel) = buffer.get_pixel_checked(x as u32, y as u32) {
                    let img_r = pixel[0] as f32;
                    let img_g = pixel[1] as f32;
                    let img_b = pixel[2] as f32;
                    
                    let (bg_r, bg_g, bg_b) = Self::to_rgb(bg);
                    let bg_r = bg_r as f32;
                    let bg_g = bg_g as f32;
                    let bg_b = bg_b as f32;
                    
                    // Standard Alpha Blending:
                    // Result = Background * (1 - Alpha) + Image * Alpha
                    // Here 'opacity' acts as the Image's Alpha channel coverage over the background.
                    
                    let alpha = self.opacity;
                    let inv_alpha = 1.0 - alpha;
                    
                    let new_r = (bg_r * inv_alpha + img_r * alpha) as u8;
                    let new_g = (bg_g * inv_alpha + img_g * alpha) as u8;
                    let new_b = (bg_b * inv_alpha + img_b * alpha) as u8;
                    
                    return Color::Rgb(new_r, new_g, new_b);
                }
            }
        }

        bg
    }
    
    /// Apply background blending (no-op when feature disabled).
    #[cfg(not(feature = "background-images"))]
    pub fn blend(&self, _x: u16, _y: u16, _fg: Color, bg: Color) -> Color {
        bg
    }
    
    /// Apply blending to a whole buffer.
    #[cfg(feature = "background-images")]
    pub fn process_buffer(&mut self, buffer: &mut ratatui::buffer::Buffer) {
        let area = buffer.area;
        self.resize_if_needed(area.width, area.height);
        
        if self.resized_buffer.is_none() {
            return;
        }

        for y in area.y..area.bottom() {
            for x in area.x..area.right() {
                let cell = buffer.get_mut(x, y);
                cell.bg = self.blend(x, y, cell.fg, cell.bg);
            }
        }
    }
    
    /// Apply blending to a whole buffer (no-op when feature disabled).
    #[cfg(not(feature = "background-images"))]
    pub fn process_buffer(&mut self, _buffer: &mut ratatui::buffer::Buffer) {
        // No-op when background images are disabled
    }
    
    /// Render high-resolution background using terminal-specific protocols.
    /// Returns escape sequence to display the image, or None if not in high-res mode.
    #[cfg(feature = "background-images")]
    pub fn render_highres(&self, width_cells: u16, height_cells: u16) -> Option<String> {
        if self.render_mode != RenderMode::HighRes {
            return None;
        }
        
        let img = self.source_image.as_ref()?;
        
        match self.terminal_type.graphics_capability() {
            GraphicsCapability::ITerm2Inline => {
                match ITerm2ImageRenderer::render_background(img, width_cells, height_cells) {
                    Ok(sequence) => {
                        tracing::debug!("Rendered iTerm2 inline image: {} cells", width_cells * height_cells);
                        Some(sequence)
                    }
                    Err(e) => {
                        tracing::error!("Failed to render iTerm2 image: {}", e);
                        None
                    }
                }
            }
            GraphicsCapability::KittyGraphics => {
                // TODO: Implement Kitty graphics protocol
                tracing::warn!("Kitty graphics protocol not yet implemented, falling back to cell-based");
                None
            }
            GraphicsCapability::CellBased => None,
        }
    }
    
    /// Render high-resolution background (no-op when feature disabled).
    #[cfg(not(feature = "background-images"))]
    pub fn render_highres(&self, _width_cells: u16, _height_cells: u16) -> Option<String> {
        None
    }
}