external_theme_test.go RawHistoryFolder

1package config
2
3import (
4	"context"
5	"os"
6	"path/filepath"
7	"testing"
8
9	"github.com/bnema/dumber/internal/application/dto"
10	"github.com/spf13/viper"
11	"github.com/stretchr/testify/require"
12)
13
14func TestDefaultConfig_ExternalThemeDefaults(t *testing.T) {
15	cfg := DefaultConfig()
16
17	require.False(t, cfg.Appearance.ExternalTheme.Enabled)
18	require.Equal(t, "noctalia", cfg.Appearance.ExternalTheme.Provider)
19	require.Equal(t, "colors-json", cfg.Appearance.ExternalTheme.Format)
20	require.Equal(t, filepath.Join("noctalia", "colors.json"), lastPathElements(cfg.Appearance.ExternalTheme.Path, 2))
21}
22
23func TestManagerSetDefaults_ExternalThemeViperDefaults(t *testing.T) {
24	mgr := &Manager{viper: viper.New()}
25	mgr.setDefaults()
26
27	require.False(t, mgr.viper.GetBool("appearance.external_theme.enabled"))
28	require.Equal(t, "noctalia", mgr.viper.GetString("appearance.external_theme.provider"))
29	require.Equal(t, "colors-json", mgr.viper.GetString("appearance.external_theme.format"))
30	require.Empty(t, mgr.viper.GetString("appearance.external_theme.path"))
31}
32
33func TestManagerLoad_ExternalThemeEnabledEmptyPathUsesDefaultColorsJSON(t *testing.T) {
34	configHome := t.TempDir()
35	t.Setenv("ENV", "")
36	t.Setenv("XDG_CONFIG_HOME", configHome)
37
38	configDir := filepath.Join(configHome, appName)
39	require.NoError(t, os.MkdirAll(configDir, dirPerm))
40	require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.toml"), []byte(`
41[appearance.external_theme]
42enabled = true
43provider = 'noctalia'
44path = ''
45`), filePerm))
46
47	mgr, err := NewManager()
48	require.NoError(t, err)
49	require.NoError(t, mgr.Load())
50
51	externalTheme := mgr.Get().Appearance.ExternalTheme
52	require.True(t, externalTheme.Enabled)
53	require.Equal(t, "noctalia", externalTheme.Provider)
54	require.Equal(t, "colors-json", externalTheme.Format)
55	require.Equal(t, filepath.Join(configHome, "noctalia", "colors.json"), externalTheme.Path)
56}
57
58func TestManagerLoad_ExternalThemeDumberJSONEmptyPathUsesTemplateDefault(t *testing.T) {
59	configHome := t.TempDir()
60	t.Setenv("ENV", "")
61	t.Setenv("XDG_CONFIG_HOME", configHome)
62
63	configDir := filepath.Join(configHome, appName)
64	require.NoError(t, os.MkdirAll(configDir, dirPerm))
65	require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.toml"), []byte(`
66[appearance.external_theme]
67enabled = true
68provider = 'noctalia'
69format = 'dumber-json'
70path = ''
71`), filePerm))
72
73	mgr, err := NewManager()
74	require.NoError(t, err)
75	require.NoError(t, mgr.Load())
76
77	externalTheme := mgr.Get().Appearance.ExternalTheme
78	require.True(t, externalTheme.Enabled)
79	require.Equal(t, "noctalia", externalTheme.Provider)
80	require.Equal(t, "dumber-json", externalTheme.Format)
81	require.Equal(t, filepath.Join(configDir, defaultExternalThemeTemplateFilename), externalTheme.Path)
82}
83
84func TestManagerLoad_ExternalThemeDumberJSONOmittedPathUsesTemplateDefault(t *testing.T) {
85	configHome := t.TempDir()
86	t.Setenv("ENV", "")
87	t.Setenv("XDG_CONFIG_HOME", configHome)
88
89	configDir := filepath.Join(configHome, appName)
90	require.NoError(t, os.MkdirAll(configDir, dirPerm))
91	require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.toml"), []byte(`
92[appearance.external_theme]
93enabled = true
94provider = 'noctalia'
95format = 'dumber-json'
96`), filePerm))
97
98	mgr, err := NewManager()
99	require.NoError(t, err)
100	require.NoError(t, mgr.Load())
101
102	externalTheme := mgr.Get().Appearance.ExternalTheme
103	require.True(t, externalTheme.Enabled)
104	require.Equal(t, "noctalia", externalTheme.Provider)
105	require.Equal(t, "dumber-json", externalTheme.Format)
106	require.Equal(t, filepath.Join(configDir, defaultExternalThemeTemplateFilename), externalTheme.Path)
107}
108
109func TestValidateConfig_ExternalTheme(t *testing.T) {
110	tests := []struct {
111		name     string
112		mutate   func(*Config)
113		wantErr  bool
114		wantText string
115	}{
116		{
117			name: "disabled empty path validates",
118			mutate: func(cfg *Config) {
119				cfg.Appearance.ExternalTheme.Path = ""
120			},
121		},
122		{
123			name: "enabled colors-json with path validates",
124			mutate: func(cfg *Config) {
125				cfg.Appearance.ExternalTheme.Enabled = true
126				cfg.Appearance.ExternalTheme.Format = "colors-json"
127				cfg.Appearance.ExternalTheme.Path = "/tmp/colors.json"
128			},
129		},
130		{
131			name: "enabled dumber-json with path validates",
132			mutate: func(cfg *Config) {
133				cfg.Appearance.ExternalTheme.Enabled = true
134				cfg.Appearance.ExternalTheme.Format = "dumber-json"
135				cfg.Appearance.ExternalTheme.Path = "/tmp/theme.json"
136			},
137		},
138		{
139			name: "enabled empty path uses format-specific default",
140			mutate: func(cfg *Config) {
141				cfg.Appearance.ExternalTheme.Enabled = true
142				cfg.Appearance.ExternalTheme.Path = ""
143			},
144		},
145		{
146			name: "enabled rejects unsupported provider",
147			mutate: func(cfg *Config) {
148				cfg.Appearance.ExternalTheme.Enabled = true
149				cfg.Appearance.ExternalTheme.Provider = "other"
150				cfg.Appearance.ExternalTheme.Path = "/tmp/theme.json"
151			},
152			wantErr:  true,
153			wantText: "appearance.external_theme.provider",
154		},
155		{
156			name: "enabled rejects unsupported format",
157			mutate: func(cfg *Config) {
158				cfg.Appearance.ExternalTheme.Enabled = true
159				cfg.Appearance.ExternalTheme.Format = "toml"
160				cfg.Appearance.ExternalTheme.Path = "/tmp/theme.json"
161			},
162			wantErr:  true,
163			wantText: "appearance.external_theme.format",
164		},
165	}
166
167	for _, tt := range tests {
168		t.Run(tt.name, func(t *testing.T) {
169			cfg := DefaultConfig()
170			tt.mutate(cfg)
171			normalizeConfig(cfg)
172
173			err := validateConfig(cfg)
174			if tt.wantErr {
175				require.Error(t, err)
176				require.Contains(t, err.Error(), tt.wantText)
177				return
178			}
179			require.NoError(t, err)
180		})
181	}
182}
183
184func TestNormalizeConfig_ExternalTheme(t *testing.T) {
185	cfg := DefaultConfig()
186	cfg.Appearance.ExternalTheme.Provider = "  NOCTALIA  "
187	cfg.Appearance.ExternalTheme.Format = "  COLORS-JSON  "
188	cfg.Appearance.ExternalTheme.Path = "  /tmp/colors.json  "
189
190	normalizeConfig(cfg)
191
192	require.Equal(t, "noctalia", cfg.Appearance.ExternalTheme.Provider)
193	require.Equal(t, "colors-json", cfg.Appearance.ExternalTheme.Format)
194	require.Equal(t, "/tmp/colors.json", cfg.Appearance.ExternalTheme.Path)
195
196	cfg.Appearance.ExternalTheme.Provider = ""
197	cfg.Appearance.ExternalTheme.Format = ""
198	normalizeConfig(cfg)
199	require.Equal(t, "noctalia", cfg.Appearance.ExternalTheme.Provider)
200	require.Equal(t, "colors-json", cfg.Appearance.ExternalTheme.Format)
201}
202
203func TestSchemaProvider_ExternalThemeKeys(t *testing.T) {
204	keys := NewSchemaProvider().GetSchema()
205	byKey := map[string]bool{}
206	for _, key := range keys {
207		if key.Key == "appearance.external_theme.provider" {
208			require.Equal(t, SectionAppearance, key.Section)
209			require.Equal(t, "noctalia", key.Default)
210			require.Equal(t, []string{"noctalia"}, key.Values)
211		}
212		if key.Key == "appearance.external_theme.format" {
213			require.Equal(t, SectionAppearance, key.Section)
214			require.Equal(t, "colors-json", key.Default)
215			require.Equal(t, []string{"colors-json", "dumber-json"}, key.Values)
216		}
217		if key.Key == "appearance.external_theme.path" {
218			require.Equal(t, filepath.Join("noctalia", "colors.json"), lastPathElements(key.Default, 2))
219		}
220		byKey[key.Key] = true
221	}
222
223	for _, key := range []string{
224		"appearance.external_theme.enabled",
225		"appearance.external_theme.provider",
226		"appearance.external_theme.format",
227		"appearance.external_theme.path",
228	} {
229		require.True(t, byKey[key], "missing schema key %s", key)
230	}
231}
232
233func TestWriteConfigOrdered_IncludesExternalThemeTable(t *testing.T) {
234	path := filepath.Join(t.TempDir(), "config.toml")
235	cfg := DefaultConfig()
236
237	require.NoError(t, WriteConfigOrdered(cfg, path))
238	content, err := os.ReadFile(path)
239	require.NoError(t, err)
240	require.Contains(t, string(content), "[appearance.external_theme]")
241	require.Contains(t, string(content), "enabled = false")
242	require.Contains(t, string(content), "provider = 'noctalia'")
243	require.Contains(t, string(content), "format = 'colors-json'")
244	require.Contains(t, string(content), "colors.json")
245}
246
247func TestBuildSystemviewConfigPayload_ProjectsExternalTheme(t *testing.T) {
248	cfg := DefaultConfig()
249	cfg.Appearance.ExternalTheme.Enabled = true
250	cfg.Appearance.ExternalTheme.Path = "/tmp/colors.json"
251
252	payload := BuildSystemviewConfigPayload(cfg, nil)
253
254	require.True(t, payload.Appearance.ExternalTheme.Enabled)
255	require.Equal(t, "noctalia", payload.Appearance.ExternalTheme.Provider)
256	require.Equal(t, "colors-json", payload.Appearance.ExternalTheme.Format)
257	require.Equal(t, "/tmp/colors.json", payload.Appearance.ExternalTheme.Path)
258}
259
260func lastPathElements(path string, count int) string {
261	cleaned := filepath.Clean(path)
262	parts := make([]string, 0, count)
263	for len(parts) < count && cleaned != "." && cleaned != string(filepath.Separator) {
264		parts = append([]string{filepath.Base(cleaned)}, parts...)
265		cleaned = filepath.Dir(cleaned)
266	}
267	return filepath.Join(parts...)
268}
269
270func TestWebUIConfigGatewaySaveWebUIConfig_ExternalThemeRoundTrip(t *testing.T) {
271	path := filepath.Join(t.TempDir(), "config.toml")
272	require.NoError(t, os.WriteFile(path, []byte(""), 0o644))
273
274	v := viper.New()
275	v.SetConfigFile(path)
276	require.NoError(t, v.ReadInConfig())
277
278	previousGlobal := globalManager
279	defer func() { globalManager = previousGlobal }()
280
281	mgr := &Manager{config: DefaultConfig(), viper: v}
282	globalManager = mgr
283
284	webCfg := dto.WebUIConfig{
285		Appearance: buildSystemviewAppearancePayload(DefaultConfig().Appearance),
286	}
287	webCfg.Appearance.ExternalTheme = dto.WebUIExternalThemeConfig{
288		Enabled:  true,
289		Provider: " NOCTALIA ",
290		Format:   " COLORS-JSON ",
291		Path:     " /tmp/colors.json ",
292	}
293	webCfg.DefaultUIScale = DefaultConfig().DefaultUIScale
294	webCfg.DefaultSearchEngine = DefaultConfig().DefaultSearchEngine
295	webCfg.SearchShortcuts = map[string]dto.SearchShortcut{}
296	for key, shortcut := range DefaultConfig().SearchShortcuts {
297		webCfg.SearchShortcuts[key] = dto.SearchShortcut{URL: shortcut.URL, Description: shortcut.Description}
298	}
299
300	require.NoError(t, NewWebUIConfigGateway(mgr).SaveWebUIConfig(context.Background(), webCfg))
301
302	saved := mgr.Get()
303	require.True(t, saved.Appearance.ExternalTheme.Enabled)
304	require.Equal(t, "noctalia", saved.Appearance.ExternalTheme.Provider)
305	require.Equal(t, "colors-json", saved.Appearance.ExternalTheme.Format)
306	require.Equal(t, "/tmp/colors.json", saved.Appearance.ExternalTheme.Path)
307}

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 8

Tags 42

↑↓ moveenter openesc close