1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-22 21:01:36 +03:00

incomplete, basic implementation of kitty image protocol

This isn't complete; many of the placement options are not supported,
and the status reporting is missing in a number of cases, including
querying/probing, and shared memory objects are not supported yet.

However, this commit is sufficient to allow the kitty-png.py script
(that was copied from
https://sw.kovidgoyal.net/kitty/graphics-protocol/#a-minimal-example)
to render a PNG in the terminal.

This implementation routes the basic image display via the same
code that we use for iterm2 and sixel protocols, but it isn't
sufficient to support the rest of the placement options allowed
by the spec.

Notably, we'll need to add the concept of image placements to
the data model maintained by the terminal state and find a way
to efficiently manage placements both by id and by a viewport
range.

The renderer will need to manage separate quads for placements
and order them by z-index, and adjust the render phases so that
images can appear in the correct plane.

refs: #986
This commit is contained in:
Wez Furlong 2021-07-28 08:51:30 -07:00
parent f0996a5dc5
commit 62cc64f293
4 changed files with 551 additions and 105 deletions

View File

@ -3,9 +3,9 @@
#![cfg_attr(feature = "cargo-clippy", allow(clippy::range_plus_one))]
use super::*;
use crate::color::{ColorPalette, RgbColor};
use anyhow::bail;
use anyhow::{bail, Context};
use image::imageops::FilterType;
use image::ImageFormat;
use image::{GenericImageView, ImageFormat};
use log::{debug, error};
use num_traits::{FromPrimitive, ToPrimitive};
use ordered_float::NotNan;
@ -14,6 +14,10 @@ use std::fmt::Write;
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc;
use terminfo::{Database, Value};
use termwiz::escape::apc::{
KittyImage, KittyImageCompression, KittyImageData, KittyImageFormat, KittyImagePlacement,
KittyImageTransmit, KittyImageVerbosity,
};
use termwiz::escape::csi::{
Cursor, CursorStyle, DecPrivateMode, DecPrivateModeCode, Device, Edit, EraseInDisplay,
EraseInLine, Mode, Sgr, TabulationClear, TerminalMode, TerminalModeCode, Window, XtSmGraphics,
@ -24,8 +28,8 @@ use termwiz::escape::osc::{
Selection,
};
use termwiz::escape::{
Action, ControlCode, DeviceControlMode, Esc, EscCode, KittyImage, OneBased,
OperatingSystemCommand, Sixel, SixelData, CSI,
Action, ControlCode, DeviceControlMode, Esc, EscCode, OneBased, OperatingSystemCommand, Sixel,
SixelData, CSI,
};
use termwiz::image::{ImageCell, ImageData, TextureCoordinate};
use termwiz::surface::{CursorShape, CursorVisibility};
@ -333,6 +337,11 @@ pub struct TerminalState {
sixel_scrolls_right: bool,
user_vars: HashMap<String, String>,
kitty_image_accumulator: Vec<KittyImage>,
max_kitty_image_id: u32,
kitty_image_number_to_id: HashMap<u32, u32>,
kitty_image_id_to_data: HashMap<u32, Arc<ImageData>>,
}
fn encode_modifiers(mods: KeyModifiers) -> u8 {
@ -528,6 +537,10 @@ impl TerminalState {
writer: Box::new(std::io::BufWriter::new(writer)),
image_cache: lru::LruCache::new(16),
user_vars: HashMap::new(),
kitty_image_accumulator: vec![],
max_kitty_image_id: 0,
kitty_image_number_to_id: HashMap::new(),
kitty_image_id_to_data: HashMap::new(),
}
}
@ -1537,7 +1550,307 @@ impl TerminalState {
self.writer.write_all(res.as_bytes()).ok();
}
fn kitty_img(&mut self, _img: KittyImage) {}
fn coalesce_kitty_accumulation(&mut self, img: KittyImage) -> anyhow::Result<KittyImage> {
log::info!(
"coalesce: accumulator={:#?} img:{:#?}",
self.kitty_image_accumulator,
img
);
if self.kitty_image_accumulator.is_empty() {
Ok(img)
} else {
let mut data = vec![];
let mut trans;
let place;
let final_verbosity = img.verbosity();
self.kitty_image_accumulator.push(img);
let mut empty_data = KittyImageData::Direct(String::new());
match self.kitty_image_accumulator.remove(0) {
KittyImage::TransmitData { transmit, .. } => {
trans = transmit;
place = None;
std::mem::swap(&mut empty_data, &mut trans.data);
}
KittyImage::TransmitDataAndDisplay {
transmit,
placement,
..
} => {
place = Some(placement);
trans = transmit;
std::mem::swap(&mut empty_data, &mut trans.data);
}
_ => unreachable!(),
}
data.push(empty_data);
for item in self.kitty_image_accumulator.drain(..) {
match item {
KittyImage::TransmitData { transmit, .. }
| KittyImage::TransmitDataAndDisplay { transmit, .. } => {
data.push(transmit.data);
}
_ => unreachable!(),
}
}
let mut b64_encoded = String::new();
for data in data.into_iter() {
match data {
KittyImageData::Direct(b) => {
b64_encoded.push_str(&b);
}
data => {
anyhow::bail!("expected data chunks to be Direct data, found {:#?}", data)
}
}
}
trans.data = KittyImageData::Direct(b64_encoded);
if let Some(placement) = place {
Ok(KittyImage::TransmitDataAndDisplay {
transmit: trans,
placement,
verbosity: final_verbosity,
})
} else {
Ok(KittyImage::TransmitData {
transmit: trans,
verbosity: final_verbosity,
})
}
}
}
fn kitty_img_transmit(
&mut self,
transmit: KittyImageTransmit,
verbosity: KittyImageVerbosity,
) -> anyhow::Result<u32> {
let (image_id, image_number) = match (transmit.image_id, transmit.image_number) {
(Some(_), Some(_)) => {
// TODO: send an EINVAL error back here
anyhow::bail!("cannot use both i= and I= in the same request");
}
(None, None) => {
// Assume image id 0
(0, None)
}
(Some(id), None) => (id, None),
(None, Some(no)) => {
let id = self.max_kitty_image_id + 1;
self.kitty_image_number_to_id.insert(no, id);
(id, Some(no))
}
};
self.max_kitty_image_id = self.max_kitty_image_id.max(image_id);
let data = transmit
.data
.load_data()
.context("data should have been materialized in coalesce_kitty_accumulation")?;
log::info!("data is {} in size", data.len());
let data = match transmit.compression {
KittyImageCompression::None => data,
KittyImageCompression::Deflate => {
anyhow::bail!("TODO: handle deflate for kitty data");
}
};
let img = match transmit.format {
None | Some(KittyImageFormat::Rgba) | Some(KittyImageFormat::Rgb) => {
let (width, height) = match (transmit.width, transmit.height) {
(Some(w), Some(h)) => (w, h),
_ => {
anyhow::bail!("missing width/height info for kitty img");
}
};
let format = match transmit.format {
Some(KittyImageFormat::Rgb) => image::ColorType::Rgb8,
_ => image::ColorType::Rgba8,
};
let mut png_image_data = vec![];
let encoder = image::png::PngEncoder::new(&mut png_image_data);
encoder
.encode(&data, width, height, format)
.context("encode data as PNG")?;
self.raw_image_to_image_data(png_image_data.into_boxed_slice())
}
Some(KittyImageFormat::Png) => self.raw_image_to_image_data(data.into_boxed_slice()),
};
self.kitty_image_id_to_data.insert(image_id, img);
if let Some(no) = image_number {
match verbosity {
KittyImageVerbosity::Verbose => {
write!(self.writer, "\x1b_Gi={},I={};OK\x1b\\", image_id, no).ok();
}
_ => {}
}
}
Ok(image_id)
}
fn kitty_img_place(
&mut self,
image_id: Option<u32>,
image_number: Option<u32>,
placement: KittyImagePlacement,
verbosity: KittyImageVerbosity,
) -> anyhow::Result<()> {
let image_id = match image_id {
Some(id) => id,
None => *self
.kitty_image_number_to_id
.get(
&image_number
.ok_or_else(|| anyhow::anyhow!("no image_id or image_number specified!"))?,
)
.ok_or_else(|| anyhow::anyhow!("image_number has no matching image id"))?,
};
// FIXME: need to support all kinds of fields in placement!
if placement.x.is_some()
|| placement.y.is_some()
|| placement.w.is_some()
|| placement.h.is_some()
{
anyhow::bail!(
"kitty image placement x/y/w/h not yet implemented {:#?}",
placement
);
}
if placement.x_offset.is_some() || placement.y_offset.is_some() {
anyhow::bail!(
"kitty image placement offsets not yet implemented {:#?}",
placement
);
}
if placement.columns.is_some() || placement.rows.is_some() {
anyhow::bail!(
"kitty image placement scaling rows/columns not yet implemented {:#?}",
placement
);
}
if placement.placement_id.is_some() {
log::warn!(
"kitty image placement_id not yet implemented {:#?}",
placement
);
}
if placement.z_index.is_some() {
log::warn!("kitty image z_index not yet implemented {:#?}", placement);
}
let img = Arc::clone(
self.kitty_image_id_to_data
.get(&image_id)
.ok_or_else(|| anyhow::anyhow!("no matching image id"))?,
);
let decoded = image::load_from_memory(img.data()).context("decode png")?;
let (width, height) = decoded.dimensions();
let saved_cursor = self.cursor.clone();
self.assign_image_to_cells(width, height, img, true);
if placement.do_not_move_cursor {
self.cursor = saved_cursor;
}
Ok(())
}
fn kitty_img_inner(&mut self, img: KittyImage) -> anyhow::Result<()> {
match self
.coalesce_kitty_accumulation(img)
.context("coalesce_kitty_accumulation")?
{
KittyImage::TransmitData {
transmit,
verbosity,
} => {
self.kitty_img_transmit(transmit, verbosity)?;
Ok(())
}
KittyImage::TransmitDataAndDisplay {
transmit,
placement,
verbosity,
} => {
log::info!("TransmitDataAndDisplay {:#?} {:#?}", transmit, placement);
let image_number = transmit.image_number;
let image_id = self.kitty_img_transmit(transmit, verbosity)?;
self.kitty_img_place(Some(image_id), image_number, placement, verbosity)
}
_ => anyhow::bail!("impossible KittImage variant"),
}
}
fn kitty_img(&mut self, img: KittyImage) -> anyhow::Result<()> {
match img {
KittyImage::TransmitData {
transmit,
verbosity,
} => {
let more_data_follows = transmit.more_data_follows;
let img = KittyImage::TransmitData {
transmit,
verbosity,
};
if more_data_follows {
self.kitty_image_accumulator.push(img);
} else {
self.kitty_img_inner(img)?;
}
}
KittyImage::TransmitDataAndDisplay {
transmit,
placement,
verbosity,
} => {
let more_data_follows = transmit.more_data_follows;
let img = KittyImage::TransmitDataAndDisplay {
transmit,
placement,
verbosity,
};
if more_data_follows {
self.kitty_image_accumulator.push(img);
} else {
self.kitty_img_inner(img)?;
}
}
KittyImage::Display {
image_id,
image_number,
placement,
verbosity,
} => {
self.kitty_img_place(image_id, image_number, placement, verbosity)?;
}
KittyImage::Delete { what, verbosity } => {
log::warn!("unhandled KittyImage::Delete {:?} {:?}", what, verbosity);
}
};
Ok(())
}
fn sixel(&mut self, sixel: Box<Sixel>) {
let (width, height) = sixel.dimensions();
@ -3356,7 +3669,11 @@ impl<'a> Performer<'a> {
Action::CSI(csi) => self.csi_dispatch(csi),
Action::Sixel(sixel) => self.sixel(sixel),
Action::XtGetTcap(names) => self.xt_get_tcap(names),
Action::KittyImage(img) => self.kitty_img(img),
Action::KittyImage(img) => {
if let Err(err) = self.kitty_img(img) {
log::error!("kitty_img: {:#}", err);
}
}
}
}

View File

@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::fmt::{Display, Error as FmtError, Formatter};
use std::io::{Read, Seek};
fn get<'a>(keys: &BTreeMap<&str, &'a str>, k: &str) -> Option<&'a str> {
keys.get(k).map(|&s| s)
@ -9,38 +10,122 @@ fn geti<T: std::str::FromStr>(keys: &BTreeMap<&str, &str>, k: &str) -> Option<T>
get(keys, k).and_then(|s| s.parse().ok())
}
#[derive(Debug, Clone, PartialEq, Eq)]
fn set<T: std::string::ToString>(
keys: &mut BTreeMap<&'static str, String>,
k: &'static str,
v: &Option<T>,
) {
if let Some(v) = v {
keys.insert(k, v.to_string());
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum KittyImageData {
/// The data bytes, baes64-decoded.
/// The data bytes, baes64-encoded fragments.
/// t='d'
Direct(Vec<u8>),
Direct(String),
/// The path to a file containing the data.
/// t='f'
File(String),
File {
path: String,
/// the amount of data to read.
/// S=...
data_size: Option<u32>,
/// The offset at which to read.
/// O=...
data_offset: Option<u32>,
},
/// The path to a temporary file containing the data.
/// If the path is in a known temporary location,
/// it should be removed once the data has been read
/// t='t'
TemporaryFile(String),
TemporaryFile {
path: String,
/// the amount of data to read.
/// S=...
data_size: Option<u32>,
/// The offset at which to read.
/// O=...
data_offset: Option<u32>,
},
/// The name of a shared memory object.
/// Can be opened via shm_open() and then should be removed
/// via shm_unlink().
/// On Windows, OpenFileMapping(), MapViewOfFile(), UnmapViewOfFile()
/// and CloseHandle() are used to access and release the data.
/// t='s'
SharedMem(String),
SharedMem {
name: String,
/// the amount of data to read.
/// S=...
data_size: Option<u32>,
/// The offset at which to read.
/// O=...
data_offset: Option<u32>,
},
}
impl std::fmt::Debug for KittyImageData {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
match self {
Self::Direct(data) => write!(fmt, "Direct({} bytes of data)", data.len()),
Self::File {
path,
data_offset,
data_size,
} => fmt
.debug_struct("File")
.field("path", &path)
.field("data_offset", &data_offset)
.field("data_size", data_size)
.finish(),
Self::TemporaryFile {
path,
data_offset,
data_size,
} => fmt
.debug_struct("TemporaryFile")
.field("path", &path)
.field("data_offset", &data_offset)
.field("data_size", data_size)
.finish(),
Self::SharedMem {
name,
data_offset,
data_size,
} => fmt
.debug_struct("SharedMem")
.field("name", &name)
.field("data_offset", &data_offset)
.field("data_size", data_size)
.finish(),
}
}
}
impl KittyImageData {
fn from_keys(keys: &BTreeMap<&str, &str>, payload: &[u8]) -> Option<Self> {
let t = get(keys, "t").unwrap_or("d");
match t {
"d" => Some(Self::Direct(base64::decode(payload).ok()?)),
"f" => Some(Self::File(String::from_utf8(payload.to_vec()).ok()?)),
"t" => Some(Self::TemporaryFile(
String::from_utf8(payload.to_vec()).ok()?,
)),
"s" => Some(Self::SharedMem(String::from_utf8(payload.to_vec()).ok()?)),
"d" => Some(Self::Direct(String::from_utf8(payload.to_vec()).ok()?)),
"f" => Some(Self::File {
path: String::from_utf8(payload.to_vec()).ok()?,
data_size: geti(keys, "S"),
data_offset: geti(keys, "O"),
}),
"t" => Some(Self::TemporaryFile {
path: String::from_utf8(payload.to_vec()).ok()?,
data_size: geti(keys, "S"),
data_offset: geti(keys, "O"),
}),
"s" => Some(Self::SharedMem {
name: String::from_utf8(payload.to_vec()).ok()?,
data_size: geti(keys, "S"),
data_offset: geti(keys, "O"),
}),
_ => None,
}
}
@ -48,19 +133,37 @@ impl KittyImageData {
fn to_keys(&self, keys: &mut BTreeMap<&'static str, String>) {
match self {
Self::Direct(d) => {
keys.insert("payload", base64::encode(&d));
keys.insert("payload", d.to_string());
}
Self::File(f) => {
Self::File {
path,
data_offset,
data_size,
} => {
keys.insert("t", "f".to_string());
keys.insert("payload", base64::encode(&f));
keys.insert("payload", base64::encode(&path));
set(keys, "S", data_size);
set(keys, "S", data_offset);
}
Self::TemporaryFile(f) => {
Self::TemporaryFile {
path,
data_offset,
data_size,
} => {
keys.insert("t", "t".to_string());
keys.insert("payload", base64::encode(&f));
keys.insert("payload", base64::encode(&path));
set(keys, "S", data_size);
set(keys, "S", data_offset);
}
Self::SharedMem(f) => {
Self::SharedMem {
name,
data_offset,
data_size,
} => {
keys.insert("t", "s".to_string());
keys.insert("payload", base64::encode(&f));
keys.insert("payload", base64::encode(&name));
set(keys, "S", data_size);
set(keys, "S", data_offset);
}
}
}
@ -70,11 +173,41 @@ impl KittyImageData {
/// removing the underlying file or shared memory object as part
/// of the read operaiton.
pub fn load_data(self) -> std::io::Result<Vec<u8>> {
fn read_from_file(
path: &str,
data_offset: Option<u32>,
data_size: Option<u32>,
) -> std::io::Result<Vec<u8>> {
let mut f = std::fs::File::open(path)?;
if let Some(offset) = data_offset {
f.seek(std::io::SeekFrom::Start(offset.into()))?;
}
if let Some(len) = data_size {
let mut res = vec![0u8; len as usize];
f.read_exact(&mut res)?;
Ok(res)
} else {
let mut res = vec![];
f.read_to_end(&mut res)?;
Ok(res)
}
}
match self {
Self::Direct(data) => Ok(data),
Self::File(name) => std::fs::read(name),
Self::TemporaryFile(name) => {
let data = std::fs::read(&name)?;
Self::Direct(data) => {
base64::decode(data).or_else(|_| Err(std::io::ErrorKind::InvalidInput.into()))
}
Self::File {
path,
data_offset,
data_size,
} => read_from_file(&path, data_offset, data_size),
Self::TemporaryFile {
path,
data_offset,
data_size,
} => {
let data = read_from_file(&path, data_offset, data_size)?;
// need to sanity check that the path looks like a reasonable
// temporary directory path before blindly unlinking it here.
@ -95,11 +228,11 @@ impl KittyImageData {
false
}
if looks_like_temp_path(&name) {
if let Err(err) = std::fs::remove_file(&name) {
if looks_like_temp_path(&path) {
if let Err(err) = std::fs::remove_file(&path) {
log::error!(
"Unable to remove kitty image protocol temporary file {}: {:#}",
name,
path,
err
);
}
@ -107,13 +240,13 @@ impl KittyImageData {
log::warn!(
"kitty image protocol temporary file {} isn't in a known \
temporary directory; won't try to remove it",
name
path
);
}
Ok(data)
}
Self::SharedMem(_name) => {
Self::SharedMem { .. } => {
log::error!("kitty image protocol via shared memory is not supported");
Err(std::io::ErrorKind::Unsupported.into())
}
@ -121,7 +254,7 @@ impl KittyImageData {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum KittyImageVerbosity {
Verbose,
OnlyErrors,
@ -162,11 +295,12 @@ pub enum KittyImageFormat {
}
impl KittyImageFormat {
fn from_keys(keys: &BTreeMap<&str, &str>) -> Option<Self> {
fn from_keys(keys: &BTreeMap<&str, &str>) -> Option<Option<Self>> {
match get(keys, "f") {
None | Some("32") => Some(Self::Rgba),
Some("24") => Some(Self::Rgb),
Some("100") => Some(Self::Png),
None => Some(None),
Some("32") => Some(Some(Self::Rgba)),
Some("24") => Some(Some(Self::Rgb)),
Some("100") => Some(Some(Self::Png)),
_ => None,
}
}
@ -209,19 +343,13 @@ impl KittyImageCompression {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KittyImageTransmit {
/// f=...
pub format: KittyImageFormat,
pub format: Option<KittyImageFormat>,
/// combination of t=... and d=...
pub data: KittyImageData,
/// s=...
pub width: Option<u32>,
/// v=...
pub height: Option<u32>,
/// the amount of data to read.
/// S=...
pub data_size: Option<u32>,
/// The offset at which to read.
/// O=...
pub data_offset: Option<u32>,
/// The image id.
/// i=...
pub image_id: Option<u32>,
@ -243,8 +371,6 @@ impl KittyImageTransmit {
compression: KittyImageCompression::from_keys(keys)?,
width: geti(keys, "s"),
height: geti(keys, "v"),
data_size: geti(keys, "S"),
data_offset: geti(keys, "O"),
image_id: geti(keys, "i"),
image_number: geti(keys, "I"),
more_data_follows: match get(keys, "m") {
@ -256,26 +382,14 @@ impl KittyImageTransmit {
}
fn to_keys(&self, keys: &mut BTreeMap<&'static str, String>) {
self.format.to_keys(keys);
if let Some(f) = &self.format {
f.to_keys(keys);
}
if let Some(v) = &self.width {
keys.insert("s", v.to_string());
}
if let Some(v) = &self.height {
keys.insert("v", v.to_string());
}
if let Some(v) = &self.data_size {
keys.insert("S", v.to_string());
}
if let Some(v) = &self.data_offset {
keys.insert("O", v.to_string());
}
if let Some(v) = &self.image_id {
keys.insert("i", v.to_string());
}
if let Some(v) = &self.image_number {
keys.insert("I", v.to_string());
}
set(keys, "s", &self.width);
set(keys, "v", &self.height);
set(keys, "i", &self.image_id);
set(keys, "I", &self.image_number);
if self.more_data_follows {
keys.insert("m", "1".to_string());
}
@ -340,39 +454,21 @@ impl KittyImagePlacement {
}
fn to_keys(&self, keys: &mut BTreeMap<&'static str, String>) {
if let Some(v) = self.x {
keys.insert("x", v.to_string());
}
if let Some(v) = self.y {
keys.insert("y", v.to_string());
}
if let Some(v) = self.w {
keys.insert("w", v.to_string());
}
if let Some(v) = self.h {
keys.insert("h", v.to_string());
}
if let Some(v) = self.x_offset {
keys.insert("X", v.to_string());
}
if let Some(v) = self.y_offset {
keys.insert("Y", v.to_string());
}
if let Some(v) = self.columns {
keys.insert("c", v.to_string());
}
if let Some(v) = self.rows {
keys.insert("r", v.to_string());
}
if let Some(v) = self.placement_id {
keys.insert("p", v.to_string());
}
set(keys, "x", &self.x);
set(keys, "y", &self.y);
set(keys, "w", &self.w);
set(keys, "h", &self.h);
set(keys, "X", &self.x_offset);
set(keys, "Y", &self.y_offset);
set(keys, "c", &self.columns);
set(keys, "r", &self.rows);
set(keys, "p", &self.placement_id);
if self.do_not_move_cursor {
keys.insert("C", "1".to_string());
}
if let Some(v) = self.z_index {
keys.insert("z", v.to_string());
}
set(keys, "z", &self.z_index);
}
}
@ -581,6 +677,15 @@ pub enum KittyImage {
}
impl KittyImage {
pub fn verbosity(&self) -> KittyImageVerbosity {
match self {
Self::TransmitData { verbosity, .. } => *verbosity,
Self::TransmitDataAndDisplay { verbosity, .. } => *verbosity,
Self::Display { verbosity, .. } => *verbosity,
Self::Delete { verbosity, .. } => *verbosity,
}
}
pub fn parse_apc(data: &[u8]) -> Option<Self> {
if data.is_empty() || data[0] != b'G' {
return None;
@ -703,12 +808,10 @@ mod test {
KittyImage::parse_apc("Gf=24,s=10,v=20;aGVsbG8=".as_bytes()).unwrap(),
KittyImage::TransmitData {
transmit: KittyImageTransmit {
format: KittyImageFormat::Rgb,
data: KittyImageData::Direct(b"hello".to_vec()),
format: Some(KittyImageFormat::Rgb),
data: KittyImageData::Direct("aGVsbG8=".to_string()),
width: Some(10),
height: Some(20),
data_size: None,
data_offset: None,
image_id: None,
image_number: None,
compression: KittyImageCompression::None,

View File

@ -892,12 +892,10 @@ mod test {
vec![
Action::KittyImage(KittyImage::TransmitData {
transmit: KittyImageTransmit {
format: KittyImageFormat::Rgb,
data: KittyImageData::Direct(b"hello".to_vec()),
format: Some(KittyImageFormat::Rgb),
data: KittyImageData::Direct("aGVsbG8=".to_string()),
width: Some(10),
height: Some(20),
data_size: None,
data_offset: None,
image_id: None,
image_number: None,
compression: KittyImageCompression::None,

28
test-data/kitty-png.py Executable file
View File

@ -0,0 +1,28 @@
#!python3
# This script encodes a PNG file using the kitty image protocol
import sys
from base64 import standard_b64encode
def serialize_gr_command(**cmd):
payload = cmd.pop('payload', None)
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
ans = []
w = ans.append
w(b'\033_G'), w(cmd.encode('ascii'))
if payload:
w(b';')
w(payload)
w(b'\033\\')
return b''.join(ans)
def write_chunked(**cmd):
data = standard_b64encode(cmd.pop('data'))
while data:
chunk, data = data[:4096], data[4096:]
m = 1 if data else 0
sys.stdout.buffer.write(serialize_gr_command(payload=chunk, m=m, **cmd))
sys.stdout.flush()
cmd.clear()
with open(sys.argv[-1], 'rb') as f:
write_chunked(a='T', f=100, data=f.read())