Chooses a graphics protocol from the environment and maps it to a backend module.
Detection reads KITTY_WINDOW_ID, TERM, TERM_PROGRAM, WEZTERM_PANE, LC_TERMINAL and
KONSOLE_VERSION. A variable exported empty counts as unset. The first matching rule wins,
in this order:
| environment | protocol |
|---|---|
KITTY_WINDOW_ID set | :kitty |
TERM contains kitty or ghostty | :kitty |
TERM_PROGRAM is WezTerm | :kitty |
WEZTERM_PANE set | :kitty |
TERM_PROGRAM is iTerm.app | :iterm2 |
LC_TERMINAL is iTerm2 | :iterm2 |
KONSOLE_VERSION set | :sixel |
TERM contains foot or mlterm | :sixel |
| anything else | :braille |
:braille is the fallback because it needs no graphics support at all.
FRENCH_CURVE_ITERM2=kitty sends iTerm2 to :kitty instead; other terminals ignore
it.
WEZTERM_PANE and LC_TERMINAL cover terminals whose TERM_PROGRAM did not survive the
trip: multiplexers drop it, and ssh forwards only the variables its client is configured to
send, which for iTerm2 is LC_TERMINAL under the usual SendEnv LC_*. Detecting a remote
client therefore means passing that session's environment to detect/1, not the
environment of the host the program runs on.
A terminal detected as :kitty does not necessarily speak all of the protocol — ask
placements?/1 before storing an image under an id and placing it.
iTerm2
iTerm2 gets its own inline images. Forced to :kitty, it speaks transmit-and-display
and delete only: placements?/1 is false for it at every version, and
FrenchCurve.frame/3 retransmits each frame. What each path costs on iTerm2 is set
out in the README.
case FrenchCurve.Capability.detect() do
:braille -> IO.write(FrenchCurve.to_terminal(raster, :braille))
protocol -> IO.write(FrenchCurve.to_terminal(raster, protocol))
end
Summary
Functions
Returns the backend module implementing protocol.
Detects the protocol for the current OS environment.
Detects the protocol for env, a map of environment variable name to string value.
The protocol replies implies, or nil when they name nothing usable.
Whether env describes an iTerm2 session, locally or over ssh.
Whether this terminal keeps images under an id and draws them again by placement.
Whether the terminal described by env keeps images under an id and places them.
The bytes to write to a terminal to ask what it supports.
Whether replies contains the device attributes answer that ends the exchange.
Functions
Returns the backend module implementing protocol.
Accepts :kitty, :sixel, :iterm2, :braille and :cells; any other value raises
FunctionClauseError. Every returned module exports render/2 taking a
FrenchCurve.Raster and a keyword list.
@spec detect() :: atom()
Detects the protocol for the current OS environment.
The result is computed once and cached in :persistent_term under
{FrenchCurve.Capability, :detected} for the life of the VM, so a later change to the
environment is not picked up. Use detect/1 to detect against a specific environment.
Returns :kitty, :sixel, :iterm2 or :braille.
Detects the protocol for env, a map of environment variable name to string value.
Uncached, and missing keys are treated as unset. Returns :kitty, :sixel, :iterm2
or :braille; :braille when no rule matches.
iex> FrenchCurve.Capability.detect(%{"TERM" => "xterm-kitty"})
:kitty
The protocol replies implies, or nil when they name nothing usable.
The terminal's own name wins: a terminal that says it is kitty, ghostty or
WezTerm gets :kitty, and one that says it is iTerm2 gets :iterm2 even though
it answers kitty's queries. Only when the name is absent or unrecognised do the
device attributes decide, where a 4 among them is sixel support.
iex> FrenchCurve.Capability.from_probe("\eP>|kitty(0.32.2)\e\\\e[?62;22c")
:kitty
iex> FrenchCurve.Capability.from_probe("\e[?62;4;6c")
:sixel
iex> FrenchCurve.Capability.from_probe("\e[?62;22c")
nil
Whether env describes an iTerm2 session, locally or over ssh.
iex> FrenchCurve.Capability.iterm2?(%{"LC_TERMINAL" => "iTerm2"})
true
iex> FrenchCurve.Capability.iterm2?(%{"TERM" => "xterm-kitty"})
false
@spec placements?() :: boolean()
Whether this terminal keeps images under an id and draws them again by placement.
detect/1 saying :kitty does not have to mean the whole protocol is there: a terminal can
understand transmit-and-display (a=T) without keeping images under an id for
FrenchCurve.Backend.Kitty.place/2 to draw again. Sending one a store and then a placement
draws nothing at all — it stores a picture it never shows, then is handed a command it does
not understand.
FrenchCurve.frame/3 asks this and picks accordingly; ask it yourself only if you are
building the sequence by hand.
iex> FrenchCurve.Capability.placements?(%{"TERM" => "xterm-kitty"})
true
iex> FrenchCurve.Capability.placements?(%{"TERM_PROGRAM" => "iTerm.app", "TERM_PROGRAM_VERSION" => "3.7.2"})
false
iex> FrenchCurve.Capability.placements?(%{"TERM" => "xterm-256color"})
false
Whether the terminal described by env keeps images under an id and places them.
As placements?/0, against a given environment map rather than the OS one. Uncached.
@spec probe() :: binary()
The bytes to write to a terminal to ask what it supports.
Two queries: XTVERSION (CSI > q), which a terminal answers with its name, and
primary device attributes (CSI c), which it answers with a list of features.
The attributes reply is what probe_complete?/1 watches for, because nearly
every terminal answers it while only some answer XTVERSION — so it arrives last
and marks the end of the exchange.
A terminal that answers neither says nothing at all, which is why a caller must also give up on a timeout.
iex> FrenchCurve.Capability.probe()
"\e[>q\e[c"
Whether replies contains the device attributes answer that ends the exchange.
iex> FrenchCurve.Capability.probe_complete?("\e[?62;4c")
true
iex> FrenchCurve.Capability.probe_complete?("\eP>|kitty(0.32.2)\e\\")
false