use std::fmt::Write as _;
pub fn supported() -> bool {
match std::env::var("MAPTERM_GRAPHICS").as_deref() {
Ok("kitty") => return true,
Ok("cells") => return false,
_ => {}
}
if std::env::var_os("KITTY_WINDOW_ID").is_some() {
return true;
}
let term_program = std::env::var("TERM_PROGRAM").unwrap_or_default().to_ascii_lowercase();
let term = std::env::var("TERM").unwrap_or_default().to_ascii_lowercase();
["ghostty", "kitty", "wezterm", "herdr"].iter().any(|t| term_program.contains(t) || term.contains(t))
}
pub fn base64(bytes: &[u8]) -> String {
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
for i in 0..4 {
out.push(if i <= chunk.len() { B64[(n >> (18 - 6 * i) & 63) as usize] as char } else { '=' });
}
}
out
}
pub fn transmit(id: u32, png: &[u8]) -> String {
let data = base64(png);
let mut out = String::with_capacity(data.len() + 64);
let chunks: Vec<&str> = data.as_bytes().chunks(4096).map(|c| std::str::from_utf8(c).unwrap_or("")).collect();
for (i, chunk) in chunks.iter().enumerate() {
let more = u8::from(i + 1 < chunks.len());
if i == 0 {
let _ = write!(out, "\x1b_Ga=t,f=100,t=d,i={id},q=2,m={more};{chunk}\x1b\\");
} else {
let _ = write!(out, "\x1b_Gm={more};{chunk}\x1b\\");
}
}
out
}
#[allow(clippy::too_many_arguments)]
pub fn place(id: u32, placement: u32, col: u16, row: u16, src: (u32, u32, u32, u32), cols: u16, rows: u16, z: i32) -> String {
let (x, y, w, h) = src;
format!(
"\x1b[{};{}H\x1b_Ga=p,i={id},p={placement},x={x},y={y},w={w},h={h},c={cols},r={rows},z={z},C=1,q=2\x1b\\",
row as u32 + 1,
col as u32 + 1
)
}
pub const CLEAR_PLACEMENTS: &str = "\x1b_Ga=d,d=a,q=2\x1b\\";
pub fn free(id: u32) -> String {
format!("\x1b_Ga=d,d=I,i={id},q=2\x1b\\")
}
pub const FREE_ALL: &str = "\x1b_Ga=d,d=A,q=2\x1b\\";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_matches_rfc4648() {
assert_eq!(base64(b""), "");
assert_eq!(base64(b"f"), "Zg==");
assert_eq!(base64(b"fo"), "Zm8=");
assert_eq!(base64(b"foo"), "Zm9v");
assert_eq!(base64(b"foobar"), "Zm9vYmFy");
}
#[test]
fn transmit_chunks_and_place() {
let t = transmit(7, &vec![0u8; 6000]);
assert!(t.starts_with("\x1b_Ga=t,f=100,t=d,i=7,q=2,m=1;"));
assert!(t.ends_with("\x1b\\") && t.contains("\x1b_Gm=0;"));
assert_eq!(t.matches("\x1b_G").count(), 2, "8000 base64 chars -> two chunks");
let p = place(7, 3, 4, 2, (10, 20, 30, 40), 5, 6, -1);
assert_eq!(p, "\x1b[3;5H\x1b_Ga=p,i=7,p=3,x=10,y=20,w=30,h=40,c=5,r=6,z=-1,C=1,q=2\x1b\\");
}
}