caps

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package caps carries what this terminal can actually do (§6). Four independent axes, each with a probe ladder, a cached result and an explicit override: detection is a default, never a cage.

This port populates Appearance and Width. Depth is decided only as far as NO_COLOR demands (§6.2 rung 1); the COLORTERM/TERM ladder, the OSC 11 background probe and the glyph-tier cmap parse arrive with the capability work. The type and its defaults are fixed here so every later stage threads one value rather than growing its own detection.

Index

Constants

View Source
const (
	EnvTheme      = "CLAUDE_READOUT_THEME"
	EnvColorDepth = "CLAUDE_READOUT_COLOR_DEPTH"
	EnvGlyphs     = "CLAUDE_READOUT_GLYPHS"
	EnvWidth      = "CLAUDE_READOUT_WIDTH"
)

Environment overrides, one per capability axis. They are the rung directly below a config pin and above every probe, and they live here rather than in internal/config because they are *detection* rungs: §6 gives each axis one ladder, and one ladder means one place that reads it. A value this build does not understand is ignored rather than rejected — `doctor` is where a terminal's capability story is explained.

View Source
const BackgroundTTL = 24 * time.Hour

BackgroundTTL is how long a probed background colour is believed.

The TTL is named here rather than in the cache because how long an answer stays true is a property of the question (D4). A terminal's background is changed by a human editing a preference pane, so a day is generous and nothing worse than a stale palette for one session happens if they do; the rungs below the cache — COLORFGBG and the terminal's own config — go on answering correctly the moment it expires.

View Source
const DefaultWidth = 100

DefaultWidth is §6.4's last rung: render has no tty, so a guess is unavoidable — but it is reported, not hidden.

View Source
const ProbeTimeout = 250 * time.Millisecond

ProbeTimeout is §7.2's number. tmux and screen are the known-hard case: the query may be swallowed, or answered by the multiplexer instead of by the host terminal, and there is no way to tell the difference from here except by waiting. 250 ms is long enough for a terminal that is going to answer and short enough that a person running `doctor` does not notice.

View Source
const TierTTL = 7 * 24 * time.Hour

TierTTL is how long a measured tier is believed.

Longer than the background's day, because the two change for different reasons: a background is a preference pane a user flips between light and dark, a font repertoire changes only when they install a font. A week is short enough that installing a nerd font is noticed within one, and the rungs below the cache go on answering correctly the moment it expires.

Variables

View Source
var ErrNoCmap = errors.New("no cmap subtable in format 4 or 12")

ErrNoCmap is returned when a file is a font this parser can open but carries no Unicode character map in a format it reads.

View Source
var ErrNoTTY = errors.New("no terminal available to probe")

ErrNoTTY reports that there is no terminal to ask. It is a fact about the environment, not a failure — `render` is in exactly this position on every invocation.

View Source
var ErrNotAFont = errors.New("not a TrueType/OpenType font")

ErrNotAFont is returned when the bytes are not an sfnt container at all.

Functions

func BackgroundKey

func BackgroundKey(env Env) string

BackgroundKey is where a probed background colour is cached. It is keyed by the terminal's identity, not by the session: two terminals on one machine routinely have different themes, and one terminal's answer is good for every window of it.

func CacheBackground

func CacheBackground(store Store, env Env, c paint.RGB) error

CacheBackground records a probed background colour where detection will find it. It is the second half of §6.1 rung 2: `theme probe`, `doctor` and `init` have a terminal to ask, `render` does not, and this is the only path between them.

func CacheTier

func CacheTier(store Store, env Env, t Tier) error

CacheTier records a measured tier where detection will find it. It is the second half of §6.3's ladder, exactly as CacheBackground is of §6.1's.

func EnvKeys

func EnvKeys() []string

EnvKeys is every environment variable capability detection reads. NO_COLOR is listed separately by callers: it is not a rung, it is absolute.

func FamilyOf

func FamilyOf(data []byte) (string, bool)

FamilyOf reads the font's own family name from its `name` table. It is what turns "the terminal is configured with Monaco" into "the file I read really is Monaco" — filename matching gets a candidate, this confirms it.

func Luma

func Luma(c paint.RGB) float64

Luma is the relative-luminance weighting applied to gamma-encoded channels, scaled to 0..1.

func Multiplexer

func Multiplexer(env Env) (string, bool)

Multiplexer reports tmux or screen, which §7.2 singles out because OSC 11 may be swallowed there or answered by the multiplexer rather than by the terminal it is running in.

func OSEnv

func OSEnv(key string) (string, bool)

OSEnv reads the real process environment.

func OSRun

func OSRun(timeout time.Duration, name string, args ...string) (string, error)

OSRun executes a helper with a hard timeout. fc-match is the only command this package runs, and only on Linux; everything else it needs, it reads.

func ProbeBackground

func ProbeBackground(timeout time.Duration) (paint.RGB, error)

ProbeBackground asks the controlling terminal for its background colour.

It is the function `theme probe`, `doctor` and `init` call. On success the caller is expected to cache the answer (see Options.Probe), because this is the only path by which the best signal ever reaches the hot path.

func TTYWidthOf

func TTYWidthOf(f *os.File) (int, bool)

TTYWidthOf measures the terminal a given file is attached to, falling back to the controlling terminal.

It exists for `doctor` (§6.4). render is spawned without a terminal and gets whatever /dev/tty happens to offer, which on many hosts is nothing; doctor is typed into a window and can measure that window directly. That difference is what makes pinning `layout.width` a remediation rather than another guess — and the report says which of the two answered.

func TierKey

func TierKey(env Env) string

TierKey is where a measured tier is cached. Keyed by the terminal, like the background: the font is a property of the terminal's configuration, and one terminal's answer is good for every window of it.

func TierProbes

func TierProbes(t Tier) []rune

TierProbes is every codepoint a tier can print, required or not — the per-codepoint result §6.3 asks `doctor` to display. It is a superset of TierRequires by exactly the characters explained above.

func TierRequires

func TierRequires(t Tier) []rune

TierRequires is the codepoints a font must draw to support a tier. `ascii` requires nothing, which is what makes it the floor.

Types

type Appearance

type Appearance string

Appearance selects the light or dark palette (§6.1).

const (
	Light Appearance = "light"
	Dark  Appearance = "dark"
)

func AppearanceFor

func AppearanceFor(c paint.RGB) Appearance

AppearanceFor classifies a background colour.

The luminance is computed from the gamma-encoded channels rather than from linearised ones, and the split is at 0.5. That is not the WCAG formula and it is not an oversight: it is the arithmetic the reference implementation shipped and the arithmetic every one of this user's terminals has been classified by. The two disagree only for backgrounds in a narrow band around mid-grey, where "light or dark?" has no right answer anyway, and matching the reference is worth more than matching a standard written for a different question.

func FromColorFGBG

func FromColorFGBG(v string) (Appearance, bool)

FromColorFGBG reads §6.1 rung 3: the trailing field of COLORFGBG is the background's palette index, and 7 or anything from 9 up is a light colour.

This rung sits above the terminal's own configuration file, which looks backwards until you know why: COLORFGBG is published by the running terminal and a config file is what the terminal was told at some point. When the two disagreed on the machine this program was written on, COLORFGBG was right.

func ParseAppearance

func ParseAppearance(s string) (Appearance, bool)

ParseAppearance accepts the two spellings config and CLAUDE_READOUT_THEME use; anything else leaves the caller's default in place.

type AppearanceSource

type AppearanceSource string

AppearanceSource names the rung that answered.

const (
	AppearanceFromConfig    AppearanceSource = "config theme.appearance"
	AppearanceFromEnv       AppearanceSource = EnvTheme
	AppearanceFromOSC11     AppearanceSource = "OSC 11 (cached)"
	AppearanceFromColorFGBG AppearanceSource = "COLORFGBG"
	AppearanceFromTermConf  AppearanceSource = "terminal configuration"
	AppearanceFromDefault   AppearanceSource = "default"
)

type Caps

type Caps struct {
	Appearance Appearance
	Depth      Depth
	Tier       Tier
	Width      int
}

Caps is the single capability value threaded through paint and segment.

func Default

func Default() Caps

Default is what every axis falls back to when nothing answers: a dark terminal with truecolor, and the one glyph tier (U+2588, U+2592, U+2502) present in effectively every monospace font shipped in thirty years (§6.3).

type Coverage

type Coverage struct {
	// contains filtered or unexported fields
}

Coverage is a font's character map reduced to the one question this program asks of it: is this codepoint drawn by a real glyph?

It is kept as sorted ranges rather than a set. A monospace font covers a few thousand codepoints in a few hundred runs; the ranges answer in a binary search, and the whole structure costs a few kilobytes instead of a map with an entry per character.

func ParseFont

func ParseFont(data []byte) (Coverage, error)

ParseFont reads a TrueType/OpenType font — or the first face of a collection — and returns the codepoints it draws.

func (Coverage) Count

func (c Coverage) Count() int

Count is how many codepoints the font draws. It exists for `doctor`, which says which font it tested and how big its repertoire is.

func (Coverage) Empty

func (c Coverage) Empty() bool

Empty reports a coverage that answers no to everything, which is what an unparsed or unresolved font yields.

func (Coverage) Has

func (c Coverage) Has(r rune) bool

Has reports whether the font draws r.

func (Coverage) HasAll

func (c Coverage) HasAll(rs ...rune) bool

HasAll reports whether the font draws every one of the codepoints.

type Depth

type Depth string

Depth is the colour resolution the terminal can render (§6.2).

const (
	TrueColor Depth = "truecolor"
	Depth256  Depth = "256"
	Depth16   Depth = "16"
	NoColor   Depth = "none"
)

func ParseDepth

func ParseDepth(s string) (Depth, bool)

ParseDepth accepts the spellings §8's `color_depth` uses. "auto" and any unrecognised value leave the caller's default in place.

func (Depth) PaintMode

func (d Depth) PaintMode() paint.Mode

PaintMode is how the painter renders this depth. It is the one place the detection vocabulary and the ANSI vocabulary meet.

type DepthSource

type DepthSource string

DepthSource names the rung that answered. `doctor` prints it, because "colour is off" and "colour is off *because TERM is unset*" are different conversations.

const (
	DepthFromNoColor   DepthSource = "NO_COLOR"
	DepthFromConfig    DepthSource = "config theme.color_depth"
	DepthFromEnv       DepthSource = EnvColorDepth
	DepthFromColorTerm DepthSource = "COLORTERM"
	DepthFromTerm      DepthSource = "TERM"
	DepthFromTermDumb  DepthSource = "TERM is dumb or unset"
)

type Detection

type Detection struct {
	Caps
	WidthSource      WidthSource
	AppearanceSource AppearanceSource
	DepthSource      DepthSource
	TierSource       TierSource
	// Background is the terminal's measured background colour, when a rung
	// produced one. nil when the appearance was inferred from something
	// coarser than a colour, or defaulted.
	Background *paint.RGB
	// Multiplexer names tmux or screen when the session is inside one. §7.2
	// makes it the known-hard case for the background probe, and `doctor` is
	// required to say so and to recommend pinning appearance in config.
	Multiplexer string
	// NoColorForced records that NO_COLOR was present in the environment.
	// It is kept because NO_COLOR is the one signal a configured pin may not
	// override (§6.2 rung 1), so Apply has to know it was the reason.
	NoColorForced bool
}

Detection is Caps plus the provenance doctor needs to explain itself.

func Detect

func Detect(o Options) Detection

Detect walks every ladder §6 defines, as far as the given Options allow.

Config is the highest rung of every ladder (§6), and is layered on top of this result by the caller once a config file exists.

func (Detection) Apply

func (d Detection) Apply(o Override) Detection

Apply layers a configured pin over a detection result.

NO_COLOR is the exception to "config wins": it is absolute (§6.2 rung 1, §13.2), so a terminal that set it keeps `none` even against an explicit color_depth. Any other pin replaces the detected value and re-sources it, so doctor can still say which rung answered.

type DirEntry

type DirEntry struct {
	Name  string
	IsDir bool
}

DirEntry is the little of os.DirEntry this package needs. It is its own type so that a test can describe a Windows font directory on a Mac.

func OSReadDir

func OSReadDir(dir string) ([]DirEntry, error)

OSReadDir lists a real directory.

type Env

type Env func(key string) (string, bool)

Env is the environment the ladders read. Injecting it keeps detection pure and testable, and keeps the tests off the developer's real environment.

type Font

type Font struct {
	// Family is the name the user configured, as they spelled it.
	Family string
	// Path is the file it resolved to. Empty means the family is known but the
	// file is not — which §6.3 says to treat as `unicode`, not as `ascii`.
	Path string
	// Source names how the family was learned, for `doctor`.
	Source FontSource
}

Font is the font a terminal draws with.

type FontSource

type FontSource string

FontSource names the rung that produced a font family.

const (
	FontFromITerm      FontSource = "iTerm2 profile"
	FontFromKitty      FontSource = "kitty.conf"
	FontFromGhostty    FontSource = "ghostty config"
	FontFromAlacritty  FontSource = "alacritty.toml"
	FontFromWindows    FontSource = "Windows Terminal settings.json"
	FontFromFontconfig FontSource = "fc-match"
	FontFromDefault    FontSource = "platform default monospace"
)

type GlyphSupport

type GlyphSupport struct {
	Rune rune
	// Required marks the codepoints §6.3's table demands for the tier, as
	// opposed to the ones it merely prints.
	Required bool
	Present  bool
}

GlyphSupport is one codepoint's verdict, for `doctor`.

func Measure

func Measure(c Coverage, t Tier) []GlyphSupport

Measure reports the per-codepoint result for a tier against a font, which is what §6.3 requires `doctor` to print alongside the name of the font it tested.

type Options

type Options struct {
	Env  Env
	Home string
	// Cache stores the probed background colour (§6.1 rung 2). nil is a cache
	// that always misses.
	Cache Store
	// ReadFile reads a terminal's configuration file, and a font file. Injected
	// so the whole of §6.1 rung 4 — including F1 — and the whole of §6.3's font
	// resolution are testable on any OS without touching the developer's own
	// preferences or their installed fonts.
	ReadFile func(string) ([]byte, error)
	// ReadDir lists a font directory (§6.3). nil means the real filesystem.
	ReadDir func(string) ([]DirEntry, error)
	// GOOS is the platform whose ladders to walk. Empty means the one this
	// binary was built for. It is a field rather than a build tag because font
	// resolution differs per OS in ways worth testing from any OS (see
	// font.go).
	GOOS string
	// Run executes a helper — fc-match, and nothing else (§6.3, Linux). nil
	// means the real one (OSRun). It is only ever reached from MeasureTier,
	// which is not on render's path, so no render forks anything.
	Run func(timeout time.Duration, name string, args ...string) (string, error)
	// TTYWidth is §6.4 rung 4: the terminal this process is attached to, if
	// it has one. nil means the real ioctl on the controlling terminal, which
	// is all `render` can reach. A command that has a terminal of its own —
	// `doctor` — passes that instead, which is a strictly better answer and
	// is why the rung is a seam rather than a syscall in place.
	TTYWidth func() (int, bool)
}

Options is everything the ladders are allowed to consult.

Cache is the interesting one: it is how the background probe's answer reaches a process that has no terminal to probe with, which is every invocation of `render` (§5.1, §6.1 rung 2).

func (Options) MeasureTier

func (o Options) MeasureTier() (TierMeasurement, bool)

MeasureTier resolves the terminal's font and reads its cmap (§6.3).

It is deliberately *not* on `render`'s ladder. Like the OSC 11 probe it is the highest-fidelity signal on its axis and, like the OSC 11 probe, it costs far more than a render may spend — resolving a family means listing font directories and reading candidate files. Measured on the machine this was written on: 2.8 ms to reach Monaco through the iTerm profile, 1.8 ms for Menlo's 2 MB collection, against a 10 ms whole-render budget whose p99 is otherwise 84 µs (invariant 5). So the commands that have time to be thorough call this and CacheTier; `render` reads the cache. That is the same split §6.1 rung 2 already makes, for the same reason.

func (Options) ResolveFont

func (o Options) ResolveFont() (Font, bool)

ResolveFont walks §6.3's resolution ladder for the running platform.

type Override

type Override struct {
	Appearance string
	Depth      string
	Tier       string
	Width      int
}

Override is a configured pin for one or more axes. §6 is explicit that "every axis can be pinned in config; detection is a default, never a cage", so a pin here outranks every probe below it — with one exception, recorded in Apply.

An empty string or a zero width means "auto": no pin, leave detection's answer standing.

type Store

type Store interface {
	Get(key string, ttl time.Duration) ([]byte, bool)
	Put(key string, data []byte) error
}

Store is the cache this package needs: exactly two methods, declared here rather than imported, because internal/cache already depends on this package for its environment reader. A nil Store is a cache that always misses.

type Tier

type Tier string

Tier is glyph availability, measured rather than assumed (§6.3).

const (
	ASCII    Tier = "ascii"
	Unicode  Tier = "unicode"
	Extended Tier = "extended"
	NerdFont Tier = "nerdfont"
)

func ParseTier

func ParseTier(s string) (Tier, bool)

ParseTier accepts the spellings §8's `glyphs` uses.

func TierFor

func TierFor(c Coverage) Tier

TierFor is the best tier a measured font supports.

An empty coverage — an unresolved font, an unreadable file, a format this parser does not read — yields `unicode`, not `ascii`. §6.3 is explicit that this is the fallback: the three characters `unicode` needs are present in effectively every monospace font shipped in thirty years, so assuming them is safe in a way that assuming eighth-blocks is not, and dropping a terminal that merely failed to *resolve* its font down to `#` and `=` would be a punishment for a missing measurement rather than a missing glyph.

func Tiers

func Tiers() []Tier

Tiers is every tier, from the most conservative to the most demanding. The order is the search order: TierFor walks it backwards and takes the first tier the font satisfies.

type TierMeasurement

type TierMeasurement struct {
	Font     Font
	Coverage Coverage
	Tier     Tier
}

TierMeasurement is a completed font measurement: which font was read, and what it can draw. It is the value `doctor` reports and the value the cache carries between the command that measured and the `render` that consumes it.

type TierSource

type TierSource string

TierSource names the rung of §6.3's ladder that answered.

const (
	TierFromConfig  TierSource = "config theme.glyphs"
	TierFromEnv     TierSource = EnvGlyphs
	TierFromFont    TierSource = "font measurement (cached)"
	TierFromTerm    TierSource = "TERM"
	TierFromDefault TierSource = "default"
)

type WidthSource

type WidthSource string

WidthSource names the rung of §6.4's ladder that answered. doctor reports it, because a wrong width is the difference between an elegant line and a wrapped one.

const (
	WidthFromConfig  WidthSource = "config layout.width"
	WidthFromEnv     WidthSource = EnvWidth
	WidthFromColumns WidthSource = "COLUMNS"
	WidthFromIoctl   WidthSource = "ioctl(TIOCGWINSZ)"
	WidthFromDefault WidthSource = "default"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL