feat/data-dnd e05cb15

libinput_test.go RawHistoryFolder

1package libinput
2
3import (
4	"bytes"
5	"errors"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/bnema/neferwl/internal/adapters/xkb"
11	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/zerowrap"
14)
15
16func TestHotkey(t *testing.T) {
17	ca := ports.ModCtrl | ports.ModAlt
18	for _, tc := range []struct {
19		name   string
20		ev     ports.KeyEvent
21		action hotkeyAction
22		vt     int
23	}{
24		{"ctrl alt backspace", ports.KeyEvent{Keysym: "BackSpace", Keycode: 14, Mods: ca, Pressed: true}, hotkeyQuit, 0},
25		{"release ignored", ports.KeyEvent{Keysym: "BackSpace", Keycode: 14, Mods: ca}, hotkeyNone, 0},
26		{"ctrl only", ports.KeyEvent{Keysym: "BackSpace", Keycode: 14, Mods: ports.ModCtrl, Pressed: true}, hotkeyNone, 0},
27		{"xkb vt sym", ports.KeyEvent{Keysym: "XF86Switch_VT_3", Keycode: 61, Mods: ca, Pressed: true}, hotkeyVT, 3},
28		{"ctrl alt f2 raw", ports.KeyEvent{Keysym: "F2", Keycode: 60, Mods: ca, Pressed: true}, hotkeyVT, 2},
29		{"ctrl alt f12 raw", ports.KeyEvent{Keysym: "F12", Keycode: 88, Mods: ca, Pressed: true}, hotkeyVT, 12},
30		{"plain f2", ports.KeyEvent{Keysym: "F2", Keycode: 60, Pressed: true}, hotkeyNone, 0},
31	} {
32		for _, protected := range []bool{false, true} {
33			want := tc.action
34			if protected && want == hotkeyQuit {
35				want = hotkeyNone
36			}
37			a, vt := hotkey(tc.ev, protected)
38			if a != want || vt != tc.vt {
39				t.Errorf("%s protected=%v: got %v %d", tc.name, protected, a, vt)
40			}
41		}
42	}
43}
44
45func TestGesturesIgnoreSecondTouchpad(t *testing.T) {
46	var g gestures
47	const pad, other = 1, 2
48	g.begin(pad, ports.GestureSwipe, 3, time.Second)
49	if ev := g.begin(other, ports.GestureSwipe, 3, time.Second); ev != nil {
50		t.Fatalf("second touchpad began: %v", ev)
51	}
52	if ev := g.swipeUpdate(other, 1, 1, 1, 1, time.Second); ev != nil {
53		t.Fatalf("second touchpad moved: %v", ev)
54	}
55	if ev := g.end(other, ports.GestureSwipe, false, time.Second); ev != nil {
56		t.Fatalf("second touchpad ended the swipe: %v", ev)
57	}
58	if ev := g.swipeUpdate(pad, 1, 1, 1, 1, 2*time.Second); ev == nil {
59		t.Fatal("first touchpad lost its swipe")
60	}
61	g.end(pad, ports.GestureSwipe, false, 3*time.Second)
62	if ev := g.begin(other, ports.GestureSwipe, 3, 4*time.Second); ev == nil {
63		t.Fatal("second touchpad cannot swipe after the first ended")
64	}
65}
66
67func TestGesturesStreamSwipe(t *testing.T) {
68	var g gestures
69	const pad, other = 1, 2
70	if ev := g.begin(pad, ports.GestureSwipe, 0, time.Second); ev != nil {
71		t.Fatalf("no fingers: %v", ev)
72	}
73	if ev := g.swipeUpdate(pad, 1, 1, 1, 1, time.Second); ev != nil {
74		t.Fatalf("update without a swipe: %v", ev)
75	}
76	if ev := g.begin(pad, ports.GestureSwipe, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
77		t.Fatalf("begin: %v", ev)
78	}
79	if ev := g.swipeUpdate(pad, -3, 2, -6, 4, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, AccelDX: -6, AccelDY: 4, Time: 2 * time.Second}) {
80		t.Fatalf("update: %v", ev)
81	}
82	if ev := g.end(other, ports.GestureSwipe, false, 0); ev != nil {
83		t.Fatalf("end on another device: %v", ev)
84	}
85	if ev := g.end(pad, ports.GestureSwipe, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
86		t.Fatalf("end: %v", ev)
87	}
88	if ev := g.end(pad, ports.GestureSwipe, false, 0); ev != nil {
89		t.Fatalf("second end: %v", ev)
90	}
91}
92
93// Swipes of every finger count stream; core picks which ones it keeps.
94func TestGesturesStreamAnyFingerCount(t *testing.T) {
95	for _, fingers := range []int{2, 3, 4, 5} {
96		var g gestures
97		if ev := g.begin(1, ports.GestureSwipe, fingers, time.Second); ev != (ports.SwipeBegin{Fingers: fingers, Time: time.Second}) {
98			t.Fatalf("%d fingers: %v", fingers, ev)
99		}
100		if ev := g.begin(1, ports.GestureSwipe, 5, time.Second); ev != nil {
101			t.Fatalf("a second swipe began during a swipe: %v", ev)
102		}
103	}
104}
105
106func TestGesturesStreamPinch(t *testing.T) {
107	var g gestures
108	const pad = 1
109	if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != (ports.PinchBegin{Fingers: 2, Time: time.Second}) {
110		t.Fatalf("begin: %v", ev)
111	}
112	if ev := g.swipeUpdate(pad, 1, 1, 1, 1, time.Second); ev != nil {
113		t.Fatalf("swipe update during a pinch: %v", ev)
114	}
115	if ev := g.end(pad, ports.GestureHold, false, time.Second); ev != nil {
116		t.Fatalf("hold end during a pinch: %v", ev)
117	}
118	want := ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.5, Rotation: -10, Time: 2 * time.Second}
119	if ev := g.pinchUpdate(pad, 1, -2, 1.5, -10, 2*time.Second); ev != want {
120		t.Fatalf("update: %v", ev)
121	}
122	if ev := g.end(pad, ports.GesturePinch, false, 3*time.Second); ev != (ports.PinchEnd{Time: 3 * time.Second}) {
123		t.Fatalf("end: %v", ev)
124	}
125	if ev := g.pinchUpdate(pad, 1, 1, 1, 0, 4*time.Second); ev != nil {
126		t.Fatalf("update after the end: %v", ev)
127	}
128}
129
130func TestGesturesStreamHold(t *testing.T) {
131	var g gestures
132	const pad = 1
133	if ev := g.begin(pad, ports.GestureHold, 3, time.Second); ev != (ports.HoldBegin{Fingers: 3, Time: time.Second}) {
134		t.Fatalf("begin: %v", ev)
135	}
136	if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != nil {
137		t.Fatalf("pinch began during a hold: %v", ev)
138	}
139	if ev := g.end(pad, ports.GestureHold, true, 2*time.Second); ev != (ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}) {
140		t.Fatalf("end: %v", ev)
141	}
142}
143
144// A device that goes during a gesture ends it cancelled, whatever its kind.
145func TestGesturesAbortOnRemoval(t *testing.T) {
146	for kind, want := range map[ports.GestureKind]ports.InputEvent{
147		ports.GestureSwipe: ports.SwipeEnd{Cancelled: true},
148		ports.GesturePinch: ports.PinchEnd{Cancelled: true},
149		ports.GestureHold:  ports.HoldEnd{Cancelled: true},
150	} {
151		var g gestures
152		g.begin(1, kind, 3, time.Second)
153		if ev := g.abort(2); ev != nil {
154			t.Fatalf("kind %d: another device aborted: %v", kind, ev)
155		}
156		if ev := g.abort(1); ev != want {
157			t.Fatalf("kind %d: abort %v, want %v", kind, ev, want)
158		}
159		if ev := g.abort(1); ev != nil {
160			t.Fatalf("kind %d: second abort %v", kind, ev)
161		}
162		if ev := g.begin(1, kind, 3, time.Second); ev == nil {
163			t.Fatalf("kind %d: no gesture after the abort", kind)
164		}
165	}
166}
167
168func TestAccelProfile(t *testing.T) {
169	if accelProfile(ports.AccelFlat) != 1 || accelProfile(ports.AccelAdaptive) != 2 {
170		t.Fatal(accelProfile(ports.AccelFlat), accelProfile(ports.AccelAdaptive))
171	}
172}
173
174func TestTimestamps(t *testing.T) {
175	if usec(1500) != 1500*time.Microsecond {
176		t.Fatal(usec(1500))
177	}
178}
179
180func TestPointerAcrossOutputs(t *testing.T) {
181	layout := ports.Layout{
182		{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 50, Scale: 1},
183		{Info: ports.OutputInfo{Name: "B"}, X: 100, Width: 200, Height: 100, Scale: 2},
184	}
185	p := newPointer(layout)
186	if p.x != 50 || p.y != 25 {
187		t.Fatal(p.x, p.y)
188	}
189	// Off the bottom of A: clamped to A.
190	if m := p.move(-80, 100); m.X != 0 || m.Y != 49 {
191		t.Fatalf("got %v %v", m.X, m.Y)
192	}
193	// Into B, where deltas count in physical pixels (scale 2).
194	p.set(150, 20)
195	if m := p.move(20, 10); m.X != 160 || m.Y != 25 || m.DX != 10 || m.DY != 5 {
196		t.Fatalf("got %+v", m)
197	}
198	var out string
199	var px, py float64
200	p.moved(func(o string, x, y float64, _ bool) { out, px, py = o, x, y }, true)
201	if out != "B" || px != 120 || py != 50 {
202		t.Fatal(out, px, py)
203	}
204	// B unplugged: the pointer lands on A.
205	p.setLayout(layout[:1])
206	if p.x != 99 || p.y != 25 {
207		t.Fatal(p.x, p.y)
208	}
209}
210
211func TestPointerStartsOnPrimary(t *testing.T) {
212	p := newPointer(ports.Layout{
213		{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 50, Scale: 1},
214		{Info: ports.OutputInfo{Name: "B"}, X: 100, Width: 200, Height: 100, Scale: 1, Primary: true},
215	})
216	if p.x != 200 || p.y != 50 {
217		t.Fatal(p.x, p.y)
218	}
219}
220
221func TestPointerConstraints(t *testing.T) {
222	p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}})
223	p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 50, Y: 50})
224	if m := p.move(10, -5); m.X != 50 || m.Y != 50 || m.DX != 10 || m.DY != -5 {
225		t.Fatalf("locked: %+v", m)
226	}
227	// Confining moves the pointer inside, then keeps it there.
228	p.constrain(ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 60, Y: 10, W: 20, H: 20}, X: 50, Y: 50})
229	if p.x != 60 || p.y != 29 {
230		t.Fatal(p.x, p.y)
231	}
232	if m := p.move(100, 0); m.X != 79 || m.DX != 100 {
233		t.Fatalf("confined: %+v", m)
234	}
235	p.constrain(ports.PointerConstraint{})
236	if m := p.move(10, 0); m.X != 89 {
237		t.Fatalf("free: %+v", m)
238	}
239	// A lock takes core's cursor position, and so does its release.
240	p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 20, Y: 30})
241	if p.x != 20 || p.y != 30 {
242		t.Fatal(p.x, p.y)
243	}
244	p.move(40, 40)
245	p.constrain(ports.PointerConstraint{X: 21, Y: 31})
246	if p.x != 21 || p.y != 31 {
247		t.Fatal(p.x, p.y)
248	}
249	// A warp moves a free pointer to core's cursor.
250	p.constrain(ports.PointerConstraint{X: 70, Y: 40, Warp: true})
251	if p.x != 70 || p.y != 40 {
252		t.Fatal(p.x, p.y)
253	}
254}
255
256func TestProtectedKeyboardPolicy(t *testing.T) {
257	ca := ports.ModCtrl | ports.ModAlt
258	for _, key := range []ports.KeyEvent{
259		{Keysym: "credential-secret", Base: "raw-secret", Keycode: 424242, Pressed: true},
260		{Keysym: "credential-secret", Keycode: 424242},
261		{Keysym: "BackSpace", Mods: ca, Pressed: true},
262		{Keysym: "raw-secret", Keycode: 14, Mods: ca, Pressed: true},
263	} {
264		t.Run(key.Keysym, func(t *testing.T) {
265			var buf bytes.Buffer
266			security := portsmocks.NewMockSessionSecurity(t)
267			state := ports.SecurityState{Generation: 7, Protected: true}
268			security.EXPECT().Snapshot().Return(state).Once()
269			opts := Options{Security: security, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
270			produced := securitySnapshot(opts.Security)
271			ev, err := translateKey(key, opts, produced)
272			if err != nil || errors.Is(err, ErrEmergencyQuit) || ev != key {
273				t.Fatalf("protected key lost/quit: event=%#v err=%v", ev, err)
274			}
275			if got := secureInput(ev, opts.Security, produced); got != (ports.SecurityInput{State: state, Event: key}) {
276				t.Fatalf("wrong production epoch: %#v", got)
277			}
278			if buf.Len() != 0 {
279				t.Fatalf("protected key logged identifiers: %s", &buf)
280			}
281		})
282	}
283}
284
285func TestKeyboardPolicyUnlockedAndVT(t *testing.T) {
286	ca := ports.ModCtrl | ports.ModAlt
287	for _, protected := range []bool{false, true} {
288		t.Run(map[bool]string{false: "unlocked", true: "protected"}[protected], func(t *testing.T) {
289			var buf bytes.Buffer
290			security := portsmocks.NewMockSessionSecurity(t)
291			state := ports.SecurityState{Generation: 9, Protected: protected}
292			security.EXPECT().Snapshot().Return(state).Once()
293			seat := portsmocks.NewMockSeat(t)
294			seat.EXPECT().SwitchVT(2).Once()
295			opts := Options{Security: security, Seat: seat, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
296			ev, err := translateKey(ports.KeyEvent{Keysym: "XF86Switch_VT_2", Keycode: 60, Mods: ca, Pressed: true}, opts, securitySnapshot(opts.Security))
297			if ev != nil || err != nil {
298				t.Fatalf("VT not consumed: %#v %v", ev, err)
299			}
300			if protected && buf.Len() != 0 {
301				t.Fatalf("protected VT key logged: %s", &buf)
302			}
303			if !protected && !strings.Contains(buf.String(), "XF86Switch_VT_2") {
304				t.Fatalf("unlocked debug logging changed: %s", &buf)
305			}
306		})
307	}
308	var buf bytes.Buffer
309	opts := Options{LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
310	key := ports.KeyEvent{Keysym: "q", Keycode: 16, Pressed: true}
311	state := securitySnapshot(nil)
312	ev, err := translateKey(key, opts, state)
313	if err != nil || secureInput(ev, nil, state) != key || !strings.Contains(buf.String(), `"keysym":"q"`) {
314		t.Fatalf("nil gate changed unlocked behavior: %#v %v %s", ev, err, &buf)
315	}
316	ev, err = translateKey(ports.KeyEvent{Keysym: "BackSpace", Mods: ca, Pressed: true}, opts, state)
317	if ev != nil || !errors.Is(err, ErrEmergencyQuit) || !strings.Contains(buf.String(), "emergency-key") {
318		t.Fatalf("unlocked emergency quit changed: %#v %v %s", ev, err, &buf)
319	}
320}
321
322func TestSecurityInputAllKindsProductionEpoch(t *testing.T) {
323	security := portsmocks.NewMockSessionSecurity(t)
324	old := ports.SecurityState{Generation: 11, Protected: true}
325	newState := ports.SecurityState{Generation: 12}
326	events := []ports.InputEvent{
327		ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},
328		ports.PointerButton{Button: 272}, ports.PointerAxis{},
329		ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},
330		ports.PinchBegin{}, ports.PinchUpdate{}, ports.PinchEnd{}, ports.HoldBegin{}, ports.HoldEnd{},
331	}
332	security.EXPECT().Snapshot().Return(old).Times(len(events))
333	security.EXPECT().Snapshot().Return(newState).Once()
334	f := newForwarder(zerowrap.Default())
335	for _, ev := range events {
336		f.push(secureInput(ev, security, securitySnapshot(security)))
337	}
338	if securitySnapshot(security) != newState {
339		t.Fatal("did not transition")
340	}
341	for _, want := range events {
342		got, ok := f.pop()
343		if !ok || got != (ports.SecurityInput{State: old, Event: want}) {
344			t.Fatalf("queued event changed epoch: %#v", got)
345		}
346	}
347	if secureInput(nil, security, old) != nil {
348		t.Fatal("nil translated event was wrapped")
349	}
350}
351
352func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) {
353	km, err := xkb.New(xkb.RMLVO{Layout: "us"})
354	if err != nil {
355		t.Fatal(err)
356	}
357	defer km.Close()
358	initial := ports.SecurityState{}
359	locked := ports.SecurityState{Generation: 1, Protected: true}
360	unlocked := ports.SecurityState{Generation: 2}
361	security := portsmocks.NewMockSessionSecurity(t)
362	security.EXPECT().Snapshot().Return(initial).Times(3)
363	security.EXPECT().Snapshot().Return(locked).Times(4)
364	security.EXPECT().Snapshot().Return(unlocked).Times(5)
365	var buf bytes.Buffer
366	opts := Options{Keymap: km, Security: security, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
367	produce := func(code uint32, down bool) ports.InputEvent {
368		t.Helper()
369		state := securitySnapshot(opts.Security)
370		ev, err := translateKeyboard(code, down, 123*time.Millisecond, opts, state)
371		if err != nil {
372			t.Fatal(err)
373		}
374		return secureInput(ev, opts.Security, state)
375	}
376	for _, code := range []uint32{125, 29, 56} {
377		produce(code, true)
378	}
379	buf.Reset()
380	for _, code := range []uint32{125, 29, 56} {
381		if ev := produce(code, true); ev != nil {
382			t.Fatalf("inherited repeat delivered: %#v", ev)
383		}
384	}
385	// Ctrl+Alt held before lock must not activate emergency quit, even on a
386	// fresh Backspace. Super must not contaminate the credential either.
387	if ev := produce(14, true).(ports.SecurityInput).Event.(ports.KeyEvent); ev.Mods != 0 || ev.State != (ports.ModState{}) {
388		t.Fatalf("protected fresh key inherited native state: %+v", ev)
389	}
390	if buf.Len() != 0 {
391		t.Fatalf("protected keyboard logged: %s", &buf)
392	}
393	if ev := produce(16, true).(ports.SecurityInput); ev.State != unlocked || ev.Event.(ports.KeyEvent).Mods != 0 || ev.Event.(ports.KeyEvent).State != (ports.ModState{}) {
394		t.Fatalf("unlocked q inherited state: %#v", ev)
395	}
396	buf.Reset()
397	for _, code := range []uint32{14, 56, 29, 125} {
398		if ev := produce(code, false); ev != nil {
399			t.Fatalf("inherited release delivered: %#v", ev)
400		}
401	}
402	if buf.Len() != 0 {
403		t.Fatalf("quarantined release logged: %s", &buf)
404	}
405}
406
407// The cursor is placed in target pixels: after the output transform.
408func TestPointerMovedTransform(t *testing.T) {
409	p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}})
410	p.set(10, 20)
411	var out string
412	var px, py float64
413	p.moved(func(o string, x, y float64, _ bool) { out, px, py = o, x, y }, true)
414	if out != "A" || px != 20 || py != 90 {
415		t.Fatal(out, px, py)
416	}
417}

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