use std::sync::Arc;
use crate::input::InputEndType;
pub(crate) const MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES: usize = 8 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TerminalPassthrough {
kind: TerminalPassthroughKind,
cursor_x: u32,
cursor_y: u32,
palette_index: Option<TerminalPaletteIndex>,
clipboard_query: Option<TerminalClipboardQuery>,
payload: Arc<[u8]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalPassthroughKind {
Raw,
Clipboard,
PaletteQuery,
KittyGraphics,
Sixel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TerminalPaletteIndex(u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalClipboardQuery {
selection: Option<u8>,
terminator: InputEndType,
}
impl TerminalClipboardQuery {
const VALID_SELECTIONS: &'static [u8] = b"cpqs01234567";
#[must_use]
pub fn new(selection: &str, terminator: InputEndType) -> Self {
Self {
selection: selection
.bytes()
.find(|byte| Self::VALID_SELECTIONS.contains(byte)),
terminator,
}
}
#[must_use]
pub const fn selection(self) -> Option<u8> {
self.selection
}
#[must_use]
pub const fn terminator(self) -> InputEndType {
self.terminator
}
}
impl TerminalPaletteIndex {
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
value.parse::<u8>().ok().map(Self)
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
}
impl From<u8> for TerminalPaletteIndex {
fn from(value: u8) -> Self {
Self(value)
}
}
impl TerminalPassthrough {
#[must_use]
pub fn raw(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
Self {
kind: TerminalPassthroughKind::Raw,
cursor_x,
cursor_y,
palette_index: None,
clipboard_query: None,
payload: Arc::from(payload.into()),
}
}
#[must_use]
pub fn clipboard(payload: impl Into<Vec<u8>>) -> Self {
Self {
kind: TerminalPassthroughKind::Clipboard,
cursor_x: 0,
cursor_y: 0,
palette_index: None,
clipboard_query: None,
payload: Arc::from(payload.into()),
}
}
#[must_use]
pub fn clipboard_query(query: TerminalClipboardQuery, payload: impl Into<Vec<u8>>) -> Self {
Self {
kind: TerminalPassthroughKind::Clipboard,
cursor_x: 0,
cursor_y: 0,
palette_index: None,
clipboard_query: Some(query),
payload: Arc::from(payload.into()),
}
}
#[must_use]
pub fn palette_query(index: TerminalPaletteIndex) -> Self {
let payload = format!("\x1b]4;{};?\x1b\\", index.get()).into_bytes();
Self {
kind: TerminalPassthroughKind::PaletteQuery,
cursor_x: 0,
cursor_y: 0,
palette_index: Some(index),
clipboard_query: None,
payload: Arc::from(payload),
}
}
#[must_use]
pub fn kitty_graphics(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
Self {
kind: TerminalPassthroughKind::KittyGraphics,
cursor_x,
cursor_y,
palette_index: None,
clipboard_query: None,
payload: Arc::from(payload.into()),
}
}
#[must_use]
pub fn sixel(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
Self {
kind: TerminalPassthroughKind::Sixel,
cursor_x,
cursor_y,
palette_index: None,
clipboard_query: None,
payload: Arc::from(payload.into()),
}
}
#[must_use]
pub const fn kind(&self) -> TerminalPassthroughKind {
self.kind
}
#[must_use]
pub const fn cursor_x(&self) -> u32 {
self.cursor_x
}
#[must_use]
pub const fn cursor_y(&self) -> u32 {
self.cursor_y
}
#[must_use]
pub fn payload(&self) -> &[u8] {
&self.payload
}
#[must_use]
pub const fn palette_query_index(&self) -> Option<TerminalPaletteIndex> {
self.palette_index
}
#[must_use]
pub const fn clipboard_query_metadata(&self) -> Option<TerminalClipboardQuery> {
self.clipboard_query
}
#[must_use]
pub fn render_sequence(&self) -> Vec<u8> {
if self.clipboard_query.is_some() {
return Vec::new();
}
match self.kind {
TerminalPassthroughKind::Raw => self.payload.to_vec(),
TerminalPassthroughKind::Clipboard => self.payload.to_vec(),
TerminalPassthroughKind::PaletteQuery => self.payload.to_vec(),
TerminalPassthroughKind::KittyGraphics => {
let mut sequence = Vec::with_capacity(self.payload.len() + 4);
sequence.extend_from_slice(b"\x1b_");
sequence.extend_from_slice(&self.payload);
sequence.extend_from_slice(b"\x1b\\");
sequence
}
TerminalPassthroughKind::Sixel => {
let mut sequence = Vec::with_capacity(self.payload.len() + 4);
sequence.extend_from_slice(b"\x1bP");
sequence.extend_from_slice(&self.payload);
sequence.extend_from_slice(b"\x1b\\");
sequence
}
}
}
}
#[cfg(test)]
mod tests {
use super::{TerminalClipboardQuery, TerminalPaletteIndex, TerminalPassthrough};
use crate::input::InputEndType;
#[test]
fn renders_kitty_apc_sequence() {
let passthrough = TerminalPassthrough::kitty_graphics(0, 0, b"Gf=100;AAAA".to_vec());
assert_eq!(passthrough.render_sequence(), b"\x1b_Gf=100;AAAA\x1b\\");
}
#[test]
fn renders_raw_sequence_verbatim() {
let passthrough = TerminalPassthrough::raw(0, 0, b"\x1b]52;c;QQ==\x1b\\".to_vec());
assert_eq!(passthrough.render_sequence(), b"\x1b]52;c;QQ==\x1b\\");
}
#[test]
fn renders_clipboard_sequence_verbatim() {
let passthrough = TerminalPassthrough::clipboard(b"\x1b]52;c;QQ==\x07".to_vec());
assert_eq!(passthrough.render_sequence(), b"\x1b]52;c;QQ==\x07");
}
#[test]
fn clipboard_query_is_typed_bounded_and_never_generically_rendered() {
let query = TerminalClipboardQuery::new("zzpc", InputEndType::St);
assert_eq!(query.selection(), Some(b'p'));
assert_eq!(query.terminator(), InputEndType::St);
let passthrough =
TerminalPassthrough::clipboard_query(query, b"\x1b]52;zzpc;?\x1b\\".to_vec());
assert_eq!(
passthrough.kind(),
super::TerminalPassthroughKind::Clipboard
);
assert_eq!(passthrough.clipboard_query_metadata(), Some(query));
assert_eq!(passthrough.payload(), b"\x1b]52;zzpc;?\x1b\\");
assert!(passthrough.render_sequence().is_empty());
let invalid = TerminalClipboardQuery::new("xyz", InputEndType::Bel);
assert_eq!(invalid.selection(), None);
}
#[test]
fn palette_query_is_bounded_typed_and_canonical() {
assert_eq!(
TerminalPaletteIndex::parse("0").map(TerminalPaletteIndex::get),
Some(0)
);
assert_eq!(
TerminalPaletteIndex::parse("255").map(TerminalPaletteIndex::get),
Some(255)
);
assert_eq!(TerminalPaletteIndex::parse("256"), None);
assert_eq!(TerminalPaletteIndex::parse("-1"), None);
let query = TerminalPassthrough::palette_query(TerminalPaletteIndex::from(255));
assert_eq!(query.render_sequence(), b"\x1b]4;255;?\x1b\\");
assert_eq!(
query.palette_query_index(),
Some(TerminalPaletteIndex::from(255))
);
}
#[test]
fn renders_sixel_dcs_sequence() {
let passthrough = TerminalPassthrough::sixel(0, 0, b"q#0!10~".to_vec());
assert_eq!(passthrough.render_sequence(), b"\x1bPq#0!10~\x1b\\");
}
}