979 lines
30 KiB
Rust
979 lines
30 KiB
Rust
use std::cmp::min;
|
|
use std::io::Read;
|
|
#[cfg(feature = "local_fs")]
|
|
use std::{env, fs, str};
|
|
|
|
use anyhow::Result;
|
|
use base64::Engine;
|
|
use flate2::read::ZlibDecoder;
|
|
use galaxyui::image_cache::{resize_dimensions, FitType};
|
|
use galaxyui::{
|
|
assets::asset_cache::Asset,
|
|
image_cache::{CustomHeaderCreationError, CustomImageFormat, CustomImageHeader, ImageType},
|
|
util::{parse_i32, parse_u32},
|
|
};
|
|
use pathfinder_geometry::vector::Vector2F;
|
|
use rand::Rng;
|
|
use galaxyui::assets::asset_cache::Asset;
|
|
use galaxyui::image_cache::{
|
|
resize_dimensions, CustomHeaderCreationError, CustomImageFormat, CustomImageHeader, FitType,
|
|
ImageType,
|
|
};
|
|
use galaxyui::util::{parse_i32, parse_u32};
|
|
|
|
use super::escape_sequences::C1;
|
|
|
|
/// Actions specified by the [Kitty Image Protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/)
|
|
#[derive(Debug, Clone)]
|
|
pub enum KittyAction {
|
|
StoreOnly(StoreOnly),
|
|
StoreAndDisplay(StoreAndDisplay),
|
|
DisplayStoredImage(DisplayStoredImage),
|
|
QuerySupport(QuerySupport),
|
|
Delete {
|
|
delete_placements_only: bool,
|
|
deletion_type: DeletionType,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct StoreOnly {
|
|
pub image: KittyImage,
|
|
pub image_id: u32,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct StoreAndDisplay {
|
|
pub image: KittyImage,
|
|
pub placement_data: KittyPlacementData,
|
|
pub image_id: u32,
|
|
pub placement_id: u32,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct DisplayStoredImage {
|
|
pub placement_data: KittyPlacementData,
|
|
pub image_id: u32,
|
|
pub placement_id: u32,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct QuerySupport {
|
|
pub image: KittyImage,
|
|
pub image_id: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum DeletionType {
|
|
DeleteAll,
|
|
DeleteById(DeleteById),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DeleteById {
|
|
pub image_id: u32,
|
|
pub placement_id: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct KittyImage {
|
|
pub metadata: KittyImageMetadata,
|
|
pub data: Vec<u8>,
|
|
}
|
|
|
|
impl TryFrom<PendingKittyMessage> for KittyMessage {
|
|
type Error = InvalidKittyPayload;
|
|
|
|
fn try_from(pending: PendingKittyMessage) -> Result<KittyMessage, InvalidKittyPayload> {
|
|
let mut decoded_payload = vec![];
|
|
|
|
for payload in pending.payload {
|
|
decoded_payload.extend(base64_decode_padding_agnostic(&payload[..])?);
|
|
}
|
|
|
|
Ok(KittyMessage {
|
|
control_data: pending.control_data,
|
|
payload: decoded_payload,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl TryFrom<KittyMessage> for KittyImage {
|
|
type Error = InvalidKittyPayload;
|
|
|
|
fn try_from(message: KittyMessage) -> Result<Self, InvalidKittyPayload> {
|
|
let decoded_data =
|
|
decode_kitty_image_data(message.payload, message.control_data.compressed)?;
|
|
|
|
let decoded_data = match message.control_data.transmission_medium {
|
|
KittyTransmissionMedium::Direct => decoded_data,
|
|
KittyTransmissionMedium::SimpleFile => {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(feature = "local_fs")] {
|
|
read_file(decoded_data, false)?
|
|
} else {
|
|
return Err(InvalidKittyPayload::FileError(FileError::UnsupportedPlatform));
|
|
}
|
|
}
|
|
}
|
|
KittyTransmissionMedium::TemporaryFile => {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(feature = "local_fs")] {
|
|
read_file(decoded_data, true)?
|
|
} else {
|
|
return Err(InvalidKittyPayload::FileError(FileError::UnsupportedPlatform));
|
|
}
|
|
}
|
|
}
|
|
KittyTransmissionMedium::SharedMemoryObject => {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(all(feature = "local_fs", unix))] {
|
|
read_shared_memory(message.control_data.clone(), decoded_data)?
|
|
} else {
|
|
return Err(InvalidKittyPayload::ShmError(ShmError::UnsupportedPlatform));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(Self {
|
|
metadata: KittyImageMetadata::from(message.control_data),
|
|
data: decoded_data,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub type KittyResponse = Result<(), KittyError>;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum KittyError {
|
|
InvalidKittyAction(InvalidKittyAction),
|
|
StorageError(StorageError),
|
|
KittyFeatureDisabled,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum StorageError {
|
|
UnknownId { id: u32 },
|
|
}
|
|
|
|
impl From<StorageError> for KittyError {
|
|
fn from(value: StorageError) -> Self {
|
|
KittyError::StorageError(value)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum InvalidKittyAction {
|
|
UnsupportedAction,
|
|
InvalidKittyPayload(InvalidKittyPayload),
|
|
InvalidControlData(InvalidControlData),
|
|
}
|
|
|
|
impl From<InvalidKittyAction> for KittyError {
|
|
fn from(value: InvalidKittyAction) -> Self {
|
|
KittyError::InvalidKittyAction(value)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum InvalidControlData {
|
|
IdMissing,
|
|
UnicodePlaceholderUnsupported,
|
|
}
|
|
|
|
impl From<InvalidControlData> for KittyError {
|
|
fn from(value: InvalidControlData) -> Self {
|
|
KittyError::InvalidKittyAction(InvalidKittyAction::InvalidControlData(value))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum InvalidKittyPayload {
|
|
InvalidTransmissionMedium(KittyTransmissionMedium),
|
|
KittyDecodeError(KittyDecodeError),
|
|
InvalidKittyImage(InvalidKittyImage),
|
|
FileError(FileError),
|
|
ShmError(ShmError),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum FileError {
|
|
FileReadError(String),
|
|
UnsupportedPlatform,
|
|
}
|
|
|
|
impl From<InvalidKittyPayload> for KittyError {
|
|
fn from(value: InvalidKittyPayload) -> Self {
|
|
KittyError::InvalidKittyAction(InvalidKittyAction::InvalidKittyPayload(value))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ShmError {
|
|
ObjectOpenError(String),
|
|
MmapError,
|
|
EmptyObject,
|
|
ObjectTooSmall {
|
|
expected_bytes: usize,
|
|
actual_bytes: usize,
|
|
},
|
|
InvalidObjectSize,
|
|
FileStatError(String),
|
|
UnsupportedPlatform,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum KittyDecodeError {
|
|
InvalidBase64(String),
|
|
InvalidCompression(String),
|
|
InvalidUtf8(String),
|
|
}
|
|
|
|
impl From<KittyDecodeError> for InvalidKittyPayload {
|
|
fn from(value: KittyDecodeError) -> Self {
|
|
InvalidKittyPayload::KittyDecodeError(value)
|
|
}
|
|
}
|
|
|
|
impl From<KittyDecodeError> for KittyError {
|
|
fn from(value: KittyDecodeError) -> Self {
|
|
KittyError::InvalidKittyAction(InvalidKittyAction::InvalidKittyPayload(
|
|
InvalidKittyPayload::KittyDecodeError(value),
|
|
))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum InvalidKittyImage {
|
|
KittyPngError(KittyPngError),
|
|
KittyRgbError(KittyRgbError),
|
|
MalformedImage,
|
|
}
|
|
|
|
impl From<InvalidKittyImage> for KittyError {
|
|
fn from(value: InvalidKittyImage) -> Self {
|
|
KittyError::InvalidKittyAction(InvalidKittyAction::InvalidKittyPayload(
|
|
InvalidKittyPayload::InvalidKittyImage(value),
|
|
))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum KittyPngError {
|
|
InvalidBytes(String),
|
|
}
|
|
|
|
impl From<KittyPngError> for KittyError {
|
|
fn from(value: KittyPngError) -> Self {
|
|
KittyError::InvalidKittyAction(InvalidKittyAction::InvalidKittyPayload(
|
|
InvalidKittyPayload::InvalidKittyImage(InvalidKittyImage::KittyPngError(value)),
|
|
))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum KittyRgbError {
|
|
ExpectedDataSizeMismatch {
|
|
expected_bytes: usize,
|
|
actual_bytes: usize,
|
|
},
|
|
}
|
|
|
|
impl From<KittyRgbError> for KittyError {
|
|
fn from(value: KittyRgbError) -> Self {
|
|
KittyError::InvalidKittyAction(InvalidKittyAction::InvalidKittyPayload(
|
|
InvalidKittyPayload::InvalidKittyImage(InvalidKittyImage::KittyRgbError(value)),
|
|
))
|
|
}
|
|
}
|
|
|
|
impl From<CustomHeaderCreationError> for KittyRgbError {
|
|
fn from(value: CustomHeaderCreationError) -> Self {
|
|
match value {
|
|
CustomHeaderCreationError::ExpectedDataSizeMismatch {
|
|
expected_bytes,
|
|
actual_bytes,
|
|
} => KittyRgbError::ExpectedDataSizeMismatch {
|
|
expected_bytes,
|
|
actual_bytes,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<KittyMessage> for KittyAction {
|
|
type Error = KittyError;
|
|
|
|
fn try_from(message: KittyMessage) -> Result<KittyAction, KittyError> {
|
|
match message.control_data.placement_action {
|
|
KittyPlacementAction::StoreOnly => {
|
|
let mut action = StoreOnly {
|
|
image_id: message
|
|
.control_data
|
|
.image_id
|
|
.unwrap_or(rand::thread_rng().gen()),
|
|
image: KittyImage::try_from(message)?,
|
|
};
|
|
|
|
if action.image.metadata.pixel_data_format == KittyPixelDataFormat::Png {
|
|
action.image = set_kitty_png_size(action.image)?;
|
|
} else {
|
|
action.image = set_kitty_rgb_headers(action.image)?;
|
|
}
|
|
|
|
Ok(KittyAction::StoreOnly(action))
|
|
}
|
|
KittyPlacementAction::StoreAndDisplay => {
|
|
if message.control_data.unicode_placeholder {
|
|
return Err(InvalidControlData::UnicodePlaceholderUnsupported.into());
|
|
}
|
|
|
|
let mut action = StoreAndDisplay {
|
|
image_id: message
|
|
.control_data
|
|
.image_id
|
|
.unwrap_or(rand::thread_rng().gen()),
|
|
placement_id: message
|
|
.control_data
|
|
.placement_id
|
|
.unwrap_or(rand::thread_rng().gen()),
|
|
placement_data: KittyPlacementData {
|
|
z_index: message.control_data.z_index,
|
|
cols: message.control_data.cols,
|
|
rows: message.control_data.rows,
|
|
cursor_movement_policy: message.control_data.cursor_movement_policy,
|
|
},
|
|
image: KittyImage::try_from(message)?,
|
|
};
|
|
|
|
if action.image.metadata.pixel_data_format == KittyPixelDataFormat::Png {
|
|
action.image = set_kitty_png_size(action.image)?;
|
|
} else {
|
|
action.image = set_kitty_rgb_headers(action.image)?;
|
|
}
|
|
|
|
Ok(KittyAction::StoreAndDisplay(action))
|
|
}
|
|
KittyPlacementAction::DisplayStoredImage => {
|
|
if message.control_data.unicode_placeholder {
|
|
return Err(InvalidControlData::UnicodePlaceholderUnsupported.into());
|
|
}
|
|
|
|
let id = match message.control_data.image_id {
|
|
Some(id) => id,
|
|
None => return Err(InvalidControlData::IdMissing.into()),
|
|
};
|
|
|
|
Ok(KittyAction::DisplayStoredImage(DisplayStoredImage {
|
|
image_id: id,
|
|
placement_id: message
|
|
.control_data
|
|
.placement_id
|
|
.unwrap_or(rand::thread_rng().gen()),
|
|
placement_data: KittyPlacementData {
|
|
z_index: message.control_data.z_index,
|
|
cols: message.control_data.cols,
|
|
rows: message.control_data.rows,
|
|
cursor_movement_policy: message.control_data.cursor_movement_policy,
|
|
},
|
|
}))
|
|
}
|
|
KittyPlacementAction::QuerySupport => {
|
|
let mut action = QuerySupport {
|
|
image_id: message
|
|
.control_data
|
|
.image_id
|
|
.unwrap_or(rand::thread_rng().gen()),
|
|
image: KittyImage::try_from(message)?,
|
|
};
|
|
|
|
if action.image.metadata.pixel_data_format == KittyPixelDataFormat::Png {
|
|
action.image = set_kitty_png_size(action.image)?;
|
|
} else {
|
|
action.image = set_kitty_rgb_headers(action.image)?;
|
|
}
|
|
|
|
Ok(KittyAction::QuerySupport(action))
|
|
}
|
|
KittyPlacementAction::Delete => {
|
|
let deletion_type = match message.control_data.delete_action {
|
|
DeleteAction::DeleteAll => DeletionType::DeleteAll,
|
|
DeleteAction::DeleteById => {
|
|
let image_id = match message.control_data.image_id {
|
|
Some(image_id) => image_id,
|
|
None => return Err(InvalidControlData::IdMissing.into()),
|
|
};
|
|
|
|
DeletionType::DeleteById(DeleteById {
|
|
image_id,
|
|
placement_id: message.control_data.placement_id,
|
|
})
|
|
}
|
|
};
|
|
|
|
Ok(KittyAction::Delete {
|
|
deletion_type,
|
|
delete_placements_only: message.control_data.delete_placements_only,
|
|
})
|
|
}
|
|
KittyPlacementAction::Unknown => Err(InvalidKittyAction::UnsupportedAction.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct KittyImageMetadata {
|
|
pub pixel_data_format: KittyPixelDataFormat,
|
|
pub transmission_medium: KittyTransmissionMedium,
|
|
pub image_size: Vector2F,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct KittyPlacementData {
|
|
pub z_index: i32,
|
|
pub cols: Option<u32>,
|
|
pub rows: Option<u32>,
|
|
pub cursor_movement_policy: CursorMovementPolicy,
|
|
}
|
|
|
|
impl KittyPlacementData {
|
|
pub fn get_desired_dimensions(
|
|
&self,
|
|
image_size: Vector2F,
|
|
cell_height: usize,
|
|
cell_width: usize,
|
|
max_width_px: usize,
|
|
max_height_px: usize,
|
|
) -> Vector2F {
|
|
let aspect_ratio = image_size.x() / image_size.y();
|
|
|
|
let (width_px, height_px) = resize_dimensions(
|
|
image_size.x() as u32,
|
|
image_size.y() as u32,
|
|
min(image_size.x() as u32, max_width_px as u32),
|
|
min(image_size.y() as u32, max_height_px as u32),
|
|
FitType::Contain,
|
|
);
|
|
|
|
let (desired_width_px, desired_height_px) = match (self.cols, self.rows) {
|
|
(Some(width), None) => {
|
|
let desired_width_px = ((cell_width as u32) * width) as f32;
|
|
(desired_width_px, desired_width_px / aspect_ratio)
|
|
}
|
|
(None, Some(height)) => {
|
|
let desired_height_px = ((cell_height as u32) * height) as f32;
|
|
(desired_height_px * aspect_ratio, desired_height_px)
|
|
}
|
|
(Some(width), Some(height)) => (
|
|
((cell_width as u32) * width) as f32,
|
|
((cell_height as u32) * height) as f32,
|
|
),
|
|
(None, None) => (width_px as f32, height_px as f32),
|
|
};
|
|
|
|
Vector2F::new(desired_width_px, desired_height_px)
|
|
}
|
|
}
|
|
|
|
impl From<KittyControlData> for KittyImageMetadata {
|
|
fn from(control_data: KittyControlData) -> Self {
|
|
Self {
|
|
pixel_data_format: control_data.pixel_data_format,
|
|
image_size: Vector2F::new(control_data.width as f32, control_data.height as f32),
|
|
transmission_medium: control_data.transmission_medium,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct KittyChunk {
|
|
pub control_data: KittyControlData,
|
|
pub payload: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct PendingKittyMessage {
|
|
pub control_data: KittyControlData,
|
|
pub payload: Vec<Vec<u8>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct KittyMessage {
|
|
pub control_data: KittyControlData,
|
|
pub payload: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone)]
|
|
pub enum KittyPlacementAction {
|
|
#[default]
|
|
StoreOnly,
|
|
StoreAndDisplay,
|
|
DisplayStoredImage,
|
|
QuerySupport,
|
|
Delete,
|
|
Unknown,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct KittyControlData {
|
|
pixel_data_format: KittyPixelDataFormat,
|
|
width: u32,
|
|
height: u32,
|
|
pub compressed: bool,
|
|
transmission_medium: KittyTransmissionMedium,
|
|
pub further_chunks: bool,
|
|
pub image_id: Option<u32>,
|
|
pub placement_id: Option<u32>,
|
|
pub placement_action: KittyPlacementAction,
|
|
pub verbosity: KittyResponseVerbosity,
|
|
pub z_index: i32,
|
|
pub delete_action: DeleteAction,
|
|
delete_placements_only: bool,
|
|
pub rows: Option<u32>,
|
|
pub cols: Option<u32>,
|
|
pub cursor_movement_policy: CursorMovementPolicy,
|
|
pub unicode_placeholder: bool,
|
|
}
|
|
|
|
impl Default for KittyControlData {
|
|
fn default() -> Self {
|
|
Self {
|
|
pixel_data_format: KittyPixelDataFormat::default(),
|
|
width: 0,
|
|
height: 0,
|
|
compressed: false,
|
|
transmission_medium: KittyTransmissionMedium::default(),
|
|
further_chunks: false,
|
|
image_id: None,
|
|
placement_id: None,
|
|
placement_action: KittyPlacementAction::default(),
|
|
verbosity: KittyResponseVerbosity::default(),
|
|
z_index: 0,
|
|
delete_action: DeleteAction::default(),
|
|
delete_placements_only: true,
|
|
rows: None,
|
|
cols: None,
|
|
cursor_movement_policy: CursorMovementPolicy::default(),
|
|
unicode_placeholder: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Debug, PartialEq, Clone, Copy)]
|
|
pub enum DeleteAction {
|
|
#[default]
|
|
DeleteAll,
|
|
DeleteById,
|
|
}
|
|
|
|
#[derive(Default, Debug, PartialEq, Clone, Copy)]
|
|
pub enum KittyPixelDataFormat {
|
|
Rgb24Bit,
|
|
#[default]
|
|
Rgba32Bit,
|
|
Png,
|
|
}
|
|
|
|
#[derive(Default, Debug, PartialEq, Clone, Copy)]
|
|
pub enum KittyTransmissionMedium {
|
|
#[default]
|
|
Direct,
|
|
SimpleFile,
|
|
TemporaryFile,
|
|
SharedMemoryObject,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, Copy)]
|
|
pub enum KittyResponseVerbosity {
|
|
#[default]
|
|
All,
|
|
ErrorsOnly,
|
|
None,
|
|
}
|
|
|
|
impl KittyResponseVerbosity {
|
|
pub fn send_ok(&self) -> bool {
|
|
matches!(self, KittyResponseVerbosity::All)
|
|
}
|
|
|
|
pub fn send_error(&self) -> bool {
|
|
matches!(self, KittyResponseVerbosity::All)
|
|
|| matches!(self, KittyResponseVerbosity::ErrorsOnly)
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, Copy)]
|
|
pub enum CursorMovementPolicy {
|
|
#[default]
|
|
MoveCursor,
|
|
DoNotMoveCursor,
|
|
}
|
|
|
|
fn parse_kitty_control_data(control_data: &[u8]) -> KittyControlData {
|
|
let params: Vec<&[u8]> = control_data.split(|&byte| byte == b',').collect();
|
|
|
|
let mut parsed_control_data = KittyControlData::default();
|
|
for param in params {
|
|
let (key, value) = match param.iter().position(|&byte| byte == b'=') {
|
|
Some(position) => (¶m[..position], ¶m[position + 1..]),
|
|
None => continue,
|
|
};
|
|
|
|
match key {
|
|
b"f" => {
|
|
parsed_control_data.pixel_data_format = match value {
|
|
b"24" => KittyPixelDataFormat::Rgb24Bit,
|
|
b"32" => KittyPixelDataFormat::Rgba32Bit,
|
|
b"100" => KittyPixelDataFormat::Png,
|
|
_ => KittyPixelDataFormat::default(),
|
|
};
|
|
}
|
|
b"s" => {
|
|
if let Some(value) = parse_u32(value) {
|
|
parsed_control_data.width = value;
|
|
}
|
|
}
|
|
b"v" => {
|
|
if let Some(value) = parse_u32(value) {
|
|
parsed_control_data.height = value;
|
|
}
|
|
}
|
|
b"o" => {
|
|
parsed_control_data.compressed = value == b"z";
|
|
}
|
|
b"m" => {
|
|
parsed_control_data.further_chunks = value == b"1";
|
|
}
|
|
b"t" => {
|
|
parsed_control_data.transmission_medium = match value {
|
|
b"d" => KittyTransmissionMedium::Direct,
|
|
b"f" => KittyTransmissionMedium::SimpleFile,
|
|
b"t" => KittyTransmissionMedium::TemporaryFile,
|
|
b"s" => KittyTransmissionMedium::SharedMemoryObject,
|
|
_ => KittyTransmissionMedium::default(),
|
|
}
|
|
}
|
|
b"i" => {
|
|
if let Some(value) = parse_u32(value) {
|
|
parsed_control_data.image_id = Some(value);
|
|
}
|
|
}
|
|
b"p" => {
|
|
if let Some(value) = parse_u32(value) {
|
|
parsed_control_data.placement_id = Some(value);
|
|
}
|
|
}
|
|
b"a" => {
|
|
parsed_control_data.placement_action = match value {
|
|
b"t" => KittyPlacementAction::StoreOnly,
|
|
b"T" => KittyPlacementAction::StoreAndDisplay,
|
|
b"p" => KittyPlacementAction::DisplayStoredImage,
|
|
b"q" => KittyPlacementAction::QuerySupport,
|
|
b"d" => KittyPlacementAction::Delete,
|
|
_ => KittyPlacementAction::Unknown,
|
|
}
|
|
}
|
|
b"q" => {
|
|
parsed_control_data.verbosity = match value {
|
|
b"0" => KittyResponseVerbosity::All,
|
|
b"1" => KittyResponseVerbosity::ErrorsOnly,
|
|
b"2" => KittyResponseVerbosity::None,
|
|
_ => KittyResponseVerbosity::default(),
|
|
}
|
|
}
|
|
b"z" => {
|
|
if let Some(value) = parse_i32(value) {
|
|
parsed_control_data.z_index = value;
|
|
}
|
|
}
|
|
b"d" => {
|
|
if value.iter().all(|x| x.is_ascii_uppercase()) {
|
|
parsed_control_data.delete_placements_only = false;
|
|
}
|
|
|
|
parsed_control_data.delete_action = match &value.to_ascii_lowercase()[..] {
|
|
// Note: Kitty Protocol specifies "a" as "Delete all placements on screen",
|
|
// we're just starting with DeleteAll placements to expedite the launch.
|
|
b"a" => DeleteAction::DeleteAll,
|
|
b"i" => DeleteAction::DeleteById,
|
|
_ => DeleteAction::default(),
|
|
}
|
|
}
|
|
b"c" => {
|
|
if let Some(value) = parse_u32(value) {
|
|
parsed_control_data.cols = Some(value);
|
|
}
|
|
}
|
|
b"r" => {
|
|
if let Some(value) = parse_u32(value) {
|
|
parsed_control_data.rows = Some(value);
|
|
}
|
|
}
|
|
b"C" => {
|
|
parsed_control_data.cursor_movement_policy = match value {
|
|
b"0" => CursorMovementPolicy::MoveCursor,
|
|
b"1" => CursorMovementPolicy::DoNotMoveCursor,
|
|
_ => CursorMovementPolicy::default(),
|
|
}
|
|
}
|
|
b"U" => {
|
|
parsed_control_data.unicode_placeholder = value == b"1";
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
parsed_control_data
|
|
}
|
|
|
|
pub fn parse_kitty_chunk(chunk: Vec<u8>) -> KittyChunk {
|
|
let (control_data, payload) = match chunk.iter().position(|&byte| byte == b';') {
|
|
Some(position) => (&chunk[..position], &chunk[position + 1..]),
|
|
None => (&chunk[..], &vec![][..]),
|
|
};
|
|
|
|
let control_data = parse_kitty_control_data(control_data);
|
|
|
|
KittyChunk {
|
|
control_data,
|
|
payload: payload.to_vec(),
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
fn read_file(decoded_payload: Vec<u8>, is_temp: bool) -> Result<Vec<u8>, InvalidKittyPayload> {
|
|
let path = match str::from_utf8(&decoded_payload[..]) {
|
|
Ok(path) => path,
|
|
Err(err) => return Err(KittyDecodeError::InvalidUtf8(err.to_string()).into()),
|
|
};
|
|
|
|
let data = match fs::read(path) {
|
|
Ok(data) => data,
|
|
Err(err) => {
|
|
return Err(InvalidKittyPayload::FileError(FileError::FileReadError(
|
|
err.to_string(),
|
|
)))
|
|
}
|
|
};
|
|
|
|
if is_temp {
|
|
safe_delete_temp_file(path);
|
|
}
|
|
|
|
Ok(data)
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
fn safe_delete_temp_file(path: &str) {
|
|
if is_path_in_temp_dir(path) && path.contains("tty-graphics-protocol") {
|
|
if let Err(err) = fs::remove_file(path) {
|
|
log::error!("Failed to delete kitty temporary file (path = {path}): {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
fn is_path_in_temp_dir(path: &str) -> bool {
|
|
let temp_dirs = vec!["/tmp", "/var/tmp", "/dev/shm"];
|
|
|
|
for temp_dir in temp_dirs {
|
|
if path.starts_with(temp_dir) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if let Ok(temp_dir) = env::var("TMPDIR") {
|
|
if path.starts_with(&temp_dir) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
#[cfg(all(feature = "local_fs", unix))]
|
|
fn read_shared_memory(
|
|
control_data: KittyControlData,
|
|
decoded_payload: Vec<u8>,
|
|
) -> Result<Vec<u8>, InvalidKittyPayload> {
|
|
use nix::sys::mman::{shm_open, shm_unlink};
|
|
|
|
let path = match str::from_utf8(&decoded_payload[..]) {
|
|
Ok(path) => path,
|
|
Err(err) => return Err(KittyDecodeError::InvalidUtf8(err.to_string()).into()),
|
|
};
|
|
|
|
let fd = match shm_open(
|
|
path,
|
|
nix::fcntl::OFlag::O_RDONLY,
|
|
nix::sys::stat::Mode::empty(),
|
|
) {
|
|
Ok(fd) => fd,
|
|
Err(err) => {
|
|
return Err(InvalidKittyPayload::ShmError(ShmError::ObjectOpenError(
|
|
err.to_string(),
|
|
)))
|
|
}
|
|
};
|
|
|
|
let bytes_per_pixel = match control_data.pixel_data_format {
|
|
KittyPixelDataFormat::Rgb24Bit => Some(3),
|
|
KittyPixelDataFormat::Rgba32Bit => Some(4),
|
|
KittyPixelDataFormat::Png => None,
|
|
};
|
|
|
|
let size = bytes_per_pixel.map(|bytes_per_pixel| {
|
|
(bytes_per_pixel * control_data.width * control_data.height) as usize
|
|
});
|
|
|
|
let data = read_from_shared_memory_fd(fd, size);
|
|
|
|
if let Err(err) = shm_unlink(path) {
|
|
log::warn!("Failed to unlink kitty shm file (path = {path}): {err:?}");
|
|
};
|
|
|
|
data
|
|
}
|
|
|
|
#[cfg(all(feature = "local_fs", unix))]
|
|
fn read_from_shared_memory_fd(
|
|
fd: i32,
|
|
size: Option<usize>,
|
|
) -> Result<Vec<u8>, InvalidKittyPayload> {
|
|
use std::num::NonZero;
|
|
|
|
use nix::sys::mman::{mmap, MapFlags, ProtFlags};
|
|
use nix::sys::stat::fstat;
|
|
|
|
let file_size = match fstat(fd) {
|
|
Ok(stat) => stat.st_size,
|
|
Err(err) => {
|
|
return Err(InvalidKittyPayload::ShmError(ShmError::FileStatError(
|
|
err.to_string(),
|
|
)))
|
|
}
|
|
};
|
|
|
|
let Ok(file_size) = usize::try_from(file_size) else {
|
|
return Err(InvalidKittyPayload::ShmError(ShmError::InvalidObjectSize));
|
|
};
|
|
|
|
let size = size.unwrap_or(file_size);
|
|
|
|
if file_size < size {
|
|
return Err(InvalidKittyPayload::ShmError(ShmError::ObjectTooSmall {
|
|
expected_bytes: size,
|
|
actual_bytes: file_size,
|
|
}));
|
|
}
|
|
|
|
let Some(size) = NonZero::new(size) else {
|
|
return Err(InvalidKittyPayload::ShmError(ShmError::EmptyObject));
|
|
};
|
|
|
|
let ptr = unsafe {
|
|
mmap(
|
|
None,
|
|
size,
|
|
ProtFlags::PROT_READ,
|
|
MapFlags::MAP_SHARED,
|
|
fd,
|
|
0,
|
|
)
|
|
};
|
|
|
|
let Ok(ptr) = ptr else {
|
|
return Err(InvalidKittyPayload::ShmError(ShmError::MmapError));
|
|
};
|
|
|
|
let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, size.into()) };
|
|
let data = slice.to_vec();
|
|
|
|
Ok(data)
|
|
}
|
|
|
|
fn base64_decode_padding_agnostic(data: &[u8]) -> Result<Vec<u8>, KittyDecodeError> {
|
|
if let Ok(decoded_bytes) = base64::engine::general_purpose::STANDARD.decode(data) {
|
|
return Ok(decoded_bytes);
|
|
}
|
|
|
|
match base64::engine::general_purpose::STANDARD_NO_PAD.decode(data) {
|
|
Ok(decoded_bytes) => Ok(decoded_bytes),
|
|
Err(err) => Err(KittyDecodeError::InvalidBase64(err.to_string())),
|
|
}
|
|
}
|
|
|
|
pub fn decode_kitty_image_data(
|
|
payload: Vec<u8>,
|
|
compressed: bool,
|
|
) -> Result<Vec<u8>, KittyDecodeError> {
|
|
let mut result = payload;
|
|
|
|
if compressed {
|
|
let mut decoder = ZlibDecoder::new(&result[..]);
|
|
let mut decompresed_bytes = vec![];
|
|
match decoder.read_to_end(&mut decompresed_bytes) {
|
|
Ok(_) => {}
|
|
Err(err) => return Err(KittyDecodeError::InvalidCompression(err.to_string())),
|
|
};
|
|
result = decompresed_bytes;
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn set_kitty_png_size(mut image: KittyImage) -> Result<KittyImage, KittyPngError> {
|
|
let image_type = match ImageType::try_from_bytes(&image.data[..]) {
|
|
Ok(image_type) => image_type,
|
|
Err(err) => return Err(KittyPngError::InvalidBytes(err.to_string())),
|
|
};
|
|
let image_size = match image_type.image_size() {
|
|
Some(image_size) => image_size,
|
|
None => {
|
|
return Err(KittyPngError::InvalidBytes(
|
|
"Could not retrieve image size from ImageType for Kitty PNG.".to_string(),
|
|
))
|
|
}
|
|
};
|
|
|
|
image.metadata.image_size = image_size.to_f32();
|
|
|
|
Ok(image)
|
|
}
|
|
|
|
pub fn set_kitty_rgb_headers(mut image: KittyImage) -> Result<KittyImage, KittyRgbError> {
|
|
let image_format = match image.metadata.pixel_data_format {
|
|
KittyPixelDataFormat::Rgb24Bit => CustomImageFormat::Rgb,
|
|
KittyPixelDataFormat::Rgba32Bit => CustomImageFormat::Rgba,
|
|
KittyPixelDataFormat::Png => return Ok(image),
|
|
};
|
|
|
|
image.data = CustomImageHeader::prepend_custom_header(
|
|
image.data,
|
|
image.metadata.image_size.x() as u32,
|
|
image.metadata.image_size.y() as u32,
|
|
image_format,
|
|
)?;
|
|
|
|
Ok(image)
|
|
}
|
|
|
|
fn create_kitty_reply(image_id: u32, message: String) -> Vec<u8> {
|
|
[
|
|
C1::APC,
|
|
b"G",
|
|
format!("i={image_id};{message}").as_bytes(),
|
|
C1::ST,
|
|
]
|
|
.concat()
|
|
}
|
|
|
|
pub fn create_kitty_ok_reply(image_id: u32) -> Vec<u8> {
|
|
create_kitty_reply(image_id, "OK".to_string())
|
|
}
|
|
|
|
pub fn create_kitty_error_reply(image_id: u32, err: KittyError) -> Vec<u8> {
|
|
create_kitty_reply(image_id, format!("{err:?}"))
|
|
}
|