v0.3.0 ea9abc7

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		{strings.Repeat("#", maxBytes+1), "exceeds 4096 bytes"},
65	} {
66		t.Run(tc.text[:min(len(tc.text), 40)], func(t *testing.T) {
67			path := configRoot(t)
68			saveConfig(t, path, tc.text)
69			_, err := Load()
70			require.ErrorContains(t, err, tc.want)
71			assert.Contains(t, err.Error(), path)
72		})
73	}
74}
75
76func TestLoadHomeFallback(t *testing.T) {
77	home := t.TempDir()
78	t.Setenv("HOME", home)
79	t.Setenv("XDG_CONFIG_HOME", "")
80	saveConfig(t, filepath.Join(home, ".config", "nefercap", "config"), "selector.grid = off\n")
81	got, err := Load()
82	require.NoError(t, err)
83	assert.False(t, got.Grid)
84}
85
86func TestLoadRelativeXDGUsesHome(t *testing.T) {
87	home := t.TempDir()
88	t.Setenv("HOME", home)
89	t.Setenv("XDG_CONFIG_HOME", "relative-config")
90	saveConfig(t, filepath.Join(home, ".config", "nefercap", "config"), "selector.grid = off\n")
91	got, err := Load()
92	require.NoError(t, err)
93	assert.False(t, got.Grid)
94}
95
96func TestLoadNoConfigDirectoryUsesDefaults(t *testing.T) {
97	t.Setenv("HOME", "")
98	t.Setenv("XDG_CONFIG_HOME", "")
99	got, err := Load()
100	require.NoError(t, err)
101	assert.True(t, got.Grid)
102}
103
104func TestLoadFIFORejectedWithoutBlocking(t *testing.T) {
105	path := configRoot(t)
106	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
107	require.NoError(t, syscall.Mkfifo(path, 0o600))
108	_, err := Load()
109	require.ErrorContains(t, err, "expected a regular file")
110}
111
112func TestLoadSizeBoundary(t *testing.T) {
113	path := configRoot(t)
114	saveConfig(t, path, strings.Repeat("#", maxBytes))
115	got, err := Load()
116	require.NoError(t, err)
117	assert.True(t, got.Grid)
118}
119
120func TestLoadDirectoryRejected(t *testing.T) {
121	path := configRoot(t)
122	require.NoError(t, os.MkdirAll(path, 0o700))
123	_, err := Load()
124	require.ErrorContains(t, err, "expected a regular file")
125}
126
127func TestLoadPathsAndOutput(t *testing.T) {
128	home := t.TempDir()
129	t.Setenv("HOME", home)
130	path := configRoot(t)
131	saveConfig(t, path, "# c\nscreenshot.dir = ~/Pics/Shots\nvideo.dir=/var/tmp/vids/\nscreenshot.output = file+clipboard\nselector.grid = off\n")
132	got, err := Load()
133	require.NoError(t, err)
134	assert.Equal(t, Settings{Grid: false, ScreenshotDir: filepath.Join(home, "Pics", "Shots"), VideoDir: "/var/tmp/vids", Output: OutputFileClipboard}, got)
135
136	saveConfig(t, path, "screenshot.dir = ~/\nscreenshot.output = clipboard\n")
137	got, err = Load()
138	require.NoError(t, err)
139	assert.Equal(t, home, got.ScreenshotDir)
140	assert.Equal(t, OutputClipboard, got.Output)
141	assert.Empty(t, got.VideoDir)
142}
143
144func TestLoadDefaultsOutputFile(t *testing.T) {
145	configRoot(t)
146	got, err := Load()
147	require.NoError(t, err)
148	assert.Equal(t, Settings{Grid: true, Output: OutputFile}, got)
149}
150
151func TestLoadRejectsInvalidNewKeys(t *testing.T) {
152	for _, tc := range []struct{ text, want string }{
153		{"screenshot.dir = Pictures", "line 1: screenshot.dir must be an absolute path or start with ~/"},
154		{"video.dir = ./v", "line 1: video.dir must be an absolute path"},
155		{"video.dir = ~user/v", "line 1: video.dir must be an absolute path"},
156		{"video.dir = ~", "line 1: video.dir must be an absolute path"},
157		{"video.dir =", "line 1: video.dir must be a non-empty path"},
158		{"screenshot.output = both", "line 1: screenshot.output must be file, file+clipboard or clipboard"},
159		{"screenshot.output = File", "screenshot.output must be"},
160		{"screenshot.dir = /a\nscreenshot.dir = /b", "line 2: expected each of"},
161		{"screenshot.output = file\n\nscreenshot.output = file", "line 3: expected each of"},
162		{"video.dir = /a\nvideo.dir = /a", "line 2: expected each of"},
163		{"video.output = file", "line 1: expected each of"},
164	} {
165		t.Run(tc.text, func(t *testing.T) {
166			t.Setenv("HOME", t.TempDir())
167			path := configRoot(t)
168			saveConfig(t, path, tc.text)
169			_, err := Load()
170			require.ErrorContains(t, err, tc.want)
171			assert.Contains(t, err.Error(), path)
172		})
173	}
174}

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