#[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 {
CellBased,
HighRes,
}
pub struct BackgroundManager {
#[cfg(feature = "background-images")]
source_image: Option<DynamicImage>,
#[cfg(feature = "background-images")]
resized_buffer: Option<image::RgbaImage>,
last_size: (u16, u16),
opacity: f32,
render_mode: RenderMode,
terminal_type: TerminalType,
}
impl BackgroundManager {
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,
}
}
pub fn render_mode(&self) -> RenderMode {
self.render_mode
}
pub fn set_render_mode(&mut self, mode: RenderMode) {
self.render_mode = mode;
tracing::info!("Render mode manually set to: {:?}", mode);
}
#[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);
self.last_size = (0, 0);
Ok(())
}
Err(e) => Err(format!("Failed to load image: {}", e)),
}
}
#[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())
}
pub fn set_opacity(&mut self, opacity: f32) {
self.opacity = opacity.clamp(0.0, 1.0);
}
#[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);
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);
}
}
#[cfg(not(feature = "background-images"))]
pub fn resize_if_needed(&mut self, _width: u16, _height: u16) {
}
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),
Color::Reset => (0, 0, 0),
Color::Indexed(_) => (0, 0, 0),
}
}
#[cfg(feature = "background-images")]
pub fn blend(&self, x: u16, y: u16, _fg: Color, bg: Color) -> Color {
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;
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
}
#[cfg(not(feature = "background-images"))]
pub fn blend(&self, _x: u16, _y: u16, _fg: Color, bg: Color) -> Color {
bg
}
#[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);
}
}
}
#[cfg(not(feature = "background-images"))]
pub fn process_buffer(&mut self, _buffer: &mut ratatui::buffer::Buffer) {
}
#[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 => {
tracing::warn!("Kitty graphics protocol not yet implemented, falling back to cell-based");
None
}
GraphicsCapability::CellBased => None,
}
}
#[cfg(not(feature = "background-images"))]
pub fn render_highres(&self, _width_cells: u16, _height_cells: u16) -> Option<String> {
None
}
}