use crate::ansi::glyph_protocol;
use crate::ansi::iterm2_image_protocol;
use crate::ansi::kitty_graphics_protocol;
use crate::ansi::CursorShape;
use crate::ansi::{sixel, KeyboardModes, KeyboardModesApplyBehavior};
use crate::batched_parser::BatchedParser;
use crate::config::colors::{AnsiColor, ColorRgb, NamedColor};
use crate::crosswords::pos::{CharsetIndex, Column, Line, StandardCharset};
use crate::crosswords::square::Hyperlink;
use crate::simd_utf8;
use cursor_icon::CursorIcon;
use std::mem;
use std::str::FromStr;
use std::time::Duration;
use std::time::Instant;
use sugarloaf::GraphicData;
use tracing::{debug, warn};
use crate::crosswords::attr::Attr;
use crate::ansi::control::C0;
use crate::ansi::{
mode::{Mode, NamedPrivateMode, PrivateMode},
ClearMode, LineClearMode, TabulationClearMode,
};
use std::fmt::Write;
use copa::{Params, ParamsIter};
const SYNC_UPDATE_TIMEOUT: Duration = Duration::from_millis(150);
const SYNC_BUFFER_SIZE: usize = 0x20_0000;
const SYNC_ESCAPE_LEN: usize = 8;
const BSU_CSI: [u8; SYNC_ESCAPE_LEN] = *b"\x1b[?2026h";
const ESU_CSI: [u8; SYNC_ESCAPE_LEN] = *b"\x1b[?2026l";
fn xparse_color(color: &[u8]) -> Option<ColorRgb> {
if !color.is_empty() && color[0] == b'#' {
parse_legacy_color(&color[1..])
} else if color.len() >= 4 && &color[..4] == b"rgb:" {
parse_rgb_color(&color[4..])
} else {
None
}
}
fn parse_rgb_color(color: &[u8]) -> Option<ColorRgb> {
let colors = simd_utf8::from_utf8_fast(color)
.ok()?
.split('/')
.collect::<Vec<_>>();
if colors.len() != 3 {
return None;
}
let scale = |input: &str| {
if input.len() > 4 {
None
} else {
let max = u32::pow(16, input.len() as u32) - 1;
let value = u32::from_str_radix(input, 16).ok()?;
Some((255 * value / max) as u8)
}
};
Some(ColorRgb {
r: scale(colors[0])?,
g: scale(colors[1])?,
b: scale(colors[2])?,
})
}
fn parse_legacy_color(color: &[u8]) -> Option<ColorRgb> {
let item_len = color.len() / 3;
let color_from_slice = |slice: &[u8]| {
let col =
usize::from_str_radix(simd_utf8::from_utf8_fast(slice).ok()?, 16).ok()? << 4;
Some((col >> (4 * slice.len().saturating_sub(1))) as u8)
};
Some(ColorRgb {
r: color_from_slice(&color[0..item_len])?,
g: color_from_slice(&color[item_len..item_len * 2])?,
b: color_from_slice(&color[item_len * 2..])?,
})
}
fn parse_number(input: &[u8]) -> Option<u8> {
if input.is_empty() {
return None;
}
let mut num: u8 = 0;
for c in input {
let c = *c as char;
if let Some(digit) = c.to_digit(10) {
num = num
.checked_mul(10)
.and_then(|v| v.checked_add(digit as u8))?
} else {
return None;
}
}
Some(num)
}
fn parse_sgr_color(params: &mut dyn Iterator<Item = u16>) -> Option<AnsiColor> {
match params.next() {
Some(2) => Some(AnsiColor::Spec(ColorRgb {
r: u8::try_from(params.next()?).ok()?,
g: u8::try_from(params.next()?).ok()?,
b: u8::try_from(params.next()?).ok()?,
})),
Some(5) => Some(AnsiColor::Indexed(u8::try_from(params.next()?).ok()?)),
_ => None,
}
}
#[inline]
fn handle_colon_rgb(params: &[u16]) -> Option<AnsiColor> {
let rgb_start = if params.len() > 4 { 2 } else { 1 };
let rgb_iter = params[rgb_start..].iter().copied();
let mut iter = std::iter::once(params[0]).chain(rgb_iter);
parse_sgr_color(&mut iter)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScpCharPath {
Default,
LTR,
RTL,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScpUpdateMode {
ImplementationDependant,
DataToPresentation,
PresentationToData,
}
pub trait Handler {
fn set_title(&mut self, _: Option<String>) {}
fn set_current_directory(&mut self, _: std::path::PathBuf) {}
fn set_cursor_style(&mut self, _style: Option<CursorShape>, _blinking: bool) {}
fn set_cursor_shape(&mut self, _shape: CursorShape) {}
fn input(&mut self, _c: char) {}
fn goto(&mut self, _: Line, _: Column) {}
fn goto_line(&mut self, _: Line) {}
fn goto_col(&mut self, _: Column) {}
fn insert_blank(&mut self, _: usize) {}
fn move_up(&mut self, _: usize) {}
fn move_down(&mut self, _: usize) {}
fn identify_terminal(&mut self, _intermediate: Option<char>) {}
fn device_status(&mut self, _: usize) {}
fn move_forward(&mut self, _: Column) {}
fn move_backward(&mut self, _: Column) {}
fn move_down_and_cr(&mut self, _: usize) {}
fn move_up_and_cr(&mut self, _: usize) {}
fn put_tab(&mut self, _count: u16) {}
fn backspace(&mut self) {}
fn carriage_return(&mut self) {}
fn linefeed(&mut self) {}
fn bell(&mut self) {}
fn desktop_notification(&mut self, _title: String, _body: String) {}
fn substitute(&mut self) {}
fn newline(&mut self) {}
fn set_horizontal_tabstop(&mut self) {}
fn scroll_up(&mut self, _: usize) {}
fn scroll_down(&mut self, _: usize) {}
fn insert_blank_lines(&mut self, _: usize) {}
fn delete_lines(&mut self, _: usize) {}
fn erase_chars(&mut self, _: Column) {}
fn delete_chars(&mut self, _: usize) {}
fn move_backward_tabs(&mut self, _count: u16) {}
fn move_forward_tabs(&mut self, _count: u16) {}
fn save_cursor_position(&mut self) {}
fn restore_cursor_position(&mut self) {}
fn clear_line(&mut self, _mode: LineClearMode) {}
fn clear_screen(&mut self, _mode: ClearMode) {}
fn set_tabs(&mut self, _interval: u16) {}
fn clear_tabs(&mut self, _mode: TabulationClearMode) {}
fn reset_state(&mut self) {}
fn reverse_index(&mut self) {}
fn terminal_attribute(&mut self, _attr: Attr) {}
fn set_mode(&mut self, _mode: Mode) {}
fn unset_mode(&mut self, _mode: Mode) {}
fn report_mode(&mut self, _mode: Mode) {}
fn set_private_mode(&mut self, _mode: PrivateMode) {}
fn unset_private_mode(&mut self, _mode: PrivateMode) {}
fn report_private_mode(&mut self, _mode: PrivateMode) {}
fn report_version(&mut self) {}
fn set_scrolling_region(&mut self, _top: usize, _bottom: Option<usize>) {}
fn set_keypad_application_mode(&mut self) {}
fn unset_keypad_application_mode(&mut self) {}
fn set_active_charset(&mut self, _: CharsetIndex) {}
fn configure_charset(&mut self, _: CharsetIndex, _: StandardCharset) {}
fn set_color(&mut self, _: usize, _: ColorRgb) {}
fn dynamic_color_sequence(&mut self, _: String, _: usize, _: &str) {}
fn reset_color(&mut self, _: usize) {}
fn clipboard_store(&mut self, _: u8, _: &[u8]) {}
fn clipboard_load(&mut self, _: u8, _: &str) {}
fn decaln(&mut self) {}
fn push_title(&mut self) {}
fn pop_title(&mut self) {}
fn text_area_size_pixels(&mut self) {}
fn cells_size_pixels(&mut self) {}
fn text_area_size_chars(&mut self) {}
fn graphics_attribute(&mut self, _: u16, _: u16) {}
fn sixel_graphic_start(&mut self, _params: &Params) {}
fn is_sixel_graphic_active(&self) -> bool {
false
}
fn sixel_graphic_put(&mut self, _byte: u8) -> Result<(), sixel::Error> {
Ok(())
}
fn sixel_graphic_reset(&mut self) {}
fn sixel_graphic_finish(&mut self) {}
fn insert_graphic(
&mut self,
_data: GraphicData,
_palette: Option<Vec<ColorRgb>>,
_cursor_movement: Option<u8>,
) {
}
fn store_graphic(&mut self, _data: GraphicData) {}
fn kitty_transmit_and_display(
&mut self,
_data: GraphicData,
_placement: kitty_graphics_protocol::PlacementRequest,
) {
}
fn place_graphic(&mut self, _placement: kitty_graphics_protocol::PlacementRequest) {}
fn delete_graphics(&mut self, _delete: kitty_graphics_protocol::DeleteRequest) {}
fn set_hyperlink(&mut self, _: Option<Hyperlink>) {}
fn set_mouse_cursor_icon(&mut self, _: CursorIcon) {}
fn set_progress_report(&mut self, _: crate::event::ProgressReport) {}
fn report_keyboard_mode(&mut self) {}
fn push_keyboard_mode(&mut self, _mode: KeyboardModes) {}
fn pop_keyboard_modes(&mut self, _to_pop: u16) {}
fn set_keyboard_mode(
&mut self,
_mode: KeyboardModes,
_behavior: KeyboardModesApplyBehavior,
) {
}
fn xtgettcap_response(&mut self, _response: String) {}
fn kitty_graphics_response(&mut self, _response: String) {}
fn glyph_protocol_response(&mut self, _response: String) {}
fn glyph_register(
&mut self,
_cp: u32,
_payload: glyph_protocol::GlyphPayload,
) -> Result<(), glyph_protocol::RegisterError> {
Ok(())
}
fn glyph_clear(&mut self, _cp: Option<u32>) {}
fn glyph_query(&mut self, _cp: u32) {}
fn kitty_chunking_state_mut(
&mut self,
) -> Option<&mut kitty_graphics_protocol::KittyGraphicsState> {
None
}
fn set_scp(&mut self, _char_path: ScpCharPath, _update_mode: ScpUpdateMode) {}
}
pub trait Timeout: Default {
fn set_timeout(&mut self, duration: Duration);
fn clear_timeout(&mut self);
fn pending_timeout(&self) -> bool;
}
#[derive(Debug, Default)]
struct ProcessorState<T: Timeout> {
preceding_char: Option<char>,
sync_state: SyncState<T>,
xtgettcap_state: XtgettcapState,
apc_state: ApcState,
}
#[derive(Debug)]
struct SyncState<T: Timeout> {
timeout: T,
buffer: Vec<u8>,
}
#[derive(Debug, Default)]
struct XtgettcapState {
active: bool,
buffer: Vec<u8>,
}
#[derive(Debug, Default)]
struct ApcState {
buffer: Vec<u8>,
}
impl<T: Timeout> Default for SyncState<T> {
fn default() -> Self {
Self {
buffer: Vec::with_capacity(SYNC_BUFFER_SIZE),
timeout: Default::default(),
}
}
}
#[derive(Default)]
pub struct StdSyncHandler {
timeout: Option<Instant>,
}
impl StdSyncHandler {
#[inline]
pub fn sync_timeout(&self) -> Option<Instant> {
self.timeout
}
}
impl Timeout for StdSyncHandler {
#[inline]
fn set_timeout(&mut self, duration: Duration) {
self.timeout = Some(Instant::now() + duration);
}
#[inline]
fn clear_timeout(&mut self) {
self.timeout = None;
}
#[inline]
fn pending_timeout(&self) -> bool {
self.timeout.is_some()
}
}
#[derive(Default)]
pub struct Processor<T: Timeout = StdSyncHandler> {
state: ProcessorState<T>,
parser: BatchedParser<1024>,
}
impl<T: Timeout> Processor<T> {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn sync_timeout(&self) -> &T {
&self.state.sync_state.timeout
}
#[inline]
pub fn advance<H>(&mut self, handler: &mut H, bytes: &[u8])
where
H: Handler,
{
let mut processed = 0;
while processed != bytes.len() {
if self.state.sync_state.timeout.pending_timeout() {
processed += self.advance_sync(handler, &bytes[processed..]);
} else {
let mut performer = Performer::new(&mut self.state, handler);
processed += self
.parser
.advance_until_terminated(&mut performer, &bytes[processed..]);
}
}
}
#[inline]
pub fn flush<H>(&mut self, handler: &mut H)
where
H: Handler,
{
let mut performer = Performer::new(&mut self.state, handler);
self.parser.flush(&mut performer);
}
pub fn stop_sync<H>(&mut self, handler: &mut H)
where
H: Handler,
{
self.stop_sync_internal(handler, None);
}
fn stop_sync_internal<H>(&mut self, handler: &mut H, bsu_offset: Option<usize>)
where
H: Handler,
{
let buffer = mem::take(&mut self.state.sync_state.buffer);
let offset = bsu_offset.unwrap_or(buffer.len());
let mut performer = Performer::new(&mut self.state, handler);
self.parser.advance(&mut performer, &buffer[..offset]);
self.parser.flush(&mut performer);
self.state.sync_state.buffer = buffer;
match bsu_offset {
Some(bsu_offset) => {
let new_len = self.state.sync_state.buffer.len() - bsu_offset;
self.state.sync_state.buffer.copy_within(bsu_offset.., 0);
self.state.sync_state.buffer.truncate(new_len);
}
None => {
handler.unset_private_mode(NamedPrivateMode::SyncUpdate.into());
self.state.sync_state.timeout.clear_timeout();
self.state.sync_state.buffer.clear();
}
}
}
#[inline]
pub fn sync_bytes_count(&self) -> usize {
self.state.sync_state.buffer.len()
}
#[cold]
fn advance_sync<H>(&mut self, handler: &mut H, bytes: &[u8]) -> usize
where
H: Handler,
{
if self.state.sync_state.buffer.len() + bytes.len() >= SYNC_BUFFER_SIZE - 1 {
self.stop_sync_internal(handler, None);
let mut performer = Performer::new(&mut self.state, handler);
self.parser.advance_until_terminated(&mut performer, bytes)
} else {
self.state.sync_state.buffer.extend(bytes);
self.advance_sync_csi(handler, bytes.len());
bytes.len()
}
}
fn advance_sync_csi<H>(&mut self, handler: &mut H, new_bytes: usize)
where
H: Handler,
{
let buffer_len = self.state.sync_state.buffer.len();
let start_offset = (buffer_len - new_bytes).saturating_sub(SYNC_ESCAPE_LEN - 1);
let end_offset = buffer_len.saturating_sub(SYNC_ESCAPE_LEN - 1);
let search_buffer = &self.state.sync_state.buffer[start_offset..end_offset];
let mut bsu_offset = None;
for index in memchr::memchr_iter(0x1B, search_buffer).rev() {
let offset = start_offset + index;
let escape = &self.state.sync_state.buffer[offset..offset + SYNC_ESCAPE_LEN];
if escape == BSU_CSI {
self.state
.sync_state
.timeout
.set_timeout(SYNC_UPDATE_TIMEOUT);
bsu_offset = Some(offset);
} else if escape == ESU_CSI {
self.stop_sync_internal(handler, bsu_offset);
break;
}
}
}
}
struct Performer<'a, H: Handler, T: Timeout> {
state: &'a mut ProcessorState<T>,
handler: &'a mut H,
}
impl<'a, H: Handler + 'a, T: Timeout> Performer<'a, H, T> {
#[inline]
pub fn new<'b>(
state: &'b mut ProcessorState<T>,
handler: &'b mut H,
) -> Performer<'b, H, T> {
Performer { state, handler }
}
fn process_apc_buffer(&mut self) {
let buffer = &self.state.apc_state.buffer;
if buffer.is_empty() {
return;
}
let data = if buffer.last() == Some(&0x1b) {
&buffer[..buffer.len() - 1]
} else if buffer.len() >= 2
&& buffer[buffer.len() - 2] == 0x1b
&& buffer[buffer.len() - 1] == 0x5c
{
&buffer[..buffer.len() - 2]
} else {
buffer.as_slice()
};
debug!(
"[process_apc_buffer] Processing {} bytes (stripped to {}), starts with: {}",
buffer.len(),
data.len(),
String::from_utf8_lossy(&data[..data.len().min(50)])
);
if data.starts_with(glyph_protocol::GLYPH_PROTOCOL_PREFIX) {
let body = data.to_vec();
self.dispatch_glyph_protocol(&body);
return;
}
if data.first() == Some(&b'G') {
debug!("[process_apc_buffer] Kitty graphics APC detected");
let mut parts = data.splitn(2, |&b| b == b';');
let control_data = parts.next().unwrap_or(b"");
let payload_data = parts.next().unwrap_or(b"");
let control = if control_data.first() == Some(&b'G') {
&control_data[1..]
} else {
control_data
};
let control = if control.last() == Some(&0x1b) {
&control[..control.len() - 1]
} else {
control
};
let payload = if payload_data.last() == Some(&0x1b) {
&payload_data[..payload_data.len() - 1]
} else {
payload_data
};
let kitty_params: Vec<&[u8]> = vec![b"G", control, payload];
debug!(
"[process_apc_buffer] Parsed Kitty params: control={}, payload_len={}",
String::from_utf8_lossy(control),
payload.len()
);
let Some(chunking_state) = self.handler.kitty_chunking_state_mut() else {
debug!("[process_apc_buffer] Handler does not support Kitty graphics, ignoring");
return;
};
if let Some(response) =
kitty_graphics_protocol::parse(&kitty_params, chunking_state)
{
if response.incomplete {
debug!("[process_apc_buffer] Kitty graphics chunk accumulated");
return;
}
debug!("[process_apc_buffer] Kitty graphics parsed successfully");
if let Some(graphic_data) = response.graphic_data {
debug!(
"[process_apc_buffer] Graphic data present: id={}, {}x{}",
graphic_data.id.get(),
graphic_data.width,
graphic_data.height
);
if let Some(placement) = response.placement_request {
self.handler
.kitty_transmit_and_display(graphic_data, placement);
} else {
self.handler.store_graphic(graphic_data);
}
} else if let Some(placement) = response.placement_request {
debug!(
"[process_apc_buffer] Placement request: image_id={}",
placement.image_id
);
self.handler.place_graphic(placement);
}
if let Some(delete) = response.delete_request {
debug!("[process_apc_buffer] Delete request");
self.handler.delete_graphics(delete);
}
if let Some(response_str) = response.response {
self.handler.kitty_graphics_response(response_str);
}
} else {
warn!("[process_apc_buffer] Failed to parse kitty graphics protocol");
}
} else {
warn!(
"[process_apc_buffer] Unknown APC sequence: {}",
String::from_utf8_lossy(&buffer[..buffer.len().min(20)])
);
}
}
fn dispatch_glyph_protocol(&mut self, data: &[u8]) {
match glyph_protocol::parse(data) {
Ok(glyph_protocol::GlyphCommand::Support) => {
let resp = glyph_protocol::format_support_response(
glyph_protocol::SUPPORTED_FORMATS,
);
self.handler.glyph_protocol_response(resp);
}
Ok(glyph_protocol::GlyphCommand::Query { cp }) => {
self.handler.glyph_query(cp);
}
Ok(glyph_protocol::GlyphCommand::Register { cp, payload, reply }) => {
match self.handler.glyph_register(cp, payload) {
Ok(()) => {
if reply.emit_success() {
let resp = glyph_protocol::format_register_ok(cp);
self.handler.glyph_protocol_response(resp);
}
}
Err(reason) => {
if reply.emit_error() {
let resp = glyph_protocol::format_register_error(cp, reason);
self.handler.glyph_protocol_response(resp);
}
}
}
}
Ok(glyph_protocol::GlyphCommand::Clear { cp }) => {
self.handler.glyph_clear(cp);
let resp = glyph_protocol::format_clear_ok(cp);
self.handler.glyph_protocol_response(resp);
}
Err(glyph_protocol::ParseError::NotGlyphProtocol) => {
}
Err(glyph_protocol::ParseError::RegisterFailed { cp, reason, reply }) => {
if reply.emit_error() {
let resp = glyph_protocol::format_register_error(cp, reason);
self.handler.glyph_protocol_response(resp);
}
}
Err(glyph_protocol::ParseError::ClearOutOfNamespace) => {
let resp = glyph_protocol::format_clear_error_out_of_namespace();
self.handler.glyph_protocol_response(resp);
}
Err(glyph_protocol::ParseError::Malformed(why)) => {
warn!("[glyph_protocol] malformed APC: {why}");
}
}
}
}
impl<U: Handler, T: Timeout> copa::Perform for Performer<'_, U, T> {
fn print(&mut self, c: char) {
self.handler.input(c);
self.state.preceding_char = Some(c);
}
fn execute(&mut self, byte: u8) {
tracing::trace!("[execute] {byte:04x}");
match byte {
C0::HT => self.handler.put_tab(1),
C0::BS => self.handler.backspace(),
C0::CR => self.handler.carriage_return(),
C0::LF | C0::VT | C0::FF => self.handler.linefeed(),
C0::BEL => self.handler.bell(),
C0::SUB => self.handler.substitute(),
C0::SI => self.handler.set_active_charset(CharsetIndex::G0),
C0::SO => self.handler.set_active_charset(CharsetIndex::G1),
_ => warn!("[unhandled] execute byte={byte:02x}"),
}
}
fn hook(
&mut self,
params: &Params,
intermediates: &[u8],
ignore: bool,
action: char,
) {
match (action, intermediates) {
('q', []) => {
self.handler.sixel_graphic_start(params);
}
('q', [b'+']) => {
self.state.xtgettcap_state.active = true;
self.state.xtgettcap_state.buffer.clear();
}
_ => debug!(
"[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}",
params, intermediates, ignore, action
),
}
}
fn put(&mut self, byte: u8) {
if self.handler.is_sixel_graphic_active() {
if let Err(err) = self.handler.sixel_graphic_put(byte) {
tracing::warn!("Failed to parse Sixel data: {}", err);
self.handler.sixel_graphic_reset();
}
} else if self.state.xtgettcap_state.active {
self.state.xtgettcap_state.buffer.push(byte);
} else {
debug!("[unhandled put] byte={:?}", byte);
}
}
#[inline]
fn unhook(&mut self) {
if self.handler.is_sixel_graphic_active() {
self.handler.sixel_graphic_finish();
} else if self.state.xtgettcap_state.active {
let response = process_xtgettcap_request(&self.state.xtgettcap_state.buffer);
self.handler.xtgettcap_response(response);
self.state.xtgettcap_state.active = false;
self.state.xtgettcap_state.buffer.clear();
} else {
debug!("[unhandled dcs_unhook]");
}
}
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
debug!("[osc_dispatch] params={params:?} bell_terminated={bell_terminated}");
let terminator = if bell_terminated { "\x07" } else { "\x1b\\" };
fn unhandled(params: &[&[u8]]) {
let mut buf = String::new();
for items in params {
buf.push('[');
for item in *items {
let _ = write!(buf, "{:?}", *item as char);
}
buf.push_str("],");
}
warn!("[unhandled osc_dispatch]: [{}] at line {}", &buf, line!());
}
if params.is_empty() || params[0].is_empty() {
return;
}
match params[0] {
b"0" | b"2" => {
if params.len() >= 2 {
let title = params[1..]
.iter()
.flat_map(|x| simd_utf8::from_utf8_fast(x))
.collect::<Vec<&str>>()
.join(";")
.trim()
.to_owned();
self.handler.set_title(Some(title));
return;
}
unhandled(params);
}
b"4" => {
if params.len() <= 1 || params.len().is_multiple_of(2) {
unhandled(params);
return;
}
for chunk in params[1..].chunks(2) {
let index = match parse_number(chunk[0]) {
Some(index) => index,
None => {
unhandled(params);
continue;
}
};
if let Some(c) = xparse_color(chunk[1]) {
self.handler.set_color(index as usize, c);
} else if chunk[1] == b"?" {
let prefix = format!("4;{index}");
self.handler.dynamic_color_sequence(
prefix,
index as usize,
terminator,
);
} else {
unhandled(params);
}
}
}
b"7" => {
if let Ok(s) = simd_utf8::from_utf8_fast(params[1]) {
if let Ok(url) = url::Url::parse(s) {
let path = url.path();
#[cfg(windows)]
let path = &path[1..];
self.handler.set_current_directory(path.into());
}
}
}
b"8" if params.len() > 2 => {
let link_params = params[1];
let uri = simd_utf8::from_utf8_fast(params[2]).unwrap_or_default();
if uri.is_empty() {
self.handler.set_hyperlink(None);
return;
}
let id = link_params
.split(|&b| b == b':')
.find_map(|kv| kv.strip_prefix(b"id="))
.and_then(|kv| simd_utf8::from_utf8_fast(kv).ok());
self.handler.set_hyperlink(Some(Hyperlink::new(id, uri)));
}
b"9" => {
if params.len() >= 3 && params[1] == b"4" {
let state = match params[2] {
b"0" => Some(crate::event::ProgressState::Remove),
b"1" => Some(crate::event::ProgressState::Set),
b"2" => Some(crate::event::ProgressState::Error),
b"3" => Some(crate::event::ProgressState::Indeterminate),
b"4" => Some(crate::event::ProgressState::Pause),
_ => None,
};
if let Some(state) = state {
let progress = if params.len() >= 4 {
parse_number(params[3]).map(|p| p.min(100))
} else {
None
};
let report = crate::event::ProgressReport { state, progress };
self.handler.set_progress_report(report);
return;
}
}
if params.len() >= 2 {
let body = std::str::from_utf8(params[1])
.unwrap_or_default()
.to_string();
self.handler.desktop_notification(String::new(), body);
return;
}
unhandled(params);
}
b"777" => {
if params.len() >= 4 && params[1] == b"notify" {
let title = std::str::from_utf8(params[2])
.unwrap_or_default()
.to_string();
let body = std::str::from_utf8(params[3])
.unwrap_or_default()
.to_string();
self.handler.desktop_notification(title, body);
return;
}
unhandled(params);
}
b"10" | b"11" | b"12" => {
if params.len() >= 2 {
if let Some(mut dynamic_code) = parse_number(params[0]) {
for param in ¶ms[1..] {
let offset = dynamic_code as usize - 10;
let index = NamedColor::Foreground as usize + offset;
if index > NamedColor::Cursor as usize {
unhandled(params);
break;
}
if let Some(color) = xparse_color(param) {
self.handler.set_color(index, color);
} else if param == b"?" {
self.handler.dynamic_color_sequence(
dynamic_code.to_string(),
index,
terminator,
);
} else {
unhandled(params);
}
dynamic_code += 1;
}
return;
}
}
unhandled(params);
}
b"22" if params.len() == 2 => {
let shape = simd_utf8::from_utf8_lossy_fast(params[1]);
match CursorIcon::from_str(&shape) {
Ok(cursor_icon) => self.handler.set_mouse_cursor_icon(cursor_icon),
Err(_) => {
debug!("[osc 22] unrecognized cursor icon shape: {shape:?}")
}
}
}
b"50" => {
if params.len() >= 2
&& params[1].len() >= 13
&& params[1][0..12] == *b"CursorShape="
{
let shape = match params[1][12] as char {
'0' => CursorShape::Block,
'1' => CursorShape::Beam,
'2' => CursorShape::Underline,
_ => return unhandled(params),
};
self.handler.set_cursor_shape(shape);
return;
}
unhandled(params);
}
b"52" => {
if params.len() < 3 {
return unhandled(params);
}
let clipboard = params[1].first().unwrap_or(&b'c');
match params[2] {
b"?" => self.handler.clipboard_load(*clipboard, terminator),
base64 => self.handler.clipboard_store(*clipboard, base64),
}
}
b"104" => {
if params.len() == 1 || params[1].is_empty() {
for i in 0..256 {
self.handler.reset_color(i);
}
return;
}
for param in ¶ms[1..] {
match parse_number(param) {
Some(index) => self.handler.reset_color(index as usize),
None => unhandled(params),
}
}
}
b"110" => self.handler.reset_color(NamedColor::Foreground as usize),
b"111" => self.handler.reset_color(NamedColor::Background as usize),
b"112" => self.handler.reset_color(NamedColor::Cursor as usize),
b"1337" => {
if let Some(graphic) = iterm2_image_protocol::parse(params) {
self.handler.insert_graphic(graphic, None, None);
}
}
_ => unhandled(params),
}
}
fn csi_dispatch(
&mut self,
params: &Params,
intermediates: &[u8],
should_ignore: bool,
action: char,
) {
debug!("[csi_dispatch] {params:?} {action:?}");
macro_rules! csi_unhandled {
() => {{
warn!(
"[csi_dispatch] params={params:#?}, intermediates={intermediates:?}, should_ignore={should_ignore:?}, action={action:?}"
);
}};
}
if should_ignore || intermediates.len() > 2 {
csi_unhandled!();
return;
}
let mut params_iter = params.iter();
let handler = &mut self.handler;
let mut next_param_or = |default: u16| match params_iter.next() {
Some(&[param, ..]) if param != 0 => param,
_ => default,
};
match (action, intermediates) {
('@', []) => handler.insert_blank(next_param_or(1) as usize),
('A', []) => handler.move_up(next_param_or(1) as usize),
('B', []) | ('e', []) => handler.move_down(next_param_or(1) as usize),
('b', []) => {
if let Some(c) = self.state.preceding_char {
for _ in 0..next_param_or(1) {
handler.input(c);
}
} else {
warn!("tried to repeat with no preceding char");
}
}
('C', []) | ('a', []) => {
handler.move_forward(Column(next_param_or(1) as usize))
}
('c', intermediates) if next_param_or(0) == 0 => {
handler.identify_terminal(intermediates.first().map(|&i| i as char))
}
('D', []) => handler.move_backward(Column(next_param_or(1) as usize)),
('d', []) => handler.goto_line(Line(next_param_or(1) as i32 - 1)),
('E', []) => handler.move_down_and_cr(next_param_or(1) as usize),
('F', []) => handler.move_up_and_cr(next_param_or(1) as usize),
('G', []) | ('`', []) => {
handler.goto_col(Column(next_param_or(1) as usize - 1))
}
('W', [b'?']) if next_param_or(0) == 5 => handler.set_tabs(8),
('g', []) => {
let mode = match next_param_or(0) {
0 => TabulationClearMode::Current,
3 => TabulationClearMode::All,
_ => {
csi_unhandled!();
return;
}
};
handler.clear_tabs(mode);
}
('H', []) | ('f', []) => {
let y = next_param_or(1) as i32;
let x = next_param_or(1) as usize;
handler.goto(Line(y - 1), Column(x - 1));
}
('h', []) => {
for param in params_iter.map(|param| param[0]) {
handler.set_mode(Mode::new(param))
}
}
('h', [b'?']) => {
for param in params_iter.map(|param| param[0]) {
if param == NamedPrivateMode::SyncUpdate as u16 {
self.state
.sync_state
.timeout
.set_timeout(SYNC_UPDATE_TIMEOUT);
}
handler.set_private_mode(PrivateMode::new(param))
}
}
('I', []) => handler.move_forward_tabs(next_param_or(1)),
('J', []) => {
let mode = match next_param_or(0) {
0 => ClearMode::Below,
1 => ClearMode::Above,
2 => ClearMode::All,
3 => ClearMode::Saved,
_ => {
csi_unhandled!();
return;
}
};
handler.clear_screen(mode);
}
('K', []) => {
let mode = match next_param_or(0) {
0 => LineClearMode::Right,
1 => LineClearMode::Left,
2 => LineClearMode::All,
_ => {
csi_unhandled!();
return;
}
};
handler.clear_line(mode);
}
('k', [b' ']) => {
let char_path = match next_param_or(0) {
0 => ScpCharPath::Default,
1 => ScpCharPath::LTR,
2 => ScpCharPath::RTL,
_ => {
csi_unhandled!();
return;
}
};
let update_mode = match next_param_or(0) {
0 => ScpUpdateMode::ImplementationDependant,
1 => ScpUpdateMode::DataToPresentation,
2 => ScpUpdateMode::PresentationToData,
_ => {
csi_unhandled!();
return;
}
};
handler.set_scp(char_path, update_mode);
}
('L', []) => handler.insert_blank_lines(next_param_or(1) as usize),
('l', []) => {
for param in params_iter.map(|param| param[0]) {
handler.unset_mode(Mode::new(param))
}
}
('l', [b'?']) => {
for param in params_iter.map(|param| param[0]) {
handler.unset_private_mode(PrivateMode::new(param))
}
}
('M', []) => handler.delete_lines(next_param_or(1) as usize),
('m', []) => {
if params.is_empty() {
handler.terminal_attribute(Attr::Reset);
} else {
for attr in attrs_from_sgr_parameters(&mut params_iter) {
match attr {
Some(attr) => handler.terminal_attribute(attr),
None => csi_unhandled!(),
}
}
}
}
('n', []) => handler.device_status(next_param_or(0) as usize),
('P', []) => handler.delete_chars(next_param_or(1) as usize),
('p', [b'$']) => {
let mode = next_param_or(0);
handler.report_mode(Mode::new(mode));
}
('p', [b'?', b'$']) => {
let mode = next_param_or(0);
handler.report_private_mode(PrivateMode::new(mode));
}
('q', [b'>']) => {
if next_param_or(0) != 0 {
csi_unhandled!();
return;
}
handler.report_version();
}
('q', [b' ']) => {
let cursor_style_id = next_param_or(0);
let shape = match cursor_style_id {
0 => None,
1 | 2 => Some(CursorShape::Block),
3 | 4 => Some(CursorShape::Underline),
5 | 6 => Some(CursorShape::Beam),
_ => {
csi_unhandled!();
return;
}
};
handler.set_cursor_style(shape, cursor_style_id % 2 == 1);
}
('r', []) => {
let top = next_param_or(1) as usize;
let bottom = params_iter
.next()
.map(|param| param[0] as usize)
.filter(|¶m| param != 0);
handler.set_scrolling_region(top, bottom);
}
('S', []) => handler.scroll_up(next_param_or(1) as usize),
('S', [b'?']) => {
handler.graphics_attribute(next_param_or(0), next_param_or(0))
}
('s', []) => handler.save_cursor_position(),
('T', []) => handler.scroll_down(next_param_or(1) as usize),
('t', []) => match next_param_or(1) as usize {
14 => handler.text_area_size_pixels(),
16 => handler.cells_size_pixels(),
18 => handler.text_area_size_chars(),
22 => handler.push_title(),
23 => handler.pop_title(),
_ => csi_unhandled!(),
},
('u', [b'?']) => handler.report_keyboard_mode(),
('u', [b'=']) => {
let mode = KeyboardModes::from_bits_truncate(next_param_or(0) as u8);
let behavior = match next_param_or(1) {
3 => KeyboardModesApplyBehavior::Difference,
2 => KeyboardModesApplyBehavior::Union,
_ => KeyboardModesApplyBehavior::Replace,
};
handler.set_keyboard_mode(mode, behavior);
}
('u', [b'>']) => {
let mode = KeyboardModes::from_bits_truncate(next_param_or(0) as u8);
handler.push_keyboard_mode(mode);
}
('u', [b'<']) => {
handler.pop_keyboard_modes(next_param_or(1));
}
('u', []) => handler.restore_cursor_position(),
('X', []) => handler.erase_chars(Column(next_param_or(1) as usize)),
('Z', []) => handler.move_backward_tabs(next_param_or(1)),
_ => csi_unhandled!(),
};
}
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
macro_rules! unhandled {
() => {{
warn!(
"[unhandled] esc_dispatch ints={:?}, byte={:?} ({:02x})",
intermediates, byte as char, byte
);
}};
}
macro_rules! configure_charset {
($charset:path, $intermediates:expr) => {{
let index: CharsetIndex = match $intermediates {
[b'('] => CharsetIndex::G0,
[b')'] => CharsetIndex::G1,
[b'*'] => CharsetIndex::G2,
[b'+'] => CharsetIndex::G3,
_ => {
unhandled!();
return;
}
};
self.handler.configure_charset(index, $charset)
}};
}
match (byte, intermediates) {
(b'B', intermediates) => {
configure_charset!(StandardCharset::Ascii, intermediates)
}
(b'D', []) => self.handler.linefeed(),
(b'E', []) => {
self.handler.linefeed();
self.handler.carriage_return();
}
(b'H', []) => self.handler.set_horizontal_tabstop(),
(b'M', []) => self.handler.reverse_index(),
(b'Z', []) => self.handler.identify_terminal(None),
(b'c', []) => self.handler.reset_state(),
(b'0', intermediates) => {
configure_charset!(
StandardCharset::SpecialCharacterAndLineDrawing,
intermediates
)
}
(b'7', []) => self.handler.save_cursor_position(),
(b'8', [b'#']) => self.handler.decaln(),
(b'8', []) => self.handler.restore_cursor_position(),
(b'=', []) => self.handler.set_keypad_application_mode(),
(b'>', []) => self.handler.unset_keypad_application_mode(),
(b'\\', []) => (),
_ => unhandled!(),
}
}
fn apc_start(&mut self) {
debug!("[apc_start] Beginning APC accumulation");
self.state.apc_state.buffer.clear();
self.state.apc_state.buffer.reserve(4096);
}
fn apc_put(&mut self, byte: u8) {
self.state.apc_state.buffer.push(byte);
}
fn apc_end(&mut self) {
debug!(
"[apc_end] APC complete, accumulated {} bytes",
self.state.apc_state.buffer.len()
);
self.process_apc_buffer();
}
}
#[inline]
fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec<Option<Attr>> {
let mut attrs = Vec::with_capacity(params.size_hint().0);
while let Some(param) = params.next() {
let attr = match param {
[0] => Some(Attr::Reset),
[1] => Some(Attr::Bold),
[2] => Some(Attr::Dim),
[3] => Some(Attr::Italic),
[4, 0] => Some(Attr::CancelUnderline),
[4, 2] => Some(Attr::DoubleUnderline),
[4, 3] => Some(Attr::Undercurl),
[4, 4] => Some(Attr::DottedUnderline),
[4, 5] => Some(Attr::DashedUnderline),
[4, ..] => Some(Attr::Underline),
[5] => Some(Attr::BlinkSlow),
[6] => Some(Attr::BlinkFast),
[7] => Some(Attr::Reverse),
[8] => Some(Attr::Hidden),
[9] => Some(Attr::Strike),
[21] => Some(Attr::CancelBold),
[22] => Some(Attr::CancelBoldDim),
[23] => Some(Attr::CancelItalic),
[24] => Some(Attr::CancelUnderline),
[25] => Some(Attr::CancelBlink),
[27] => Some(Attr::CancelReverse),
[28] => Some(Attr::CancelHidden),
[29] => Some(Attr::CancelStrike),
[30] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::Black))),
[31] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::Red))),
[32] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::Green))),
[33] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::Yellow))),
[34] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::Blue))),
[35] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::Magenta))),
[36] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::Cyan))),
[37] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::White))),
[38] => {
let mut iter = params.map(|param| param[0]);
parse_sgr_color(&mut iter).map(Attr::Foreground)
}
[38, params @ ..] => handle_colon_rgb(params).map(Attr::Foreground),
[39] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::Foreground))),
[40] => Some(Attr::Background(AnsiColor::Named(NamedColor::Black))),
[41] => Some(Attr::Background(AnsiColor::Named(NamedColor::Red))),
[42] => Some(Attr::Background(AnsiColor::Named(NamedColor::Green))),
[43] => Some(Attr::Background(AnsiColor::Named(NamedColor::Yellow))),
[44] => Some(Attr::Background(AnsiColor::Named(NamedColor::Blue))),
[45] => Some(Attr::Background(AnsiColor::Named(NamedColor::Magenta))),
[46] => Some(Attr::Background(AnsiColor::Named(NamedColor::Cyan))),
[47] => Some(Attr::Background(AnsiColor::Named(NamedColor::White))),
[48] => {
let mut iter = params.map(|param| param[0]);
parse_sgr_color(&mut iter).map(Attr::Background)
}
[48, params @ ..] => handle_colon_rgb(params).map(Attr::Background),
[49] => Some(Attr::Background(AnsiColor::Named(NamedColor::Background))),
[58] => {
let mut iter = params.map(|param| param[0]);
parse_sgr_color(&mut iter).map(|color| Attr::UnderlineColor(Some(color)))
}
[58, params @ ..] => {
handle_colon_rgb(params).map(|color| Attr::UnderlineColor(Some(color)))
}
[59] => Some(Attr::UnderlineColor(None)),
[90] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::LightBlack))),
[91] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::LightRed))),
[92] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::LightGreen))),
[93] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::LightYellow))),
[94] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::LightBlue))),
[95] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::LightMagenta))),
[96] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::LightCyan))),
[97] => Some(Attr::Foreground(AnsiColor::Named(NamedColor::LightWhite))),
[100] => Some(Attr::Background(AnsiColor::Named(NamedColor::LightBlack))),
[101] => Some(Attr::Background(AnsiColor::Named(NamedColor::LightRed))),
[102] => Some(Attr::Background(AnsiColor::Named(NamedColor::LightGreen))),
[103] => Some(Attr::Background(AnsiColor::Named(NamedColor::LightYellow))),
[104] => Some(Attr::Background(AnsiColor::Named(NamedColor::LightBlue))),
[105] => Some(Attr::Background(AnsiColor::Named(NamedColor::LightMagenta))),
[106] => Some(Attr::Background(AnsiColor::Named(NamedColor::LightCyan))),
[107] => Some(Attr::Background(AnsiColor::Named(NamedColor::LightWhite))),
_ => None,
};
attrs.push(attr);
}
attrs
}
fn process_xtgettcap_request(buffer: &[u8]) -> String {
debug!("Processing XTGETTCAP request: {:?}", buffer);
let mut response = String::new();
for query in buffer.split(|&b| b == b';') {
if query.is_empty() {
continue;
}
let capability_name = match decode_hex_string(query) {
Ok(name) => name,
Err(_) => {
debug!("Invalid hex encoding in XTGETTCAP request");
continue;
}
};
debug!("XTGETTCAP query for: {}", capability_name);
let hex_name = encode_hex_string(&capability_name);
if let Some(value) = get_termcap_capability(&capability_name) {
if value.is_empty() {
response.push_str(&format!("\x1bP1+r{hex_name}\x1b\\"));
} else {
let decoded = decode_terminfo_value(&value);
let hex_value = encode_hex_bytes(&decoded);
response.push_str(&format!("\x1bP1+r{hex_name}={hex_value}\x1b\\"));
}
} else {
response.push_str(&format!("\x1bP0+r{hex_name}\x1b\\"));
}
}
if response.is_empty() {
"\x1bP0+r\x1b\\".to_string()
} else {
response
}
}
fn decode_terminfo_value(value: &str) -> Vec<u8> {
if value.contains('%') {
return value.as_bytes().to_vec();
}
let mut result = Vec::with_capacity(value.len());
let bytes = value.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
match bytes[i + 1] {
b'E' => {
result.push(0x1b);
i += 2;
}
b'n' => {
result.push(b'\n');
i += 2;
}
b'r' => {
result.push(b'\r');
i += 2;
}
b't' => {
result.push(b'\t');
i += 2;
}
b'\\' => {
result.push(b'\\');
i += 2;
}
_ => {
result.push(bytes[i]);
i += 1;
}
}
} else if bytes[i] == b'^' && i + 1 < bytes.len() {
let ctrl = if bytes[i + 1] == b'?' {
0x7F
} else {
bytes[i + 1].wrapping_sub(64)
};
result.push(ctrl);
i += 2;
} else {
result.push(bytes[i]);
i += 1;
}
}
result
}
fn encode_hex_bytes(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02X}")).collect()
}
fn decode_hex_string(hex_bytes: &[u8]) -> Result<String, &'static str> {
if !hex_bytes.len().is_multiple_of(2) {
return Err("Invalid hex string length");
}
let mut result = Vec::new();
for chunk in hex_bytes.chunks(2) {
let hex_str = std::str::from_utf8(chunk).map_err(|_| "Invalid UTF-8")?;
let byte = u8::from_str_radix(hex_str, 16).map_err(|_| "Invalid hex digit")?;
result.push(byte);
}
String::from_utf8(result).map_err(|_| "Invalid UTF-8 in decoded string")
}
fn encode_hex_string(s: &str) -> String {
s.bytes().map(|b| format!("{b:02X}")).collect()
}
fn get_termcap_capability(name: &str) -> Option<String> {
debug!("XTGETTCAP query for capability: {}", name);
match name {
"TN" | "name" => Some("rio".to_string()),
"Co" | "colors" => Some("256".to_string()),
"pa" | "pairs" => Some("32767".to_string()),
"RGB" => Some("8/8/8".to_string()),
"ccc" => Some("".to_string()),
"co" | "cols" => Some("80".to_string()),
"li" | "lines" => Some("24".to_string()),
"it" => Some("8".to_string()),
"OTbs" | "bs" => Some("".to_string()), "am" => Some("".to_string()), "bce" => Some("".to_string()), "km" => Some("".to_string()), "mir" => Some("".to_string()), "msgr" => Some("".to_string()), "xenl" | "xn" => Some("".to_string()), "AX" => Some("".to_string()), "XT" => Some("".to_string()), "XF" => Some("".to_string()), "hs" => Some("".to_string()), "ms" => Some("".to_string()), "mi" => Some("".to_string()), "mc5i" => Some("".to_string()), "npc" => Some("".to_string()),
"sixel" => Some("".to_string()), "iterm2" => Some("".to_string()), "TK" | "kitty" => Some("yes".to_string()),
"cup" | "cm" => Some("\\E[%i%p1%d;%p2%dH".to_string()),
"cuu1" | "up" => Some("\\E[A".to_string()),
"cud1" | "do" => Some("\\n".to_string()),
"cuf1" | "nd" => Some("\\E[C".to_string()),
"cub1" | "le" => Some("^H".to_string()),
"home" | "ho" => Some("\\E[H".to_string()),
"cuu" | "UP" => Some("\\E[%p1%dA".to_string()),
"cud" | "DO" => Some("\\E[%p1%dB".to_string()),
"cuf" | "RI" => Some("\\E[%p1%dC".to_string()),
"cub" | "LE" => Some("\\E[%p1%dD".to_string()),
"hpa" => Some("\\E[%i%p1%dG".to_string()),
"vpa" => Some("\\E[%i%p1%dd".to_string()),
"clear" | "cl" => Some("\\E[H\\E[2J".to_string()),
"el" | "ce" => Some("\\E[K".to_string()),
"ed" | "cd" => Some("\\E[J".to_string()),
"el1" => Some("\\E[1K".to_string()),
"E3" => Some("\\E[3J".to_string()),
"setaf" => Some("\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m".to_string()),
"setab" => Some("\\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m".to_string()),
"setf" => Some("\\E[3%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m".to_string()),
"setb" => Some("\\E[4%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m".to_string()),
"op" => Some("\\E[39;49m".to_string()),
"oc" => Some("\\E]104\\007".to_string()),
"initc" => Some("\\E]4;%p1%d;rgb\\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\\E\\\\".to_string()),
"bold" | "md" => Some("\\E[1m".to_string()),
"dim" | "mh" => Some("\\E[2m".to_string()),
"smul" | "us" => Some("\\E[4m".to_string()),
"rmul" | "ue" => Some("\\E[24m".to_string()),
"rev" | "mr" => Some("\\E[7m".to_string()),
"smso" | "so" => Some("\\E[7m".to_string()),
"rmso" | "se" => Some("\\E[27m".to_string()),
"invis" => Some("\\E[8m".to_string()),
"blink" | "mb" => Some("\\E[5m".to_string()),
"sitm" => Some("\\E[3m".to_string()),
"ritm" => Some("\\E[23m".to_string()),
"smxx" => Some("\\E[9m".to_string()),
"rmxx" => Some("\\E[29m".to_string()),
"sgr0" | "me" => Some("\\E(B\\E[m".to_string()),
"sgr" => Some("%?%p9%t\\E(0%e\\E(B%;\\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m".to_string()),
"smacs" | "as" => Some("\\E(0".to_string()),
"rmacs" | "ae" => Some("\\E(B".to_string()),
"acsc" => Some("``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~".to_string()),
"ich" | "IC" => Some("\\E[%p1%d@".to_string()),
"dch1" | "dc" => Some("\\E[P".to_string()),
"dch" | "DC" => Some("\\E[%p1%dP".to_string()),
"il1" | "al" => Some("\\E[L".to_string()),
"il" | "AL" => Some("\\E[%p1%dL".to_string()),
"dl1" => Some("\\E[M".to_string()),
"dl" | "DL" => Some("\\E[%p1%dM".to_string()),
"ech" | "ec" => Some("\\E[%p1%dX".to_string()),
"csr" | "cs" => Some("\\E[%i%p1%d;%p2%dr".to_string()),
"ri" | "sr" => Some("\\EM".to_string()),
"ind" | "sf" => Some("\\n".to_string()),
"indn" | "SF" => Some("\\E[%p1%dS".to_string()),
"rin" | "SR" => Some("\\E[%p1%dT".to_string()),
"civis" | "vi" => Some("\\E[?25l".to_string()),
"cnorm" | "ve" => Some("\\E[?12l\\E[?25h".to_string()),
"cvvis" | "vs" => Some("\\E[?12;25h".to_string()),
"Ss" => Some("\\E[%p1%d q".to_string()),
"Se" => Some("\\E[0 q".to_string()),
"Cs" => Some("\\E]12;%p1%s\\007".to_string()),
"Cr" => Some("\\E]112\\007".to_string()),
"smkx" | "ks" => Some("\\E[?1h\\E=".to_string()),
"rmkx" | "ke" => Some("\\E[?1l\\E>".to_string()),
"smir" | "im" => Some("\\E[4h".to_string()),
"rmir" | "ei" => Some("\\E[4l".to_string()),
"smam" => Some("\\E[?7h".to_string()),
"rmam" => Some("\\E[?7l".to_string()),
"smm" => Some("\\E[?1034h".to_string()),
"rmm" => Some("\\E[?1034l".to_string()),
"smcup" | "ti" => Some("\\E[?1049h\\E[22;0;0t".to_string()),
"rmcup" | "te" => Some("\\E[?1049l\\E[23;0;0t".to_string()),
"sc" => Some("\\E7".to_string()),
"rc" => Some("\\E8".to_string()),
"ht" | "ta" => Some("^I".to_string()),
"hts" | "st" => Some("\\EH".to_string()),
"tbc" | "ct" => Some("\\E[3g".to_string()),
"cbt" | "bt" => Some("\\E[Z".to_string()),
"bel" | "bl" => Some("^G".to_string()),
"flash" | "vb" => Some("\\E[?5h$<100/>\\E[?5l".to_string()),
"tsl" | "ts" => Some("\\E]2;".to_string()),
"fsl" | "fs" => Some("^G".to_string()),
"dsl" | "ds" => Some("\\E]2;\\007".to_string()),
"kf1" | "k1" => Some("\\EOP".to_string()),
"kf2" | "k2" => Some("\\EOQ".to_string()),
"kf3" | "k3" => Some("\\EOR".to_string()),
"kf4" | "k4" => Some("\\EOS".to_string()),
"kf5" | "k5" => Some("\\E[15~".to_string()),
"kf6" | "k6" => Some("\\E[17~".to_string()),
"kf7" | "k7" => Some("\\E[18~".to_string()),
"kf8" | "k8" => Some("\\E[19~".to_string()),
"kf9" | "k9" => Some("\\E[20~".to_string()),
"kf10" => Some("\\E[21~".to_string()),
"kf11" => Some("\\E[23~".to_string()),
"kf12" => Some("\\E[24~".to_string()),
"kf13" => Some("\\E[1;2P".to_string()),
"kf14" => Some("\\E[1;2Q".to_string()),
"kf15" => Some("\\E[1;2R".to_string()),
"kf16" => Some("\\E[1;2S".to_string()),
"kf17" => Some("\\E[15;2~".to_string()),
"kf18" => Some("\\E[17;2~".to_string()),
"kf19" => Some("\\E[18;2~".to_string()),
"kf20" => Some("\\E[19;2~".to_string()),
"kf21" => Some("\\E[20;2~".to_string()),
"kf22" => Some("\\E[21;2~".to_string()),
"kf23" => Some("\\E[23;2~".to_string()),
"kf24" => Some("\\E[24;2~".to_string()),
"kf25" => Some("\\E[1;5P".to_string()),
"kf26" => Some("\\E[1;5Q".to_string()),
"kf27" => Some("\\E[1;5R".to_string()),
"kf28" => Some("\\E[1;5S".to_string()),
"kf29" => Some("\\E[15;5~".to_string()),
"kf30" => Some("\\E[17;5~".to_string()),
"kf31" => Some("\\E[18;5~".to_string()),
"kf32" => Some("\\E[19;5~".to_string()),
"kf33" => Some("\\E[20;5~".to_string()),
"kf34" => Some("\\E[21;5~".to_string()),
"kf35" => Some("\\E[23;5~".to_string()),
"kf36" => Some("\\E[24;5~".to_string()),
"kf37" => Some("\\E[1;6P".to_string()),
"kf38" => Some("\\E[1;6Q".to_string()),
"kf39" => Some("\\E[1;6R".to_string()),
"kf40" => Some("\\E[1;6S".to_string()),
"kf41" => Some("\\E[15;6~".to_string()),
"kf42" => Some("\\E[17;6~".to_string()),
"kf43" => Some("\\E[18;6~".to_string()),
"kf44" => Some("\\E[19;6~".to_string()),
"kf45" => Some("\\E[20;6~".to_string()),
"kf46" => Some("\\E[21;6~".to_string()),
"kf47" => Some("\\E[23;6~".to_string()),
"kf48" => Some("\\E[24;6~".to_string()),
"kf49" => Some("\\E[1;3P".to_string()),
"kf50" => Some("\\E[1;3Q".to_string()),
"kf51" => Some("\\E[1;3R".to_string()),
"kf52" => Some("\\E[1;3S".to_string()),
"kf53" => Some("\\E[15;3~".to_string()),
"kf54" => Some("\\E[17;3~".to_string()),
"kf55" => Some("\\E[18;3~".to_string()),
"kf56" => Some("\\E[19;3~".to_string()),
"kf57" => Some("\\E[20;3~".to_string()),
"kf58" => Some("\\E[21;3~".to_string()),
"kf59" => Some("\\E[23;3~".to_string()),
"kf60" => Some("\\E[24;3~".to_string()),
"kf61" => Some("\\E[1;4P".to_string()),
"kf62" => Some("\\E[1;4Q".to_string()),
"kf63" => Some("\\E[1;4R".to_string()),
"kcuu1" | "ku" => Some("\\EOA".to_string()),
"kcud1" | "kd" => Some("\\EOB".to_string()),
"kcuf1" | "kr" => Some("\\EOC".to_string()),
"kcub1" | "kl" => Some("\\EOD".to_string()),
"khome" | "kh" => Some("\\EOH".to_string()),
"kend" => Some("\\EOF".to_string()),
"kbs" | "kb" => Some("\x7f".to_string()),
"kdch1" | "kD" => Some("\\E[3~".to_string()),
"kich1" | "kI" => Some("\\E[2~".to_string()),
"knp" | "kN" => Some("\\E[6~".to_string()),
"kpp" | "kP" => Some("\\E[5~".to_string()),
"kb2" => Some("\\EOE".to_string()),
"kcbt" => Some("\\E[Z".to_string()),
"kent" => Some("\\EOM".to_string()),
"kLFT" => Some("\\E[1;2D".to_string()),
"kRIT" => Some("\\E[1;2C".to_string()),
"kind" => Some("\\E[1;2B".to_string()),
"kri" => Some("\\E[1;2A".to_string()),
"kDN" => Some("\\E[1;2B".to_string()),
"kUP" => Some("\\E[1;2A".to_string()),
"kDN3" => Some("\\E[1;3B".to_string()),
"kLFT3" => Some("\\E[1;3D".to_string()),
"kRIT3" => Some("\\E[1;3C".to_string()),
"kUP3" => Some("\\E[1;3A".to_string()),
"kDN4" => Some("\\E[1;4B".to_string()),
"kLFT4" => Some("\\E[1;4D".to_string()),
"kRIT4" => Some("\\E[1;4C".to_string()),
"kUP4" => Some("\\E[1;4A".to_string()),
"kDN5" => Some("\\E[1;5B".to_string()),
"kLFT5" => Some("\\E[1;5D".to_string()),
"kRIT5" => Some("\\E[1;5C".to_string()),
"kUP5" => Some("\\E[1;5A".to_string()),
"kDN6" => Some("\\E[1;6B".to_string()),
"kLFT6" => Some("\\E[1;6D".to_string()),
"kRIT6" => Some("\\E[1;6C".to_string()),
"kUP6" => Some("\\E[1;6A".to_string()),
"kDN7" => Some("\\E[1;7B".to_string()),
"kLFT7" => Some("\\E[1;7D".to_string()),
"kRIT7" => Some("\\E[1;7C".to_string()),
"kUP7" => Some("\\E[1;7A".to_string()),
"kDC" => Some("\\E[3;2~".to_string()),
"kEND" => Some("\\E[1;2F".to_string()),
"kHOM" => Some("\\E[1;2H".to_string()),
"kIC" => Some("\\E[2;2~".to_string()),
"kNXT" => Some("\\E[6;2~".to_string()),
"kPRV" => Some("\\E[5;2~".to_string()),
"kDC3" => Some("\\E[3;3~".to_string()),
"kEND3" => Some("\\E[1;3F".to_string()),
"kHOM3" => Some("\\E[1;3H".to_string()),
"kIC3" => Some("\\E[2;3~".to_string()),
"kNXT3" => Some("\\E[6;3~".to_string()),
"kPRV3" => Some("\\E[5;3~".to_string()),
"kDC4" => Some("\\E[3;4~".to_string()),
"kEND4" => Some("\\E[1;4F".to_string()),
"kHOM4" => Some("\\E[1;4H".to_string()),
"kIC4" => Some("\\E[2;4~".to_string()),
"kNXT4" => Some("\\E[6;4~".to_string()),
"kPRV4" => Some("\\E[5;4~".to_string()),
"kDC5" => Some("\\E[3;5~".to_string()),
"kEND5" => Some("\\E[1;5F".to_string()),
"kHOM5" => Some("\\E[1;5H".to_string()),
"kIC5" => Some("\\E[2;5~".to_string()),
"kNXT5" => Some("\\E[6;5~".to_string()),
"kPRV5" => Some("\\E[5;5~".to_string()),
"kDC6" => Some("\\E[3;6~".to_string()),
"kEND6" => Some("\\E[1;6F".to_string()),
"kHOM6" => Some("\\E[1;6H".to_string()),
"kIC6" => Some("\\E[2;6~".to_string()),
"kNXT6" => Some("\\E[6;6~".to_string()),
"kPRV6" => Some("\\E[5;6~".to_string()),
"kDC7" => Some("\\E[3;7~".to_string()),
"kEND7" => Some("\\E[1;7F".to_string()),
"kHOM7" => Some("\\E[1;7H".to_string()),
"kIC7" => Some("\\E[2;7~".to_string()),
"kNXT7" => Some("\\E[6;7~".to_string()),
"kPRV7" => Some("\\E[5;7~".to_string()),
"kmous" => Some("\\E[M".to_string()),
"meml" => Some("\\El".to_string()),
"memu" => Some("\\Em".to_string()),
"mc0" => Some("\\E[i".to_string()),
"mc4" => Some("\\E[4i".to_string()),
"mc5" => Some("\\E[5i".to_string()),
"rs1" => Some("\\Ec\\E]104\\007".to_string()),
"rs2" => Some("\\E[!p\\E[?3;4l\\E[4l\\E>".to_string()),
"is2" => Some("\\E[!p\\E[?3;4l\\E[4l\\E>".to_string()),
"u6" => Some("\\E[%i%d;%dR".to_string()),
"u7" => Some("\\E[6n".to_string()),
"u8" => Some("\\E[?%[;0123456789]c".to_string()),
"u9" => Some("\\E[c".to_string()),
"rep" => Some("%p1%c\\E[%p2%{1}%-%db".to_string()),
"Smulx" => Some("\\E[4\\:%p1%dm".to_string()),
"Sync" => Some("\\EP=%p1%ds\\E\\\\".to_string()),
"kxIN" => Some("\\E[I".to_string()),
"kxOUT" => Some("\\E[O".to_string()),
"BE" => Some("\\E[?2004h".to_string()),
"BD" => Some("\\E[?2004l".to_string()),
"PS" => Some("\\E[200~".to_string()),
"PE" => Some("\\E[201~".to_string()),
"Ms" => Some("\\E]52;%p1%s;%p2%s\\007".to_string()),
"cr" => Some("\\r".to_string()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_encoding() {
assert_eq!(encode_hex_string("TN"), "544E");
assert_eq!(encode_hex_string("Co"), "436F");
assert_eq!(encode_hex_string("RGB"), "524742");
}
#[test]
fn test_hex_decoding() {
assert_eq!(decode_hex_string(b"544E").unwrap(), "TN");
assert_eq!(decode_hex_string(b"436F").unwrap(), "Co");
assert_eq!(decode_hex_string(b"524742").unwrap(), "RGB");
}
#[test]
fn test_xtgettcap_processing() {
let response = process_xtgettcap_request(b"544E");
assert!(response.starts_with("\x1bP1+r"));
assert!(response.contains("544E="));
assert!(response.ends_with("\x1b\\"));
let response = process_xtgettcap_request(b"436F");
assert!(response.starts_with("\x1bP1+r"));
assert!(response.contains("436F="));
let response = process_xtgettcap_request(b"5858"); assert_eq!(response, "\x1bP0+r5858\x1b\\");
let response = process_xtgettcap_request(b"ZZ");
assert_eq!(response, "\x1bP0+r\x1b\\");
}
#[test]
fn test_single_capability_requests() {
let response = process_xtgettcap_request(b"544E"); assert_eq!(response, "\x1bP1+r544E=72696F\x1b\\");
let response = process_xtgettcap_request(b"436F"); assert_eq!(response, "\x1bP1+r436F=323536\x1b\\");
let response = process_xtgettcap_request(b"524742"); assert_eq!(response, "\x1bP1+r524742=382F382F38\x1b\\");
let response = process_xtgettcap_request(b"5858"); assert_eq!(response, "\x1bP0+r5858\x1b\\");
}
#[test]
fn test_xtgettcap_multiple_queries() {
let response = process_xtgettcap_request(b"4245;4244;5053;5045");
assert!(response.contains("\x1bP1+r4245="));
assert!(response.contains("\x1bP1+r4244="));
assert!(response.contains("\x1bP1+r5053="));
assert!(response.contains("\x1bP1+r5045="));
let count = response.matches("\x1bP1+r").count();
assert_eq!(
count, 4,
"Should have 4 separate DCS responses, got {count}"
);
}
#[test]
fn test_xtgettcap_boolean_capability() {
let response = process_xtgettcap_request(b"616D");
assert_eq!(response, "\x1bP1+r616D\x1b\\");
}
#[test]
fn test_xtgettcap_value_decoding() {
let response = process_xtgettcap_request(b"5053"); assert_eq!(response, "\x1bP1+r5053=1B5B3230307E\x1b\\");
let response = process_xtgettcap_request(b"5045"); assert_eq!(response, "\x1bP1+r5045=1B5B3230317E\x1b\\");
}
#[test]
fn test_decode_terminfo_value() {
assert_eq!(
decode_terminfo_value("\\E[200~"),
vec![0x1b, b'[', b'2', b'0', b'0', b'~']
);
let param = "\\E[%p1%dA";
assert_eq!(decode_terminfo_value(param), param.as_bytes().to_vec());
assert_eq!(decode_terminfo_value("^H"), vec![8]);
assert_eq!(decode_terminfo_value("\\n"), vec![b'\n']);
assert_eq!(decode_terminfo_value("\\r"), vec![b'\r']);
}
#[test]
fn test_capability_lookup() {
assert_eq!(get_termcap_capability("TN"), Some("rio".to_string()));
assert_eq!(get_termcap_capability("Co"), Some("256".to_string()));
assert_eq!(get_termcap_capability("RGB"), Some("8/8/8".to_string()));
assert_eq!(get_termcap_capability("invalid"), None);
}
#[test]
fn test_extended_capabilities() {
assert_eq!(get_termcap_capability("kf13"), Some("\\E[1;2P".to_string()));
assert_eq!(get_termcap_capability("kf25"), Some("\\E[1;5P".to_string()));
assert_eq!(get_termcap_capability("kLFT"), Some("\\E[1;2D".to_string()));
assert_eq!(get_termcap_capability("kUP3"), Some("\\E[1;3A".to_string()));
assert_eq!(get_termcap_capability("kDC5"), Some("\\E[3;5~".to_string()));
assert_eq!(
get_termcap_capability("kHOM7"),
Some("\\E[1;7H".to_string())
);
assert_eq!(
get_termcap_capability("rs1"),
Some("\\Ec\\E]104\\007".to_string())
);
assert_eq!(get_termcap_capability("sixel"), Some("".to_string()));
assert_eq!(get_termcap_capability("iterm2"), Some("".to_string()));
}
#[test]
fn test_apc_state_default() {
let state = ApcState::default();
assert_eq!(state.buffer.len(), 0);
assert_eq!(state.buffer.capacity(), 0);
}
#[test]
fn test_apc_state_buffer_operations() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
assert!(state.buffer.capacity() >= 4096);
let data = b"Ga=T,f=32,s=1,v=1;AQIDBA==";
state.buffer.extend_from_slice(data);
assert_eq!(state.buffer.len(), data.len());
assert_eq!(&state.buffer[..], data);
}
#[test]
fn test_apc_state_small_sequence() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
let data = b"Ga=T,f=32,s=1,v=1;AQIDBA==";
for &byte in data {
state.buffer.push(byte);
}
assert_eq!(state.buffer.len(), data.len());
assert_eq!(&state.buffer[..], data);
}
#[test]
fn test_apc_state_large_sequence() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
let header = b"Ga=T,f=32,s=100,v=100,m=1;";
let payload = vec![b'A'; 4096];
for &byte in header {
state.buffer.push(byte);
}
for &byte in &payload {
state.buffer.push(byte);
}
let expected_len = header.len() + payload.len();
assert_eq!(state.buffer.len(), expected_len);
assert!(
state.buffer.len() > 1024,
"Should handle data larger than Copa's default OSC buffer (1024 bytes)"
);
}
#[test]
fn test_apc_state_very_large_sequence() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
let header = b"Ga=T,f=32,s=200,v=200,m=1;";
for &byte in header {
state.buffer.push(byte);
}
let large_payload = vec![b'B'; 16384];
for &byte in &large_payload {
state.buffer.push(byte);
}
assert_eq!(state.buffer.len(), header.len() + large_payload.len());
assert!(
state.buffer.len() > 16000,
"Should handle very large payloads"
);
}
#[test]
fn test_apc_state_multiple_sequences() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
let data1 = b"Ga=T,f=32,s=10,v=10;AQIDBA==";
state.buffer.extend_from_slice(data1);
assert_eq!(state.buffer.len(), data1.len());
state.buffer.clear();
assert_eq!(
state.buffer.len(),
0,
"Buffer should be cleared between sequences"
);
let data2 = b"Ga=T,f=32,s=20,v=20;BQYHCAk=";
state.buffer.extend_from_slice(data2);
assert_eq!(state.buffer.len(), data2.len());
}
#[test]
fn test_apc_state_chunked_transmission() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
let chunk1 = b"Ga=T,f=32,s=100,v=100,m=1;AQIDBAUG";
state.buffer.extend_from_slice(chunk1);
assert!(!state.buffer.is_empty());
state.buffer.clear();
let chunk2 = b"Gm=1;BwgJCgsM";
state.buffer.extend_from_slice(chunk2);
assert_eq!(&state.buffer[..], chunk2);
state.buffer.clear();
let chunk3 = b"Gm=0;DQ4PEBES";
state.buffer.extend_from_slice(chunk3);
assert_eq!(&state.buffer[..], chunk3);
}
#[test]
fn test_apc_state_preallocation() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
assert!(
state.buffer.capacity() >= 4096,
"Buffer should pre-allocate at least 4KB for Kitty graphics"
);
}
#[test]
fn test_apc_buffer_parsing_small_kitty() {
let buffer = b"Ga=T,f=32,s=10,v=10;AQIDBA==";
assert_eq!(buffer.first(), Some(&b'G'));
let mut parts = buffer.splitn(2, |&b| b == b';');
let control_data = parts.next().unwrap();
let payload_data = parts.next().unwrap();
let control = &control_data[1..];
assert_eq!(control, b"a=T,f=32,s=10,v=10");
assert_eq!(payload_data, b"AQIDBA==");
}
#[test]
fn test_apc_buffer_parsing_large_kitty() {
let mut buffer = Vec::new();
buffer.extend_from_slice(b"Ga=T,f=32,s=100,v=100,m=1;");
buffer.extend_from_slice(&vec![b'A'; 4096]);
assert_eq!(buffer.first(), Some(&b'G'));
let mut parts = buffer.splitn(2, |&b| b == b';');
let control_data = parts.next().unwrap();
let payload_data = parts.next().unwrap();
let control = &control_data[1..];
assert_eq!(control, b"a=T,f=32,s=100,v=100,m=1");
assert_eq!(payload_data.len(), 4096);
assert!(
payload_data.len() > 1024,
"Payload should be larger than Copa's default OSC buffer"
);
}
#[test]
fn test_apc_buffer_parsing_no_semicolon() {
let buffer = b"Ga=T,f=32,s=10,v=10";
let mut parts = buffer.splitn(2, |&b| b == b';');
let control_data = parts.next().unwrap();
let payload_data = parts.next().unwrap_or(b"");
assert_eq!(control_data, buffer);
assert_eq!(payload_data, b"");
}
#[test]
fn test_apc_buffer_parsing_empty_payload() {
let buffer = b"Ga=T,f=32,s=10,v=10;";
let mut parts = buffer.splitn(2, |&b| b == b';');
let _control_data = parts.next().unwrap();
let payload_data = parts.next().unwrap();
assert_eq!(payload_data, b"");
}
#[test]
fn test_apc_buffer_non_kitty() {
let buffer = b"some_other_apc;data";
assert_ne!(buffer.first(), Some(&b'G'));
}
#[test]
fn test_kitty_chunked_transmission_terminal_doom() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
state
.buffer
.extend_from_slice(b"Ga=T,f=24,s=100,v=100,i=1,m=1;");
state.buffer.extend(vec![b'A'; 4096]);
assert!(state.buffer.len() > 4096);
assert!(state.buffer.starts_with(b"Ga=T,f=24,s=100,v=100,i=1,m=1;"));
state.buffer.clear();
state.buffer.extend_from_slice(b"Gm=1;");
state.buffer.extend(vec![b'B'; 4096]);
assert_eq!(&state.buffer[..5], b"Gm=1;");
state.buffer.clear();
state.buffer.extend_from_slice(b"Gm=0;");
state.buffer.extend(vec![b'C'; 2048]);
assert_eq!(&state.buffer[..5], b"Gm=0;");
}
#[test]
fn test_kitty_large_payload_accumulation() {
let mut total_accumulated = 0;
let chunk_size = 4096;
let total_payload_size = 1_024_000;
for chunk_num in 0..(total_payload_size / chunk_size) {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(4096);
if chunk_num == 0 {
state
.buffer
.extend_from_slice(b"Ga=T,f=24,s=640,v=400,i=10,m=1;");
} else if chunk_num == (total_payload_size / chunk_size) - 1 {
state.buffer.extend_from_slice(b"Gm=0;");
} else {
state.buffer.extend_from_slice(b"Gm=1;");
}
state.buffer.extend(vec![b'X'; chunk_size]);
total_accumulated += chunk_size;
}
assert_eq!(total_accumulated, total_payload_size);
}
#[test]
fn test_kitty_parsing_with_escape_terminator() {
let buffer = b"Ga=T,f=24,s=10,v=10;AQIDBA==\x1b";
let data = if buffer.last() == Some(&0x1b) {
&buffer[..buffer.len() - 1]
} else {
buffer.as_slice()
};
let mut parts = data.splitn(2, |&b| b == b';');
let control_data = parts.next().unwrap();
let payload_data = parts.next().unwrap();
let control = &control_data[1..]; assert_eq!(control, b"a=T,f=24,s=10,v=10");
assert_eq!(payload_data, b"AQIDBA==");
assert_ne!(
payload_data.last(),
Some(&0x1b),
"Terminator should be stripped"
);
}
#[test]
fn test_kitty_multiple_images_sequential() {
let images = vec![
(1, b"Ga=T,f=24,s=100,v=100,i=1;AQIDBA==".as_slice()),
(2, b"Ga=T,f=24,s=100,v=100,i=2;BQYHCAk=".as_slice()),
(3, b"Ga=T,f=24,s=100,v=100,i=3;CgsMDQ4=".as_slice()),
];
for (image_id, data) in images {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.extend_from_slice(data);
assert!(state.buffer.starts_with(b"Ga=T,f=24,s=100,v=100"));
let control_str = std::str::from_utf8(&state.buffer[..30]).unwrap();
assert!(control_str.contains(&format!("i={}", image_id)));
}
}
#[test]
fn test_kitty_chunked_with_different_image_ids() {
let mut state = ApcState::default();
state
.buffer
.extend_from_slice(b"Ga=T,f=24,s=100,v=100,i=5,m=1;AAAA");
let buffer_str = std::str::from_utf8(&state.buffer).unwrap();
assert!(buffer_str.contains("i=5"));
state.buffer.clear();
state.buffer.extend_from_slice(b"Gm=1;BBBB");
let buffer_str = std::str::from_utf8(&state.buffer).unwrap();
assert!(!buffer_str.contains("i="));
state.buffer.clear();
state.buffer.extend_from_slice(b"Gm=0;CCCC");
let buffer_str = std::str::from_utf8(&state.buffer).unwrap();
assert!(!buffer_str.contains("i="));
state.buffer.clear();
state
.buffer
.extend_from_slice(b"Ga=T,f=24,s=100,v=100,i=6,m=1;DDDD");
let buffer_str = std::str::from_utf8(&state.buffer).unwrap();
assert!(buffer_str.contains("i=6"));
}
#[test]
fn test_kitty_placement_after_transmission() {
let mut state = ApcState::default();
state
.buffer
.extend_from_slice(b"Ga=t,f=24,s=100,v=100,i=7;/9j/4AAQ");
let mut parts = state.buffer.splitn(2, |&b| b == b';');
let control = std::str::from_utf8(parts.next().unwrap()).unwrap();
assert!(control.contains("a=t"), "Should be transmit-only action");
assert!(control.contains("i=7"), "Should have image ID");
state.buffer.clear();
state.buffer.extend_from_slice(b"Ga=p,i=7,c=10,r=5;");
let mut parts = state.buffer.splitn(2, |&b| b == b';');
let control = std::str::from_utf8(parts.next().unwrap()).unwrap();
assert!(control.contains("a=p"), "Should be placement action");
assert!(control.contains("i=7"), "Should reference same image ID");
}
#[test]
fn test_kitty_delete_command() {
let mut state = ApcState::default();
state.buffer.extend_from_slice(b"Ga=d;");
let mut parts = state.buffer.splitn(2, |&b| b == b';');
let control = std::str::from_utf8(parts.next().unwrap()).unwrap();
assert!(control.contains("a=d"), "Should be delete action");
let payload = parts.next().unwrap();
assert_eq!(payload, b"", "Delete commands have empty payload");
}
#[test]
fn test_kitty_very_large_single_chunk() {
let mut state = ApcState::default();
state.buffer.clear();
state.buffer.reserve(16384);
state
.buffer
.extend_from_slice(b"Ga=T,f=24,s=200,v=200,i=8;");
state.buffer.extend(vec![b'Z'; 16384]);
assert!(state.buffer.len() > 16384);
assert!(
state.buffer.len() > 1024,
"Should exceed Copa's old 1024-byte limit"
);
}
#[test]
fn test_kitty_buffer_reuse_between_transmissions() {
let mut state = ApcState::default();
state
.buffer
.extend_from_slice(b"Ga=T,f=24,s=100,v=100,i=9;FIRST");
let buffer_str = std::str::from_utf8(&state.buffer).unwrap();
assert!(buffer_str.contains("FIRST"));
let first_len = state.buffer.len();
state.buffer.clear();
assert_eq!(state.buffer.len(), 0);
state
.buffer
.extend_from_slice(b"Ga=T,f=24,s=100,v=100,i=10;SECOND");
let buffer_str = std::str::from_utf8(&state.buffer).unwrap();
assert!(buffer_str.contains("SECOND"));
assert!(!buffer_str.contains("FIRST"), "Old data should be cleared");
assert!(state.buffer.capacity() >= first_len);
}
}