v0.1.0 47aaa0f

harness_input_test.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"context"
7	"fmt"
8	"os"
9	"path/filepath"
10	"strings"
11	"testing"
12	"time"
13
14	"github.com/bnema/nefergui/internal/harness"
15	"github.com/bnema/wlturbo/protocol/core"
16	"github.com/bnema/wlturbo/protocol/xdgshell"
17	"github.com/bnema/wlturbo/wl"
18	"golang.org/x/sys/unix"
19)
20
21// The child uses a plain shm toplevel so the test does not depend on a GPU
22// renderer. The parent runs the pinned compositor with its real input script.
23func TestHarnessInput(t *testing.T) {
24	if os.Getenv("NEFERGUI_HARNESS") != "1" {
25		t.Skip("set NEFERGUI_HARNESS=1 for NeferWL input injection")
26	}
27	script := filepath.Join(t.TempDir(), "input.txt")
28	if err := os.WriteFile(script, []byte("sleep 2s\nmove 100 100\nsleep 200ms\nclick left\nkey a\nsleep 500ms\n"), 0600); err != nil {
29		t.Fatal(err)
30	}
31	root := filepath.Join(t.TempDir(), "artifacts")
32	result, code := harness.Run(context.Background(), harness.Options{Size: "400x300", Scale: "1", Layout: "us", Background: "#111111", Out: root, Input: script, Timeout: 25 * time.Second, ReadyTimeout: 8 * time.Second, Client: []string{os.Args[0], "-test.run=^TestHarnessInputClient$", "-test.v"}})
33	if code != harness.Pass {
34		t.Fatalf("harness code=%d status=%s error=%s artifacts=%s", code, result.Status, result.Error, root)
35	}
36	data, err := os.ReadFile(result.Artifacts["client_stdout"])
37	if err != nil {
38		t.Fatal(err)
39	}
40	for _, want := range []string{"INPUT motion", "INPUT press", "INPUT release", "INPUT key a"} {
41		if !strings.Contains(string(data), want) {
42			t.Fatalf("missing %s in client output:\n%s", want, data)
43		}
44	}
45}
46
47func TestHarnessInputClient(t *testing.T) {
48	if os.Getenv("NEFERGUI_DEBUG_DIR") == "" {
49		t.Skip("harness child only")
50	}
51	d, err := wl.Connect("")
52	if err != nil {
53		t.Fatal(err)
54	}
55	defer d.Close()
56	if err = d.Roundtrip(); err != nil {
57		t.Fatal(err)
58	}
59	ctx := d.Context()
60	compositor := core.NewCompositor(ctx)
61	if _, err = d.Registry().BindNegotiated(core.CompositorInterface, 6, compositor); err != nil {
62		t.Fatal(err)
63	}
64	shell := xdgshell.NewXdgWmBase(ctx)
65	if _, err = d.Registry().BindNegotiated(xdgshell.XdgWmBaseInterface, 6, shell); err != nil {
66		t.Fatal(err)
67	}
68	shm := core.NewShm(ctx)
69	if _, err = d.Registry().BindNegotiated(core.ShmInterface, 1, shm); err != nil {
70		t.Fatal(err)
71	}
72	surface, err := compositor.CreateSurface()
73	if err != nil {
74		t.Fatal(err)
75	}
76	xdg, err := shell.GetXdgSurface(surface)
77	if err != nil {
78		t.Fatal(err)
79	}
80	if _, err = xdg.GetToplevel(); err != nil {
81		t.Fatal(err)
82	}
83	configured := false
84	xdg.OnConfigure(func(serial uint32) {
85		if e := xdg.AckConfigure(serial); e != nil {
86			t.Error(e)
87		}
88		configured = true
89	})
90	if err = surface.Commit(); err != nil {
91		t.Fatal(err)
92	}
93	for !configured {
94		if err = d.Dispatch(); err != nil {
95			t.Fatal(err)
96		}
97	}
98	fd, err := unix.MemfdCreate("nefergui-harness-input", unix.MFD_CLOEXEC)
99	if err != nil {
100		t.Fatal(err)
101	}
102	if err = unix.Ftruncate(fd, 400*300*4); err != nil {
103		t.Fatal(err)
104	}
105	pool, err := shm.CreatePool(fd, 400*300*4) // consumes fd on success
106	if err != nil {
107		_ = unix.Close(fd)
108		t.Fatal(err)
109	}
110	buffer, err := pool.CreateBuffer(0, 400, 300, 400*4, 0) // wl_shm XRGB8888
111	if err != nil {
112		t.Fatal(err)
113	}
114	if err = surface.Attach(buffer, 0, 0); err != nil {
115		t.Fatal(err)
116	}
117	if err = surface.DamageBuffer(0, 0, 400, 300); err != nil {
118		t.Fatal(err)
119	}
120	if err = surface.Commit(); err != nil {
121		t.Fatal(err)
122	}
123	w := &Window{Display: d, Surface: surface, events: make(chan Event, 128), readerStop: make(chan struct{})}
124	if err = w.BindSeat(); err != nil {
125		t.Fatal(err)
126	}
127	adapter, err := NewInputAdapter("en_US.UTF-8")
128	if err != nil {
129		t.Fatal(err)
130	}
131	defer adapter.Close()
132	deadline := time.After(6 * time.Second)
133	seen := map[string]bool{}
134	for !(seen["motion"] && seen["press"] && seen["release"] && seen["key a"]) {
135		select {
136		case <-deadline:
137			t.Fatalf("timed out waiting for injected input: %v", seen)
138		default:
139		}
140		if err = d.Dispatch(); err != nil {
141			t.Fatal(err)
142		}
143		for len(w.events) > 0 {
144			ev := <-w.events
145			switch ev.Kind {
146			case SeatCapabilities, SeatRemoved:
147				if err = w.ApplySeat(ev); err != nil {
148					t.Fatal(err)
149				}
150			case InputFrame, InputKeymap, InputFocusIn, InputFocusOut, InputKey, InputModifiers, InputRepeatInfo:
151				inputs, e := adapter.ApplyInput(ev, time.Now())
152				if e != nil {
153					t.Fatal(e)
154				}
155				for _, in := range inputs {
156					kind := in.Kind
157					if kind == "key" && in.Key.Pressed && in.Key.Text == "a" {
158						kind = "key a"
159					}
160					if kind == "motion" || kind == "press" || kind == "release" || kind == "key a" {
161						seen[kind] = true
162						fmt.Println("INPUT", kind)
163					}
164				}
165			}
166		}
167	}
168}

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 12

↑↓ moveenter openesc close