v0.2.0 c616855

theme_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 5

Tags 4

↑↓ moveenter openesc close