v0.2.0 13c6a90

textinput_test.go RawHistoryFolder

1package app
2
3import (
4	"bytes"
5	"context"
6	"os"
7	"os/exec"
8	"path/filepath"
9	"strings"
10	"sync"
11	"testing"
12	"time"
13
14	"github.com/bnema/neferwl/internal/adapters/config"
15	"github.com/bnema/wlturbo"
16	clientcore "github.com/bnema/wlturbo/protocol/core"
17	clienttext "github.com/bnema/wlturbo/protocol/textinput"
18	clientxdg "github.com/bnema/wlturbo/protocol/xdgshell"
19	"golang.org/x/sys/unix"
20)
21
22// lockedBuffer is an io.Writer read while exec writes to it.
23type lockedBuffer struct {
24	mu  sync.Mutex
25	buf bytes.Buffer
26}
27
28func (b *lockedBuffer) Write(p []byte) (int, error) {
29	b.mu.Lock()
30	defer b.mu.Unlock()
31	return b.buf.Write(p)
32}
33
34func (b *lockedBuffer) String() string {
35	b.mu.Lock()
36	defer b.mu.Unlock()
37	return b.buf.String()
38}
39
40// textClient retains observations from an ordinary generated text-input proxy.
41type textClient struct {
42	*clienttext.TextInputV3
43	entered bool
44	preedit []string
45	commits []string
46}
47
48// NeferWL headless, the examples/testime input method and a text input
49// client: the input method's preedit and commit reach the client.
50func TestHeadlessTextInputIME(t *testing.T) {
51	runtime := t.TempDir()
52	t.Setenv("XDG_RUNTIME_DIR", runtime)
53	t.Setenv("XDG_STATE_HOME", t.TempDir())
54	ime := filepath.Join(t.TempDir(), "testime")
55	build := exec.Command("go", "build", "-o", ime, "../../examples/testime")
56	build.Env = append(os.Environ(), "CGO_ENABLED=0")
57	if out, err := build.CombinedOutput(); err != nil {
58		t.Fatalf("build testime: %v: %s", err, out)
59	}
60
61	ctx, cancel := context.WithCancel(context.Background())
62	done := make(chan error, 1)
63	go func() {
64		done <- Run(ctx, Options{Backend: "headless", NoXwayland: true, NoTerminal: true, Config: config.Defaults()})
65	}()
66	stopped := false // Run's result was already received
67	defer func() {
68		cancel()
69		if stopped {
70			return
71		}
72		select {
73		case err := <-done:
74			if err != nil {
75				t.Errorf("Run: %v", err)
76			}
77		case <-time.After(5 * time.Second):
78			t.Error("Run did not stop")
79		}
80	}()
81	socket := filepath.Join(runtime, "wayland-1")
82	for deadline := time.Now().Add(5 * time.Second); ; time.Sleep(20 * time.Millisecond) {
83		if _, err := os.Stat(socket); err == nil {
84			break
85		}
86		select {
87		case err := <-done:
88			stopped = true
89			if err != nil && strings.Contains(strings.ToLower(err.Error()), "vulkan") {
90				t.Skipf("Vulkan unavailable: %v", err)
91			}
92			t.Fatalf("neferwl exited: %v", err)
93		default:
94		}
95		if time.Now().After(deadline) {
96			t.Fatal("no wayland socket")
97		}
98	}
99
100	// Two commits: the second comes from enabling the text input again.
101	cmd := exec.Command(ime, "-count", "2", "-preedit", "nihon", "-commit", "日本")
102	cmd.Env = append(os.Environ(), "WAYLAND_DISPLAY=wayland-1")
103	imeOut := &lockedBuffer{}
104	cmd.Stderr = imeOut
105	if err := cmd.Start(); err != nil {
106		t.Fatal(err)
107	}
108	imeDone := make(chan error, 1)
109	go func() { imeDone <- cmd.Wait() }()
110	imeExited := false
111	defer func() {
112		if !imeExited {
113			_ = cmd.Process.Kill()
114			<-imeDone
115		}
116	}()
117	for deadline := time.Now().Add(5 * time.Second); !strings.Contains(imeOut.String(), "ready"); time.Sleep(10 * time.Millisecond) {
118		if time.Now().After(deadline) {
119			t.Fatalf("testime not ready: %s", imeOut.String())
120		}
121	}
122
123	c, err := wlturbo.Connect(socket)
124	if err != nil {
125		t.Fatal(err)
126	}
127	defer c.Close()
128	text := mapTextClient(t, c)
129	poll := func(what string, ok func() bool) {
130		t.Helper()
131		for deadline := time.Now().Add(5 * time.Second); !ok(); time.Sleep(10 * time.Millisecond) {
132			if time.Now().After(deadline) {
133				t.Fatalf("%s: preedit %q commits %q, testime: %s", what, text.preedit, text.commits, imeOut.String())
134			}
135			if err := c.Roundtrip(); err != nil {
136				t.Fatal(err)
137			}
138		}
139	}
140	poll("no text input enter", func() bool { return text.entered })
141	for i := 1; i <= 2; i++ {
142		if err := text.Enable(); err != nil {
143			t.Fatal(err)
144		}
145		if err := text.SetContentType(0, 0); err != nil {
146			t.Fatal(err)
147		}
148		if err := text.Commit(); err != nil {
149			t.Fatal(err)
150		}
151		poll("missing commit", func() bool { return len(text.commits) == i })
152	}
153	if len(text.preedit) != 2 || text.preedit[0] != "nihon" || text.commits[0] != "日本" || text.commits[1] != "日本" {
154		t.Fatalf("preedit %q commits %q", text.preedit, text.commits)
155	}
156	select {
157	case err := <-imeDone:
158		imeExited = true
159		if err != nil {
160			t.Fatalf("testime: %v: %s", err, imeOut.String())
161		}
162	case <-time.After(5 * time.Second):
163		t.Fatalf("testime did not exit after two commits: %s", imeOut.String())
164	}
165}
166
167// mapTextClient maps a 1x1 toplevel with a text input on c.
168func mapTextClient(t *testing.T, c *wlturbo.Display) *textClient {
169	t.Helper()
170	check := func(err error) {
171		t.Helper()
172		if err != nil {
173			t.Fatal(err)
174		}
175	}
176	check(c.Roundtrip())
177	comp := clientcore.NewCompositor(c.Context())
178	shm := clientcore.NewShm(c.Context())
179	seat := clientcore.NewSeat(c.Context())
180	tim := clienttext.NewTextInputManagerV3(c.Context())
181	wm := clientxdg.NewXdgWmBase(c.Context())
182	wm.OnPing(func(serial uint32) { check(wm.Pong(serial)) })
183	bindTestClient(t, c, clientcore.CompositorInterface, 1, comp)
184	bindTestClient(t, c, clientcore.ShmInterface, 1, shm)
185	bindTestClient(t, c, clientcore.SeatInterface, 1, seat)
186	bindTestClient(t, c, clienttext.TextInputManagerV3Interface, 1, tim)
187	bindTestClient(t, c, clientxdg.XdgWmBaseInterface, 1, wm)
188	fd, err := unix.MemfdCreate("text-client", 0)
189	check(err)
190	if err := unix.Ftruncate(fd, 4); err != nil {
191		_ = unix.Close(fd)
192		t.Fatal(err)
193	}
194	// CreatePool owns the sent descriptor on success; only close it on error.
195	pool, err := shm.CreatePool(fd, 4)
196	if err != nil {
197		_ = unix.Close(fd)
198		t.Fatal(err)
199	}
200	buffer, err := pool.CreateBuffer(0, 1, 1, 4, clientcore.FORMAT_ARGB8888)
201	check(err)
202	surf, err := comp.CreateSurface()
203	check(err)
204	xdg, err := wm.GetXdgSurface(surf)
205	check(err)
206	var serial uint32
207	xdg.OnConfigure(func(value uint32) { serial = value })
208	_, err = xdg.GetToplevel()
209	check(err)
210	proxy, err := tim.GetTextInput(seat)
211	check(err)
212	text := &textClient{TextInputV3: proxy}
213	proxy.OnEnter(func(uint32) { text.entered = true })
214	proxy.OnPreeditString(func(value string, _, _ int32) { text.preedit = append(text.preedit, value) })
215	proxy.OnCommitString(func(value string) { text.commits = append(text.commits, value) })
216	check(surf.Commit())
217	for deadline := time.Now().Add(5 * time.Second); serial == 0; time.Sleep(10 * time.Millisecond) {
218		if time.Now().After(deadline) {
219			t.Fatal("no configure")
220		}
221		check(c.Roundtrip())
222	}
223	check(xdg.AckConfigure(serial))
224	check(surf.Attach(buffer, 0, 0))
225	check(surf.Commit())
226	return text
227}

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