v0.1.0 f503a24

keyboard_test.go RawHistoryFolder

1package neferclient
2
3import (
4	"os"
5	"testing"
6
7	"github.com/stretchr/testify/require"
8	"golang.org/x/sys/unix"
9
10	"github.com/bnema/purego-xkbcommon/raw"
11)
12
13// Evdev codes used below (US QWERTY positions).
14const (
15	keyQ         = 16
16	keyE         = 18
17	keyBackspace = 14
18	keyEnter     = 28
19	keyU         = 22
20	keyLeftShift = 42
21	keyCircum    = 41 // de: dead_circumflex
22)
23
24// testKeyboard is a keyboard on a real xkb keymap compiled from rules.
25func testKeyboard(t testing.TB, locale, layout string) *keyboard {
26	t.Helper()
27	k, err := newKeyboard(locale)
28	require.NoError(t, err)
29	t.Cleanup(k.close)
30	km, err := k.ctx.NewKeymapRules("evdev", "pc105", layout, "", "")
31	require.NoError(t, err)
32	require.NoError(t, k.replace(km))
33	k.setFocus(true)
34	return k
35}
36
37func pressKey(t testing.TB, k *keyboard, code uint32) (KeyEvent, bool) {
38	t.Helper()
39	var ev KeyEvent
40	ok, err := k.press(code, &ev)
41	require.NoError(t, err)
42	return ev, ok
43}
44
45func releaseKey(k *keyboard, code uint32) (KeyEvent, bool) {
46	var ev KeyEvent
47	ok := k.release(code, &ev)
48	return ev, ok
49}
50
51func TestKeyboardLayoutsAndModifiers(t *testing.T) {
52	for _, tc := range []struct {
53		layout string
54		code   uint32
55		want   string
56	}{{"us", keyQ, "q"}, {"fr", keyQ, "a"}, {"de", 21, "z"}} {
57		k := testKeyboard(t, "en_US.UTF-8", tc.layout)
58		ev, ok := pressKey(t, k, tc.code)
59		require.True(t, ok)
60		require.Equal(t, tc.want, string(ev.Text), tc.layout)
61		require.True(t, ev.Pressed)
62		require.NotZero(t, ev.Keysym)
63		require.Equal(t, tc.code, ev.Keycode)
64		rel, ok := releaseKey(k, tc.code)
65		require.True(t, ok)
66		require.False(t, rel.Pressed)
67		require.Nil(t, rel.Text)
68		require.Equal(t, ev.Keysym, rel.Keysym)
69	}
70
71	k := testKeyboard(t, "en_US.UTF-8", "us")
72	idx, err := k.keymap.ModIndex("Shift")
73	require.NoError(t, err)
74	require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
75	ev, _ := pressKey(t, k, keyQ)
76	require.Equal(t, "Q", string(ev.Text))
77	require.Equal(t, ModShift, ev.Modifiers)
78
79	// The compositor's mask survives a keymap change.
80	km, err := k.ctx.NewKeymapRules("evdev", "pc105", "fr", "", "")
81	require.NoError(t, err)
82	require.NoError(t, k.replace(km))
83	k.setFocus(true)
84	ev, _ = pressKey(t, k, keyQ)
85	require.Equal(t, "A", string(ev.Text))
86}
87
88func TestKeyboardKeymapFD(t *testing.T) {
89	data, err := os.ReadFile("testdata/fr.xkb")
90	require.NoError(t, err)
91	f, err := os.CreateTemp(t.TempDir(), "keymap")
92	require.NoError(t, err)
93	defer f.Close()
94	_, err = f.Write(append(data, 0))
95	require.NoError(t, err)
96
97	k := testKeyboard(t, "C", "us")
98	fd, err := unix.Dup(int(f.Fd()))
99	require.NoError(t, err)
100	require.NoError(t, k.replaceFD(fd, len(data)+1))
101	_, err = unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
102	require.ErrorIs(t, err, unix.EBADF, "replaceFD consumes the descriptor")
103	k.setFocus(true)
104	ev, _ := pressKey(t, k, keyQ)
105	require.Equal(t, "a", string(ev.Text))
106
107	// A failing map keeps the previous one and still consumes its fd.
108	bad, err := unix.Dup(int(f.Fd()))
109	require.NoError(t, err)
110	require.Error(t, k.replaceFD(bad, 5))
111	_, err = unix.FcntlInt(uintptr(bad), unix.F_GETFD, 0)
112	require.ErrorIs(t, err, unix.EBADF)
113	ev, _ = pressKey(t, k, keyQ)
114	require.Equal(t, "a", string(ev.Text))
115}
116
117func TestKeyboardComposeAndFocus(t *testing.T) {
118	k := testKeyboard(t, "en_US.UTF-8", "de")
119	dead, _ := pressKey(t, k, keyCircum)
120	require.Equal(t, uint32(raw.XKB_KEY_dead_circumflex), dead.Keysym)
121	require.Nil(t, dead.Text)
122	releaseKey(k, keyCircum)
123	composed, _ := pressKey(t, k, keyE)
124	require.Equal(t, "ê", string(composed.Text))
125	releaseKey(k, keyE)
126
127	pressKey(t, k, keyCircum)
128	k.setFocus(false)
129	_, ok := pressUnchecked(k, keyE)
130	require.False(t, ok, "no key without focus")
131	k.setFocus(true)
132	ev, _ := pressKey(t, k, keyE)
133	require.Equal(t, "e", string(ev.Text), "focus loss resets compose")
134}
135
136func pressUnchecked(k *keyboard, code uint32) (KeyEvent, bool) {
137	var ev KeyEvent
138	ok, _ := k.press(code, &ev)
139	return ev, ok
140}
141
142func TestKeyboardRepeatTimer(t *testing.T) {
143	k := testKeyboard(t, "C", "us")
144	k.setRepeat(20, 50)
145	_, ok := pressKey(t, k, keyQ)
146	require.True(t, ok)
147	require.Equal(t, uint32(keyQ), k.repeating)
148	var ev KeyEvent
149	require.Eventually(t, k.expired, 2e9, 5e6)
150	ok, err := k.repeat(&ev)
151	require.NoError(t, err)
152	require.True(t, ok)
153	require.True(t, ev.Repeat)
154	require.Equal(t, "q", string(ev.Text))
155
156	releaseKey(k, keyQ)
157	require.Zero(t, k.repeating)
158	require.False(t, k.expired(), "timer disarmed on release")
159
160	// Modifiers never repeat.
161	pressKey(t, k, keyLeftShift)
162	require.Zero(t, k.repeating)
163	// A rate of 0 disables repeat.
164	k.setRepeat(0, 0)
165	pressKey(t, k, keyQ)
166	require.Zero(t, k.repeating)
167}
168
169func TestKeyboardRepeatTracksCurrentMask(t *testing.T) {
170	k := testKeyboard(t, "C", "us")
171	k.setRepeat(100, 1)
172	pressKey(t, k, keyQ)
173	idx, err := k.keymap.ModIndex("Shift")
174	require.NoError(t, err)
175	require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
176	require.Eventually(t, k.expired, 2e9, 1e6)
177	var ev KeyEvent
178	ok, err := k.repeat(&ev)
179	require.NoError(t, err)
180	require.True(t, ok)
181	require.Equal(t, "Q", string(ev.Text))
182}
183
184// TestSecretKeysNeverReachText covers the secret rules end to end on the
185// keyboard: printable text only into the buffer, BackSpace by code point,
186// every other key delivered without text.
187func TestSecretKeysNeverReachText(t *testing.T) {
188	k := testKeyboard(t, "en_US.UTF-8", "de")
189	buf := NewSecretBuffer(8)
190	k.setSecret(buf)
191
192	press := func(code uint32) KeyEvent {
193		ev, ok := pressKey(t, k, code)
194		require.True(t, ok)
195		require.Nil(t, ev.Text, "secret mode never fills KeyEvent.Text")
196		rel, ok := releaseKey(k, code)
197		require.True(t, ok)
198		require.Equal(t, ev.Secret, rel.Secret, "the release is hidden like the press")
199		if ev.Secret {
200			require.Zero(t, rel.Keycode)
201			require.Zero(t, rel.Keysym)
202		}
203		return ev
204	}
205	for _, code := range []uint32{keyQ, keyCircum, keyE} { // text, dead key, composed ê
206		ev := press(code)
207		require.True(t, ev.Secret, "text keys are hidden")
208		require.Zero(t, ev.Keycode)
209		require.Zero(t, ev.Keysym)
210	}
211	require.Equal(t, "qê", string(buf.Bytes()))
212	require.Equal(t, 2, buf.Len())
213
214	bs := press(keyBackspace)
215	require.False(t, bs.Secret)
216	require.Equal(t, uint32(raw.XKB_KEY_BackSpace), bs.Keysym, "BackSpace keeps its keysym")
217	require.Equal(t, 1, buf.Len(), "backspace removes one code point")
218	press(keyBackspace)
219	press(keyBackspace) // empty: nothing to remove
220	require.Zero(t, buf.Len())
221
222	press(keyQ)
223	enter := press(keyEnter)
224	require.False(t, enter.Secret)
225	require.Equal(t, uint32(raw.XKB_KEY_Return), enter.Keysym)
226	require.Equal(t, 1, buf.Len(), "Enter is delivered, not typed")
227
228	// Control combinations are keys, not text.
229	idx, err := k.keymap.ModIndex("Control")
230	require.NoError(t, err)
231	require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
232	ev := press(keyU)
233	require.False(t, ev.Secret)
234	require.NotZero(t, ev.Keysym, "Control combinations keep their keysym")
235	require.Equal(t, ModCtrl, ev.Modifiers)
236	require.Equal(t, 1, buf.Len())
237	require.NoError(t, k.setMask(0, 0, 0, 0))
238
239	// Capacity: extra text is dropped, the key is still delivered.
240	for range 10 {
241		press(keyQ)
242	}
243	require.Equal(t, 8, buf.Len())
244
245	// Scratch and the work area are wiped after use.
246	require.Equal(t, make([]byte, scratchSize), k.scratch[:])
247	require.Equal(t, secretWork{}, k.w)
248	for _, h := range k.held {
249		require.Zero(t, h, "no stale held key")
250	}
251
252	k.setSecret(nil)
253	ev, _ = pressKey(t, k, keyQ)
254	require.False(t, ev.Secret)
255	require.Equal(t, "q", string(ev.Text), "ordinary text again once the buffer is removed")
256}
257
258func TestSecretChangedFlag(t *testing.T) {
259	k := testKeyboard(t, "C", "us")
260	buf := NewSecretBuffer(2)
261	k.setSecret(buf)
262	pressKey(t, k, keyQ)
263	require.True(t, k.changed)
264	pressKey(t, k, keyEnter)
265	require.False(t, k.changed)
266	pressKey(t, k, keyBackspace)
267	require.True(t, k.changed)
268	pressKey(t, k, keyBackspace)
269	require.False(t, k.changed, "nothing left to remove")
270}
271
272func TestSecretRepeat(t *testing.T) {
273	k := testKeyboard(t, "C", "us")
274	buf := NewSecretBuffer(16)
275	k.setSecret(buf)
276	k.setRepeat(100, 1)
277	var ev KeyEvent
278	pressKey(t, k, keyQ)
279	for range 2 {
280		require.Eventually(t, k.expired, 2e9, 1e6)
281		ok, err := k.repeat(&ev)
282		require.NoError(t, err)
283		require.True(t, ok)
284		require.Nil(t, ev.Text)
285	}
286	require.Equal(t, "qqq", string(buf.Bytes()))
287
288	// Enter cancels a previous printable repeat.
289	pressKey(t, k, keyEnter)
290	require.Zero(t, k.repeating)
291	releaseKey(k, keyQ)
292	releaseKey(k, keyEnter)
293
294	// Held backspace repeats.
295	pressKey(t, k, keyBackspace)
296	require.Equal(t, uint32(keyBackspace), k.repeating)
297	require.Eventually(t, k.expired, 2e9, 1e6)
298	ok, err := k.repeat(&ev)
299	require.NoError(t, err)
300	require.True(t, ok)
301	require.Equal(t, 1, buf.Len())
302}
303
304func TestSecretRepeatIsHiddenToo(t *testing.T) {
305	k := testKeyboard(t, "C", "us")
306	k.setSecret(NewSecretBuffer(8))
307	k.setRepeat(100, 1)
308	pressKey(t, k, keyQ)
309	require.Eventually(t, k.expired, 2e9, 1e6)
310	var ev KeyEvent
311	ok, err := k.repeat(&ev)
312	require.NoError(t, err)
313	require.True(t, ok)
314	require.True(t, ev.Secret)
315	require.Zero(t, ev.Keycode)
316	require.Zero(t, ev.Keysym)
317}
318
319func TestKeyboardHeldTailAndResetAreCleared(t *testing.T) {
320	k := testKeyboard(t, "C", "us")
321	k.setSecret(NewSecretBuffer(8))
322	pressKey(t, k, keyQ)
323	pressKey(t, k, keyE)
324	releaseKey(k, keyQ)
325	require.Equal(t, heldKey{}, k.held[:cap(k.held)][1], "vacated slot cleared")
326	k.setFocus(false)
327	for _, h := range k.held[:cap(k.held)] {
328		require.Zero(t, h)
329	}
330}
331
332func TestKeyboardIdenticalMaskIsNoOp(t *testing.T) {
333	k := testKeyboard(t, "C", "us")
334	idx, err := k.keymap.ModIndex("Shift")
335	require.NoError(t, err)
336	require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
337	require.Equal(t, ModShift, k.mods)
338	require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
339	require.Equal(t, ModShift, k.mods)
340	require.NoError(t, k.setMask(0, 0, 0, 0))
341	require.Zero(t, k.mods)
342}

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 4

↑↓ moveenter openesc close