main f2d166b

cursor_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	"github.com/bnema/go-wayland-bindings/server/cursorshape"
9	"github.com/bnema/go-wayland-bindings/server/wayland"
10	"github.com/bnema/neferwl/internal/adapters/logging"
11	"github.com/bnema/neferwl/internal/ports"
12	"golang.org/x/sys/unix"
13)
14
15func cursorServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.CursorChange, string) {
16	t.Helper()
17	dir := t.TempDir()
18	events := make(chan ports.ClientEvent, 16)
19	commands := make(chan ports.ClientCommand, 16)
20	cursors := make(chan ports.CursorChange, 16)
21	s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Cursors: cursors}, logging.For(context.Background(), "wayland"))
22	if err != nil {
23		t.Fatal(err)
24	}
25	ctx, cancel := context.WithCancel(context.Background())
26	done := make(chan error, 1)
27	go func() { done <- s.Run(ctx) }()
28	t.Cleanup(func() { cancel(); <-done })
29	return s, events, commands, cursors, dir
30}
31
32// nextCursor returns the latest cursor change sent within a short wait.
33func nextCursor(t *testing.T, cursors <-chan ports.CursorChange) ports.CursorChange {
34	t.Helper()
35	var c ports.CursorChange
36	select {
37	case c = <-cursors:
38	case <-time.After(2 * time.Second):
39		t.Fatal("no cursor change")
40	}
41	for {
42		select {
43		case c = <-cursors:
44		case <-time.After(50 * time.Millisecond):
45			return c
46		}
47	}
48}
49
50func TestCursorShapeFollowsPointerFocus(t *testing.T) {
51	s, events, commands, cursors, dir := cursorServer(t)
52	c := protocolClient(t, s, dir)
53	seat := bindProtocol(t, c, "wl_seat")
54	registerProtocol(t, c, seat)
55	pointer := c.AllocateID()
56	registerProtocol(t, c, pointer)
57	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
58	manager := bindProtocol(t, c, "wp_cursor_shape_manager_v1")
59	device := c.AllocateID()
60	registerProtocol(t, c, device)
61	requestProtocol(t, c, manager, cursorshape.WpCursorShapeManagerV1RequestGetPointer, device, pointer)
62	w := toplevelMapper(t, c, events)()
63
64	// Without pointer focus the request is ignored.
65	requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(1), uint32(cursorshape.WpCursorShapeDeviceV1ShapeText))
66	if err := c.Roundtrip(); err != nil {
67		t.Fatal(err)
68	}
69	select {
70	case got := <-cursors:
71		t.Fatalf("cursor change without focus: %+v", got)
72	case <-time.After(50 * time.Millisecond):
73	}
74
75	commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
76	if got := nextCursor(t, cursors); got != (ports.CursorChange{}) {
77		t.Fatalf("enter: %+v, want the arrow", got)
78	}
79	requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(2), uint32(cursorshape.WpCursorShapeDeviceV1ShapePointer))
80	if err := c.Roundtrip(); err != nil {
81		t.Fatal(err)
82	}
83	if got := nextCursor(t, cursors); got.Shape != "pointer" {
84		t.Fatalf("shape: %+v", got)
85	}
86	commands <- ports.CursorOverride{Shape: "ew-resize"}
87	if got := nextCursor(t, cursors); got != (ports.CursorChange{Shape: "ew-resize"}) {
88		t.Fatalf("override: %+v", got)
89	}
90	requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(3), uint32(cursorshape.WpCursorShapeDeviceV1ShapeText))
91	if err := c.Roundtrip(); err != nil {
92		t.Fatal(err)
93	}
94	select {
95	case got := <-cursors:
96		t.Fatalf("client cursor forwarded during override: %+v", got)
97	case <-time.After(50 * time.Millisecond):
98	}
99	commands <- ports.CursorOverride{}
100	if got := nextCursor(t, cursors); got != (ports.CursorChange{Shape: "text"}) {
101		t.Fatalf("restored client cursor: %+v", got)
102	}
103	commands <- ports.PointerFocus{ID: 0}
104	if got := nextCursor(t, cursors); got != (ports.CursorChange{}) {
105		t.Fatalf("leave: %+v, want the arrow", got)
106	}
107}
108
109func TestCursorShapeNames(t *testing.T) {
110	for shape, want := range map[cursorshape.WpCursorShapeDeviceV1Shape]string{
111		cursorshape.WpCursorShapeDeviceV1ShapeDefault:      "default",
112		cursorshape.WpCursorShapeDeviceV1ShapeNwseResize:   "nwse-resize",
113		cursorshape.WpCursorShapeDeviceV1ShapeZoomOut:      "zoom-out",
114		cursorshape.WpCursorShapeDeviceV1ShapeAllResize:    "all-resize",
115		cursorshape.WpCursorShapeDeviceV1ShapeVerticalText: "vertical-text",
116	} {
117		if got := cursorShapes[shape]; got != want {
118			t.Errorf("shape %d = %q, want %q", shape, got, want)
119		}
120	}
121}
122
123func TestCursorShapeInvalid(t *testing.T) {
124	s, _, _, _, dir := cursorServer(t)
125	c := protocolClient(t, s, dir)
126	seat := bindProtocol(t, c, "wl_seat")
127	registerProtocol(t, c, seat)
128	pointer := c.AllocateID()
129	registerProtocol(t, c, pointer)
130	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
131	manager := bindProtocol(t, c, "wp_cursor_shape_manager_v1")
132	device := c.AllocateID()
133	registerProtocol(t, c, device)
134	requestProtocol(t, c, manager, cursorshape.WpCursorShapeManagerV1RequestGetPointer, device, pointer)
135	// Version 1 ends at zoom_out.
136	requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(1), uint32(cursorshape.WpCursorShapeDeviceV1ShapeDndAsk))
137	expectProtocolError(t, c, device, uint32(cursorshape.WpCursorShapeDeviceV1ErrorInvalidShape))
138}
139
140func TestSetCursorSurface(t *testing.T) {
141	s, events, commands, cursors, dir := cursorServer(t)
142	c := protocolClient(t, s, dir)
143	seat := bindProtocol(t, c, "wl_seat")
144	registerProtocol(t, c, seat)
145	pointer := c.AllocateID()
146	registerProtocol(t, c, pointer)
147	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
148	w := toplevelMapper(t, c, events)()
149	commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
150	nextCursor(t, cursors)
151
152	comp := bindProtocol(t, c, "wl_compositor")
153	shm := bindProtocol(t, c, "wl_shm")
154	registerProtocol(t, c, shm)
155	fd, err := unix.MemfdCreate("cursor", unix.MFD_CLOEXEC)
156	if err != nil {
157		t.Fatal(err)
158	}
159	defer unix.Close(fd)
160	// 2x1 ARGB: opaque blue, then transparent; stride padded to 12.
161	if _, err := unix.Pwrite(fd, []byte{255, 0, 0, 255, 0, 0, 0, 0, 9, 9, 9, 9}, 0); err != nil {
162		t.Fatal(err)
163	}
164	pool, buf := c.AllocateID(), c.AllocateID()
165	registerProtocol(t, c, pool)
166	registerProtocol(t, c, buf)
167	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(12)); err != nil {
168		t.Fatal(err)
169	}
170	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(2), int32(1), int32(12), uint32(wayland.ShmFormatArgb8888))
171	surf := c.AllocateID()
172	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
173	registerProtocol(t, c, surf)
174
175	// A cursor surface without a buffer hides the cursor.
176	requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), surf, int32(1), int32(0))
177	if err := c.Roundtrip(); err != nil {
178		t.Fatal(err)
179	}
180	if got := nextCursor(t, cursors); !got.Hidden {
181		t.Fatalf("empty surface: %+v", got)
182	}
183	// A commit updates the image.
184	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
185	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
186	if err := c.Roundtrip(); err != nil {
187		t.Fatal(err)
188	}
189	got := nextCursor(t, cursors)
190	if got.Image == nil || got.Scale != 1 || got.Image.W != 2 || got.Image.H != 1 || got.Image.HotX != 1 || string(got.Image.Pixels) != string([]byte{255, 0, 0, 255, 0, 0, 0, 0}) {
191		t.Fatalf("image: %+v %+v", got, got.Image)
192	}
193	// A null surface hides the cursor.
194	requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), uint32(0), int32(0), int32(0))
195	if err := c.Roundtrip(); err != nil {
196		t.Fatal(err)
197	}
198	if got := nextCursor(t, cursors); !got.Hidden {
199		t.Fatalf("null surface: %+v", got)
200	}
201}
202
203func TestSetCursorRoleError(t *testing.T) {
204	s, events, _, _, dir := cursorServer(t)
205	c := protocolClient(t, s, dir)
206	seat := bindProtocol(t, c, "wl_seat")
207	registerProtocol(t, c, seat)
208	pointer := c.AllocateID()
209	registerProtocol(t, c, pointer)
210	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
211	toplevelMapper(t, c, events)()
212	// Surface 3 of the mapper's connection is the toplevel's wl_surface:
213	// find it through the server instead of guessing ids.
214	var surf uint32
215	s.display.Do(func() {
216		for _, st := range s.surfaces {
217			if st.kind == roleXDG {
218				surf = st.wl.ID()
219			}
220		}
221	})
222	requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), surf, int32(0), int32(0))
223	expectProtocolError(t, c, pointer, uint32(wayland.PointerErrorRole))
224}
225
226func TestCursorOverrideProtection(t *testing.T) {
227	s, _, commands, cursors, _ := cursorServer(t)
228	state := installSecurity(t, s)
229	commands <- ports.CursorOverride{Shape: "ns-resize"}
230	if got := nextCursor(t, cursors); got.Shape != "ns-resize" {
231		t.Fatalf("override: %+v", got)
232	}
233	protect(t, s, state, 1, true)
234	if !s.display.Do(func() {
235		s.resetProtectionInput()
236		s.apply(ports.SecurityCommand{State: *state.Load(), Command: ports.CursorOverride{Shape: "ew-resize"}})
237		if s.cursorOverride != "" {
238			t.Errorf("protected override: %q", s.cursorOverride)
239		}
240	}) {
241		t.Fatal("display stopped")
242	}
243	select {
244	case got := <-cursors:
245		t.Fatalf("protected cursor forwarded: %+v", got)
246	case <-time.After(50 * time.Millisecond):
247	}
248	protect(t, s, state, 2, false)
249	if !s.display.Do(func() { s.setCursor(ports.CursorChange{Hidden: true}) }) {
250		t.Fatal("display stopped")
251	}
252	if got := nextCursor(t, cursors); got != (ports.CursorChange{Hidden: true}) {
253		t.Fatalf("override survived protection: %+v", got)
254	}
255}

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 26

Tags 10

↑↓ moveenter openesc close