Compare commits

..

11 Commits

Author SHA1 Message Date
hl-valdemar 437356272e fix: warm Pumpkin Light terminal chrome accents 2026-10-23 16:24:43 +02:00
hl-valdemar 2b0cee20ba feat: bundle Pumpkin Light and paired appearance switching 2026-10-23 11:40:58 +02:00
hl-valdemar f414bf0170 fix: strengthen Pumpkin syntax color separation 2026-10-23 11:36:08 +02:00
hl-valdemar 7cb4ce2902 fix: warm Pumpkin function and keyword colors 2026-10-23 11:24:57 +02:00
hl-valdemar 4136aa53c8 refactor: rename the bundled theme to Pumpkin 2026-10-23 11:24:18 +02:00
hl-valdemar 08135a407b feat: bundle Terebi Sunset as a selectable local theme 2026-10-23 11:24:18 +02:00
hl-valdemar 90cf08729e fix: install versioned default theme state 2026-10-23 10:25:11 +02:00
hl-valdemar 92200c76ec feat: include Conjure palette appearance 2026-10-22 16:58:52 +02:00
hl-valdemar b42e789f29 docs: describe live Conjure theme updates 2026-10-22 16:58:52 +02:00
hl-valdemar 586e1c75e7 docs: use TOML for Conjure configuration 2026-10-22 16:58:52 +02:00
hl-valdemar c60890eb4a feat: generate Conjure color palette 2026-10-22 16:58:52 +02:00
10 changed files with 601 additions and 22 deletions
+28 -7
View File
@@ -11,27 +11,29 @@ 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
install:
.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; }
mkdir -p "$(BIN_DIR)" "$(CONFIG_DIR)" "$(HERDR_DIR)" "$(LAUNCH_AGENT_DIR)" "$(dir $(DARK_NOTIFY_LOG))"
go build -o "$(BIN_DIR)/tint" .
@if [ "$(CURDIR)" != "$(CONFIG_DIR)" ]; then \
@set -e; if [ "$(CURDIR)" != "$(CONFIG_DIR)" ]; then \
if [ -e "$(CONFIG_MARKER)" ] || [ ! -e "$(CONFIG_DIR)/config.toml" ]; then \
cp config.toml "$(CONFIG_DIR)/config.toml"; \
touch "$(CONFIG_MARKER)"; \
fi; \
if [ ! -e "$(CONFIG_DIR)/state.toml" ]; then \
cp state.toml "$(CONFIG_DIR)/state.toml"; \
touch "$(STATE_MARKER)"; \
fi; \
fi; \
if [ ! -e "$(CONFIG_DIR)/state.toml" ]; then \
cp default-state.toml "$(CONFIG_DIR)/state.toml"; \
touch "$(STATE_MARKER)"; \
fi
@if [ ! -e "$(HERDR_CONFIG)" ]; then \
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' \
'<?xml version="1.0" encoding="UTF-8"?>' \
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
@@ -45,6 +47,15 @@ install:
' <string>-c</string>' \
' <string>$(BIN_DIR)/tint</string>' \
' </array>' \
' <key>EnvironmentVariables</key>' \
' <dict>' \
' <key>HOME</key>' \
' <string>$(HOME)</string>' \
' <key>PATH</key>' \
" <string>$$tinty_dir:/usr/bin:/bin:/usr/sbin:/sbin</string>" \
' <key>XDG_CONFIG_HOME</key>' \
' <string>$(XDG_CONFIG_HOME)</string>' \
' </dict>' \
' <key>RunAtLoad</key>' \
' <true/>' \
' <key>KeepAlive</key>' \
@@ -59,9 +70,19 @@ install:
@launchctl bootout "gui/$$(id -u)/$(LAUNCH_AGENT_LABEL)" 2>/dev/null || true
launchctl bootstrap "gui/$$(id -u)" "$(LAUNCH_AGENT)"
install-schemes: schemes/base16/pumpkin.yaml schemes/base16/pumpkin-light.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/pumpkin.yaml schemes/base16/pumpkin-light.yaml "$$data_dir/custom-schemes/base16/"
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/pumpkin.yaml" "$$data_dir/custom-schemes/base16/pumpkin-light.yaml"; \
fi
@if [ -e "$(STATE_MARKER)" ]; then \
rm -f "$(CONFIG_DIR)/state.toml" "$(STATE_MARKER)"; \
fi
+35 -4
View File
@@ -1,6 +1,6 @@
# tint
A small wrapper around [Tinty](https://github.com/tinted-theming/tinty) that manages light/dark theme pairs and applies the selected theme to Ghostty, Zed, Herdr, and OMP.
A small wrapper around [Tinty](https://github.com/tinted-theming/tinty) that manages light/dark theme pairs and applies the selected theme to Ghostty, Zed, Herdr, OMP, and Conjure.
## Requirements
@@ -21,7 +21,7 @@ brew install tinty go
brew install cormacrelf/tap/dark-notify
```
Ghostty, Zed, Herdr, and OMP are optional consumers. Install only the applications you use.
Ghostty, Zed, Herdr, OMP, and Conjure are optional consumers. Install only the applications you use.
## Install
@@ -35,9 +35,10 @@ The installer:
- Builds `~/.local/bin/tint`.
- Installs `config.toml` under `$XDG_CONFIG_HOME/tinted-theming/tinty`, or `~/.config/tinted-theming/tinty` when `XDG_CONFIG_HOME` is unset.
- Copies `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.
- 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:
@@ -119,6 +120,18 @@ The wrapper writes:
Configure OMP to use `tinty-current` where required.
### Conjure
The `apply-apps` flow writes `$XDG_CONFIG_HOME/conjure/theme.json`, falling back to `~/.config/conjure/theme.json` when `XDG_CONFIG_HOME` is unset, empty, or relative. The generated JSON contains the selected scheme's `variant` (`light` or `dark`) and seven opaque colors: `background`, `text`, `secondaryText`, `accent`, `glow`, `keyBackground`, and `error`, using Tinty semantic UI colors with Base16/Base24 or tinted8 palette fallbacks. Conjure uses the variant to match its native window appearance to the palette without an additional user setting.
Opt in by adding this to `conjure/config.toml` in the same configuration directory:
```toml
theme = "tint"
```
The wrapper never changes `config.toml`. Optional entries in its `[colors]` table override generated colors. Only the machine-generated `theme.json` uses JSON; edit the TOML configuration instead. Conjure watches both files and applies changes live, including automatic macOS light/dark switches, without restarting or discarding active tool state. The wrapper does not need to signal or restart Conjure.
## Usage
Show the current pair and available commands:
@@ -160,6 +173,24 @@ TINTY_PAIR_APPEARANCE=light tint swamp
TINTY_PAIR_APPEARANCE=dark tint swamp
```
### Pumpkin
The bundled [`base16-pumpkin`](schemes/base16/pumpkin.yaml) and [`base16-pumpkin-light`](schemes/base16/pumpkin-light.yaml) palettes come from the sibling Terebi project. Dark Pumpkin uses brown, cream and vivid autumn accents; Pumpkin Light uses parchment, brown ink and deeper orange, persimmon, toasted amber, caramel and olive accents. Its ANSI cyan slot uses caramel so inverted terminal bars stay warm rather than olive. All light text/accent slots exceed 4.5:1 contrast against the background.
`make install` installs both palettes automatically. To install the palettes and updated wrapper without syncing repositories or reloading the appearance agent, then save and select their named pair:
```sh
make install-schemes
go build -o "$HOME/.local/bin/tint" .
tint pair pumpkin dark:base16-pumpkin light:base16-pumpkin-light
tint favorite pumpkin
tint pumpkin
```
`tint pumpkin` sets both sides and applies the side matching macOS appearance. Use `tint light` or `tint dark` to apply a saved side immediately. Saving/favoriting the pair without selecting it leaves the current pair and active theme unchanged.
Tinty stores both YAML files under `$(tinty config --data-dir-path)/custom-schemes/base16/` and builds the installed app templates when applying either theme. 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 palettes without changing saved state; fresh installs include the favorite `pumpkin` pair, and uninstalling removes both bundled YAML files.
## Update
After changing or updating the wrapper source:
@@ -168,7 +199,7 @@ After changing or updating the wrapper source:
make install
```
Existing `state.toml` content is preserved.
Existing `state.toml` content is preserved. Missing state is recreated from `default-state.toml`, including the bundled named pairs and favorites. The source directory's ignored `state.toml` is personal state and is not used by the installer.
## Uninstall
+42 -11
View File
@@ -40,6 +40,9 @@ func applyApps(zedSource string) error {
if err := writeHerdrTheme(colors, ui, syntax); err != nil {
return err
}
if err := writeConjureTheme(scheme.Variant, colors, ui); err != nil {
return err
}
return writeZedTheme(zedSource)
}
@@ -48,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)
@@ -186,6 +191,32 @@ func semanticTheme(colors, ui, syntax map[string]string) (map[string]string, map
return omp, herdr
}
func writeConjureTheme(variant string, colors, ui map[string]string) error {
if variant != "light" && variant != "dark" {
return fmt.Errorf("invalid Conjure variant: %q", variant)
}
mapping := map[string]string{
"background": pick(ui, "global.background.normal", colors, "base00"),
"text": pick(ui, "global.foreground.normal", colors, "base05"),
"secondaryText": pick(ui, "global.foreground.dark", colors, "base04"),
"accent": pick(ui, "accent.normal", colors, "base0D"),
"glow": pick(ui, "status.error", colors, "base08"),
"keyBackground": pick(ui, "chrome.background.normal", colors, "base01"),
"error": pick(ui, "status.error", colors, "base08"),
}
validColor := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
for key, value := range mapping {
if !validColor.MatchString(value) {
return fmt.Errorf("invalid Conjure color %s: %q", key, value)
}
}
configHome := os.Getenv("XDG_CONFIG_HOME")
if !filepath.IsAbs(configHome) {
configHome = filepath.Join(homeDir(), ".config")
}
return writeJSON(filepath.Join(configHome, "conjure", "theme.json"), map[string]any{"variant": variant, "colors": mapping})
}
func writeOMPTheme(colors, ui, syntax map[string]string) error {
semantic, _ := semanticTheme(colors, ui, syntax)
theme := map[string]any{
+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/pumpkin.yaml")
if err != nil {
t.Fatal(err)
}
files := map[string][]byte{
filepath.Join(data, "custom-schemes", "base16", "pumpkin.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":"Pumpkin","themes":[{"name":"Pumpkin","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-pumpkin"}, &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-pumpkin" {
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-pumpkin").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)
}
}
+109
View File
@@ -0,0 +1,109 @@
[current]
light = "base16-swamp-light"
dark = "base16-darktooth"
[[favorites.pairs]]
name = "gruvbox-material-medium"
dark = "base16-gruvbox-material-dark-medium"
light = "base16-gruvbox-material-light-medium"
[[favorites.pairs]]
name = "gruvbox-pale"
dark = "base16-gruvbox-dark-pale"
light = "base16-gruvbox-light-soft"
[[favorites.pairs]]
name = "pumpkin"
dark = "base16-pumpkin"
light = "base16-pumpkin-light"
[[favorites.pairs]]
name = "swamp"
dark = "base16-swamp-dark"
light = "base16-swamp-light"
[favorites]
themes = [
"base16-0x96f",
"base16-arroz-con-dulce",
"base16-ascendancy",
"base16-atelier-plateau-light",
"base16-cacao",
"base16-caroline",
"base16-chinoiserie-midnight",
"base16-corduroy",
"base16-cupcake",
"base16-darktooth",
"base16-embers",
"base16-gruvbox-dark-pale",
"base16-pastelon-de-amarillos-dark",
"base16-precious-light-warm",
"base16-rose-pine-dawn",
"base16-selenized-light",
"base16-swamp-dark",
"base16-swamp-light",
"base24-ic-orange-ppl",
"base24-later-this-evening",
"tinted8-gruvbox-dark",
]
[[others.pairs]]
name = "amarillos"
dark = "base16-pastelon-de-amarillos-dark"
light = "base16-pastelon-de-amarillos"
[[others.pairs]]
name = "charcoal"
dark = "base16-charcoal-dark"
light = "base16-charcoal-light"
[[others.pairs]]
name = "embers"
dark = "base16-embers"
light = "base16-embers-light"
[[others.pairs]]
name = "gruvbox-material-hard"
dark = "base16-gruvbox-material-dark-hard"
light = "base16-gruvbox-material-light-hard"
[[others.pairs]]
name = "gruvbox-material-soft"
dark = "base16-gruvbox-material-dark-soft"
light = "base16-gruvbox-material-light-soft"
[[others.pairs]]
name = "pale-swamp"
dark = "base16-gruvbox-dark-pale"
light = "base16-swamp-light"
[others]
themes = [
"base16-apprentice",
"base16-ashes",
"base16-atelier-heath",
"base16-atelier-heath-light",
"base16-black-metal",
"base16-black-metal-bathory",
"base16-black-metal-khold",
"base16-black-metal-mayhem",
"base16-chinoiserie-morandi",
"base16-corduroy-dark",
"base16-da-one-gray",
"base16-default-dark",
"base16-equilibrium-light",
"base16-helios",
"base16-kanagawa-dragon",
"base16-mezcal",
"base16-railscasts",
"base16-sandcastle",
"base16-tarot",
"base16-vesper",
"base16-zenbones",
"base24-alucard",
"base24-earthsong",
"base24-flexoki-light",
"base24-mountain",
"base24-pastelon-de-amarillos",
"base24-sleepy-hollow",
]
+100
View File
@@ -0,0 +1,100 @@
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestInstallFromFreshCheckout(t *testing.T) {
tmp := t.TempDir()
source := filepath.Join(tmp, "source")
bin := filepath.Join(tmp, "bin")
for _, dir := range []string{source, bin} {
if err := os.Mkdir(dir, 0o755); err != nil {
t.Fatal(err)
}
}
entries, err := os.ReadDir(".")
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
if !entry.Type().IsRegular() || entry.Name() == "state.toml" {
continue
}
data, err := os.ReadFile(entry.Name())
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(source, entry.Name()), data, 0o644); err != nil {
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 ;;
esac
`)
for _, name := range []string{"dark-notify", "launchctl"} {
writeExecutable(t, filepath.Join(bin, name), "#!/bin/sh\nexit 0\n")
}
home := filepath.Join(tmp, "home")
config := filepath.Join(home, ".config")
log := filepath.Join(tmp, "applied")
cache, err := exec.Command("go", "env", "GOMODCACHE").Output()
if err != nil {
t.Fatal(err)
}
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)
cmd.Dir = source
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("install failed: %v\n%s", err, output)
}
}
install()
statePath := filepath.Join(config, "tinted-theming", "tinty", "state.toml")
value, err := loadState(statePath)
if err != nil {
t.Fatalf("fresh install did not provide usable state: %v", err)
}
cmd := exec.Command(filepath.Join(home, ".local", "bin", "tint"), "swamp")
cmd.Env = append(os.Environ(), "TINT_STATE_FILE="+statePath, "TINTY_PAIR_APPEARANCE=dark")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("selecting an installed pair failed: %v\n%s", err, output)
}
if got := strings.TrimSpace(readFile(t, log)); got != "base16-swamp-dark" {
t.Fatalf("applied theme = %q, want base16-swamp-dark", got)
}
value.Current = currentPair{Light: "personal-light", Dark: "personal-dark"}
if err := saveState(statePath, value); err != nil {
t.Fatal(err)
}
before, err := os.ReadFile(statePath)
if err != nil {
t.Fatal(err)
}
install()
after, err := os.ReadFile(statePath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(before, after) {
t.Fatal("reinstall overwrote personal state")
}
}
+5
View File
@@ -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()
}
+137
View File
@@ -138,6 +138,7 @@ func TestApplyAppsUsesTinted8Semantics(t *testing.T) {
"black-normal": {Hex: "#000000"}, "black-bright": {Hex: "#111111"},
"gray-dim": {Hex: "#222222"}, "gray-normal": {Hex: "#333333"}, "gray-bright": {Hex: "#444444"},
"white-normal": {Hex: "#eeeeee"}, "white-bright": {Hex: "#ffffff"},
"black-dim": {Hex: "#111111"}, "white-dim": {Hex: "#dddddd"},
"red-normal": {Hex: "#aa0000"}, "orange-normal": {Hex: "#bb5500"},
"yellow-normal": {Hex: "#aaaa00"}, "green-normal": {Hex: "#00aa00"},
"cyan-normal": {Hex: "#00aaaa"}, "blue-normal": {Hex: "#0000aa"},
@@ -167,6 +168,7 @@ case "$1" in
esac
`)
t.Setenv("HOME", tmp)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(tmp, "xdg"))
t.Setenv("PATH", bin+":"+os.Getenv("PATH"))
t.Setenv("TINTY_TEST_SCHEME", scheme.ID)
t.Setenv("TINTY_TEST_LIST", listPath)
@@ -180,6 +182,14 @@ esac
if err := os.WriteFile(zed, []byte(`{"name":"old","themes":[{"name":"old"}]}`), 0o644); err != nil {
t.Fatal(err)
}
conjureConfig := filepath.Join(tmp, "xdg", "conjure", "config.toml")
if err := os.MkdirAll(filepath.Dir(conjureConfig), 0o755); err != nil {
t.Fatal(err)
}
const userConfig = "theme = \"tint\"\n\n[colors]\naccent = \"#abcdef\"\n"
if err := os.WriteFile(conjureConfig, []byte(userConfig), 0o644); err != nil {
t.Fatal(err)
}
if err := applyApps(zed); err != nil {
t.Fatal(err)
@@ -201,6 +211,58 @@ esac
if !strings.Contains(zedOutput, `"name": "Tinty Current"`) {
t.Fatalf("Zed theme was not renamed:\n%s", zedOutput)
}
var conjure struct {
Variant string `json:"variant"`
Colors map[string]string `json:"colors"`
}
if err := json.Unmarshal([]byte(readFile(t, filepath.Join(filepath.Dir(conjureConfig), "theme.json"))), &conjure); err != nil {
t.Fatal(err)
}
if conjure.Variant != "dark" {
t.Fatalf("Conjure variant = %q, want dark", conjure.Variant)
}
expected := map[string]string{
"background": "#010101", "text": "#fefefe", "secondaryText": "#444444",
"accent": "#123456", "glow": "#aa0000", "keyBackground": "#111111", "error": "#aa0000",
}
if len(conjure.Colors) != len(expected) {
t.Fatalf("wrong Conjure roles: %#v", conjure.Colors)
}
for key, value := range expected {
if conjure.Colors[key] != value {
t.Errorf("Conjure %s = %q, want %q", key, conjure.Colors[key], value)
}
}
if readFile(t, conjureConfig) != userConfig {
t.Fatal("Conjure user configuration changed")
}
scheme.Variant = "light"
expected["secondaryText"] = "#222222"
expected["keyBackground"] = "#dddddd"
data, err = json.Marshal([]tintyScheme{scheme})
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(listPath, data, 0o644); err != nil {
t.Fatal(err)
}
if err := applyApps(zed); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal([]byte(readFile(t, filepath.Join(filepath.Dir(conjureConfig), "theme.json"))), &conjure); err != nil {
t.Fatal(err)
}
if conjure.Variant != "light" {
t.Fatalf("Conjure variant = %q, want light", conjure.Variant)
}
for key, value := range expected {
if conjure.Colors[key] != value {
t.Errorf("light Conjure %s = %q, want %q", key, conjure.Colors[key], value)
}
}
if readFile(t, conjureConfig) != userConfig {
t.Fatal("Conjure user configuration changed")
}
}
func TestBase16SemanticFallback(t *testing.T) {
@@ -226,6 +288,81 @@ func TestBase16SemanticFallback(t *testing.T) {
}
}
func TestConjureThemeFallbacks(t *testing.T) {
for _, system := range []string{"base16", "base24"} {
for _, xdg := range []string{"", "relative", "absolute"} {
t.Run(system+"/"+xdg, func(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
configHome := filepath.Join(home, ".config")
value := xdg
if xdg == "absolute" {
value = filepath.Join(home, "xdg")
configHome = value
}
t.Setenv("XDG_CONFIG_HOME", value)
palette := make(map[string]tintyColor)
for i := range 24 {
palette[fmt.Sprintf("base%02X", i)] = tintyColor{Hex: fmt.Sprintf("#%06x", i)}
}
colors, err := semanticColors(tintyScheme{System: system, Palette: palette})
if err != nil {
t.Fatal(err)
}
if err := writeConjureTheme("dark", colors, nil); err != nil {
t.Fatal(err)
}
var theme struct {
Colors map[string]string `json:"colors"`
}
if err := json.Unmarshal([]byte(readFile(t, filepath.Join(configHome, "conjure", "theme.json"))), &theme); err != nil {
t.Fatal(err)
}
for key, base := range map[string]string{
"background": "base00", "text": "base05", "secondaryText": "base04",
"accent": "base0D", "glow": "base08", "keyBackground": "base01", "error": "base08",
} {
if theme.Colors[key] != colors[base] {
t.Errorf("%s = %q, want %q", key, theme.Colors[key], colors[base])
}
}
})
}
}
}
func TestConjureThemeRejectsInvalidColors(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("XDG_CONFIG_HOME", "")
for _, value := range []string{"", "#12345", "#12345678", "#gggggg", "#123456"} {
t.Run(value, func(t *testing.T) {
colors := map[string]string{
"base00": "#000000", "base01": "#111111", "base04": "#444444",
"base05": "#555555", "base08": "#880000", "base0D": value,
}
if err := writeConjureTheme("dark", colors, nil); err == nil {
t.Fatalf("accepted invalid accent %q", value)
}
})
}
}
func TestConjureThemeRejectsInvalidVariants(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("XDG_CONFIG_HOME", "")
colors := map[string]string{
"base00": "#000000", "base01": "#111111", "base04": "#444444",
"base05": "#555555", "base08": "#880000", "base0D": "#0000dd",
}
for _, variant := range []string{"", "auto", "Light", "DARK"} {
t.Run(variant, func(t *testing.T) {
if err := writeConjureTheme(variant, colors, nil); err == nil {
t.Fatalf("accepted invalid variant %q", variant)
}
})
}
}
func writeExecutable(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
+23
View File
@@ -0,0 +1,23 @@
# Matching light companion to Pumpkin; deeper accents for parchment backgrounds.
# Styling roles: https://github.com/tinted-theming/home/blob/main/styling.md
system: "base16"
name: "Pumpkin Light"
author: "Terebi"
variant: "light"
palette:
base00: "#fff6e6" # Parchment background
base01: "#f4e6ce" # Raised surface
base02: "#e6ceaa" # Selection
base03: "#7c624e" # Comments / muted text
base04: "#70543f" # Secondary foreground
base05: "#453126" # Brown foreground
base06: "#30221b" # Deep foreground
base07: "#1f1714" # Darkest foreground
base08: "#b13425" # Red — variables / errors / deletions
base09: "#a64b00" # Burnt orange — numbers / constants
base0A: "#8a621c" # Toasted amber — types / search
base0B: "#52651f" # Olive — strings / additions
base0C: "#995b2b" # Caramel — escapes / support / inverted terminal bars
base0D: "#b34d00" # Pumpkin orange — functions / headings / UI accent
base0E: "#b63d24" # Persimmon — keywords
base0F: "#c2452e" # Ember — deprecated
+24
View File
@@ -0,0 +1,24 @@
# Palette source: terebi/Themes/pumpkin.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: "Pumpkin"
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: "#d6c56b" # Harvest gold — escapes / support
base0D: "#ff9e3b" # Pumpkin orange — functions / headings / UI accent
base0E: "#ef7956" # Persimmon — keywords
base0F: "#c2452e" # Brown/red — deprecated; app ember (3.51:1 on base00)