From 08135a407b86337d4bc18cdb1b3c3f3b0d96cc15 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 23 Oct 2026 11:07:22 +0200 Subject: [PATCH] feat: bundle Terebi Sunset as a selectable local theme --- Makefile | 13 +++- README.md | 18 ++++++ apps.go | 24 ++++---- custom_scheme_test.go | 98 +++++++++++++++++++++++++++++++ default-state.toml | 1 + install_test.go | 5 ++ main.go | 5 ++ schemes/base16/terebi-sunset.yaml | 24 ++++++++ 8 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 custom_scheme_test.go create mode 100644 schemes/base16/terebi-sunset.yaml diff --git a/Makefile b/Makefile index 610e5af..836e011 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DARK_NOTIFY_LOG := $(HOME)/Library/Logs/dark-notify.log CONFIG_MARKER := $(CONFIG_DIR)/.tint-installed-config STATE_MARKER := $(CONFIG_DIR)/.tint-installed-state -.PHONY: install uninstall +.PHONY: install install-schemes uninstall install: default-state.toml config.toml @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; } @@ -31,6 +31,7 @@ install: default-state.toml config.toml touch "$(HERDR_CONFIG)" "$(HERDR_MARKER)"; \ fi XDG_CONFIG_HOME="$(XDG_CONFIG_HOME)" tinty sync + $(MAKE) install-schemes @dark_notify="$$(command -v dark-notify)"; \ tinty_dir="$$(dirname "$$(command -v tinty)")"; \ 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 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: @launchctl bootout "gui/$$(id -u)/$(LAUNCH_AGENT_LABEL)" 2>/dev/null || true 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 \ rm -f "$(CONFIG_DIR)/state.toml" "$(STATE_MARKER)"; \ fi diff --git a/README.md b/README.md index 9c7b0f5..b0724a8 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ The installer: - 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. - 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. Add the binary directory to your shell path: @@ -172,6 +173,23 @@ TINTY_PAIR_APPEARANCE=light 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 After changing or updating the wrapper source: diff --git a/apps.go b/apps.go index 58e4b8e..00dd0b6 100644 --- a/apps.go +++ b/apps.go @@ -51,18 +51,20 @@ func currentScheme() (tintyScheme, error) { if err != nil { 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)) - for _, scheme := range schemes { - if scheme.ID == id { - return scheme, nil + for _, args := range [][]string{{"list", "--json"}, {"list", "--json", "--custom-schemes"}} { + output, err := exec.Command("tinty", args...).Output() + 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) diff --git a/custom_scheme_test.go b/custom_scheme_test.go new file mode 100644 index 0000000..89c730a --- /dev/null +++ b/custom_scheme_test.go @@ -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) + } +} diff --git a/default-state.toml b/default-state.toml index a1917d7..f0d06a4 100644 --- a/default-state.toml +++ b/default-state.toml @@ -37,6 +37,7 @@ themes = [ "base16-selenized-light", "base16-swamp-dark", "base16-swamp-light", +"base16-terebi-sunset", "base24-ic-orange-ppl", "base24-later-this-evening", "tinted8-gruvbox-dark", diff --git a/install_test.go b/install_test.go index 7e4eb9a..c4f9029 100644 --- a/install_test.go +++ b/install_test.go @@ -34,8 +34,12 @@ func TestInstallFromFreshCheckout(t *testing.T) { 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 case "$1" in + config) printf '%s/tinted-theming/tinty\n' "$XDG_DATA_HOME" ;; sync|info) exit 0 ;; apply) printf '%s\n' "$2" >> "$TINTY_LOG" ;; *) exit 2 ;; @@ -54,6 +58,7 @@ esac t.Setenv("GOMODCACHE", strings.TrimSpace(string(cache))) t.Setenv("PATH", bin+":"+os.Getenv("PATH")) t.Setenv("TINTY_LOG", log) + t.Setenv("XDG_DATA_HOME", filepath.Join(tmp, "data")) install := func() { t.Helper() cmd := exec.Command("make", "install", "HOME="+home, "XDG_CONFIG_HOME="+config) diff --git a/main.go b/main.go index c2d3721..dfb87d9 100644 --- a/main.go +++ b/main.go @@ -192,6 +192,11 @@ func validatePair(pair currentPair, stderr io.Writer) error { func validateTheme(theme string, stderr io.Writer) error { cmd := exec.Command("tinty", "info", theme) 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 return cmd.Run() } diff --git a/schemes/base16/terebi-sunset.yaml b/schemes/base16/terebi-sunset.yaml new file mode 100644 index 0000000..29dc257 --- /dev/null +++ b/schemes/base16/terebi-sunset.yaml @@ -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)