use std::cell::RefCell;
use std::io::{self, Write};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use ratatui_core::layout::Rect;
const ST: &str = "\x1b\\";
const CHUNK: usize = 4096;
#[derive(Clone, Debug)]
pub struct ImageData {
rgba: Arc<[u8]>,
pixel_width: u32,
pixel_height: u32,
}
impl ImageData {
pub fn from_rgba(
pixel_width: u32,
pixel_height: u32,
rgba: impl Into<Arc<[u8]>>,
) -> Option<Self> {
let rgba = rgba.into();
let expected = (pixel_width as usize)
.checked_mul(pixel_height as usize)?
.checked_mul(4)?;
if pixel_width == 0 || pixel_height == 0 || rgba.len() != expected {
return None;
}
Some(Self {
rgba,
pixel_width,
pixel_height,
})
}
pub fn pixel_width(&self) -> u32 {
self.pixel_width
}
pub fn pixel_height(&self) -> u32 {
self.pixel_height
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ImageSupport {
None,
Kitty,
ITerm2,
Sixel,
}
impl ImageSupport {
pub fn detect() -> Self {
Self::detect_from(
std::env::var("TERM").ok().as_deref(),
std::env::var("TERM_PROGRAM").ok().as_deref(),
std::env::var("KITTY_WINDOW_ID").ok().as_deref(),
std::env::var("GHOSTTY_RESOURCES_DIR").ok().as_deref(),
)
}
pub fn detect_from(
term: Option<&str>,
term_program: Option<&str>,
kitty_window_id: Option<&str>,
ghostty_resources_dir: Option<&str>,
) -> Self {
let program = term_program.map(|p| p.to_ascii_lowercase());
let kitty = kitty_window_id.is_some_and(|s| !s.is_empty())
|| ghostty_resources_dir.is_some_and(|s| !s.is_empty())
|| term.is_some_and(|t| t.contains("kitty"))
|| program
.as_deref()
.is_some_and(|p| p == "ghostty" || p == "wezterm");
if kitty {
return ImageSupport::Kitty;
}
if program.as_deref().is_some_and(|p| p.contains("iterm")) {
return ImageSupport::ITerm2;
}
if term.is_some_and(|t| t.contains("foot") || t.contains("mlterm") || t.contains("contour"))
{
return ImageSupport::Sixel;
}
ImageSupport::None
}
}
#[derive(Clone, Debug)]
struct Placement {
rect: Rect,
data: ImageData,
support: ImageSupport,
kitty_image_number: Option<u32>,
}
#[derive(Debug, Default)]
struct ImageLayerState {
placements: Vec<Placement>,
previous_kitty_images: Vec<u32>,
}
static NEXT_KITTY_IMAGE_NUMBER: AtomicU32 = AtomicU32::new(1);
fn next_kitty_image_number() -> u32 {
NEXT_KITTY_IMAGE_NUMBER
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |number| {
Some(if number == u32::MAX { 1 } else { number + 1 })
})
.expect("the update closure always returns a value")
}
#[derive(Clone, Debug, Default)]
pub struct ImageLayer(Rc<RefCell<ImageLayerState>>);
impl ImageLayer {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn record(&self, rect: Rect, data: ImageData, support: ImageSupport) {
if rect.width == 0 || rect.height == 0 {
return;
}
self.0.borrow_mut().placements.push(Placement {
rect,
data,
support,
kitty_image_number: (support == ImageSupport::Kitty).then(next_kitty_image_number),
});
}
pub fn clear(&self) {
self.0.borrow_mut().placements.clear();
}
pub fn is_empty(&self) -> bool {
self.0.borrow().placements.is_empty()
}
pub fn len(&self) -> usize {
self.0.borrow().placements.len()
}
pub fn emit(&self, out: &mut impl Write) -> io::Result<()> {
let mut state = self.0.borrow_mut();
if state.placements.is_empty() && state.previous_kitty_images.is_empty() {
return Ok(());
}
out.write_all(b"\x1b7")?;
for p in &state.placements {
let (row, col) = (p.rect.y + 1, p.rect.x + 1);
write!(out, "\x1b[{row};{col}H")?;
match p.support {
ImageSupport::ITerm2 => {
write_iterm2(out, &p.data, p.rect.width, p.rect.height)?;
}
ImageSupport::Sixel => {
out.write_all(encode_sixel(&p.data, p.rect.width, p.rect.height).as_bytes())?;
}
_ => write_kitty(
out,
&p.data,
p.rect.width,
p.rect.height,
p.kitty_image_number,
)?,
}
}
for number in &state.previous_kitty_images {
write!(out, "\x1b_Ga=d,d=N,I={number},q=2\x1b\\")?;
}
out.write_all(b"\x1b8")?;
out.flush()?;
state.previous_kitty_images = state
.placements
.iter()
.filter_map(|placement| placement.kitty_image_number)
.collect();
Ok(())
}
}
#[cfg(test)]
fn encode_kitty(data: &ImageData, cols: u16, rows: u16) -> String {
let mut out = Vec::new();
write_kitty(&mut out, data, cols, rows, None).expect("writing to Vec cannot fail");
String::from_utf8(out).expect("graphics escapes and base64 are ASCII")
}
fn write_kitty(
out: &mut impl Write,
data: &ImageData,
cols: u16,
rows: u16,
image_number: Option<u32>,
) -> io::Result<()> {
const RAW_CHUNK: usize = CHUNK / 4 * 3;
let chunks = data.rgba.len().div_ceil(RAW_CHUNK);
for (i, raw) in data.rgba.chunks(RAW_CHUNK).enumerate() {
let more = u8::from(i + 1 != chunks);
out.write_all(b"\x1b_G")?;
if i == 0 {
write!(
out,
"f=32,s={},v={},a=T,c={},r={},C=1,q=2,m={}",
data.pixel_width, data.pixel_height, cols, rows, more
)?;
if let Some(number) = image_number {
write!(out, ",I={number}")?;
}
} else {
write!(out, "m={more}")?;
}
out.write_all(b";")?;
write_base64(out, raw)?;
out.write_all(ST.as_bytes())?;
}
Ok(())
}
#[cfg(test)]
fn base64_encode(bytes: &[u8]) -> String {
let mut out = Vec::with_capacity(bytes.len().div_ceil(3) * 4);
write_base64(&mut out, bytes).expect("writing to Vec cannot fail");
String::from_utf8(out).expect("base64 is ASCII")
}
fn write_base64(out: &mut impl Write, bytes: &[u8]) -> io::Result<()> {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const RAW_CHUNK: usize = 3 * 1024;
let mut encoded = [0; RAW_CHUNK / 3 * 4];
for chunk in bytes.chunks(RAW_CHUNK) {
let mut len = 0;
for group in chunk.chunks(3) {
let b0 = u32::from(group[0]);
let b1 = u32::from(*group.get(1).unwrap_or(&0));
let b2 = u32::from(*group.get(2).unwrap_or(&0));
let bits = (b0 << 16) | (b1 << 8) | b2;
encoded[len] = ALPHABET[(bits >> 18) as usize & 0x3f];
encoded[len + 1] = ALPHABET[(bits >> 12) as usize & 0x3f];
encoded[len + 2] = if group.len() > 1 {
ALPHABET[(bits >> 6) as usize & 0x3f]
} else {
b'='
};
encoded[len + 3] = if group.len() > 2 {
ALPHABET[bits as usize & 0x3f]
} else {
b'='
};
len += 4;
}
out.write_all(&encoded[..len])?;
}
Ok(())
}
#[cfg(test)]
fn encode_iterm2(data: &ImageData, cols: u16, rows: u16) -> String {
let mut out = Vec::new();
write_iterm2(&mut out, data, cols, rows).expect("writing to Vec cannot fail");
String::from_utf8(out).expect("graphics escapes and base64 are ASCII")
}
fn write_iterm2(out: &mut impl Write, data: &ImageData, cols: u16, rows: u16) -> io::Result<()> {
let png = png_rgba(data.pixel_width, data.pixel_height, &data.rgba);
write!(
out,
"\x1b]1337;File=inline=1;width={cols};height={rows};preserveAspectRatio=0;size={}:",
png.len()
)?;
write_base64(out, &png)?;
out.write_all(b"\x07")
}
const SIXEL_CELL_W: u32 = 10;
const SIXEL_CELL_H: u32 = 20;
fn encode_sixel(data: &ImageData, cols: u16, rows: u16) -> String {
let tw = (cols as u32 * SIXEL_CELL_W).max(1);
let th = (rows as u32 * SIXEL_CELL_H).max(1);
let (sw, sh) = (data.pixel_width.max(1), data.pixel_height.max(1));
let mut idx = vec![0u8; (tw * th) as usize];
for ty in 0..th {
let sy = ty * sh / th;
for tx in 0..tw {
let sx = tx * sw / tw;
let p = ((sy * sw + sx) * 4) as usize;
let q = |c: u8| (c as u32 * 5 + 127) / 255; idx[(ty * tw + tx) as usize] =
(q(data.rgba[p]) * 36 + q(data.rgba[p + 1]) * 6 + q(data.rgba[p + 2])) as u8;
}
}
let mut out = String::new();
out.push_str("\x1bPq"); out.push_str(&format!("\"1;1;{tw};{th}")); for i in 0u32..216 {
let (r, g, b) = (i / 36 % 6, i / 6 % 6, i % 6);
out.push_str(&format!("#{};2;{};{};{}", i, r * 20, g * 20, b * 20));
}
let bands = th.div_ceil(6);
for band in 0..bands {
let y0 = band * 6;
let mut present = [false; 216];
for p in 0..6 {
let y = y0 + p;
if y >= th {
break;
}
for tx in 0..tw {
present[idx[(y * tw + tx) as usize] as usize] = true;
}
}
let colors: Vec<usize> = (0..216).filter(|&c| present[c]).collect();
for (ci, &c) in colors.iter().enumerate() {
out.push_str(&format!("#{c}"));
let (mut run, mut len) = (0u8, 0u32);
for tx in 0..tw {
let mut bits = 0u8;
for p in 0..6 {
let y = y0 + p;
if y < th && idx[(y * tw + tx) as usize] as usize == c {
bits |= 1 << p;
}
}
if bits == run {
len += 1;
} else {
sixel_run(&mut out, run, len);
run = bits;
len = 1;
}
}
sixel_run(&mut out, run, len);
if ci + 1 < colors.len() {
out.push('$');
}
}
if band + 1 < bands {
out.push('-');
}
}
out.push_str("\x1b\\");
out
}
fn sixel_run(out: &mut String, bits: u8, len: u32) {
if len == 0 {
return;
}
let ch = (0x3f + bits) as char;
if len >= 3 {
out.push_str(&format!("!{len}"));
out.push(ch);
} else {
for _ in 0..len {
out.push(ch);
}
}
}
fn png_rgba(w: u32, h: u32, rgba: &[u8]) -> Vec<u8> {
let row = (w * 4) as usize;
let mut raw = Vec::with_capacity(rgba.len() + h as usize);
for y in 0..h as usize {
raw.push(0); raw.extend_from_slice(&rgba[y * row..(y + 1) * row]);
}
let mut png = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&w.to_be_bytes());
ihdr.extend_from_slice(&h.to_be_bytes());
ihdr.extend_from_slice(&[8, 6, 0, 0, 0]); png_chunk(&mut png, b"IHDR", &ihdr);
png_chunk(&mut png, b"IDAT", &zlib_stored(&raw));
png_chunk(&mut png, b"IEND", &[]);
png
}
fn png_chunk(out: &mut Vec<u8>, tag: &[u8; 4], data: &[u8]) {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
out.extend_from_slice(tag);
out.extend_from_slice(data);
out.extend_from_slice(&crc32([tag.as_slice(), data]).to_be_bytes());
}
fn zlib_stored(data: &[u8]) -> Vec<u8> {
let mut out = vec![0x78, 0x01]; let mut i = 0;
loop {
let end = (i + 0xffff).min(data.len());
let block = &data[i..end];
let last = end == data.len();
out.push(u8::from(last)); let len = block.len() as u16;
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&(!len).to_le_bytes());
out.extend_from_slice(block);
i = end;
if last {
break;
}
}
out.extend_from_slice(&adler32(data).to_be_bytes());
out
}
const CRC32_TABLE: [u32; 256] = {
let mut table = [0; 256];
let mut i = 0;
while i < table.len() {
let mut crc = i as u32;
let mut bit = 0;
while bit < 8 {
crc = if crc & 1 != 0 {
(crc >> 1) ^ 0xedb8_8320
} else {
crc >> 1
};
bit += 1;
}
table[i] = crc;
i += 1;
}
table
};
fn crc32<'a>(parts: impl IntoIterator<Item = &'a [u8]>) -> u32 {
let mut crc = 0xffff_ffffu32;
for part in parts {
for &byte in part {
crc = CRC32_TABLE[((crc ^ u32::from(byte)) & 0xff) as usize] ^ (crc >> 8);
}
}
!crc
}
fn adler32(data: &[u8]) -> u32 {
let (mut a, mut b) = (1u32, 0u32);
for block in data.chunks(5552) {
for &x in block {
a += u32::from(x);
b += a;
}
a %= 65521;
b %= 65521;
}
(b << 16) | a
}
#[cfg(test)]
mod tests {
use super::*;
fn solid(w: u32, h: u32, rgba: [u8; 4]) -> ImageData {
let buf: Vec<u8> = std::iter::repeat_n(rgba, (w * h) as usize)
.flatten()
.collect();
ImageData::from_rgba(w, h, buf).expect("valid rgba")
}
#[test]
fn from_rgba_validates_length() {
assert!(ImageData::from_rgba(2, 2, vec![0u8; 16]).is_some());
assert!(ImageData::from_rgba(2, 2, vec![0u8; 15]).is_none());
assert!(ImageData::from_rgba(0, 2, vec![0u8; 0]).is_none());
}
#[test]
fn base64_matches_known_vectors() {
assert_eq!(base64_encode(b""), "");
assert_eq!(base64_encode(b"f"), "Zg==");
assert_eq!(base64_encode(b"fo"), "Zm8=");
assert_eq!(base64_encode(b"foo"), "Zm9v");
assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
}
#[test]
fn png_checksums_match_known_vectors() {
assert_eq!(crc32([b"123456789".as_slice()]), 0xcbf4_3926);
assert_eq!(adler32(b"Wikipedia"), 0x11e6_0398);
}
#[test]
fn encode_kitty_single_chunk_shape() {
let img = solid(1, 1, [1, 2, 3, 4]);
let out = encode_kitty(&img, 4, 2);
assert!(out.starts_with("\x1b_Gf=32,s=1,v=1,a=T,c=4,r=2,C=1,q=2,m=0;"));
assert!(out.ends_with("\x1b\\"));
assert!(out.contains(";AQIDBA==\x1b\\"));
assert_eq!(out.matches("\x1b_G").count(), 1);
}
#[test]
fn encode_kitty_chunks_large_payloads() {
let img = solid(64, 64, [9, 9, 9, 9]);
let out = encode_kitty(&img, 10, 5);
let commands = out.matches("\x1b_G").count();
assert!(commands > 1, "large image should span multiple chunks");
assert!(out.starts_with("\x1b_Gf=32,s=64,v=64,a=T,c=10,r=5,C=1,q=2,m=1;"));
assert!(out.contains("\x1b_Gm=1;"));
assert!(out.contains("\x1b_Gm=0;"));
assert_eq!(out.matches("\x1b\\").count(), commands);
}
#[test]
fn detect_recognizes_kitty_signals() {
let kitty = ImageSupport::Kitty;
let none = ImageSupport::None;
assert_eq!(
ImageSupport::detect_from(Some("xterm-kitty"), None, None, None),
kitty
);
assert_eq!(
ImageSupport::detect_from(Some("xterm-256color"), None, Some("1"), None),
kitty
);
assert_eq!(
ImageSupport::detect_from(None, Some("ghostty"), None, None),
kitty
);
assert_eq!(
ImageSupport::detect_from(None, Some("WezTerm"), None, None),
kitty
);
assert_eq!(
ImageSupport::detect_from(None, None, None, Some("/x")),
kitty
);
assert_eq!(
ImageSupport::detect_from(Some("xterm-256color"), Some("Apple_Terminal"), None, None),
none
);
assert_eq!(
ImageSupport::detect_from(None, None, Some(""), Some("")),
none
);
}
#[test]
fn emit_positions_each_image_and_brackets_with_cursor_save_restore() {
let layer = ImageLayer::new();
let k = ImageSupport::Kitty;
layer.record(Rect::new(2, 1, 4, 2), solid(1, 1, [1, 2, 3, 4]), k);
layer.record(Rect::new(0, 5, 3, 3), solid(1, 1, [5, 6, 7, 8]), k);
let mut out: Vec<u8> = Vec::new();
layer.emit(&mut out).expect("emit");
let s = String::from_utf8(out).expect("utf8");
assert!(s.starts_with("\x1b7"));
assert!(s.ends_with("\x1b8"));
assert!(s.contains("\x1b[2;3H\x1b_G"));
assert!(s.contains("\x1b[6;1H\x1b_G"));
assert_eq!(s.matches("\x1b_G").count(), 2, "one command per image");
}
#[test]
fn emit_dispatches_the_recorded_protocol() {
let layer = ImageLayer::new();
layer.record(
Rect::new(0, 0, 4, 2),
solid(1, 1, [1, 2, 3, 4]),
ImageSupport::Kitty,
);
layer.record(
Rect::new(0, 3, 4, 2),
solid(1, 1, [5, 6, 7, 8]),
ImageSupport::ITerm2,
);
let mut out: Vec<u8> = Vec::new();
layer.emit(&mut out).expect("emit");
let s = String::from_utf8(out).expect("utf8");
assert_eq!(s.matches("\x1b_G").count(), 1, "one Kitty command");
assert_eq!(
s.matches("\x1b]1337;File=").count(),
1,
"one iTerm2 command"
);
}
#[test]
fn emit_is_a_noop_when_empty() {
let layer = ImageLayer::new();
let mut out: Vec<u8> = Vec::new();
layer.emit(&mut out).expect("emit");
assert!(
out.is_empty(),
"no images → no bytes, no stray cursor moves"
);
}
#[test]
fn clear_resets_between_frames() {
let layer = ImageLayer::new();
layer.record(
Rect::new(0, 0, 2, 2),
solid(1, 1, [0, 0, 0, 0]),
ImageSupport::Kitty,
);
assert_eq!(layer.len(), 1);
layer.clear();
assert!(layer.is_empty());
}
#[test]
fn kitty_emit_removes_the_previous_frame_placement() {
let layer = ImageLayer::new();
layer.record(
Rect::new(0, 0, 8, 4),
solid(1, 1, [1, 2, 3, 255]),
ImageSupport::Kitty,
);
let mut first = Vec::new();
layer.emit(&mut first).expect("first emit");
let first = String::from_utf8(first).expect("graphics commands are ASCII");
let previous_number = first
.split(",I=")
.nth(1)
.and_then(|tail| tail.split(';').next())
.expect("transmission has an image number");
layer.clear();
layer.record(
Rect::new(0, 0, 4, 2),
solid(1, 1, [4, 5, 6, 255]),
ImageSupport::Kitty,
);
let mut out = Vec::new();
layer.emit(&mut out).expect("resized emit");
let out = String::from_utf8(out).expect("graphics commands are ASCII");
assert!(
out.contains(&format!("a=d,d=N,I={previous_number},q=2")),
"the old, larger placement must be removed after resize: {out:?}"
);
}
#[test]
fn kitty_emit_removes_an_image_that_disappeared() {
let layer = ImageLayer::new();
layer.record(
Rect::new(0, 0, 8, 4),
solid(1, 1, [1, 2, 3, 255]),
ImageSupport::Kitty,
);
layer.emit(&mut Vec::new()).expect("image frame");
layer.clear();
let mut out = Vec::new();
layer.emit(&mut out).expect("empty frame");
let out = String::from_utf8(out).expect("graphics commands are ASCII");
assert!(out.contains("a=d,d=N,I="), "stale image remains: {out:?}");
}
#[test]
fn zero_area_placements_are_dropped() {
let layer = ImageLayer::new();
let k = ImageSupport::Kitty;
layer.record(Rect::new(0, 0, 0, 3), solid(1, 1, [0, 0, 0, 0]), k);
layer.record(Rect::new(0, 0, 3, 0), solid(1, 1, [0, 0, 0, 0]), k);
assert!(
layer.is_empty(),
"clipped-away images have nothing to paint"
);
}
#[test]
fn encode_iterm2_wraps_a_png_file() {
let img = solid(2, 2, [10, 20, 30, 255]);
let out = encode_iterm2(&img, 8, 4);
assert!(
out.starts_with("\x1b]1337;File=inline=1;width=8;height=4;preserveAspectRatio=0;size=")
);
assert!(out.ends_with('\x07'));
let payload = out
.trim_end_matches('\x07')
.rsplit(':')
.next()
.expect("payload");
let decoded = b64_decode(payload);
assert_eq!(
&decoded[..8],
&[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]
);
let size: usize = out
.split("size=")
.nth(1)
.and_then(|s| s.split(':').next())
.and_then(|n| n.parse().ok())
.expect("size field");
assert_eq!(size, decoded.len());
}
#[test]
fn png_rgba_is_well_formed() {
let img = solid(3, 2, [1, 2, 3, 4]);
let png = png_rgba(img.pixel_width, img.pixel_height, &img.rgba);
assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]);
let s = String::from_utf8_lossy(&png);
assert!(s.contains("IHDR") && s.contains("IDAT") && s.contains("IEND"));
}
#[test]
fn encode_sixel_has_dcs_frame_palette_and_data() {
let img = solid(2, 2, [255, 0, 0, 255]); let out = encode_sixel(&img, 1, 1);
assert!(
out.starts_with("\x1bPq\"1;1;10;20"),
"intro/raster: {:?}",
&out[..24]
);
assert!(out.ends_with("\x1b\\"), "ST terminated");
assert!(out.contains("#180;2;100;0;0"), "red register defined");
assert!(out.contains("#180"), "red register selected in data");
assert!(
out.contains("!"),
"run-length encoding used for the solid fill"
);
assert!(
out.contains('~'),
"a fully-set sixel column (0x3f+63) present"
);
}
#[test]
fn encode_sixel_bytes_stay_in_range() {
let img = solid(3, 2, [0, 128, 255, 255]);
let out = encode_sixel(&img, 2, 1);
for b in out.bytes() {
let ok = b == 0x1b
|| b == b'P'
|| b == b'q'
|| b == b'\\'
|| b == b'"'
|| b == b'#'
|| b == b'!'
|| b == b'$'
|| b == b'-'
|| b == b';'
|| b.is_ascii_digit()
|| (0x3f..=0x7e).contains(&b);
assert!(ok, "unexpected sixel byte {b:#x}");
}
}
#[test]
fn detect_recognizes_sixel_terminals() {
assert_eq!(
ImageSupport::detect_from(Some("foot"), None, None, None),
ImageSupport::Sixel
);
assert_eq!(
ImageSupport::detect_from(Some("xterm-mlterm"), None, None, None),
ImageSupport::Sixel
);
}
#[test]
fn emit_dispatches_sixel() {
let layer = ImageLayer::new();
layer.record(
Rect::new(0, 0, 2, 1),
solid(1, 1, [1, 2, 3, 255]),
ImageSupport::Sixel,
);
let mut out: Vec<u8> = Vec::new();
layer.emit(&mut out).expect("emit");
let s = String::from_utf8(out).expect("utf8");
assert!(s.contains("\x1bPq"), "sixel DCS emitted: {s:?}");
}
#[test]
fn detect_recognizes_iterm2_and_prefers_kitty() {
assert_eq!(
ImageSupport::detect_from(Some("xterm-256color"), Some("iTerm.app"), None, None),
ImageSupport::ITerm2
);
assert_eq!(
ImageSupport::detect_from(None, Some("WezTerm"), None, None),
ImageSupport::Kitty
);
}
fn b64_decode(s: &str) -> Vec<u8> {
fn val(c: u8) -> Option<u32> {
match c {
b'A'..=b'Z' => Some((c - b'A') as u32),
b'a'..=b'z' => Some((c - b'a' + 26) as u32),
b'0'..=b'9' => Some((c - b'0' + 52) as u32),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
let mut acc = 0u32;
let mut bits = 0;
let mut out = Vec::new();
for &c in s.as_bytes() {
let Some(v) = val(c) else { continue }; acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
out
}
}