mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why Older iTerm2 builds can be detected as supporting the image transport that terminal pets use, but in practice they fail to render the pet flow correctly. Instead of silently attempting image rendering, Codex should tell the user that their iTerm2 version is too old and that upgrading is the fix. ## What Changed - gate iTerm2 pet auto-detection on version `3.6.0` or newer - show a dedicated upgrade message for older or unknown iTerm2 versions instead of the generic unsupported-terminal warning - keep the existing generic unsupported-terminal path for non-iTerm terminals - add regression coverage for iTerm2 version parsing and the old-iTerm warning path ## How to Test 1. Start Codex in iTerm2 3.6 or newer. 2. Run `/pets`. 3. Confirm the pets picker opens instead of showing a warning. 4. Start Codex in an older iTerm2 build, or exercise the equivalent test path. 5. Run `/pets`. 6. Confirm Codex warns that pets require iTerm2 3.6 or newer and tells the user to upgrade. 7. Also verify that a non-iTerm unsupported terminal still shows the generic unsupported-terminal message. Targeted tests: - `cargo test -p codex-terminal-detection` - `cargo test -p codex-tui pets::` - `cargo test -p codex-tui slash_pets_on_unsupported_terminal` - `cargo test -p codex-tui slash_pets_on_old_iterm2`
698 lines
22 KiB
Rust
698 lines
22 KiB
Rust
use std::env;
|
||
use std::fs;
|
||
use std::path::Path;
|
||
use std::path::PathBuf;
|
||
use std::str::FromStr;
|
||
|
||
use anyhow::Context;
|
||
use anyhow::Result;
|
||
use anyhow::bail;
|
||
use base64::Engine as _;
|
||
use base64::engine::general_purpose;
|
||
use codex_terminal_detection::Multiplexer;
|
||
use codex_terminal_detection::TerminalInfo;
|
||
use codex_terminal_detection::TerminalName;
|
||
use codex_terminal_detection::terminal_info;
|
||
use image::imageops::FilterType;
|
||
|
||
use super::sixel;
|
||
|
||
const ESC: &str = "\x1b";
|
||
const ST: &str = "\x1b\\";
|
||
const KITTY_CHUNK_SIZE: usize = 4096;
|
||
const SIXEL_CACHE_VERSION: &str = "v2";
|
||
const ITERM2_KITTY_MIN_VERSION: (u64, u64, u64) = (3, 6, 0);
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum ImageProtocol {
|
||
Kitty,
|
||
KittyLocalFile,
|
||
Sixel,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub(crate) enum PetImageSupport {
|
||
Supported(ImageProtocol),
|
||
Unsupported(PetImageUnsupportedReason),
|
||
}
|
||
|
||
impl PetImageSupport {
|
||
pub(crate) fn protocol(self) -> Option<ImageProtocol> {
|
||
match self {
|
||
Self::Supported(protocol) => Some(protocol),
|
||
Self::Unsupported(_) => None,
|
||
}
|
||
}
|
||
|
||
pub(crate) fn unsupported_message(self) -> Option<&'static str> {
|
||
match self {
|
||
Self::Supported(_) => None,
|
||
Self::Unsupported(reason) => Some(reason.message()),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub(crate) enum PetImageUnsupportedReason {
|
||
Tmux,
|
||
Zellij,
|
||
Iterm2TooOld,
|
||
Terminal,
|
||
}
|
||
|
||
impl PetImageUnsupportedReason {
|
||
fn message(self) -> &'static str {
|
||
match self {
|
||
Self::Tmux => {
|
||
"Pets are disabled in tmux. Terminal images don’t stay pane-local in tmux and can corrupt scrollback or move between panes. Run Codex outside tmux to use pets."
|
||
}
|
||
Self::Zellij => {
|
||
"Pets are disabled in Zellij. Terminal images don’t stay reliably pane-local in Zellij. Run Codex outside Zellij to use pets."
|
||
}
|
||
Self::Iterm2TooOld => {
|
||
"Pets require iTerm2 3.6 or newer. Upgrade iTerm2 to use terminal pets."
|
||
}
|
||
Self::Terminal => {
|
||
"Pets aren’t available in this terminal. Terminal pets need image support, and this terminal environment doesn’t expose a supported image protocol. Try a terminal with Kitty graphics or Sixel support, or run Codex outside tmux."
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum ProtocolSelection {
|
||
Auto,
|
||
Kitty,
|
||
Sixel,
|
||
}
|
||
|
||
impl ProtocolSelection {
|
||
pub(crate) fn resolve(self) -> PetImageSupport {
|
||
match self {
|
||
Self::Kitty => PetImageSupport::Supported(ImageProtocol::Kitty),
|
||
Self::Sixel => PetImageSupport::Supported(ImageProtocol::Sixel),
|
||
Self::Auto => detect_pet_image_support(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl FromStr for ProtocolSelection {
|
||
type Err = anyhow::Error;
|
||
|
||
fn from_str(value: &str) -> Result<Self> {
|
||
match value {
|
||
"auto" => Ok(Self::Auto),
|
||
"kitty" => Ok(Self::Kitty),
|
||
"sixel" => Ok(Self::Sixel),
|
||
other => bail!("unknown protocol {other}; expected auto, kitty, or sixel"),
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn detect_pet_image_support() -> PetImageSupport {
|
||
if env::var_os("TMUX").is_some() || env::var_os("TMUX_PANE").is_some() {
|
||
return PetImageSupport::Unsupported(PetImageUnsupportedReason::Tmux);
|
||
}
|
||
|
||
if env::var_os("ZELLIJ").is_some()
|
||
|| env::var_os("ZELLIJ_SESSION_NAME").is_some()
|
||
|| env::var_os("ZELLIJ_VERSION").is_some()
|
||
{
|
||
return PetImageSupport::Unsupported(PetImageUnsupportedReason::Zellij);
|
||
}
|
||
|
||
if env::var_os("KITTY_WINDOW_ID").is_some() {
|
||
return PetImageSupport::Supported(ImageProtocol::Kitty);
|
||
}
|
||
|
||
if env::var_os("WEZTERM_EXECUTABLE").is_some() || env::var_os("WEZTERM_VERSION").is_some() {
|
||
return PetImageSupport::Supported(ImageProtocol::Kitty);
|
||
}
|
||
|
||
pet_image_support_for_terminal(&terminal_info())
|
||
}
|
||
|
||
fn pet_image_support_for_terminal(info: &TerminalInfo) -> PetImageSupport {
|
||
match info.multiplexer {
|
||
Some(Multiplexer::Tmux { .. }) => {
|
||
return PetImageSupport::Unsupported(PetImageUnsupportedReason::Tmux);
|
||
}
|
||
Some(Multiplexer::Zellij { .. }) => {
|
||
return PetImageSupport::Unsupported(PetImageUnsupportedReason::Zellij);
|
||
}
|
||
None => {}
|
||
}
|
||
|
||
if supports_iterm2_kitty_graphics(info) {
|
||
return PetImageSupport::Supported(ImageProtocol::KittyLocalFile);
|
||
}
|
||
|
||
if is_iterm2_terminal(info) {
|
||
return PetImageSupport::Unsupported(PetImageUnsupportedReason::Iterm2TooOld);
|
||
}
|
||
|
||
if supports_kitty_graphics(info) {
|
||
return PetImageSupport::Supported(ImageProtocol::Kitty);
|
||
}
|
||
|
||
if supports_sixel(info) {
|
||
return PetImageSupport::Supported(ImageProtocol::Sixel);
|
||
}
|
||
|
||
PetImageSupport::Unsupported(PetImageUnsupportedReason::Terminal)
|
||
}
|
||
|
||
fn supports_iterm2_kitty_graphics(info: &TerminalInfo) -> bool {
|
||
is_iterm2_terminal(info)
|
||
&& version_is_at_least(
|
||
info.version.as_deref(),
|
||
/*minimum*/ ITERM2_KITTY_MIN_VERSION,
|
||
)
|
||
}
|
||
|
||
fn is_iterm2_terminal(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::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(), "wezterm")
|
||
}
|
||
|
||
fn supports_sixel(info: &TerminalInfo) -> bool {
|
||
matches!(info.name, TerminalName::WindowsTerminal)
|
||
|| terminal_field_contains(info.term.as_deref(), "sixel")
|
||
|| terminal_field_contains(info.term.as_deref(), "mlterm")
|
||
|| terminal_field_contains(info.term.as_deref(), "foot")
|
||
}
|
||
|
||
fn terminal_field_contains(value: Option<&str>, needle: &str) -> bool {
|
||
value.is_some_and(|value| value.to_ascii_lowercase().contains(needle))
|
||
}
|
||
|
||
fn version_is_at_least(version: Option<&str>, minimum: (u64, u64, u64)) -> bool {
|
||
parse_dotted_version(version).is_some_and(|version| version >= minimum)
|
||
}
|
||
|
||
fn parse_dotted_version(version: Option<&str>) -> Option<(u64, u64, u64)> {
|
||
let version = version?;
|
||
let mut parts = version.split('.');
|
||
let major = parts.next()?.parse().ok()?;
|
||
let minor = parts.next().unwrap_or("0").parse().ok()?;
|
||
let patch = parts.next().unwrap_or("0").parse().ok()?;
|
||
|
||
if parts.next().is_some() {
|
||
return None;
|
||
}
|
||
|
||
Some((major, minor, patch))
|
||
}
|
||
|
||
pub fn kitty_delete_image(image_id: u32) -> String {
|
||
wrap_for_tmux_if_needed(&format!("{ESC}_Ga=d,d=I,i={image_id},q=2;{ST}"))
|
||
}
|
||
|
||
pub fn kitty_transmit_png_with_id(
|
||
path: &Path,
|
||
columns: u16,
|
||
rows: u16,
|
||
image_id: Option<u32>,
|
||
) -> Result<String> {
|
||
let png = fs::read(path).with_context(|| format!("read {}", path.display()))?;
|
||
let payload = general_purpose::STANDARD.encode(png);
|
||
let chunks = payload
|
||
.as_bytes()
|
||
.chunks(KITTY_CHUNK_SIZE)
|
||
.collect::<Vec<_>>();
|
||
|
||
let mut command = String::new();
|
||
for (index, chunk) in chunks.iter().enumerate() {
|
||
let chunk = std::str::from_utf8(chunk).context("base64 payload is not valid UTF-8")?;
|
||
let has_more = index + 1 < chunks.len();
|
||
let more_flag = u8::from(has_more);
|
||
if index == 0 {
|
||
let image_id = kitty_image_id_arg(image_id);
|
||
command.push_str(&format!(
|
||
"{ESC}_Ga=T,t=d,f=100,c={columns},r={rows},q=2{image_id},m={more_flag};{chunk}{ST}",
|
||
));
|
||
} else {
|
||
command.push_str(&format!("{ESC}_Gm={more_flag};{chunk}{ST}"));
|
||
}
|
||
}
|
||
|
||
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 = kitty_image_id_arg(image_id);
|
||
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 kitty_image_id_arg(image_id: Option<u32>) -> String {
|
||
image_id
|
||
.map(|image_id| format!(",i={image_id}"))
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
fn wrap_for_tmux_if_needed(command: &str) -> String {
|
||
if env::var_os("TMUX").is_none() {
|
||
return command.to_string();
|
||
}
|
||
|
||
let escaped = command.replace(ESC, "\x1b\x1b");
|
||
format!("{ESC}Ptmux;{escaped}{ST}")
|
||
}
|
||
|
||
pub fn sixel_frame(frame_path: &Path, cache_dir: &Path, height_px: u16) -> Result<PathBuf> {
|
||
fs::create_dir_all(cache_dir).with_context(|| format!("create {}", cache_dir.display()))?;
|
||
|
||
let stem = frame_path
|
||
.file_stem()
|
||
.and_then(|stem| stem.to_str())
|
||
.context("frame path has no valid file stem")?;
|
||
let path = cache_dir.join(format!("{stem}_h{height_px}_{SIXEL_CACHE_VERSION}.six"));
|
||
if path.exists() {
|
||
return Ok(path);
|
||
}
|
||
|
||
let frame =
|
||
image::open(frame_path).with_context(|| format!("read {}", frame_path.display()))?;
|
||
let height = u32::from(height_px).max(1);
|
||
let width = ((u64::from(frame.width()) * u64::from(height)) / u64::from(frame.height()))
|
||
.try_into()
|
||
.unwrap_or(u32::MAX)
|
||
.max(1);
|
||
let rgba = frame.resize(width, height, FilterType::Lanczos3).to_rgba8();
|
||
let (width, height) = rgba.dimensions();
|
||
let sixel = sixel::encode_rgba(&rgba.into_raw(), width, height)?;
|
||
|
||
fs::write(&path, sixel).with_context(|| format!("write {}", path.display()))?;
|
||
Ok(path)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use serial_test::serial;
|
||
|
||
use super::*;
|
||
|
||
struct EnvVarGuard {
|
||
name: &'static str,
|
||
previous: Option<std::ffi::OsString>,
|
||
}
|
||
|
||
impl EnvVarGuard {
|
||
fn new(name: &'static str, value: Option<&str>) -> Self {
|
||
let previous = env::var_os(name);
|
||
match value {
|
||
Some(value) => unsafe { env::set_var(name, value) },
|
||
None => unsafe { env::remove_var(name) },
|
||
}
|
||
Self { name, previous }
|
||
}
|
||
}
|
||
|
||
impl Drop for EnvVarGuard {
|
||
fn drop(&mut self) {
|
||
match self.previous.take() {
|
||
Some(value) => unsafe { env::set_var(self.name, value) },
|
||
None => unsafe { env::remove_var(self.name) },
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[serial]
|
||
fn kitty_png_transmission_encodes_inline_data() {
|
||
let _guard = EnvVarGuard::new("TMUX", /*value*/ None);
|
||
let dir = tempfile::tempdir().unwrap();
|
||
let path = dir.path().join("frame.png");
|
||
fs::write(&path, b"png").unwrap();
|
||
|
||
let command = kitty_transmit_png_with_id(
|
||
&path, /*columns*/ 4, /*rows*/ 3, /*image_id*/ None,
|
||
)
|
||
.unwrap();
|
||
|
||
assert!(command.starts_with("\x1b_Ga=T,t=d,f=100,c=4,r=3,q=2,m=0;"));
|
||
assert!(command.contains("cG5n"));
|
||
assert!(command.ends_with("\x1b\\"));
|
||
}
|
||
|
||
#[test]
|
||
#[serial]
|
||
fn tmux_passthrough_wraps_and_escapes_control_sequence() {
|
||
let _guard = EnvVarGuard::new("TMUX", Some("session"));
|
||
assert_eq!(
|
||
wrap_for_tmux_if_needed("\x1b_Gx;\x1b\\"),
|
||
"\x1bPtmux;\x1b\x1b_Gx;\x1b\x1b\\\x1b\\"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn parses_protocol_selection() {
|
||
assert_eq!(
|
||
"auto".parse::<ProtocolSelection>().unwrap(),
|
||
ProtocolSelection::Auto
|
||
);
|
||
assert_eq!(
|
||
"kitty".parse::<ProtocolSelection>().unwrap(),
|
||
ProtocolSelection::Kitty
|
||
);
|
||
assert_eq!(
|
||
"sixel".parse::<ProtocolSelection>().unwrap(),
|
||
ProtocolSelection::Sixel
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
#[serial]
|
||
fn auto_protocol_is_disabled_inside_tmux() {
|
||
let _guard = EnvVarGuard::new("TMUX", Some("session"));
|
||
|
||
assert_eq!(
|
||
ProtocolSelection::Auto.resolve(),
|
||
PetImageSupport::Unsupported(PetImageUnsupportedReason::Tmux)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
#[serial]
|
||
fn explicit_protocol_still_resolves_inside_tmux() {
|
||
let _guard = EnvVarGuard::new("TMUX", Some("session"));
|
||
|
||
assert_eq!(
|
||
ProtocolSelection::Kitty.resolve(),
|
||
PetImageSupport::Supported(ImageProtocol::Kitty)
|
||
);
|
||
assert_eq!(
|
||
ProtocolSelection::Sixel.resolve(),
|
||
PetImageSupport::Supported(ImageProtocol::Sixel)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn pet_image_support_prefers_multiplexer_safety() {
|
||
assert_eq!(
|
||
pet_image_support_for_terminal(&terminal_info_for_test(
|
||
TerminalName::Ghostty,
|
||
Some(Multiplexer::Tmux { version: None }),
|
||
Some("Ghostty"),
|
||
/*term*/ None,
|
||
)),
|
||
PetImageSupport::Unsupported(PetImageUnsupportedReason::Tmux)
|
||
);
|
||
assert_eq!(
|
||
pet_image_support_for_terminal(&terminal_info_for_test(
|
||
TerminalName::Kitty,
|
||
Some(Multiplexer::Zellij { version: None }),
|
||
Some("kitty"),
|
||
/*term*/ None,
|
||
)),
|
||
PetImageSupport::Unsupported(PetImageUnsupportedReason::Zellij)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn pet_image_support_detects_iterm2_kitty_file_graphics() {
|
||
for info in [
|
||
terminal_info_with_version_for_test(
|
||
TerminalName::Iterm2,
|
||
/*multiplexer*/ None,
|
||
Some("iTerm.app"),
|
||
Some("3.6.10"),
|
||
/*term*/ None,
|
||
),
|
||
terminal_info_with_version_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
Some("iTerm.app"),
|
||
Some("3.6.10"),
|
||
Some("xterm-256color"),
|
||
),
|
||
] {
|
||
assert_eq!(
|
||
pet_image_support_for_terminal(&info),
|
||
PetImageSupport::Supported(ImageProtocol::KittyLocalFile)
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn pet_image_support_rejects_old_iterm2_versions() {
|
||
for info in [
|
||
terminal_info_with_version_for_test(
|
||
TerminalName::Iterm2,
|
||
/*multiplexer*/ None,
|
||
Some("iTerm.app"),
|
||
Some("3.5.14"),
|
||
/*term*/ None,
|
||
),
|
||
terminal_info_with_version_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
Some("iTerm.app"),
|
||
/*version*/ None,
|
||
Some("xterm-256color"),
|
||
),
|
||
terminal_info_with_version_for_test(
|
||
TerminalName::Iterm2,
|
||
/*multiplexer*/ None,
|
||
Some("iTerm.app"),
|
||
Some("3.5"),
|
||
/*term*/ None,
|
||
),
|
||
] {
|
||
assert_eq!(
|
||
pet_image_support_for_terminal(&info),
|
||
PetImageSupport::Unsupported(PetImageUnsupportedReason::Iterm2TooOld)
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn pet_image_support_old_iterm2_message_mentions_upgrade() {
|
||
let message = PetImageSupport::Unsupported(PetImageUnsupportedReason::Iterm2TooOld)
|
||
.unsupported_message();
|
||
|
||
assert_eq!(
|
||
message,
|
||
Some("Pets require iTerm2 3.6 or newer. Upgrade iTerm2 to use terminal pets.")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn pet_image_support_detects_kitty_graphics_terminals() {
|
||
for info in [
|
||
terminal_info_for_test(
|
||
TerminalName::Ghostty,
|
||
/*multiplexer*/ None,
|
||
Some("Ghostty"),
|
||
/*term*/ None,
|
||
),
|
||
terminal_info_for_test(
|
||
TerminalName::Kitty,
|
||
/*multiplexer*/ None,
|
||
Some("kitty"),
|
||
/*term*/ None,
|
||
),
|
||
terminal_info_for_test(
|
||
TerminalName::WezTerm,
|
||
/*multiplexer*/ None,
|
||
Some("WezTerm"),
|
||
/*term*/ None,
|
||
),
|
||
terminal_info_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
/*term_program*/ None,
|
||
Some("xterm-kitty"),
|
||
),
|
||
terminal_info_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
/*term_program*/ None,
|
||
Some("wezterm"),
|
||
),
|
||
terminal_info_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
Some("WezTerm"),
|
||
Some("xterm-256color"),
|
||
),
|
||
] {
|
||
assert_eq!(
|
||
pet_image_support_for_terminal(&info),
|
||
PetImageSupport::Supported(ImageProtocol::Kitty)
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn pet_image_support_detects_sixel_terminals() {
|
||
for info in [
|
||
terminal_info_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
/*term_program*/ None,
|
||
Some("xterm-sixel"),
|
||
),
|
||
terminal_info_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
/*term_program*/ None,
|
||
Some("foot"),
|
||
),
|
||
terminal_info_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
/*term_program*/ None,
|
||
Some("mlterm"),
|
||
),
|
||
terminal_info_for_test(
|
||
TerminalName::WindowsTerminal,
|
||
/*multiplexer*/ None,
|
||
Some("WindowsTerminal"),
|
||
Some("xterm-256color"),
|
||
),
|
||
] {
|
||
assert_eq!(
|
||
pet_image_support_for_terminal(&info),
|
||
PetImageSupport::Supported(ImageProtocol::Sixel)
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[serial]
|
||
fn wezterm_env_uses_kitty_graphics_for_ambient_pets() {
|
||
let _tmux = EnvVarGuard::new("TMUX", /*value*/ None);
|
||
let _tmux_pane = EnvVarGuard::new("TMUX_PANE", /*value*/ None);
|
||
let _zellij = EnvVarGuard::new("ZELLIJ", /*value*/ None);
|
||
let _zellij_session = EnvVarGuard::new("ZELLIJ_SESSION_NAME", /*value*/ None);
|
||
let _zellij_version = EnvVarGuard::new("ZELLIJ_VERSION", /*value*/ None);
|
||
let _kitty = EnvVarGuard::new("KITTY_WINDOW_ID", /*value*/ None);
|
||
let _wezterm = EnvVarGuard::new("WEZTERM_VERSION", Some("20240203"));
|
||
let _wezterm_executable = EnvVarGuard::new("WEZTERM_EXECUTABLE", /*value*/ None);
|
||
|
||
assert_eq!(
|
||
detect_pet_image_support(),
|
||
PetImageSupport::Supported(ImageProtocol::Kitty)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn pet_image_support_rejects_unknown_terminals() {
|
||
assert_eq!(
|
||
pet_image_support_for_terminal(&terminal_info_for_test(
|
||
TerminalName::Unknown,
|
||
/*multiplexer*/ None,
|
||
/*term_program*/ None,
|
||
Some("xterm-256color"),
|
||
)),
|
||
PetImageSupport::Unsupported(PetImageUnsupportedReason::Terminal)
|
||
);
|
||
}
|
||
|
||
fn terminal_info_for_test(
|
||
name: TerminalName,
|
||
multiplexer: Option<Multiplexer>,
|
||
term_program: Option<&str>,
|
||
term: Option<&str>,
|
||
) -> TerminalInfo {
|
||
terminal_info_with_version_for_test(
|
||
name,
|
||
multiplexer,
|
||
term_program,
|
||
/*version*/ None,
|
||
term,
|
||
)
|
||
}
|
||
|
||
fn terminal_info_with_version_for_test(
|
||
name: TerminalName,
|
||
multiplexer: Option<Multiplexer>,
|
||
term_program: Option<&str>,
|
||
version: Option<&str>,
|
||
term: Option<&str>,
|
||
) -> TerminalInfo {
|
||
TerminalInfo {
|
||
name,
|
||
term_program: term_program.map(str::to_string),
|
||
version: version.map(str::to_string),
|
||
term: term.map(str::to_string),
|
||
multiplexer,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn parse_dotted_version_requires_simple_numeric_components() {
|
||
assert_eq!(parse_dotted_version(Some("3.6.10")), Some((3, 6, 10)));
|
||
assert_eq!(parse_dotted_version(Some("3.6")), Some((3, 6, 0)));
|
||
assert_eq!(parse_dotted_version(Some("3")), Some((3, 0, 0)));
|
||
assert_eq!(parse_dotted_version(Some("3.6.10.1")), None);
|
||
assert_eq!(parse_dotted_version(Some("3.6beta")), None);
|
||
assert_eq!(parse_dotted_version(/*version*/ None), None);
|
||
}
|
||
|
||
#[test]
|
||
fn sixel_frame_encodes_without_external_crate() {
|
||
let dir = tempfile::tempdir().unwrap();
|
||
let frame_path = dir.path().join("frame.png");
|
||
let rgba = image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 0, 0, 255]));
|
||
rgba.save(&frame_path).unwrap();
|
||
|
||
let sixel_path =
|
||
sixel_frame(&frame_path, &dir.path().join("sixel"), /*height_px*/ 1).unwrap();
|
||
let sixel = fs::read_to_string(sixel_path).unwrap();
|
||
|
||
assert!(sixel.starts_with("\x1bP9;1;0q\"1;1;1;1"));
|
||
assert!(sixel.contains("#224;2;100;0;0"));
|
||
assert!(sixel.contains("#224@"));
|
||
assert!(sixel.ends_with("\x1b\\"));
|
||
}
|
||
|
||
#[test]
|
||
#[serial]
|
||
fn kitty_file_png_transmission_encodes_local_file_reference() {
|
||
let _guard = EnvVarGuard::new("TMUX", /*value*/ None);
|
||
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\\")
|
||
);
|
||
}
|
||
}
|