use base64::Engine as _;
const CHUNK: usize = 4096;
pub const APC_GRAPHICS_INTRODUCER: &[u8] = b"\x1b_G";
pub const APC_TERMINATOR: &[u8] = b"\x1b\\";
fn b64(bytes: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
fn write_apc(out: &mut Vec<u8>, control: &str, payload_b64: &[u8]) {
if payload_b64.is_empty() {
out.extend_from_slice(b"\x1b_G");
out.extend_from_slice(control.as_bytes());
out.extend_from_slice(b"\x1b\\");
return;
}
let chunks: Vec<&[u8]> = payload_b64.chunks(CHUNK).collect();
let last = chunks.len() - 1;
for (i, chunk) in chunks.iter().enumerate() {
out.extend_from_slice(b"\x1b_G");
if i == 0 {
out.extend_from_slice(control.as_bytes());
out.push(b',');
}
if i == last {
out.extend_from_slice(b"m=0");
} else {
out.extend_from_slice(b"m=1");
}
out.push(b';');
out.extend_from_slice(chunk);
out.extend_from_slice(b"\x1b\\");
}
}
pub fn transmit_and_display_rgba(out: &mut Vec<u8>, id: u32, width: u32, height: u32, rgba: &[u8]) {
let control = format!("a=T,f=32,s={width},v={height},i={id},q=2");
let payload = b64(rgba);
write_apc(out, &control, payload.as_bytes());
}
pub fn transmit_rgba(out: &mut Vec<u8>, id: u32, width: u32, height: u32, rgba: &[u8]) {
let control = format!("a=t,f=32,s={width},v={height},i={id},q=2");
let payload = b64(rgba);
write_apc(out, &control, payload.as_bytes());
}
pub fn place(out: &mut Vec<u8>, img_id: u32, placement_id: u32, z: i32) {
let control = format!("a=p,i={img_id},p={placement_id},z={z},C=1,q=2");
write_apc(out, &control, &[]);
}
#[allow(clippy::too_many_arguments)]
pub fn place_scaled(
out: &mut Vec<u8>,
img_id: u32,
placement_id: u32,
z: i32,
cols: u32,
rows: u32,
xoff: u32,
yoff: u32,
) {
let mut control = format!("a=p,i={img_id},p={placement_id},z={z},C=1,q=2");
if cols > 0 {
control.push_str(&format!(",c={cols}"));
}
if rows > 0 {
control.push_str(&format!(",r={rows}"));
}
if xoff > 0 {
control.push_str(&format!(",X={xoff}"));
}
if yoff > 0 {
control.push_str(&format!(",Y={yoff}"));
}
write_apc(out, &control, &[]);
}
pub fn delete_placement(out: &mut Vec<u8>, img_id: u32, placement_id: u32) {
let control = format!("a=d,d=i,i={img_id},p={placement_id},q=2");
write_apc(out, &control, &[]);
}
pub fn delete_all_placements(out: &mut Vec<u8>) {
write_apc(out, "a=d,d=a,q=2", &[]);
}
pub fn cursor_to(out: &mut Vec<u8>, row: u16, col: u16) {
out.extend_from_slice(format!("\x1b[{row};{col}H").as_bytes());
}
pub fn enter_screen(out: &mut Vec<u8>) {
out.extend_from_slice(b"\x1b[2J\x1b[H\x1b[?25l");
}
pub fn leave_screen(out: &mut Vec<u8>) {
out.extend_from_slice(b"\x1b[?25h");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn small_payload_is_single_chunk_with_m0() {
let mut out = Vec::new();
transmit_and_display_rgba(&mut out, 1, 2, 1, &[255, 0, 0, 255, 0, 255, 0, 255]);
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("\x1b_Ga=T,f=32,s=2,v=1,i=1,q=2,m=0;"));
assert!(s.ends_with("\x1b\\"));
assert_eq!(s.matches("\x1b_G").count(), 1);
}
#[test]
fn large_payload_splits_into_chunks() {
let mut out = Vec::new();
let rgba = vec![128u8; 320 * 180 * 4];
transmit_and_display_rgba(&mut out, 1, 320, 180, &rgba);
let s = String::from_utf8(out).unwrap();
let packets = s.matches("\x1b_G").count();
assert!(packets > 1, "expected multiple chunks, got {packets}");
assert!(s.contains("a=T,f=32,s=320,v=180,i=1,q=2,m=1;"));
assert_eq!(s.matches("m=0;").count(), 1);
}
#[test]
fn control_only_packet_has_no_payload_sep_chunks() {
let mut out = Vec::new();
delete_all_placements(&mut out);
let s = String::from_utf8(out).unwrap();
assert_eq!(s, "\x1b_Ga=d,d=a,q=2\x1b\\");
}
#[test]
fn place_scaled_omits_zero_keys() {
let mut out = Vec::new();
place_scaled(&mut out, 7, 9, 42, 0, 0, 3, 4);
let s = String::from_utf8(out).unwrap();
assert_eq!(s, "\x1b_Ga=p,i=7,p=9,z=42,C=1,q=2,X=3,Y=4\x1b\\");
assert!(!s.contains("c="));
assert!(!s.contains("r="));
}
#[test]
fn place_scaled_emits_all_keys_when_set() {
let mut out = Vec::new();
place_scaled(&mut out, 1001, 1, 100246, 12, 6, 6, 16);
let s = String::from_utf8(out).unwrap();
assert_eq!(
s,
"\x1b_Ga=p,i=1001,p=1,z=100246,C=1,q=2,c=12,r=6,X=6,Y=16\x1b\\"
);
}
#[test]
fn negative_z_survives_formatting() {
let mut out = Vec::new();
place(&mut out, 1, 1, -100_000);
let s = String::from_utf8(out).unwrap();
assert!(s.contains("z=-100000"), "got {s:?}");
}
#[test]
fn chunk_boundary_payload_still_ends_with_m0() {
let mut out = Vec::new();
transmit_rgba(&mut out, 5, 768, 1, &vec![7u8; 3072]);
let s = String::from_utf8(out).unwrap();
assert_eq!(s.matches("\x1b_G").count(), 1, "should be one chunk");
assert_eq!(s.matches("m=0;").count(), 1);
assert!(!s.contains("m=1"));
}
#[test]
fn cursor_is_one_based_row_then_col() {
let mut out = Vec::new();
cursor_to(&mut out, 42, 81);
assert_eq!(String::from_utf8(out).unwrap(), "\x1b[42;81H");
}
#[test]
fn delete_placement_targets_one_image_and_placement() {
let mut out = Vec::new();
delete_placement(&mut out, 1001, 3);
assert_eq!(
String::from_utf8(out).unwrap(),
"\x1b_Ga=d,d=i,i=1001,p=3,q=2\x1b\\"
);
}
#[test]
fn the_exported_introducer_matches_what_is_actually_written() {
let mut out = Vec::new();
transmit_rgba(&mut out, 1, 1, 1, &[0, 0, 0, 255]);
assert!(out.starts_with(APC_GRAPHICS_INTRODUCER));
assert!(out.ends_with(APC_TERMINATOR));
let mut out = Vec::new();
delete_all_placements(&mut out);
assert!(out.starts_with(APC_GRAPHICS_INTRODUCER));
assert!(out.ends_with(APC_TERMINATOR));
}
}