theme

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 3, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package theme defines the open-ended, capability-aware theming primitives used by Nabat and its extensions.

A Theme is a struct that carries one or more Palette entries (one per declared variant), a default variant selector, and cross-variant defaults (list enumerator, table border). Theme.Resolve picks a variant based on Capabilities and returns an immutable ResolvedTheme that consumers query by Token or by accessor.

The package has no dependency on the nabat root package or on [nabat.IOStreams]. Extensions can import it directly to read styles from a resolved theme without pulling in command, IO, or option machinery.

See the package-level docs and docs/themes.md for the full design.

Index

Examples

Constants

View Source
const (
	// Default is the theme installed when the caller does not pass
	// nabat.WithTheme. Capability-aware: defers to the terminal's
	// detected color profile and background luminance.
	Default = "default"

	// Minimal is a low-color theme that relies on bold instead of
	// foreground colors. It declares variant=notty so the framework
	// disables chroma syntax highlighting and forces glamour into
	// plain-text mode, matching pipe-friendly defaults.
	Minimal = "minimal"

	// Charm is the higher-contrast palette aligned with Charm.land
	// defaults, suitable for dark terminals.
	Charm = "charm"

	// Dracula ships Dracula Classic (dark) and Alucard Classic (light)
	// per draculatheme.com/spec; pickVariant selects by terminal
	// luminance. Dark uses chroma/glamour/huh "dracula"; light uses
	// chroma "github", glamour "light", and token-derived prompts.
	Dracula = "dracula"

	// Gruvbox is the retro groove palette from morhetz/gruvbox; dark and
	// light variants switch via pickVariant. Pairs with chroma's "gruvbox"
	// and "gruvbox-light" styles.
	Gruvbox = "gruvbox"

	// CatppuccinLatte is the Catppuccin Latte palette for light
	// backgrounds. Pairs with chroma's "catppuccin-latte" style.
	CatppuccinLatte = "catppuccin-latte"

	// CatppuccinFrappe is the Catppuccin Frappé palette for dark
	// backgrounds. Pairs with chroma's "catppuccin-frappe" style.
	CatppuccinFrappe = "catppuccin-frappe"

	// CatppuccinMacchiato is the Catppuccin Macchiato palette for dark
	// backgrounds. Pairs with chroma's "catppuccin-macchiato" style.
	CatppuccinMacchiato = "catppuccin-macchiato"

	// CatppuccinMocha is the Catppuccin Mocha palette for dark
	// backgrounds. Pairs with chroma's "catppuccin-mocha" style.
	CatppuccinMocha = "catppuccin-mocha"

	// Nabat is the brand palette: warm Persian rock-candy tones
	// (saffron, pistachio, pomegranate, turquoise). It ships with a
	// matching framework-owned chroma style and huh adapter, both
	// referenced by name from the manifest.
	Nabat = "nabat"

	// Nord is the Nord palette (Polar Night, Snow Storm, Frost,
	// Aurora) for dark backgrounds. Pairs with chroma's "nord" style.
	Nord = "nord"

	// Solarized is the canonical Solarized palette (base tones plus eight
	// accents). Dark/light variants follow https://ethanschoonover.com/solarized/;
	// pickVariant selects by terminal luminance. Pairs with chroma's
	// "solarized-dark" / "solarized-light" styles.
	Solarized = "solarized"
)

Untyped string constants for the well-known theme names shipped with Nabat. Use them as the argument to nabat.WithTheme to get IDE autocomplete and a compile-time check that the spelling matches an embedded manifest:

app, _ := nabat.New("myctl", nabat.WithTheme(theme.Dracula))

These constants are untyped on purpose so they compose with strings from other sources (env vars, flags, user config) without explicit conversions:

name := os.Getenv("MYCTL_THEME")
if name == "" {
    name = theme.Default
}
app, _ := nabat.New("myctl", nabat.WithTheme(name))

Every constant here corresponds to an embedded data/<name>.json file; TestConstsHaveManifests in catalog_test.go enforces that the two stay in lockstep so a misspelled or missing manifest fails the build, not nabat.New at runtime.

Variables

View Source
var DefaultAliases = map[Token]Token{

	ListEnumerator: TextMuted,
	TreeEnumerator: ListEnumerator,
	TableBorder:    TextMuted,

	ListItem:    TextPrimary,
	TreeItem:    TextPrimary,
	TableCell:   TextPrimary,
	TableHeader: TextTitle,
}

DefaultAliases is the framework-owned fall-through map consulted by ResolvedTheme.Style when a Token has no direct entry in the resolved Palette. The chain is: lookup t -> if missing, try DefaultAliases[t] -> if missing, try DefaultAliases[that] -> ... When the chain bottoms out without a hit, ResolvedTheme.Style returns the zero lipgloss.Style (terminal default), preserving the existing "unset token = terminal default" contract.

The defaults are chosen so the most common authoring pattern — "I picked four status colors, two text colors, and one muted color, and that's all I want to think about" — produces a fully-themed CLI without forcing every theme to repeat the same "tree.enumerator -> muted" / "table.border -> muted" mappings.

Per-theme Palette.Aliases overrides any entry here; cycles are detected at ResolvedTheme.Style time and treated as "stop here, return the zero style".

Functions

func All

func All() map[string]Theme

All returns a defensive copy of the registry, parsing every manifest on the way out. It is intended for tools that want to iterate the catalog (for example a docs generator rendering one section per theme); callers may mutate the returned map without affecting the registry.

Manifest parse errors panic — the catalog is part of the binary, and a tool walking every theme cannot meaningfully recover from a half-broken catalog. Callers that want graceful per-theme error handling should iterate Names and call Get in a loop instead.

func ChromaFromTokens

func ChromaFromTokens(name string, tokens map[Token]lipgloss.Style) *chroma.Style

ChromaFromTokens derives a chroma style from semantic tokens.

func ChromaPreset

func ChromaPreset(v Variant) string

ChromaPreset picks the upstream chroma style name that fits the given variant. It is the fallback Theme.Resolve applies when Palette.Chroma is nil and Palette.ChromaName is empty.

Mapping:

  • VariantDark -> "monokai" (lines up with Charm's defaults).
  • VariantLight -> "github" (high-contrast on light terminals).
  • VariantNoTTY -> "" (chroma falls back to no styling).

An empty return tells the caller to leave the chroma slot empty and let chroma's own default kick in at render time.

func GlamourFromTokens

func GlamourFromTokens(tokens map[Token]lipgloss.Style, base *ansi.StyleConfig) *ansi.StyleConfig

GlamourFromTokens derives a glamour style from semantic tokens. The base style is cloned so callers can pass one of glamour's built-in dark/light/notty presets and keep untouched slots intact.

func GlamourPreset

func GlamourPreset(v Variant, c Capabilities) string

GlamourPreset picks the upstream glamour style name that fits the given variant + capabilities combination. It is the fallback Theme.Resolve applies when both Palette.Glamour and Palette.GlamourFor are nil and Palette.GlamourName is empty.

Mapping:

The catalog and programmatic themes get the same defaults out of this one helper.

func HuhFromTokens

func HuhFromTokens(tokens map[Token]lipgloss.Style) huh.Theme

HuhFromTokens derives a huh.Theme from a per-token style map. It is a thin wrapper around PromptFromTokens for callers that want the legacy "give me a huh.Theme directly" entry point; Theme.Resolve uses PromptFromTokens + Prompt.Huh internally so the two paths produce identical output.

New code should reach for PromptFromTokens (which returns the inspectable Nabat-native Prompt value) when possible. HuhFromTokens stays for ergonomic compatibility with consumers that already hold a token map and want a huh.Theme out the other end.

func Names

func Names() []string

Names returns the registered theme names in lexical order. It is suitable for shell completion, documentation, and the "available: [...]" segment of error messages built elsewhere.

Example

ExampleNames demonstrates that Names returns themes in lexical order, which is what callers typically want for completion lists.

package main

import (
	"fmt"

	"nabat.dev/theme"
)

func main() {
	names := theme.Names()
	for i := 1; i < len(names); i++ {
		if names[i] < names[i-1] {
			fmt.Println("unsorted")
			return
		}
	}
	fmt.Println("sorted")
}
Output:
sorted

func Schema

func Schema() []byte

Schema returns the embedded JSON Schema describing the theme manifest format. Tools that need to validate manifests (a future `nabat themes validate` subcommand, a static-site generator, etc.) can serve these bytes directly without re-hosting.

The returned slice is a fresh copy on each call; callers may mutate it without affecting subsequent calls.

Types

type Capabilities

type Capabilities struct {
	// Dark reports whether the terminal background is dark. Themes use it
	// to pick foreground hex values that contrast appropriately and to
	// pick between glamour's "dark" and "light" presets.
	Dark bool

	// BackgroundHex is the exact terminal background color when the
	// detector could read it (typically via the OSC 11 query). Empty
	// when the detector could not run (non-TTY output, redirected
	// input, custom IO bundle). Themes that want to adapt to the
	// real background — say, a manifest tuned for a specific
	// off-white — branch on this value; themes that only need
	// "darkish vs lightish" stick with [Dark].
	BackgroundHex string

	// Profile is the active [colorprofile.Profile] for the primary output
	// stream. Themes use it to fall back from full-color hex values to
	// ANSI16/ANSI256 indexes, and to switch glamour to "notty" when the
	// stream is plain text.
	Profile colorprofile.Profile

	// Interactive reports whether the primary output stream is a TTY and
	// the input stream allows prompting. Themes that animate or use
	// background color blocks check this so non-interactive output stays
	// pipe-friendly.
	Interactive bool

	// Width is the terminal width in cells, or 0 when the framework
	// could not measure it (non-TTY output, sandboxed test bundle).
	// Themes use it for table / tree decisions; the framework's own
	// table layout reads it through the IO bundle.
	Width int

	// Hyperlinks reports whether the terminal supports OSC 8
	// hyperlinks. Themes / extensions that emit clickable URLs gate
	// the escape-sequence emission on this flag so they degrade to
	// raw URL text in unsupported terminals (most CI runners,
	// minimal SSH targets, etc.).
	Hyperlinks bool

	// Unicode reports the terminal's Unicode capability tier. Themes
	// pick enumerator characters (•, -, └─) and prefix glyphs based
	// on this; sticking to ASCII when the terminal cannot render
	// box-drawing characters keeps the output readable on legacy
	// Windows consoles and minimal CI environments.
	Unicode UnicodeLevel

	// ReducedMotion reports whether the framework should suppress
	// animations (spinners, progress sweeps, transition effects).
	// Set when the env signals "no motion please" via NO_MOTION,
	// REDUCE_MOTION, or similar accessibility flags. Consumers that
	// run animations check this and substitute a static rendering.
	ReducedMotion bool
}

Capabilities describes the rendering surface a Theme is being resolved for. Themes branch on these fields to pick capability-aware colors, fall back to plain text when colors are unavailable, and choose the right glamour preset for the current background luminance.

Capabilities is a plain, constructible struct on purpose: the theme/ package is a leaf with no IOStreams dependency, so tests build a Capabilities value directly to exercise theme branches without having to stand up an IO bundle. The nabat root package owns capability detection and produces a populated Capabilities value at App.finalize time.

Phase 9 widened the struct beyond the original three fields with [Width], [BackgroundHex], [Hyperlinks], [Unicode], and [ReducedMotion] so theme recipes and consumers can branch on the terminal facts that matter beyond just dark / light. Detection defaults remain conservative: in doubt, the framework reports the safer (less-feature) value.

type Metadata

type Metadata struct {
	// Name is the manifest's "name" field, identical to the registry
	// key used by [Get].
	Name string

	// Description is the manifest's "description" field. Empty when
	// the manifest omitted it.
	Description string

	// Default is the manifest's "default" field — the variant
	// [Theme.Resolve] falls back to when capabilities don't pin a
	// pick. Empty for single-variant themes.
	Default string

	// Variants is the sorted list of variant keys this manifest
	// declares ("dark" / "light" / "notty"). Always non-empty (the
	// schema requires at least one entry).
	Variants []string

	// TokenNames is the sorted, deduplicated set of token paths any
	// variant declares under its "tokens" map. Tools surfacing
	// "what does this theme cover?" use this without having to
	// re-aggregate per variant.
	TokenNames []string
}

Metadata describes a built-in manifest without invoking Theme.Resolve. A future `nabat themes list` subcommand and any documentation generator (or IDE plugin) can introspect what a theme advertises — its name, default variant, the set of declared variants, and the set of tokens any variant covers — by calling Manifest rather than resolving the theme against a fabricated Capabilities.

Metadata is read-only and constructed fresh on each Manifest call; mutating the returned value (for example sorting TokenNames or Variants in place) is safe and does not affect the registry.

func Manifest

func Manifest(name string) (Metadata, error)

Manifest returns the Metadata for a built-in theme without invoking the recipe. Use it in tooling — `nabat themes list`, shell-completion descriptions, documentation generators — that needs to inspect what a theme advertises rather than apply it.

The error mirrors Get: an unknown name returns an actionable "available: [...]" listing so CLI users can recover from typos without consulting the docs.

The returned Metadata is freshly constructed on each call; callers may sort or modify TokenNames in place without affecting subsequent calls or the registry. The underlying decode is memoized internally so repeated calls (e.g. from a `themes list` subcommand iterating every name) do not re-parse the embedded JSON.

type Override

type Override interface {
	// contains filtered or unexported methods
}

Override is a per-Palette mutation produced by the Set* helpers in this file. The framework applies overrides to every variant of the underlying Theme so a one-line "make status.error magenta" affects whichever variant Theme.Resolve picks at runtime.

Override is an interface (not a function type) so the Set* helpers can return concrete typed values that test assertions match against without reflection. Third-party code rarely implements Override directly; reach for the Set* helpers (and, for [App] users, the nabat.WithThemeOverride option) instead.

func SetAlias

func SetAlias(tok, target Token) Override

SetAlias returns an Override that records src as the fall-through target for tok in the palette's Palette.Aliases map. Pass an empty target to disable an alias (matches the Palette.Aliases "empty value disables the framework default" semantics).

func SetChroma

func SetChroma(s *chroma.Style) Override

SetChroma returns an Override that swaps in a different owned *chroma.Style for syntax highlighting. The override clears Palette.ChromaName so the resulting cascade resolves through the explicit pointer alone — no risk of the registered name silently winning back when the override is dropped later.

func SetChromaName

func SetChromaName(name string) Override

SetChromaName returns an Override that switches the upstream chroma style name. Like SetChroma it clears the sibling field (in this case Palette.Chroma) so the cascade picks the named preset deterministically.

func SetGlamour

func SetGlamour(s *ansi.StyleConfig) Override

SetGlamour returns an Override that swaps in a different owned *ansi.StyleConfig for markdown rendering. The override clears Palette.GlamourName and Palette.GlamourFor so the resulting cascade resolves through the explicit pointer alone.

func SetGlamourName

func SetGlamourName(name string) Override

SetGlamourName returns an Override that switches the upstream glamour preset name. Clears the sibling owned-style and factory fields so the cascade picks the named preset deterministically.

func SetHuh

func SetHuh(h huh.Theme) Override

SetHuh returns an Override that swaps in a different huh.Theme for interactive prompts. Setting nil reverts to the HuhFromTokens fallback at Theme.Resolve time.

func SetToken

func SetToken(t Token, s lipgloss.Style) Override

SetToken returns an Override that records s under token t, shadowing any value the underlying Palette already carries. Overrides apply to every variant of the underlying Theme; pair with the multi-variant resolution that Theme.Resolve performs to keep "tweak this one slot" trivial regardless of how many variants the theme declares.

Use case: a downstream app that picks the bundled Dracula theme but wants its own brand color for the error status. One line at nabat.New is enough; the Theme value stays declarative.

type Palette

type Palette struct {
	// Tokens is the per-token style map. Lookups in
	// [ResolvedTheme.Style] hit this map first; tokens the
	// palette omits fall through to the alias chain (per-palette
	// [Aliases] overlaid on [DefaultAliases]) before defaulting to
	// the zero [lipgloss.Style] (terminal default).
	Tokens map[Token]lipgloss.Style

	// Aliases overrides entries in [DefaultAliases] for this
	// palette. A non-empty mapping replaces the framework default;
	// an explicit empty Token value (Aliases[X] = "") disables the
	// framework default for that key without substituting another.
	//
	// Typical use: the manifest's "aliases" field, where a theme
	// author wants list bullets to follow text.secondary instead of
	// text.muted. Most themes leave this nil and inherit the
	// framework defaults wholesale.
	Aliases map[Token]Token

	// Chroma is an owned [*chroma.Style] for syntax highlighting.
	// When non-nil it wins over [Palette.ChromaName]; when both are
	// zero the palette's variant determines the framework default
	// via [ChromaPreset].
	Chroma *chroma.Style

	// ChromaName is the upstream chroma style name (e.g. "monokai",
	// "dracula"). Used when [Palette.Chroma] is nil. Unknown names
	// fall through to chroma's own default at render time.
	ChromaName string

	// Glamour is an owned [*ansi.StyleConfig] for markdown
	// rendering. When non-nil it wins over both
	// [Palette.GlamourName] and [Palette.GlamourFor].
	Glamour *ansi.StyleConfig

	// GlamourName is the upstream glamour preset name (e.g. "dark",
	// "light", "notty"). Used when both Glamour and GlamourFor are
	// nil; unknown names fall through to glamour's own default.
	GlamourName string

	// GlamourFor is the capability-aware factory used by themes
	// (typically the manifest loader's inline "glamourStyle" path)
	// that need to evaluate glamour against the current
	// [Capabilities]. Called by [Theme.Resolve] when both Glamour
	// and GlamourName are zero. The function may return an error
	// that surfaces from [Theme.Resolve].
	GlamourFor func(Capabilities) (*ansi.StyleConfig, error)

	// Prompt is the Nabat-native style block for interactive
	// prompts. The framework converts it to a [huh.Theme] at
	// [Theme.Resolve] time. Zero (the empty Prompt) means the
	// catalog falls back to [PromptFromTokens] using
	// [Palette.Tokens]; setting any field opts into the closed
	// Nabat-native surface and forgoes the framework default.
	//
	// [Palette.Huh] still wins over [Prompt] when set — that's
	// the escape hatch for themes that need huh's full surface.
	Prompt Prompt

	// Huh is the [huh.Theme] used by interactive prompts. When
	// non-nil it wins outright over [Prompt] and the
	// [PromptFromTokens] fallback. The escape hatch is for themes
	// that need huh's full per-state surface (separate focused /
	// blurred styling, custom textInput layout, etc.) — the
	// closed Nabat-native [Prompt] cannot express those.
	Huh huh.Theme
}

Palette is the per-variant style data a Theme carries. Each declared variant maps to one Palette; Theme.Resolve picks one and fills any nil/empty cascade slot with framework defaults.

The chroma and glamour cascades are intentionally three-way (owned value > registered name > capability default) because both upstream libraries support either form, and themes commonly mix them — for example "use the registered Dracula chroma style but ship a custom glamour config". The cascade is folded into a single value at Theme.Resolve time so ResolvedTheme consumers see only the resolved result, not the source path.

type Prompt

type Prompt struct {
	// Title is the group / section title shown above prompts.
	Title lipgloss.Style

	// Description is the explanatory text rendered under each
	// prompt.
	Description lipgloss.Style

	// Cursor styles the text-input cursor.
	Cursor lipgloss.Style

	// Placeholder styles the text-input placeholder copy.
	Placeholder lipgloss.Style

	// SelectedOption styles the currently-selected list item.
	SelectedOption lipgloss.Style

	// UnselectedOption styles the items not currently selected.
	UnselectedOption lipgloss.Style

	// SelectedPrefix styles the marker drawn next to the selected
	// item (often "✓ " or "● ").
	SelectedPrefix lipgloss.Style

	// UnselectedPrefix styles the marker drawn next to non-selected
	// items (often "  " or "○ ").
	UnselectedPrefix lipgloss.Style

	// Error styles error indicators and messages.
	Error lipgloss.Style

	// Help styles the keybind footer text.
	Help lipgloss.Style

	// Selector styles the active-row indicator and navigation
	// arrows (next / prev).
	Selector lipgloss.Style

	// ButtonFocused styles the focused submit / next button.
	ButtonFocused lipgloss.Style

	// ButtonBlurred styles the inactive button.
	ButtonBlurred lipgloss.Style

	// Border applies as the form / card border. The zero
	// [lipgloss.Border] leaves huh's base border untouched.
	Border lipgloss.Border
}

Prompt is the Nabat-native style block for interactive prompts. It is a closed enum of the slots the framework cares about — closed meaning the schema and the manifest authoring experience never have to track the upstream huh.Styles struct shape.

Each field is a lipgloss.Style applied to one prompt slot. Zero (the default lipgloss.Style) means "inherit huh's base styling for this slot" — Prompt overlays only the slots the author opted into.

Authors who need huh's full surface drop into the programmatic path with a huh.Theme of their own, set via Palette.Huh directly. That escape hatch wins outright when present, so the closed Prompt surface and the open huh.Theme escape hatch coexist without confusion.

Fields that take a literal text payload (the prefix-style markers) use lipgloss.Style.SetString to inject the literal in addition to the visual style — `theme.SetString` style call sites work the same way they did in the old huhStyle path.

func PromptFromTokens

func PromptFromTokens(tokens map[Token]lipgloss.Style) Prompt

PromptFromTokens derives a Prompt from a per-token style map. It is the fallback the catalog applies when a Palette declares neither Palette.Prompt nor Palette.Huh, so every theme — even bare-bones programmatic ones that only declare token colors — gets a usable interactive surface for free, themed in the same colors as the rest of the CLI.

The mapping is intentionally narrow and stable; new prompt slots added to Prompt in the future should map here too so the "tokens are enough" promise keeps holding.

func (Prompt) Huh

func (p Prompt) Huh() huh.Theme

Huh returns a huh.Theme derived from this Prompt. The theme starts from huh.ThemeBase and overlays the supplied fields onto the slots huh exposes. Zero-style fields inherit huh's base styling.

The mapping mirrors the curated set in PromptFromTokens — the Focused fields users see most (titles, errors, selection indicators, prefixes, text input), the Group surface, and the Help footer. The Blurred mirror reuses the focused style for stable cross-state appearance; authors who want different blurred styling drop into Palette.Huh directly.

Border applies to Focused.Base and Focused.Card when non-zero, then mirrors onto Blurred.Base / Blurred.Card so the form chrome stays consistent.

func (Prompt) IsZero

func (p Prompt) IsZero() bool

IsZero reports whether p has any field set. The framework uses it to detect "this palette did not declare a prompt" so the catalog can fall back to PromptFromTokens without having to special-case nil.

A Prompt with only [Border] set is non-zero; a Prompt with only the zero lipgloss.Border but every style field set is also non-zero. The zero Prompt — every style empty AND the zero border — is the only "unset" value.

lipgloss.Style is not comparable (it carries slices internally), so the check inspects every field through styleIsZero rather than using `p == Prompt{}`.

type PromptKnobs

type PromptKnobs struct {
	// SelectedPrefix is the literal prefix rendered before selected
	// options (for example "✓ ").
	SelectedPrefix string

	// UnselectedPrefix is the literal prefix rendered before
	// unselected options (for example "  ").
	UnselectedPrefix string

	// Border is the form/card border to apply to prompt output. The
	// zero [lipgloss.Border] leaves the prompt border unchanged.
	Border lipgloss.Border
}

PromptKnobs carries non-color prompt settings that are convenient to share across variants.

func (PromptKnobs) Apply

func (k PromptKnobs) Apply(p Prompt) Prompt

Apply overlays non-zero knobs from k onto p and returns the result.

func (PromptKnobs) IsZero

func (k PromptKnobs) IsZero() bool

IsZero reports whether k leaves every prompt knob unset.

type Requirement

type Requirement struct {
	Consumer string
	Tokens   []Token
}

Requirement declares the Token set a consumer reads from a ResolvedTheme. Extensions and the framework itself produce Requirement values so the app can detect, at construction time, when an installed theme is missing tokens a consumer needs.

The Consumer field identifies who needs the tokens (typically the extension name, "logging extension", or "core help renderer") and is folded into the diagnostic message so users can act on the signal: "set these tokens in your manifest, or pick a different theme."

Requirement is plain data; pass it by value. The framework treats the Tokens slice as read-only.

func CoreRequirements

func CoreRequirements() []Requirement

CoreRequirements returns the Requirement entries the nabat root package's own output / help / structured paths read from a ResolvedTheme. The framework includes this list automatically when validating; extensions only need to declare the tokens they add on top.

Adding a new core consumer (a new Status*, Text*, etc.) means adding to the right Requirement here so missing-token diagnostics stay accurate.

func Require

func Require(consumer string, tokens ...Token) Requirement

Require returns a Requirement for the supplied consumer name and the tokens it reads. It is the canonical constructor; using a struct literal works too but Require keeps call sites tidy:

func (e *Extension) ThemeRequires() theme.Requirement {
    return theme.Require("logging extension",
        theme.StatusInfo, theme.StatusWarning, theme.StatusError,
        theme.AccentPrimary, theme.TextPrimary,
    )
}

type ResolvedTheme

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

ResolvedTheme is the final, immutable result of resolving a Theme against a Capabilities snapshot via Theme.Resolve. Consumers (the nabat root package, the logging adapter, third-party extensions) hold a ResolvedTheme and query it by Token or by accessor.

ResolvedTheme is safe for concurrent use after construction; nothing on it mutates after Theme.Resolve returns. Multiple goroutines may call any accessor on the same ResolvedTheme without synchronization. Slice and map results are returned as defensive copies (e.g. ResolvedTheme.Tokens) or are read-only data shared by design (e.g. the embedded chroma.Style pointer).

The zero value of ResolvedTheme is the "empty" theme: every Style call returns the zero lipgloss.Style (which lipgloss renders as the terminal default), [Chroma] and [Glamour] return nil (consumers fall through to the upstream library defaults), and huh uses its own defaults. App.New produces a non-zero ResolvedTheme even when the user supplies no theme — the empty result here is a fallback for tests and for code that constructs a ResolvedTheme directly.

Phase 3 of the theme redesign collapsed the three-accessor owned/named/cascade pattern into single accessors per integration: [Chroma], [Glamour], [Huh]. The cascade is folded once at Theme.Resolve time, so callers see one already-resolved value without having to re-implement the precedence rule.

func (ResolvedTheme) CheckRequirements

func (r ResolvedTheme) CheckRequirements(reqs []Requirement) error

CheckRequirements applies every Requirement in reqs to this ResolvedTheme and returns one error per consumer whose tokens are not fully covered. The errors are joined; nil means every consumer's requirement is satisfied.

Diagnostic format:

theme "minimal" is missing tokens required by:
  - logging extension: status.info, status.warning
  - core help renderer: text.title

The framework calls CheckRequirements at App.finalize time. The resulting error either blocks construction (when the strict mode is on) or is rendered as a warning to stderr (the default).

func (ResolvedTheme) Chroma

func (r ResolvedTheme) Chroma() *chroma.Style

Chroma returns the resolved *chroma.Style for syntax highlighting. Theme.Resolve folds the owned-style / named-preset / variant- default cascade into this single value at construction time so consumers (markdown highlighter, structured logger, third-party extensions) never have to rebuild the precedence rule. nil means "no syntax highlighting" — the upstream chroma fallback applies at render time.

func (ResolvedTheme) Glamour

func (r ResolvedTheme) Glamour() *ansi.StyleConfig

Glamour returns the resolved *ansi.StyleConfig for markdown rendering. Theme.Resolve folds the owned-style / capability-aware- factory / named-preset / capability-default cascade into this single value at construction time. nil means "use glamour's own default styling at render time".

func (ResolvedTheme) HasToken

func (r ResolvedTheme) HasToken(t Token) bool

HasToken reports whether token t resolves to a non-zero lipgloss.Style on this ResolvedTheme — either directly via Palette.Tokens or transitively through the alias chain. It is the predicate the framework uses for requirement validation; consumers querying styles still call ResolvedTheme.Style, which returns the zero style on a miss.

HasToken treats only "set to a non-zero style" as covered. A token explicitly set to the zero lipgloss.Style is reported as covered because the manifest author opted in (the only way to land a zero style is an explicit empty styleSpec).

func (ResolvedTheme) Huh

func (r ResolvedTheme) Huh() huh.Theme

Huh returns the huh.Theme from Palette.Huh (or the HuhFromTokens fallback derived from the palette's tokens when that field was nil). Consumers pass the result to form.WithTheme or spinner.New().Theme.

func (ResolvedTheme) ListEnumerator

func (r ResolvedTheme) ListEnumerator() list.Enumerator

ListEnumerator returns the default list enumerator. The zero ResolvedTheme returns nil, which Context.List interprets as the lipgloss default (list.Bullet); Theme.Resolve always produces a non-nil value.

func (ResolvedTheme) MissingTokens

func (r ResolvedTheme) MissingTokens(req Requirement) []Token

MissingTokens returns the tokens from req that this ResolvedTheme does not cover (neither directly nor via the alias chain). The result is sorted lexically so error messages are deterministic.

An empty return means the requirement is fully satisfied.

func (ResolvedTheme) Name

func (r ResolvedTheme) Name() string

Name returns the theme name from Theme.Name (or the registry parser for manifest-defined themes). The empty string indicates a nameless theme — typically the zero ResolvedTheme used as a fallback.

func (ResolvedTheme) Style

func (r ResolvedTheme) Style(t Token) lipgloss.Style

Style returns the lipgloss.Style registered for token t, walking the alias chain when t has no direct entry. The chain is the merge of DefaultAliases and the resolved palette's Palette.Aliases; Theme.Resolve folds them at construction time so [Style] never touches the package-level default map.

Lookup order: tokens[t] -> tokens[aliases[t]] -> tokens[aliases[aliases[t]]] -> ... When the chain bottoms out (no direct entry, no further alias) Style returns the zero lipgloss.Style — lipgloss renders it as the terminal default, which is the right fallback for "this theme didn't customize this slot".

Cycle safety: Style maintains a per-call seen set so a malformed alias chain (one that loops on itself) returns the zero style instead of looping forever. Theme.Resolve also validates aliases at construction time, so a cycle is detected and reported there before [Style] ever sees it.

func (ResolvedTheme) TableBorder

func (r ResolvedTheme) TableBorder() lipgloss.Border

TableBorder returns the default table border. The zero ResolvedTheme returns the zero lipgloss.Border (no characters drawn); Theme.Resolve always produces a usable border (lipgloss.NormalBorder when none was set).

func (ResolvedTheme) Tokens

func (r ResolvedTheme) Tokens() []Token

Tokens returns the set of tokens this theme has explicitly set. The returned slice is freshly allocated; callers may sort or modify it without affecting the theme. Tokens is intended for diagnostics (printing what a theme covers) and for extensions that want to skip tokens the theme did not opt into.

func (ResolvedTheme) Variant

func (r ResolvedTheme) Variant() Variant

Variant returns the variant Theme.Resolve picked. The zero value VariantUnset means the source theme made no declaration; consumers should treat it as compatible with every Capabilities snapshot.

Use Variant for advisory diagnostics (e.g. logging when a dark theme is paired with what looks like a light terminal); do NOT short-circuit theme application based on it — Theme.Resolve has already picked a palette compatible with the supplied capabilities.

type Resolver

type Resolver interface {
	Resolve(Capabilities) ResolvedTheme
}

Resolver is the escape hatch for themes whose palette choice depends on runtime Capabilities in a way that cannot be expressed as "one Palette per Variant". Most themes (every built-in, every straight programmatic theme) declare one Palette per variant and let Theme.Resolve pick. The rare cases — for example a theme that switches palettes when the color profile is ANSI16 vs TrueColor — implement Resolver directly and bypass Theme entirely.

Theme implements Resolver via its Theme.Resolve method, so a Theme value can be passed anywhere a Resolver is expected.

Example

ExampleResolver shows the escape hatch for themes that need to pick a palette based on runtime Capabilities in a way one Palette per Variant cannot express. Most themes never need this — the built-in catalog and every straight programmatic theme satisfies Resolver via the Theme.Resolve method that comes with the struct.

package main

import (
	"fmt"

	"charm.land/lipgloss/v2"

	"nabat.dev/theme"
)

func main() {
	r := capabilityAwareResolver{}
	rt := r.Resolve(theme.Capabilities{Dark: false, Interactive: true})
	fmt.Println(rt.Style(theme.StatusError).GetBold())
}

// capabilityAwareResolver picks a palette by examining capabilities at
// resolve time, branching on the Profile field in a way one Palette
// per Variant cannot express directly.
type capabilityAwareResolver struct{}

func (capabilityAwareResolver) Resolve(c theme.Capabilities) theme.ResolvedTheme {
	fg := lipgloss.Color("#E05454")
	if !c.Dark {
		fg = lipgloss.Color("#A03030")
	}
	t := theme.Theme{
		Name: "capability-aware",
		Variants: map[theme.Variant]theme.Palette{
			theme.VariantUnset: {
				Tokens: map[theme.Token]lipgloss.Style{
					theme.StatusError: lipgloss.NewStyle().Foreground(fg).Bold(true),
				},
			},
		},
	}
	return t.Resolve(c)
}
Output:
true

type Theme

type Theme struct {
	// Name identifies the theme in error messages, the catalog
	// registry key, and [ResolvedTheme.Name]. Required for built-in
	// themes; programmatic themes may leave it empty.
	Name string

	// Variants maps each declared [Variant] to its [Palette]. Most
	// themes ship a single entry; multi-variant themes carry one
	// palette per dark/light/notty mode and let [Theme.Resolve] pick
	// at runtime.
	//
	// An empty Variants produces a zero [ResolvedTheme] from
	// [Theme.Resolve] — every Style call returns the zero
	// [lipgloss.Style], which lipgloss renders as the terminal
	// default.
	Variants map[Variant]Palette

	// Default is the variant [Theme.Resolve] picks when the runtime
	// [Capabilities] do not point at one of the declared variants.
	// For single-variant themes it must equal the only key in
	// Variants; the zero [VariantUnset] is treated as the lone key
	// when exactly one variant exists.
	Default Variant

	// ListEnum is the default enumerator for [Context.List] output.
	// Themes that want bullets, dashes, or numbers without a
	// per-call override set it once here; the framework picks
	// [list.Bullet] when this is nil.
	ListEnum list.Enumerator

	// TableBorder is the default border drawn by [Context.Table]
	// when no per-call override is supplied. The zero
	// [lipgloss.Border] resolves to [lipgloss.NormalBorder].
	TableBorder lipgloss.Border

	// PromptKnobs are theme-wide non-color prompt settings applied
	// to token-derived and palette prompt styles.
	PromptKnobs PromptKnobs
}

Theme is declarative data describing a complete CLI styling. It carries one or more Palette entries (one per declared variant), the variant the theme defaults to when Capabilities do not pin a clear pick, and a small set of cross-variant defaults (list enumerator, table border).

Themes resolve once per [App] at construction time via Theme.Resolve, which picks a variant, applies framework defaults for any per-palette field the author left zero, and returns an immutable ResolvedTheme. The choice to model themes as data (rather than the closure type they used to be) is what lets the catalog return inspectable values, lets Override build derived themes without re-running setup, and keeps the manifest loader a pure data pipeline.

Theme is safe to copy by value; however the copy shares the Variants map with the original — mutating `copy.Variants[k] = p` affects the source. To derive a modified theme with an independent Variants map, use Theme.Clone. To tweak a single token in a built-in theme without constructing a full copy, use [nabat.WithThemeOverride].

Theme implements Resolver; pass it anywhere a Resolver is expected.

Example

ExampleTheme demonstrates the canonical declarative shape: a Theme is data — one Palette per declared variant, plus cross-variant defaults. Resolution picks a variant based on Capabilities and applies framework defaults for any nil/empty cascade slot.

package main

import (
	"fmt"

	"charm.land/lipgloss/v2"

	"nabat.dev/theme"
)

func main() {
	t := theme.Theme{
		Name:    "plain",
		Default: theme.VariantDark,
		Variants: map[theme.Variant]theme.Palette{
			theme.VariantDark: {
				Tokens: map[theme.Token]lipgloss.Style{
					theme.StatusSuccess: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7EC87E")),
				},
			},
		},
	}

	r := t.Resolve(theme.Capabilities{Dark: true, Interactive: true})
	fmt.Println(r.Name())
	fmt.Println(r.Style(theme.StatusSuccess).GetBold())
}
Output:
plain
true

func Get

func Get(name string) (Theme, error)

Get returns the Theme registered under name. The error lists every available name on a miss, so callers and CLI users get an actionable diagnosis rather than just "not found":

t, err := theme.Get("draculaa") // typo
// err: nabat/theme: no theme named "draculaa"; available: [default minimal charm dracula catppuccin-mocha nabat]

The first Get for a name parses the embedded manifest and caches the result; subsequent calls return the cached value without re-parsing. Parse errors surface here on first access; once cached, later callers always receive the parsed Theme without re-checking.

Example

ExampleGet shows the typical lookup path: resolve a built-in theme by name, then call Theme.Resolve against a Capabilities snapshot to read the resulting ResolvedTheme. Most consumers reach this through nabat.WithTheme rather than calling Get directly.

package main

import (
	"fmt"

	"nabat.dev/theme"
)

func main() {
	t, err := theme.Get(theme.Default)
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	r := t.Resolve(theme.Capabilities{Dark: true, Interactive: true})
	fmt.Println(r.Name())
}
Output:
default
Example (Unknown)

ExampleGet_unknown shows the actionable error returned when a theme name does not exist in the catalog. The error lists every available name so users can fix typos without consulting docs.

package main

import (
	"fmt"

	"nabat.dev/theme"
)

func main() {
	_, err := theme.Get("draculaa")
	fmt.Println(err != nil)
}
Output:
true

func (Theme) Clone

func (t Theme) Clone() Theme

Clone returns a shallow copy of t with an independent Variants map. Palette values inside the map are shared (not deep-copied); the new map itself is separate, so assigning to Clone().Variants[k] does not affect the original theme.

func (Theme) Resolve

func (t Theme) Resolve(c Capabilities) ResolvedTheme

Resolve picks a Variant for the supplied Capabilities, applies framework defaults to any zero field on the chosen Palette, and returns an immutable ResolvedTheme. The result is safe to share across goroutines.

Variant selection:

  • If the theme declares exactly one variant, that one is used and Theme.Default is ignored.
  • Otherwise, Theme.Default picks; the zero default plus multiple variants returns the zero ResolvedTheme rather than a guess (multi-variant resolution sharpens in a later phase).

Cascade defaults applied to the chosen Palette:

Cross-palette defaults applied:

Errors:

  • The inline-glamour Palette.GlamourFor callback may fail; the error is wrapped with the theme name and returned. Resolve still produces a usable ResolvedTheme in that case (the glamour slot stays empty, falling through to glamour's own defaults at render time) so consumers can surface the error diagnostically without losing the rest of the styling.

func (Theme) ResolveErr

func (t Theme) ResolveErr(c Capabilities) (ResolvedTheme, error)

ResolveErr behaves like Theme.Resolve but also returns the error from any per-Palette callback (today only Palette.GlamourFor). Construction paths that want to surface those failures (App.finalize, the catalog loader's own validations) should use this; consumers that just need a styling pick the error-eating Resolve.

func (Theme) Validate

func (t Theme) Validate() error

Validate returns any structural problems with the theme. The catalog uses it to surface broken built-in themes at registry load; user code rarely needs it directly.

Errors:

func (Theme) With

func (t Theme) With(overrides ...Override) Theme

With returns a derived Theme with the supplied overrides applied to every variant. The receiver is not modified; the returned theme owns fresh per-variant maps so subsequent overrides on the receiver (or the original Theme) do not leak across.

Override application order matches the supplied slice (left to right), so the right-most override of a token wins. Overrides targeting different fields compose freely.

Typical use:

dracula, _ := theme.Get(theme.Dracula)
mine := dracula.With(
    theme.SetToken(theme.StatusError, magenta),
    theme.SetAlias(theme.ListItem, theme.TextSecondary),
)
app, _ := nabat.New("myctl", nabat.WithCustomTheme(mine))

type Token

type Token string

Token names a semantic style slot in a ResolvedTheme. Tokens are dotted lowercase strings (for example "status.success", "text.primary") that identify the role a style plays rather than its appearance. The constants in this file are Nabat's well-known set; third-party themes and extensions may define and consume additional tokens — token names are an open set, not an enum.

The named string type catches arbitrary-string misuse at [Builder.Set] and ResolvedTheme.Style call sites without preventing dynamic lookup by callers that already hold a string (for example a user manifest or a flag value).

const (
	// StatusSuccess marks affirmative output: completed deploys,
	// "ok" badges, the leading symbol on Context.Success.
	StatusSuccess Token = "status.success"

	// StatusWarning marks warnings: degraded operation, deprecated APIs,
	// the leading symbol on Context.Warn.
	StatusWarning Token = "status.warning"

	// StatusError marks failure output: rejected commands, the leading
	// symbol on Context.Error, the "error:" prefix on uncaught errors.
	StatusError Token = "status.error"

	// StatusInfo marks neutral status narrative: retrying, connecting,
	// the leading symbol on Context.Info, version-line text.
	StatusInfo Token = "status.info"

	// TextPrimary styles primary body text, table cell values, and
	// list/tree item text.
	TextPrimary Token = "text.primary"

	// TextSecondary styles descriptive text and prose.
	TextSecondary Token = "text.secondary"

	// TextTitle styles help titles, table headers, and other prominent
	// section titles.
	TextTitle Token = "text.title"

	// TextLink styles hyperlinks.
	TextLink Token = "text.link"

	// AccentPrimary styles labels and key chrome accents.
	AccentPrimary Token = "accent.primary"

	// TextMuted styles de-emphasized chrome such as table borders,
	// list enumerators, and tree connectors.
	TextMuted Token = "text.muted"

	// CodeSurface styles code block backgrounds.
	CodeSurface Token = "code.surface"

	// TableBorder styles the characters drawn between table cells.
	TableBorder Token = "table.border"

	// TableHeader styles the cells in a table's header row.
	TableHeader Token = "table.header"

	// TableCell styles the cells in a table's data rows.
	TableCell Token = "table.cell"

	// ListItem styles list item text.
	ListItem Token = "list.item"

	// ListEnumerator styles list enumerator markers (•, -, 1., …).
	ListEnumerator Token = "list.enumerator"

	// TreeItem styles tree item text.
	TreeItem Token = "tree.item"

	// TreeEnumerator styles tree enumerator markers (├──, └──, …).
	TreeEnumerator Token = "tree.enumerator"
)

Well-known semantic tokens used by the Nabat core consumers.

type UnicodeLevel

type UnicodeLevel uint8

UnicodeLevel describes how much of the Unicode plane the terminal can render correctly. The tiers compose monotonically — higher values include the lower ones — so consumers can compare with less-than / greater-than for "at least N".

const (
	UnicodeASCII UnicodeLevel = iota
	UnicodeWide
	UnicodeEmoji
)

UnicodeLevel constants describe the Unicode capability tier of the terminal:

  • UnicodeASCII: only 7-bit ASCII renders reliably. Themes should stick to "+", "-", "|" for box drawing.
  • UnicodeWide: Unicode wide characters render correctly, including box-drawing, list bullets, and arrows. The framework's defaults assume this tier.
  • UnicodeEmoji: emoji and other multi-codepoint sequences render correctly. Themes that want to use emoji glyphs (✓ ❌ 🔍) gate them on this tier.

type Variant

type Variant string

Variant is a theme's intended luminance / TTY context — the background brightness or render mode the author designed for. It is the slot a future "--theme-variant" override flag and runtime diagnostics will read (e.g. "the active theme targets dark backgrounds; your terminal looks light") to choose between light and dark palette flavors of a brand theme.

The zero Variant (VariantUnset) means "the theme did not declare a target variant"; consumers should treat it as compatible with every Capabilities snapshot.

const (
	// VariantUnset is the zero value: the theme did not declare a
	// target variant. Consumers should not gate behavior on this.
	VariantUnset Variant = ""

	// VariantDark indicates the theme was designed for dark
	// terminals. Use it when the palette assumes a dark background.
	VariantDark Variant = "dark"

	// VariantLight indicates the theme was designed for light
	// terminals. Use it when the palette assumes a light background.
	VariantLight Variant = "light"

	// VariantNoTTY indicates the theme was designed for non-TTY
	// output (logs, CI artifacts). Use it when the palette assumes
	// no ANSI styling at all.
	VariantNoTTY Variant = "notty"
)

Variant constants enumerate the values a manifest's "variant" field (and a programmatic Theme.Default / map key) accepts. They match the strings the manifest schema validates against, so a manifest's "variant" field round-trips through this type without a translation step.

Directories

Path Synopsis
internal
manifest
Package manifest holds the parser machinery that turns a Nabat theme manifest (DTCG JSON, schema/v1.json) into a [theme.Theme] closure.
Package manifest holds the parser machinery that turns a Nabat theme manifest (DTCG JSON, schema/v1.json) into a [theme.Theme] closure.

Jump to

Keyboard shortcuts

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