v0.3.0 6800c6a

input_test.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"encoding/binary"
7	"net"
8	"os"
9	"testing"
10	"time"
11
12	"github.com/bnema/wlturbo"
13	"github.com/bnema/wlturbo/protocol/core"
14	"golang.org/x/sys/unix"
15)
16
17func inputPair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
18	t.Helper()
19	f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
20	if err != nil {
21		t.Fatal(err)
22	}
23	a := os.NewFile(uintptr(f[0]), "client")
24	b := os.NewFile(uintptr(f[1]), "server")
25	ca, e := net.FileConn(a)
26	_ = a.Close()
27	if e != nil {
28		t.Fatal(e)
29	}
30	cb, e := net.FileConn(b)
31	_ = b.Close()
32	if e != nil {
33		t.Fatal(e)
34	}
35	t.Cleanup(func() { _ = ca.Close(); _ = cb.Close() })
36	return ca.(*net.UnixConn), cb.(*net.UnixConn)
37}
38func inputFrame(id uint32, op uint16, body ...uint32) []byte {
39	b := make([]byte, 8+len(body)*4)
40	binary.LittleEndian.PutUint32(b, id)
41	binary.LittleEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
42	for i, v := range body {
43		binary.LittleEndian.PutUint32(b[8+i*4:], v)
44	}
45	return b
46}
47func inputSend(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) {
48	t.Helper()
49	var oob []byte
50	if len(fd) > 0 {
51		oob = unix.UnixRights(fd...)
52	}
53	if _, _, err := p.WriteMsgUnix(msg, oob, nil); err != nil {
54		t.Fatal(err)
55	}
56}
57func inputRequest(t *testing.T, p *net.UnixConn) {
58	t.Helper()
59	_ = p.SetReadDeadline(time.Now().Add(time.Second))
60	b := make([]byte, 256)
61	if _, err := p.Read(b); err != nil {
62		t.Fatal(err)
63	}
64}
65func TestPointerFrameOverSocketpair(t *testing.T) {
66	ca, server := inputPair(t)
67	d, err := wlturbo.ConnectFromConn(ca)
68	if err != nil {
69		t.Fatal(err)
70	}
71	defer d.Close()
72	inputRequest(t, server)
73	ctx := d.Context()
74	seat := core.NewSeat(ctx)
75	if err = d.Registry().Bind(1, core.SeatInterface, 9, seat); err != nil {
76		t.Fatal(err)
77	}
78	inputRequest(t, server)
79	p, err := seat.GetPointer()
80	if err != nil {
81		t.Fatal(err)
82	}
83	inputRequest(t, server)
84	surface := core.NewSurface(ctx)
85	surface.SetID(30)
86	ctx.Register(surface)
87	w := &Window{Surface: surface, seatVersion: 9, events: make(chan Event, 10), readerStop: make(chan struct{})}
88	w.watchPointer(p)
89	for _, msg := range [][]byte{
90		inputFrame(p.ID(), 0, 1, 30, 256*12, 256*7), // enter: surface-local logical pixels
91		inputFrame(p.ID(), 2, 2, 256*14, 256*8),
92		inputFrame(p.ID(), 6, 1), // wheel source
93		inputFrame(p.ID(), 4, 3, 0, 256*5),
94		inputFrame(p.ID(), 9, 0, 120),
95		inputFrame(p.ID(), 3, 4, 4, 0x110, 1),
96		inputFrame(p.ID(), 5),
97	} {
98		inputSend(t, server, msg)
99		if err = d.Dispatch(); err != nil {
100			t.Fatal(err)
101		}
102	}
103	select {
104	case ev := <-w.events:
105		if ev.Kind != InputFrame || len(ev.Inputs) != 5 {
106			t.Fatalf("frame: %+v", ev)
107		}
108		if !ev.Inputs[0].Enter || ev.Inputs[0].Serial != 1 || ev.Inputs[0].PointerGen != w.pointerGen || ev.Inputs[0].X != 12 || ev.Inputs[1].X != 14 || ev.Inputs[2].DY != 5 || ev.Inputs[2].AxisSource != 1 || ev.Inputs[3].Value120 != 120 || ev.Inputs[4].Kind != "press" {
109			t.Fatalf("inputs: %+v", ev.Inputs)
110		}
111	default:
112		t.Fatal("missing pointer frame")
113	}
114}
115func TestKeyboardFDAndRepeatOverSocketpair(t *testing.T) {
116	ca, server := inputPair(t)
117	d, err := wlturbo.ConnectFromConn(ca)
118	if err != nil {
119		t.Fatal(err)
120	}
121	defer d.Close()
122	inputRequest(t, server)
123	ctx := d.Context()
124	seat := core.NewSeat(ctx)
125	if err = d.Registry().Bind(1, core.SeatInterface, 9, seat); err != nil {
126		t.Fatal(err)
127	}
128	inputRequest(t, server)
129	k, err := seat.GetKeyboard()
130	if err != nil {
131		t.Fatal(err)
132	}
133	inputRequest(t, server)
134	surface := core.NewSurface(ctx)
135	surface.SetID(30)
136	ctx.Register(surface)
137	w := &Window{Surface: surface, events: make(chan Event, 10), readerStop: make(chan struct{})}
138	w.watchKeyboard(k)
139	f, err := os.CreateTemp(t.TempDir(), "keymap")
140	if err != nil {
141		t.Fatal(err)
142	}
143	defer f.Close()
144	inputSend(t, server, inputFrame(k.ID(), 0, 0, 5), int(f.Fd()))
145	if err = d.Dispatch(); err != nil {
146		t.Fatal(err)
147	}
148	ev := <-w.events
149	if ev.Kind != InputKeymap || ev.FD < 0 {
150		t.Fatalf("keymap: %+v", ev)
151	}
152	adapter, err := NewInputAdapter("")
153	if err != nil {
154		t.Skipf("xkb unavailable: %v", err)
155	}
156	defer adapter.Close()
157	if _, err = adapter.ApplyInput(ev, time.Now()); err == nil {
158		t.Fatal("unsupported keymap accepted")
159	}
160	if err = unix.Fstat(ev.FD, &unix.Stat_t{}); err != unix.EBADF {
161		t.Fatalf("fd not closed: %v", err)
162	}
163	inputSend(t, server, inputFrame(k.ID(), 5, 0, 200))
164	if err = d.Dispatch(); err != nil {
165		t.Fatal(err)
166	}
167	if _, err = adapter.ApplyInput(<-w.events, time.Now()); err != nil {
168		t.Fatal(err)
169	}
170	if _, ok := adapter.NextRepeat(); ok {
171		t.Fatal("repeat should be disabled")
172	}
173}
174
175// Each keymap path transfers ownership to the adapter or the discarded-event
176// cleanup, including a valid XKB keymap and malformed compositor payloads.
177func TestKeymapOwnershipAndFocusRepeat(t *testing.T) {
178	adapter, err := NewInputAdapter("")
179	if err != nil {
180		t.Fatal(err)
181	}
182	defer adapter.Close()
183	data, err := os.ReadFile("../../keyboard/testdata/fr.xkb")
184	if err != nil {
185		t.Fatal(err)
186	}
187	for _, tc := range []struct {
188		name    string
189		data    []byte
190		format  uint32
191		discard bool
192		good    bool
193	}{
194		{"valid", append(data, 0), 1, false, true},
195		{"invalid", []byte("bad keymap\x00"), 1, false, false},
196		{"unsupported", append(data, 0), 2, false, false},
197		{"disconnect", append(data, 0), 1, true, false},
198	} {
199		t.Run(tc.name, func(t *testing.T) {
200			f, err := os.CreateTemp(t.TempDir(), "keymap")
201			if err != nil {
202				t.Fatal(err)
203			}
204			if _, err = f.Write(tc.data); err != nil {
205				t.Fatal(err)
206			}
207			// ApplyInput owns a duplicate, not the os.File descriptor.
208			fd, err := unix.Dup(int(f.Fd()))
209			if err != nil {
210				t.Fatal(err)
211			}
212			defer f.Close()
213			ev := Event{Kind: InputKeymap, FD: fd, Format: tc.format, Size: uint32(len(tc.data))}
214			if tc.discard {
215				CloseEventFD(ev)
216			} else {
217				_, err = adapter.ApplyInput(ev, time.Now())
218				if (err == nil) != tc.good {
219					t.Fatalf("apply: %v", err)
220				}
221			}
222			if err = unix.Fstat(ev.FD, &unix.Stat_t{}); err != unix.EBADF {
223				t.Fatalf("keymap FD still open: %v", err)
224			}
225		})
226	}
227	now := time.Now()
228	for _, ev := range []Event{{Kind: InputFocusIn, Serial: 42}, {Kind: InputRepeatInfo, Rate: 20, Delay: 100}, {Kind: InputKey, Code: 16, Pressed: true, Serial: 43}} {
229		if _, err := adapter.ApplyInput(ev, now); err != nil {
230			t.Fatal(err)
231		}
232	}
233	if _, ok := adapter.NextRepeat(); !ok {
234		t.Fatal("missing repeat")
235	}
236	if _, err := adapter.ApplyInput(Event{Kind: InputRepeatInfo, Rate: 0}, now); err != nil {
237		t.Fatal(err)
238	}
239	if _, ok := adapter.NextRepeat(); ok {
240		t.Fatal("rate zero retained repeat")
241	}
242	if _, err := adapter.ApplyInput(Event{Kind: InputRepeatInfo, Rate: 20, Delay: 100}, now); err != nil {
243		t.Fatal(err)
244	}
245	if _, err := adapter.ApplyInput(Event{Kind: InputKey, Code: 16, Pressed: true}, now); err != nil {
246		t.Fatal(err)
247	}
248	if _, ok := adapter.NextRepeat(); !ok {
249		t.Fatal("repeat not rearmed")
250	}
251	if _, err := adapter.ApplyInput(Event{Kind: InputFocusOut}, now); err != nil {
252		t.Fatal(err)
253	}
254	if _, ok := adapter.NextRepeat(); ok {
255		t.Fatal("keyboard leave retained repeat")
256	}
257
258	// Overflow reset cancels repeat from a discarded press but keeps focus.
259	w := &Window{inputAdapter: adapter}
260	for _, ev := range []Event{{Kind: InputFocusIn, Serial: 44}, {Kind: InputKey, Code: 16, Pressed: true, Serial: 45}} {
261		if _, err := adapter.ApplyInput(ev, now); err != nil {
262			t.Fatal(err)
263		}
264	}
265	for i := 0; i <= 4096; i++ {
266		w.queueInput([]Input{{Kind: "press"}})
267	}
268	if _, ok := adapter.NextRepeat(); ok {
269		t.Fatal("overflow reset retained repeat")
270	}
271	out, err := adapter.ApplyInput(Event{Kind: InputKey, Code: 16, Pressed: true, Serial: 46}, now)
272	if err != nil || len(out) != 1 {
273		t.Fatalf("key after reset: %v %+v", err, out)
274	}
275	if _, ok := adapter.NextRepeat(); !ok {
276		t.Fatal("focus lost by overflow reset")
277	}
278}
279
280func TestInputQueueOverflowResetAndCoalescing(t *testing.T) {
281	w := &Window{}
282	for i := 0; i < 4096; i++ {
283		w.queueInput([]Input{{Kind: "press", Button: uint32(i)}})
284	}
285	w.queueInput([]Input{{Kind: "release", Button: 7}})
286	got := w.DrainInput(nil)
287	if len(got) != 2 || got[0].Kind != InputReset || got[1].Kind != "release" || w.InputOverflow != 4096 {
288		t.Fatalf("reset: len=%d events=%+v overflows=%d", len(got), got, w.InputOverflow)
289	}
290	w.queueInput([]Input{{Kind: "motion", X: 1}})
291	for i := 1; i < 4096; i++ {
292		w.queueInput([]Input{{Kind: "press"}})
293	}
294	w.queueInput([]Input{{Kind: "focus-out"}})
295	got = w.DrainInput(nil)
296	if len(got) != 4096 || got[0].Kind != "press" || got[4095].Kind != "focus-out" || w.InputOverflow != 4097 {
297		t.Fatalf("eviction: len=%d first=%+v last=%+v overflows=%d", len(got), got[0], got[len(got)-1], w.InputOverflow)
298	}
299	w.queueInput([]Input{{Kind: "axis", Axis: 1, DY: 2}})
300	w.queueInput([]Input{{Kind: "axis", Axis: 1, DY: 3}})
301	for i := 2; i < 4096; i++ {
302		w.queueInput([]Input{{Kind: "press"}})
303	}
304	w.queueInput([]Input{{Kind: "release"}})
305	got = w.DrainInput(nil)
306	if len(got) != 4096 || got[0].Kind != "axis" || got[0].DY != 5 || got[4095].Kind != "release" || w.InputOverflow != 4097 {
307		t.Fatalf("axis coalescing: len=%d first=%+v last=%+v overflows=%d", len(got), got[0], got[len(got)-1], w.InputOverflow)
308	}
309	// Eviction skips enter motions; a reset keeps the latest one.
310	w.queueInput([]Input{{Kind: "motion", Enter: true, Serial: 5}})
311	for i := 1; i < 4096; i++ {
312		w.queueInput([]Input{{Kind: "press"}})
313	}
314	w.queueInput([]Input{{Kind: "release"}})
315	got = w.DrainInput(nil)
316	if len(got) != 3 || got[0].Kind != InputReset || !got[1].Enter || got[1].Serial != 5 || got[2].Kind != "release" {
317		t.Fatalf("enter across reset: len=%d first=%+v", len(got), got[:min(3, len(got))])
318	}
319}
320
321func TestApplySeatPointerKeyAndDrain(t *testing.T) {
322	w := &Window{}
323	data, err := os.ReadFile("../../keyboard/testdata/fr.xkb")
324	if err != nil {
325		t.Fatal(err)
326	}
327	f, err := os.CreateTemp(t.TempDir(), "keymap")
328	if err != nil {
329		t.Fatal(err)
330	}
331	if _, err = f.Write(append(data, 0)); err != nil {
332		t.Fatal(err)
333	}
334	// ApplyInput consumes the event FD; keep the os.File descriptor separate.
335	fd, err := unix.Dup(int(f.Fd()))
336	if err != nil {
337		t.Fatal(err)
338	}
339	defer f.Close()
340	for _, ev := range []Event{
341		{Kind: InputKeymap, FD: fd, Format: 1, Size: uint32(len(data) + 1)},
342		{Kind: InputFocusIn, Serial: 3},
343		{Kind: SeatCapabilities, Capabilities: 3},
344		{Kind: InputFrame, Inputs: []Input{{Kind: "motion", X: 4}, {Kind: "press", Button: 1}}},
345		{Kind: InputKey, Code: 16, Pressed: true, Serial: 4},
346	} {
347		if err := w.Apply(ev); err != nil {
348			t.Fatal(err)
349		}
350	}
351	defer w.inputAdapter.Close()
352	got := w.DrainInput(nil)
353	if len(got) != 4 || got[0].Kind != "focus-in" || got[1].Kind != "motion" || got[2].Kind != "press" || got[3].Kind != "key" {
354		t.Fatalf("drained: %+v", got)
355	}
356	if len(w.DrainInput(nil)) != 0 {
357		t.Fatal("queue not drained")
358	}
359}

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 1

Tags 12

↑↓ moveenter openesc close