mirror of
https://github.com/openai/codex.git
synced 2026-05-16 01:02:48 +00:00
fix(tui): use local-file kitty graphics for iterm pets
This commit is contained in:
@@ -25,6 +25,7 @@ const KITTY_CHUNK_SIZE: usize = 4096;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImageProtocol {
|
||||
Kitty,
|
||||
KittyLocalFile,
|
||||
Sixel,
|
||||
}
|
||||
|
||||
@@ -137,6 +138,10 @@ fn pet_image_support_for_terminal(info: &TerminalInfo) -> PetImageSupport {
|
||||
None => {}
|
||||
}
|
||||
|
||||
if supports_iterm2_kitty_graphics(info) {
|
||||
return PetImageSupport::Supported(ImageProtocol::KittyLocalFile);
|
||||
}
|
||||
|
||||
if supports_kitty_graphics(info) {
|
||||
return PetImageSupport::Supported(ImageProtocol::Kitty);
|
||||
}
|
||||
@@ -148,16 +153,20 @@ fn pet_image_support_for_terminal(info: &TerminalInfo) -> PetImageSupport {
|
||||
PetImageSupport::Unsupported(PetImageUnsupportedReason::Terminal)
|
||||
}
|
||||
|
||||
fn supports_iterm2_kitty_graphics(info: &TerminalInfo) -> bool {
|
||||
matches!(info.name, TerminalName::Iterm2)
|
||||
|| terminal_field_contains(info.term_program.as_deref(), "iterm")
|
||||
}
|
||||
|
||||
fn supports_kitty_graphics(info: &TerminalInfo) -> bool {
|
||||
matches!(
|
||||
info.name,
|
||||
TerminalName::Ghostty | TerminalName::Iterm2 | TerminalName::Kitty | TerminalName::WezTerm
|
||||
TerminalName::Ghostty | TerminalName::Kitty | TerminalName::WezTerm
|
||||
) || terminal_field_contains(info.term.as_deref(), "kitty")
|
||||
|| terminal_field_contains(info.term.as_deref(), "ghostty")
|
||||
|| terminal_field_contains(info.term.as_deref(), "wezterm")
|
||||
|| terminal_field_contains(info.term_program.as_deref(), "kitty")
|
||||
|| terminal_field_contains(info.term_program.as_deref(), "ghostty")
|
||||
|| terminal_field_contains(info.term_program.as_deref(), "iterm")
|
||||
|| terminal_field_contains(info.term_program.as_deref(), "wezterm")
|
||||
}
|
||||
|
||||
@@ -213,6 +222,24 @@ pub fn kitty_transmit_png_with_id(
|
||||
Ok(wrap_for_tmux_if_needed(&command))
|
||||
}
|
||||
|
||||
pub fn kitty_transmit_png_file_with_id(
|
||||
path: &Path,
|
||||
columns: u16,
|
||||
rows: u16,
|
||||
image_id: Option<u32>,
|
||||
) -> Result<String> {
|
||||
let path = path
|
||||
.canonicalize()
|
||||
.with_context(|| format!("canonicalize {}", path.display()))?;
|
||||
let payload = general_purpose::STANDARD.encode(path.to_string_lossy().as_bytes());
|
||||
let image_id = image_id
|
||||
.map(|image_id| format!(",i={image_id}"))
|
||||
.unwrap_or_default();
|
||||
let command = format!("{ESC}_Ga=T,t=f,f=100,c={columns},r={rows},q=2{image_id};{payload}{ST}");
|
||||
|
||||
Ok(wrap_for_tmux_if_needed(&command))
|
||||
}
|
||||
|
||||
fn wrap_for_tmux_if_needed(command: &str) -> String {
|
||||
if env::var_os("TMUX").is_none() {
|
||||
return command.to_string();
|
||||
@@ -377,6 +404,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pet_image_support_detects_iterm2_kitty_file_graphics() {
|
||||
for info in [
|
||||
terminal_info_for_test(
|
||||
TerminalName::Iterm2,
|
||||
/*multiplexer*/ None,
|
||||
Some("iTerm.app"),
|
||||
/*term*/ None,
|
||||
),
|
||||
terminal_info_for_test(
|
||||
TerminalName::Unknown,
|
||||
/*multiplexer*/ None,
|
||||
Some("iTerm.app"),
|
||||
Some("xterm-256color"),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
pet_image_support_for_terminal(&info),
|
||||
PetImageSupport::Supported(ImageProtocol::KittyLocalFile)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pet_image_support_detects_kitty_graphics_terminals() {
|
||||
for info in [
|
||||
@@ -386,12 +436,6 @@ mod tests {
|
||||
Some("Ghostty"),
|
||||
/*term*/ None,
|
||||
),
|
||||
terminal_info_for_test(
|
||||
TerminalName::Iterm2,
|
||||
/*multiplexer*/ None,
|
||||
Some("iTerm.app"),
|
||||
/*term*/ None,
|
||||
),
|
||||
terminal_info_for_test(
|
||||
TerminalName::Kitty,
|
||||
/*multiplexer*/ None,
|
||||
@@ -416,12 +460,6 @@ mod tests {
|
||||
/*term_program*/ None,
|
||||
Some("wezterm"),
|
||||
),
|
||||
terminal_info_for_test(
|
||||
TerminalName::Unknown,
|
||||
/*multiplexer*/ None,
|
||||
Some("iTerm.app"),
|
||||
Some("xterm-256color"),
|
||||
),
|
||||
terminal_info_for_test(
|
||||
TerminalName::Unknown,
|
||||
/*multiplexer*/ None,
|
||||
@@ -525,4 +563,26 @@ mod tests {
|
||||
assert!(sixel.starts_with("\x1bP"));
|
||||
assert!(sixel.ends_with("\x1b\\"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_file_png_transmission_encodes_local_file_reference() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("frame.png");
|
||||
fs::write(&path, b"png").unwrap();
|
||||
|
||||
let command = kitty_transmit_png_file_with_id(
|
||||
&path,
|
||||
/*columns*/ 4,
|
||||
/*rows*/ 3,
|
||||
/*image_id*/ Some(7),
|
||||
)
|
||||
.unwrap();
|
||||
let path = path.canonicalize().unwrap();
|
||||
let payload = general_purpose::STANDARD.encode(path.to_string_lossy().as_bytes());
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
format!("\x1b_Ga=T,t=f,f=100,c=4,r=3,q=2,i=7;{payload}\x1b\\")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +59,19 @@ fn render_pet_image(
|
||||
use crossterm::queue;
|
||||
use image_protocol::ImageProtocol;
|
||||
|
||||
write!(writer, "{}", image_protocol::kitty_delete_image(image_id))?;
|
||||
let Some(request) = request else {
|
||||
write!(writer, "{}", image_protocol::kitty_delete_image(image_id))?;
|
||||
writer.flush()?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if matches!(
|
||||
request.protocol,
|
||||
ImageProtocol::Kitty | ImageProtocol::KittyLocalFile
|
||||
) {
|
||||
write!(writer, "{}", image_protocol::kitty_delete_image(image_id))?;
|
||||
}
|
||||
|
||||
let payload = match request.protocol {
|
||||
ImageProtocol::Kitty => {
|
||||
AmbientPetPayload::Text(image_protocol::kitty_transmit_png_with_id(
|
||||
@@ -74,6 +81,14 @@ fn render_pet_image(
|
||||
Some(image_id),
|
||||
)?)
|
||||
}
|
||||
ImageProtocol::KittyLocalFile => {
|
||||
AmbientPetPayload::Text(image_protocol::kitty_transmit_png_file_with_id(
|
||||
&request.frame,
|
||||
request.columns,
|
||||
request.rows,
|
||||
Some(image_id),
|
||||
)?)
|
||||
}
|
||||
ImageProtocol::Sixel => {
|
||||
let path =
|
||||
image_protocol::sixel_frame(&request.frame, &request.sixel_dir, request.height_px)?;
|
||||
@@ -145,4 +160,31 @@ mod tests {
|
||||
assert!(!output.contains("\x1b["));
|
||||
assert!(!output.contains("\x1b8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_local_file_pet_image_uses_file_reference_without_inline_payload() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let frame = dir.path().join("frame.png");
|
||||
std::fs::write(&frame, b"png").unwrap();
|
||||
let request = AmbientPetDraw {
|
||||
frame,
|
||||
protocol: ImageProtocol::KittyLocalFile,
|
||||
x: 2,
|
||||
y: 3,
|
||||
columns: 4,
|
||||
rows: 2,
|
||||
height_px: 75,
|
||||
sixel_dir: PathBuf::new(),
|
||||
};
|
||||
let mut output = Vec::new();
|
||||
|
||||
render_ambient_pet_image(&mut output, Some(request)).unwrap();
|
||||
|
||||
let output = String::from_utf8(output).unwrap();
|
||||
assert!(output.contains("a=d,d=I,i=49374,q=2;"));
|
||||
assert!(output.contains("\x1b[4;3H"));
|
||||
assert!(output.contains("a=T,t=f,f=100,c=4,r=2,q=2,i=49374;"));
|
||||
assert!(!output.contains("cG5n"));
|
||||
assert!(output.contains("\x1b8"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user