use std::env;
use base64::Engine;
pub fn auto_detect() -> ImageProtocol {
if env::var("KITTY_WINDOW_ID").is_ok() {
return ImageProtocol::Kitty;
}
if env::var("TERM").is_ok_and(|t| t == "xterm-ghostty")
|| env::var("GHOSTTY_RESOURCES_DIR").is_ok()
{
return ImageProtocol::Kitty;
}
ImageProtocol::Iterm2
}
#[derive(Debug, Clone, Copy)]
pub enum ImageProtocol {
Iterm2,
Kitty,
}
impl ImageProtocol {
pub fn encode(&self, bytes: &[u8], cell_width: usize) -> String {
match self {
ImageProtocol::Iterm2 => iterm2_encode(bytes, cell_width, 1),
ImageProtocol::Kitty => kitty_encode(bytes, cell_width, 1),
}
}
pub fn clear_line(&self, y: u16) {
match self {
ImageProtocol::Iterm2 => {}
ImageProtocol::Kitty => kitty_clear_line(y),
}
}
pub fn clear(&self) {
match self {
ImageProtocol::Iterm2 => {}
ImageProtocol::Kitty => kitty_clear(),
}
}
}
fn to_base64_str(bytes: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
fn iterm2_encode(bytes: &[u8], cell_width: usize, cell_height: usize) -> String {
format!(
"\x1b]1337;File=size={};width={};height={};preserveAspectRatio=0;inline=1:{}\u{0007}",
bytes.len(),
cell_width,
cell_height,
to_base64_str(bytes)
)
}
fn kitty_encode(bytes: &[u8], cell_width: usize, cell_height: usize) -> String {
let base64_str = to_base64_str(bytes);
let chunk_size = 4096;
let mut s = String::new();
let chunks = base64_str.as_bytes().chunks(chunk_size);
let total_chunks = chunks.len();
s.push_str("\x1b_Ga=d,d=C;\x1b\\");
for (i, chunk) in chunks.enumerate() {
s.push_str("\x1b_G");
if i == 0 {
s.push_str(&format!("a=T,f=100,c={cell_width},r={cell_height},"));
}
if i < total_chunks - 1 {
s.push_str("m=1;");
} else {
s.push_str("m=0;");
}
s.push_str(std::str::from_utf8(chunk).unwrap());
s.push_str("\x1b\\");
}
s
}
fn kitty_clear_line(y: u16) {
let y = y + 1; print!("\x1b_Ga=d,d=Y,y={y};\x1b\\");
}
fn kitty_clear() {
print!("\x1b_Ga=d,d=A;\x1b\\");
}