v0.2.0 13c6a90

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 TestSwipesIgnoreSecondTouchpad(t *testing.T) {
46	var s swipes
47	const pad, other = 1, 2
48	s.begin(pad, 3, time.Second)
49	if ev := s.begin(other, 3, time.Second); ev != nil {
50		t.Fatalf("second touchpad began: %v", ev)
51	}
52	if ev := s.update(other, 1, 1, time.Second); ev != nil {
53		t.Fatalf("second touchpad moved: %v", ev)
54	}
55	if ev := s.end(other, false, time.Second); ev != nil {
56		t.Fatalf("second touchpad ended the swipe: %v", ev)
57	}
58	if ev := s.update(pad, 1, 1, 2*time.Second); ev == nil {
59		t.Fatal("first touchpad lost its swipe")
60	}
61	s.end(pad, false, 3*time.Second)
62	if ev := s.begin(other, 3, 4*time.Second); ev == nil {
63		t.Fatal("second touchpad cannot swipe after the first ended")
64	}
65}
66
67func TestSwipesStreamThreeFingers(t *testing.T) {
68	var s swipes
69	const pad, other = 1, 2
70	if ev := s.begin(pad, 2, time.Second); ev != nil {
71		t.Fatalf("two fingers: %v", ev)
72	}
73	if ev := s.update(pad, 1, 1, time.Second); ev != nil {
74		t.Fatalf("update without a swipe: %v", ev)
75	}
76	if ev := s.begin(pad, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
77		t.Fatalf("begin: %v", ev)
78	}
79	if ev := s.update(pad, -3, 2, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, Time: 2 * time.Second}) {
80		t.Fatalf("update: %v", ev)
81	}
82	if ev := s.end(other, false, 0); ev != nil {
83		t.Fatalf("end on another device: %v", ev)
84	}
85	if ev := s.end(pad, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
86		t.Fatalf("end: %v", ev)
87	}
88	if ev := s.end(pad, false, 0); ev != nil {
89		t.Fatalf("second end: %v", ev)
90	}
91}
92
93func TestSwipesStreamFourFingers(t *testing.T) {
94	var s swipes
95	if ev := s.begin(1, 4, time.Second); ev != (ports.SwipeBegin{Fingers: 4, Time: time.Second}) {
96		t.Fatalf("four fingers: %v", ev)
97	}
98	if ev := s.begin(1, 5, time.Second); ev != nil {
99		t.Fatalf("five fingers during a swipe: %v", ev)
100	}
101}
102
103func TestAccelProfile(t *testing.T) {
104	if accelProfile(ports.AccelFlat) != 1 || accelProfile(ports.AccelAdaptive) != 2 {
105		t.Fatal(accelProfile(ports.AccelFlat), accelProfile(ports.AccelAdaptive))
106	}
107}
108
109func TestTimestamps(t *testing.T) {
110	if usec(1500) != 1500*time.Microsecond || msec(1_234_567) != 1234 {
111		t.Fatal(usec(1500), msec(1_234_567))
112	}
113}
114
115func TestPointerAcrossOutputs(t *testing.T) {
116	layout := ports.Layout{
117		{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 50, Scale: 1},
118		{Info: ports.OutputInfo{Name: "B"}, X: 100, Width: 200, Height: 100, Scale: 2},
119	}
120	p := newPointer(layout)
121	if p.x != 50 || p.y != 25 {
122		t.Fatal(p.x, p.y)
123	}
124	// Off the bottom of A: clamped to A.
125	if m := p.move(-80, 100); m.X != 0 || m.Y != 49 {
126		t.Fatalf("got %v %v", m.X, m.Y)
127	}
128	// Into B, where deltas count in physical pixels (scale 2).
129	p.set(150, 20)
130	if m := p.move(20, 10); m.X != 160 || m.Y != 25 || m.DX != 10 || m.DY != 5 {
131		t.Fatalf("got %+v", m)
132	}
133	var out string
134	var px, py float64
135	p.moved(func(o string, x, y float64) { out, px, py = o, x, y })
136	if out != "B" || px != 120 || py != 50 {
137		t.Fatal(out, px, py)
138	}
139	// B unplugged: the pointer lands on A.
140	p.setLayout(layout[:1])
141	if p.x != 99 || p.y != 25 {
142		t.Fatal(p.x, p.y)
143	}
144}
145
146func TestPointerStartsOnPrimary(t *testing.T) {
147	p := newPointer(ports.Layout{
148		{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 50, Scale: 1},
149		{Info: ports.OutputInfo{Name: "B"}, X: 100, Width: 200, Height: 100, Scale: 1, Primary: true},
150	})
151	if p.x != 200 || p.y != 50 {
152		t.Fatal(p.x, p.y)
153	}
154}
155
156func TestPointerConstraints(t *testing.T) {
157	p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}})
158	p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 50, Y: 50})
159	if m := p.move(10, -5); m.X != 50 || m.Y != 50 || m.DX != 10 || m.DY != -5 {
160		t.Fatalf("locked: %+v", m)
161	}
162	// Confining moves the pointer inside, then keeps it there.
163	p.constrain(ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 60, Y: 10, W: 20, H: 20}, X: 50, Y: 50})
164	if p.x != 60 || p.y != 29 {
165		t.Fatal(p.x, p.y)
166	}
167	if m := p.move(100, 0); m.X != 79 || m.DX != 100 {
168		t.Fatalf("confined: %+v", m)
169	}
170	p.constrain(ports.PointerConstraint{})
171	if m := p.move(10, 0); m.X != 89 {
172		t.Fatalf("free: %+v", m)
173	}
174	// A lock takes core's cursor position, and so does its release.
175	p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 20, Y: 30})
176	if p.x != 20 || p.y != 30 {
177		t.Fatal(p.x, p.y)
178	}
179	p.move(40, 40)
180	p.constrain(ports.PointerConstraint{X: 21, Y: 31})
181	if p.x != 21 || p.y != 31 {
182		t.Fatal(p.x, p.y)
183	}
184	// A warp moves a free pointer to core's cursor.
185	p.constrain(ports.PointerConstraint{X: 70, Y: 40, Warp: true})
186	if p.x != 70 || p.y != 40 {
187		t.Fatal(p.x, p.y)
188	}
189}
190
191func TestProtectedKeyboardPolicy(t *testing.T) {
192	ca := ports.ModCtrl | ports.ModAlt
193	for _, key := range []ports.KeyEvent{
194		{Keysym: "credential-secret", Base: "raw-secret", Keycode: 424242, Pressed: true},
195		{Keysym: "credential-secret", Keycode: 424242},
196		{Keysym: "BackSpace", Mods: ca, Pressed: true},
197		{Keysym: "raw-secret", Keycode: 14, Mods: ca, Pressed: true},
198	} {
199		t.Run(key.Keysym, func(t *testing.T) {
200			var buf bytes.Buffer
201			security := portsmocks.NewMockSessionSecurity(t)
202			state := ports.SecurityState{Generation: 7, Protected: true}
203			security.EXPECT().Snapshot().Return(state).Once()
204			opts := Options{Security: security, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
205			produced := securitySnapshot(opts.Security)
206			ev, err := translateKey(key, opts, produced)
207			if err != nil || errors.Is(err, ErrEmergencyQuit) || ev != key {
208				t.Fatalf("protected key lost/quit: event=%#v err=%v", ev, err)
209			}
210			if got := secureInput(ev, opts.Security, produced); got != (ports.SecurityInput{State: state, Event: key}) {
211				t.Fatalf("wrong production epoch: %#v", got)
212			}
213			if buf.Len() != 0 {
214				t.Fatalf("protected key logged identifiers: %s", &buf)
215			}
216		})
217	}
218}
219
220func TestKeyboardPolicyUnlockedAndVT(t *testing.T) {
221	ca := ports.ModCtrl | ports.ModAlt
222	for _, protected := range []bool{false, true} {
223		t.Run(map[bool]string{false: "unlocked", true: "protected"}[protected], func(t *testing.T) {
224			var buf bytes.Buffer
225			security := portsmocks.NewMockSessionSecurity(t)
226			state := ports.SecurityState{Generation: 9, Protected: protected}
227			security.EXPECT().Snapshot().Return(state).Once()
228			seat := portsmocks.NewMockSeat(t)
229			seat.EXPECT().SwitchVT(2).Once()
230			opts := Options{Security: security, Seat: seat, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
231			ev, err := translateKey(ports.KeyEvent{Keysym: "XF86Switch_VT_2", Keycode: 60, Mods: ca, Pressed: true}, opts, securitySnapshot(opts.Security))
232			if ev != nil || err != nil {
233				t.Fatalf("VT not consumed: %#v %v", ev, err)
234			}
235			if protected && buf.Len() != 0 {
236				t.Fatalf("protected VT key logged: %s", &buf)
237			}
238			if !protected && !strings.Contains(buf.String(), "XF86Switch_VT_2") {
239				t.Fatalf("unlocked debug logging changed: %s", &buf)
240			}
241		})
242	}
243	var buf bytes.Buffer
244	opts := Options{LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
245	key := ports.KeyEvent{Keysym: "q", Keycode: 16, Pressed: true}
246	state := securitySnapshot(nil)
247	ev, err := translateKey(key, opts, state)
248	if err != nil || secureInput(ev, nil, state) != key || !strings.Contains(buf.String(), `"keysym":"q"`) {
249		t.Fatalf("nil gate changed unlocked behavior: %#v %v %s", ev, err, &buf)
250	}
251	ev, err = translateKey(ports.KeyEvent{Keysym: "BackSpace", Mods: ca, Pressed: true}, opts, state)
252	if ev != nil || !errors.Is(err, ErrEmergencyQuit) || !strings.Contains(buf.String(), "emergency-key") {
253		t.Fatalf("unlocked emergency quit changed: %#v %v %s", ev, err, &buf)
254	}
255}
256
257func TestSecurityInputAllKindsProductionEpoch(t *testing.T) {
258	security := portsmocks.NewMockSessionSecurity(t)
259	old := ports.SecurityState{Generation: 11, Protected: true}
260	newState := ports.SecurityState{Generation: 12}
261	events := []ports.InputEvent{
262		ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},
263		ports.PointerButton{Button: 272}, ports.PointerAxis{},
264		ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},
265	}
266	security.EXPECT().Snapshot().Return(old).Times(len(events))
267	security.EXPECT().Snapshot().Return(newState).Once()
268	f := newForwarder(zerowrap.Default())
269	for _, ev := range events {
270		f.push(secureInput(ev, security, securitySnapshot(security)))
271	}
272	if securitySnapshot(security) != newState {
273		t.Fatal("did not transition")
274	}
275	for _, want := range events {
276		got, ok := f.pop()
277		if !ok || got != (ports.SecurityInput{State: old, Event: want}) {
278			t.Fatalf("queued event changed epoch: %#v", got)
279		}
280	}
281	if secureInput(nil, security, old) != nil {
282		t.Fatal("nil translated event was wrapped")
283	}
284}
285
286func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) {
287	km, err := xkb.New(xkb.RMLVO{Layout: "us"})
288	if err != nil {
289		t.Fatal(err)
290	}
291	defer km.Close()
292	initial := ports.SecurityState{}
293	locked := ports.SecurityState{Generation: 1, Protected: true}
294	unlocked := ports.SecurityState{Generation: 2}
295	security := portsmocks.NewMockSessionSecurity(t)
296	security.EXPECT().Snapshot().Return(initial).Times(3)
297	security.EXPECT().Snapshot().Return(locked).Times(4)
298	security.EXPECT().Snapshot().Return(unlocked).Times(5)
299	var buf bytes.Buffer
300	opts := Options{Keymap: km, Security: security, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
301	produce := func(code uint32, down bool) ports.InputEvent {
302		t.Helper()
303		state := securitySnapshot(opts.Security)
304		ev, err := translateKeyboard(code, down, 123, opts, state)
305		if err != nil {
306			t.Fatal(err)
307		}
308		return secureInput(ev, opts.Security, state)
309	}
310	for _, code := range []uint32{125, 29, 56} {
311		produce(code, true)
312	}
313	buf.Reset()
314	for _, code := range []uint32{125, 29, 56} {
315		if ev := produce(code, true); ev != nil {
316			t.Fatalf("inherited repeat delivered: %#v", ev)
317		}
318	}
319	// Ctrl+Alt held before lock must not activate emergency quit, even on a
320	// fresh Backspace. Super must not contaminate the credential either.
321	if ev := produce(14, true).(ports.SecurityInput).Event.(ports.KeyEvent); ev.Mods != 0 || ev.State != (ports.ModState{}) {
322		t.Fatalf("protected fresh key inherited native state: %+v", ev)
323	}
324	if buf.Len() != 0 {
325		t.Fatalf("protected keyboard logged: %s", &buf)
326	}
327	if ev := produce(16, true).(ports.SecurityInput); ev.State != unlocked || ev.Event.(ports.KeyEvent).Mods != 0 || ev.Event.(ports.KeyEvent).State != (ports.ModState{}) {
328		t.Fatalf("unlocked q inherited state: %#v", ev)
329	}
330	buf.Reset()
331	for _, code := range []uint32{14, 56, 29, 125} {
332		if ev := produce(code, false); ev != nil {
333			t.Fatalf("inherited release delivered: %#v", ev)
334		}
335	}
336	if buf.Len() != 0 {
337		t.Fatalf("quarantined release logged: %s", &buf)
338	}
339}

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