How a chart meets a terminal: the charset and color ladders, what detection reads, and the overrides. The argument is Degradation is the contract. This file is the mechanics.
A charset is the glyph tier the subpixel surface encodes through. Glyph tables are data, not code.
| charset | subpixels per cell | standing |
|---|---|---|
Octants |
2×4 solid blocks | Unicode 16; densest ink, explicit opt-in |
Sextants |
2×3 solid blocks | Unicode 13; explicit opt-in |
Braille |
2×4 dots | dense opt-in; dots, not blocks |
Quadrants |
2×2 solid blocks | the conservative UTF-8 default |
HalfBlocks |
1×2 | the lowest Unicode rung |
Ascii |
1×1 | the guaranteed fallback |
The dense tiers are opt-in. No environment variable can prove the configured
font covers them. A terminal name is not a font. Frame::detect picks
quadrants in any UTF-8 environment, and ASCII otherwise. Octants, sextants,
and braille are choices you make for fonts you know.
The gallery's charsets example renders one curve at every rung:
cargo run --example charsets.
Four tiers, quantized honestly downhill: TrueColor, Ansi256, Ansi16,
Plain. The sixteen-color pick is made in OKLab, by lightness and hue, the
two things sixteen colors can carry. A teal drops to green, never to the
grey nearer in RGB. Heatmap half-blocks carry independent upper and lower
colors. Plain output keeps an averaged shade on the charset's own ramp
(░▒▓█, or .:#@ in ASCII). In colorless output, color_by categories
cycle portable marker shapes (•, +, x, *, o), so groups never
vanish in a pipe.
Frame::detect sniffs. It never writes to the terminal. The rules, in
order:
- Charset.
MALEVICH_CHARSETwins if set to a known name.TERM=dumborunknownmeans ASCII. Then the locale, POSIX precedence:LC_ALL,LC_CTYPE,LANG. A locale withoututfmeans ASCII. Otherwise quadrants. - Color.
NO_COLOR(any value) means plain. Output that is not a terminal means plain, unlessCLICOLOR_FORCEorFORCE_COLORis set and not0.TERM=dumborunknownmeans plain.COLORTERM=truecoloror24bit, or aTERMending in-direct, means truecolor. ATERMstarting withscreencaps at 256, since the multiplexer re-encodes what passes through it. ATERMcontaining256colormeans 256. Otherwise 16. - Size. The terminal's reported size. The plot takes a third of the
height. No terminal:
COLUMNSandLINESwhen a shell exports them, else 80×16. - Theme.
COLORFGBGdistinguishes dark from light backgrounds.
A pipe is clean plain text by default. Detection sees a non-terminal and drops color, and the charset never emits anything a file cannot hold.
MALEVICH_CHARSET—ascii,half,quad,sextants,octants,braille, orauto.MALEVICH_GRAPHICS—kitty,sixel,iterm2, ornone: the pixel protocol, outranking the sniff and skipping the probe (featurepixel).NO_COLOR— force plain output (no-color.org).CLICOLOR_FORCEorFORCE_COLOR— keep color when piping.COLUMNS/LINES— size a render that has no terminal to measure.- An explicit
Frame— set in code. It consults nothing.
- CJK labels are measured in display cells and stay aligned.
- Combining marks are deliberately dropped at the cell grid.
- Control characters are dropped at the cell grid. A title, label, or category carrying escape bytes can never smuggle them into any encoder's output. The only escapes in ANSI output are the encoder's own SGR sequences. A regression test pins this.
NaNis always a visible gap, never interpolated away.
When the frame shrinks, furniture sheds before data: legend, then titles,
then tick density. The data region is the last thing standing, and
TERM=dumb at any width still gets a correct chart.