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}