extforeigntoplevel_test.go RawHistoryFolder

1package wayland
2
3import (
4	"slices"
5	"testing"
6
7	ext "github.com/bnema/go-wayland-bindings/server/extforeigntoplevellist"
8	source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
9	"github.com/bnema/go-wayland-bindings/server/wayland"
10	xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell"
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/bnema/wlturbo"
13)
14
15// extListEvents registers a handle proxy for each announced toplevel.
16type extListEvents struct {
17	wlturbo.BaseProxy
18	client  *wlturbo.Display
19	handles chan uint32
20	events  chan string
21}
22
23func (p *extListEvents) Dispatch(e *wlturbo.Event) {
24	switch uint32(e.Opcode) {
25	case ext.ExtForeignToplevelListV1EventToplevel:
26		id := e.Uint32()
27		h := &extHandleEvents{events: p.events}
28		h.SetID(id)
29		registerWireProxy(p.client, h)
30		p.handles <- id
31	case ext.ExtForeignToplevelListV1EventFinished:
32		p.events <- "finished"
33	}
34}
35
36// extHandleEvents records handle events as text.
37type extHandleEvents struct {
38	wlturbo.BaseProxy
39	events chan string
40}
41
42func (p *extHandleEvents) Dispatch(e *wlturbo.Event) {
43	switch uint32(e.Opcode) {
44	case ext.ExtForeignToplevelHandleV1EventIdentifier:
45		p.events <- "identifier " + e.String()
46	case ext.ExtForeignToplevelHandleV1EventTitle:
47		p.events <- "title " + e.String()
48	case ext.ExtForeignToplevelHandleV1EventAppId:
49		p.events <- "app_id " + e.String()
50	case ext.ExtForeignToplevelHandleV1EventDone:
51		p.events <- "done"
52	case ext.ExtForeignToplevelHandleV1EventClosed:
53		p.events <- "closed"
54	}
55}
56
57// The ext list announces mapped windows with a stable identifier, sends
58// changes, and closed when a window unmaps.
59func TestExtForeignToplevelList(t *testing.T) {
60	s, events, _, dir := lifecycleServer(t)
61	c := protocolClient(t, s, dir)
62	w, _, xdg := surfaceMapper(t, c, events)()
63	registerProtocol(t, c, xdg)
64	top := xdg + 1
65	requestProtocol(t, c, top, xdgshell.ToplevelRequestSetTitle, "editor")
66	requestProtocol(t, c, top, xdgshell.ToplevelRequestSetAppId, "app.editor")
67
68	list := bindProtocol(t, c, "ext_foreign_toplevel_list_v1")
69	p := &extListEvents{client: c, handles: make(chan uint32, 4), events: make(chan string, 64)}
70	p.SetID(list)
71	registerWireProxy(c, p)
72	want := []string{"identifier " + toplevelIdentifier(w.ID, 1), "title editor", "app_id app.editor", "done"}
73	if got := takeEvents(t, c, p.events); !slices.Equal(got, want) {
74		t.Fatalf("announce = %q, want %q", got, want)
75	}
76	<-p.handles
77	requestProtocol(t, c, top, xdgshell.ToplevelRequestSetTitle, "editor - file")
78	if got, want := takeEvents(t, c, p.events), []string{"title editor - file", "done"}; !slices.Equal(got, want) {
79		t.Fatalf("title = %q, want %q", got, want)
80	}
81
82	// After stop, a handle already sent still follows its window.
83	requestProtocol(t, c, list, ext.ExtForeignToplevelListV1RequestStop)
84	if err := c.Roundtrip(); err != nil {
85		t.Fatal(err)
86	}
87	if ev := <-p.events; ev != "finished" {
88		t.Fatalf("stop = %q", ev)
89	}
90	requestProtocol(t, c, top, xdgshell.ToplevelRequestSetTitle, "editor - other")
91	if got, want := takeEvents(t, c, p.events), []string{"title editor - other", "done"}; !slices.Equal(got, want) {
92		t.Fatalf("title after stop = %q, want %q", got, want)
93	}
94	requestProtocol(t, c, top, xdgshell.ToplevelRequestDestroy)
95	if got := takeEvents(t, c, p.events); !slices.Equal(got, []string{"closed"}) {
96		t.Fatalf("close = %q", got)
97	}
98}
99
100// A handle of the list makes a window capture source: its session opens on
101// the window in core, its frames ask for that window off screen, and the
102// session stops when the window goes.
103func TestWindowCaptureSource(t *testing.T) {
104	h := newCaptureHarness(t)
105	cc := h.client(t)
106	win, surf, xdg := surfaceMapper(t, cc.c, h.events)()
107	registerProtocol(t, cc.c, xdg)
108	h.send(ports.ConfigureWindow{ID: win.ID, Width: 3, Height: 2, Output: "HEADLESS-1", Visible: true})
109	h.settle(t, cc)
110
111	list := bindProtocol(t, cc.c, "ext_foreign_toplevel_list_v1")
112	p := &extListEvents{client: cc.c, handles: make(chan uint32, 4), events: make(chan string, 64)}
113	p.SetID(list)
114	registerWireProxy(cc.c, p)
115	takeEvents(t, cc.c, p.events)
116	handle := <-p.handles
117
118	mgr := bindProtocol(t, cc.c, "ext_foreign_toplevel_image_capture_source_manager_v1")
119	src := cc.c.AllocateID()
120	registerProtocol(t, cc.c, src)
121	requestProtocol(t, cc.c, mgr, source.ExtForeignToplevelImageCaptureSourceManagerV1RequestCreateSource, src, handle)
122	session, ev := cc.session(t, src)
123	if w, hh := constraints(t, cc, ev); w != 3 || hh != 2 {
124		t.Fatalf("buffer %dx%d, want the window's 3x2", w, hh)
125	}
126	open := h.open(t)
127	if open.Window != win.ID || open.Workspace != 0 || open.Output != "" {
128		t.Fatalf("open %+v", open)
129	}
130	h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 3, H: 2}, Hidden: true, Active: true})
131	h.settle(t, cc)
132	cc.extFrame(t, session, 3, 2)
133	r := receiveCapture(t, h.captures)
134	if r.Window != win.ID || !r.OffScreen || r.Region.Dx() != 3 || r.Region.Dy() != 2 {
135		t.Fatalf("request %+v", r)
136	}
137
138	// The window unmaps: the session stops at once.
139	requestProtocol(t, cc.c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
140	requestProtocol(t, cc.c, surf, wayland.SurfaceRequestCommit)
141	for {
142		if got := nextSession(t, cc, ev); got[0] == evStopped {
143			break
144		}
145	}
146	if ev := clientEvent[ports.WindowUnmapped](t, h.events); ev.ID != win.ID {
147		t.Fatalf("unmapped %d, want %d", ev.ID, win.ID)
148	}
149
150	// The remap is a new toplevel: a new identifier, and the source of the
151	// closed handle is dead.
152	serials := make(chan uint32, 8)
153	xp := &configureProxy{serial: serials}
154	xp.SetID(xdg)
155	registerWireProxy(cc.c, xp)
156	requestProtocol(t, cc.c, surf, wayland.SurfaceRequestCommit)
157	ackAndAttach(t, cc.c, surf, xdg, shmBuffer(t, cc.c), serials)
158	clientEvent[ports.WindowMapped](t, h.events)
159	if got := takeEvents(t, cc.c, p.events); !slices.Equal(got, []string{"closed"}) {
160		t.Fatalf("unmap = %q, want closed", got)
161	}
162	got := takeEvents(t, cc.c, p.events)
163	if !slices.Contains(got, "identifier "+toplevelIdentifier(win.ID, 2)) {
164		t.Fatalf("remap = %q", got)
165	}
166	_, ev = cc.session(t, src)
167	if got := nextSession(t, cc, ev); got[0] != evStopped {
168		t.Fatalf("session on a stale source: %v", got)
169	}
170}

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