fix(sixel): only advertise capability if host terminal supports it (#5432)

* fix(sixel): only advertize capability if host terminal supports it

* add pr
This commit is contained in:
Aram Drevekenin
2026-08-03 09:58:42 +02:00
committed by GitHub
parent 0e6e440402
commit c3c7e65965
23 changed files with 384 additions and 15 deletions
+4
View File
@@ -342,6 +342,10 @@ impl InputHandler {
self.os_input
.send_to_server(ClientToServerMsg::KittyGraphicsSupport { supported });
},
AnsiStdinInstruction::SixelSupport(supported) => {
self.os_input
.send_to_server(ClientToServerMsg::SixelSupport { supported });
},
}
}
fn handle_nested_session_frame_from_host(&mut self, payload_bytes: Vec<u8>) {
+20
View File
@@ -63,6 +63,7 @@ pub enum HostReply {
/// terminal's color-palette theme mode (CSI 2031).
HostTerminalThemeChanged(HostTerminalThemeMode),
KittyGraphicsSupport(bool),
SixelSupport(bool),
}
/// Retained alias for the pre-refactor type name used by other modules in
@@ -156,6 +157,22 @@ impl HostReply {
}
None
}
/// Classify a Primary Device Attributes reply (`CSI ? Ps ; Ps ... c`)
/// into a Sixel-capability advertisement. Attribute `4` in the reply
/// means the host terminal supports Sixel graphics.
///
/// Replies that are not a primary-DA form (eg. secondary-DA `CSI > ... c`)
/// yield `None` so they do not clobber the host capability state.
pub fn sixel_support_from_primary_da(raw: &[u8]) -> Option<HostReply> {
lazy_static! {
static ref PRIMARY_DA_RE: Regex = Regex::new(r"^\u{1b}\[\?([0-9;]*)c$").unwrap();
}
let s = std::str::from_utf8(raw).ok()?;
let caps = PRIMARY_DA_RE.captures(s)?;
let supports_sixel = caps[1].split(';').any(|attribute| attribute == "4");
Some(HostReply::SixelSupport(supports_sixel))
}
}
/// The "slot" tracking state for a single forwarded query currently in
@@ -419,6 +436,9 @@ impl StdinAnsiParser {
self.startup_kitty_probe = StartupKittyProbe::Resolved;
out.replies.push(HostReply::KittyGraphicsSupport(false));
}
if let Some(reply) = HostReply::sixel_support_from_primary_da(&raw) {
out.replies.push(reply);
}
// Primary-DA — the barrier. Close the slot and
// emit the completed forwarded reply if active.
if let Some(slot) = self.active_forward.take() {
+57 -7
View File
@@ -146,9 +146,11 @@ fn forwarding_window_accumulates_and_barrier_closes() {
chunk.extend_from_slice(b"\x1b]11;rgb:aaaa/bbbb/dddd\x1b\\");
chunk.extend_from_slice(b"\x1b[?65;1c");
let out = parser.feed(&chunk);
// OSC 11 was classified (double-dispatch).
assert_eq!(out.replies.len(), 1);
// OSC 11 was classified (double-dispatch), and the barrier itself
// advertises the host's sixel capability.
assert_eq!(out.replies.len(), 2);
matches!(out.replies[0], HostReply::BackgroundColor(_));
matches!(out.replies[1], HostReply::SixelSupport(false));
// Barrier closed the window, producing a completed forward.
let (token, reply_bytes) = out
.completed_forward
@@ -1241,11 +1243,59 @@ fn kitty_probe_absence_resolves_false_on_barrier() {
parser.expect_kitty_probe_reply();
let (replies, residue) = feed_once(&mut parser, b"\x1b[?62;4;52c");
assert!(residue.is_empty());
assert_eq!(replies.len(), 1);
match &replies[0] {
HostReply::KittyGraphicsSupport(supported) => assert!(!*supported),
other => panic!("unexpected reply: {:?}", other),
}
let kitty_replies: Vec<bool> = replies
.iter()
.filter_map(|r| match r {
HostReply::KittyGraphicsSupport(supported) => Some(*supported),
_ => None,
})
.collect();
assert_eq!(kitty_replies, vec![false]);
}
#[test]
fn primary_da_with_sixel_attribute_classifies_sixel_support_true() {
let mut parser = StdinAnsiParser::new();
let (replies, residue) = feed_once(&mut parser, b"\x1b[?62;4;52c");
assert!(residue.is_empty());
let sixel_replies: Vec<bool> = replies
.iter()
.filter_map(|r| match r {
HostReply::SixelSupport(supported) => Some(*supported),
_ => None,
})
.collect();
assert_eq!(sixel_replies, vec![true]);
}
#[test]
fn primary_da_without_sixel_attribute_classifies_sixel_support_false() {
let mut parser = StdinAnsiParser::new();
let (replies, residue) = feed_once(&mut parser, b"\x1b[?62;22c");
assert!(residue.is_empty());
let sixel_replies: Vec<bool> = replies
.iter()
.filter_map(|r| match r {
HostReply::SixelSupport(supported) => Some(*supported),
_ => None,
})
.collect();
assert_eq!(sixel_replies, vec![false]);
}
#[test]
fn primary_da_sixel_attribute_is_not_matched_as_a_substring() {
let mut parser = StdinAnsiParser::new();
let (replies, residue) = feed_once(&mut parser, b"\x1b[?64;14;21;22c");
assert!(residue.is_empty());
let sixel_replies: Vec<bool> = replies
.iter()
.filter_map(|r| match r {
HostReply::SixelSupport(supported) => Some(*supported),
_ => None,
})
.collect();
assert_eq!(sixel_replies, vec![false]);
}
#[test]
+1
View File
@@ -61,6 +61,7 @@ pub(crate) fn stdin_loop(
} else {
let _ = send_input_instructions.send(InputInstruction::AnsiStdinInstructions(vec![
HostReply::KittyGraphicsSupport(false),
HostReply::SixelSupport(false),
]));
}
}