virtualkeyboard_test.go RawHistoryFolder

1package wayland
2
3import (
4	"slices"
5	"strings"
6	"testing"
7	"time"
8
9	"github.com/bnema/go-wayland-bindings/server/virtualkeyboard"
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/bnema/wlturbo"
13	"golang.org/x/sys/unix"
14)
15
16// virtualTyper binds a virtual keyboard on its own connection, as wtype does.
17func virtualTyper(t *testing.T, s *Server, dir string) (*wlturbo.Display, uint32) {
18	t.Helper()
19	c := protocolClient(t, s, dir)
20	seat := bindProtocol(t, c, "wl_seat")
21	registerProtocol(t, c, seat)
22	manager := bindProtocol(t, c, "zwp_virtual_keyboard_manager_v1")
23	registerProtocol(t, c, manager)
24	kb := c.AllocateID()
25	registerProtocol(t, c, kb)
26	requestProtocol(t, c, manager, virtualkeyboard.ZwpVirtualKeyboardManagerV1RequestCreateVirtualKeyboard, seat, kb)
27	return c, kb
28}
29
30func sendVirtualKeymap(t *testing.T, c *wlturbo.Display, kb uint32, keymap string) {
31	t.Helper()
32	fd, size, err := keymapFile(keymap)
33	if err != nil {
34		t.Fatal(err)
35	}
36	defer unix.Close(fd)
37	if err := wireRequest(c, kb, uint16(virtualkeyboard.ZwpVirtualKeyboardV1RequestKeymap), []int{fd}, uint32(wayland.KeyboardKeymapFormatXkbV1), size); err != nil {
38		t.Fatal(err)
39	}
40}
41
42// Virtual keys reach the focused client under the virtual keymap; the next
43// real key switches it back to the seat keymap.
44func TestVirtualKeyboardTypesIntoFocus(t *testing.T) {
45	s, events, commands, dir := keyboardServer(t)
46	target := protocolClient(t, s, dir)
47	seat := bindProtocol(t, target, "wl_seat")
48	registerProtocol(t, target, seat)
49	proxy := &eventsProxy{}
50	proxy.SetID(target.AllocateID())
51	registerWireProxy(target, proxy)
52	requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, proxy.ID())
53	w := toplevelMapper(t, target, events)()
54	commands <- ports.FocusWindow{ID: w.ID}
55	waitFocus(t, s, w.ID)
56	if err := target.Roundtrip(); err != nil {
57		t.Fatal(err)
58	}
59	proxy.opcodes, proxy.keymaps = nil, nil
60
61	typer, kb := virtualTyper(t, s, dir)
62	virtual := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap  ", 1)
63	sendVirtualKeymap(t, typer, kb, virtual)
64	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
65	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
66	if err := typer.Roundtrip(); err != nil {
67		t.Fatal(err)
68	}
69	if err := target.Roundtrip(); err != nil {
70		t.Fatal(err)
71	}
72	want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey), uint16(wayland.KeyboardEventKey)}
73	if !slices.Equal(proxy.opcodes, want) {
74		t.Fatalf("virtual events %v, want %v", proxy.opcodes, want)
75	}
76	if len(proxy.keymaps) != 1 || proxy.keymaps[0] != virtual {
77		t.Fatal("focused client did not get the virtual keymap")
78	}
79
80	proxy.opcodes, proxy.keymaps = nil, nil
81	commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 3}}
82	deadline := time.Now().Add(2 * time.Second)
83	for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) {
84		if err := target.Roundtrip(); err != nil {
85			t.Fatal(err)
86		}
87		time.Sleep(5 * time.Millisecond)
88	}
89	want = []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey)}
90	if !slices.Equal(proxy.opcodes, want) {
91		t.Fatalf("real key events %v, want %v", proxy.opcodes, want)
92	}
93	if len(proxy.keymaps) != 1 || proxy.keymaps[0] != keymapText(t) {
94		t.Fatal("seat keymap not restored")
95	}
96}
97
98// focusedTarget maps a focused toplevel with one keyboard and returns its
99// connection and recorder, drained of setup events.
100func focusedTarget(t *testing.T, s *Server, events chan ports.ClientEvent, commands chan ports.ClientCommand, dir string) (*wlturbo.Display, *eventsProxy) {
101	t.Helper()
102	target := protocolClient(t, s, dir)
103	seat := bindProtocol(t, target, "wl_seat")
104	registerProtocol(t, target, seat)
105	proxy := &eventsProxy{}
106	proxy.SetID(target.AllocateID())
107	registerWireProxy(target, proxy)
108	requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, proxy.ID())
109	w := toplevelMapper(t, target, events)()
110	commands <- ports.FocusWindow{ID: w.ID}
111	waitFocus(t, s, w.ID)
112	if err := target.Roundtrip(); err != nil {
113		t.Fatal(err)
114	}
115	proxy.opcodes, proxy.keymaps = nil, nil
116	return target, proxy
117}
118
119func roundtrip(t *testing.T, cs ...*wlturbo.Display) {
120	t.Helper()
121	for _, c := range cs {
122		if err := c.Roundtrip(); err != nil {
123			t.Fatal(err)
124		}
125	}
126}
127
128// A typer dying mid-key releases the key and restores the seat keymap.
129func TestVirtualKeyboardDisconnectReleases(t *testing.T) {
130	s, events, commands, dir := keyboardServer(t)
131	target, proxy := focusedTarget(t, s, events, commands, dir)
132	typer, kb := virtualTyper(t, s, dir)
133	sendVirtualKeymap(t, typer, kb, strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap  ", 1))
134	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestModifiers, uint32(1), uint32(0), uint32(0), uint32(0))
135	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
136	roundtrip(t, typer)
137	_ = typer.Close()
138	deadline := time.Now().Add(2 * time.Second)
139	for len(proxy.keymaps) < 2 && time.Now().Before(deadline) {
140		roundtrip(t, target)
141		time.Sleep(5 * time.Millisecond)
142	}
143	k, m := uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers)
144	key := uint16(wayland.KeyboardEventKey)
145	// The Modifiers request comes after the switch's own modifiers event.
146	want := []uint16{k, m, m, key, key, k, m}
147	if !slices.Equal(proxy.opcodes, want) {
148		t.Fatalf("events %v, want %v", proxy.opcodes, want)
149	}
150	if proxy.keymaps[1] != keymapText(t) {
151		t.Fatal("seat keymap not restored")
152	}
153}
154
155// A keyboard created while a typer owns the keymap starts with it; a
156// malformed keymap is ignored and the last good one stays.
157func TestVirtualKeymapLateKeyboardAndRejected(t *testing.T) {
158	s, events, commands, dir := keyboardServer(t)
159	target, _ := focusedTarget(t, s, events, commands, dir)
160	typer, kb := virtualTyper(t, s, dir)
161	virtual := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap  ", 1)
162	sendVirtualKeymap(t, typer, kb, virtual)
163	sendVirtualKeymap(t, typer, kb, "not a keymap")
164	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
165	roundtrip(t, typer, target)
166	seat := bindProtocol(t, target, "wl_seat")
167	registerProtocol(t, target, seat)
168	late := &eventsProxy{}
169	late.SetID(target.AllocateID())
170	registerWireProxy(target, late)
171	requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, late.ID())
172	roundtrip(t, target)
173	if len(late.keymaps) != 1 || late.keymaps[0] != virtual {
174		t.Fatal("late keyboard did not get the virtual keymap")
175	}
176}
177
178func TestVirtualKeyBeforeKeymap(t *testing.T) {
179	s, _, _, dir := keyboardServer(t)
180	typer, kb := virtualTyper(t, s, dir)
181	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
182	expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap))
183}
184
185// A virtual keyboard with the seat keymap (dictation tools, wtype) sends
186// no keymap to clients: only its keys, then no keymap on the switch back.
187func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) {
188	s, events, commands, dir := keyboardServer(t)
189	target, proxy := focusedTarget(t, s, events, commands, dir)
190	for range 2 { // one typer per use, like a dictation app
191		typer, kb := virtualTyper(t, s, dir)
192		sendVirtualKeymap(t, typer, kb, keymapText(t))
193		requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
194		requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
195		roundtrip(t, typer, target)
196		_ = typer.Close()
197	}
198	deadline := time.Now().Add(200 * time.Millisecond)
199	for time.Now().Before(deadline) {
200		roundtrip(t, target)
201		time.Sleep(5 * time.Millisecond)
202	}
203	if len(proxy.keymaps) != 0 {
204		t.Fatalf("%d keymaps sent for a virtual keyboard with the seat keymap", len(proxy.keymaps))
205	}
206	keys := 0
207	for _, o := range proxy.opcodes {
208		if o == uint16(wayland.KeyboardEventKey) {
209			keys++
210		}
211	}
212	if keys != 4 {
213		t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes)
214	}
215}
216
217// A virtual keyboard with the seat keymap types without the seat's
218// modifiers: its switch sends its own (none), and the seat's come back
219// with the next real key.
220func TestVirtualKeyboardSameKeymapOwnModifiers(t *testing.T) {
221	s, events, commands, dir := keyboardServer(t)
222	target, proxy := focusedTarget(t, s, events, commands, dir)
223	const capsLocked = 2
224	set := make(chan struct{})
225	s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; close(set) })
226	<-set
227	proxy.locked = nil
228	typer, kb := virtualTyper(t, s, dir)
229	sendVirtualKeymap(t, typer, kb, keymapText(t))
230	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
231	roundtrip(t, typer, target)
232	m, key := uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey)
233	if !slices.Equal(proxy.opcodes, []uint16{m, key}) || !slices.Equal(proxy.locked, []uint32{0}) {
234		t.Fatalf("events %v locked %v, want modifiers with nothing locked, then the key", proxy.opcodes, proxy.locked)
235	}
236}
237
238// A virtual keymap that differs from the seat's is sent on switch, on
239// update while it owns the keyboards, and the seat's again on switch back.
240func TestVirtualKeyboardOtherKeymapSent(t *testing.T) {
241	s, events, commands, dir := keyboardServer(t)
242	target, proxy := focusedTarget(t, s, events, commands, dir)
243	typer, kb := virtualTyper(t, s, dir)
244	first := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap  ", 1)
245	second := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap   ", 1)
246	sendVirtualKeymap(t, typer, kb, first)
247	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
248	sendVirtualKeymap(t, typer, kb, second)
249	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
250	roundtrip(t, typer, target)
251	_ = typer.Close()
252	deadline := time.Now().Add(2 * time.Second)
253	for len(proxy.keymaps) < 3 && time.Now().Before(deadline) {
254		roundtrip(t, target)
255		time.Sleep(5 * time.Millisecond)
256	}
257	if !slices.Equal(proxy.keymaps, []string{first, second, keymapText(t)}) {
258		t.Fatalf("keymaps %d, want virtual, updated virtual, seat", len(proxy.keymaps))
259	}
260}
261
262// smallKeymap is a dictation tool keymap: only the keys it types.
263const smallKeymap = `xkb_keymap {
264xkb_keycodes "(unnamed)" {
265minimum = 8;
266maximum = 26;
267<K1> = 9;
268};
269xkb_types "(unnamed)" { include "complete" };
270xkb_compatibility "(unnamed)" { include "complete" };
271xkb_symbols "(unnamed)" {
272key <K1> {[D]};
273};
274};
275`
276
277func TestWidenKeycodes(t *testing.T) {
278	const tail = "\nxkb_symbols { key <K1> {[D]}; };\n};\n"
279	cases := []struct {
280		name, in, want string
281		from           int
282	}{
283		{"small maximum", smallKeymap, strings.Replace(smallKeymap, "maximum = 26;", "maximum = 255;", 1), 26},
284		{"maximum 127..254", "xkb_keymap {\nxkb_keycodes { minimum = 8; maximum = 200; <K1> = 9; };" + tail,
285			"xkb_keymap {\nxkb_keycodes { minimum = 8; maximum = 255; <K1> = 9; };" + tail, 200},
286		{"no maximum", "xkb_keymap {\nxkb_keycodes \"x\" { minimum = 8; <K1> = 9; <K2> = 25; };" + tail,
287			"xkb_keymap {\nxkb_keycodes \"x\" { maximum = 255; minimum = 8; <K1> = 9; <K2> = 25; };" + tail, 25},
288		{"mixed case", "XKB_Keymap {\nXkb_KeyCodes { MINIMUM = 8; Maximum = 26; <K1> = 9; };" + tail,
289			"XKB_Keymap {\nXkb_KeyCodes { MINIMUM = 8; Maximum = 255; <K1> = 9; };" + tail, 26},
290		{"brace in comment", "xkb_keymap {\nxkb_keycodes {\n// ends here }\n# and } here\nminimum = 8;\nmaximum = 26;\n<K1> = 9;\n};" + tail,
291			"xkb_keymap {\nxkb_keycodes {\n// ends here }\n# and } here\nminimum = 8;\nmaximum = 255;\n<K1> = 9;\n};" + tail, 26},
292		{"commented maximum", "xkb_keymap {\nxkb_keycodes {\n// maximum = 26;\nminimum = 8;\n<K1> = 9;\n};" + tail,
293			"xkb_keymap {\nxkb_keycodes { maximum = 255;\n// maximum = 26;\nminimum = 8;\n<K1> = 9;\n};" + tail, 9},
294		{"maximum after comment", "xkb_keymap {\nxkb_keycodes {\nminimum = 8;\nmaximum = // why\n 26;\n<K1> = 9;\n};" + tail,
295			"xkb_keymap {\nxkb_keycodes {\nminimum = 8;\nmaximum = // why\n 255;\n<K1> = 9;\n};" + tail, 26},
296	}
297	for _, c := range cases {
298		got, from := widenKeycodes(c.in)
299		if got != c.want || from != c.from {
300			t.Errorf("%s: from %d want %d\n%s", c.name, from, c.from, got)
301		}
302	}
303	for name, keep := range map[string]string{
304		"seat keymap":       keymapText(t),
305		"maximum 255":       "xkb_keymap { xkb_keycodes { maximum = 255; <K1> = 9; }; };",
306		"maximum above 255": "xkb_keymap { xkb_keycodes { minimum = 8; maximum = 708; <K1> = 9; }; };",
307		"no keycodes":       "xkb_keymap { xkb_symbols { }; };",
308		"includes":          "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; };",
309		"maximum in string": "xkb_keymap { xkb_keycodes \"maximum = 26;\" { minimum = 8; <K1> = 300; }; };",
310	} {
311		if got, from := widenKeycodes(keep); from != 0 || got != keep {
312			t.Errorf("%s changed:\n%s", name, got)
313		}
314	}
315}
316
317// Xwayland overflows its key maps on a keymap whose maximum keycode is
318// small: clients get the virtual keymap with the full keycode range.
319func TestVirtualKeymapWidenedForClients(t *testing.T) {
320	s, events, commands, dir := keyboardServer(t)
321	target, proxy := focusedTarget(t, s, events, commands, dir)
322	typer, kb := virtualTyper(t, s, dir)
323	sendVirtualKeymap(t, typer, kb, smallKeymap)
324	requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(1), uint32(1))
325	roundtrip(t, typer, target)
326	deadline := time.Now().Add(2 * time.Second)
327	for len(proxy.keymaps) < 1 && time.Now().Before(deadline) {
328		roundtrip(t, target)
329		time.Sleep(5 * time.Millisecond)
330	}
331	if n := len(proxy.keymaps); n == 0 || !strings.Contains(proxy.keymaps[n-1], "maximum = 255;") || strings.Contains(proxy.keymaps[n-1], "maximum = 26;") {
332		t.Fatalf("keymaps %q", proxy.keymaps)
333	}
334}

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