use std::{env, io::Write};
use image::{
ExtendedColorType, ImageEncoder, RgbImage,
codecs::png::{CompressionType, FilterType, PngEncoder},
};
pub fn supported() -> bool {
supported_in(
&env::var("TERM_PROGRAM").unwrap_or_default(),
&env::var("LC_TERMINAL").unwrap_or_default(),
env::var_os("TMUX").is_some(),
env::var_os("STY").is_some(),
env::var_os("MDVIEW_NO_INLINE_IMAGES").is_some(),
)
}
fn supported_in(term: &str, lc_terminal: &str, tmux: bool, screen: bool, off: bool) -> bool {
if off || tmux || screen {
return false;
}
matches!(term, "iTerm.app" | "WezTerm" | "rio") || lc_terminal == "iTerm2"
}
pub fn encode(pixels: &RgbImage, cells: (u16, u16), rows: (u16, u16)) -> std::io::Result<Vec<u8>> {
let px_per_row = pixels.height() / cells.1.max(1) as u32;
let stride = pixels.width() as usize * 3;
let (first, count) = (rows.0 as u32 * px_per_row, rows.1 as u32 * px_per_row);
let buf = &pixels.as_raw()[first as usize * stride..(first + count) as usize * stride];
let mut png = Vec::new();
PngEncoder::new_with_quality(&mut png, CompressionType::Fast, FilterType::Adaptive)
.write_image(buf, pixels.width(), count, ExtendedColorType::Rgb8)
.map_err(std::io::Error::other)?;
Ok(png)
}
pub fn place<W: Write>(
out: &mut W,
x: u16,
y: u16,
cells_w: u16,
rows_h: u16,
png: &[u8],
) -> std::io::Result<()> {
write!(
out,
"\x1b7\x1b[{};{}H\x1b]1337;File=inline=1;width={};height={};preserveAspectRatio=0:",
y + 1,
x + 1,
cells_w,
rows_h,
)?;
out.write_all(encode_base64(png).as_bytes())?;
out.write_all(b"\x07\x1b8")
}
fn encode_base64(data: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for c in data.chunks(3) {
let n = u32::from(c[0]) << 16
| u32::from(c.get(1).copied().unwrap_or(0)) << 8
| u32::from(c.get(2).copied().unwrap_or(0));
out.push(T[(n >> 18) as usize & 63] as char);
out.push(T[(n >> 12) as usize & 63] as char);
out.push(if c.len() > 1 {
T[(n >> 6) as usize & 63] as char
} else {
'='
});
out.push(if c.len() > 2 {
T[n as usize & 63] as char
} else {
'='
});
}
out
}
#[cfg(test)]
mod tests {
use super::{encode_base64, supported_in};
#[test]
fn only_terminals_that_parse_the_sequence_are_opted_in() {
for term in ["iTerm.app", "WezTerm", "rio"] {
assert!(supported_in(term, "", false, false, false), "{term}");
}
for term in ["Apple_Terminal", "vscode", "ghostty", "kitty", ""] {
assert!(!supported_in(term, "", false, false, false), "{term}");
}
assert!(supported_in("", "iTerm2", false, false, false));
}
#[test]
fn multiplexers_and_the_escape_hatch_win_over_the_terminal() {
for (tmux, screen, off) in [(true, false, false), (false, true, false), (false, false, true)]
{
assert!(!supported_in("iTerm.app", "iTerm2", tmux, screen, off));
}
}
#[test]
fn base64_matches_rfc4648_vectors() {
assert_eq!(encode_base64(b""), "");
assert_eq!(encode_base64(b"f"), "Zg==");
assert_eq!(encode_base64(b"fo"), "Zm8=");
assert_eq!(encode_base64(b"foo"), "Zm9v");
assert_eq!(encode_base64(b"foob"), "Zm9vYg==");
assert_eq!(encode_base64(b"fooba"), "Zm9vYmE=");
assert_eq!(encode_base64(b"foobar"), "Zm9vYmFy");
assert_eq!(encode_base64(&[0xff, 0xfe, 0xfd]), "//79");
}
}