v0.1.0 24ec840

display.go RawHistoryFolder

1package server
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"runtime"
8	"strings"
9	"sync/atomic"
10	"unsafe"
11
12	"golang.org/x/sys/unix"
13)
14
15// Handler receives every request sent to a resource.
16type Handler func(r *Resource, opcode uint32, args []Arg)
17
18// BindFunc runs when a client binds a global.
19type BindFunc func(c Client, version, id uint32)
20
21// Client is a connected wl_client.
22type Client struct{ c uintptr }
23
24// Resource is a server-side wl_resource.
25type Resource struct {
26	c         uintptr
27	iface     *Interface
28	handler   Handler
29	OnDestroy func()
30	gone      bool
31
32	// Data holds the owner's state, typically the generated wrapper.
33	Data any
34}
35
36// live holds Go state reachable from C callbacks. It is only touched on the
37// display goroutine, so it needs no lock.
38var live = struct {
39	resources map[uintptr]*Resource
40	binds     map[uintptr]BindFunc
41	nextBind  uintptr
42}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}}
43
44// Display owns a wl_display. All methods except Do must run on the goroutine
45// that calls Run, or before Run starts.
46// Do wakes the loop through an eventfd so calls run at once instead of after
47// the dispatch timeout.
48type Display struct {
49	c     uintptr
50	loop  uintptr
51	calls chan func()
52	wake  int
53	// wakeSrc owns libwayland's duplicate of wake; only removing the source
54	// closes it, destroying the display does not.
55	wakeSrc uintptr
56	stopped chan struct{}
57	state   atomic.Uint32
58}
59
60func NewDisplay() (*Display, error) {
61	if err := load(); err != nil {
62		return nil, err
63	}
64	c := wlDisplayCreate()
65	if c == 0 {
66		return nil, errors.New("purego-libwayland: wl_display_create failed")
67	}
68	wake, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
69	if err != nil {
70		wlDisplayDestroy(c)
71		return nil, fmt.Errorf("purego-libwayland: eventfd: %w", err)
72	}
73	d := &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func(), 64), wake: wake, stopped: make(chan struct{})}
74	// The callback only drains the counter; Run then executes queued calls.
75	if d.wakeSrc = wlEventLoopAddFD(d.loop, int32(wake), 1 /* WL_EVENT_READABLE */, cbWake, 0); d.wakeSrc == 0 {
76		unix.Close(wake)
77		wlDisplayDestroy(c)
78		return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
79	}
80	return d, nil
81}
82
83// AddSocketFD serves clients on an already bound, listening unix socket.
84func (d *Display) AddSocketFD(fd int) error {
85	if wlDisplayAddSocketFD(d.c, int32(fd)) != 0 {
86		return fmt.Errorf("purego-libwayland: wl_display_add_socket_fd(%d) failed", fd)
87	}
88	return nil
89}
90
91func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error {
92	_, err := d.AddGlobal(iface, version, bindFn)
93	return err
94}
95
96// Global is a global that can be withdrawn, such as a wl_output on hotplug.
97type Global struct{ c uintptr }
98
99// AddGlobal advertises a global and returns a handle to remove it later.
100func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
101	live.nextBind++
102	token := live.nextBind
103	live.binds[token] = bindFn
104	c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
105	if c == 0 {
106		delete(live.binds, token)
107		return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
108	}
109	return &Global{c: c}, nil
110}
111
112// Remove withdraws the global: clients get wl_registry.global_remove.
113// Existing resources stay valid. A client that binds before it sees the
114// removal still gets its bind function called, so it always receives a
115// resource (the protocol allows binding a just-removed global); the bind
116// function should then send inert state. The global is freed with the
117// display. Display goroutine only; idempotent.
118func (g *Global) Remove() {
119	if g == nil || g.c == 0 {
120		return
121	}
122	wlGlobalRemove(g.c)
123	g.c = 0
124}
125
126// Close destroys a display that has not started Run. It is a no-op once Run
127// has started; Run owns destruction in that case. Call before Run starts.
128func (d *Display) Close() {
129	if !d.state.CompareAndSwap(0, 1) {
130		return
131	}
132	d.destroy()
133	close(d.stopped)
134}
135
136// destroy frees the display and the wake-up fds.
137func (d *Display) destroy() {
138	wlDisplayDestroyClients(d.c)
139	wlEventSourceRemove(d.wakeSrc)
140	wlDisplayDestroy(d.c)
141	unix.Close(d.wake)
142}
143
144// Run dispatches libwayland on one OS-locked goroutine until ctx is done.
145func (d *Display) Run(ctx context.Context) error {
146	if !d.state.CompareAndSwap(0, 2) {
147		return errors.New("purego-libwayland: display already started or closed")
148	}
149	runtime.LockOSThread()
150	defer runtime.UnlockOSThread()
151	defer close(d.stopped)
152	defer d.destroy()
153	for {
154		if ctx.Err() != nil {
155			return nil
156		}
157		// Run every queued call; each Do also wrote the eventfd.
158	drain:
159		for {
160			select {
161			case fn := <-d.calls:
162				fn()
163			default:
164				break drain
165			}
166		}
167		wlDisplayFlushClients(d.c)
168		// The timeout only bounds how late ctx cancellation is seen.
169		if wlEventLoopDispatch(d.loop, 100) < 0 {
170			return errors.New("purego-libwayland: wl_event_loop_dispatch failed")
171		}
172		wlDisplayFlushClients(d.c)
173	}
174}
175
176// Do runs fn on the display goroutine and waits for it. It returns false if
177// the display loop has stopped (fn was not run, or its run was not confirmed).
178// Do must not be called from the display goroutine: it would deadlock.
179func (d *Display) Do(fn func()) bool {
180	done := make(chan struct{})
181	select {
182	case d.calls <- func() { fn(); close(done) }:
183	case <-d.stopped:
184		return false
185	}
186	var one = [8]byte{1}
187	_, _ = unix.Write(d.wake, one[:])
188	select {
189	case <-done:
190		return true
191	case <-d.stopped:
192		// If both are ready, prefer confirmation of a completed call.
193		select {
194		case <-done:
195			return true
196		default:
197			return false
198		}
199	}
200}
201
202// Stopped is closed after Run returns and the display is destroyed.
203func (d *Display) Stopped() <-chan struct{} { return d.stopped }
204
205// LiveResources reports resources not yet destroyed. Display goroutine only.
206func LiveResources() int { return len(live.resources) }
207
208// PID is the process ID of the client, read from the socket credentials
209// (SO_PEERCRED) when it connected. Zero for an invalid client.
210// Display goroutine only.
211func (c Client) PID() int {
212	if c.c == 0 {
213		return 0
214	}
215	var pid, uid, gid int32
216	wlClientGetCredentials(c.c, unsafe.Pointer(&pid), unsafe.Pointer(&uid), unsafe.Pointer(&gid))
217	return int(pid)
218}
219
220func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) {
221	rc := wlResourceCreate(c.c, iface.c, version, id)
222	if rc == 0 {
223		return nil, fmt.Errorf("purego-libwayland: wl_resource_create(%s) failed", iface.Name)
224	}
225	r := &Resource{c: rc, iface: iface, handler: h}
226	live.resources[rc] = r
227	wlResourceSetDispatcher(rc, cbDispatcher, implMarker(), 0, cbDestroy)
228	return r, nil
229}
230
231func (r *Resource) Alive() bool { return !r.gone }
232func (r *Resource) ID() uint32 {
233	if r.gone {
234		return 0
235	}
236	return wlResourceGetID(r.c)
237}
238func (r *Resource) Version() int32 {
239	if r.gone {
240		return 0
241	}
242	return wlResourceGetVersion(r.c)
243}
244func (r *Resource) Client() Client {
245	if r.gone {
246		return Client{}
247	}
248	return Client{wlResourceGetClient(r.c)}
249}
250func (r *Resource) Iface() *Interface { return r.iface }
251
252// Destroy frees the resource. It is a no-op once the resource is gone, so
253// generated destructor requests and handlers may both call it.
254func (r *Resource) Destroy() {
255	if !r.gone {
256		wlResourceDestroy(r.c)
257	}
258}
259
260// PostEvent sends an event. Arguments must match the event signature; build
261// string and array arguments with a Pinner and unpin it after this returns.
262// Calling it after destruction is safe and does nothing.
263func (r *Resource) PostEvent(opcode uint32, args ...Arg) {
264	if r.gone {
265		return
266	}
267	var p unsafe.Pointer
268	if len(args) > 0 {
269		p = unsafe.Pointer(&args[0])
270	}
271	var pin runtime.Pinner
272	if p != nil {
273		pin.Pin(p)
274		defer pin.Unpin()
275	}
276	wlResourcePostEventArr(r.c, opcode, p)
277	runtime.KeepAlive(args)
278}
279
280// PostError sends a protocol error and disconnects the client.
281// Calling it after destruction is safe and does nothing.
282func (r *Resource) PostError(code uint32, msg string) {
283	if r.gone {
284		return
285	}
286	// wl_resource_post_error takes a printf format; escape it and pass no
287	// variadic arguments (purego clears AL, as SysV varargs require).
288	b := append([]byte(strings.ReplaceAll(msg, "%", "%%")), 0)
289	var pin runtime.Pinner
290	pin.Pin(&b[0])
291	defer pin.Unpin()
292	wlResourcePostError(r.c, code, uintptr(unsafe.Pointer(&b[0])))
293	runtime.KeepAlive(b)
294}
295
296// implMarker is a non-nil implementation pointer. libwayland only passes it
297// back to our dispatcher.
298func implMarker() uintptr {
299	if marker == 0 {
300		tablesMu.Lock()
301		if err := tables.reserve(8); err != nil {
302			panic(err)
303		}
304		_, marker = tables.alloc(8)
305		tablesMu.Unlock()
306	}
307	return marker
308}
309
310var marker uintptr
311
312// --- C callbacks (display goroutine) ---
313
314func dispatch(_ uintptr, target uintptr, opcode uint32, msg unsafe.Pointer, args unsafe.Pointer) int32 {
315	r := live.resources[target]
316	sig := msgSignature(msg)
317	if r == nil || r.handler == nil {
318		closeRequestFDs(sig, args)
319		return 0
320	}
321	n := argCount(sig)
322	var a []Arg
323	if n > 0 {
324		a = append([]Arg(nil), unsafe.Slice((*Arg)(args), n)...)
325	}
326	r.handler(r, opcode, a)
327	return 0
328}
329
330func destroyed(res uintptr) {
331	r := live.resources[res]
332	delete(live.resources, res)
333	if r == nil {
334		return
335	}
336	r.gone = true
337	if r.OnDestroy != nil {
338		r.OnDestroy()
339	}
340}
341
342// wake drains a Display's eventfd (wl_event_loop_fd_func_t).
343func wake(fd int32, mask uint32, data uintptr) int32 {
344	var buf [8]byte
345	_, _ = unix.Read(int(fd), buf[:])
346	return 0
347}
348
349func bind(client uintptr, data uintptr, version uint32, id uint32) {
350	if fn := live.binds[data]; fn != nil {
351		fn(Client{client}, version, id)
352	}
353}
354
355// msgSignature reads wl_message.signature.
356func msgSignature(msg unsafe.Pointer) string {
357	p := *(*unsafe.Pointer)(unsafe.Add(msg, 8))
358	n := 0
359	for *(*byte)(unsafe.Add(p, n)) != 0 {
360		n++
361	}
362	return string(unsafe.Slice((*byte)(p), n))
363}
364
365// closeRequestFDs takes ownership of fd arguments when there is no handler.
366func closeRequestFDs(sig string, args unsafe.Pointer) {
367	i := 0
368	for _, ch := range sig {
369		if ch == '?' || (ch >= '0' && ch <= '9') {
370			continue
371		}
372		if ch == 'h' {
373			_ = unix.Close((*Arg)(unsafe.Add(args, i*8)).Fd())
374		}
375		i++
376	}
377}
378
379// argCount counts arguments in a libwayland signature ("?oii", "2n", ...).
380func argCount(sig string) int {
381	n := 0
382	for _, ch := range sig {
383		if ch != '?' && (ch < '0' || ch > '9') {
384			n++
385		}
386	}
387	return n
388}

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 14

↑↓ moveenter openesc close