Kitty · iTerm2 · Sixel · Half blocks

Images in the terminal. No dependencies.

A .NET library that draws images with the best protocol the terminal has. Detection reads the environment, then asks the terminal once, and never hangs. The core publishes with NativeAOT.

dotnet add package Nullean.TerminalImages
● ● ●terminal
$ tcat sunset.png
Half blocks here: two pixels in each character cell. On a Kitty, iTerm2 or Sixel terminal the same command draws the real pixels.

Detect, then draw

The renderer scales the image to fit, picks the encoder, and leaves the cursor below the image.

● ● ●Program.cs
var capabilities = await TerminalImageDetector.DetectAsync();
var renderer = new TerminalImageRenderer(capabilities);
renderer.RenderPng(File.ReadAllBytes("photo.png"), Console.Out);

Asking the terminal, once

Terminals answer in order, and every one of them answers Device Attributes. So that goes last.

● ● ●the probe, in one write
ESC _ G i=31,s=1,v=1,a=q,t=d,f=24;AAAA ESC \  Kitty query
ESC [ > 0 q                                       name and version
ESC [ 16 t                                        cell size in pixels
ESC [ c                                           the sentinel: DA1

Cheapest evidence first

Detection stops as soon as it has an answer it can trust.

  1. OverrideThe NULLEAN_TERMINAL_IMAGES variable, or code. Never probed.
  2. GatesOutput redirected, or TERM=dumb: nothing is drawn.
  3. EnvironmentKitty, Ghostty, iTerm2, WezTerm, Windows Terminal and others, with no I/O.
  4. ProbeOne write to /dev/tty, with a timeout that applies only when the terminal says nothing.

And it says why

The result lists what was found and a line for every decision. This is a scripted terminal that answers like WezTerm.

● ● ●tcat --diagnostics
protocol:    ITerm2
supported:   ITerm2, Sixel, Kitty, HalfBlocks
terminal:    wezterm 20240203-110809-5046fc22
confidence:  Probed
cell size:   10x20px
sixel regs:  256
truecolor:   True
multiplexer: False   ssh: False
  - No terminal recognised from the environment.
  - Probe answered: preferred protocol ITerm2, Sixel listed by the terminal, cell 10x20px.
The brand table puts WezTerm's iTerm2 images before its Sixel and Kitty, so iTerm2 wins although the terminal answered all three.

Four ways to draw

The renderer picks, in the order your terminal prefers.

ProtocolSent asScalingTerminals
Kittyraw RGBA in 4096-byte base64 chunks, or a PNGby the terminalKitty, Ghostty
iTerm2a PNG file in one OSC 1337 sequenceby the terminaliTerm2, WezTerm, Konsole, mintty
Sixel6-pixel bands with a palette, run-length encodednone: drawn at pixel sizefoot, Windows Terminal, xterm, WezTerm
Half blocks▀ with a foreground and a background colournoneanything with colour

What is verified

Verified: the encoders, PNG codec, resampler, detection logic and renderer, with unit tests that were checked by breaking the code on purpose; the terminal probe, by running the published NativeAOT binary in a pseudo-terminal with a scripted terminal, on macOS and Linux; the Terminal.Gui view in a live Terminal.Gui application; XenoAtom on its in-memory backend.

Not yet verified: output on real terminals (Kitty, Ghostty, iTerm2, WezTerm, foot, Windows Terminal, xterm), and tmux or SSH. The terminal-specific choices come from how other tools and the terminals' own documentation treat them. If one is wrong, tcat --diagnostics shows why, and an override fixes it. What detection assumes →