quinjet 0.0.69

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

use image::{DynamicImage, RgbaImage};
use ratatui::buffer::CellDiffOption;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui_image::picker::cap_parser::QueryStdioOptions;
use ratatui_image::picker::{Picker, ProtocolType};
use ratatui_image::protocol::Protocol;
use ratatui_image::{Image, Resize};
use unicode_width::UnicodeWidthStr;

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

mod kitty;
use kitty::KittyPlacement;

static IMAGE_PICKER: OnceLock<(Picker, ImageProtocol)> = OnceLock::new();
thread_local! {
    static ENCODED_IMAGES: RefCell<Vec<EncodedImage>> = const { RefCell::new(Vec::new()) };
}

struct EncodedImage {
    raster: Arc<ImageRaster>,
    protocol: ImageProtocol,
    width: u16,
    height: u16,
    display: NativeDisplay,
}

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));
    }
}

pub(crate) fn initialize_image_picker() {
    let _state = IMAGE_PICKER.get_or_init(|| {
        let inferred = ImageProtocol::detect();
        let override_value = std::env::var("QUINJET_IMAGE_PROTOCOL")
            .unwrap_or_default()
            .to_ascii_lowercase();
        let query = should_query(inferred, &override_value);
        let picker = if query {
            Picker::from_query_stdio_with_options(QueryStdioOptions {
                timeout: Duration::from_millis(500),
                text_sizing_protocol: false,
            })
            .unwrap_or_else(|_| Picker::halfblocks())
        } else {
            Picker::halfblocks()
        };
        let protocol = choose_protocol(inferred, &override_value, picker.protocol_type());
        (picker, protocol)
    });
}

fn should_query(inferred: ImageProtocol, override_value: &str) -> bool {
    inferred.is_native() || override_value == "auto"
}

fn choose_protocol(
    inferred: ImageProtocol,
    override_value: &str,
    queried: ProtocolType,
) -> ImageProtocol {
    if override_value == "halfblocks" || inferred.is_native() {
        return inferred;
    }
    match queried {
        ProtocolType::Kitty => ImageProtocol::Kitty,
        ProtocolType::Iterm2 => ImageProtocol::Iterm2,
        ProtocolType::Sixel => ImageProtocol::Sixel,
        ProtocolType::Halfblocks => ImageProtocol::Halfblocks,
    }
}

pub(super) fn selected_image_protocol() -> ImageProtocol {
    IMAGE_PICKER
        .get()
        .map_or_else(ImageProtocol::detect, |(_, protocol)| *protocol)
}

#[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;
    }
    ENCODED_IMAGES.with(|cache| {
        let mut cache = cache.borrow_mut();
        let position = cache.iter().position(|entry| {
            Arc::ptr_eq(&entry.raster, raster)
                && entry.protocol == protocol
                && entry.width == width
                && entry.height == height
        });
        if let Some(position) = position {
            let Some(entry) = cache.get_mut(position) else {
                return false;
            };
            return render_native(frame, area, preview.row, remaining_height, entry);
        }
        let Some(image) = raster_dynamic(raster) else {
            return false;
        };
        let mut picker = IMAGE_PICKER
            .get()
            .map_or_else(Picker::halfblocks, |(picker, _)| picker.clone());
        picker.set_protocol_type(match protocol {
            ImageProtocol::Kitty => ProtocolType::Kitty,
            ImageProtocol::Iterm2 => ProtocolType::Iterm2,
            ImageProtocol::Sixel => ProtocolType::Sixel,
            ImageProtocol::Halfblocks => ProtocolType::Halfblocks,
        });
        let Ok(encoded) =
            picker.new_protocol(image, Rect::new(0, 0, width, height), Resize::Fit(None))
        else {
            return false;
        };
        if cache.len() == 4 {
            drop(cache.remove(0));
        }
        let display = if protocol == ImageProtocol::Kitty {
            let Some(placement) = KittyPlacement::new(&encoded) else {
                return false;
            };
            NativeDisplay::Kitty(placement)
        } else {
            NativeDisplay::Other(encoded)
        };
        cache.push(EncodedImage {
            raster: Arc::clone(raster),
            protocol,
            width,
            height,
            display,
        });
        let Some(entry) = cache.last_mut() else {
            return false;
        };
        render_native(frame, area, preview.row, remaining_height, entry)
    })
}

fn render_native(
    frame: &mut Frame<'_>,
    area: Rect,
    row_offset: u16,
    remaining_height: u16,
    entry: &mut EncodedImage,
) -> bool {
    match &mut entry.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.width, entry.height),
            );
            for row in 0..entry.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;