v0.1.0 d9218d6

config_test.go RawHistoryFolder

1package config
2
3import (
4	"os"
5	"path/filepath"
6	"reflect"
7	"strings"
8	"testing"
9
10	"github.com/bnema/neferwl/internal/ports"
11)
12
13func parseString(t *testing.T, s string) (ports.Config, []Warning) {
14	t.Helper()
15	c, w, err := Parse(strings.NewReader(s))
16	if err != nil {
17		t.Fatal(err)
18	}
19	return c, w
20}
21
22func TestDefaultsAndLoad(t *testing.T) {
23	d := Defaults()
24	if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 52 || d.Layout.MaxColumns != 2 {
25		t.Fatalf("defaults: %+v", d)
26	}
27	if d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove {
28		t.Fatal(d.Binds)
29	}
30	if d.Binds["Cmd+Ctrl+space"] != "spawn fuzzel" || d.Binds["Alt+Ctrl+BackSpace"] != "quit" {
31		t.Fatal(d.Binds)
32	}
33	if _, _, err := Load(filepath.Join(t.TempDir(), "missing")); !os.IsNotExist(err) {
34		t.Fatalf("explicit missing: %v", err)
35	}
36	t.Setenv("XDG_CONFIG_HOME", t.TempDir())
37	if DefaultPath() != filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "neferwl", "config") {
38		t.Fatal(DefaultPath())
39	}
40	got, w, err := LoadDefault()
41	if err != nil || len(w) != 0 || !reflect.DeepEqual(got, d) {
42		t.Fatalf("missing: %v %v %+v", err, w, got)
43	}
44	example, w, err := Load("../../../examples/config")
45	if err != nil || len(w) != 0 || !reflect.DeepEqual(example, d) {
46		t.Fatalf("example: %v %v\n%+v\n%+v", err, w, example, d)
47	}
48}
49
50func TestFollowMove(t *testing.T) {
51	c, w := parseString(t, "focus.follow-move = on\n")
52	if !c.Focus.FollowMove || len(w) != 0 {
53		t.Fatal(c.Focus, w)
54	}
55	c, w = parseString(t, "focus.follow-move = yes\n")
56	if c.Focus.FollowMove || len(w) != 1 {
57		t.Fatal(c.Focus, w)
58	}
59}
60
61func TestParse(t *testing.T) {
62	c, w := parseString(t, `
63# comment
64keyboard.layout = fr
65keyboard.repeat-rate = 40   # inline comment
66terminal = foot --server
67background = #000000
68border.inactive =
69layout.presets = 1/3, 1/2 ,1
70log.debug = core, input
71output.DP-2.scale = 4/3
72output.DP-2 = 5120x2160@165.058
73output.HDMI-A-1 = off
74output.DP-1 = preferred
75output.DP-1.scale = 1.5
76output.DP-1.primary = on
77bind.cmd+code:30 = quit
78render.direct-scanout = off
79render.tearing = off
80render.vrr = off
81startup = wl-paste --watch cliphist store
82startup = wl-paste --primary --watch cliphist store
83`)
84	if len(w) != 0 {
85		t.Fatal(w)
86	}
87	if !reflect.DeepEqual(c.Startup, [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"wl-paste", "--primary", "--watch", "cliphist", "store"}}) {
88		t.Fatalf("startup: %q", c.Startup)
89	}
90	if c.Keyboard.Layout != "fr" || c.Keyboard.RepeatRate != 40 || !reflect.DeepEqual(c.Terminal.Command, []string{"foot", "--server"}) || c.Background.Color != "#000000" || c.Render.DirectScanout || c.Render.Tearing || c.Render.VRR {
91		t.Fatalf("%+v", c)
92	}
93	if !reflect.DeepEqual(c.Layout.Presets, []string{"1/3", "1/2", "1"}) || !reflect.DeepEqual(c.Log.Debug, []string{"core", "input"}) {
94		t.Fatalf("%+v", c)
95	}
96	// Scale and mode lines combine in either order.
97	if len(c.Outputs) != 3 || c.Outputs[0].Mode != "5120x2160@165.058" || c.Outputs[0].Scale != 4.0/3 || !c.Outputs[1].Off || c.Outputs[2].Mode != "" || c.Outputs[2].Off || c.Outputs[2].Scale != 1.5 || !c.Outputs[2].Primary || c.Outputs[0].Primary {
98		t.Fatalf("%+v", c.Outputs)
99	}
100	if c.Binds["Cmd+code:30"] != "quit" {
101		t.Fatal(c.Binds)
102	}
103}
104
105func TestWarningsKeepDefaults(t *testing.T) {
106	cases := []struct{ line, want string }{
107		{"keyboard.repeat-rate = 0", "keyboard.repeat-rate"},
108		{"keyboard.cmd = meta", "keyboard.cmd"},
109		{"terminal =", "terminal"},
110		{"xwayland = a b", "xwayland"},
111		{"background = red", "background"},
112		{"layout.gaps = 201", "layout.gaps"},
113		{"layout.max-columns = 0", "layout.max-columns"},
114		{"bind.cmd+x = focus-workspace 0", "focus-workspace"},
115		{"layout.presets = 0px", "layout.presets"},
116		{"log.level = trace", "log.level"},
117		{"log.debug = nope", "log.debug"},
118		{"output.DP-2 = big", "output.DP-2"},
119		{"output.DP-2 = 1920x1080@0", "output.DP-2"},
120		{"output.DP-2.scale = 0.5", "between 1 and 4"},
121		{"output.DP-2.primary = yes", "output.DP-2.primary"},
122		{"output.DP-2.scale = 3/0", "between 1 and 4"},
123		{"bind.cmd+code:x = quit", "evdev key code"},
124		{"nope = 1", "nope"},
125		{"just text", "expected key = value"},
126		{"bind.cmd+ = quit", "missing key"},
127		{"bind.hyper+x = quit", "unknown modifier"},
128		{"bind.cmd+x = bogus", "unknown action"},
129		{"bind.cmd+x = spawn   ", "unknown action"},
130	}
131	for _, tc := range cases {
132		t.Run(tc.line, func(t *testing.T) {
133			c, w := parseString(t, tc.line)
134			if len(w) != 1 || w[0].Line != 1 || !strings.Contains(w[0].Msg, tc.want) {
135				t.Fatalf("warnings %v, want %q", w, tc.want)
136			}
137			if !reflect.DeepEqual(c, Defaults()) {
138				t.Fatalf("config changed: %+v", c)
139			}
140		})
141	}
142}
143
144func TestXwayland(t *testing.T) {
145	if Defaults().Xwayland != "xwayland-satellite" {
146		t.Fatal(Defaults().Xwayland)
147	}
148	if c, w := parseString(t, "xwayland = off"); c.Xwayland != "" || len(w) != 0 {
149		t.Fatal(c.Xwayland, w)
150	}
151	if c, w := parseString(t, "xwayland = /opt/xwls"); c.Xwayland != "/opt/xwls" || len(w) != 0 {
152		t.Fatal(c.Xwayland, w)
153	}
154}
155
156func TestDuplicateKeyLastWins(t *testing.T) {
157	c, w := parseString(t, "background = #000000\nbackground = #111111\n")
158	if c.Background.Color != "#111111" || len(w) != 1 || w[0].Line != 2 {
159		t.Fatal(c.Background, w)
160	}
161}
162
163func TestBinds(t *testing.T) {
164	c, w := parseString(t, `
165bind.CTRL+Cmd+E = spawn foo --bar
166bind.cmd+; = close-window
167bind.cmd+é = quit
168bind.cmd+semicolon = quit
169bind.cmd+equal = spawn-terminal
170bind.cmd+# = none
171bind.cmd++ = quit
172bind.alt+f5 = quit
173bind.cmd+return = none
174bind.cmd+w = focus-workspace 12
175`)
176	want := map[string]string{
177		"Cmd+Ctrl+e":    "spawn foo --bar",
178		"Cmd+semicolon": "quit",
179		"Cmd+eacute":    "quit",
180		"Cmd+equal":     "spawn-terminal",
181		"Cmd+plus":      "quit",
182		"Alt+F5":        "quit",
183	}
184	for combo, action := range want {
185		if c.Binds[combo] != action {
186			t.Errorf("%s = %q, want %q", combo, c.Binds[combo], action)
187		}
188	}
189	if _, ok := c.Binds["Cmd+Return"]; ok {
190		t.Error("none did not remove the default")
191	}
192	if _, ok := c.Binds["Cmd+numbersign"]; ok {
193		t.Error("none kept")
194	}
195	// cmd+; then cmd+semicolon is the same key: warn and keep the last.
196	if len(w) != 1 || w[0].Line != 5 || !strings.Contains(w[0].Msg, "overrides line 3") {
197		t.Fatal(w)
198	}
199}
200
201func TestBindReservedCharacter(t *testing.T) {
202	_, w := parseString(t, "bind.cmd+= = quit\n")
203	if len(w) != 1 || !strings.Contains(w[0].Msg, "cmd+equal") {
204		t.Fatal(w)
205	}
206}
207
208func TestBindResolvedCollision(t *testing.T) {
209	// The later line wins, like any duplicate key.
210	c, w := parseString(t, "keyboard.cmd = ctrl\nbind.ctrl+x = quit\nbind.cmd+x = close-window\nbind.cmd+ctrl+y = quit\n")
211	if c.Binds["Cmd+x"] != "close-window" {
212		t.Fatal(c.Binds)
213	}
214	if _, ok := c.Binds["Ctrl+x"]; ok {
215		t.Fatal(c.Binds)
216	}
217	if _, ok := c.Binds["Cmd+Ctrl+y"]; ok {
218		t.Fatal(c.Binds)
219	}
220	if len(w) != 2 || w[0].Line != 4 || w[1].Line != 2 || !strings.Contains(w[1].Msg, "overridden by bind.Cmd+x") {
221		t.Fatal(w)
222	}
223	// A user bind replaces a default with the same resolved keys, without a warning.
224	c, w = parseString(t, "bind.super+q = quit\n")
225	if c.Binds["Super+q"] != "quit" || c.Binds["Cmd+q"] != "" || len(w) != 0 {
226		t.Fatal(c.Binds, w)
227	}
228}
229
230func TestInvalidDuplicateKeepsEarlierValue(t *testing.T) {
231	c, w := parseString(t, "background = #000000\nbackground = red\nbackground = #111111\n")
232	if c.Background.Color != "#111111" || len(w) != 2 || w[0].Line != 2 || w[1].Line != 3 || !strings.Contains(w[1].Msg, "overrides line 1") {
233		t.Fatal(c.Background, w)
234	}
235}
236
237func TestBindCharacters(t *testing.T) {
238	c, w := parseString(t, "bind.cmd+É = quit\nbind.cmd+ж = quit\n")
239	if c.Binds["Cmd+eacute"] != "quit" {
240		t.Fatal(c.Binds)
241	}
242	if len(w) != 1 || w[0].Line != 2 || !strings.Contains(w[0].Msg, "keysym name") {
243		t.Fatal(w)
244	}
245}
246
247func TestParseMode(t *testing.T) {
248	w, h, hz, err := ParseMode("3440x1440")
249	if err != nil || w != 3440 || h != 1440 || hz != 0 {
250		t.Fatalf("%d %d %v %v", w, h, hz, err)
251	}
252	if _, _, hz, err := ParseMode("1920x1080@59.94"); err != nil || hz != 59.94 {
253		t.Fatal(hz, err)
254	}
255}
256
257func TestDigitBindReplacesPhysicalDefault(t *testing.T) {
258	c, w := parseString(t, "bind.cmd+1 = none\nbind.cmd+shift+2 = quit\n")
259	if len(w) != 0 {
260		t.Fatal(w)
261	}
262	if _, ok := c.Binds["Cmd+code:2"]; ok {
263		t.Fatal("default cmd+code:2 kept")
264	}
265	if _, ok := c.Binds["Cmd+Shift+code:3"]; ok || c.Binds["Cmd+Shift+2"] != "quit" {
266		t.Fatal(c.Binds)
267	}
268	if c.Binds["Cmd+code:4"] != "focus-workspace 3" {
269		t.Fatal("other defaults must stay")
270	}
271}
272
273func TestWorkspaces(t *testing.T) {
274	c, w := parseString(t, `layout.overflow = fixed
275workspace.dev.hidden = on
276workspace.dev.max-columns = 3
277workspace.web.overflow = scroll
278workspace.dev.monitor = DP-2
279workspace.bad name.hidden = on
280workspace.dev.color = red
281workspace.web.overflow = spiral
282bind.cmd+d = workspace dev
283bind.cmd+w = workspace
284bind.cmd+x = workspace typo
285workspace.lost.hidden = on
286`)
287	want := []ports.WorkspaceConfig{
288		{Name: "dev", Monitor: "DP-2", Hidden: true, MaxColumns: 3},
289		{Name: "web", Overflow: "scroll"},
290		{Name: "lost", Hidden: true},
291	}
292	if c.Layout.Overflow != "fixed" || !reflect.DeepEqual(c.Workspaces, want) {
293		t.Fatalf("%q %+v", c.Layout.Overflow, c.Workspaces)
294	}
295	if c.Binds["Cmd+d"] != "workspace dev" {
296		t.Fatal(c.Binds["Cmd+d"])
297	}
298	lines := []int{}
299	for _, x := range w {
300		lines = append(lines, x.Line)
301	}
302	if !reflect.DeepEqual(lines, []int{6, 7, 8, 10, 11, 12}) {
303		t.Fatal(w)
304	}
305}
306
307func TestWorkspaceSlots(t *testing.T) {
308	c, w := parseString(t, `workspace.dev.hidden = on
309workspace.dev.column.2 = 33%, foot --title x
310workspace.dev.column.1 = 67%, code --new-window
311workspace.dev.column.2 = 1/3, kitty
312workspace.dev.column.0 = 50%, foot
313workspace.dev.column.3 = 50%
314workspace.dev.column.4 = huge, foot
315bind.cmd+d = workspace dev
316`)
317	want := []ports.SlotConfig{
318		{Index: 1, Width: "67%", Argv: []string{"code", "--new-window"}},
319		{Index: 2, Width: "1/3", Argv: []string{"kitty"}},
320	}
321	if len(c.Workspaces) != 1 || !reflect.DeepEqual(c.Workspaces[0].Slots, want) {
322		t.Fatalf("%+v", c.Workspaces)
323	}
324	lines := []int{}
325	for _, x := range w {
326		lines = append(lines, x.Line)
327	}
328	if !reflect.DeepEqual(lines, []int{4, 5, 6, 7}) {
329		t.Fatal(w)
330	}
331}
332
333func TestSlotWidthsIgnoredWithFixedOverflow(t *testing.T) {
334	_, w := parseString(t, `workspace.dev.overflow = fixed
335workspace.dev.column.1 = 67%, foot
336`)
337	if len(w) != 1 || w[0].Line != 2 || !strings.Contains(w[0].Msg, "ignored") {
338		t.Fatal(w)
339	}
340}

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 23

Tags 10

↑↓ moveenter openesc close