Skip to main content

void_graph/
protocol.rs

1//! Terminal image protocol encoding.
2//!
3//! This module provides support for displaying images in terminals using the
4//! iTerm2 and Kitty graphics protocols.
5//!
6//! Adapted from [Serie](https://github.com/lusingander/serie) by lusingander,
7//! licensed under MIT.
8
9use std::env;
10
11use base64::Engine;
12
13/// Auto-detect the best image protocol for the current terminal.
14///
15/// By default assumes iTerm2 is the best protocol to use for all terminals
16/// *unless* an environment variable is set that suggests the terminal is
17/// probably Kitty.
18pub fn auto_detect() -> ImageProtocol {
19    // https://sw.kovidgoyal.net/kitty/glossary/#envvar-KITTY_WINDOW_ID
20    if env::var("KITTY_WINDOW_ID").is_ok() {
21        return ImageProtocol::Kitty;
22    }
23    // https://ghostty.org/docs/help/terminfo
24    if env::var("TERM").is_ok_and(|t| t == "xterm-ghostty") {
25        return ImageProtocol::Kitty;
26    }
27    ImageProtocol::Iterm2
28}
29
30/// Supported terminal image protocols.
31#[derive(Debug, Clone, Copy)]
32pub enum ImageProtocol {
33    /// iTerm2 inline images protocol.
34    Iterm2,
35    /// Kitty graphics protocol.
36    Kitty,
37}
38
39impl ImageProtocol {
40    /// Encode image bytes for display in the terminal.
41    pub fn encode(&self, bytes: &[u8], cell_width: usize) -> String {
42        match self {
43            ImageProtocol::Iterm2 => iterm2_encode(bytes, cell_width, 1),
44            ImageProtocol::Kitty => kitty_encode(bytes, cell_width, 1),
45        }
46    }
47
48    /// Clear a previously rendered image line.
49    pub fn clear_line(&self, y: u16) {
50        match self {
51            ImageProtocol::Iterm2 => {}
52            ImageProtocol::Kitty => kitty_clear_line(y),
53        }
54    }
55}
56
57fn to_base64_str(bytes: &[u8]) -> String {
58    base64::engine::general_purpose::STANDARD.encode(bytes)
59}
60
61// https://iterm2.com/documentation-images.html
62fn iterm2_encode(bytes: &[u8], cell_width: usize, cell_height: usize) -> String {
63    format!(
64        "\x1b]1337;File=size={};width={};height={};preserveAspectRatio=0;inline=1:{}\u{0007}",
65        bytes.len(),
66        cell_width,
67        cell_height,
68        to_base64_str(bytes)
69    )
70}
71
72// https://sw.kovidgoyal.net/kitty/graphics-protocol/
73fn kitty_encode(bytes: &[u8], cell_width: usize, cell_height: usize) -> String {
74    let base64_str = to_base64_str(bytes);
75    let chunk_size = 4096;
76
77    let mut s = String::new();
78
79    let chunks = base64_str.as_bytes().chunks(chunk_size);
80    let total_chunks = chunks.len();
81
82    s.push_str("\x1b_Ga=d,d=C;\x1b\\");
83    for (i, chunk) in chunks.enumerate() {
84        s.push_str("\x1b_G");
85        if i == 0 {
86            s.push_str(&format!("a=T,f=100,c={cell_width},r={cell_height},"));
87        }
88        if i < total_chunks - 1 {
89            s.push_str("m=1;");
90        } else {
91            s.push_str("m=0;");
92        }
93        s.push_str(std::str::from_utf8(chunk).unwrap());
94        s.push_str("\x1b\\");
95    }
96
97    s
98}
99
100fn kitty_clear_line(y: u16) {
101    let y = y + 1; // 1-based
102    print!("\x1b_Ga=d,d=P,x=1,y={y};\x1b\\");
103}