Skip to content

Kitty graphics protocol is detected by terminal name only, never by query #349

Description

@mike-ward

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions