diff --git a/Makefile b/Makefile
index 4a94da9..9239b50 100644
--- a/Makefile
+++ b/Makefile
@@ -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' \
'' \
'' \
@@ -45,6 +46,15 @@ install:
' -c' \
' $(BIN_DIR)/tint' \
' ' \
+ ' EnvironmentVariables' \
+ ' ' \
+ ' HOME' \
+ ' $(HOME)' \
+ ' PATH' \
+ " $$tinty_dir:/usr/bin:/bin:/usr/sbin:/sbin" \
+ ' XDG_CONFIG_HOME' \
+ ' $(XDG_CONFIG_HOME)' \
+ ' ' \
' RunAtLoad' \
' ' \
' KeepAlive' \
diff --git a/README.md b/README.md
index 8ac9704..b524886 100644
--- a/README.md
+++ b/README.md
@@ -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 seven opaque colors: `background`, `text`, `secondaryText`, `accent`, `glow`, `keyBackground`, and `error`, using Tinty semantic UI colors with Base16/Base24 or tinted8 palette fallbacks.
+
+Opt in by adding this to `conjure/config.json` in the same configuration directory:
+
+```json
+{"theme":"tint"}
+```
+
+The wrapper never changes `config.json`. Optional entries in its `colors` object override generated colors. Conjure reads configuration only at launch: restart Conjure after applying a theme or editing its configuration. The wrapper does not signal, reload, restart, or notify Conjure.
+
## Usage
Show the current pair and available commands:
diff --git a/apps.go b/apps.go
index 83fe382..ccb1e82 100644
--- a/apps.go
+++ b/apps.go
@@ -40,6 +40,9 @@ func applyApps(zedSource string) error {
if err := writeHerdrTheme(colors, ui, syntax); err != nil {
return err
}
+ if err := writeConjureTheme(colors, ui); err != nil {
+ return err
+ }
return writeZedTheme(zedSource)
}
@@ -186,6 +189,29 @@ func semanticTheme(colors, ui, syntax map[string]string) (map[string]string, map
return omp, herdr
}
+func writeConjureTheme(colors, ui map[string]string) error {
+ 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{"colors": mapping})
+}
+
func writeOMPTheme(colors, ui, syntax map[string]string) error {
semantic, _ := semanticTheme(colors, ui, syntax)
theme := map[string]any{
diff --git a/main_test.go b/main_test.go
index 2a5778a..07dcdcf 100644
--- a/main_test.go
+++ b/main_test.go
@@ -167,6 +167,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 +181,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.json")
+ if err := os.MkdirAll(filepath.Dir(conjureConfig), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ const userConfig = `{"theme":"tint","colors":{"accent":"#abcdef"}}`
+ if err := os.WriteFile(conjureConfig, []byte(userConfig), 0o644); err != nil {
+ t.Fatal(err)
+ }
if err := applyApps(zed); err != nil {
t.Fatal(err)
@@ -201,6 +210,27 @@ esac
if !strings.Contains(zedOutput, `"name": "Tinty Current"`) {
t.Fatalf("Zed theme was not renamed:\n%s", zedOutput)
}
+ var conjure struct {
+ 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)
+ }
+ 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")
+ }
}
func TestBase16SemanticFallback(t *testing.T) {
@@ -226,6 +256,65 @@ 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(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(colors, nil); err == nil {
+ t.Fatalf("accepted invalid accent %q", value)
+ }
+ })
+ }
+}
+
func writeExecutable(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {