v0.2.0 e178e0b

conn.go RawHistoryFolder

1package neferclient
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"net"
8	"os"
9	"path/filepath"
10	"time"
11
12	"github.com/bnema/go-wayland-bindings/client/wayland"
13	"github.com/bnema/wlturbo"
14	"github.com/bnema/wlturbo/wl"
15	"golang.org/x/sys/unix"
16)
17
18// dialTimeout bounds the unix-socket dial; a local dial is normally instant.
19const dialTimeout = 5 * time.Second
20
21// maxOutputVersion is the wl_output version bound: 4 adds names.
22const maxOutputVersion = 4
23
24var (
25	// ErrClosed is returned by calls on a closed Conn.
26	ErrClosed = errors.New("neferclient: connection closed")
27	// ErrGlobalNotFound is returned by [Conn.Bind] when the compositor does
28	// not announce the interface. Test it with errors.Is.
29	ErrGlobalNotFound = wlturbo.ErrGlobalNotFound
30)
31
32// Output describes one wl_output after its latest wl_output.done. Only
33// wl_output globals of version 2 or later are bound (version 1 has no done
34// event, so its description is never known to be complete); older ones are
35// ignored.
36type Output struct {
37	Global        uint32 // registry name; identifies the output for its lifetime
38	Name          string // wl_output.name, empty before version 4
39	Scale         int32  // integer scale, 1 until announced
40	Width, Height int32  // current mode in physical pixels
41}
42
43// outputEntry holds the owner-side state of one bound wl_output. Properties
44// accumulate in the pending fields and become visible at wl_output.done.
45type outputEntry struct {
46	proxy *wayland.Output
47	done  bool // first wl_output.done applied; the output is public
48	pub   Output
49
50	nameBuf       [maxNameLen]byte
51	nameLen       int
52	scale         int32
53	width, height int32
54}
55
56// Conn is one Wayland connection. Two goroutines run beside the owner: the
57// reader, which only decodes wire events into a fixed queue, and an epoll
58// waiter for [Conn.WatchFD]. Everything else — event handling, requests,
59// Bind, Roundtrip, Outputs, Close — belongs to the owner goroutine, the one
60// that calls [Conn.Dispatch].
61type Conn struct {
62	sock    net.Conn
63	display *wl.Display
64	wlctx   *wl.Context
65	q       *queue
66
67	readerStarted bool
68	readerDone    chan struct{}
69	pauseDepth    int
70
71	epfd, efd  int
72	epollStart bool
73	epollDone  chan struct{}
74	watched    map[int32]uint64 // fd -> caller id
75
76	outputs []*outputEntry // sorted by Global
77	view    []Output       // reused by Outputs
78
79	g        globals // lazily bound protocol globals, owner only
80	surfaces map[SurfaceID]*Surface
81	nextSurf SurfaceID
82	nextLock uint32
83	curLock  *Lock // the live session lock, if any
84	tableBuf []byte
85	seat     *Seat
86	seatErr  error // a seat bind failure, reported by the next Dispatch
87
88	ready       bool // Connect finished; announcements go to the Handler
89	dispatching bool
90	closed      bool
91}
92
93// Connect dials the compositor and completes the registry setup: every
94// wl_output announced at that point is bound and its first description is
95// available from [Conn.Outputs]. display names the socket relative to
96// $XDG_RUNTIME_DIR, or is an absolute path; "" uses $WAYLAND_DISPLAY, then
97// "wayland-0". Cancelling ctx abandons setup; afterwards ctx is not used.
98func Connect(ctx context.Context, display string) (*Conn, error) {
99	if ctx == nil {
100		return nil, errors.New("neferclient: nil context")
101	}
102	if err := ctx.Err(); err != nil {
103		return nil, err
104	}
105	path, err := socketPath(display)
106	if err != nil {
107		return nil, err
108	}
109	sock, err := (&net.Dialer{Timeout: dialTimeout}).DialContext(ctx, "unix", path)
110	if err != nil {
111		return nil, fmt.Errorf("neferclient: connect %s: %w", path, err)
112	}
113	return connectConn(ctx, sock)
114}
115
116func socketPath(name string) (string, error) {
117	if name == "" {
118		name = os.Getenv("WAYLAND_DISPLAY")
119		if name == "" {
120			name = "wayland-0"
121		}
122	}
123	if filepath.IsAbs(name) {
124		return name, nil
125	}
126	dir := os.Getenv("XDG_RUNTIME_DIR")
127	if dir == "" {
128		return "", errors.New("neferclient: XDG_RUNTIME_DIR not set")
129	}
130	return filepath.Join(dir, name), nil
131}
132
133// connectConn runs the setup over an established connection and takes
134// ownership of it.
135func connectConn(ctx context.Context, sock net.Conn) (res *Conn, err error) {
136	display, err := wlturbo.ConnectFromConn(sock)
137	if err != nil {
138		_ = sock.Close()
139		return nil, fmt.Errorf("neferclient: %w", err)
140	}
141	c := &Conn{
142		sock:    sock,
143		display: display,
144		wlctx:   display.Context(),
145		q:       newQueue(true), // no reader yet: owner-side events are parked
146		efd:     -1,
147		epfd:    -1,
148		watched: make(map[int32]uint64),
149
150		surfaces: make(map[SurfaceID]*Surface),
151	}
152	watch := context.AfterFunc(ctx, func() { _ = display.Close() })
153	defer func() {
154		if !watch() && err == nil {
155			err = ctx.Err()
156		}
157		if err != nil {
158			_ = c.Close()
159			res = nil
160			if ctx.Err() != nil {
161				err = errors.Join(ctx.Err(), err)
162			}
163		}
164	}()
165	if c.epfd, err = unix.EpollCreate1(unix.EPOLL_CLOEXEC); err != nil {
166		return nil, fmt.Errorf("neferclient: epoll: %w", err)
167	}
168	if c.efd, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
169		return nil, fmt.Errorf("neferclient: eventfd: %w", err)
170	}
171	ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: int32(c.efd)}
172	if err = unix.EpollCtl(c.epfd, unix.EPOLL_CTL_ADD, c.efd, &ev); err != nil {
173		return nil, fmt.Errorf("neferclient: epoll: %w", err)
174	}
175
176	reg := display.Registry()
177	reg.AddHandler(wayland.OutputInterface, func(_ *wl.Registry, name, version uint32) {
178		e := event{kind: evGlobal, global: name, version: version}
179		c.q.post(&e)
180	})
181	reg.AddGlobalRemoveHandler(removeHook{c.q})
182
183	// With no reader running, the owner's own roundtrips park every event in
184	// the queue's held list, in order; applying it here is the startup drain.
185	if err = display.Roundtrip(); err != nil {
186		return nil, fmt.Errorf("neferclient: registry roundtrip: %w", err)
187	}
188	if err = c.applyHeld(); err != nil {
189		return nil, err
190	}
191	if err = display.Roundtrip(); err != nil { // wl_output properties follow the binds
192		return nil, fmt.Errorf("neferclient: output roundtrip: %w", err)
193	}
194	if err = c.applyHeld(); err != nil {
195		return nil, err
196	}
197
198	c.ready = true
199	c.readerDone = make(chan struct{})
200	c.readerStarted = true
201	c.epollDone = make(chan struct{})
202	c.epollStart = true
203	c.q.setPausing(false)
204	go c.readLoop()
205	go c.epollLoop()
206	return c, nil
207}
208
209// removeHook forwards registry global_remove to the queue.
210type removeHook struct{ q *queue }
211
212func (h removeHook) HandleRegistryGlobalRemove(e wl.RegistryGlobalRemoveEvent) {
213	ev := event{kind: evGlobalRemove, global: e.Name}
214	h.q.post(&ev)
215}
216
217// setupHandler applies setup events without notifying anyone and keeps the
218// first failure, which makes Connect fail.
219type setupHandler struct {
220	NopHandler
221	err *error
222}
223
224func (h setupHandler) Error(err error) {
225	if *h.err == nil {
226		*h.err = err
227	}
228}
229
230// applyHeld applies the events parked during setup.
231func (c *Conn) applyHeld() error {
232	var err error
233	h := setupHandler{err: &err}
234	held := c.q.takeHeld()
235	for i := range held {
236		c.apply(&held[i], h)
237	}
238	return err
239}
240
241// readLoop is the reader goroutine. It only calls Display.Dispatch; the
242// generated handlers it triggers only push events into the queue.
243func (c *Conn) readLoop() {
244	defer close(c.readerDone)
245	defer c.q.readerDone()
246	for {
247		if !c.q.gate() {
248			return
249		}
250		err := c.display.Dispatch()
251		if err == nil || errors.Is(err, os.ErrDeadlineExceeded) {
252			continue // a pause request interrupted the read
253		}
254		if !c.q.isStopped() {
255			c.q.setFatal(fmt.Errorf("neferclient: read: %w", err))
256		}
257		return
258	}
259}
260
261// epollLoop is the second goroutine: it waits for watched descriptors and the
262// shutdown eventfd.
263func (c *Conn) epollLoop() {
264	defer close(c.epollDone)
265	var evs [8]unix.EpollEvent
266	for {
267		n, err := unix.EpollWait(c.epfd, evs[:], -1)
268		if err != nil {
269			if errors.Is(err, unix.EINTR) {
270				continue
271			}
272			c.q.setFatal(fmt.Errorf("neferclient: epoll wait: %w", err))
273			return
274		}
275		for i := range n {
276			if evs[i].Fd == int32(c.efd) {
277				return
278			}
279			e := event{kind: evFDReady, fd: evs[i].Fd}
280			c.q.post(&e)
281		}
282	}
283}
284
285// pause parks the reader between two reads and returns once it is parked.
286// Nested calls on the owner goroutine share the first pause. Before the reader
287// starts, the queue already parks everything and pause does nothing.
288func (c *Conn) pause() {
289	if !c.readerStarted {
290		return
291	}
292	c.pauseDepth++
293	if c.pauseDepth > 1 {
294		return
295	}
296	c.q.setPausing(true)
297	_ = c.sock.SetReadDeadline(time.Unix(1, 0)) // wakes a blocked read
298	c.q.awaitParked()
299	_ = c.sock.SetReadDeadline(time.Time{})
300}
301
302func (c *Conn) resume() {
303	if !c.readerStarted {
304		return
305	}
306	c.pauseDepth--
307	if c.pauseDepth == 0 {
308		c.q.endPause()
309	}
310}
311
312// Wake returns a channel with capacity 1 that is signaled whenever events are
313// queued: call [Conn.Dispatch] when it fires. A signal can be stale (the
314// events were already drained), so Dispatch may find nothing to do.
315func (c *Conn) Wake() <-chan struct{} { return c.q.wake }
316
317// Context returns the wlturbo context that generated proxies are created on.
318func (c *Conn) Context() *wl.Context { return c.wlctx }
319
320// Display returns the underlying wlturbo display. Only the owner goroutine
321// may call Dispatch or Roundtrip on it; use [Conn.Dispatch] and
322// [Conn.Roundtrip] instead.
323func (c *Conn) Display() *wl.Display { return c.display }
324
325// Dispatch applies the events the reader queued and calls h for each one that
326// matters to the application, on the calling goroutine. It handles at most one
327// ring of events per call and signals [Conn.Wake] again if more are pending.
328// A nil h discards the notifications but still applies the state changes. It
329// returns the transport error once the queue is drained after the connection
330// failed, and [ErrClosed] after Close.
331func (c *Conn) Dispatch(h Handler) error {
332	if c.closed {
333		return ErrClosed
334	}
335	if c.dispatching {
336		return errors.New("neferclient: Dispatch called from a Handler")
337	}
338	if h == nil {
339		h = NopHandler{}
340	}
341	c.dispatching = true
342	defer func() { c.dispatching = false }()
343	if err := c.seatErr; err != nil {
344		c.seatErr = nil
345		h.Error(err)
346	}
347	var ev event
348	for range ringSize {
349		if !c.q.pop(&ev) {
350			return c.q.fatalErr()
351		}
352		c.apply(&ev, h)
353		if c.closed {
354			return ErrClosed
355		}
356	}
357	if c.q.pending() {
358		c.q.signal()
359		return nil
360	}
361	return c.q.fatalErr()
362}
363
364// apply runs on the owner goroutine.
365func (c *Conn) apply(ev *event, h Handler) {
366	switch ev.kind {
367	case evGlobal:
368		if err := c.bindOutput(ev.global, ev.version); err != nil {
369			h.Error(err)
370		}
371	case evGlobalRemove:
372		c.removeOutput(ev.global, h)
373		c.seatGlobalRemoved(ev.global, h)
374	case evOutputName:
375		if o := c.entry(ev.global); o != nil {
376			o.nameLen = copy(o.nameBuf[:], ev.nameBytes())
377		}
378	case evOutputScale:
379		if o := c.entry(ev.global); o != nil && ev.a > 0 {
380			o.scale = ev.a
381		}
382	case evOutputMode:
383		if o := c.entry(ev.global); o != nil && ev.flags&1 != 0 { // current mode
384			o.width, o.height = ev.a, ev.b
385		}
386	case evOutputDone:
387		if o := c.entry(ev.global); o != nil {
388			c.commitOutput(o, h)
389		}
390	case evFDReady:
391		if c.seatTimer(ev.fd) {
392			c.seatRepeat(h)
393			return
394		}
395		id, ok := c.watched[ev.fd]
396		if !ok {
397			return // unwatched while the event was queued
398		}
399		h.FDReady(id)
400		if c.closed {
401			return // the handler closed the connection
402		}
403		if _, ok := c.watched[ev.fd]; ok { // the handler may have unwatched it
404			if err := c.armFD(int(ev.fd), unix.EPOLL_CTL_MOD); err != nil {
405				h.Error(err)
406			}
407		}
408	default:
409		if ev.kind >= evSeatCaps {
410			c.applySeat(ev, h)
411			return
412		}
413		c.applySurface(ev, h)
414	}
415}
416
417func (c *Conn) entry(global uint32) *outputEntry {
418	for _, o := range c.outputs {
419		if o.pub.Global == global {
420			return o
421		}
422	}
423	return nil
424}
425
426func (c *Conn) bindOutput(global, version uint32) error {
427	if version < 2 || c.entry(global) != nil { // v1 has no wl_output.done
428		return nil
429	}
430	o := &outputEntry{pub: Output{Global: global, Scale: 1}, scale: 1}
431	out := wayland.NewOutput(c.wlctx)
432	q := c.q
433	// Handlers go on before the bind request is sent, so no early event of the
434	// new object is lost. They run on the reader goroutine and only queue.
435	out.OnName(func(name string) {
436		e := event{kind: evOutputName, global: global}
437		e.setName(name)
438		q.post(&e)
439	})
440	out.OnScale(func(factor int32) {
441		e := event{kind: evOutputScale, global: global, a: factor}
442		q.post(&e)
443	})
444	out.OnMode(func(flags uint32, width, height, _ int32) {
445		e := event{kind: evOutputMode, global: global, flags: flags, a: width, b: height}
446		q.post(&e)
447	})
448	out.OnDone(func() {
449		e := event{kind: evOutputDone, global: global}
450		q.post(&e)
451	})
452	if err := c.display.Registry().Bind(global, wayland.OutputInterface, min(version, maxOutputVersion), out); err != nil {
453		c.wlctx.Unregister(out)
454		return fmt.Errorf("neferclient: bind wl_output %d: %w", global, err)
455	}
456	o.proxy = out
457	i := len(c.outputs)
458	c.outputs = append(c.outputs, o)
459	for ; i > 0 && c.outputs[i-1].pub.Global > global; i-- {
460		c.outputs[i], c.outputs[i-1] = c.outputs[i-1], c.outputs[i]
461	}
462	return nil
463}
464
465func (c *Conn) commitOutput(o *outputEntry, h Handler) {
466	if string(o.nameBuf[:o.nameLen]) != o.pub.Name {
467		o.pub.Name = string(o.nameBuf[:o.nameLen])
468	}
469	o.pub.Scale, o.pub.Width, o.pub.Height = o.scale, o.width, o.height
470	if !o.done {
471		o.done = true
472		if c.ready {
473			h.OutputAdded(&o.pub)
474		}
475	}
476}
477
478func (c *Conn) removeOutput(global uint32, h Handler) {
479	for i, o := range c.outputs {
480		if o.pub.Global != global {
481			continue
482		}
483		if err := o.proxy.Release(); err != nil {
484			c.wlctx.Unregister(o.proxy) // version 1 and 2 have no release
485		}
486		c.outputs = append(c.outputs[:i], c.outputs[i+1:]...)
487		if o.done {
488			h.OutputRemoved(global)
489		}
490		return
491	}
492}
493
494// Outputs returns the outputs that finished their first description, ordered
495// by Global. The slice is reused by the next call; owner goroutine only.
496func (c *Conn) Outputs() []Output {
497	c.view = c.view[:0]
498	for _, o := range c.outputs {
499		if o.done {
500			c.view = append(c.view, o.pub)
501		}
502	}
503	return c.view
504}
505
506// Roundtrip sends wl_display.sync and dispatches until it returns. The reader
507// is paused meanwhile; when Roundtrip returns, every event handled before the
508// sync reply is queued for [Conn.Dispatch] (up to the ring capacity; the rest
509// follows as the reader resumes).
510func (c *Conn) Roundtrip() error {
511	if c.closed {
512		return ErrClosed
513	}
514	c.pause()
515	defer c.resume()
516	if err := c.display.Roundtrip(); err != nil {
517		return fmt.Errorf("neferclient: roundtrip: %w", err)
518	}
519	return nil
520}
521
522// Bind binds the global named iface at min(announced, version) and returns the
523// proxy and the negotiated version. newProxy creates the proxy on the given
524// context; register its event handlers inside newProxy: the reader is paused
525// until Bind returns, but the bound object's first events can arrive right
526// after, so a handler added later may miss them. A missing global yields an
527// error matching [ErrGlobalNotFound].
528func (c *Conn) Bind[P wl.Proxy](iface string, version uint32, newProxy func(*wl.Context) P) (P, uint32, error) {
529	var zero P
530	if c.closed {
531		return zero, 0, ErrClosed
532	}
533	c.pause()
534	defer c.resume()
535	p := newProxy(c.wlctx)
536	v, err := c.display.Registry().BindNegotiated(iface, version, p)
537	if err != nil {
538		c.wlctx.Unregister(p)
539		return zero, 0, err
540	}
541	return p, v, nil
542}
543
544// WatchFD reports readability of fd through Handler.FDReady(id). The watch is
545// one-shot: it is re-armed after each FDReady call returns. fd stays owned by
546// the caller and must be unwatched before it is closed.
547func (c *Conn) WatchFD(fd int, id uint64) error {
548	if c.closed {
549		return ErrClosed
550	}
551	if _, ok := c.watched[int32(fd)]; ok {
552		return fmt.Errorf("neferclient: fd %d already watched", fd)
553	}
554	if err := c.armFD(fd, unix.EPOLL_CTL_ADD); err != nil {
555		return err
556	}
557	c.watched[int32(fd)] = id
558	return nil
559}
560
561// UnwatchFD stops watching fd. A notification already queued is dropped.
562func (c *Conn) UnwatchFD(fd int) error {
563	if c.closed {
564		return ErrClosed
565	}
566	if _, ok := c.watched[int32(fd)]; !ok {
567		return fmt.Errorf("neferclient: fd %d not watched", fd)
568	}
569	delete(c.watched, int32(fd))
570	if err := unix.EpollCtl(c.epfd, unix.EPOLL_CTL_DEL, fd, nil); err != nil {
571		return fmt.Errorf("neferclient: unwatch fd %d: %w", fd, err)
572	}
573	return nil
574}
575
576func (c *Conn) armFD(fd, op int) error {
577	ev := unix.EpollEvent{Events: unix.EPOLLIN | unix.EPOLLONESHOT, Fd: int32(fd)}
578	if err := unix.EpollCtl(c.epfd, op, fd, &ev); err != nil {
579		return fmt.Errorf("neferclient: watch fd %d: %w", fd, err)
580	}
581	return nil
582}
583
584// Close closes the connection and stops both goroutines. It never requests an
585// unlock or any other protocol shutdown; those are explicit calls. It is safe
586// to call more than once.
587func (c *Conn) Close() error {
588	if c == nil || c.closed {
589		return nil
590	}
591	c.closed = true
592	c.q.stop()
593	err := c.display.Close()
594	if c.readerStarted {
595		<-c.readerDone
596	}
597	if c.epollStart {
598		var one = [8]byte{1}
599		_, _ = unix.Write(c.efd, one[:])
600		<-c.epollDone
601	}
602	if c.epfd >= 0 {
603		_ = unix.Close(c.epfd)
604	}
605	if c.efd >= 0 {
606		_ = unix.Close(c.efd)
607	}
608	c.closeSeat()
609	c.q.closeFDs() // both producers are gone: close what nobody will drain
610	return err
611}

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 4

↑↓ moveenter openesc close