Compare commits

...

4 Commits

Author SHA1 Message Date
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
4 changed files with 190 additions and 2 deletions
+10
View File
@@ -32,6 +32,7 @@ install:
fi
XDG_CONFIG_HOME="$(XDG_CONFIG_HOME)" tinty sync
@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 +46,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>' \
+14 -2
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
@@ -119,6 +119,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:
+29
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)
}
@@ -186,6 +189,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{
+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 {