v0.10.0 d5d679d

input_test.go RawHistoryFolder

1package headlessinput
2
3import (
4	"context"
5	"strings"
6	"testing"
7	"testing/synctest"
8	"time"
9
10	"github.com/bnema/neferwl/internal/adapters/logging"
11	"github.com/bnema/neferwl/internal/adapters/xkb"
12	"github.com/bnema/neferwl/internal/ports"
13)
14
15func TestScript(t *testing.T) {
16	for _, tt := range []struct {
17		line  string
18		names []string
19		codes []uint32
20	}{
21		{"type Hi!", []string{"Shift_L", "H", "H", "Shift_L", "i", "i", "Shift_L", "exclam", "exclam", "Shift_L"}, nil},
22		{"key Shift+A", []string{"Shift_L", "A", "A", "Shift_L"}, []uint32{42, 30, 30, 42}},
23		{"key Super+Return", []string{"Super_L", "Return", "Return", "Super_L"}, []uint32{125, 28, 28, 125}},
24		{"hold Super+h 20ms", []string{"Super_L", "h", "h", "Super_L"}, []uint32{125, 35, 35, 125}},
25		{"hold h", nil, nil},
26	} {
27		t.Run(tt.line, func(t *testing.T) {
28			km, err := xkb.New(xkb.RMLVO{Layout: "us"})
29			if err != nil {
30				if strings.Contains(err.Error(), "libxkbcommon.so.0") {
31					t.Skip(err)
32				}
33				t.Fatal(err)
34			}
35			script := make(chan string, 1)
36			script <- tt.line
37			close(script)
38			input := make(chan ports.InputEvent, 32)
39			if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil {
40				t.Fatal(err)
41			}
42			if len(input) != len(tt.names) {
43				t.Fatalf("events=%d want %d", len(input), len(tt.names))
44			}
45			for i, name := range tt.names {
46				ev := (<-input).(ports.KeyEvent)
47				if ev.Keysym != name {
48					t.Errorf("event %d: %s want %s", i, ev.Keysym, name)
49				}
50				if tt.codes != nil && ev.Keycode != tt.codes[i] {
51					t.Errorf("code %d: %d", i, ev.Keycode)
52				}
53				if tt.line == "key Super+Return" && i == 1 && ev.Mods&ports.ModSuper == 0 {
54					t.Error("Return missing Super")
55				}
56				if tt.line == "key Super+Return" && i == 3 && ev.Mods&ports.ModSuper != 0 {
57					t.Error("Super not released")
58				}
59			}
60		})
61	}
62}
63
64// hold keeps the key and its modifiers down for the given duration, then
65// releases the key before the modifier.
66func TestHoldKeepsKeyDown(t *testing.T) {
67	synctest.Test(t, func(t *testing.T) {
68		km, err := xkb.New(xkb.RMLVO{Layout: "us"})
69		if err != nil {
70			t.Skip(err)
71		}
72		script := make(chan string, 1)
73		script <- "hold Super+h 1s"
74		close(script)
75		input := make(chan ports.InputEvent, 8)
76		done := make(chan error, 1)
77		go func() {
78			done <- Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input"))
79		}()
80		time.Sleep(900 * time.Millisecond)
81		synctest.Wait()
82		if len(input) != 2 {
83			t.Fatalf("before the hold ends: %d events, want the 2 presses", len(input))
84		}
85		if err := <-done; err != nil {
86			t.Fatal(err)
87		}
88		for i, want := range []struct {
89			name    string
90			pressed bool
91			super   bool
92		}{
93			{"Super_L", true, true},
94			{"h", true, true},
95			{"h", false, true},
96			{"Super_L", false, false},
97		} {
98			ev := (<-input).(ports.KeyEvent)
99			if ev.Keysym != want.name || ev.Pressed != want.pressed || (ev.Mods&ports.ModSuper != 0) != want.super {
100				t.Errorf("event %d: %s pressed=%v mods=%v, want %s pressed=%v super=%v", i, ev.Keysym, ev.Pressed, ev.Mods, want.name, want.pressed, want.super)
101			}
102		}
103	})
104}
105
106func TestPointerScript(t *testing.T) {
107	km, err := xkb.New(xkb.RMLVO{Layout: "us"})
108	if err != nil {
109		t.Skip(err)
110	}
111	script := make(chan string, 8)
112	for _, line := range []string{"move 12.5 45", "click", "down right", "up right", "swipe sideways", "swipe left", "swipe4 up"} {
113		script <- line
114	}
115	close(script)
116	input := make(chan ports.InputEvent, 32)
117	if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil {
118		t.Fatal(err)
119	}
120	if len(input) != 5+2*(swipeSteps+2) {
121		t.Fatalf("events: %d", len(input))
122	}
123	m := (<-input).(ports.PointerMotion)
124	if m.X != 12.5 || m.Y != 45 {
125		t.Fatal(m)
126	}
127	for _, want := range []ports.PointerButton{{Button: 0x110, Pressed: true}, {Button: 0x110}, {Button: 0x111, Pressed: true}, {Button: 0x111}} {
128		got := (<-input).(ports.PointerButton)
129		if got.Button != want.Button || got.Pressed != want.Pressed {
130			t.Fatalf("got %+v want %+v", got, want)
131		}
132	}
133	begin := (<-input).(ports.SwipeBegin)
134	if begin.Fingers != 3 {
135		t.Fatal(begin)
136	}
137	last := begin.Time
138	for range swipeSteps {
139		u := (<-input).(ports.SwipeUpdate)
140		if u.DX >= 0 || u.DY != 0 || u.Time <= last {
141			t.Fatalf("update %+v after %v", u, last)
142		}
143		last = u.Time
144	}
145	if e := (<-input).(ports.SwipeEnd); e.Cancelled || e.Time != last {
146		t.Fatal(e)
147	}
148	if b := (<-input).(ports.SwipeBegin); b.Fingers != 4 {
149		t.Fatal(b)
150	}
151	if u := (<-input).(ports.SwipeUpdate); u.DY >= 0 || u.DX != 0 {
152		t.Fatal(u)
153	}
154}
155
156// The cursor is placed in target pixels: after the output transform.
157func TestMoveCursorTarget(t *testing.T) {
158	km, err := xkb.New(xkb.RMLVO{Layout: "us"})
159	if err != nil {
160		t.Skip(err)
161	}
162	layouts := make(chan ports.Layout)
163	script := make(chan string)
164	input := make(chan ports.InputEvent, 4)
165	type pos struct {
166		out  string
167		x, y float64
168	}
169	moved := make(chan pos, 1)
170	done := make(chan error, 1)
171	go func() {
172		done <- Run(context.Background(), km, nil, script, input, layouts, func(o string, x, y float64) { moved <- pos{o, x, y} }, logging.For(context.Background(), "input"))
173	}()
174	// Unbuffered: the layout is taken before the move is read.
175	layouts <- ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}}
176	script <- "move 10 20"
177	if got, want := <-moved, (pos{"A", 20, 90}); got != want {
178		t.Fatalf("got %+v want %+v", got, want)
179	}
180	close(script)
181	if err := <-done; err != nil {
182		t.Fatal(err)
183	}
184}

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 23

Tags 10

↑↓ moveenter openesc close