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
- Variables
- func BackgroundKey(env Env) string
- func CacheBackground(store Store, env Env, c paint.RGB) error
- func CacheTier(store Store, env Env, t Tier) error
- func EnvKeys() []string
- func FamilyOf(data []byte) (string, bool)
- func Luma(c paint.RGB) float64
- func Multiplexer(env Env) (string, bool)
- func OSEnv(key string) (string, bool)
- func OSRun(timeout time.Duration, name string, args ...string) (string, error)
- func ProbeBackground(timeout time.Duration) (paint.RGB, error)
- func TTYWidthOf(f *os.File) (int, bool)
- func TierKey(env Env) string
- func TierProbes(t Tier) []rune
- func TierRequires(t Tier) []rune
- type Appearance
- type AppearanceSource
- type Caps
- type Coverage
- type Depth
- type DepthSource
- type Detection
- type DirEntry
- type Env
- type Font
- type FontSource
- type GlyphSupport
- type Options
- type Override
- type Store
- type Tier
- type TierMeasurement
- type TierSource
- type WidthSource
Constants ¶
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.
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.
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.
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.
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 ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Luma is the relative-luminance weighting applied to gamma-encoded channels, scaled to 0..1.
func Multiplexer ¶
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 OSRun ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
ParseFont reads a TrueType/OpenType font — or the first face of a collection — and returns the codepoints it draws.
func (Coverage) Count ¶
Count is how many codepoints the font draws. It exists for `doctor`, which says which font it tested and how big its repertoire is.
type Depth ¶
type Depth string
Depth is the colour resolution the terminal can render (§6.2).
func ParseDepth ¶
ParseDepth accepts the spellings §8's `color_depth` uses. "auto" and any unrecognised value leave the caller's default in place.
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 ¶
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 ¶
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 ¶
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.
type Env ¶
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 ¶
ResolveFont walks §6.3's resolution ladder for the running platform.
type Override ¶
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).
func TierFor ¶
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.
type TierMeasurement ¶
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" )