quinjet 0.0.72

A fast, live, keyboard-first Git source-control interface for the terminal
use std::num::NonZeroU16;
use std::sync::{Arc, OnceLock};

use image::{DynamicImage, Rgba, RgbaImage, imageops};
use ratatui::buffer::CellDiffOption;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui_image::protocol::halfblocks::Halfblocks;
use ratatui_image::protocol::iterm2::Iterm2;
use ratatui_image::protocol::kitty::Kitty;
use ratatui_image::protocol::sixel::Sixel;
use ratatui_image::protocol::{ImageSource, Protocol};
use ratatui_image::{Image, Resize};
use unicode_width::UnicodeWidthStr;

use super::{App, DiffLine, Frame, Rect, Theme};
use crate::git::diff::{ImagePreview, ImageProtocol, ImageRaster, ImageSide};

mod kitty;
use kitty::KittyPlacement;
pub(super) mod picker;
use picker::ImagePicker;
pub(super) use picker::selected_image_protocol;
pub(super) mod preparation;
pub(super) use preparation::PreparationFrame;
use preparation::{EncodedImage, IMAGE_PREPARATION, ImageKey};

static IMAGE_PICKER: OnceLock<(ImagePicker, ImageProtocol)> = OnceLock::new();

pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, theme: &Theme) {
    let _images_ready = crate::ui::image_preparation_ready();
    let _image_frame = PreparationFrame::begin();
    super::layout::draw(frame, app, theme);
}

enum NativeDisplay {
    Kitty(KittyPlacement),
    Other(Protocol),
}

#[derive(Default)]
pub(super) struct ImageDrawState {
    active: Vec<Arc<ImageRaster>>,
    allow_other_native: bool,
}

impl ImageDrawState {
    pub(super) const fn new(allow_other_native: bool) -> Self {
        Self {
            active: Vec::new(),
            allow_other_native,
        }
    }

    fn contains(&self, raster: &Arc<ImageRaster>) -> bool {
        self.active.iter().any(|active| Arc::ptr_eq(active, raster))
    }

    fn remember(&mut self, raster: &Arc<ImageRaster>) {
        self.active.push(Arc::clone(raster));
    }
}

#[expect(
    clippy::too_many_arguments,
    reason = "image rows need both viewport and frame state"
)]
pub(super) fn draw_image_line(
    frame: &mut Frame<'_>,
    area: Rect,
    line: &DiffLine,
    remaining_height: u16,
    theme: &Theme,
    protocol: ImageProtocol,
    state: &mut ImageDrawState,
) {
    if area.width == 0 {
        return;
    }
    let Some(preview) = line.image.as_ref() else {
        draw_caption(frame, area, &line.text(), theme);
        return;
    };
    if protocol.is_native()
        && let Some(raster) = preview.raster.as_ref()
    {
        if state.contains(raster) {
            return;
        }
        if (protocol == ImageProtocol::Kitty
            || (state.allow_other_native && preview.row == 0 && remaining_height >= preview.rows))
            && draw_native(frame, area, preview, protocol, remaining_height)
        {
            state.remember(raster);
            return;
        }
    }
    if preview.cells.is_empty() {
        draw_caption(frame, area, &preview.caption(), theme);
        return;
    }
    draw_halfblocks(frame, area, preview, theme);
}

fn draw_caption(frame: &mut Frame<'_>, area: Rect, text: &str, theme: &Theme) {
    frame.render_widget(
        Paragraph::new(Line::from(Span::styled(
            text.to_owned(),
            Style::default().fg(theme.muted),
        )))
        .style(Style::default().bg(theme.panel)),
        area,
    );
}

fn draw_halfblocks(frame: &mut Frame<'_>, area: Rect, preview: &ImagePreview, theme: &Theme) {
    let mut spans = Vec::with_capacity(preview.cells.len().saturating_add(1));
    if preview.row == 0 {
        spans.push(Span::styled(
            format!("{} ", preview.caption()),
            Style::default().fg(theme.muted),
        ));
    }
    let used = spans
        .first()
        .map_or(0, |span| UnicodeWidthStr::width(span.content.as_ref()));
    let remaining = (area.width as usize).saturating_sub(used);
    for cell in preview.cells.iter().take(remaining) {
        spans.push(Span::styled(
            "â–€",
            Style::default()
                .fg(Color::Rgb(cell.upper[0], cell.upper[1], cell.upper[2]))
                .bg(Color::Rgb(cell.lower[0], cell.lower[1], cell.lower[2])),
        ));
    }
    frame.render_widget(
        Paragraph::new(Line::from(spans)).style(Style::default().bg(theme.panel)),
        area,
    );
}

fn draw_native(
    frame: &mut Frame<'_>,
    area: Rect,
    preview: &ImagePreview,
    protocol: ImageProtocol,
    remaining_height: u16,
) -> bool {
    let Some(raster) = preview.raster.as_ref() else {
        return false;
    };
    let width = u16::try_from(preview.cells.len())
        .unwrap_or(area.width)
        .min(area.width);
    let height = preview.rows.max(1);
    if width == 0 {
        return false;
    }
    IMAGE_PREPARATION.with(|preparation| {
        let mut preparation = preparation.borrow_mut();
        let key = ImageKey {
            raster: Arc::clone(raster),
            protocol,
            width,
            height,
        };
        let Some(entry) = preparation.display(key) else {
            return false;
        };
        render_native(frame, area, preview.row, remaining_height, entry)
    })
}

fn prepare_native(key: &ImageKey) -> Option<NativeDisplay> {
    let image = raster_dynamic(&key.raster)?;
    let picker = IMAGE_PICKER
        .get()
        .map_or_else(picker::fallback_picker, |(picker, _)| *picker);
    let encoded = encode_native(
        image,
        key.protocol,
        Rect::new(0, 0, key.width, key.height),
        picker,
    )?;
    if key.protocol == ImageProtocol::Kitty {
        KittyPlacement::new(&encoded).map(NativeDisplay::Kitty)
    } else {
        Some(NativeDisplay::Other(encoded))
    }
}

fn encode_native(
    image: DynamicImage,
    protocol: ImageProtocol,
    size: Rect,
    picker: ImagePicker,
) -> Option<Protocol> {
    let source = ImageSource::new(image, picker.font_size, Rgba([0, 0, 0, 0]));
    let (image, area) = match Resize::Fit(None).needs_resize(
        &source,
        picker.font_size,
        source.desired,
        size,
        false,
    ) {
        Some(area) => {
            let width = u32::from(area.width) * u32::from(picker.font_size.0);
            let height = u32::from(area.height) * u32::from(picker.font_size.1);
            let resized = source
                .image
                .resize(width, height, imageops::FilterType::Nearest);
            let mut padded = DynamicImage::new_rgba8(width, height);
            imageops::overlay(&mut padded, &resized, 0, 0);
            (padded, area)
        }
        None => (source.image, source.desired),
    };
    match protocol {
        ImageProtocol::Halfblocks => Halfblocks::new(image, area).ok().map(Protocol::Halfblocks),
        ImageProtocol::Sixel => Sixel::new(image, area, picker.is_tmux)
            .ok()
            .map(Protocol::Sixel),
        ImageProtocol::Kitty => Kitty::new(image, area, rand::random(), picker.is_tmux)
            .ok()
            .map(Protocol::Kitty),
        ImageProtocol::Iterm2 => Iterm2::new(image, area, picker.is_tmux)
            .ok()
            .map(Protocol::ITerm2),
    }
}

fn render_native(
    frame: &mut Frame<'_>,
    area: Rect,
    row_offset: u16,
    remaining_height: u16,
    entry: &mut EncodedImage,
) -> bool {
    let Some(display) = entry.display.as_mut() else {
        return false;
    };
    match display {
        NativeDisplay::Kitty(placement) => {
            placement.render(frame, area, row_offset, remaining_height)
        }
        NativeDisplay::Other(encoded) => {
            frame.render_widget(
                Image::new(encoded),
                Rect::new(area.x, area.y, entry.key.width, entry.key.height),
            );
            for row in 0..entry.key.height {
                if let Some(cell) = frame
                    .buffer_mut()
                    .cell_mut((area.x, area.y.saturating_add(row)))
                {
                    cell.diff_option = CellDiffOption::ForcedWidth(NonZeroU16::MIN);
                }
            }
            true
        }
    }
}

fn raster_dynamic(raster: &ImageRaster) -> Option<DynamicImage> {
    RgbaImage::from_raw(raster.width, raster.height, raster.rgba.clone())
        .map(DynamicImage::ImageRgba8)
}

pub(super) fn image_side(line: &DiffLine) -> Option<ImageSide> {
    line.image.as_ref().map(|preview| preview.side)
}

#[cfg(test)]
mod tests;

#[cfg(test)]
mod encoding_tests;