Fix/kitty keyboard (#21)
* Add dynamic Kitty protocol tracking, unit tests, and version badge with CI check
This commit is contained in:
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -12,6 +12,21 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
version-badge:
|
||||||
|
name: Version Badge
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Check version badge matches Cargo.toml
|
||||||
|
run: |
|
||||||
|
CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||||
|
BADGE_VERSION=$(grep -oP 'badge/version-\K[0-9]+\.[0-9]+\.[0-9]+' README.md | head -1)
|
||||||
|
if [ "$CARGO_VERSION" != "$BADGE_VERSION" ]; then
|
||||||
|
echo "Version mismatch: Cargo.toml=$CARGO_VERSION README badge=$BADGE_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Version badge matches: $CARGO_VERSION"
|
||||||
|
|
||||||
check:
|
check:
|
||||||
name: Check
|
name: Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["David Beesley"]
|
authors = ["David Beesley"]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
[](https://github.com/davidbeesley/claude-chill/actions/workflows/ci.yml)
|
[](https://github.com/davidbeesley/claude-chill/actions/workflows/ci.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -116,6 +117,12 @@ auto_lookback_timeout_ms = 15000 # Auto-lookback after 15s idle (0 to disable)
|
|||||||
|
|
||||||
Note: History is cleared on full screen redraws, so lookback shows output since Claude's last full render.
|
Note: History is cleared on full screen redraws, so lookback shows output since Claude's last full render.
|
||||||
|
|
||||||
|
### Kitty Keyboard Protocol
|
||||||
|
|
||||||
|
Modern terminals like Kitty, Ghostty, and WezTerm support the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) which encodes keys differently than legacy terminals.
|
||||||
|
|
||||||
|
claude-chill automatically tracks Kitty protocol state by monitoring the escape sequences passing through the proxy. When Claude Code enables Kitty mode, claude-chill switches to expecting Kitty-encoded key sequences for the lookback key. When Claude Code disables it, claude-chill switches back to legacy mode. This happens transparently with no configuration needed.
|
||||||
|
|
||||||
### Key Format
|
### Key Format
|
||||||
|
|
||||||
`[modifier][key]` - Examples: `[f12]`, `[ctrl][g]`, `[ctrl][shift][j]`
|
`[modifier][key]` - Examples: `[f12]`, `[ctrl][g]`, `[ctrl][shift][j]`
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use clap::Parser;
|
|||||||
use claude_chill::config::Config;
|
use claude_chill::config::Config;
|
||||||
use claude_chill::key_parser;
|
use claude_chill::key_parser;
|
||||||
use claude_chill::proxy::{Proxy, ProxyConfig};
|
use claude_chill::proxy::{Proxy, ProxyConfig};
|
||||||
|
use log::debug;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -33,15 +34,25 @@ fn main() -> ExitCode {
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| config.lookback_key.clone());
|
.unwrap_or_else(|| config.lookback_key.clone());
|
||||||
|
|
||||||
let lookback_sequence = match key_parser::parse(&lookback_key) {
|
let (lookback_sequence_legacy, lookback_sequence_kitty) = match key_parser::parse(&lookback_key)
|
||||||
Ok(key) => key.to_escape_sequence(),
|
{
|
||||||
|
Ok(key) => {
|
||||||
|
let legacy = key.to_escape_sequence();
|
||||||
|
let kitty = key.to_kitty_sequence().unwrap_or_else(|| legacy.clone());
|
||||||
|
(legacy, kitty)
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Invalid lookback key '{}': {}", lookback_key, e);
|
eprintln!("Invalid lookback key '{}': {}", lookback_key, e);
|
||||||
eprintln!("Using default: [ctrl][6]");
|
eprintln!("Using default: [ctrl][6]");
|
||||||
config.lookback_sequence()
|
(vec![0x1E], b"\x1b[54;5u".to_vec())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Lookback sequences: legacy={:?} kitty={:?}",
|
||||||
|
lookback_sequence_legacy, lookback_sequence_kitty
|
||||||
|
);
|
||||||
|
|
||||||
let auto_lookback_timeout_ms = cli
|
let auto_lookback_timeout_ms = cli
|
||||||
.auto_lookback_timeout
|
.auto_lookback_timeout
|
||||||
.unwrap_or(config.auto_lookback_timeout_ms);
|
.unwrap_or(config.auto_lookback_timeout_ms);
|
||||||
@@ -49,7 +60,8 @@ fn main() -> ExitCode {
|
|||||||
let proxy_config = ProxyConfig {
|
let proxy_config = ProxyConfig {
|
||||||
max_history_lines: history_lines,
|
max_history_lines: history_lines,
|
||||||
lookback_key,
|
lookback_key,
|
||||||
lookback_sequence,
|
lookback_sequence_legacy,
|
||||||
|
lookback_sequence_kitty,
|
||||||
auto_lookback_timeout_ms,
|
auto_lookback_timeout_ms,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,29 @@ impl KeyCombination {
|
|||||||
pub fn to_escape_sequence(&self) -> Vec<u8> {
|
pub fn to_escape_sequence(&self) -> Vec<u8> {
|
||||||
key_to_escape_sequence(&self.code, &self.modifiers)
|
key_to_escape_sequence(&self.code, &self.modifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_kitty_sequence(&self) -> Option<Vec<u8>> {
|
||||||
|
let codepoint = match &self.code {
|
||||||
|
KeyCode::Char(c) => *c as u32,
|
||||||
|
KeyCode::Esc => 27,
|
||||||
|
KeyCode::Enter => 13,
|
||||||
|
KeyCode::Tab => 9,
|
||||||
|
KeyCode::Backspace => 127,
|
||||||
|
KeyCode::Space => 32,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let modifier = 1
|
||||||
|
+ if self.modifiers.shift { 1 } else { 0 }
|
||||||
|
+ if self.modifiers.alt { 2 } else { 0 }
|
||||||
|
+ if self.modifiers.ctrl { 4 } else { 0 };
|
||||||
|
|
||||||
|
if modifier == 1 {
|
||||||
|
Some(format!("\x1b[{}u", codepoint).into_bytes())
|
||||||
|
} else {
|
||||||
|
Some(format!("\x1b[{};{}u", codepoint, modifier).into_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for KeyCombination {
|
impl fmt::Display for KeyCombination {
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ extern "C" fn handle_sigterm(_: libc::c_int) {
|
|||||||
pub struct ProxyConfig {
|
pub struct ProxyConfig {
|
||||||
pub max_history_lines: usize,
|
pub max_history_lines: usize,
|
||||||
pub lookback_key: String,
|
pub lookback_key: String,
|
||||||
pub lookback_sequence: Vec<u8>,
|
pub lookback_sequence_legacy: Vec<u8>,
|
||||||
|
pub lookback_sequence_kitty: Vec<u8>,
|
||||||
pub auto_lookback_timeout_ms: u64,
|
pub auto_lookback_timeout_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ impl Default for ProxyConfig {
|
|||||||
Self {
|
Self {
|
||||||
max_history_lines: 100_000,
|
max_history_lines: 100_000,
|
||||||
lookback_key: "[ctrl][6]".to_string(),
|
lookback_key: "[ctrl][6]".to_string(),
|
||||||
lookback_sequence: vec![0x1E],
|
lookback_sequence_legacy: vec![0x1E],
|
||||||
|
lookback_sequence_kitty: b"\x1b[54;5u".to_vec(),
|
||||||
auto_lookback_timeout_ms: 15000,
|
auto_lookback_timeout_ms: 15000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,6 +109,10 @@ pub struct Proxy {
|
|||||||
in_sync_block: bool,
|
in_sync_block: bool,
|
||||||
in_lookback_mode: bool,
|
in_lookback_mode: bool,
|
||||||
in_alternate_screen: bool,
|
in_alternate_screen: bool,
|
||||||
|
kitty_mode_supported: bool,
|
||||||
|
kitty_mode_enabled: bool,
|
||||||
|
kitty_input_buffer: Vec<u8>,
|
||||||
|
kitty_output_buffer: Vec<u8>,
|
||||||
vt_render_pending: bool,
|
vt_render_pending: bool,
|
||||||
lookback_cache: Vec<u8>,
|
lookback_cache: Vec<u8>,
|
||||||
lookback_input_buffer: Vec<u8>,
|
lookback_input_buffer: Vec<u8>,
|
||||||
@@ -121,6 +127,72 @@ pub struct Proxy {
|
|||||||
alt_screen_exit_legacy_finder: memmem::Finder<'static>,
|
alt_screen_exit_legacy_finder: memmem::Finder<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_KITTY_SEQ_LEN: usize = 16;
|
||||||
|
|
||||||
|
fn is_complete_kitty_output_seq(data: &[u8]) -> bool {
|
||||||
|
if data.len() < 4 || data[0] != 0x1b || data[1] != b'[' {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if data[2] == b'<' && data[3] == b'u' {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if data[2] == b'>' {
|
||||||
|
let mut j = 3;
|
||||||
|
while j < data.len() && data[j].is_ascii_digit() {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if j > 3 && j < data.len() && data[j] == b'u' {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_complete_kitty_input_seq(data: &[u8]) -> bool {
|
||||||
|
if data.len() < 5 || data[0] != 0x1b || data[1] != b'[' || data[2] != b'?' {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut j = 3;
|
||||||
|
while j < data.len() && data[j].is_ascii_digit() {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
j > 3 && j < data.len() && data[j] == b'u'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kitty_output_split_point(buffer: &[u8]) -> usize {
|
||||||
|
let last_esc = buffer.iter().rposition(|&b| b == 0x1b);
|
||||||
|
|
||||||
|
match last_esc {
|
||||||
|
None => buffer.len(),
|
||||||
|
Some(pos) if pos + MAX_KITTY_SEQ_LEN <= buffer.len() => buffer.len(),
|
||||||
|
Some(pos) => {
|
||||||
|
let tail = &buffer[pos..];
|
||||||
|
if is_complete_kitty_output_seq(tail) {
|
||||||
|
buffer.len()
|
||||||
|
} else {
|
||||||
|
pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kitty_input_split_point(buffer: &[u8]) -> usize {
|
||||||
|
let last_esc = buffer.iter().rposition(|&b| b == 0x1b);
|
||||||
|
|
||||||
|
match last_esc {
|
||||||
|
None => buffer.len(),
|
||||||
|
Some(pos) if pos + MAX_KITTY_SEQ_LEN <= buffer.len() => buffer.len(),
|
||||||
|
Some(pos) => {
|
||||||
|
let tail = &buffer[pos..];
|
||||||
|
if is_complete_kitty_input_seq(tail) {
|
||||||
|
buffer.len()
|
||||||
|
} else {
|
||||||
|
pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Proxy {
|
impl Proxy {
|
||||||
pub fn spawn(command: &str, args: &[&str], config: ProxyConfig) -> Result<Self> {
|
pub fn spawn(command: &str, args: &[&str], config: ProxyConfig) -> Result<Self> {
|
||||||
let winsize = get_terminal_size()?;
|
let winsize = get_terminal_size()?;
|
||||||
@@ -191,6 +263,10 @@ impl Proxy {
|
|||||||
in_sync_block: false,
|
in_sync_block: false,
|
||||||
in_lookback_mode: false,
|
in_lookback_mode: false,
|
||||||
in_alternate_screen: false,
|
in_alternate_screen: false,
|
||||||
|
kitty_mode_supported: false,
|
||||||
|
kitty_mode_enabled: false,
|
||||||
|
kitty_input_buffer: Vec::with_capacity(16),
|
||||||
|
kitty_output_buffer: Vec::with_capacity(16),
|
||||||
vt_render_pending: false,
|
vt_render_pending: false,
|
||||||
lookback_cache: Vec::new(),
|
lookback_cache: Vec::new(),
|
||||||
lookback_input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY),
|
lookback_input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY),
|
||||||
@@ -302,6 +378,9 @@ impl Proxy {
|
|||||||
feed_vt
|
feed_vt
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track Kitty keyboard protocol state from output
|
||||||
|
self.update_kitty_mode_from_output(data);
|
||||||
|
|
||||||
if self.in_alternate_screen {
|
if self.in_alternate_screen {
|
||||||
// Still feed VT and history while in alt screen so they stay in sync
|
// Still feed VT and history while in alt screen so they stay in sync
|
||||||
if feed_vt {
|
if feed_vt {
|
||||||
@@ -466,6 +545,93 @@ impl Proxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_kitty_mode_from_output(&mut self, data: &[u8]) {
|
||||||
|
self.kitty_output_buffer.extend_from_slice(data);
|
||||||
|
|
||||||
|
let split_point = kitty_output_split_point(&self.kitty_output_buffer);
|
||||||
|
|
||||||
|
if split_point > 0 {
|
||||||
|
let to_parse: Vec<u8> = self.kitty_output_buffer.drain(..split_point).collect();
|
||||||
|
self.parse_kitty_output(&to_parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety cap on carryover buffer
|
||||||
|
if self.kitty_output_buffer.len() > 64 {
|
||||||
|
let overflow: Vec<u8> = self.kitty_output_buffer.drain(..).collect();
|
||||||
|
self.parse_kitty_output(&overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_kitty_output(&mut self, data: &[u8]) {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < data.len() {
|
||||||
|
if data[i] == 0x1b && i + 3 < data.len() && data[i + 1] == b'[' {
|
||||||
|
if data[i + 2] == b'<' && data[i + 3] == b'u' {
|
||||||
|
debug!("Kitty keyboard protocol disabled");
|
||||||
|
self.kitty_mode_enabled = false;
|
||||||
|
i += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if data[i + 2] == b'>' && self.kitty_mode_supported {
|
||||||
|
let mut j = i + 3;
|
||||||
|
while j < data.len() && data[j].is_ascii_digit() {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if j > i + 3 && j < data.len() && data[j] == b'u' {
|
||||||
|
debug!("Kitty keyboard protocol enabled");
|
||||||
|
self.kitty_mode_enabled = true;
|
||||||
|
i = j + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_kitty_support_from_input(&mut self, data: &[u8]) {
|
||||||
|
if self.kitty_mode_supported {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.kitty_input_buffer.extend_from_slice(data);
|
||||||
|
|
||||||
|
let split_point = kitty_input_split_point(&self.kitty_input_buffer);
|
||||||
|
|
||||||
|
if split_point > 0 {
|
||||||
|
let to_parse: Vec<u8> = self.kitty_input_buffer.drain(..split_point).collect();
|
||||||
|
self.parse_kitty_input(&to_parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety cap on carryover buffer
|
||||||
|
if self.kitty_input_buffer.len() > 64 {
|
||||||
|
let overflow: Vec<u8> = self.kitty_input_buffer.drain(..).collect();
|
||||||
|
self.parse_kitty_input(&overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_kitty_input(&mut self, data: &[u8]) {
|
||||||
|
if self.kitty_mode_supported {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
while i < data.len() {
|
||||||
|
if data[i] == 0x1b && i + 4 < data.len() && data[i + 1] == b'[' && data[i + 2] == b'?' {
|
||||||
|
let mut j = i + 3;
|
||||||
|
while j < data.len() && data[j].is_ascii_digit() {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if j > i + 3 && j < data.len() && data[j] == b'u' {
|
||||||
|
debug!("Kitty keyboard protocol supported (saw query response)");
|
||||||
|
self.kitty_mode_supported = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn flush_sync_block_to_history(&mut self) {
|
fn flush_sync_block_to_history(&mut self) {
|
||||||
let has_clear_screen = self.clear_screen_finder.find(&self.sync_buffer).is_some();
|
let has_clear_screen = self.clear_screen_finder.find(&self.sync_buffer).is_some();
|
||||||
let has_cursor_home = self.cursor_home_finder.find(&self.sync_buffer).is_some();
|
let has_cursor_home = self.cursor_home_finder.find(&self.sync_buffer).is_some();
|
||||||
@@ -641,10 +807,18 @@ impl Proxy {
|
|||||||
fn process_input<F: AsFd>(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> {
|
fn process_input<F: AsFd>(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> {
|
||||||
self.last_stdin_time = Some(Instant::now());
|
self.last_stdin_time = Some(Instant::now());
|
||||||
|
|
||||||
|
self.update_kitty_support_from_input(data);
|
||||||
|
|
||||||
if self.in_alternate_screen {
|
if self.in_alternate_screen {
|
||||||
return write_all(&self.pty_master, data);
|
return write_all(&self.pty_master, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lookback_sequence = if self.kitty_mode_enabled {
|
||||||
|
self.config.lookback_sequence_kitty.clone()
|
||||||
|
} else {
|
||||||
|
self.config.lookback_sequence_legacy.clone()
|
||||||
|
};
|
||||||
|
|
||||||
for &byte in data {
|
for &byte in data {
|
||||||
if self.in_lookback_mode && byte == 0x03 {
|
if self.in_lookback_mode && byte == 0x03 {
|
||||||
self.lookback_input_buffer.clear();
|
self.lookback_input_buffer.clear();
|
||||||
@@ -655,13 +829,13 @@ impl Proxy {
|
|||||||
let lookback_action = self.check_sequence_match(
|
let lookback_action = self.check_sequence_match(
|
||||||
byte,
|
byte,
|
||||||
&mut self.lookback_input_buffer.clone(),
|
&mut self.lookback_input_buffer.clone(),
|
||||||
&self.config.lookback_sequence.clone(),
|
&lookback_sequence,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.lookback_input_buffer.push(byte);
|
self.lookback_input_buffer.push(byte);
|
||||||
|
|
||||||
if self.lookback_input_buffer.len() > self.config.lookback_sequence.len() {
|
if self.lookback_input_buffer.len() > lookback_sequence.len() {
|
||||||
let excess = self.lookback_input_buffer.len() - self.config.lookback_sequence.len();
|
let excess = self.lookback_input_buffer.len() - lookback_sequence.len();
|
||||||
self.lookback_input_buffer.drain(..excess);
|
self.lookback_input_buffer.drain(..excess);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,11 +851,7 @@ impl Proxy {
|
|||||||
}
|
}
|
||||||
SequenceMatch::Partial => {}
|
SequenceMatch::Partial => {}
|
||||||
SequenceMatch::None => {
|
SequenceMatch::None => {
|
||||||
if !self
|
if !lookback_sequence.starts_with(&self.lookback_input_buffer) {
|
||||||
.config
|
|
||||||
.lookback_sequence
|
|
||||||
.starts_with(&self.lookback_input_buffer)
|
|
||||||
{
|
|
||||||
self.lookback_input_buffer.clear();
|
self.lookback_input_buffer.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -901,3 +1071,217 @@ fn write_all<F: AsFd>(fd: &F, data: &[u8]) -> Result<()> {
|
|||||||
fn nix_read<F: AsFd>(fd: &F, buf: &mut [u8]) -> Result<usize, Errno> {
|
fn nix_read<F: AsFd>(fd: &F, buf: &mut [u8]) -> Result<usize, Errno> {
|
||||||
read(fd.as_fd(), buf)
|
read(fd.as_fd(), buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Tests for is_complete_kitty_output_seq
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_disable_complete() {
|
||||||
|
assert!(is_complete_kitty_output_seq(b"\x1b[<u"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_disable_with_trailing() {
|
||||||
|
assert!(is_complete_kitty_output_seq(b"\x1b[<uhello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_enable_complete() {
|
||||||
|
assert!(is_complete_kitty_output_seq(b"\x1b[>1u"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_enable_multi_digit() {
|
||||||
|
assert!(is_complete_kitty_output_seq(b"\x1b[>31u"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_enable_with_trailing() {
|
||||||
|
assert!(is_complete_kitty_output_seq(b"\x1b[>1umore"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_incomplete_esc_only() {
|
||||||
|
assert!(!is_complete_kitty_output_seq(b"\x1b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_incomplete_esc_bracket() {
|
||||||
|
assert!(!is_complete_kitty_output_seq(b"\x1b["));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_incomplete_disable() {
|
||||||
|
assert!(!is_complete_kitty_output_seq(b"\x1b[<"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_incomplete_enable_no_digit() {
|
||||||
|
assert!(!is_complete_kitty_output_seq(b"\x1b[>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_incomplete_enable_digit_no_u() {
|
||||||
|
assert!(!is_complete_kitty_output_seq(b"\x1b[>1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_not_kitty() {
|
||||||
|
assert!(!is_complete_kitty_output_seq(b"\x1b[H"));
|
||||||
|
assert!(!is_complete_kitty_output_seq(b"\x1b[2J"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_empty() {
|
||||||
|
assert!(!is_complete_kitty_output_seq(b""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_seq_no_esc() {
|
||||||
|
assert!(!is_complete_kitty_output_seq(b"hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for is_complete_kitty_input_seq
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_query_response_complete() {
|
||||||
|
assert!(is_complete_kitty_input_seq(b"\x1b[?1u"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_query_response_multi_digit() {
|
||||||
|
assert!(is_complete_kitty_input_seq(b"\x1b[?31u"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_query_response_with_trailing() {
|
||||||
|
assert!(is_complete_kitty_input_seq(b"\x1b[?1umore"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_incomplete_esc_only() {
|
||||||
|
assert!(!is_complete_kitty_input_seq(b"\x1b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_incomplete_esc_bracket() {
|
||||||
|
assert!(!is_complete_kitty_input_seq(b"\x1b["));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_incomplete_esc_bracket_question() {
|
||||||
|
assert!(!is_complete_kitty_input_seq(b"\x1b[?"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_incomplete_no_u() {
|
||||||
|
assert!(!is_complete_kitty_input_seq(b"\x1b[?1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_not_kitty() {
|
||||||
|
assert!(!is_complete_kitty_input_seq(b"\x1b[H"));
|
||||||
|
assert!(!is_complete_kitty_input_seq(b"\x1b[?25h"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_seq_empty() {
|
||||||
|
assert!(!is_complete_kitty_input_seq(b""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for kitty_output_split_point
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_no_esc() {
|
||||||
|
assert_eq!(kitty_output_split_point(b"hello world"), 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_empty() {
|
||||||
|
assert_eq!(kitty_output_split_point(b""), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_complete_disable() {
|
||||||
|
// Complete sequence - split at end (process all)
|
||||||
|
assert_eq!(kitty_output_split_point(b"\x1b[<u"), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_complete_enable() {
|
||||||
|
assert_eq!(kitty_output_split_point(b"\x1b[>1u"), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_incomplete_esc_only() {
|
||||||
|
// Just ESC - incomplete, split at 0 (keep all)
|
||||||
|
assert_eq!(kitty_output_split_point(b"\x1b"), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_incomplete_enable() {
|
||||||
|
// Incomplete enable - split at ESC position
|
||||||
|
assert_eq!(kitty_output_split_point(b"\x1b[>"), 0);
|
||||||
|
assert_eq!(kitty_output_split_point(b"\x1b[>1"), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_data_then_incomplete() {
|
||||||
|
// Data followed by incomplete - split before ESC
|
||||||
|
let data = b"hello\x1b[>";
|
||||||
|
assert_eq!(kitty_output_split_point(data), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_complete_then_incomplete() {
|
||||||
|
// Complete disable followed by incomplete enable
|
||||||
|
let data = b"\x1b[<u\x1b[>";
|
||||||
|
assert_eq!(kitty_output_split_point(data), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_split_esc_far_from_end() {
|
||||||
|
// ESC more than 16 bytes from end - process all
|
||||||
|
let mut data = b"\x1b[<u".to_vec();
|
||||||
|
data.extend_from_slice(b"this is padding longer than 16 bytes!");
|
||||||
|
assert_eq!(kitty_output_split_point(&data), data.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for kitty_input_split_point
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_split_no_esc() {
|
||||||
|
assert_eq!(kitty_input_split_point(b"hello world"), 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_split_empty() {
|
||||||
|
assert_eq!(kitty_input_split_point(b""), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_split_complete_query_response() {
|
||||||
|
assert_eq!(kitty_input_split_point(b"\x1b[?1u"), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_split_incomplete() {
|
||||||
|
assert_eq!(kitty_input_split_point(b"\x1b[?"), 0);
|
||||||
|
assert_eq!(kitty_input_split_point(b"\x1b[?1"), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_split_data_then_incomplete() {
|
||||||
|
let data = b"hello\x1b[?";
|
||||||
|
assert_eq!(kitty_input_split_point(data), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_split_complete_then_incomplete() {
|
||||||
|
let data = b"\x1b[?1u\x1b[?";
|
||||||
|
assert_eq!(kitty_input_split_point(data), 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user