main 7c2aaeb

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

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