Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,30 @@ Light themes are honored too: `tailTUI` reads the theme's `mode` key and
shades its panels and modals in the right direction, so a light palette
renders as a light UI rather than an inverted one.

### Live theme switching (optional)

Install the bundled Omarchy template and `tailTUI` re-colors itself **while
running**, within a few seconds of you switching themes — no restart:

```bash
mkdir -p ~/.config/omarchy/themed
cp contrib/tailtui.toml.tpl ~/.config/omarchy/themed/
```

Omarchy renders the template into the active theme directory on every switch,
and `tailTUI` notices the file change on its next refresh tick.

Live reload is only half the reason to install it. The other half is control:
the template decides which palette slot drives which part of the UI, so you can
fix mappings that no automatic rule can get right for every theme. For example
`osaka-jade` defines its `yellow` as a green, which leaves exit-node markers
nearly indistinguishable from the online color — one line in the template
(`warning = "{{ orange }}"`) fixes it, for your themes, permanently.

The template is fully commented with every available placeholder. And it stays
optional: without it, `tailTUI` reads `colors.toml` and maps the palette itself,
exactly as before.

**The Omarchy binding is purely cosmetic, not a requirement.** `tailTUI` is a
stock [Bubble Tea](https://github.com/charmbracelet/bubbletea) program, so
the default "Matrix Core" palette renders beautifully on any modern desktop
Expand All @@ -237,6 +261,10 @@ the keys present rather than by filename:
`red`, `yellow`, `orange`, `green`
- **Omarchy 3** — flat terminal palette: `accent`, `foreground`,
`background`, `color0`–`color15`
- **tailTUI's own** — one key per UI role: `mode`, `primary`, `secondary`,
`background`, `surface`, `surface_bright`, `border`, `text`, `text_dim`,
`warning`, `error`. This is what the template above generates, and it is
the format to hand-write if you want full control on any distro.

Mapping is per-key, so a partial or unusual palette keeps the "Matrix Core"
default for whatever it omits — never a blank or a crash. All colors are
Expand Down
66 changes: 66 additions & 0 deletions contrib/tailtui.toml.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# tailTUI theme template for Omarchy
#
# Install:
# mkdir -p ~/.config/omarchy/themed
# cp contrib/tailtui.toml.tpl ~/.config/omarchy/themed/
#
# Omarchy renders this into the active theme directory as `tailtui.toml` every
# time you switch themes, and tailTUI picks it up within a few seconds — no
# restart. Without it, tailTUI reads the theme's own `colors.toml` directly and
# maps the palette itself, which needs no setup at all.
#
# The point of installing it is control: you decide which palette slot drives
# which part of the UI, per theme, instead of tailTUI guessing. See below for
# where that matters.
#
# Every {{ placeholder }} is substituted by Omarchy from the current theme.
# Available names include the semantic slots (accent, background, foreground,
# selection, muted, red, green, yellow, orange, blue, cyan, magenta, and the
# bright_/dark_/light_ variants), the legacy terminal palette (color0-color15),
# and `mode`. A _strip suffix on a name drops the leading #, _rgb gives it
# as decimal "r,g,b", and a mix function blends two colors. The bundled
# alacritty.toml.tpl.sample in this directory documents their exact syntax.
#
# Run `omarchy-theme-color --file ~/.local/state/omarchy/current/theme/colors.toml --all`
# to print every name your current theme defines.

# "dark" or "light". Controls which direction panels are shaded relative to the
# background: a light theme shades its surfaces down, a dark theme lifts them up.
mode = "{{ mode }}"

# Focus, borders on the active pane, buttons, key hints.
primary = "{{ accent }}"

# Online nodes, approved routes, success chips.
secondary = "{{ green }}"

# The base canvas.
background = "{{ background }}"

# Elevated panels and modals — one step away from the canvas. On a light theme
# use the dark_background slot instead, so panels shade downward, not up.
surface = "{{ lighter_background }}"

# The selected row's highlight bar.
surface_bright = "{{ selection }}"

# Unfocused pane borders and dividers.
border = "{{ muted }}"

# Body text.
text = "{{ foreground }}"

# Labels, timestamps, secondary text.
text_dim = "{{ muted }}"

# Exit nodes, relayed connections, elevated latency.
#
# `yellow` is the obvious choice, but some themes define it as something that
# collides with `green` — osaka-jade sets yellow to #459451, which would make
# exit-node markers nearly indistinguishable from the online color. If that
# happens in your theme, use the orange slot here instead. This is exactly the
# judgement call a template exists to hand back to you.
warning = "{{ yellow }}"

# Conflicts, failures, critical latency.
error = "{{ red }}"
91 changes: 87 additions & 4 deletions internal/styles/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package styles
import (
"os"
"path/filepath"
"time"

"github.com/charmbracelet/lipgloss"
"github.com/pelletier/go-toml/v2"
Expand Down Expand Up @@ -52,6 +53,55 @@ func DefaultTheme() Theme {
}
}

// tailtuiTheme is tailTUI's own theme schema — one key per Theme field, so a
// palette can be stated directly instead of inferred from someone else's
// vocabulary. It is what an Omarchy template renders to (see
// contrib/tailtui.toml.tpl), and it is equally hand-writable on any distro.
//
// Its keys are deliberately disjoint from the Omarchy schemas' (except the
// shared `mode`), so schema detection stays a matter of which names appear.
type tailtuiTheme struct {
Mode string `toml:"mode"`
Primary string `toml:"primary"`
Secondary string `toml:"secondary"`
Background string `toml:"background"`
Surface string `toml:"surface"`
SurfaceBright string `toml:"surface_bright"`
Border string `toml:"border"`
Text string `toml:"text"`
TextDim string `toml:"text_dim"`
Warning string `toml:"warning"`
Error string `toml:"error"`
}

// hasMarkers reports whether the file uses tailTUI's schema. `mode` and
// `background` are shared with the Omarchy schemas and so are excluded.
func (t tailtuiTheme) hasMarkers() bool {
return t.Primary != "" || t.Secondary != "" || t.Surface != "" ||
t.SurfaceBright != "" || t.Border != "" || t.Text != "" ||
t.TextDim != "" || t.Warning != "" || t.Error != ""
}

// mapTailtui maps tailTUI's own schema onto the Theme. Every key is optional;
// whatever a file omits keeps its Matrix Core default.
func mapTailtui(o tailtuiTheme) Theme {
t := DefaultTheme()
if o.Mode == ModeLight {
t.Mode = ModeLight
}
set(&t.PrimaryAccent, o.Primary)
set(&t.SecondaryAccent, o.Secondary)
set(&t.Background, o.Background)
set(&t.Surface, o.Surface)
set(&t.SurfaceBright, o.SurfaceBright)
set(&t.BorderInactive, o.Border)
set(&t.TextNormal, o.Text)
set(&t.TextDim, o.TextDim)
set(&t.Warning, o.Warning)
set(&t.Error, o.Error)
return t
}

// omarchyV4 mirrors the Omarchy 4 ("Quattro") colors.toml schema: semantically
// named slots rather than a raw terminal palette. Only the fields we map are
// declared — go-toml ignores the rest (cyan/blue/magenta/brown/bright_*).
Expand Down Expand Up @@ -116,10 +166,20 @@ func themeCandidates() []string {
if err != nil {
return nil
}
return []string{
filepath.Join(home, ".local", "state", "omarchy", "current", "theme", "colors.toml"), // Omarchy 4+
filepath.Join(home, ".config", "omarchy", "current", "theme", "colors.toml"), // Omarchy <= 3
// Within each theme directory, tailtui.toml wins over colors.toml: it is
// opt-in (someone installed a template or wrote it), so it is the more
// deliberate statement of intent.
var paths []string
for _, dir := range []string{
filepath.Join(home, ".local", "state", "omarchy", "current", "theme"), // Omarchy 4+
filepath.Join(home, ".config", "omarchy", "current", "theme"), // Omarchy <= 3
} {
paths = append(paths,
filepath.Join(dir, "tailtui.toml"),
filepath.Join(dir, "colors.toml"),
)
}
return paths
}

// ThemePath returns the colors.toml that LoadTheme will actually read: the
Expand All @@ -139,6 +199,20 @@ func ThemePath() string {
return candidates[0]
}

// ThemeStamp identifies the theme file LoadTheme would currently read, by path
// and modification time. Callers poll it to notice a theme switch: Omarchy
// rewrites the theme directory on every switch, so both the path and the mtime
// can change. Zero values mean no theme file is present, which is itself a
// state worth detecting (a theme file appearing should take effect).
func ThemeStamp() (path string, mod time.Time) {
for _, p := range themeCandidates() {
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
return p, fi.ModTime()
}
}
return "", time.Time{}
}

// LoadTheme returns the system (Omarchy) theme if one can be found and parsed,
// otherwise it silently falls back to the default Matrix Core theme. It
// understands both the Omarchy 4 semantic schema and the legacy color0..15
Expand All @@ -165,9 +239,18 @@ func loadThemeFile(path string) (Theme, bool) {
return Theme{}, false // no theme file (or unreadable) — try the next
}

// tailTUI's own schema is the most specific, so it is tried first.
var own tailtuiTheme
if err := toml.Unmarshal(data, &own); err != nil {
return Theme{}, false // malformed TOML — don't crash
}
if own.hasMarkers() {
return mapTailtui(own), true
}

var v4 omarchyV4
if err := toml.Unmarshal(data, &v4); err != nil {
return Theme{}, false // malformed TOML — don't crash
return Theme{}, false
}
if v4.hasMarkers() {
return mapV4(v4), true
Expand Down
Loading
Loading