v0.2.0 13c6a90

cursor_test.go RawHistoryFolder

1package app
2
3import (
4	"encoding/binary"
5	"os"
6	"path/filepath"
7	"testing"
8
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// themeCursor writes a one-image Xcursor file of side size under
13// dir/theme/cursors/name, every pixel set to px.
14func themeCursor(t *testing.T, dir, name string, size int, px byte) {
15	t.Helper()
16	le := binary.LittleEndian
17	out := le.AppendUint32([]byte("Xcur"), 16)
18	out = le.AppendUint32(out, 0x10000)
19	out = le.AppendUint32(out, 1)
20	for _, v := range []int{0xfffd0002, size, 28, 36, 0xfffd0002, size, 1, size, size, 0, 0, 0} {
21		out = le.AppendUint32(out, uint32(v))
22	}
23	for range size * size * 4 {
24		out = append(out, px)
25	}
26	path := filepath.Join(dir, "theme", "cursors", name)
27	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
28		t.Fatal(err)
29	}
30	if err := os.WriteFile(path, out, 0o644); err != nil {
31		t.Fatal(err)
32	}
33}
34
35// resetThemeCursors empties the theme cache now and after the test.
36func resetThemeCursors(t *testing.T) {
37	reset := func() {
38		themeCursors.Lock()
39		clear(themeCursors.m)
40		themeCursors.Unlock()
41	}
42	reset()
43	t.Cleanup(reset)
44}
45
46func TestLoadCursorShapes(t *testing.T) {
47	dir := t.TempDir()
48	t.Setenv("XCURSOR_PATH", dir)
49	t.Setenv("XCURSOR_THEME", "theme")
50	t.Setenv("XCURSOR_SIZE", "24")
51	resetThemeCursors(t)
52	themeCursor(t, dir, "left_ptr", 24, 1)
53	themeCursor(t, dir, "hand2", 24, 2)
54	for _, tc := range []struct {
55		change ports.CursorChange
56		px     byte
57	}{
58		{ports.CursorChange{}, 1},
59		// Themes with only X11 names still get the hand.
60		{ports.CursorChange{Shape: "pointer"}, 2},
61		// Unknown shapes fall back to the arrow.
62		{ports.CursorChange{Shape: "zoom-in"}, 1},
63	} {
64		img, err := loadCursor(tc.change, 2, 256)
65		if err != nil {
66			t.Fatal(tc.change, err)
67		}
68		if img.W != 48 || img.Pixels[0] != tc.px {
69			t.Errorf("%+v: %dx%d px %d, want 48 wide px %d", tc.change, img.W, img.H, img.Pixels[0], tc.px)
70		}
71	}
72	// Cached: a removed theme file still loads.
73	if err := os.RemoveAll(dir); err != nil {
74		t.Fatal(err)
75	}
76	if img, err := loadCursor(ports.CursorChange{Shape: "pointer"}, 2, 256); err != nil || img.Pixels[0] != 2 {
77		t.Fatalf("cached: %v", err)
78	}
79	if img, err := loadCursor(ports.CursorChange{Hidden: true}, 1, 256); err != nil || img.W != 0 {
80		t.Fatalf("hidden: %+v %v", img, err)
81	}
82}
83
84// A client cursor drawn at buffer scale 2 is shown at the output scale.
85func TestLoadCursorClientImage(t *testing.T) {
86	src := &ports.CursorImage{W: 4, H: 2, HotX: 2, HotY: 1, Pixels: make([]byte, 4*2*4)}
87	img, err := loadCursor(ports.CursorChange{Image: src, Scale: 2}, 1, 256)
88	if err != nil || img.W != 2 || img.H != 1 || img.HotX != 1 || img.HotY != 0 {
89		t.Fatalf("%+v %v", img, err)
90	}
91	img, err = loadCursor(ports.CursorChange{Image: src, Scale: 2}, 2, 256)
92	if err != nil || img.W != 4 || img.HotX != 2 {
93		t.Fatalf("%+v %v", img, err)
94	}
95	// Bounded by the cursor plane.
96	img, err = loadCursor(ports.CursorChange{Image: src, Scale: 1}, 4, 8)
97	if err != nil || img.W != 8 || img.H != 4 {
98		t.Fatalf("%+v %v", img, err)
99	}
100}
101
102// A theme that only overrides the arrow still gets its parent's shapes.
103func TestLoadCursorInheritedShape(t *testing.T) {
104	dir := t.TempDir()
105	resetThemeCursors(t)
106	t.Setenv("XCURSOR_PATH", dir)
107	t.Setenv("XCURSOR_THEME", "child")
108	t.Setenv("XCURSOR_SIZE", "24")
109	themeCursor(t, dir, "left_ptr", 24, 1) // in "theme", the parent
110	if err := os.MkdirAll(filepath.Join(dir, "child", "cursors"), 0o755); err != nil {
111		t.Fatal(err)
112	}
113	if err := os.WriteFile(filepath.Join(dir, "child", "index.theme"), []byte("[Icon Theme]\nInherits=theme\n"), 0o644); err != nil {
114		t.Fatal(err)
115	}
116	child := filepath.Join(dir, "child", "cursors", "default")
117	themeCursor(t, dir, "text", 24, 3)
118	if err := os.Rename(filepath.Join(dir, "theme", "cursors", "left_ptr"), child); err != nil {
119		t.Fatal(err)
120	}
121	img, err := loadCursor(ports.CursorChange{Shape: "text"}, 1, 256)
122	if err != nil || img.Pixels[0] != 3 {
123		t.Fatalf("got arrow instead of the inherited text cursor: %v", err)
124	}
125}

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 25

Tags 10

↑↓ moveenter openesc close