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		{"press Super", []string{"Super_L"}, []uint32{125}},
27		{"release Super", []string{"Super_L"}, []uint32{125}},
28		{"press Shift", []string{"Shift_L"}, []uint32{42}},
29		{"press Ctrl", []string{"Control_L"}, []uint32{29}},
30		{"press Alt", []string{"Alt_L"}, []uint32{56}},
31		{"press", nil, nil},
32		{"release Super extra", nil, nil},
33		{"press unknown", nil, nil},
34	} {
35		t.Run(tt.line, func(t *testing.T) {
36			km, err := xkb.New(xkb.RMLVO{Layout: "us"})
37			if err != nil {
38				if strings.Contains(err.Error(), "libxkbcommon.so.0") {
39					t.Skip(err)
40				}
41				t.Fatal(err)
42			}
43			script := make(chan string, 1)
44			script <- tt.line
45			close(script)
46			input := make(chan ports.InputEvent, 32)
47			if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil {
48				t.Fatal(err)
49			}
50			if len(input) != len(tt.names) {
51				t.Fatalf("events=%d want %d", len(input), len(tt.names))
52			}
53			for i, name := range tt.names {
54				ev := (<-input).(ports.KeyEvent)
55				if ev.Keysym != name {
56					t.Errorf("event %d: %s want %s", i, ev.Keysym, name)
57				}
58				if tt.codes != nil && ev.Keycode != tt.codes[i] {
59					t.Errorf("code %d: %d", i, ev.Keycode)
60				}
61				if tt.line == "key Super+Return" && i == 1 && ev.Mods&ports.ModSuper == 0 {
62					t.Error("Return missing Super")
63				}
64				if tt.line == "key Super+Return" && i == 3 && ev.Mods&ports.ModSuper != 0 {
65					t.Error("Super not released")
66				}
67			}
68		})
69	}
70}
71
72func TestModifierAcrossMouseActions(t *testing.T) {
73	km, err := xkb.New(xkb.RMLVO{Layout: "us"})
74	if err != nil {
75		t.Fatal(err)
76	}
77	script := make(chan string, 8)
78	for _, line := range []string{"press Super", "move 500 100", "down right", "move 700 100", "up right", "release Super"} {
79		script <- line
80	}
81	close(script)
82	input := make(chan ports.InputEvent, 16)
83	if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil {
84		t.Fatal(err)
85	}
86	if len(input) != 6 {
87		t.Fatal(len(input))
88	}
89	press := (<-input).(ports.KeyEvent)
90	if !press.Pressed || press.Mods&ports.ModSuper == 0 {
91		t.Fatal(press)
92	}
93	if _, ok := (<-input).(ports.PointerMotion); !ok {
94		t.Fatal("missing motion")
95	}
96	down := (<-input).(ports.PointerButton)
97	if down.Button != 0x111 || !down.Pressed {
98		t.Fatal(down)
99	}
100	if _, ok := (<-input).(ports.PointerMotion); !ok {
101		t.Fatal("missing drag motion")
102	}
103	up := (<-input).(ports.PointerButton)
104	if up.Button != 0x111 || up.Pressed {
105		t.Fatal(up)
106	}
107	release := (<-input).(ports.KeyEvent)
108	if release.Pressed || release.Mods&ports.ModSuper != 0 {
109		t.Fatal(release)
110	}
111}
112
113// hold keeps the key and its modifiers down for the given duration, then
114// releases the key before the modifier.
115func TestHoldKeepsKeyDown(t *testing.T) {
116	synctest.Test(t, func(t *testing.T) {
117		km, err := xkb.New(xkb.RMLVO{Layout: "us"})
118		if err != nil {
119			t.Skip(err)
120		}
121		script := make(chan string, 1)
122		script <- "hold Super+h 1s"
123		close(script)
124		input := make(chan ports.InputEvent, 8)
125		done := make(chan error, 1)
126		go func() {
127			done <- Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input"))
128		}()
129		time.Sleep(900 * time.Millisecond)
130		synctest.Wait()
131		if len(input) != 2 {
132			t.Fatalf("before the hold ends: %d events, want the 2 presses", len(input))
133		}
134		if err := <-done; err != nil {
135			t.Fatal(err)
136		}
137		for i, want := range []struct {
138			name    string
139			pressed bool
140			super   bool
141		}{
142			{"Super_L", true, true},
143			{"h", true, true},
144			{"h", false, true},
145			{"Super_L", false, false},
146		} {
147			ev := (<-input).(ports.KeyEvent)
148			if ev.Keysym != want.name || ev.Pressed != want.pressed || (ev.Mods&ports.ModSuper != 0) != want.super {
149				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)
150			}
151		}
152	})
153}
154
155func TestPointerScript(t *testing.T) {
156	km, err := xkb.New(xkb.RMLVO{Layout: "us"})
157	if err != nil {
158		t.Skip(err)
159	}
160	script := make(chan string, 8)
161	for _, line := range []string{"move 12.5 45", "click", "down right", "up right", "swipe sideways", "swipe left", "swipe4 up"} {
162		script <- line
163	}
164	close(script)
165	input := make(chan ports.InputEvent, 32)
166	if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil {
167		t.Fatal(err)
168	}
169	if len(input) != 5+2*(swipeSteps+2) {
170		t.Fatalf("events: %d", len(input))
171	}
172	m := (<-input).(ports.PointerMotion)
173	if m.X != 12.5 || m.Y != 45 {
174		t.Fatal(m)
175	}
176	for _, want := range []ports.PointerButton{{Button: 0x110, Pressed: true}, {Button: 0x110}, {Button: 0x111, Pressed: true}, {Button: 0x111}} {
177		got := (<-input).(ports.PointerButton)
178		if got.Button != want.Button || got.Pressed != want.Pressed {
179			t.Fatalf("got %+v want %+v", got, want)
180		}
181	}
182	begin := (<-input).(ports.SwipeBegin)
183	if begin.Fingers != 3 {
184		t.Fatal(begin)
185	}
186	last := begin.Time
187	for range swipeSteps {
188		u := (<-input).(ports.SwipeUpdate)
189		if u.DX >= 0 || u.DY != 0 || u.Time <= last {
190			t.Fatalf("update %+v after %v", u, last)
191		}
192		last = u.Time
193	}
194	if e := (<-input).(ports.SwipeEnd); e.Cancelled || e.Time != last {
195		t.Fatal(e)
196	}
197	if b := (<-input).(ports.SwipeBegin); b.Fingers != 4 {
198		t.Fatal(b)
199	}
200	if u := (<-input).(ports.SwipeUpdate); u.DY >= 0 || u.DX != 0 {
201		t.Fatal(u)
202	}
203}
204
205// The cursor is placed in target pixels: after the output transform.
206func TestMoveCursorTarget(t *testing.T) {
207	km, err := xkb.New(xkb.RMLVO{Layout: "us"})
208	if err != nil {
209		t.Skip(err)
210	}
211	layouts := make(chan ports.Layout)
212	script := make(chan string)
213	input := make(chan ports.InputEvent, 4)
214	type pos struct {
215		out  string
216		x, y float64
217	}
218	moved := make(chan pos, 1)
219	done := make(chan error, 1)
220	go func() {
221		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"))
222	}()
223	// Unbuffered: the layout is taken before the move is read.
224	layouts <- ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}}
225	script <- "move 10 20"
226	if got, want := <-moved, (pos{"A", 20, 90}); got != want {
227		t.Fatalf("got %+v want %+v", got, want)
228	}
229	close(script)
230	if err := <-done; err != nil {
231		t.Fatal(err)
232	}
233}

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