Documentation
¶
Overview ¶
Package system publishes the operating system's appearance — dark mode and the accent colour — as a reactive stream, and bridges it to the theme the components above read. A per-OS shim reads the live state behind a Source; FromSource turns a Source plus a poll interval into an rx.Observable that emits only when the value changes; Live wires the current platform's shim, and LiveTheme maps that stream to theme.Theme values whose Color matches the OS setting. Since E3.2 LiveTheme also composes the OS accessibility preferences (spectrum/a11y): reduce motion zeroes the emitted motion scale's durations so animated components snap, and high contrast routes the resolved palette pair through HighContrastVariant.
Reach for it as the theme argument of a window: LiveTheme(time.Second) is what every workbench application hands to spectrum/window, and from there an appearance change reaches every component with no application code. Pass your own Source to FromSource or FromSourceTheme to stub the OS out in a test. The package never imports Gio — it speaks to the OS directly, so it is usable with or without a window.
Platform support is uneven, and the matrix below is the contract; where a cell says "no", the shim reports the zero value for that dimension and an application that looks like it is ignoring the setting is not misconfigured — the source has nothing to read.
platform dark mode accent colour
macOS yes — AppleInterfaceStyle yes — AppleAccentColor index,
via `defaults read -g` normalized to [Accent] (throttled)
Windows no (always light) yes — HKCU\Software\Microsoft\
Windows\DWM AccentColor, an
arbitrary colour → AccentSeed
Linux no (always light) GNOME 47+: the named accent via
`gsettings`, mapped to libadwaita's
published colour → AccentSeed
KDE Plasma: kdeglobals [General]
AccentColor r,g,b → AccentSeed
other desktops, older GNOME, or a
KDE scheme with no explicit accent:
none — the default seed's palette
other no (always light) no
Dark-mode sources for Windows and Linux are a later milestone. The two accent shapes are deliberate: macOS's accent is one of eight named choices, carried as the Accent enum; Windows and Linux accents are arbitrary colours, carried raw in Appearance.AccentSeed. Both feed the same tokens.FromSeed derivation.
The streams are shared (FX.5). One FromSource/Live/LiveTheme value runs one poll loop no matter how many subscribers attach: the loop starts with the first subscriber, later subscribers immediately replay the latest value and then track changes, and the loop stops when the subscriber count drops to zero (restarting, latest-first, on the next subscription). A LiveTheme handed to n layers therefore polls each of its two sources — appearance and a11y — once per interval, not n times. Distinct calls still get distinct loops: sharing is per observable value, so build the stream once and hand the same value around. Keep the interval at the intended one second; the OS caches these values and will not report a change much sooner.
Errors are invisible by design: a failing Read is folded into the zero Appearance rather than an error emission, so a broken source is indistinguishable from light mode with no accent. The accent is not just carried: with no palette option, LiveTheme follows it — each Accent maps to Apple's published seed colour and the emitted palette is tokens.FromSeed of that seed, derived once per accent value and cached. An explicit WithSeed or WithPalette beats the OS accent: the app chose its brand, so the accent is ignored entirely.
Index ¶
- Variables
- func FromSource(src Source, interval time.Duration) rx.Observable[Appearance]
- func FromSourceTheme(src Source, interval time.Duration, opts ...Option) rx.Observable[theme.Theme]
- func Live(interval time.Duration) rx.Observable[Appearance]
- func LiveTheme(interval time.Duration, opts ...Option) rx.Observable[theme.Theme]
- type Accent
- type Appearance
- type Option
- type Source
Constants ¶
This section is empty.
Variables ¶
var HighContrastVariant = func(light, dark tokens.ColorTokens) (hcLight, hcDark tokens.ColorTokens) { hcMu.Lock() defer hcMu.Unlock() key := colorPair{light: light, dark: dark} if c, ok := hcByPair[key]; ok { return c.light, c.dark } l, d := tokens.FromSeedHighContrast(light.Primary) if hcByPair == nil { hcByPair = make(map[colorPair]colorPair) } hcByPair[key] = colorPair{light: l, dark: d} return l, d }
HighContrastVariant selects the high-contrast variant of a resolved light/dark palette pair. The theme stream calls it while the OS "Increase Contrast" preference is on, AFTER palette precedence has resolved the pair — so it derives the high-contrast variant OF the chosen palette, whether that came from WithSeed, WithPalette, the OS accent, or the defaults.
The default (E3.3) re-derives from the resolved pair's own seed: tokens.FromSeedHighContrast of light.Primary, which for every seed-derived pair — the defaults, WithSeed, an OS accent — IS the seed, byte-for-byte, per the FromSeed pin contract. A hand-built WithPalette pair carries no seed, but its light Primary is still its pinned brand base, so it gets a seed-derived high-contrast approximation via that pin — FromSeedHighContrast accepts any colour, so derivation never fails. Derivations are memoized per pair, mirroring the per-seed palette cache.
It is a variable so an application (or test) can substitute its own derivation.
Functions ¶
func FromSource ¶
func FromSource(src Source, interval time.Duration) rx.Observable[Appearance]
FromSource returns a shared Observable that polls src every interval, emitting Appearance only when the value changes. The first read is scheduled immediately (no initial delay).
The returned observable is multicast (FX.5): all subscribers to this one value share a single poll loop, a subscriber arriving after the first read immediately observes the latest Appearance before tracking changes, and the loop stops when the last subscriber unsubscribes (restarting on the next subscription). Each FromSource call builds its own loop — sharing is per returned value, not per Source.
Read errors are folded into the zero-value Appearance — the stream is never an error stream. This keeps the contract simple for consumers that only care about the last good value, and matches a11y.FromSource.
func FromSourceTheme ¶
FromSourceTheme is the test-friendly variant of LiveTheme: it lets a caller plug in a fake Source while exercising the same Appearance → theme.Theme bridge, including any options. Unlike LiveTheme it does NOT read the OS accessibility preferences by default — the a11y stream is a constant all-off value, so a test's emissions cannot depend on the machine it runs on; pass WithA11ySource to drive that half too.
func Live ¶
func Live(interval time.Duration) rx.Observable[Appearance]
Live returns an Observable backed by the current OS's appearance APIs, polling every interval and emitting whenever a value changes. Like FromSource it is shared: n subscribers to one Live value cost one poll loop, not n.
Recommended interval: 100–250 ms. The G2.2 acceptance budget allows up to one second between an external `defaults write` and the corresponding emission, but most desktop UIs prefer to feel snappier than that.
func LiveTheme ¶
LiveTheme bridges system-appearance changes to a theme.Theme stream. Each emission is a fresh theme.Theme whose Color field matches the OS dark-mode setting; the remaining token categories use their package defaults, modulated by the OS accessibility preferences below.
Which light/dark pair flips is decided by precedence: an explicit WithSeed or WithPalette wins outright — the app chose its brand, and the OS accent is ignored. With no palette option the stream follows the OS accent live: a raw Appearance.AccentSeed (Windows, Linux) or a non-default Accent (macOS) emits tokens.FromSeed of that seed colour (the light primary is the seed byte-for-byte per ADR-007), the raw seed beating the enum if a source ever sets both. No accent at all — AccentDefault with no AccentSeed: multicolour on macOS, an unsupported desktop, or a failed read — emits tokens.DefaultLight/DefaultDark. An accent change re-emits the theme with the new pair; each pair is derived once per seed colour and cached.
Since E3.2 the stream also composes the OS accessibility preferences (a11y.Live at the same interval, or WithA11ySource's source), and they modulate the emissions on top of the palette precedence above: while ReduceMotion is on, Motion emits tokens.Motion.Reduced() — every duration zero, so duration-driven components snap to their targets — regardless of which palette won; while HighContrast is on, Color emits HighContrastVariant of the resolved pair — the high-contrast variant OF the chosen palette, not a palette override. A preference toggle re-emits the theme just as an appearance change does.
The two streams it composes are shared: however many layers subscribe to one LiveTheme value, the appearance source and the a11y source are each polled by exactly one loop.
Types ¶
type Accent ¶ added in v0.0.12
type Accent int
Accent identifies the OS accent colour, normalized across platforms. The zero value, AccentDefault, means "no accent override": the multicolour setting on macOS, every platform whose shim has no live accent source, and a Source whose Read failed. That choice is what keeps the package's error contract honest — the zero Appearance really is "light mode with no accent", never a spurious red.
On macOS the raw AppleAccentColor key is an integer the darwin shim maps onto this enum (see accentFromIndex): -1 graphite, 0 red, 1 orange, 2 yellow, 3 green, 4 blue, 5 purple, 6 pink; an absent key means multicolour → AccentDefault.
type Appearance ¶
type Appearance struct {
// Dark is true iff the OS reports a dark interface style.
Dark bool
// Accent is the OS accent colour, normalized to this package's
// [Accent] enum — the shape for platforms whose accent is one of a
// small named set. On macOS the darwin shim maps the raw
// AppleAccentColor key (-1 graphite, 0..6 red through pink, absent =
// multicolour) onto it; platforms without an enum-shaped accent report
// the zero value. The zero value, AccentDefault, means "no accent
// override", so the zero Appearance keeps the theme's own palette.
Accent Accent
// AccentSeed is the OS accent as a raw colour, for platforms whose
// accent is an arbitrary colour rather than a named choice: the
// Windows shim decodes the DWM AccentColor registry value into it, and
// the Linux shim the GNOME named accent or the KDE kdeglobals RGB.
// It is meaningful only when AccentSeedSet is true; when set it takes
// precedence over Accent in palette resolution (an explicit WithSeed
// or WithPalette still beats both).
AccentSeed color.NRGBA
// AccentSeedSet reports whether AccentSeed carries a value. A separate
// flag rather than a sentinel colour keeps every colour — including
// black — representable, and keeps Appearance comparable for
// rx.DistinctUntilChanged.
AccentSeedSet bool
}
Appearance is the OS-level appearance state we observe. All fields are comparable so the value can be used with rx.DistinctUntilChanged.
type Option ¶ added in v0.0.12
type Option func(*config)
Option customizes a theme stream. The palette options (WithSeed, WithPalette) choose the light/dark pair the stream flips between; the default — no palette option — is tokens.DefaultLight/DefaultDark, except that with no option the stream also follows the OS accent: a non-default Accent swaps in tokens.FromSeed of that accent's seed colour. Giving any palette option pins the pair — the app chose its brand, so the OS accent is ignored. Palette options choose which light/dark pair is emitted; they never affect when emissions happen, so OS dark-mode tracking keeps working with a branded palette. WithA11ySource chooses where the accessibility preferences composed into the emissions are read from.
func WithA11ySource ¶ added in v0.0.14
WithA11ySource overrides where the stream reads accessibility preferences. LiveTheme defaults to the OS (a11y.Live); FromSourceTheme defaults to a constant all-off source so a test that stubs the appearance is hermetic by default — pass a fake a11y.Source here to exercise the reduce-motion and high-contrast composition.
func WithPalette ¶ added in v0.0.12
func WithPalette(light, dark tokens.ColorTokens) Option
WithPalette supplies both modes explicitly, for callers that need full control beyond what a seed derives. The appearance stream still decides which of the two is live. The pair is pinned: a stream given WithPalette ignores the OS accent colour.
func WithSeed ¶ added in v0.0.12
WithSeed derives the light/dark pair from one brand colour via tokens.FromSeed (derived once, up front — not per emission). The light primary is the seed byte-for-byte; everything else is generated per ADR-007. The pair is pinned: a stream given WithSeed ignores the OS accent colour.
type Source ¶
type Source interface {
Read() (Appearance, error)
}
Source reads the current OS appearance state. Implement this interface to provide a custom or test-double backend.