quinjet 0.0.71

A fast, live, keyboard-first Git source-control interface for the terminal
use std::ffi::{OsStr, OsString};
use std::io::{self, IsTerminal, Read, Write};
use std::process::ExitCode;
use std::time::Duration;

use anyhow::Result;
use crossterm::terminal::{WindowSize, is_raw_mode_enabled, window_size};
use ratatui_image::picker::cap_parser::{Parser, QueryStdioOptions, Response};

use super::{IMAGE_PICKER, ImageProtocol};

const HELPER_ARGUMENT: &str = "--internal-image-picker-query";
const QUERY_BUDGET: Duration = Duration::from_millis(500);
const MAX_RECORD_BYTES: usize = 64;
const MAX_QUERY_BYTES: u64 = 8192;

#[derive(Debug, Clone, Copy)]
pub(super) struct ImagePicker {
    pub(super) font_size: (u16, u16),
    pub(super) is_tmux: bool,
}

pub(crate) fn image_picker_helper() -> Option<ExitCode> {
    if !helper_requested(wild::args_os().skip(1)) {
        return None;
    }
    Some(if write_query_result().is_ok() {
        ExitCode::SUCCESS
    } else {
        ExitCode::FAILURE
    })
}

fn helper_requested(mut arguments: impl Iterator<Item = OsString>) -> bool {
    arguments.next().as_deref() == Some(OsStr::new(HELPER_ARGUMENT)) && arguments.next().is_none()
}

fn write_query_result() -> Result<()> {
    anyhow::ensure!(
        io::stdin().is_terminal() && io::stdout().is_terminal(),
        "image discovery requires an inherited terminal"
    );
    let record = query_record(
        &mut io::stdin().lock().take(MAX_QUERY_BYTES),
        &mut io::stdout().lock(),
        ImageProtocol::detect(),
        picker_for_window(window_size().ok().as_ref()),
    )?;
    record.write(&mut io::stderr().lock())?;
    Ok(())
}

fn query_record(
    reader: &mut impl Read,
    writer: &mut impl Write,
    inferred: ImageProtocol,
    picker: ImagePicker,
) -> Result<PickerRecord> {
    let query = Parser::query(
        picker.is_tmux,
        QueryStdioOptions {
            timeout: QUERY_BUDGET,
            text_sizing_protocol: false,
        },
    );
    writer.write_all(query.as_bytes())?;
    writer.flush()?;
    let mut parser = Parser::new();
    let mut record = PickerRecord {
        protocol: inferred,
        font_size: picker.font_size,
    };
    let mut bytes = [0; 128];
    loop {
        let count = reader.read(&mut bytes)?;
        anyhow::ensure!(
            count != 0,
            "terminal discovery ended before its status reply"
        );
        for byte in bytes.iter().take(count) {
            for response in parser.push(char::from(*byte)) {
                match response {
                    Response::Kitty if !inferred.is_native() => {
                        record.protocol = ImageProtocol::Kitty;
                    }
                    Response::Sixel
                        if !inferred.is_native() && record.protocol != ImageProtocol::Kitty =>
                    {
                        record.protocol = ImageProtocol::Sixel;
                    }
                    Response::CellSize(Some(size)) => record.font_size = size,
                    Response::Status => return Ok(record),
                    _ => {}
                }
            }
        }
    }
}

pub(crate) fn initialize_image_picker() -> Result<()> {
    if IMAGE_PICKER.get().is_none() {
        if tmux_detected() {
            crate::cli::terminal_query::enable_tmux_passthrough(QUERY_BUDGET)?;
        }
        let inferred = ImageProtocol::detect();
        let override_value = std::env::var("QUINJET_IMAGE_PROTOCOL").unwrap_or_default();
        let state = choose_picker(
            inferred,
            &override_value,
            window_size().ok().as_ref(),
            query_in_child,
        )?;
        let _state = IMAGE_PICKER.get_or_init(|| state);
    }
    Ok(())
}

fn choose_picker(
    inferred: ImageProtocol,
    override_value: &str,
    size: Option<&WindowSize>,
    query: impl FnOnce() -> Result<Option<PickerRecord>>,
) -> Result<(ImagePicker, ImageProtocol)> {
    let record = if override_value.eq_ignore_ascii_case("auto") {
        query()?
    } else {
        None
    };
    let Some(record) = record else {
        return Ok((picker_for_window(size), inferred));
    };
    let protocol = if inferred.is_native() {
        inferred
    } else {
        record.protocol
    };
    Ok((picker_with_font(record.font_size), protocol))
}

fn query_in_child() -> Result<Option<PickerRecord>> {
    if !is_raw_mode_enabled().unwrap_or(false) {
        return Ok(None);
    }
    let record =
        crate::cli::terminal_query::query_helper(HELPER_ARGUMENT, QUERY_BUDGET, MAX_RECORD_BYTES)?;
    Ok(record.and_then(|bytes| PickerRecord::parse(&bytes)))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PickerRecord {
    protocol: ImageProtocol,
    font_size: (u16, u16),
}

impl PickerRecord {
    fn parse(bytes: &[u8]) -> Option<Self> {
        if bytes.len() > MAX_RECORD_BYTES {
            return None;
        }
        let text = std::str::from_utf8(bytes).ok()?.strip_suffix('\n')?;
        let mut fields = text.split(' ');
        let protocol = match fields.next()? {
            "kitty" => ImageProtocol::Kitty,
            "iterm2" => ImageProtocol::Iterm2,
            "sixel" => ImageProtocol::Sixel,
            "halfblocks" => ImageProtocol::Halfblocks,
            _ => return None,
        };
        let width = fields.next()?.parse().ok()?;
        let height = fields.next()?.parse().ok()?;
        if width == 0 || height == 0 || fields.next().is_some() {
            return None;
        }
        Some(Self {
            protocol,
            font_size: (width, height),
        })
    }

    fn write(self, writer: &mut impl Write) -> io::Result<()> {
        let protocol = match self.protocol {
            ImageProtocol::Kitty => "kitty",
            ImageProtocol::Iterm2 => "iterm2",
            ImageProtocol::Sixel => "sixel",
            ImageProtocol::Halfblocks => "halfblocks",
        };
        writeln!(
            writer,
            "{protocol} {} {}",
            self.font_size.0, self.font_size.1
        )?;
        writer.flush()
    }
}

fn picker_for_window(size: Option<&WindowSize>) -> ImagePicker {
    let font_size = size
        .and_then(|size| {
            let width = size.width.checked_div(size.columns)?;
            let height = size.height.checked_div(size.rows)?;
            (width > 0 && height > 0).then_some((width, height))
        })
        .unwrap_or((10, 20));
    picker_with_font(font_size)
}

pub(super) fn fallback_picker() -> ImagePicker {
    picker_for_window(None)
}

fn picker_with_font(font_size: (u16, u16)) -> ImagePicker {
    ImagePicker {
        font_size,
        is_tmux: tmux_detected(),
    }
}

fn tmux_detected() -> bool {
    std::env::var("TERM").is_ok_and(|term| term.starts_with("tmux"))
        || std::env::var("TERM_PROGRAM").is_ok_and(|program| program == "tmux")
}

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

#[cfg(test)]
mod tests;