feat/freeze-key 753c87f

config_test.go RawHistoryFolder

1package config
2
3import (
4	"os"
5	"path/filepath"
6	"strings"
7	"syscall"
8	"testing"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12)
13
14func configRoot(t *testing.T) string {
15	t.Helper()
16	root := t.TempDir()
17	t.Setenv("XDG_CONFIG_HOME", root)
18	return filepath.Join(root, "nefercap", "config")
19}
20
21func saveConfig(t *testing.T, path, text string) {
22	t.Helper()
23	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
24	require.NoError(t, os.WriteFile(path, []byte(text), 0o600))
25}
26
27func TestLoadDefault(t *testing.T) {
28	configRoot(t)
29	got, err := Load()
30	require.NoError(t, err)
31	assert.True(t, got.Grid)
32}
33
34func TestLoadGrid(t *testing.T) {
35	for _, tc := range []struct {
36		name, text string
37		grid       bool
38	}{
39		{"empty", "", true},
40		{"comments", "# selector settings\n\n", true},
41		{"on", "selector.grid = on\n", true},
42		{"off", "selector.grid = off\n", false},
43		{"whitespace", "  selector.grid\t= off \n", false},
44		{"CRLF", "# config\r\nselector.grid = off\r\n", false},
45		{"BOM", "\ufeffselector.grid = off\n", false},
46	} {
47		t.Run(tc.name, func(t *testing.T) {
48			path := configRoot(t)
49			saveConfig(t, path, tc.text)
50			got, err := Load()
51			require.NoError(t, err)
52			assert.Equal(t, tc.grid, got.Grid)
53		})
54	}
55}
56
57func TestLoadRejectsInvalid(t *testing.T) {
58	for _, tc := range []struct{ text, want string }{
59		{"selector.grid = maybe", "line 1: selector.grid must be on or off"},
60		{"selector.grid", "line 1: expected each of"},
61		{"selector.grid =", "line 1: selector.grid must be on or off"},
62		{"selector.other = on", "line 1: expected each of"},
63		{"selector.grid = on\nselector.grid = off", "line 2: expected each of"},
64		{"selector.freeze = yes", "line 1: selector.freeze must be on or off"},
65		{"selector.freeze = on\nselector.freeze = on", "line 2: expected each of"},
66		{strings.Repeat("#", maxBytes+1), "exceeds 4096 bytes"},
67	} {
68		t.Run(tc.text[:min(len(tc.text), 40)], func(t *testing.T) {
69			path := configRoot(t)
70			saveConfig(t, path, tc.text)
71			_, err := Load()
72			require.ErrorContains(t, err, tc.want)
73			assert.Contains(t, err.Error(), path)
74		})
75	}
76}
77
78func TestLoadHomeFallback(t *testing.T) {
79	home := t.TempDir()
80	t.Setenv("HOME", home)
81	t.Setenv("XDG_CONFIG_HOME", "")
82	saveConfig(t, filepath.Join(home, ".config", "nefercap", "config"), "selector.grid = off\n")
83	got, err := Load()
84	require.NoError(t, err)
85	assert.False(t, got.Grid)
86}
87
88func TestLoadRelativeXDGUsesHome(t *testing.T) {
89	home := t.TempDir()
90	t.Setenv("HOME", home)
91	t.Setenv("XDG_CONFIG_HOME", "relative-config")
92	saveConfig(t, filepath.Join(home, ".config", "nefercap", "config"), "selector.grid = off\n")
93	got, err := Load()
94	require.NoError(t, err)
95	assert.False(t, got.Grid)
96}
97
98func TestLoadNoConfigDirectoryUsesDefaults(t *testing.T) {
99	t.Setenv("HOME", "")
100	t.Setenv("XDG_CONFIG_HOME", "")
101	got, err := Load()
102	require.NoError(t, err)
103	assert.True(t, got.Grid)
104}
105
106func TestLoadFIFORejectedWithoutBlocking(t *testing.T) {
107	path := configRoot(t)
108	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
109	require.NoError(t, syscall.Mkfifo(path, 0o600))
110	_, err := Load()
111	require.ErrorContains(t, err, "expected a regular file")
112}
113
114func TestLoadSizeBoundary(t *testing.T) {
115	path := configRoot(t)
116	saveConfig(t, path, strings.Repeat("#", maxBytes))
117	got, err := Load()
118	require.NoError(t, err)
119	assert.True(t, got.Grid)
120}
121
122func TestLoadDirectoryRejected(t *testing.T) {
123	path := configRoot(t)
124	require.NoError(t, os.MkdirAll(path, 0o700))
125	_, err := Load()
126	require.ErrorContains(t, err, "expected a regular file")
127}
128
129func TestLoadPathsAndOutput(t *testing.T) {
130	home := t.TempDir()
131	t.Setenv("HOME", home)
132	path := configRoot(t)
133	saveConfig(t, path, "# c\nscreenshot.dir = ~/Pics/Shots\nvideo.dir=/var/tmp/vids/\nscreenshot.output = file+clipboard\nselector.grid = off\n")
134	got, err := Load()
135	require.NoError(t, err)
136	assert.Equal(t, Settings{Grid: false, Freeze: true, ScreenshotDir: filepath.Join(home, "Pics", "Shots"), VideoDir: "/var/tmp/vids", Output: OutputFileClipboard}, got)
137
138	saveConfig(t, path, "screenshot.dir = ~/\nscreenshot.output = clipboard\n")
139	got, err = Load()
140	require.NoError(t, err)
141	assert.Equal(t, home, got.ScreenshotDir)
142	assert.Equal(t, OutputClipboard, got.Output)
143	assert.Empty(t, got.VideoDir)
144}
145
146func TestLoadDefaultsOutputFile(t *testing.T) {
147	configRoot(t)
148	got, err := Load()
149	require.NoError(t, err)
150	assert.Equal(t, Settings{Grid: true, Freeze: true, Output: OutputFile}, got)
151}
152
153func TestLoadFreeze(t *testing.T) {
154	for _, tc := range []struct {
155		text   string
156		freeze bool
157	}{
158		{"", true},
159		{"selector.freeze = on\n", true},
160		{"selector.freeze = off\n", false},
161	} {
162		t.Run(tc.text, func(t *testing.T) {
163			saveConfig(t, configRoot(t), tc.text)
164			got, err := Load()
165			require.NoError(t, err)
166			assert.Equal(t, tc.freeze, got.Freeze)
167			assert.True(t, got.Grid, "independent of selector.grid")
168		})
169	}
170}
171
172func TestLoadRejectsInvalidNewKeys(t *testing.T) {
173	for _, tc := range []struct{ text, want string }{
174		{"screenshot.dir = Pictures", "line 1: screenshot.dir must be an absolute path or start with ~/"},
175		{"video.dir = ./v", "line 1: video.dir must be an absolute path"},
176		{"video.dir = ~user/v", "line 1: video.dir must be an absolute path"},
177		{"video.dir = ~", "line 1: video.dir must be an absolute path"},
178		{"video.dir =", "line 1: video.dir must be a non-empty path"},
179		{"screenshot.output = both", "line 1: screenshot.output must be file, file+clipboard or clipboard"},
180		{"screenshot.output = File", "screenshot.output must be"},
181		{"screenshot.dir = /a\nscreenshot.dir = /b", "line 2: expected each of"},
182		{"screenshot.output = file\n\nscreenshot.output = file", "line 3: expected each of"},
183		{"video.dir = /a\nvideo.dir = /a", "line 2: expected each of"},
184		{"video.output = file", "line 1: expected each of"},
185		// The first problem in line order is reported.
186		{"selector.grid = maybe\nselector.grid = on", "line 1: selector.grid must be on or off"},
187		{"selector.grid = maybe\nnoequals", "line 1: selector.grid must be on or off"},
188		{"selector.grid = maybe\n= x", "line 1: selector.grid must be on or off"},
189		{"selector.grid = maybe\nscreenshot.dir = /a\nscreenshot.dir = /b", "line 1: selector.grid must be on or off"},
190		{"foo = 1\nbar", "line 1: expected each of"},
191		{"selector.grid = on\nselector.grid = maybe", "line 2: expected each of"},
192		// '#' only starts a comment at the start of a line; values stay raw.
193		{"selector.grid = on # c", "line 1: selector.grid must be on or off"},
194		{"screenshot.output = \"file\"", "line 1: screenshot.output must be"},
195		{"# caf\xe9\nselector.grid = on", "not valid UTF-8"},
196	} {
197		t.Run(tc.text, func(t *testing.T) {
198			t.Setenv("HOME", t.TempDir())
199			path := configRoot(t)
200			saveConfig(t, path, tc.text)
201			_, err := Load()
202			require.ErrorContains(t, err, tc.want)
203			assert.Contains(t, err.Error(), path)
204		})
205	}
206}

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 2

Tags 9

↑↓ moveenter openesc close