scry-cli 0.4.0

grep for images: search by metadata, OCR text, and meaning
//! kitty draws an image in the cells that hold its placeholder character. Diacritics in a cell
//! give the row and column of the part of the image it shows. A cell without them continues the
//! cell to its left, but only if that cell shows the same image. ratatui-image writes the
//! diacritics in the first column only. So a tile or a label that covers the left part of a row
//! breaks the rest of that row, and kitty draws nothing there.
//!
//! The first cell of a new image also carries the escape sequence that sends the image, on the
//! first draw only. A label over that cell would lose the image for good, so `keep` puts the
//! escape sequence back.

use std::num::NonZeroU16;

use ratatui::{
    buffer::{Buffer, CellDiffOption, CellWidth},
    layout::{Position, Rect},
};

const PLACEHOLDER: char = '\u{10EEEE}';

/// Writes the row and column into every placeholder cell of the image in `area`. Call right after
/// the image is drawn in `area`. Other protocols do not use placeholders and stay as they are.
pub fn anchor(buf: &mut Buffer, area: Rect) {
    // The terminal can shrink after the layout and before the frame.
    let area = area.intersection(buf.area);
    for y in area.top()..area.bottom() {
        // The placeholder, then the row, column 0 and the high byte of the image ID. The first
        // cell of the image also starts with the escape sequence that sends the image.
        let mut marks = (buf[(area.x, y)].symbol().chars())
            .skip_while(|&c| c != PLACEHOLDER)
            .skip(1);
        let (Some(row), Some(_), Some(id)) = (marks.next(), marks.next(), marks.next()) else {
            continue;
        };
        for (x, &column) in (area.x + 1..area.right()).zip(&DIACRITICS[1..]) {
            buf[(x, y)].set_symbol(&String::from_iter([PLACEHOLDER, row, column, id]));
        }
    }
}

/// The escape sequence that sends a new image, and the cell that carries it.
pub struct Transmit {
    at: Position,
    escape: String,
}

/// The transmit of the image in `area`, if this is the first draw of that image. Call right after
/// the image is drawn, before anything covers it.
pub fn transmit(buf: &Buffer, area: Rect) -> Option<Transmit> {
    let at = area.as_position();
    let symbol = buf.cell(at)?.symbol();
    let escape = &symbol[..symbol.find(PLACEHOLDER)?];
    (!escape.is_empty()).then(|| Transmit {
        at,
        escape: escape.to_owned(),
    })
}

/// Puts each escape sequence back in front of what now covers its cell. kitty reads the image
/// first and then prints the cell. Call after everything else in the frame is drawn.
pub fn keep(buf: &mut Buffer, transmits: Vec<Transmit>) {
    for Transmit { at, escape } in transmits {
        let Some(cell) = buf.cell_mut(at) else {
            continue;
        };
        // Not `starts_with`: when a new image covers another one, both escapes are in front.
        if cell.symbol().contains(&escape) {
            continue;
        }
        // The escape sequence takes no room on screen, so the cell keeps the width of what
        // covers it. Without a forced width, ratatui counts the escape as text and skips the
        // cells after it.
        let width = NonZeroU16::new(cell.cell_width()).unwrap_or(NonZeroU16::MIN);
        let symbol = escape + cell.symbol();
        cell.set_symbol(&symbol)
            .set_diff_option(CellDiffOption::ForcedWidth(width));
    }
}

/// From https://sw.kovidgoyal.net/kitty/_downloads/f0a0de9ec8d9ff4456206db8e0814937/rowcolumn-diacritics.txt
#[rustfmt::skip]
const DIACRITICS: [char; 297] = [
    '\u{0305}', '\u{030D}', '\u{030E}', '\u{0310}', '\u{0312}', '\u{033D}', '\u{033E}', '\u{033F}',
    '\u{0346}', '\u{034A}', '\u{034B}', '\u{034C}', '\u{0350}', '\u{0351}', '\u{0352}', '\u{0357}',
    '\u{035B}', '\u{0363}', '\u{0364}', '\u{0365}', '\u{0366}', '\u{0367}', '\u{0368}', '\u{0369}',
    '\u{036A}', '\u{036B}', '\u{036C}', '\u{036D}', '\u{036E}', '\u{036F}', '\u{0483}', '\u{0484}',
    '\u{0485}', '\u{0486}', '\u{0487}', '\u{0592}', '\u{0593}', '\u{0594}', '\u{0595}', '\u{0597}',
    '\u{0598}', '\u{0599}', '\u{059C}', '\u{059D}', '\u{059E}', '\u{059F}', '\u{05A0}', '\u{05A1}',
    '\u{05A8}', '\u{05A9}', '\u{05AB}', '\u{05AC}', '\u{05AF}', '\u{05C4}', '\u{0610}', '\u{0611}',
    '\u{0612}', '\u{0613}', '\u{0614}', '\u{0615}', '\u{0616}', '\u{0617}', '\u{0657}', '\u{0658}',
    '\u{0659}', '\u{065A}', '\u{065B}', '\u{065D}', '\u{065E}', '\u{06D6}', '\u{06D7}', '\u{06D8}',
    '\u{06D9}', '\u{06DA}', '\u{06DB}', '\u{06DC}', '\u{06DF}', '\u{06E0}', '\u{06E1}', '\u{06E2}',
    '\u{06E4}', '\u{06E7}', '\u{06E8}', '\u{06EB}', '\u{06EC}', '\u{0730}', '\u{0732}', '\u{0733}',
    '\u{0735}', '\u{0736}', '\u{073A}', '\u{073D}', '\u{073F}', '\u{0740}', '\u{0741}', '\u{0743}',
    '\u{0745}', '\u{0747}', '\u{0749}', '\u{074A}', '\u{07EB}', '\u{07EC}', '\u{07ED}', '\u{07EE}',
    '\u{07EF}', '\u{07F0}', '\u{07F1}', '\u{07F3}', '\u{0816}', '\u{0817}', '\u{0818}', '\u{0819}',
    '\u{081B}', '\u{081C}', '\u{081D}', '\u{081E}', '\u{081F}', '\u{0820}', '\u{0821}', '\u{0822}',
    '\u{0823}', '\u{0825}', '\u{0826}', '\u{0827}', '\u{0829}', '\u{082A}', '\u{082B}', '\u{082C}',
    '\u{082D}', '\u{0951}', '\u{0953}', '\u{0954}', '\u{0F82}', '\u{0F83}', '\u{0F86}', '\u{0F87}',
    '\u{135D}', '\u{135E}', '\u{135F}', '\u{17DD}', '\u{193A}', '\u{1A17}', '\u{1A75}', '\u{1A76}',
    '\u{1A77}', '\u{1A78}', '\u{1A79}', '\u{1A7A}', '\u{1A7B}', '\u{1A7C}', '\u{1B6B}', '\u{1B6D}',
    '\u{1B6E}', '\u{1B6F}', '\u{1B70}', '\u{1B71}', '\u{1B72}', '\u{1B73}', '\u{1CD0}', '\u{1CD1}',
    '\u{1CD2}', '\u{1CDA}', '\u{1CDB}', '\u{1CE0}', '\u{1DC0}', '\u{1DC1}', '\u{1DC3}', '\u{1DC4}',
    '\u{1DC5}', '\u{1DC6}', '\u{1DC7}', '\u{1DC8}', '\u{1DC9}', '\u{1DCB}', '\u{1DCC}', '\u{1DD1}',
    '\u{1DD2}', '\u{1DD3}', '\u{1DD4}', '\u{1DD5}', '\u{1DD6}', '\u{1DD7}', '\u{1DD8}', '\u{1DD9}',
    '\u{1DDA}', '\u{1DDB}', '\u{1DDC}', '\u{1DDD}', '\u{1DDE}', '\u{1DDF}', '\u{1DE0}', '\u{1DE1}',
    '\u{1DE2}', '\u{1DE3}', '\u{1DE4}', '\u{1DE5}', '\u{1DE6}', '\u{1DFE}', '\u{20D0}', '\u{20D1}',
    '\u{20D4}', '\u{20D5}', '\u{20D6}', '\u{20D7}', '\u{20DB}', '\u{20DC}', '\u{20E1}', '\u{20E7}',
    '\u{20E9}', '\u{20F0}', '\u{2CEF}', '\u{2CF0}', '\u{2CF1}', '\u{2DE0}', '\u{2DE1}', '\u{2DE2}',
    '\u{2DE3}', '\u{2DE4}', '\u{2DE5}', '\u{2DE6}', '\u{2DE7}', '\u{2DE8}', '\u{2DE9}', '\u{2DEA}',
    '\u{2DEB}', '\u{2DEC}', '\u{2DED}', '\u{2DEE}', '\u{2DEF}', '\u{2DF0}', '\u{2DF1}', '\u{2DF2}',
    '\u{2DF3}', '\u{2DF4}', '\u{2DF5}', '\u{2DF6}', '\u{2DF7}', '\u{2DF8}', '\u{2DF9}', '\u{2DFA}',
    '\u{2DFB}', '\u{2DFC}', '\u{2DFD}', '\u{2DFE}', '\u{2DFF}', '\u{A66F}', '\u{A67C}', '\u{A67D}',
    '\u{A6F0}', '\u{A6F1}', '\u{A8E0}', '\u{A8E1}', '\u{A8E2}', '\u{A8E3}', '\u{A8E4}', '\u{A8E5}',
    '\u{A8E6}', '\u{A8E7}', '\u{A8E8}', '\u{A8E9}', '\u{A8EA}', '\u{A8EB}', '\u{A8EC}', '\u{A8ED}',
    '\u{A8EE}', '\u{A8EF}', '\u{A8F0}', '\u{A8F1}', '\u{AAB0}', '\u{AAB2}', '\u{AAB3}', '\u{AAB7}',
    '\u{AAB8}', '\u{AABE}', '\u{AABF}', '\u{AAC1}', '\u{FE20}', '\u{FE21}', '\u{FE22}', '\u{FE23}',
    '\u{FE24}', '\u{FE25}', '\u{FE26}', '\u{10A0F}', '\u{10A38}', '\u{1D185}', '\u{1D186}', '\u{1D187}',
    '\u{1D188}', '\u{1D189}', '\u{1D1AA}', '\u{1D1AB}', '\u{1D1AC}', '\u{1D1AD}', '\u{1D242}', '\u{1D243}',
    '\u{1D244}',
];

#[cfg(test)]
mod tests {
    use ratatui::style::Style;

    use super::*;

    fn placeholder(row: usize, column: usize) -> String {
        String::from_iter([
            PLACEHOLDER,
            DIACRITICS[row],
            DIACRITICS[column],
            DIACRITICS[0],
        ])
    }

    #[test]
    fn every_cell_gets_its_row_and_column() {
        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 2));
        let first = format!("\x1b_Gsend\x1b\\{}", placeholder(1, 0));
        buf[(0, 0)].set_symbol(&first);
        for x in 1..4 {
            buf[(x, 0)].set_symbol(&PLACEHOLDER.to_string());
        }
        buf[(0, 1)].set_symbol("x");
        anchor(&mut buf, Rect::new(0, 0, 4, 2));
        assert_eq!(buf[(0, 0)].symbol(), first);
        assert_eq!(buf[(3, 0)].symbol(), placeholder(1, 3));
        assert_eq!(buf[(2, 1)].symbol(), " ");
    }

    const SEND: &str = "\x1b_Gsend\x1b\\";

    /// The first row of a new image in `buf`, as ratatui-image draws it.
    fn first_draw(buf: &mut Buffer) {
        buf[(0, 0)]
            .set_symbol(&format!("{SEND}{}", placeholder(0, 0)))
            .set_diff_option(CellDiffOption::ForcedWidth(NonZeroU16::MIN));
        for x in 1..4 {
            buf[(x, 0)]
                .set_symbol(&placeholder(0, x.into()))
                .set_diff_option(CellDiffOption::ForcedWidth(NonZeroU16::MIN));
        }
    }

    #[test]
    fn a_label_over_the_first_cell_keeps_the_transmit() {
        let area = Rect::new(0, 0, 4, 1);
        let mut buf = Buffer::empty(area);
        first_draw(&mut buf);
        let sent = transmit(&buf, area).into_iter().collect();
        buf.set_string(0, 0, "ab", Style::new());
        keep(&mut buf, sent);
        assert_eq!(buf[(0, 0)].symbol(), format!("{SEND}a"));
        assert_eq!(buf[(1, 0)].symbol(), "b");
        let cells: Vec<(u16, String)> = (Buffer::empty(area).diff(&buf).into_iter())
            .map(|(x, _, cell)| (x, cell.symbol().to_owned()))
            .collect();
        assert_eq!(cells[..2], [(0, format!("{SEND}a")), (1, "b".into())]);
    }

    #[test]
    fn the_width_holds_on_a_cell_without_options() {
        let area = Rect::new(0, 0, 4, 1);
        let mut buf = Buffer::empty(area);
        first_draw(&mut buf);
        let sent = transmit(&buf, area).into_iter().collect();
        buf[(0, 0)].reset();
        buf[(0, 0)].set_symbol("✓");
        keep(&mut buf, sent);
        assert_eq!(buf[(0, 0)].cell_width(), 1);
    }

    #[test]
    fn later_draws_and_uncovered_cells_stay() {
        let area = Rect::new(0, 0, 4, 1);
        let mut buf = Buffer::empty(area);
        first_draw(&mut buf);
        let first = buf[(0, 0)].symbol().to_owned();
        let sent = transmit(&buf, area).into_iter().collect();
        keep(&mut buf, sent);
        assert_eq!(buf[(0, 0)].symbol(), first);

        buf[(0, 0)].set_symbol(&placeholder(0, 0));
        assert!(transmit(&buf, area).is_none());
    }
}