v0.4.0 2de4e1d

layer_test.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"context"
7	"encoding/binary"
8	"errors"
9	"fmt"
10	"io"
11	"net"
12	"os"
13	"path/filepath"
14	"strings"
15	"testing"
16	"time"
17
18	"github.com/bnema/nefergui/internal/harness"
19	"github.com/bnema/wlturbo"
20	"github.com/bnema/wlturbo/protocol/core"
21	"github.com/bnema/wlturbo/wl"
22)
23
24func TestLayerConstantsMatchProtocol(t *testing.T) {
25	if LayerBackground != 0 || LayerBottom != 1 || LayerTop != 2 || LayerOverlay != 3 {
26		t.Fatal("layer values")
27	}
28	if AnchorTop != 1 || AnchorBottom != 2 || AnchorLeft != 4 || AnchorRight != 8 {
29		t.Fatal("anchor bits")
30	}
31	if KeyboardNone != 0 || KeyboardExclusive != 1 || KeyboardOnDemand != 2 {
32		t.Fatal("keyboard modes")
33	}
34}
35
36func TestSurfaceOptionsValidate(t *testing.T) {
37	ok := LayerOptions{Layer: LayerOverlay, Anchor: AnchorTop | AnchorLeft | AnchorRight | AnchorBottom, Keyboard: KeyboardOnDemand}
38	for name, tc := range map[string]struct {
39		opts SurfaceOptions
40		bad  bool
41	}{
42		"zero":            {},
43		"valid layer":     {opts: SurfaceOptions{Layer: &ok}},
44		"empty passthru":  {opts: SurfaceOptions{InputRects: []Rect{}}},
45		"layer 4":         {opts: SurfaceOptions{Layer: &LayerOptions{Layer: 4}}, bad: true},
46		"anchor 16":       {opts: SurfaceOptions{Layer: &LayerOptions{Anchor: 16}}, bad: true},
47		"zone -2":         {opts: SurfaceOptions{Layer: &LayerOptions{ExclusiveZone: -2}}, bad: true},
48		"zone -1":         {opts: SurfaceOptions{Layer: &LayerOptions{ExclusiveZone: -1}}},
49		"keyboard 3":      {opts: SurfaceOptions{Layer: &LayerOptions{Keyboard: 3}}, bad: true},
50		"namespace NUL":   {opts: SurfaceOptions{Layer: &LayerOptions{Namespace: "a\x00b"}}, bad: true},
51		"zero-size rect":  {opts: SurfaceOptions{InputRects: []Rect{{0, 0, 0, 5}}}, bad: true},
52		"overflow rect":   {opts: SurfaceOptions{InputRects: []Rect{{1<<31 - 2, 0, 5, 5}}}, bad: true},
53		"negative origin": {opts: SurfaceOptions{InputRects: []Rect{{-5, -5, 10, 10}}}},
54	} {
55		if err := tc.opts.Validate(); (err != nil) != tc.bad {
56			t.Errorf("%s: err=%v", name, err)
57		}
58	}
59	if _, err := ConnectWithOptions(context.Background(), "", 10, 10, false, SurfaceOptions{Layer: &LayerOptions{Layer: 9}}); err == nil || !strings.Contains(err.Error(), "invalid layer") {
60		t.Fatalf("connect must validate before dialing: %v", err)
61	}
62}
63
64func TestLayerConfigureSizeBounds(t *testing.T) {
65	if w, h, err := layerConfigureSize(0, 16384); err != nil || w != 0 || h != 16384 {
66		t.Fatal(w, h, err)
67	}
68	if _, _, err := layerConfigureSize(16385, 1); err == nil {
69		t.Fatal("oversize accepted")
70	}
71	w := &Window{Width: 100, Height: 50}
72	w.applyLayerSize(0, 40)
73	if w.Width != 100 || w.Height != 40 || !w.Dirty {
74		t.Fatalf("%+v", w)
75	}
76}
77
78// A post-startup configure must carry size and serial together, even when the
79// previous frame is ready. Applying it must ack before the owner can commit.
80func TestLayerConfigureAfterReaderStart(t *testing.T) {
81	srv, accept := newWireServer(t)
82	type dialed struct {
83		d   *wlturbo.Display
84		err error
85	}
86	ch := make(chan dialed, 1)
87	go func() {
88		d, err := wlturbo.Connect(srv.path)
89		ch <- dialed{d, err}
90	}()
91	conn := accept()
92	r := <-ch
93	if r.err != nil {
94		t.Fatal(r.err)
95	}
96	d := r.d
97	defer d.Close()
98	comp := core.NewCompositor(d.Context())
99	if err := d.Registry().Bind(1, core.CompositorInterface, 6, comp); err != nil {
100		t.Fatal(err)
101	}
102	surface, err := comp.CreateSurface()
103	if err != nil {
104		t.Fatal(err)
105	}
106	w := &Window{Display: d, Compositor: comp, Surface: surface, Width: 100, Height: 40,
107		Configured: true, FrameReady: true, events: make(chan Event, 8), readerStop: make(chan struct{})}
108	if err := w.bindLayerShell(func(iface string, max, min uint32, p wl.Proxy) error {
109		return d.Registry().Bind(2, iface, max, p)
110	}); err != nil {
111		t.Fatal(err)
112	}
113	if err := w.createLayerSurface(LayerOptions{Layer: LayerTop}); err != nil {
114		t.Fatal(err)
115	}
116	w.StartReader()
117	defer func() {
118		close(w.readerStop)
119		_ = d.Close()
120		<-w.readerDone
121	}()
122	for _, tc := range []struct {
123		serial, width, height uint32
124		wantW, wantH          int32
125		ready                 bool
126	}{{7, 200, 50, 200, 50, true}, {8, 0, 60, 200, 60, false}} {
127		w.FrameReady, w.Dirty = tc.ready, false
128		if _, err := conn.Write(frame(w.LayerSurface.ID(), 0, tc.serial, tc.width, tc.height)); err != nil {
129			t.Fatal(err)
130		}
131		var ev Event
132		select {
133		case ev = <-w.Events():
134		case <-time.After(5 * time.Second):
135			t.Fatal("no configure event")
136		}
137		if ev.Kind != ConfigureLayer || ev.Width != int32(tc.width) || ev.Height != int32(tc.height) || ev.Serial != tc.serial {
138			t.Fatalf("configure must be one size+serial event: %+v", ev)
139		}
140		if err := w.Apply(ev); err != nil {
141			t.Fatal(err)
142		}
143		if w.Width != tc.wantW || w.Height != tc.wantH || !w.Dirty || !w.Configured || !w.FrameReady {
144			t.Fatalf("configure state: size=%dx%d dirty=%v configured=%v ready=%v", w.Width, w.Height, w.Dirty, w.Configured, w.FrameReady)
145		}
146		if err := surface.Commit(); err != nil {
147			t.Fatal(err)
148		}
149		// Ignore setup requests, but require exactly one ack of this configure
150		// before the first subsequent surface commit.
151		_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
152		acks := 0
153		for {
154			var hdr [8]byte
155			if _, err := io.ReadFull(conn, hdr[:]); err != nil {
156				t.Fatal(err)
157			}
158			obj, word := binary.NativeEndian.Uint32(hdr[:]), binary.NativeEndian.Uint32(hdr[4:])
159			size, op := int(word>>16), uint16(word)
160			if size < 8 {
161				t.Fatalf("bad frame size %d", size)
162			}
163			args := make([]byte, size-8)
164			if _, err := io.ReadFull(conn, args); err != nil {
165				t.Fatal(err)
166			}
167			if obj == w.LayerSurface.ID() && op == 6 {
168				if len(args) != 4 || binary.NativeEndian.Uint32(args) != tc.serial {
169					t.Fatalf("wrong ack payload %x, want serial %d", args, tc.serial)
170				}
171				acks++
172			}
173			if obj == surface.ID() && op == 6 {
174				if acks != 1 {
175					t.Fatalf("commit preceded by %d configure acks, want 1", acks)
176				}
177				break
178			}
179		}
180	}
181}
182
183// Harness acceptance: a real NeferWL compositor and the real client code.
184func runLayerHarness(t *testing.T, child string) (string, string) {
185	t.Helper()
186	if os.Getenv("NEFERGUI_HARNESS") != "1" {
187		t.Skip("set NEFERGUI_HARNESS=1 and NEFERGUI_NEFERWL for the layer-shell harness")
188	}
189	root := filepath.Join(t.TempDir(), "artifacts")
190	result, code := harness.Run(context.Background(), harness.Options{Size: "400x300", Scale: "1", Layout: "us", Background: "#111111", Out: root, AllowUnpinned: os.Getenv("NEFERGUI_NEFERWL") != "", Timeout: 30 * time.Second, ReadyTimeout: 10 * time.Second, Client: []string{os.Args[0], "-test.run=^" + child + "$", "-test.v"}})
191	out, _ := os.ReadFile(result.Artifacts["client_stdout"])
192	errOut, _ := os.ReadFile(result.Artifacts["client_stderr"])
193	if code != harness.Pass {
194		t.Fatalf("code=%d status=%s error=%s\nstdout:\n%s\nstderr:\n%s", code, result.Status, result.Error, out, errOut)
195	}
196	return string(out), string(errOut)
197}
198
199func TestHarnessLayerSurface(t *testing.T) {
200	out, _ := runLayerHarness(t, "TestHarnessLayerSurfaceClient")
201	for _, want := range []string{"LAYER configured", "LAYER height=40", "LAYER input-passthrough", "LAYER input-default", "LAYER closed"} {
202		if !strings.Contains(out, want) {
203			t.Fatalf("missing %q in:\n%s", want, out)
204		}
205	}
206}
207
208func TestHarnessLayerOutputSelection(t *testing.T) {
209	out, _ := runLayerHarness(t, "TestHarnessLayerOutputClient")
210	for _, want := range []string{"OUTPUT exact ok", "OUTPUT missing rejected"} {
211		if !strings.Contains(out, want) {
212			t.Fatalf("missing %q in:\n%s", want, out)
213		}
214	}
215}
216
217func TestHarnessXDGDefaultUnchanged(t *testing.T) {
218	out, _ := runLayerHarness(t, "TestHarnessXDGClient")
219	if !strings.Contains(out, "XDG configured toplevel") {
220		t.Fatal(out)
221	}
222}
223
224func harnessChild(t *testing.T) {
225	t.Helper()
226	if os.Getenv("NEFERGUI_DEBUG_DIR") == "" {
227		t.Skip("harness child only")
228	}
229}
230
231func TestHarnessLayerSurfaceClient(t *testing.T) {
232	harnessChild(t)
233	w, err := ConnectWithOptions(context.Background(), "", 100, 40, true, SurfaceOptions{
234		InputRects: []Rect{{0, 0, 10, 10}},
235		Layer: &LayerOptions{
236			Layer: LayerTop, Anchor: AnchorTop | AnchorLeft | AnchorRight, Keyboard: KeyboardOnDemand,
237			ExclusiveZone: 40, Margin: [4]int32{2, 3, 4, 5}, Namespace: "nefergui-test",
238		},
239	})
240	if err != nil {
241		t.Fatal(err)
242	}
243	if !w.IsLayer() || w.Toplevel != nil || w.XdgSurface != nil || w.Shell != nil {
244		t.Fatal("layer window must not create xdg objects")
245	}
246	if !w.Configured || w.Width < 100 {
247		t.Fatalf("not configured: %+v", w)
248	}
249	fmt.Println("LAYER configured", w.Width, w.Height)
250	if w.Height == 40 {
251		fmt.Println("LAYER height=40")
252	}
253	if err = w.SetTitle("ignored"); err != nil {
254		t.Fatal(err)
255	}
256	// Empty non-nil region: pass-through; nil: default full surface.
257	if err = w.SetInputRects([]Rect{}); err != nil {
258		t.Fatal(err)
259	}
260	if err = w.SetInputRects([]Rect{}); err != nil {
261		t.Fatal(err)
262	}
263	fmt.Println("LAYER input-passthrough")
264	if err = w.SetInputRects(nil); err != nil {
265		t.Fatal(err)
266	}
267	fmt.Println("LAYER input-default")
268	// A roundtrip proves the compositor processed and accepted every request
269	// (a protocol error would fail it); only then does the reader take over.
270	if err = w.Display.Roundtrip(); err != nil {
271		t.Fatal(err)
272	}
273	// Hand dispatch to the reader and prove it works: a close from the
274	// owner side must still tear down cleanly.
275	events := w.StartReader()
276	for drained := false; !drained; {
277		select {
278		case ev := <-events:
279			if ev.Kind == TransportError || ev.Kind == CloseEvent {
280				t.Fatalf("unexpected %+v", ev)
281			}
282			if err = w.Apply(ev); err != nil {
283				t.Fatal(err)
284			}
285		default:
286			drained = true
287		}
288	}
289	if err = w.Close(); err != nil {
290		t.Fatal(err)
291	}
292	fmt.Println("LAYER closed")
293}
294
295func TestHarnessLayerOutputClient(t *testing.T) {
296	harnessChild(t)
297	d, err := wl.Connect("")
298	if err != nil {
299		t.Fatal(err)
300	}
301	if err = d.Roundtrip(); err != nil {
302		t.Fatal(err)
303	}
304	var name string
305	for _, g := range d.Registry().GetGlobals() {
306		if g.Interface != core.OutputInterface {
307			continue
308		}
309		if g.Version < outputNameVersion {
310			t.Fatalf("wl_output v%d lacks names", g.Version)
311		}
312		o := core.NewOutput(d.Context())
313		o.OnName(func(n string) { name = n })
314		if err = d.Registry().Bind(g.Name, core.OutputInterface, outputNameVersion, o); err != nil {
315			t.Fatal(err)
316		}
317		break
318	}
319	if err = d.Roundtrip(); err != nil {
320		t.Fatal(err)
321	}
322	_ = d.Close()
323	if name == "" {
324		t.Fatal("no output name")
325	}
326	w, err := ConnectWithOptions(context.Background(), "", 100, 40, false, SurfaceOptions{Layer: &LayerOptions{Output: name, Layer: LayerBottom, Anchor: AnchorBottom | AnchorLeft | AnchorRight}})
327	if err != nil {
328		t.Fatalf("output %q: %v", name, err)
329	}
330	if err = w.Close(); err != nil {
331		t.Fatal(err)
332	}
333	fmt.Println("OUTPUT exact ok", name)
334	_, err = ConnectWithOptions(context.Background(), "", 100, 40, false, SurfaceOptions{Layer: &LayerOptions{Output: name + "-missing"}})
335	var ce *CapabilityError
336	if !errors.As(err, &ce) {
337		t.Fatalf("missing output error: %v", err)
338	}
339	fmt.Println("OUTPUT missing rejected")
340}
341
342func TestHarnessXDGClient(t *testing.T) {
343	harnessChild(t)
344	w, err := Connect("", 100, 50, false)
345	if err != nil {
346		t.Fatal(err)
347	}
348	if w.IsLayer() || w.Toplevel == nil || w.XdgSurface == nil || !w.Configured {
349		t.Fatal("default must be xdg toplevel")
350	}
351	if err = w.SetTitle("x"); err != nil {
352		t.Fatal(err)
353	}
354	fmt.Println("XDG configured toplevel")
355	if err = w.Close(); err != nil {
356		t.Fatal(err)
357	}
358}
359
360// wireServer is a real unix-socket Wayland peer; tests speak raw frames.
361type wireServer struct {
362	conn net.Conn
363	path string
364}
365
366func newWireServer(t *testing.T) (*wireServer, func() net.Conn) {
367	t.Helper()
368	dir := t.TempDir()
369	path := filepath.Join(dir, "wl-test")
370	ln, err := net.Listen("unix", path)
371	if err != nil {
372		t.Fatal(err)
373	}
374	t.Cleanup(func() { _ = ln.Close() })
375	srv := &wireServer{path: path}
376	accepted := make(chan net.Conn, 1)
377	go func() {
378		c, e := ln.Accept()
379		if e == nil {
380			accepted <- c
381		}
382	}()
383	return srv, func() net.Conn {
384		select {
385		case c := <-accepted:
386			srv.conn = c
387			t.Cleanup(func() { _ = c.Close() })
388			return c
389		case <-time.After(5 * time.Second):
390			t.Fatal("client never connected")
391			return nil
392		}
393	}
394}
395
396func frame(object uint32, opcode uint16, args ...uint32) []byte {
397	b := make([]byte, 8+4*len(args))
398	binary.NativeEndian.PutUint32(b, object)
399	binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(opcode))
400	for i, a := range args {
401		binary.NativeEndian.PutUint32(b[8+4*i:], a)
402	}
403	return b
404}
405
406// A compositor that never answers must not block startup past ctx; the
407// connection is closed so the server sees EOF.
408func TestConnectWithOptionsCancelsHangingServer(t *testing.T) {
409	srv, accept := newWireServer(t)
410	ctx, cancel := context.WithCancel(context.Background())
411	type result struct {
412		w   *Window
413		err error
414	}
415	done := make(chan result, 1)
416	go func() {
417		w, err := ConnectWithOptions(ctx, srv.path, 100, 40, false, SurfaceOptions{})
418		done <- result{w, err}
419	}()
420	conn := accept()
421	buf := make([]byte, 64)
422	if _, err := conn.Read(buf); err != nil { // get_registry / sync requests: client is now blocked
423		t.Fatal(err)
424	}
425	select {
426	case <-done:
427		t.Fatal("returned before cancel")
428	case <-time.After(100 * time.Millisecond):
429	}
430	cancel()
431	select {
432	case r := <-done:
433		if r.w != nil || !errors.Is(r.err, context.Canceled) {
434			t.Fatalf("window=%v err=%v", r.w, r.err)
435		}
436	case <-time.After(5 * time.Second):
437		t.Fatal("ConnectWithOptions still blocked after cancel")
438	}
439	_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
440	for {
441		if _, err := conn.Read(buf); err != nil {
442			if !errors.Is(err, io.EOF) {
443				t.Fatalf("server expected EOF (client closed), got %v", err)
444			}
445			break
446		}
447	}
448}
449
450func TestConnectWithOptionsPrecancelledAndNil(t *testing.T) {
451	ctx, cancel := context.WithCancel(context.Background())
452	cancel()
453	if _, err := ConnectWithOptions(ctx, "/nonexistent", 1, 1, false, SurfaceOptions{}); !errors.Is(err, context.Canceled) {
454		t.Fatal(err)
455	}
456	//lint:ignore SA1012 nil context is an explicit error case
457	if _, err := ConnectWithOptions(nil, "/nonexistent", 1, 1, false, SurfaceOptions{}); err == nil {
458		t.Fatal("nil context accepted")
459	}
460}
461
462// Removal of the selected output's registry global closes the window, before
463// and after the reader starts; other globals are ignored.
464func TestSelectedOutputRemovalClosesWindow(t *testing.T) {
465	for _, reader := range []bool{false, true} {
466		t.Run(fmt.Sprintf("reader=%v", reader), func(t *testing.T) {
467			srv, accept := newWireServer(t)
468			type dialed struct {
469				d   *wlturbo.Display
470				err error
471			}
472			ch := make(chan dialed, 1)
473			go func() {
474				d, err := wlturbo.Connect(srv.path)
475				ch <- dialed{d, err}
476			}()
477			conn := accept()
478			r := <-ch
479			if r.err != nil {
480				t.Fatal(r.err)
481			}
482			d := r.d
483			defer d.Close()
484			w := &Window{Display: d, events: make(chan Event, 8), readerStop: make(chan struct{}), layerOutputSelected: true, layerOutputGlobal: 7}
485			d.Registry().AddGlobalRemoveHandler(outputRemoved{w})
486			if reader {
487				w.StartReader()
488			}
489			regID := d.Registry().ID()
490			if _, err := conn.Write(frame(regID, 1, 9)); err != nil { // other global
491				t.Fatal(err)
492			}
493			if !reader {
494				if err := d.Dispatch(); err != nil {
495					t.Fatal(err)
496				}
497				if w.Closed {
498					t.Fatal("unrelated removal closed the window")
499				}
500			} else {
501				select {
502				case ev := <-w.Events():
503					t.Fatalf("unrelated removal posted %+v", ev)
504				case <-time.After(100 * time.Millisecond):
505				}
506			}
507			if _, err := conn.Write(frame(regID, 1, 7)); err != nil {
508				t.Fatal(err)
509			}
510			if !reader {
511				if err := d.Dispatch(); err != nil {
512					t.Fatal(err)
513				}
514				if !w.Closed {
515					t.Fatal("selected output removal did not close")
516				}
517				return
518			}
519			select {
520			case ev := <-w.Events():
521				if ev.Kind != CloseEvent {
522					t.Fatalf("event %+v", ev)
523				}
524			case <-time.After(5 * time.Second):
525				t.Fatal("no CloseEvent")
526			}
527			close(w.readerStop)
528			_ = d.Close()
529			<-w.readerDone
530		})
531	}
532}
533
534// surfaceRequestCounts reads raw client frames until the sentinel (wl_surface.damage on
535// surface) and returns "object/opcode" counts for the surface.
536func surfaceRequestCounts(t *testing.T, conn net.Conn, surface uint32) map[uint16]int {
537	t.Helper()
538	counts := map[uint16]int{}
539	_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
540	hdr := make([]byte, 8)
541	for {
542		if _, err := io.ReadFull(conn, hdr); err != nil {
543			t.Fatal(err)
544		}
545		obj := binary.NativeEndian.Uint32(hdr)
546		word := binary.NativeEndian.Uint32(hdr[4:])
547		size, op := int(word>>16), uint16(word)
548		if size < 8 {
549			t.Fatalf("bad frame size %d", size)
550		}
551		if _, err := io.CopyN(io.Discard, conn, int64(size-8)); err != nil {
552			t.Fatal(err)
553		}
554		if obj != surface {
555			continue
556		}
557		if op == 2 { // wl_surface.damage sentinel
558			return counts
559		}
560		counts[op]++
561	}
562}
563
564// StageInputRects never commits; SetInputRects commits only a real change on a
565// configured window; an unchanged request sends nothing. Counted on the wire.
566func TestInputRectRequestsOnWire(t *testing.T) {
567	const opSetInputRegion, opCommit = 5, 6
568	srv, accept := newWireServer(t)
569	type dialed struct {
570		d   *wlturbo.Display
571		err error
572	}
573	ch := make(chan dialed, 1)
574	go func() {
575		d, err := wlturbo.Connect(srv.path)
576		ch <- dialed{d, err}
577	}()
578	conn := accept()
579	r := <-ch
580	if r.err != nil {
581		t.Fatal(r.err)
582	}
583	d := r.d
584	defer d.Close()
585	comp := core.NewCompositor(d.Context())
586	if err := d.Registry().Bind(1, core.CompositorInterface, 6, comp); err != nil {
587		t.Fatal(err)
588	}
589	surface, err := comp.CreateSurface()
590	if err != nil {
591		t.Fatal(err)
592	}
593	w := &Window{Display: d, Compositor: comp, Surface: surface}
594	step := func(name string, run func() error, wantSet, wantCommit int) {
595		t.Helper()
596		if err := run(); err != nil {
597			t.Fatal(name, err)
598		}
599		if err := surface.Damage(0, 0, 1, 1); err != nil {
600			t.Fatal(err)
601		}
602		got := surfaceRequestCounts(t, conn, surface.ID())
603		if got[opSetInputRegion] != wantSet || got[opCommit] != wantCommit {
604			t.Fatalf("%s: set_input_region=%d commit=%d, want %d/%d", name, got[opSetInputRegion], got[opCommit], wantSet, wantCommit)
605		}
606	}
607	rects := []Rect{{0, 0, 10, 10}, {20, 0, 5, 5}}
608	step("stage", func() error { return w.StageInputRects(rects) }, 1, 0)
609	step("stage identical", func() error { return w.StageInputRects(append([]Rect(nil), rects...)) }, 0, 0)
610	step("set identical while configured", func() error { w.Configured = true; return w.SetInputRects(rects) }, 0, 0)
611	step("set changed", func() error { return w.SetInputRects([]Rect{{1, 1, 2, 2}}) }, 1, 1)
612	step("set passthrough", func() error { return w.SetInputRects([]Rect{}) }, 1, 1)
613	step("set passthrough again", func() error { return w.SetInputRects([]Rect{}) }, 0, 0)
614	step("set default", func() error { return w.SetInputRects(nil) }, 1, 1)
615	step("set default again", func() error { return w.SetInputRects(nil) }, 0, 0)
616	w.Configured = false
617	step("unconfigured change stages only", func() error { return w.SetInputRects(rects) }, 1, 0)
618	before := len(w.inputRects)
619	step("invalid stays unsent", func() error {
620		if err := w.StageInputRects([]Rect{{0, 0, 0, 1}}); err == nil {
621			t.Fatal("invalid rect accepted")
622		}
623		return nil
624	}, 0, 0)
625	if len(w.inputRects) != before {
626		t.Fatal("invalid request changed cached state")
627	}
628	if n := testing.AllocsPerRun(100, func() { _ = w.StageInputRects(rects) }); n != 0 {
629		t.Fatalf("unchanged stage allocates %v", n)
630	}
631}

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