Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92200c76ec | |||
| b42e789f29 | |||
| 586e1c75e7 | |||
| c60890eb4a |
@@ -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>' \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user