Kitty graphics protocol is detected by terminal name only, never by query
Version: chafa 1.18.2 (Homebrew, macOS 15 / Darwin 25.5.0)
Summary
chafa detects sixel support by asking the terminal (DA1, CSI 0c, checking for
extension 4). It detects the Kitty graphics protocol only by recognising the
terminal's name in TERM / TERM_PROGRAM. The Kitty protocol has a standard
capability query — ESC _ G i=<id>,s=1,v=1,a=q,t=d,f=24;AAAA ESC \, answered
with ESC _ G i=<id>;OK ESC \ — and chafa never sends it, even with
--probe=on against a terminal that answers everything.
The effect is that any terminal implementing the Kitty protocol under a name
chafa's DB doesn't know cannot get Kitty output, no matter what it supports. It
falls back to sixel, which is a visible quality regression rather than a neutral
one (below). The only workaround available to such a terminal is to claim
another emulator's name, which is not something a terminal should have to do.
Reproduction
Drive chafa under a pty that answers every query chafa sends, advertising sixel
in DA1 and answering the KGP query, with TERM=xterm-256color and no
TERM_PROGRAM:
import os, pty, select
pid, fd = pty.fork()
if pid == 0:
env = dict(os.environ)
for k in ("TERM_PROGRAM", "TERM_PROGRAM_VERSION", "KITTY_WINDOW_ID", "LC_TERMINAL"):
env.pop(k, None)
env["TERM"], env["COLORTERM"] = "xterm-256color", "truecolor"
os.execvpe("chafa", ["chafa", "--probe=on", "-s", "20x6", "image.png"], env)
buf, kgp_query = b"", False
while True:
r, _, _ = select.select([fd], [], [], 3.0)
if not r:
break
d = os.read(fd, 65536)
if not d:
break
buf += d
if b"\x1b]10;?" in d: os.write(fd, b"\x1b]10;rgb:cccc/cccc/cccc\x1b\\")
if b"\x1b]11;?" in d: os.write(fd, b"\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\")
if b"\x1b[18t" in d: os.write(fd, b"\x1b[8;24;80t")
if b"\x1b[14t" in d: os.write(fd, b"\x1b[4;384;640t")
if b"\x1b[16t" in d: os.write(fd, b"\x1b[6;16;8t")
if b"\x1b[0c" in d: os.write(fd, b"\x1b[?1;2;4c") # DA1: sixel supported
if b"\x1b_G" in d and b"a=q" in d: # never reached
kgp_query = True
os.write(fd, b"\x1b_Gi=31;OK\x1b\\")
print("KGP query sent:", kgp_query)
print("output:", "kitty" if b"\x1b_G" in buf else "sixel" if b"\x1bP" in buf else "other")
Observed:
KGP query sent: False
output: sixel
The full probe chafa emits is:
ESC ] 10;? ESC \
ESC ] 11;? ESC \
ESC [ 18t
ESC [ 14t
ESC [ 16t
ESC [ 0c
— no APC sequence at any point.
Setting TERM=xterm-kitty, TERM=xterm-ghostty, or TERM_PROGRAM=ghostty on
the same terminal, with the same answers, switches the output to ESC _ G
immediately. So the capability is reachable; only the name gates it.
Why the sixel fallback is not equivalent
Sixel carries 1-bit transparency, so chafa must threshold the alpha channel.
Measured on the same PNG (a macOS screenshot with a soft drop shadow), same
terminal, same size:
| output |
distinct alpha values |
partially transparent pixels |
-f kitty (f=32 RGBA) |
179 |
30,969 |
-f sixel |
2 (0 and 255) |
0 |
The soft shadow is gone in the sixel render — a hard cut edge instead of a
gradient. This is correct behaviour for sixel; it is just not a fallback the
user would choose if the Kitty protocol were reachable.
Suggested fix
Add the KGP query to the probe, alongside the existing DA1 query:
ESC _ G i=31,s=1,v=1,a=q,t=d,f=24;AAAA ESC \
A terminal that implements the protocol replies ESC _ G i=31;OK ESC \; one
that does not replies nothing (the APC is consumed silently), so it costs one
sequence in the round chafa already makes and needs no extra wait beyond the
existing --probe timeout. Terminals that don't support APC at all discard it
without echoing, matching how kitty's own detection works.
That would make Kitty detection symmetric with sixel detection — capability
based rather than name based — and would work for terminals chafa has never
heard of, rather than requiring each one to be added to the DB individually
(as ghostty was in #296).
Context
Found while implementing the Kitty graphics protocol in
go-term, a Go terminal widget. It
answers a=q correctly and renders chafa -f kitty output including partial
alpha, but cannot be detected, because it identifies itself honestly as
TERM_PROGRAM=go-term rather than borrowing another emulator's name.
Kitty graphics protocol is detected by terminal name only, never by query
Version: chafa 1.18.2 (Homebrew, macOS 15 / Darwin 25.5.0)
Summary
chafa detects sixel support by asking the terminal (DA1,
CSI 0c, checking forextension
4). It detects the Kitty graphics protocol only by recognising theterminal's name in
TERM/TERM_PROGRAM. The Kitty protocol has a standardcapability query —
ESC _ G i=<id>,s=1,v=1,a=q,t=d,f=24;AAAA ESC \, answeredwith
ESC _ G i=<id>;OK ESC \— and chafa never sends it, even with--probe=onagainst a terminal that answers everything.The effect is that any terminal implementing the Kitty protocol under a name
chafa's DB doesn't know cannot get Kitty output, no matter what it supports. It
falls back to sixel, which is a visible quality regression rather than a neutral
one (below). The only workaround available to such a terminal is to claim
another emulator's name, which is not something a terminal should have to do.
Reproduction
Drive chafa under a pty that answers every query chafa sends, advertising sixel
in DA1 and answering the KGP query, with
TERM=xterm-256colorand noTERM_PROGRAM:Observed:
The full probe chafa emits is:
— no APC sequence at any point.
Setting
TERM=xterm-kitty,TERM=xterm-ghostty, orTERM_PROGRAM=ghosttyonthe same terminal, with the same answers, switches the output to
ESC _ Gimmediately. So the capability is reachable; only the name gates it.
Why the sixel fallback is not equivalent
Sixel carries 1-bit transparency, so chafa must threshold the alpha channel.
Measured on the same PNG (a macOS screenshot with a soft drop shadow), same
terminal, same size:
-f kitty(f=32RGBA)-f sixelThe soft shadow is gone in the sixel render — a hard cut edge instead of a
gradient. This is correct behaviour for sixel; it is just not a fallback the
user would choose if the Kitty protocol were reachable.
Suggested fix
Add the KGP query to the probe, alongside the existing DA1 query:
A terminal that implements the protocol replies
ESC _ G i=31;OK ESC \; onethat does not replies nothing (the APC is consumed silently), so it costs one
sequence in the round chafa already makes and needs no extra wait beyond the
existing
--probetimeout. Terminals that don't support APC at all discard itwithout echoing, matching how kitty's own detection works.
That would make Kitty detection symmetric with sixel detection — capability
based rather than name based — and would work for terminals chafa has never
heard of, rather than requiring each one to be added to the DB individually
(as ghostty was in #296).
Context
Found while implementing the Kitty graphics protocol in
go-term, a Go terminal widget. It
answers
a=qcorrectly and renderschafa -f kittyoutput including partialalpha, but cannot be detected, because it identifies itself honestly as
TERM_PROGRAM=go-termrather than borrowing another emulator's name.