1package theme
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func put(t *testing.T, dir, rel, body string) string {
11 t.Helper()
12 p := filepath.Join(dir, rel)
13 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
14 t.Fatal(err)
15 }
16 if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
17 t.Fatal(err)
18 }
19 return p
20}
21
22func want(t *testing.T, got Theme, bg, fg RGB, c1, c12 RGB) {
23 t.Helper()
24 if got.Background != bg || got.Foreground != fg || got.Palette[1] != c1 || got.Palette[12] != c12 {
25 t.Errorf("got bg=%v fg=%v c1=%v c12=%v, want bg=%v fg=%v c1=%v c12=%v",
26 got.Background, got.Foreground, got.Palette[1], got.Palette[12], bg, fg, c1, c12)
27 }
28}
29
30var (
31 bg = RGB{0x07, 0x07, 0x22}
32 fg = RGB{0xf3, 0xed, 0xf7}
33 red = RGB{0xfd, 0x46, 0x63}
34 blu = RGB{0xa9, 0xae, 0xfe}
35)
36
37func TestKittyWithInclude(t *testing.T) {
38 d := t.TempDir()
39 put(t, d, "themes/x.conf", "background #070722\nforeground #f3edf7\ncolor1 #fd4663\ncolor12 #a9aefe\n")
40 main := put(t, d, "kitty.conf", "font_size 12\ninclude themes/x.conf\n")
41 got, err := Load(main)
42 if err != nil {
43 t.Fatal(err)
44 }
45 want(t, got, bg, fg, red, blu)
46 if len(got.Files) != 2 {
47 t.Errorf("Files = %v, want the config and the include", got.Files)
48 }
49}
50
51func TestKittyOwnColorsWinOverInclude(t *testing.T) {
52 d := t.TempDir()
53 put(t, d, "t.conf", "color1 #000001\n")
54 main := put(t, d, "kitty.conf", "include t.conf\ncolor1 #fd4663\n")
55 got, _ := Load(main)
56 if got.Palette[1] != red {
57 t.Errorf("c1 = %v, want the file's own %v", got.Palette[1], red)
58 }
59}
60
61func TestFootWithInclude(t *testing.T) {
62 d := t.TempDir()
63 put(t, d, "themes/n", "[colors-dark]\nbackground = 070722\nforeground = f3edf7\nregular1 = fd4663\nbright4 = a9aefe\n")
64 main := put(t, d, "foot.ini", "include=~/nowhere\ninclude=themes/n\n[colors]\nforeground = 000000\n")
65 got, err := Load(main)
66 if err != nil {
67 t.Fatal(err)
68 }
69 // [colors-dark] wins over the legacy [colors], and bright4 is color 12.
70 want(t, got, bg, fg, red, blu)
71}
72
73func TestGhosttyNamedThemeAndOverrides(t *testing.T) {
74 d := t.TempDir()
75 put(t, d, "themes/n", "palette = 1=#fd4663\npalette = 12=#a9aefe\nbackground = #070722\nforeground = #f3edf7\n")
76 main := put(t, d, "config", "theme = n\nforeground = #ffffff\n")
77 got, err := Load(main)
78 if err != nil {
79 t.Fatal(err)
80 }
81 want(t, got, bg, RGB{255, 255, 255}, red, blu) // the config's own foreground wins
82}
83
84func TestGhosttyDarkLightPair(t *testing.T) {
85 d := t.TempDir()
86 put(t, d, "themes/night", "palette = 1=#fd4663\n")
87 put(t, d, "themes/day", "palette = 1=#000000\n")
88 main := put(t, d, "config", "theme = dark:night,light:day\n")
89 got, _ := Load(main)
90 if got.Palette[1] != red {
91 t.Errorf("dark theme not chosen: c1 = %v", got.Palette[1])
92 }
93}
94
95func TestAlacrittyImport(t *testing.T) {
96 d := t.TempDir()
97 put(t, d, "themes/n.toml", "[colors.primary]\nbackground = '#070722'\nforeground = '#f3edf7'\n[colors.normal]\nred = '#fd4663'\n[colors.bright]\nblue = '#a9aefe'\n")
98 main := put(t, d, "alacritty.toml", "[general]\nimport = [\"themes/n.toml\"]\n")
99 got, err := Load(main)
100 if err != nil {
101 t.Fatal(err)
102 }
103 want(t, got, bg, fg, red, blu)
104}
105
106func TestAlacrittyOwnColorsWinOverImport(t *testing.T) {
107 d := t.TempDir()
108 put(t, d, "n.toml", "[colors.primary]\nbackground = '#070722'\n")
109 main := put(t, d, "a.toml", "import = [\"n.toml\"]\n[colors.primary]\nbackground = '#000000'\n")
110 got, _ := Load(main)
111 if got.Background != (RGB{}) {
112 t.Errorf("bg = %v, want the file's own black", got.Background)
113 }
114}
115
116func TestIncludeLoopTerminates(t *testing.T) {
117 d := t.TempDir()
118 put(t, d, "a.conf", "include b.conf\ncolor1 #fd4663\n")
119 put(t, d, "b.conf", "include a.conf\n")
120 got, err := Load(filepath.Join(d, "a.conf"))
121 if err != nil || got.Palette[1] != red {
122 t.Fatalf("loop: err=%v c1=%v", err, got.Palette[1])
123 }
124}
125
126func TestLoadRejectsAFileWithNoColors(t *testing.T) {
127 p := put(t, t.TempDir(), "x.conf", "font_size 12\n")
128 if _, err := Load(p); err == nil {
129 t.Fatal("a file without colors must be an error, not a silent default")
130 }
131 if _, err := Load(filepath.Join(t.TempDir(), "missing")); err == nil {
132 t.Fatal("a missing file must be an error")
133 }
134}
135
136func fakeEnv(dir string, vars map[string]string) Env {
137 return Env{ConfigDir: dir, Getenv: func(k string) string { return vars[k] }}
138}
139
140func TestAutoReadsOnlyTheTerminalVariable(t *testing.T) {
141 d := t.TempDir()
142 put(t, d, "kitty/kitty.conf", "color1 #000001\n")
143 put(t, d, "foot/foot.ini", "[colors]\nregular1 = fd4663\n")
144 for _, value := range []string{"foot", "/usr/bin/foot", "/usr/bin/foot --server", " foot "} {
145 got, err := Auto(fakeEnv(d, map[string]string{"TERMINAL": value}))
146 if err != nil || got.Palette[1] != red {
147 t.Errorf("TERMINAL=%q: err=%v c1=%v", value, err, got.Palette[1])
148 }
149 }
150 // A config for another terminal must not be picked up by accident.
151 if got, _ := Auto(fakeEnv(d, map[string]string{"TERMINAL": "kitty"})); got.Palette[1] != (RGB{0, 0, 1}) {
152 t.Errorf("TERMINAL=kitty read the wrong file: c1=%v", got.Palette[1])
153 }
154}
155
156func TestAutoErrorsAreOneClearSentence(t *testing.T) {
157 d := t.TempDir()
158 put(t, d, "kitty/kitty.conf", "font_size 12\n") // no colors
159 cases := map[string]struct{ env, contains string }{
160 "unset": {"", "needs $TERMINAL"},
161 "blank": {" ", "needs $TERMINAL"},
162 "unknown": {"xterm", "kitty, ghostty, foot and alacritty only"},
163 "no config": {"foot", "does not exist"},
164 "no colors": {"kitty", "sets no colors"},
165 }
166 for name, c := range cases {
167 _, err := Auto(fakeEnv(d, map[string]string{"TERMINAL": c.env}))
168 if err == nil || !strings.Contains(err.Error(), c.contains) {
169 t.Errorf("%s: err = %v, want it to mention %q", name, err, c.contains)
170 }
171 }
172 // The unset message tells the user where to put the variable.
173 _, err := Auto(fakeEnv(d, nil))
174 if err == nil || !strings.Contains(err.Error(), "environment.d") {
175 t.Errorf("unset message must name environment.d: %v", err)
176 }
177}
178
179func TestGhosttySelfReferenceTerminates(t *testing.T) {
180 d := t.TempDir()
181 self := filepath.Join(d, "config")
182 put(t, d, "config", "theme = "+self+"\npalette = 1=#fd4663\n")
183 got, err := Load(self)
184 if err != nil || got.Palette[1] != red {
185 t.Fatalf("self-referencing ghostty theme: err=%v c1=%v", err, got.Palette[1])
186 }
187}
188
189func TestKittyKeysSeparatedByATab(t *testing.T) {
190 p := put(t, t.TempDir(), "k.conf", "color1\t#fd4663\nbackground\t\t#070722\n")
191 got, err := Load(p)
192 if err != nil || got.Palette[1] != red || got.Background != bg {
193 t.Fatalf("tab-separated keys: err=%v c1=%v bg=%v", err, got.Palette[1], got.Background)
194 }
195}
196
197func TestFilesAreDeduplicated(t *testing.T) {
198 d := t.TempDir()
199 put(t, d, "t.conf", "color1 #fd4663\n")
200 main := put(t, d, "k.conf", "include t.conf\ninclude t.conf\ninclude t.conf\n")
201 got, err := Load(main)
202 if err != nil || len(got.Files) != 2 {
203 t.Fatalf("Files = %v (err %v), want the config and one include", got.Files, err)
204 }
205}
206
207func TestResolveFillsGapsFromTheDefault(t *testing.T) {
208 d := t.TempDir()
209 p := put(t, d, "t.conf", "color1 #fd4663\n")
210 got, err := Resolve(p, "", fakeEnv(d, nil))
211 if err != nil {
212 t.Fatal(err)
213 }
214 if got.Palette[1] != red || got.Palette[2] != Default().Palette[2] || got.Background != Default().Background {
215 t.Errorf("gaps not filled: %+v", got.Palette[:3])
216 }
217 // A relative path is taken from the config directory.
218 rel, err := Resolve("t.conf", d, fakeEnv(d, nil))
219 if err != nil || rel.Palette[1] != red {
220 t.Errorf("relative path: err=%v", err)
221 }
222}
223
224func TestParseColorForms(t *testing.T) {
225 for _, s := range []string{"#fd4663", "fd4663", "0xfd4663", `"#fd4663"`, "'fd4663'"} {
226 if c, ok := parseColor(s); !ok || c != red {
227 t.Errorf("parseColor(%q) = %v, %v", s, c, ok)
228 }
229 }
230 for _, s := range []string{"", "#fff", "red", "#gg0000", "#fd466300"} {
231 if _, ok := parseColor(s); ok {
232 t.Errorf("parseColor(%q) accepted", s)
233 }
234 }
235}
236
237func TestEnvVarsCarryTheChosenAccent(t *testing.T) {
238 th := Default()
239 th.Palette[2] = RGB{0x7d, 0xb5, 0xb5}
240 th.Palette[4] = RGB{0x6c, 0x9b, 0xd9}
241 has := func(env []string, want string) bool {
242 for _, e := range env {
243 if e == want {
244 return true
245 }
246 }
247 return false
248 }
249 if env := th.EnvVars(RGB{}, RGB{}, 2); !has(env, "NEFERBAR_ACCENT=#7db5b5") {
250 t.Errorf("accent 2 not exported as the teal: %v", env)
251 }
252 if env := th.EnvVars(RGB{}, RGB{}, 4); !has(env, "NEFERBAR_ACCENT=#6c9bd9") {
253 t.Errorf("accent 4 not exported as the blue: %v", env)
254 }
255}