use image::{DynamicImage, GenericImageView};
use std::io::{self, Write};
use std::sync::atomic::{AtomicU32, Ordering};
static IMAGE_ID_COUNTER: AtomicU32 = AtomicU32::new(1);
fn next_image_id() -> u32 {
IMAGE_ID_COUNTER.fetch_add(1, Ordering::SeqCst)
}
#[derive(Debug)]
pub struct KittyAnimation {
pub image_id: u32,
pub frame_count: usize,
pub is_playing: bool,
}
pub fn is_kitty_terminal() -> bool {
if std::env::var("TERM")
.map(|t| t.contains("kitty"))
.unwrap_or(false)
{
return true;
}
if std::env::var("TERM_PROGRAM")
.map(|t| t.to_lowercase() == "kitty")
.unwrap_or(false)
{
return true;
}
std::env::var("KITTY_WINDOW_ID").is_ok()
}
fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as usize;
let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
result.push(ALPHABET[b0 >> 2] as char);
result.push(ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)] as char);
if chunk.len() > 1 {
result.push(ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(ALPHABET[b2 & 0x3f] as char);
} else {
result.push('=');
}
}
result
}
fn image_to_rgba(img: &DynamicImage) -> Vec<u8> {
img.to_rgba8().into_raw()
}
fn send_kitty_command<W: Write>(
writer: &mut W,
params: &str,
payload: Option<&str>,
) -> io::Result<()> {
write!(writer, "\x1b_G{}", params)?;
if let Some(data) = payload {
write!(writer, ";{}", data)?;
}
write!(writer, "\x1b\\")?;
writer.flush()
}
pub fn transmit_animation<W: Write>(
writer: &mut W,
frames: &[(DynamicImage, u32)], col: u16,
row: u16,
) -> io::Result<Option<KittyAnimation>> {
if frames.is_empty() {
return Ok(None);
}
let image_id = next_image_id();
let first_frame = &frames[0].0;
let (width, height) = first_frame.dimensions();
let rgba_data = image_to_rgba(first_frame);
let encoded = base64_encode(&rgba_data);
const CHUNK_SIZE: usize = 4096;
let chunks: Vec<&str> = encoded
.as_bytes()
.chunks(CHUNK_SIZE)
.map(|c| std::str::from_utf8(c).unwrap_or(""))
.collect();
for (i, chunk) in chunks.iter().enumerate() {
let is_last = i == chunks.len() - 1;
let m = if is_last { 0 } else { 1 };
if i == 0 {
let params = format!(
"a=T,f=32,i={},s={},v={},c={},r={},C=1,q=2,m={}",
image_id, width, height, col, row, m
);
send_kitty_command(writer, ¶ms, Some(chunk))?;
} else {
let params = format!("m={}", m);
send_kitty_command(writer, ¶ms, Some(chunk))?;
}
}
for (_frame_idx, (frame_img, delay_ms)) in frames.iter().enumerate().skip(1) {
let rgba_data = image_to_rgba(frame_img);
let encoded = base64_encode(&rgba_data);
let chunks: Vec<&str> = encoded
.as_bytes()
.chunks(CHUNK_SIZE)
.map(|c| std::str::from_utf8(c).unwrap_or(""))
.collect();
for (i, chunk) in chunks.iter().enumerate() {
let is_last = i == chunks.len() - 1;
let m = if is_last { 0 } else { 1 };
if i == 0 {
let params = format!(
"a=f,i={},z={},r=1,f=32,s={},v={},q=2,m={}",
image_id, delay_ms, width, height, m
);
send_kitty_command(writer, ¶ms, Some(chunk))?;
} else {
let params = format!("a=f,m={}", m);
send_kitty_command(writer, ¶ms, Some(chunk))?;
}
}
}
let params = format!("a=a,i={},s=3,v=0,q=2", image_id);
send_kitty_command(writer, ¶ms, None)?;
Ok(Some(KittyAnimation {
image_id,
frame_count: frames.len(),
is_playing: true,
}))
}
pub fn delete_animation<W: Write>(writer: &mut W, animation: &KittyAnimation) -> io::Result<()> {
let params = format!("a=d,d=I,i={},q=2", animation.image_id);
send_kitty_command(writer, ¶ms, None)
}
pub fn pause_animation<W: Write>(writer: &mut W, animation: &KittyAnimation) -> io::Result<()> {
let params = format!("a=a,i={},s=1,q=2", animation.image_id);
send_kitty_command(writer, ¶ms, None)
}
pub fn resume_animation<W: Write>(writer: &mut W, animation: &KittyAnimation) -> io::Result<()> {
let params = format!("a=a,i={},s=3,q=2", animation.image_id);
send_kitty_command(writer, ¶ms, None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_kitty_detection() {
let _ = is_kitty_terminal();
}
#[test]
fn test_base64_encoding() {
let data = b"Hello, World!";
let encoded = base64_encode(data);
assert_eq!(encoded, "SGVsbG8sIFdvcmxkIQ==");
}
#[test]
fn test_image_id_generation() {
let id1 = next_image_id();
let id2 = next_image_id();
assert!(id2 > id1);
}
}