use anyhow::{Context, Result};
use base64::Engine as _;
use ratatui::layout::Rect;
use std::{
fs::File,
io::{Read, Write as _},
path::Path,
};
pub(super) fn place_terminal_image_with_konsole_protocol(
path: &Path,
area: Rect,
) -> Result<Vec<u8>> {
let mut out = Vec::new();
let _ = write!(
out,
"\x1b[{};{}H",
area.y.saturating_add(1),
area.x.saturating_add(1)
);
out.extend(build_konsole_upload_sequence(
path,
konsole_image_id(),
area,
)?);
Ok(out)
}
pub(super) fn clear_terminal_images_with_konsole_protocol() -> Result<Vec<u8>> {
Ok(build_konsole_clear_sequence(konsole_image_id())
.as_bytes()
.to_vec())
}
fn build_konsole_upload_sequence(path: &Path, id: u32, area: Rect) -> Result<Vec<u8>> {
let mut file = File::open(path)
.with_context(|| format!("failed to open Konsole preview image {}", path.display()))?;
let total = file
.metadata()
.with_context(|| format!("failed to stat Konsole preview image {}", path.display()))?
.len() as usize;
if total == 0 {
anyhow::bail!("Konsole preview image {} is empty", path.display());
}
let mut sent = 0usize;
let mut chunk = vec![0u8; 3 * 4096 / 4];
let mut out = Vec::new();
while sent < total {
let remaining = total.saturating_sub(sent);
let chunk_len = remaining.min(chunk.len());
file.read_exact(&mut chunk[..chunk_len])
.with_context(|| format!("failed to read Konsole preview image {}", path.display()))?;
sent += chunk_len;
let more = sent < total;
let payload = base64::engine::general_purpose::STANDARD.encode(&chunk[..chunk_len]);
if sent == chunk_len {
write!(
out,
"\u{1b}_Ga=T,q=2,f=100,i={id},p=1,c={},r={},C=1,m={};{payload}\u{1b}\\",
area.width.max(1),
area.height.max(1),
if more { 1 } else { 0 },
)?;
} else {
write!(
out,
"\u{1b}_Gm={};{payload}\u{1b}\\",
if more { 1 } else { 0 },
)?;
}
}
Ok(out)
}
fn build_konsole_clear_sequence(id: u32) -> String {
format!("\u{1b}_Ga=d,d=I,i={id},p=1,q=2\u{1b}\\")
}
fn konsole_image_id() -> u32 {
std::process::id() % (0xff_ffff + 1)
}
#[cfg(test)]
mod tests {
use super::*;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use image::ImageFormat;
use std::{
fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
fn temp_root(label: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("elio-konsole-inline-image-{label}-{unique}"))
}
fn write_test_raster_image(path: &Path, format: ImageFormat, width: u32, height: u32) {
let image =
image::DynamicImage::ImageRgba8(image::RgbaImage::from_fn(width, height, |x, y| {
image::Rgba([(x % 255) as u8, (y % 255) as u8, 0x80, 0xff])
}));
image
.save_with_format(path, format)
.expect("test raster image should save");
}
#[test]
fn build_konsole_upload_sequence_uses_direct_placement_mode() {
let root = temp_root("konsole-upload-sequence");
fs::create_dir_all(&root).expect("failed to create temp root");
let path = root.join("demo.png");
write_test_raster_image(&path, ImageFormat::Png, 24, 16);
let payload = fs::read(&path).expect("png payload should exist");
let id = 42_u32;
let area = Rect {
x: 10,
y: 4,
width: 30,
height: 20,
};
let sequence = String::from_utf8(
build_konsole_upload_sequence(&path, id, area)
.expect("Konsole upload sequence should build"),
)
.expect("Konsole upload sequence should be utf8");
assert!(sequence.starts_with("\u{1b}_G"));
assert!(sequence.contains("a=T"));
assert!(sequence.contains("q=2"));
assert!(sequence.contains("f=100"));
assert!(sequence.contains(&format!("i={id}")));
assert!(sequence.contains("p=1"));
assert!(sequence.contains("c=30"));
assert!(sequence.contains("r=20"));
assert!(sequence.contains("C=1"));
assert!(sequence.contains("m=0"));
assert!(!sequence.contains("U=1"));
assert!(sequence.contains(&BASE64_STANDARD.encode(payload)));
fs::remove_dir_all(root).expect("failed to remove temp root");
}
#[test]
fn place_konsole_terminal_image_prefixes_cursor_move() {
let root = temp_root("konsole-cursor-prefix");
fs::create_dir_all(&root).expect("failed to create temp root");
let path = root.join("demo.png");
write_test_raster_image(&path, ImageFormat::Png, 16, 16);
let output = String::from_utf8(
place_terminal_image_with_konsole_protocol(
&path,
Rect {
x: 10,
y: 4,
width: 8,
height: 6,
},
)
.expect("Konsole placement should build"),
)
.expect("Konsole placement should be utf8");
assert!(output.starts_with("\x1b[5;11H\x1b_G"));
fs::remove_dir_all(root).expect("failed to remove temp root");
}
#[test]
fn clear_konsole_uses_targeted_delete_sequence() {
let sequence = String::from_utf8(
clear_terminal_images_with_konsole_protocol()
.expect("Konsole clear sequence should build"),
)
.expect("Konsole clear sequence should be utf8");
assert_eq!(
sequence,
format!("\u{1b}_Ga=d,d=I,i={},p=1,q=2\u{1b}\\", konsole_image_id())
);
}
}