v0.4.0 b6be3a8

keyboard_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"os"
6	"os/exec"
7	"regexp"
8	"slices"
9	"strings"
10	"testing"
11	"time"
12
13	"github.com/bnema/go-wayland-bindings/server/wayland"
14	"github.com/bnema/neferwl/internal/adapters/logging"
15	"github.com/bnema/neferwl/internal/ports"
16	"github.com/bnema/wlturbo"
17	"golang.org/x/sys/unix"
18)
19
20func keymapText(t *testing.T) string {
21	t.Helper()
22	data, err := os.ReadFile("testdata/us.xkb")
23	if err != nil {
24		t.Fatal(err)
25	}
26	return string(data)
27}
28
29// keyboardServer runs a server; setup, if any, runs before Run starts.
30func keyboardServer(t *testing.T, setup ...func(*Server)) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, string) {
31	t.Helper()
32	dir := t.TempDir()
33	events := make(chan ports.ClientEvent, 16)
34	commands := make(chan ports.ClientCommand, 32)
35	s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Keymap: keymapText(t), RepeatRate: 25, RepeatDelay: 600}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland"))
36	if err != nil {
37		t.Fatal(err)
38	}
39	for _, f := range setup {
40		f(s)
41	}
42	ctx, cancel := context.WithCancel(context.Background())
43	done := make(chan error, 1)
44	go func() { done <- s.Run(ctx) }()
45	t.Cleanup(func() {
46		cancel()
47		select {
48		case err := <-done:
49			if err != nil {
50				t.Errorf("Run: %v", err)
51			}
52		case <-time.After(2 * time.Second):
53			t.Error("Run hung")
54		}
55	})
56	return s, events, commands, dir
57}
58
59func TestKeyboardInfo(t *testing.T) {
60	tool, err := exec.LookPath("wayland-info")
61	if err != nil {
62		t.Skip("wayland-info not installed")
63	}
64	s, _, _, dir := keyboardServer(t)
65	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
66	defer cancel()
67	cmd := exec.CommandContext(ctx, tool)
68	cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName())
69	out, err := cmd.CombinedOutput()
70	if err != nil {
71		t.Fatalf("wayland-info: %v: %s", err, out)
72	}
73	for _, want := range []string{"capabilities: pointer keyboard", "keyboard repeat rate: 25", "keyboard repeat delay: 600"} {
74		if !strings.Contains(string(out), want) {
75			t.Errorf("missing %q: %s", want, out)
76		}
77	}
78}
79
80type keymapProxy struct {
81	wlturbo.BaseProxy
82	result chan error
83}
84
85func (p *keymapProxy) Dispatch(e *wlturbo.Event) {
86	if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
87		return
88	}
89	format := e.Uint32()
90	fd := int(e.Fd())
91	size := e.Uint32()
92	if fd <= 0 || size == 0 || format != uint32(wayland.KeyboardKeymapFormatXkbV1) {
93		p.result <- os.ErrInvalid
94		return
95	}
96	defer unix.Close(fd)
97	data := make([]byte, size)
98	_, err := unix.Pread(fd, data, 0)
99	if err == nil && !strings.HasPrefix(string(data), "xkb_keymap") {
100		err = os.ErrInvalid
101	}
102	p.result <- err
103}
104func TestKeyboardKeymapProtocol(t *testing.T) {
105	s, _, _, dir := keyboardServer(t)
106	c := protocolClient(t, s, dir)
107	seat := bindProtocol(t, c, "wl_seat")
108	registerProtocol(t, c, seat)
109	id := c.AllocateID()
110	proxy := &keymapProxy{result: make(chan error, 1)}
111	proxy.SetID(id)
112	registerWireProxy(c, proxy)
113	requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id)
114	if err := c.Roundtrip(); err != nil {
115		t.Fatal(err)
116	}
117	select {
118	case err := <-proxy.result:
119		if err != nil {
120			t.Fatal(err)
121		}
122	default:
123		t.Fatal("missing keymap event")
124	}
125}
126
127// events records keyboard event opcodes; a keymap event also checks its content.
128type eventsProxy struct {
129	wlturbo.BaseProxy
130	opcodes []uint16
131	keymaps []string
132	locked  []uint32 // locked modifiers of each modifiers event
133}
134
135func (p *eventsProxy) Dispatch(e *wlturbo.Event) {
136	p.opcodes = append(p.opcodes, e.Opcode)
137	if e.Opcode == uint16(wayland.KeyboardEventModifiers) {
138		e.Uint32() // serial
139		e.Uint32() // depressed
140		e.Uint32() // latched
141		p.locked = append(p.locked, e.Uint32())
142		return
143	}
144	if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
145		return
146	}
147	e.Uint32()
148	fd := int(e.Fd())
149	size := e.Uint32()
150	defer unix.Close(fd)
151	data := make([]byte, size)
152	if _, err := unix.Pread(fd, data, 0); err == nil {
153		p.keymaps = append(p.keymaps, strings.TrimRight(string(data), "\x00"))
154	}
155}
156
157func TestSetKeymap(t *testing.T) {
158	s, _, commands, dir := keyboardServer(t)
159	c := protocolClient(t, s, dir)
160	// Version 4 so the keyboard receives repeat_info.
161	g, ok := c.Registry().FindGlobal("wl_seat")
162	if !ok {
163		t.Fatal("missing wl_seat")
164	}
165	seat, err := bindWireID(c, g.Name, g.Interface, 4)
166	if err != nil {
167		t.Fatal(err)
168	}
169	registerProtocol(t, c, seat)
170	id := c.AllocateID()
171	proxy := &eventsProxy{}
172	proxy.SetID(id)
173	registerWireProxy(c, proxy)
174	requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id)
175	if err := c.Roundtrip(); err != nil {
176		t.Fatal(err)
177	}
178	proxy.opcodes, proxy.keymaps = nil, nil
179	sync := func() {
180		t.Helper()
181		// Commands apply on the display goroutine; wait until it has run them.
182		deadline := time.Now().Add(time.Second)
183		for len(commands) > 0 && time.Now().Before(deadline) {
184			time.Sleep(5 * time.Millisecond)
185		}
186		time.Sleep(20 * time.Millisecond)
187		if err := c.Roundtrip(); err != nil {
188			t.Fatal(err)
189		}
190	}
191
192	// Repeat-only: one repeat_info, no keymap.
193	commands <- ports.SetKeymap{RepeatRate: 40, RepeatDelay: 300}
194	sync()
195	if len(proxy.opcodes) != 1 || proxy.opcodes[0] != uint16(wayland.KeyboardEventRepeatInfo) {
196		t.Fatalf("repeat-only events %v", proxy.opcodes)
197	}
198
199	// New keymap: keymap then repeat_info, and the new content.
200	proxy.opcodes = nil
201	next := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
202	commands <- ports.SetKeymap{Keymap: next, RepeatRate: 40, RepeatDelay: 300}
203	sync()
204	want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventRepeatInfo)}
205	if !slices.Equal(proxy.opcodes, want) {
206		t.Fatalf("keymap events %v, want %v", proxy.opcodes, want)
207	}
208	if len(proxy.keymaps) != 1 || proxy.keymaps[0] != next {
209		t.Fatalf("keymap content not replaced (%d keymaps)", len(proxy.keymaps))
210	}
211	// A keyboard created afterwards gets the new keymap.
212	id2 := c.AllocateID()
213	late := &eventsProxy{}
214	late.SetID(id2)
215	registerWireProxy(c, late)
216	requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id2)
217	if err := c.Roundtrip(); err != nil {
218		t.Fatal(err)
219	}
220	if len(late.keymaps) != 1 || late.keymaps[0] != next {
221		t.Fatal("late keyboard got the old keymap")
222	}
223}
224
225func TestFootKeyboard(t *testing.T) {
226	tool, err := exec.LookPath("foot")
227	if err != nil {
228		t.Skip("foot not installed")
229	}
230	s, events, commands, dir := keyboardServer(t)
231	_, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + t.TempDir(), "WAYLAND_DEBUG=client"}, "-c", "/dev/null", "sh", "-c", "sleep 10")
232	w := mapped(t, events, 10*time.Second)
233	commands <- ports.FocusWindow{ID: w.ID}
234	for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} {
235		commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, Time: 1 * time.Millisecond}}
236		commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Time: 2 * time.Millisecond}}
237	}
238	enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`)
239	key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`)
240	deadline := time.After(4 * time.Second)
241	tick := time.NewTicker(10 * time.Millisecond)
242	defer tick.Stop()
243	for {
244		select {
245		case err := <-done:
246			t.Fatalf("foot exited: %v: %s", err, output.String())
247		case <-deadline:
248			t.Fatalf("keyboard events missing: %s", output.String())
249		case <-tick.C:
250			text := output.String()
251			if enter.MatchString(text) && key.MatchString(text) {
252				return
253			}
254		}
255	}
256}

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