feat: bundle Terebi Sunset as a selectable local theme

This commit is contained in:
hl-valdemar
2026-10-23 11:07:22 +02:00
parent 90cf08729e
commit 08135a407b
8 changed files with 176 additions and 12 deletions
+12 -1
View File
@@ -11,7 +11,7 @@ DARK_NOTIFY_LOG := $(HOME)/Library/Logs/dark-notify.log
CONFIG_MARKER := $(CONFIG_DIR)/.tint-installed-config CONFIG_MARKER := $(CONFIG_DIR)/.tint-installed-config
STATE_MARKER := $(CONFIG_DIR)/.tint-installed-state STATE_MARKER := $(CONFIG_DIR)/.tint-installed-state
.PHONY: install uninstall .PHONY: install install-schemes uninstall
install: default-state.toml config.toml install: default-state.toml config.toml
@command -v tinty >/dev/null || { echo "tinty is required" >&2; exit 1; } @command -v tinty >/dev/null || { echo "tinty is required" >&2; exit 1; }
@command -v dark-notify >/dev/null || { echo "dark-notify is required" >&2; exit 1; } @command -v dark-notify >/dev/null || { echo "dark-notify is required" >&2; exit 1; }
@@ -31,6 +31,7 @@ install: default-state.toml config.toml
touch "$(HERDR_CONFIG)" "$(HERDR_MARKER)"; \ touch "$(HERDR_CONFIG)" "$(HERDR_MARKER)"; \
fi fi
XDG_CONFIG_HOME="$(XDG_CONFIG_HOME)" tinty sync XDG_CONFIG_HOME="$(XDG_CONFIG_HOME)" tinty sync
$(MAKE) install-schemes
@dark_notify="$$(command -v dark-notify)"; \ @dark_notify="$$(command -v dark-notify)"; \
tinty_dir="$$(dirname "$$(command -v tinty)")"; \ tinty_dir="$$(dirname "$$(command -v tinty)")"; \
printf '%s\n' \ printf '%s\n' \
@@ -69,9 +70,19 @@ install: default-state.toml config.toml
@launchctl bootout "gui/$$(id -u)/$(LAUNCH_AGENT_LABEL)" 2>/dev/null || true @launchctl bootout "gui/$$(id -u)/$(LAUNCH_AGENT_LABEL)" 2>/dev/null || true
launchctl bootstrap "gui/$$(id -u)" "$(LAUNCH_AGENT)" launchctl bootstrap "gui/$$(id -u)" "$(LAUNCH_AGENT)"
install-schemes: schemes/base16/terebi-sunset.yaml
@set -e; \
data_dir="$$(XDG_CONFIG_HOME="$(XDG_CONFIG_HOME)" tinty config --data-dir-path)"; \
mkdir -p "$$data_dir/custom-schemes/base16"; \
cp schemes/base16/terebi-sunset.yaml "$$data_dir/custom-schemes/base16/terebi-sunset.yaml"
uninstall: uninstall:
@launchctl bootout "gui/$$(id -u)/$(LAUNCH_AGENT_LABEL)" 2>/dev/null || true @launchctl bootout "gui/$$(id -u)/$(LAUNCH_AGENT_LABEL)" 2>/dev/null || true
rm -f "$(BIN_DIR)/tint" "$(LAUNCH_AGENT)" "$(DARK_NOTIFY_LOG)" rm -f "$(BIN_DIR)/tint" "$(LAUNCH_AGENT)" "$(DARK_NOTIFY_LOG)"
@if command -v tinty >/dev/null; then \
data_dir="$$(XDG_CONFIG_HOME="$(XDG_CONFIG_HOME)" tinty config --data-dir-path)" && \
rm -f "$$data_dir/custom-schemes/base16/terebi-sunset.yaml"; \
fi
@if [ -e "$(STATE_MARKER)" ]; then \ @if [ -e "$(STATE_MARKER)" ]; then \
rm -f "$(CONFIG_DIR)/state.toml" "$(STATE_MARKER)"; \ rm -f "$(CONFIG_DIR)/state.toml" "$(STATE_MARKER)"; \
fi fi
+18
View File
@@ -38,6 +38,7 @@ The installer:
- Copies the versioned `default-state.toml` to the installed `state.toml` only when no state file already exists. - Copies the versioned `default-state.toml` to the installed `state.toml` only when no state file already exists.
- Creates `~/.config/herdr/config.toml` if needed. - Creates `~/.config/herdr/config.toml` if needed.
- Runs `tinty sync` to download the configured theme repositories. - Runs `tinty sync` to download the configured theme repositories.
- Installs bundled custom schemes into Tinty's data directory.
- Creates and loads a `dark-notify` LaunchAgent for automatic macOS appearance switching. - Creates and loads a `dark-notify` LaunchAgent for automatic macOS appearance switching.
Add the binary directory to your shell path: Add the binary directory to your shell path:
@@ -172,6 +173,23 @@ TINTY_PAIR_APPEARANCE=light tint swamp
TINTY_PAIR_APPEARANCE=dark tint swamp TINTY_PAIR_APPEARANCE=dark tint swamp
``` ```
### Terebi Sunset
The bundled [`base16-terebi-sunset`](schemes/base16/terebi-sunset.yaml) palette comes from the sibling Terebi project. It preserves the player's brown, cream, sand, orange and ember colors and adds the remaining Base16 syntax hues. It is dark-only; keep your existing light theme.
`make install` installs it automatically. To install just the palette and updated wrapper without syncing repositories or reloading the appearance agent:
```sh
make install-schemes
go build -o "$HOME/.local/bin/tint" .
tint favorite base16-terebi-sunset
tint dark:base16-terebi-sunset
```
Setting the dark side preserves the light side and applies whichever side matches macOS appearance. Use `tint dark` to apply the saved dark side immediately.
Tinty stores the YAML under `$(tinty config --data-dir-path)/custom-schemes/base16/terebi-sunset.yaml` and builds the installed app templates when applying it. Both theme validation and app palette lookup support [Tinty's native custom schemes](https://github.com/tinted-theming/tinty/blob/v0.34.0/USAGE.md#use-your-own-schemes), so Ghostty, Zed, Herdr, OMP and Conjure use the normal application flow. List local palettes with `tinty list --custom-schemes`. Reinstalling updates the bundled palette without changing the saved pair or favorites; uninstalling removes the bundled YAML.
## Update ## Update
After changing or updating the wrapper source: After changing or updating the wrapper source:
+13 -11
View File
@@ -51,18 +51,20 @@ func currentScheme() (tintyScheme, error) {
if err != nil { if err != nil {
return tintyScheme{}, err return tintyScheme{}, err
} }
output, err := exec.Command("tinty", "list", "--json").Output()
if err != nil {
return tintyScheme{}, err
}
var schemes []tintyScheme
if err := json.Unmarshal(output, &schemes); err != nil {
return tintyScheme{}, err
}
id := strings.TrimSpace(string(current)) id := strings.TrimSpace(string(current))
for _, scheme := range schemes { for _, args := range [][]string{{"list", "--json"}, {"list", "--json", "--custom-schemes"}} {
if scheme.ID == id { output, err := exec.Command("tinty", args...).Output()
return scheme, nil if err != nil {
return tintyScheme{}, err
}
var schemes []tintyScheme
if err := json.Unmarshal(output, &schemes); err != nil {
return tintyScheme{}, err
}
for _, scheme := range schemes {
if scheme.ID == id {
return scheme, nil
}
} }
} }
return tintyScheme{}, fmt.Errorf("scheme not found: %s", id) return tintyScheme{}, fmt.Errorf("scheme not found: %s", id)
+98
View File
@@ -0,0 +1,98 @@
package main
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"testing"
)
func TestCustomSchemeCanBeFavoritedAndAppliedToApps(t *testing.T) {
tinty, err := exec.LookPath("tinty")
if err != nil {
t.Skip("requires tinty 0.34+")
}
home := t.TempDir()
data := filepath.Join(home, "data")
bin := filepath.Join(home, "bin")
for _, dir := range []string{bin, filepath.Join(data, "repos", "schemes", "base16"), filepath.Join(data, "custom-schemes", "base16"), filepath.Join(home, ".config", "herdr")} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
}
palette, err := os.ReadFile("schemes/base16/terebi-sunset.yaml")
if err != nil {
t.Fatal(err)
}
files := map[string][]byte{
filepath.Join(data, "custom-schemes", "base16", "terebi-sunset.yaml"): palette,
filepath.Join(home, "config.toml"): []byte("items = []\n"),
filepath.Join(home, ".config", "herdr", "config.toml"): []byte("[server]\nport = 8080\n"),
filepath.Join(home, "zed.json"): []byte(`{"name":"Terebi Sunset","themes":[{"name":"Terebi Sunset","appearance":"dark"}]}`),
}
for path, contents := range files {
if err := os.WriteFile(path, contents, 0o644); err != nil {
t.Fatal(err)
}
}
writeExecutable(t, filepath.Join(bin, "tinty"), `#!/bin/sh
exec "$TINTY_TEST_BINARY" --data-dir "$TINTY_TEST_DATA" --config "$TINTY_TEST_CONFIG" "$@"
`)
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("PATH", bin+":/usr/bin:/bin")
t.Setenv("TINTY_TEST_BINARY", tinty)
t.Setenv("TINTY_TEST_DATA", data)
t.Setenv("TINTY_TEST_CONFIG", filepath.Join(home, "config.toml"))
statePath := filepath.Join(home, "state.toml")
t.Setenv("TINT_STATE_FILE", statePath)
initial := state{Current: currentPair{Light: "personal-light", Dark: "personal-dark"}}
if err := saveState(statePath, initial); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
if err := runCLI([]string{"favorite", "base16-terebi-sunset"}, &stdout, &stderr); err != nil {
t.Fatalf("custom scheme could not be favorited: %v: %s", err, stderr.String())
}
value, err := loadState(statePath)
if err != nil {
t.Fatal(err)
}
if value.Current != initial.Current || len(value.Catalog.FavoriteThemes) != 1 || value.Catalog.FavoriteThemes[0] != "base16-terebi-sunset" {
t.Fatalf("favorite changed the current pair or omitted the scheme: %#v", value)
}
before := readFile(t, statePath)
if err := runCLI([]string{"favorite", "base16-missing"}, &stdout, &stderr); err == nil {
t.Fatal("accepted a nonexistent custom scheme")
}
if readFile(t, statePath) != before {
t.Fatal("invalid scheme changed saved state")
}
if output, err := exec.Command("tinty", "apply", "base16-terebi-sunset").CombinedOutput(); err != nil {
t.Fatalf("Tinty could not apply the custom scheme: %v\n%s", err, output)
}
if err := applyApps(filepath.Join(home, "zed.json")); err != nil {
t.Fatalf("custom scheme could not be applied: %v", err)
}
var omp struct {
Colors map[string]string `json:"colors"`
}
if err := json.Unmarshal([]byte(readFile(t, filepath.Join(home, ".omp", "agent", "themes", "tinty-current.json"))), &omp); err != nil {
t.Fatal(err)
}
if omp.Colors["text"] != "#ffe3b5" || omp.Colors["syntaxNumber"] != "#f2943b" {
t.Fatalf("OMP did not receive the custom scheme: %#v", omp.Colors)
}
var conjure struct {
Variant string `json:"variant"`
Colors map[string]string `json:"colors"`
}
if err := json.Unmarshal([]byte(readFile(t, filepath.Join(home, ".config", "conjure", "theme.json"))), &conjure); err != nil {
t.Fatal(err)
}
if conjure.Variant != "dark" || conjure.Colors["background"] != "#1f1714" || conjure.Colors["secondaryText"] != "#d9c7a6" {
t.Fatalf("Conjure did not receive the custom scheme: %#v", conjure)
}
}
+1
View File
@@ -37,6 +37,7 @@ themes = [
"base16-selenized-light", "base16-selenized-light",
"base16-swamp-dark", "base16-swamp-dark",
"base16-swamp-light", "base16-swamp-light",
"base16-terebi-sunset",
"base24-ic-orange-ppl", "base24-ic-orange-ppl",
"base24-later-this-evening", "base24-later-this-evening",
"tinted8-gruvbox-dark", "tinted8-gruvbox-dark",
+5
View File
@@ -34,8 +34,12 @@ func TestInstallFromFreshCheckout(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
if err := os.CopyFS(filepath.Join(source, "schemes"), os.DirFS("schemes")); err != nil {
t.Fatal(err)
}
writeExecutable(t, filepath.Join(bin, "tinty"), `#!/bin/sh writeExecutable(t, filepath.Join(bin, "tinty"), `#!/bin/sh
case "$1" in case "$1" in
config) printf '%s/tinted-theming/tinty\n' "$XDG_DATA_HOME" ;;
sync|info) exit 0 ;; sync|info) exit 0 ;;
apply) printf '%s\n' "$2" >> "$TINTY_LOG" ;; apply) printf '%s\n' "$2" >> "$TINTY_LOG" ;;
*) exit 2 ;; *) exit 2 ;;
@@ -54,6 +58,7 @@ esac
t.Setenv("GOMODCACHE", strings.TrimSpace(string(cache))) t.Setenv("GOMODCACHE", strings.TrimSpace(string(cache)))
t.Setenv("PATH", bin+":"+os.Getenv("PATH")) t.Setenv("PATH", bin+":"+os.Getenv("PATH"))
t.Setenv("TINTY_LOG", log) t.Setenv("TINTY_LOG", log)
t.Setenv("XDG_DATA_HOME", filepath.Join(tmp, "data"))
install := func() { install := func() {
t.Helper() t.Helper()
cmd := exec.Command("make", "install", "HOME="+home, "XDG_CONFIG_HOME="+config) cmd := exec.Command("make", "install", "HOME="+home, "XDG_CONFIG_HOME="+config)
+5
View File
@@ -192,6 +192,11 @@ func validatePair(pair currentPair, stderr io.Writer) error {
func validateTheme(theme string, stderr io.Writer) error { func validateTheme(theme string, stderr io.Writer) error {
cmd := exec.Command("tinty", "info", theme) cmd := exec.Command("tinty", "info", theme)
cmd.Stdout = io.Discard cmd.Stdout = io.Discard
if err := cmd.Run(); err == nil {
return nil
}
cmd = exec.Command("tinty", "info", theme, "--custom-schemes")
cmd.Stdout = io.Discard
cmd.Stderr = stderr cmd.Stderr = stderr
return cmd.Run() return cmd.Run()
} }
+24
View File
@@ -0,0 +1,24 @@
# Palette source: terebi/Themes/terebi-sunset.yaml (sibling project).
# App colors are rounded to 8-bit sRGB; the translucent backdrop becomes opaque.
# Styling roles: https://github.com/tinted-theming/home/blob/main/styling.md
system: "base16"
name: "Terebi Sunset"
author: "Terebi"
variant: "dark"
palette:
base00: "#1f1714" # Background — app backdrop, without alpha
base01: "#2c221c" # Raised background / status bars
base02: "#45352b" # Selection
base03: "#9b8775" # Comments / muted text
base04: "#d9c7a6" # Secondary foreground — app secondaryText
base05: "#ffe3b5" # Foreground — app text
base06: "#ffedcf" # Light foreground
base07: "#fff6e6" # Lightest foreground
base08: "#e07864" # Red — variables / errors / deletions
base09: "#f2943b" # Orange — numbers / constants; app accent
base0A: "#e6bc65" # Yellow — types / search
base0B: "#a9b665" # Green — strings / additions
base0C: "#8bb9aa" # Cyan — escapes / support
base0D: "#7da6b8" # Blue — functions / headings
base0E: "#b294bb" # Purple — keywords
base0F: "#c2452e" # Brown/red — deprecated; app ember (3.51:1 on base00)