swatch

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 9 Imported by: 0

README

swatch

Go Reference

tuios.dev/swatch reads the colour theme of the desktop your program runs on. It gives you a Palette with semantic roles: a background, a text colour, an accent, a border, the status colours and the sixteen ANSI colours.

The first source is Omarchy. The omarchy package reads the theme Omarchy has applied, from Omarchy 4 and from Omarchy 3. Its parser is a port of Omarchy's own omarchy-theme-color, so a theme resolves to the colours Omarchy gives every other app. A test holds the port to the script's output for every theme Omarchy ships.

swatch.Watch follows the theme while your program runs, and the teaswatch package turns each change into a Bubble Tea message.

swatch and omarchy use the standard library and fsnotify, and termconfig adds a TOML parser. Theme files come from strangers, so swatch parses colours only. Nothing in a theme file is executed or expanded. An Omarchy file over 64 KiB, or a terminal config over 1 MiB, is not read.

tuios uses swatch to follow the Omarchy theme.

Install

go get tuios.dev/swatch

The go command reads the import path from tuios.dev. Go 1.25 or later can fetch it directly. With the default module proxy, any Go version can fetch it.

Read the applied Omarchy theme

package main

import (
	"errors"
	"fmt"

	"tuios.dev/swatch"
	"tuios.dev/swatch/omarchy"
)

func main() {
	p, err := omarchy.Current().Load()
	if errors.Is(err, swatch.ErrNotFound) {
		fmt.Println("Omarchy is not installed")
		return
	}
	if err != nil {
		panic(err)
	}
	fmt.Println(p.Name, swatch.Hex(p.Accent), p.Light)
	// Tokyo Night #7aa2f7 false
}

The fields are color.Color values, which Lipgloss v2 takes as they are.

Follow the theme

w, err := swatch.Watch(omarchy.Current())
if err != nil {
	panic(err) // the platform's file watcher could not be opened
}
defer w.Close()
for p := range w.Changes() {
	fmt.Println("now", p.Name)
}

The watcher holds one inotify handle and two goroutines. It runs no timer and polls nothing while the theme stays the same. It reads the theme 50 ms after the last change, so one Omarchy switch is one read. A switch to the theme already in force sends nothing. A directory that does not exist yet is watched from its nearest parent, so Omarchy installed later is still seen.

Bubble Tea

import "tuios.dev/swatch/teaswatch"

func (m model) Init() tea.Cmd { return teaswatch.Listen(m.watcher) }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case teaswatch.ChangedMsg:
		m.styles = newStyles(msg.Palette)
		return m, teaswatch.Listen(m.watcher)
	}
	return m, nil
}

teaswatch.Terminal follows the palette of the terminal itself. It asks for OSC 10, 11 and the sixteen OSC 4 colours, turns on the light and dark reports (mode 2031), and asks again after each report.

Read a terminal's colour scheme

The termconfig package reads the colour scheme of a kitty, ghostty, alacritty or wezterm config file. It tells the format from the content.

p, format, err := termconfig.Load(os.ExpandEnv("$HOME/.config/kitty/current-theme.conf"))

termconfig.Source(path) follows the file with swatch.Watch. Raw holds only the colours the file names, under foreground, background, cursor and color0 to color15.

List the installed themes

for _, t := range omarchy.Themes() {
	p, err := t.Load()
	if err != nil {
		continue
	}
	fmt.Println(t.Name(), swatch.Hex(p.Background))
}

The palette

Role Omarchy key
Background, Foreground background, foreground
Cursor bright_foreground
SelectionBackground, SelectionForeground selection_background, selection_foreground
Surface lighter_background
Muted muted
Accent accent
Border the first colour of hyprland_active_border, else accent
Error, Success, Warning, Info red, green, yellow, blue
ANSI 0 to 7 background, red, green, yellow, blue, magenta, cyan, foreground
ANSI 8 to 15 muted, then the bright_ colours, then bright_foreground
Light mode = "light", a light.mode file, or a light background

Raw holds every key the theme defines or Omarchy derives, such as orange, brown and dark_background.

The ANSI mapping is the one in Omarchy's kitty, ghostty and foot templates, so a palette matches a terminal that Omarchy themed.

Where Omarchy keeps the theme

Omarchy Applied theme
4 ~/.local/state/omarchy/current/theme/colors.toml
3 ~/.config/omarchy/current/theme/colors.toml

Set SWATCH_OMARCHY_DIR to a current directory to read that one instead.

The API is at v0 and can change between minor versions.

Licence

MIT. The Omarchy theme fixtures in omarchy/testdata are MIT too, under Omarchy's own licence in omarchy/testdata/LICENSE.omarchy.

Documentation

Overview

Package swatch reads the colour theme of the desktop an application runs on, as a Palette with semantic roles: a background, a text colour, an accent, the sixteen ANSI colours and the rest.

A Source reads one kind of theme. The omarchy sub-package reads the theme Omarchy applies. A Source also names the paths whose change means a new palette, so an application can follow the theme while it runs.

Watch follows a Source: it reads the theme again when one of its paths changes, and delivers each new palette. The teaswatch sub-package turns that into Bubble Tea messages.

swatch and its omarchy package use the standard library and fsnotify. They read theme files from strangers, so they parse colours only: nothing in a theme file is executed or expanded.

The API is at v0 and can change between minor versions.

Index

Constants

View Source
const DefaultDebounce = 50 * time.Millisecond

DefaultDebounce is how long a Watcher waits after the last relevant change before it reads the theme again. Omarchy's switch replaces the theme directory and writes its name within about 2 ms, so one read after the burst sees the whole switch.

Variables

View Source
var ErrNotFound = errors.New("swatch: theme source not found")

ErrNotFound is the error a Source's Load returns when the theme it reads is not installed on this machine.

Functions

func Hex

func Hex(c color.Color) string

Hex spells a colour as #rrggbb in lower case, and nil as the empty string. Alpha is dropped.

func ParseHex

func ParseHex(s string) (color.RGBA, bool)

ParseHex reads a colour written #rrggbb or #rgb, in either case. Anything else, including an alpha channel, is not a colour here.

Types

type Option

type Option func(*options)

Option configures a Watcher.

func WithDebounce

func WithDebounce(d time.Duration) Option

WithDebounce sets how long the Watcher waits after the last change before it reads the theme. The default is DefaultDebounce.

type Palette

type Palette struct {
	// ID is the theme's short name as the source spells it, such as
	// "tokyo-night". Name is the name to show, such as "Tokyo Night".
	ID, Name string
	// Source names the Source the palette came from, such as "omarchy".
	Source string
	// Light is true for a theme meant for a light background.
	Light bool

	Background, Foreground, Cursor           color.Color
	SelectionBackground, SelectionForeground color.Color

	// Surface is a raised ground, for a panel or a dialog over the
	// background.
	Surface color.Color
	// Muted is the colour of secondary text.
	Muted color.Color
	// Accent is the theme's highlight colour.
	Accent color.Color
	// Border is the colour of the focused window's border. It is Accent
	// when the theme names no border of its own.
	Border color.Color

	Success, Warning, Error, Info color.Color

	// ANSI is the sixteen terminal colours, 0 to 15.
	ANSI [16]color.Color

	// Raw holds every colour the source defines or derives, by the source's
	// own key, for an application that wants more than the roles above.
	// Treat it as read-only: palettes copied from one read share it.
	Raw map[string]color.Color
}

Palette is a theme with semantic roles. A role the source does not define is derived from the others, so every field is set on a palette a Source returns, except the entries of Raw.

func (Palette) Equal

func (p Palette) Equal(q Palette) bool

Equal reports whether p and q hold the same names and colours. Two loads of an unchanged theme are Equal.

func (Palette) IsDark

func (p Palette) IsDark() bool

IsDark reports whether the palette is meant for a dark background.

type Source

type Source interface {
	// Name names the source, such as "omarchy". It is the Source field of
	// every palette Load returns.
	Name() string
	// Load reads the theme now in force. It returns ErrNotFound, wrapped or
	// not, when the theme is not installed.
	Load() (Palette, error)
	// WatchPaths are the directories whose change can mean a new palette.
	// A directory that does not exist yet is still listed. Empty means the
	// source cannot be followed.
	WatchPaths() []string
	// Relevant reports whether a change to path, a file or a directory in
	// one of WatchPaths, can mean a new palette.
	Relevant(path string) bool
}

Source reads a palette from one kind of theme.

func Static

func Static(p Palette) Source

Static returns a Source that always loads p and never changes.

type Watcher

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

Watcher follows one Source. It holds one inotify (or kqueue, or ReadDirectoryChanges) handle and one goroutine that sleeps until the file system reports a change. It runs no timer and polls nothing while the theme does not change.

A watched directory that does not exist yet is followed from its nearest existing parent, so a theme source installed after the Watcher started is still seen.

func Watch

func Watch(src Source, opts ...Option) (*Watcher, error)

Watch reads src once and starts following it. It fails only when the platform's file watcher cannot be opened. A source that is not installed is not an error here: Current reports ErrNotFound until it appears.

func (*Watcher) Changes

func (w *Watcher) Changes() <-chan Palette

Changes delivers each new palette. It holds one: a palette that arrives while the last is still unread replaces it, because only the newest theme is worth applying. A read that gives the palette already in force sends nothing, so applying the same theme again is quiet. The channel is closed by Close.

func (*Watcher) Close

func (w *Watcher) Close() error

Close stops the Watcher and closes Changes.

func (*Watcher) Current

func (w *Watcher) Current() (Palette, error)

Current returns the palette in force. The error is the first read's, such as a wrapped ErrNotFound for a source that is not installed, until a read succeeds. A failed read after that keeps the last palette and no error.

Directories

Path Synopsis
Package omarchy reads the theme Omarchy applies, as a swatch.Palette.
Package omarchy reads the theme Omarchy applies, as a swatch.Palette.
Package teaswatch connects swatch to Bubble Tea v2.
Package teaswatch connects swatch to Bubble Tea v2.
Package termconfig reads the colour scheme of a terminal's config file as a swatch.Palette: kitty, ghostty, alacritty and wezterm.
Package termconfig reads the colour scheme of a terminal's config file as a swatch.Palette: kitty, ghostty, alacritty and wezterm.

Jump to

Keyboard shortcuts

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