textinput_test.go RawHistoryFolder

1package wayland
2
3import (
4	"fmt"
5	"slices"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/bnema/go-wayland-bindings/server/inputmethod"
11	"github.com/bnema/go-wayland-bindings/server/textinput"
12	"github.com/bnema/go-wayland-bindings/server/wayland"
13	"github.com/bnema/go-wayland-bindings/server/xdgshell"
14	"github.com/bnema/neferwl/internal/ports"
15	"github.com/bnema/wlturbo"
16	"golang.org/x/sys/unix"
17)
18
19// imeLog records events of one object as "name arg arg" strings, from
20// the event signatures: u, i, o (IDs), s (strings) and h (fds, closed).
21type imeLog struct {
22	wlturbo.BaseProxy
23	events []string
24	sigs   []struct{ name, args string }
25}
26
27func (l *imeLog) Dispatch(e *wlturbo.Event) {
28	if int(e.Opcode) >= len(l.sigs) {
29		l.events = append(l.events, fmt.Sprint("unknown ", e.Opcode))
30		return
31	}
32	sig := l.sigs[e.Opcode]
33	parts := []string{sig.name}
34	for _, a := range sig.args {
35		switch a {
36		case 'u', 'o':
37			parts = append(parts, fmt.Sprint(e.Uint32()))
38		case 'i':
39			parts = append(parts, fmt.Sprint(e.Int32()))
40		case 's':
41			parts = append(parts, fmt.Sprintf("%q", e.String()))
42		case 'h':
43			if fd := int(e.Fd()); fd > 0 {
44				unix.Close(fd)
45			}
46			parts = append(parts, "fd")
47		}
48	}
49	l.events = append(l.events, strings.Join(parts, " "))
50}
51
52// take returns the recorded events and forgets them.
53func (l *imeLog) take() []string {
54	ev := l.events
55	l.events = nil
56	return ev
57}
58
59func newIMELog(c *wlturbo.Display, id uint32, sigs ...string) *imeLog {
60	l := &imeLog{}
61	for _, s := range sigs {
62		name, args, _ := strings.Cut(s, ":")
63		l.sigs = append(l.sigs, struct{ name, args string }{name, args})
64	}
65	l.SetID(id)
66	registerWireProxy(c, l)
67	return l
68}
69
70var (
71	textInputEvents   = []string{"enter:o", "leave:o", "preedit_string:sii", "commit_string:s", "delete_surrounding_text:uu", "done:u"}
72	inputMethodEvents = []string{"activate", "deactivate", "surrounding_text:suu", "text_change_cause:u", "content_type:uu", "done", "unavailable"}
73	grabEvents        = []string{"keymap:uhu", "key:uuuu", "modifiers:uuuuu", "repeat_info:ii"}
74)
75
76// textApp is a focused client window with a text input.
77type textApp struct {
78	c        *wlturbo.Display
79	ti, surf uint32
80	top      uint32
81	window   ports.WindowID
82	log      *imeLog
83	keys     *eventsProxy
84}
85
86func newTextApp(t *testing.T, s *Server, events chan ports.ClientEvent, commands chan ports.ClientCommand, dir string) *textApp {
87	t.Helper()
88	c := protocolClient(t, s, dir)
89	seat := bindProtocol(t, c, "wl_seat")
90	registerProtocol(t, c, seat)
91	keys := &eventsProxy{}
92	keys.SetID(c.AllocateID())
93	registerWireProxy(c, keys)
94	requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keys.ID())
95	w, surf, xdg := surfaceMapper(t, c, events)()
96	manager := bindProtocol(t, c, "zwp_text_input_manager_v3")
97	registerProtocol(t, c, manager)
98	ti := c.AllocateID()
99	log := newIMELog(c, ti, textInputEvents...)
100	requestProtocol(t, c, manager, textinput.ZwpTextInputManagerV3RequestGetTextInput, ti, seat)
101	commands <- ports.FocusWindow{ID: w.ID}
102	waitFocus(t, s, w.ID)
103	roundtrip(t, c)
104	// surfaceMapper allocated the toplevel right after the xdg_surface.
105	return &textApp{c: c, ti: ti, surf: surf, top: xdg + 1, window: w.ID, log: log, keys: keys}
106}
107
108func (a *textApp) request(t *testing.T, op uint32, args ...any) {
109	t.Helper()
110	requestProtocol(t, a.c, a.ti, op, args...)
111}
112
113// enable commits an enabled text input with surrounding text and content type.
114func (a *textApp) enable(t *testing.T) {
115	t.Helper()
116	a.request(t, textinput.ZwpTextInputV3RequestEnable)
117	a.request(t, textinput.ZwpTextInputV3RequestSetSurroundingText, "hello", int32(5), int32(5))
118	a.request(t, textinput.ZwpTextInputV3RequestSetContentType, uint32(textinput.ZwpTextInputV3ContentHintSpellcheck), uint32(textinput.ZwpTextInputV3ContentPurposeNormal))
119	a.request(t, textinput.ZwpTextInputV3RequestCommit)
120}
121
122// imeApp is an input method client.
123type imeApp struct {
124	c   *wlturbo.Display
125	im  uint32
126	log *imeLog
127}
128
129func newIMEApp(t *testing.T, s *Server, dir string) *imeApp {
130	t.Helper()
131	c := protocolClient(t, s, dir)
132	seat := bindProtocol(t, c, "wl_seat")
133	registerProtocol(t, c, seat)
134	manager := bindProtocol(t, c, "zwp_input_method_manager_v2")
135	registerProtocol(t, c, manager)
136	im := c.AllocateID()
137	log := newIMELog(c, im, inputMethodEvents...)
138	requestProtocol(t, c, manager, inputmethod.ZwpInputMethodManagerV2RequestGetInputMethod, seat, im)
139	roundtrip(t, c)
140	return &imeApp{c: c, im: im, log: log}
141}
142
143func (m *imeApp) request(t *testing.T, op uint32, args ...any) {
144	t.Helper()
145	requestProtocol(t, m.c, m.im, op, args...)
146}
147
148func (m *imeApp) grab(t *testing.T) (uint32, *imeLog) {
149	t.Helper()
150	g := m.c.AllocateID()
151	log := newIMELog(m.c, g, grabEvents...)
152	m.request(t, inputmethod.ZwpInputMethodV2RequestGrabKeyboard, g)
153	return g, log
154}
155
156// settle lets the server handle every connection's requests, in order.
157func settle(t *testing.T, cs ...*wlturbo.Display) {
158	t.Helper()
159	roundtrip(t, cs...)
160	roundtrip(t, cs...)
161}
162
163func expectEvents(t *testing.T, what string, got []string, want ...string) {
164	t.Helper()
165	if !slices.Equal(got, want) {
166		t.Fatalf("%s events:\n got %q\nwant %q", what, got, want)
167	}
168}
169
170// Text input focus follows keyboard focus across clients and window close.
171func TestTextInputFollowsFocus(t *testing.T) {
172	s, events, commands, dir := keyboardServer(t)
173	a := newTextApp(t, s, events, commands, dir)
174	expectEvents(t, "a", a.log.take(), fmt.Sprint("enter ", a.surf))
175	b := newTextApp(t, s, events, commands, dir)
176	settle(t, a.c, b.c)
177	expectEvents(t, "a", a.log.take(), fmt.Sprint("leave ", a.surf))
178	expectEvents(t, "b", b.log.take(), fmt.Sprint("enter ", b.surf))
179
180	commands <- ports.FocusWindow{ID: a.window}
181	waitFocus(t, s, a.window)
182	settle(t, a.c, b.c)
183	expectEvents(t, "a", a.log.take(), fmt.Sprint("enter ", a.surf))
184	expectEvents(t, "b", b.log.take(), fmt.Sprint("leave ", b.surf))
185
186	// Closing the focused window leaves.
187	requestProtocol(t, a.c, a.top, xdgshell.ToplevelRequestDestroy)
188	settle(t, a.c)
189	waitFocus(t, s, 0)
190	settle(t, a.c)
191	expectEvents(t, "a", a.log.take(), fmt.Sprint("leave ", a.surf))
192}
193
194// Enable activates the input method with the client state; its preedit,
195// commit and delete reach the client in order with the commit count.
196func TestTextInputIMERoundTrip(t *testing.T) {
197	s, events, commands, dir := keyboardServer(t)
198	ime := newIMEApp(t, s, dir)
199	app := newTextApp(t, s, events, commands, dir)
200	app.log.take()
201	app.enable(t)
202	settle(t, app.c, ime.c)
203	expectEvents(t, "ime", ime.log.take(), "activate", `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
204
205	ime.request(t, inputmethod.ZwpInputMethodV2RequestSetPreeditString, "nihon", int32(5), int32(5))
206	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(1))
207	settle(t, ime.c, app.c)
208	expectEvents(t, "app", app.log.take(), `preedit_string "nihon" 5 5`, "done 1")
209
210	ime.request(t, inputmethod.ZwpInputMethodV2RequestDeleteSurroundingText, uint32(1), uint32(0))
211	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "日本")
212	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(1))
213	settle(t, ime.c, app.c)
214	expectEvents(t, "app", app.log.take(), `commit_string "日本"`, "delete_surrounding_text 1 0", "done 1")
215
216	// State updates reach the input method; done carries the latest
217	// client commit count even when the input method is behind.
218	app.request(t, textinput.ZwpTextInputV3RequestSetSurroundingText, "hello日本", int32(11), int32(11))
219	app.request(t, textinput.ZwpTextInputV3RequestSetTextChangeCause, uint32(textinput.ZwpTextInputV3ChangeCauseOther))
220	app.request(t, textinput.ZwpTextInputV3RequestCommit)
221	app.request(t, textinput.ZwpTextInputV3RequestCommit)
222	settle(t, app.c, ime.c)
223	expectEvents(t, "ime", ime.log.take(), `surrounding_text "hello日本" 11 11`, "text_change_cause 1", "content_type 2 0", "done", `surrounding_text "hello日本" 11 11`, "text_change_cause 0", "content_type 2 0", "done")
224	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "x")
225	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(1))
226	settle(t, ime.c, app.c)
227	expectEvents(t, "app", app.log.take(), `commit_string "x"`, "done 3")
228
229	app.request(t, textinput.ZwpTextInputV3RequestDisable)
230	app.request(t, textinput.ZwpTextInputV3RequestCommit)
231	settle(t, app.c, ime.c)
232	expectEvents(t, "ime", ime.log.take(), "deactivate", "done")
233	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "lost")
234	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(4))
235	settle(t, ime.c, app.c)
236	expectEvents(t, "app", app.log.take())
237}
238
239// Focus loss deactivates; a second text input cannot take over; state
240// past the spec limit is ignored.
241func TestTextInputFocusLossAndLimits(t *testing.T) {
242	s, events, commands, dir := keyboardServer(t)
243	ime := newIMEApp(t, s, dir)
244	app := newTextApp(t, s, events, commands, dir)
245	app.enable(t)
246	settle(t, app.c, ime.c)
247	ime.log.take()
248
249	app.request(t, textinput.ZwpTextInputV3RequestSetSurroundingText, strings.Repeat("a", maxIMEText+1), int32(0), int32(0))
250	app.request(t, textinput.ZwpTextInputV3RequestCommit)
251	settle(t, app.c, ime.c)
252	expectEvents(t, "ime", ime.log.take(), `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
253
254	other := newTextApp(t, s, events, commands, dir)
255	settle(t, app.c, ime.c)
256	expectEvents(t, "ime", ime.log.take(), "deactivate", "done")
257	// After leave, app's requests are ignored until the next enter.
258	app.enable(t)
259	settle(t, app.c, ime.c)
260	expectEvents(t, "ime", ime.log.take())
261	other.enable(t)
262	settle(t, other.c, ime.c)
263	expectEvents(t, "ime", ime.log.take(), "activate", `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
264}
265
266// Only one input method at a time; the slot frees when it disconnects,
267// and the next one is activated for the enabled text input.
268func TestInputMethodSingleSlot(t *testing.T) {
269	s, events, commands, dir := keyboardServer(t)
270	first := newIMEApp(t, s, dir)
271	second := newIMEApp(t, s, dir)
272	expectEvents(t, "second", second.log.take(), "unavailable")
273	// An inert input method's requests do nothing.
274	second.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "x")
275	second.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(0))
276	app := newTextApp(t, s, events, commands, dir)
277	app.enable(t)
278	settle(t, second.c, app.c, first.c)
279	app.log.take()
280	_ = first.c.Close()
281	deadline := time.Now().Add(2 * time.Second)
282	for len(app.log.events) == 0 && time.Now().Before(deadline) {
283		roundtrip(t, app.c)
284		time.Sleep(5 * time.Millisecond)
285	}
286	// The preedit is cleared by an empty done.
287	expectEvents(t, "app", app.log.take(), "done 1")
288	third := newIMEApp(t, s, dir)
289	expectEvents(t, "third", third.log.take(), "activate", `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
290}
291
292// until roundtrips cs until ok holds: commands reach the display
293// goroutine on their own channel, not in order with requests.
294func until(t *testing.T, what string, ok func() bool, cs ...*wlturbo.Display) {
295	t.Helper()
296	for deadline := time.Now().Add(2 * time.Second); !ok(); time.Sleep(5 * time.Millisecond) {
297		if time.Now().After(deadline) {
298			t.Fatal(what)
299		}
300		roundtrip(t, cs...)
301	}
302}
303
304// A keyboard grab takes the focused client's keys until released; a key
305// the client saw pressed is released to the client.
306func TestInputMethodKeyboardGrab(t *testing.T) {
307	s, events, commands, dir := keyboardServer(t)
308	app := newTextApp(t, s, events, commands, dir)
309	ime := newIMEApp(t, s, dir)
310	app.keys.opcodes = nil
311	key := func(code uint32, pressed bool) {
312		commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, TimeMsec: 7}}
313	}
314	k := uint16(wayland.KeyboardEventKey)
315	key(31, true) // held by the client before the grab
316	until(t, "client did not get the key before the grab", func() bool { return len(app.keys.opcodes) == 1 }, app.c)
317	g, grab := ime.grab(t)
318	settle(t, ime.c)
319	got := grab.take()
320	if len(got) != 3 || got[0] != "keymap 1 fd "+fmt.Sprint(len(keymapText(t))+1) || got[1] != "repeat_info 25 600" || !strings.HasPrefix(got[2], "modifiers ") {
321		t.Fatalf("grab setup %q", got)
322	}
323	key(30, true)
324	key(30, false)
325	key(31, false)
326	until(t, "client did not get the release of 31", func() bool { return len(app.keys.opcodes) == 2 }, app.c)
327	settle(t, ime.c)
328	if len(grab.events) != 2 || !strings.HasSuffix(grab.events[0], " 7 30 1") || !strings.HasSuffix(grab.events[1], " 7 30 0") {
329		t.Fatalf("grab keys %q", grab.events)
330	}
331	if !slices.Equal(app.keys.opcodes, []uint16{k, k}) {
332		t.Fatalf("client keys %v, want the press and release of 31", app.keys.opcodes)
333	}
334	// 33 is pressed through the grab and released after it ends: the
335	// client never sees it.
336	key(33, true)
337	until(t, "grab did not get 33", func() bool { return len(grab.events) == 3 }, ime.c)
338	requestProtocol(t, ime.c, g, inputmethod.ZwpInputMethodKeyboardGrabV2RequestRelease)
339	settle(t, ime.c)
340	app.keys.opcodes = nil
341	key(33, false)
342	key(32, true)
343	until(t, "key after release did not reach the client", func() bool { return slices.Contains(app.keys.opcodes, k) }, app.c)
344	settle(t, ime.c)
345	if len(grab.events) != 3 {
346		t.Fatalf("released grab got %q", grab.events)
347	}
348	keys := 0
349	for _, op := range app.keys.opcodes {
350		if op == k {
351			keys++
352		}
353	}
354	if keys != 1 {
355		t.Fatalf("client keys %v, want only the press of 32", app.keys.opcodes)
356	}
357}
358
359// Disconnects and destroyed surfaces in active states do not break the
360// seat: keys return to the client, the input method is deactivated.
361func TestTextInputTeardown(t *testing.T) {
362	s, events, commands, dir := keyboardServer(t)
363	ime := newIMEApp(t, s, dir)
364	app := newTextApp(t, s, events, commands, dir)
365	app.enable(t)
366	ime.grab(t)
367	settle(t, app.c, ime.c)
368	ime.log.take()
369
370	// The focused surface dies with an active text input.
371	requestProtocol(t, app.c, app.top, xdgshell.ToplevelRequestDestroy)
372	requestProtocol(t, app.c, app.surf, wayland.SurfaceRequestDestroy)
373	settle(t, app.c, ime.c)
374	unmapped(t, events, app.window)
375	waitFocus(t, s, 0)
376	settle(t, ime.c)
377	expectEvents(t, "ime", ime.log.take(), "deactivate", "done")
378
379	other := newTextApp(t, s, events, commands, dir)
380	other.enable(t)
381	settle(t, other.c, ime.c)
382	ime.log.take()
383	_ = other.c.Close()
384	deadline := time.Now().Add(2 * time.Second)
385	for len(ime.log.events) == 0 && time.Now().Before(deadline) {
386		roundtrip(t, ime.c)
387		time.Sleep(5 * time.Millisecond)
388	}
389	expectEvents(t, "ime", ime.log.take(), "deactivate", "done")
390	unmapped(t, events, other.window)
391
392	// With the grabbing input method gone, keys reach the client again.
393	last := newTextApp(t, s, events, commands, dir)
394	_ = ime.c.Close()
395	last.keys.opcodes = nil
396	deadline = time.Now().Add(2 * time.Second)
397	for !slices.Contains(last.keys.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) {
398		commands <- ports.ForwardKey{ID: last.window, Key: ports.KeyEvent{Keycode: 30, Pressed: true}}
399		commands <- ports.ForwardKey{ID: last.window, Key: ports.KeyEvent{Keycode: 30}}
400		roundtrip(t, last.c)
401		time.Sleep(10 * time.Millisecond)
402	}
403	if !slices.Contains(last.keys.opcodes, uint16(wayland.KeyboardEventKey)) {
404		t.Fatal("keys did not return to the client")
405	}
406}
407
408// An input popup gets the text cursor rectangle; a surface with a role
409// cannot become one.
410func TestInputPopupSurface(t *testing.T) {
411	s, events, commands, dir := keyboardServer(t)
412	ime := newIMEApp(t, s, dir)
413	app := newTextApp(t, s, events, commands, dir)
414	comp := bindProtocol(t, ime.c, "wl_compositor")
415	surf, popup := ime.c.AllocateID(), ime.c.AllocateID()
416	registerProtocol(t, ime.c, surf)
417	requestProtocol(t, ime.c, comp, wayland.CompositorRequestCreateSurface, surf)
418	rects := newIMELog(ime.c, popup, "text_input_rectangle:iiii")
419	ime.request(t, inputmethod.ZwpInputMethodV2RequestGetInputPopupSurface, popup, surf)
420	app.request(t, textinput.ZwpTextInputV3RequestEnable)
421	app.request(t, textinput.ZwpTextInputV3RequestSetCursorRectangle, int32(10), int32(20), int32(2), int32(16))
422	app.request(t, textinput.ZwpTextInputV3RequestCommit)
423	settle(t, ime.c, app.c, ime.c)
424	expectEvents(t, "popup", rects.take(), "text_input_rectangle 10 20 2 16")
425
426	again := ime.c.AllocateID()
427	registerProtocol(t, ime.c, again)
428	ime.request(t, inputmethod.ZwpInputMethodV2RequestGetInputPopupSurface, again, surf)
429	expectProtocolError(t, ime.c, ime.im, uint32(inputmethod.ZwpInputMethodV2ErrorRole))
430}
431
432// tagged forwards a text input's events to a shared log, prefixed, so
433// the order across objects shows.
434type tagged struct {
435	wlturbo.BaseProxy
436	tag  string
437	into *imeLog
438}
439
440func (p *tagged) Dispatch(e *wlturbo.Event) {
441	p.into.Dispatch(e)
442	last := &p.into.events[len(p.into.events)-1]
443	*last = p.tag + " " + *last
444}
445
446// On a focus change every leave comes before any enter, whatever the
447// order the text inputs were created in.
448func TestTextInputLeaveBeforeEnter(t *testing.T) {
449	s, events, commands, dir := keyboardServer(t)
450	c := protocolClient(t, s, dir)
451	seat := bindProtocol(t, c, "wl_seat")
452	registerProtocol(t, c, seat)
453	mapWindow := surfaceMapper(t, c, events)
454	a, aSurf, _ := mapWindow()
455	b, bSurf, _ := mapWindow()
456	manager := bindProtocol(t, c, "zwp_text_input_manager_v3")
457	registerProtocol(t, c, manager)
458	log := newIMELog(c, 0, textInputEvents...)
459	for _, tag := range []string{"t1", "t2"} {
460		p := &tagged{tag: tag, into: log}
461		p.SetID(c.AllocateID())
462		registerWireProxy(c, p)
463		requestProtocol(t, c, manager, textinput.ZwpTextInputManagerV3RequestGetTextInput, p.ID(), seat)
464	}
465	commands <- ports.FocusWindow{ID: a.ID}
466	waitFocus(t, s, a.ID)
467	settle(t, c)
468	log.take()
469	commands <- ports.FocusWindow{ID: b.ID}
470	waitFocus(t, s, b.ID)
471	settle(t, c)
472	expectEvents(t, "focus", log.take(), fmt.Sprint("t1 leave ", aSurf), fmt.Sprint("t2 leave ", aSurf), fmt.Sprint("t1 enter ", bSurf), fmt.Sprint("t2 enter ", bSurf))
473}
474
475// A commit sent for a previous activation does not reach the next text
476// input; one for the current activation does.
477func TestInputMethodCommitBeforeActivationDropped(t *testing.T) {
478	s, events, commands, dir := keyboardServer(t)
479	ime := newIMEApp(t, s, dir)
480	first := newTextApp(t, s, events, commands, dir)
481	first.enable(t)
482	settle(t, first.c, ime.c)
483	expectEvents(t, "ime", ime.log.take(), "activate", `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
484	second := newTextApp(t, s, events, commands, dir)
485	second.log.take()
486	second.enable(t)
487	settle(t, second.c, ime.c)
488	if got := ime.log.take(); len(got) != 7 || got[0] != "deactivate" || got[2] != "activate" {
489		t.Fatalf("ime events %q", got)
490	}
491	// Serial 1 is first's activation; second's is 3.
492	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "late")
493	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(1))
494	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "ok")
495	ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(3))
496	settle(t, ime.c, second.c)
497	expectEvents(t, "second", second.log.take(), `commit_string "ok"`, "done 1")
498}
499
500// An unavailable input method's popup request has no effect: the surface
501// keeps no role.
502func TestInertInputMethodPopupIgnored(t *testing.T) {
503	s, _, _, dir := keyboardServer(t)
504	newIMEApp(t, s, dir)
505	inert := newIMEApp(t, s, dir)
506	inert.log.take()
507	comp := bindProtocol(t, inert.c, "wl_compositor")
508	surf := inert.c.AllocateID()
509	registerProtocol(t, inert.c, surf)
510	requestProtocol(t, inert.c, comp, wayland.CompositorRequestCreateSurface, surf)
511	for range 2 {
512		popup := inert.c.AllocateID()
513		registerProtocol(t, inert.c, popup)
514		inert.request(t, inputmethod.ZwpInputMethodV2RequestGetInputPopupSurface, popup, surf)
515	}
516	settle(t, inert.c)
517	expectEvents(t, "inert", inert.log.take())
518}

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