Compare commits
4 Commits
08abf6393f
...
92200c76ec
| Author | SHA1 | Date | |
|---|---|---|---|
| 92200c76ec | |||
| b42e789f29 | |||
| 586e1c75e7 | |||
| c60890eb4a |
@@ -32,6 +32,7 @@ install:
|
|||||||
fi
|
fi
|
||||||
XDG_CONFIG_HOME="$(XDG_CONFIG_HOME)" tinty sync
|
XDG_CONFIG_HOME="$(XDG_CONFIG_HOME)" tinty sync
|
||||||
@dark_notify="$$(command -v dark-notify)"; \
|
@dark_notify="$$(command -v dark-notify)"; \
|
||||||
|
tinty_dir="$$(dirname "$$(command -v tinty)")"; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
'<?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">' \
|
'<!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>-c</string>' \
|
||||||
' <string>$(BIN_DIR)/tint</string>' \
|
' <string>$(BIN_DIR)/tint</string>' \
|
||||||
' </array>' \
|
' </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>' \
|
' <key>RunAtLoad</key>' \
|
||||||
' <true/>' \
|
' <true/>' \
|
||||||
' <key>KeepAlive</key>' \
|
' <key>KeepAlive</key>' \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# tint
|
# 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
|
## Requirements
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ brew install tinty go
|
|||||||
brew install cormacrelf/tap/dark-notify
|
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
|
## Install
|
||||||
|
|
||||||
@@ -119,6 +119,18 @@ The wrapper writes:
|
|||||||
|
|
||||||
Configure OMP to use `tinty-current` where required.
|
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
|
## Usage
|
||||||
|
|
||||||
Show the current pair and available commands:
|
Show the current pair and available commands:
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ func applyApps(zedSource string) error {
|
|||||||
if err := writeHerdrTheme(colors, ui, syntax); err != nil {
|
if err := writeHerdrTheme(colors, ui, syntax); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := writeConjureTheme(scheme.Variant, colors, ui); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return writeZedTheme(zedSource)
|
return writeZedTheme(zedSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +189,32 @@ func semanticTheme(colors, ui, syntax map[string]string) (map[string]string, map
|
|||||||
return omp, herdr
|
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 {
|
func writeOMPTheme(colors, ui, syntax map[string]string) error {
|
||||||
semantic, _ := semanticTheme(colors, ui, syntax)
|
semantic, _ := semanticTheme(colors, ui, syntax)
|
||||||
theme := map[string]any{
|
theme := map[string]any{
|
||||||
|
|||||||
+137
@@ -138,6 +138,7 @@ func TestApplyAppsUsesTinted8Semantics(t *testing.T) {
|
|||||||
"black-normal": {Hex: "#000000"}, "black-bright": {Hex: "#111111"},
|
"black-normal": {Hex: "#000000"}, "black-bright": {Hex: "#111111"},
|
||||||
"gray-dim": {Hex: "#222222"}, "gray-normal": {Hex: "#333333"}, "gray-bright": {Hex: "#444444"},
|
"gray-dim": {Hex: "#222222"}, "gray-normal": {Hex: "#333333"}, "gray-bright": {Hex: "#444444"},
|
||||||
"white-normal": {Hex: "#eeeeee"}, "white-bright": {Hex: "#ffffff"},
|
"white-normal": {Hex: "#eeeeee"}, "white-bright": {Hex: "#ffffff"},
|
||||||
|
"black-dim": {Hex: "#111111"}, "white-dim": {Hex: "#dddddd"},
|
||||||
"red-normal": {Hex: "#aa0000"}, "orange-normal": {Hex: "#bb5500"},
|
"red-normal": {Hex: "#aa0000"}, "orange-normal": {Hex: "#bb5500"},
|
||||||
"yellow-normal": {Hex: "#aaaa00"}, "green-normal": {Hex: "#00aa00"},
|
"yellow-normal": {Hex: "#aaaa00"}, "green-normal": {Hex: "#00aa00"},
|
||||||
"cyan-normal": {Hex: "#00aaaa"}, "blue-normal": {Hex: "#0000aa"},
|
"cyan-normal": {Hex: "#00aaaa"}, "blue-normal": {Hex: "#0000aa"},
|
||||||
@@ -167,6 +168,7 @@ case "$1" in
|
|||||||
esac
|
esac
|
||||||
`)
|
`)
|
||||||
t.Setenv("HOME", tmp)
|
t.Setenv("HOME", tmp)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(tmp, "xdg"))
|
||||||
t.Setenv("PATH", bin+":"+os.Getenv("PATH"))
|
t.Setenv("PATH", bin+":"+os.Getenv("PATH"))
|
||||||
t.Setenv("TINTY_TEST_SCHEME", scheme.ID)
|
t.Setenv("TINTY_TEST_SCHEME", scheme.ID)
|
||||||
t.Setenv("TINTY_TEST_LIST", listPath)
|
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 {
|
if err := os.WriteFile(zed, []byte(`{"name":"old","themes":[{"name":"old"}]}`), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if err := applyApps(zed); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -201,6 +211,58 @@ esac
|
|||||||
if !strings.Contains(zedOutput, `"name": "Tinty Current"`) {
|
if !strings.Contains(zedOutput, `"name": "Tinty Current"`) {
|
||||||
t.Fatalf("Zed theme was not renamed:\n%s", zedOutput)
|
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) {
|
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) {
|
func writeExecutable(t *testing.T, path, content string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user