1use std::env;
10
11use base64::Engine;
12
13pub fn auto_detect() -> ImageProtocol {
19 if env::var("KITTY_WINDOW_ID").is_ok() {
21 return ImageProtocol::Kitty;
22 }
23 if env::var("TERM").is_ok_and(|t| t == "xterm-ghostty") {
25 return ImageProtocol::Kitty;
26 }
27 ImageProtocol::Iterm2
28}
29
30#[derive(Debug, Clone, Copy)]
32pub enum ImageProtocol {
33 Iterm2,
35 Kitty,
37}
38
39impl ImageProtocol {
40 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 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
61fn 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
72fn 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; print!("\x1b_Ga=d,d=P,x=1,y={y};\x1b\\");
103}