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}