v0.6.0 9be920f

client.go RawHistoryFolder

1// Package wlturbo is a Wayland client transport for Go: connection, framing,
2// descriptor passing, object lifecycle and the bootstrap registry. Generated
3// protocol bindings live in github.com/bnema/go-wayland-bindings (client/).
4package wlturbo
5
6import (
7	"bytes"
8	"encoding/binary"
9	"errors"
10	"fmt"
11	"io"
12	"net"
13	"os"
14	"path/filepath"
15	"strings"
16	"sync"
17	"sync/atomic"
18)
19
20// Pre-allocated buffer pool for performance
21var bufferPool = sync.Pool{
22	New: func() interface{} {
23		return &bytes.Buffer{}
24	},
25}
26
27// Fixed represents a 24.8 fixed-point number
28type Fixed int32
29
30// Float64 converts Fixed to float64
31func (f Fixed) Float64() float64 {
32	return float64(f) / 256.0
33}
34
35// NewFixed creates a Fixed from float64
36func NewFixed(v float64) Fixed {
37	return Fixed(v * 256.0)
38}
39
40// Object represents a Wayland object
41type Object interface {
42	ID() uint32
43}
44
45// Display represents a connection to the Wayland display
46type Display struct {
47	conn       net.Conn
48	unix       *net.UnixConn // set when conn is a Unix socket, nil otherwise
49	objects    sync.Map      // map[uint32]Object
50	nextID     uint32
51	idMu       sync.Mutex
52	freeIDs    []uint32
53	retiredIDs map[uint32]bool
54	sendMu     sync.Mutex
55	recvMu     sync.Mutex
56	dispatchMu sync.Mutex // serialize readers without holding receive state across handlers
57	signatures sync.Map
58
59	// Core objects
60	registry *Registry
61	context  *Context // Store context once
62
63	// Connection-local receive state. rbuf[rpos:] holds bytes read from the
64	// socket that have not been framed yet; recvBuf is the scratch buffer used
65	// for a single read. pendingFDs holds ancillary descriptors that arrived
66	// with those bytes and have not been consumed yet.
67	rbuf       []byte
68	rpos       int
69	recvBuf    []byte
70	readErr    error
71	maxMsgLen  uint32
72	closed     atomic.Bool
73	pendingFDs []int
74}
75
76// newDisplay builds a Display over an established connection.
77func newDisplay(conn net.Conn) *Display {
78	d := &Display{
79		conn:   conn,
80		nextID: 2, // 1 is reserved for wl_display
81	}
82	if uc, ok := conn.(*net.UnixConn); ok {
83		d.unix = uc
84	}
85	d.context = NewContext(d)
86	// Register display object (ID 1)
87	d.objects.Store(uint32(displayObjectID), d)
88	// Initialize registry
89	d.registry = &Registry{
90		id:       d.allocateID(),
91		display:  d,
92		globals:  make(map[uint32]Global),
93		handlers: make(map[string][]GlobalHandler),
94	}
95	d.objects.Store(d.registry.id, d.registry)
96	return d
97}
98
99// Registry represents the global registry
100type Registry struct {
101	id             uint32
102	display        *Display
103	globals        map[uint32]Global
104	mu             sync.RWMutex
105	handlers       map[string][]GlobalHandler
106	removeHandlers []RegistryGlobalRemoveHandler
107}
108
109// callbackObject represents a wl_callback object
110type callbackObject struct {
111	BaseProxy
112	display *Display
113}
114
115func (c *callbackObject) ID() uint32 {
116	return c.id
117}
118func (c *callbackObject) EventSignature(op uint16) (string, bool) {
119	if op == 0 {
120		return "uint,", true
121	}
122	return "", false
123}
124
125// Dispatch handles callback events (opcode 0 = done)
126func (c *callbackObject) Dispatch(event *Event) {
127	if event.Opcode != 0 { // done event
128		return
129	}
130	c.display.context.Unregister(c)
131}
132
133// Global represents a global object
134type Global struct {
135	Name      uint32
136	Interface string
137	Version   uint32
138}
139
140// GlobalHandler is called when a global is announced
141type GlobalHandler func(registry *Registry, name uint32, version uint32)
142
143// ConnectFromConn attaches to an established Wayland socket and requests its registry.
144func ConnectFromConn(conn net.Conn) (*Display, error) {
145	d := newDisplay(conn)
146	if err := d.getRegistry(); err != nil {
147		_ = d.Close()
148		return nil, fmt.Errorf("get registry: %w", err)
149	}
150	return d, nil
151}
152
153// Connect connects to the Wayland display and requests the registry.
154func Connect(socketPath string) (*Display, error) {
155	if socketPath == "" {
156		socketPath = os.Getenv("WAYLAND_DISPLAY")
157		if socketPath == "" {
158			socketPath = "wayland-0"
159		}
160	}
161
162	// Resolve socket path
163	if !filepath.IsAbs(socketPath) {
164		runDir := os.Getenv("XDG_RUNTIME_DIR")
165		if runDir == "" {
166			return nil, errors.New("XDG_RUNTIME_DIR not set")
167		}
168		socketPath = filepath.Join(runDir, socketPath)
169	}
170
171	conn, err := net.Dial("unix", socketPath)
172	if err != nil {
173		return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
174	}
175
176	return ConnectFromConn(conn)
177}
178
179// Close closes the display connection. It is safe to call more than once;
180// later calls are no-ops. After Close, Dispatch returns net.ErrClosed.
181func (d *Display) Close() error {
182	if !d.closed.CompareAndSwap(false, true) {
183		return nil
184	}
185	d.context.closed.Store(true)
186	err := d.conn.Close()
187	d.recvMu.Lock()
188	d.closePendingFDs()
189	d.recvMu.Unlock()
190	return err
191}
192
193// Closed reports whether the connection has been closed.
194func (d *Display) Closed() bool {
195	return d.closed.Load()
196}
197
198// ID returns the display's object ID (always 1)
199func (d *Display) ID() uint32 {
200	return 1
201}
202
203// allocateID allocates a new object ID
204func (d *Display) allocateID() uint32 {
205	d.idMu.Lock()
206	defer d.idMu.Unlock()
207	if n := len(d.freeIDs); n != 0 {
208		id := d.freeIDs[n-1]
209		d.freeIDs = d.freeIDs[:n-1]
210		delete(d.retiredIDs, id)
211		return id
212	}
213	if d.nextID == 0 || d.nextID >= 0xff000000 {
214		return 0 // exhausted; never allocate a reserved server ID
215	}
216	id := d.nextID
217	d.nextID++
218	return id
219}
220
221// AllocateID allocates a new object ID (public method)
222func (d *Display) AllocateID() uint32 {
223	return d.allocateID()
224}
225
226// SendRequest sends a request to the compositor
227func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{}) error {
228	return d.SendRequestWithFDs(objectID, opcode, nil, args...)
229}
230
231// SendRequestWithFDs sends a request with file descriptors
232func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {
233	return d.sendRequest(objectID, opcode, fds, nil, args, nil)
234}
235
236// sendRequest marshals a request, then runs guard and writes the message
237// under the send lock. Guard failures and marshaling errors write nothing.
238// The body is args (boxed, legacy path) followed by vals (typed path); a
239// caller uses one of the two.
240func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}, vals []Arg) error {
241	// Get buffer from pool
242	buf := bufferPool.Get().(*bytes.Buffer)
243	defer func() {
244		buf.Reset()
245		bufferPool.Put(buf)
246	}()
247
248	// Write header placeholder
249	var header [HeaderSize]byte
250	_, _ = buf.Write(header[:])
251
252	// Marshal arguments
253	for _, arg := range args {
254		if err := d.marshalArg(buf, arg); err != nil {
255			return fmt.Errorf("failed to marshal argument: %w", err)
256		}
257	}
258	for i := range vals {
259		if err := marshalValue(buf, &vals[i]); err != nil {
260			return fmt.Errorf("failed to marshal argument: %w", err)
261		}
262	}
263
264	// Update header with actual size in bytes
265	bufLen := buf.Len()
266	if bufLen > 0xFFFF || bufLen%4 != 0 {
267		return fmt.Errorf("invalid message size: %d bytes", bufLen)
268	}
269	size := uint32(bufLen) // Safe: checked above
270	binary.LittleEndian.PutUint32(header[0:4], objectID)
271	// Upper 16 bits = size, lower 16 bits = opcode
272	binary.LittleEndian.PutUint32(header[4:8], (size<<16)|uint32(opcode))
273
274	// Update buffer with correct header
275	data := buf.Bytes()
276	copy(data[0:8], header[:])
277
278	// Send message with optional file descriptors
279	return d.sendmsgWithFDs(data, fds, guard)
280}
281
282// putUint32 appends one little-endian wire word without allocating.
283func putUint32(buf *bytes.Buffer, v uint32) {
284	var w [4]byte
285	binary.LittleEndian.PutUint32(w[:], v)
286	_, _ = buf.Write(w[:])
287}
288
289// putString appends a wire string: length including the terminating NUL, the
290// bytes, the NUL, and padding to a 32-bit boundary.
291func putString(buf *bytes.Buffer, v string) error {
292	strlen := len(v) + 1
293	if strlen > 0xffff-HeaderSize {
294		return fmt.Errorf("string too long: %d bytes", strlen)
295	}
296	putUint32(buf, uint32(strlen)) // Safe: checked above
297	_, _ = buf.WriteString(v)
298	_ = buf.WriteByte(0)
299	for i := (4 - (strlen % 4)) % 4; i > 0; i-- {
300		_ = buf.WriteByte(0)
301	}
302	return nil
303}
304
305// putArray appends a wire array: length, the bytes and padding to a 32-bit
306// boundary.
307func putArray(buf *bytes.Buffer, v []byte) error {
308	arrlen := len(v)
309	if arrlen > 0xffff-HeaderSize {
310		return fmt.Errorf("array too long: %d bytes", arrlen)
311	}
312	putUint32(buf, uint32(arrlen))
313	_, _ = buf.Write(v)
314	for i := (4 - (arrlen % 4)) % 4; i > 0; i-- {
315		_ = buf.WriteByte(0)
316	}
317	return nil
318}
319
320// marshalValue marshals one typed argument. It shares the string and array
321// encoders with marshalArg, so both paths produce identical bytes.
322func marshalValue(buf *bytes.Buffer, a *Arg) error {
323	switch a.kind {
324	case argWord:
325		putUint32(buf, a.word)
326	case argString:
327		return putString(buf, a.str)
328	case argArray:
329		return putArray(buf, a.arr)
330	case argObject:
331		if a.obj != nil {
332			putUint32(buf, a.obj.ID())
333		} else {
334			putUint32(buf, 0)
335		}
336	case argFD:
337		// Carried by SCM_RIGHTS only; no body word.
338	default:
339		return errors.New("unsupported argument: zero or invalid Arg")
340	}
341	return nil
342}
343
344// marshalArg marshals a single argument
345func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
346	switch v := arg.(type) {
347	case uint32:
348		putUint32(buf, v)
349	case int32:
350		putUint32(buf, uint32(v))
351	case Fixed:
352		putUint32(buf, uint32(v))
353	case string:
354		return putString(buf, v)
355	case []byte:
356		return putArray(buf, v)
357	case Object:
358		if v != nil {
359			putUint32(buf, v.ID())
360		} else {
361			putUint32(buf, 0)
362		}
363	case nil:
364		// Null object
365		putUint32(buf, 0)
366	case uintptr:
367		// File descriptor as uintptr - NO placeholder in message for neurlang compatibility
368		// FD is ONLY sent via SCM_RIGHTS, not in the message body
369		return nil
370	default:
371		return fmt.Errorf("unsupported argument type: %T", arg)
372	}
373	return nil
374}
375
376// Dispatch reads one complete Wayland message and delivers it to the object
377// that owns it.
378//
379// Read boundaries never define message boundaries: a header split across
380// several reads and several messages arriving in one read are both handled.
381// Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
382// malformed frame, io.ErrUnexpectedEOF when the peer closes mid-frame, and
383// os.ErrDeadlineExceeded (not sticky) when a read deadline expires.
384//
385// Event handlers run while Dispatch holds the dispatch lock, so a handler must
386// not call Dispatch or Roundtrip on the same Display: that call deadlocks.
387// Hand work that needs a roundtrip to another goroutine instead.
388func (d *Display) Dispatch() error {
389	d.dispatchMu.Lock()
390	defer d.dispatchMu.Unlock()
391	d.recvMu.Lock()
392
393	if d.closed.Load() {
394		d.recvMu.Unlock()
395		return net.ErrClosed
396	}
397
398	frame, err := d.nextFrame()
399	d.recvMu.Unlock()
400	if d.closed.Load() {
401		return net.ErrClosed
402	}
403	if err != nil {
404		return d.fail(err)
405	}
406	return d.fail(d.dispatchFrame(frame))
407}
408
409// fail releases resources that cannot be used once the connection is
410// unusable. A protocol violation invalidates the whole connection, so queued
411// descriptors are closed; transport errors may be transient and keep them.
412func (d *Display) fail(err error) error {
413	if err == nil {
414		return nil
415	}
416	d.recvMu.Lock()
417	defer d.recvMu.Unlock()
418	var perr *ProtocolError
419	if errors.As(err, &perr) {
420		d.closePendingFDs()
421	}
422	if errors.Is(err, io.ErrUnexpectedEOF) {
423		d.closePendingFDs()
424	}
425	return err
426}
427
428// maxMessageSize reports the largest accepted message size.
429func (d *Display) maxMessageSize() uint32 {
430	if d.maxMsgLen == 0 {
431		return DefaultMaxMessageSize
432	}
433	return d.maxMsgLen
434}
435
436// nextFrame returns the next complete frame, reading more bytes as needed.
437func (d *Display) nextFrame() (receivedFrame, error) {
438	for {
439		buf := d.rbuf[d.rpos:]
440		if len(buf) >= HeaderSize {
441			object, opcode, size, err := parseHeader(buf[:HeaderSize], d.maxMessageSize())
442			if err != nil {
443				return receivedFrame{}, err
444			}
445			if uint32(len(buf)) >= size {
446				// The body aliases the receive buffer. It stays valid until
447				// the next fill, which only happens in a later Dispatch.
448				frame := receivedFrame{object: object, opcode: opcode}
449				if size > HeaderSize {
450					frame.body = buf[HeaderSize:size:size]
451				}
452				d.rpos += int(size)
453				return frame, nil
454			}
455		}
456		if err := d.fill(); err != nil {
457			if d.closed.Load() {
458				return receivedFrame{}, net.ErrClosed
459			}
460			return receivedFrame{}, err
461		}
462	}
463}
464
465// unread reports whether received bytes remain that have not been framed.
466func (d *Display) unread() bool { return d.rpos < len(d.rbuf) }
467
468// fill reads one chunk from the connection into the receive buffer. Bytes that
469// arrive together with an error are kept so they are still parsed before the
470// error surfaces.
471func (d *Display) fill() error {
472	if d.readErr != nil {
473		return d.readErr
474	}
475	if d.recvBuf == nil {
476		d.recvBuf = make([]byte, readChunkSize)
477	}
478	// Compact once per read rather than once per frame, so framing cost does
479	// not grow with the number of buffered messages.
480	if d.rpos > 0 {
481		d.rbuf = d.rbuf[:copy(d.rbuf, d.rbuf[d.rpos:])]
482		d.rpos = 0
483	}
484	n, err := d.readChunk()
485	if n > 0 {
486		d.rbuf = append(d.rbuf, d.recvBuf[:n]...)
487	}
488	if err != nil {
489		if errors.Is(err, io.EOF) {
490			err = io.ErrUnexpectedEOF
491		}
492		// A missed deadline is not a broken stream: report it without making
493		// it sticky, so a later Dispatch can read again.
494		if errors.Is(err, os.ErrDeadlineExceeded) {
495			return err
496		}
497		d.readErr = err
498		// A control-message failure corrupts the descriptor stream, so it is
499		// reported immediately instead of after the buffered bytes are used.
500		var perr *ProtocolError
501		if n == 0 || errors.As(err, &perr) {
502			return err
503		}
504	}
505	return nil
506}
507
508// signatureProvider is implemented by generated objects; every opcode must
509// have a known signature before any descriptor can be assigned or dispatched.
510type signatureProvider interface{ EventSignature(uint16) (string, bool) }
511
512// dispatchFrame delivers one complete frame with precisely its signature's FDs.
513func (d *Display) dispatchFrame(f receivedFrame) error {
514	d.recvMu.Lock()
515	obj, ev, err := d.prepareFrame(f)
516	d.recvMu.Unlock()
517	if err != nil {
518		return err
519	}
520	if obj == nil {
521		return d.handleDisplayEvent(f.opcode, f.body)
522	}
523	defer func() {
524		for _, fd := range ev.fds {
525			_ = fd.Close()
526		}
527		*ev = Event{}
528		eventPool.Put(ev)
529	}()
530	if _, isZombie := obj.(*zombie); isZombie {
531		return nil
532	}
533	// Another goroutine may have destroyed the object after prepareFrame
534	// looked it up; a destroyed object receives no further events.
535	if current, _ := d.objects.Load(f.object); current != obj {
536		return nil
537	}
538	if r, ok := obj.(eventReceiver); ok {
539		r.Dispatch(ev)
540	}
541	return nil
542}
543
544// eventReceiver is any registered object that handles its own events:
545// generated proxies, callbacks and the registry.
546type eventReceiver interface{ Dispatch(*Event) }
547
548// prepareFrame assigns owned descriptors under recvMu, but never calls user code.
549// A nil object with a nil error means the frame targets wl_display.
550func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, error) {
551	if d.closed.Load() {
552		return nil, nil, net.ErrClosed
553	}
554	if f.object == displayObjectID {
555		if len(d.pendingFDs) != 0 && !d.unread() {
556			return nil, nil, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
557		}
558		return nil, nil, nil
559	}
560	obj, ok := d.objects.Load(f.object)
561	if !ok {
562		return nil, nil, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
563	}
564	sigObj := obj
565	z, isZombie := obj.(*zombie)
566	if isZombie {
567		sigObj = z.object
568	}
569	sig, valid := "", false
570	p, isGenerated := sigObj.(signatureProvider)
571	if isGenerated {
572		sig, valid = p.EventSignature(f.opcode)
573	}
574	// Legacy custom proxies may declare their signatures with RegisterEventSignature.
575	if !valid && !isGenerated {
576		if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok {
577			sig, valid = v.(string), true
578		}
579	}
580	if !valid {
581		return nil, nil, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
582	}
583	if err := validateEventBody(sig, f.body); err != nil {
584		return nil, nil, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
585	}
586	count := strings.Count(sig, "fd,")
587	if len(d.pendingFDs) < count {
588		return nil, nil, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
589	}
590	ev := eventPool.Get().(*Event)
591	*ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
592	if count != 0 {
593		ev.fds = make([]*OwnedFD, count)
594		for i := range ev.fds {
595			ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]}
596		}
597		d.pendingFDs = d.pendingFDs[count:]
598	}
599	// FDs from a single recvmsg can accompany multiple coalesced frames. Once
600	// all buffered bytes have been framed, surplus descriptors are not legal.
601	if !d.unread() && len(d.pendingFDs) != 0 {
602		for _, fd := range ev.fds {
603			_ = fd.Close()
604		}
605		*ev = Event{}
606		eventPool.Put(ev)
607		return nil, nil, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
608	}
609	// For a zombie the descriptors were still consumed above so the stream
610	// stays aligned; dispatchFrame closes them without running any handler.
611	return obj.(Object), ev, nil
612}
613
614type signatureKey struct {
615	object uint32
616	opcode uint16
617}
618
619// RegisterEventSignature defines a signature for a legacy proxy. Generated
620// proxies supply it themselves and cannot be overridden.
621func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) {
622	if object == 0 || object == displayObjectID {
623		return
624	}
625	d.signatures.Store(signatureKey{object, opcode}, signature)
626}
627
628// handleDisplayEvent handles events on the display object
629func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
630	switch opcode {
631	case 0: // error
632		if err := validateEventBody("object,uint,string,", data); err != nil {
633			return &ProtocolError{Kind: "malformed_display_error", Err: err}
634		}
635		event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
636		objectID := event.Uint32()
637		code := event.Uint32()
638		return &DisplayError{ObjectID: objectID, Code: code, Message: event.String()}
639
640	case 1: // delete_id
641		if len(data) != 4 {
642			return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
643		}
644		id := binary.LittleEndian.Uint32(data[0:4])
645		d.idMu.Lock()
646		obj, live := d.objects.Load(id)
647		if _, isZombie := obj.(*zombie); isZombie {
648			d.objects.CompareAndDelete(id, obj)
649			live = false
650		}
651		if id < 2 || id >= 0xff000000 || id >= d.nextID || live || d.retiredIDs[id] {
652			d.idMu.Unlock()
653			return &ProtocolError{Kind: "invalid_delete_id", Object: id, Err: ErrMalformedFrame}
654		}
655		if d.retiredIDs == nil {
656			d.retiredIDs = make(map[uint32]bool)
657		}
658		d.retiredIDs[id] = true
659		d.freeIDs = append(d.freeIDs, id)
660		d.idMu.Unlock()
661		return nil
662
663	default:
664		return &ProtocolError{Kind: "unknown_opcode", Object: displayObjectID, Opcode: opcode, Err: ErrUnknownOpcode}
665	}
666}
667
668// Roundtrip waits for one wl_display.sync callback. Concurrent event loops
669// must not call Roundtrip while another goroutine dispatches this connection.
670func (d *Display) Roundtrip() error {
671	callback, err := d.Sync()
672	if err != nil {
673		return err
674	}
675	for {
676		if err = d.Dispatch(); err != nil {
677			return err
678		}
679		if _, exists := d.objects.Load(callback.ID()); !exists {
680			return nil
681		}
682	}
683}
684
685// getRegistry sends wl_display.get_registry for the bootstrap registry.
686func (d *Display) getRegistry() error {
687	return d.SendRequest(displayObjectID, 1, d.registry.id)
688}
689
690// Registry returns the global registry
691func (d *Display) Registry() *Registry {
692	return d.registry
693}
694
695// ID returns the registry's object ID
696func (r *Registry) ID() uint32 {
697	return r.id
698}
699
700// EventSignature reports the wl_registry event signatures.
701func (r *Registry) EventSignature(opcode uint16) (string, bool) {
702	switch opcode {
703	case 0:
704		return "uint,string,uint,", true
705	case 1:
706		return "uint,", true
707	}
708	return "", false
709}
710
711// Dispatch handles wl_registry.global and wl_registry.global_remove. The
712// transport has already validated the body against EventSignature.
713func (r *Registry) Dispatch(event *Event) {
714	switch event.Opcode {
715	case 0:
716		r.handleGlobal(event.Uint32(), event.String(), event.Uint32())
717	case 1:
718		r.handleGlobalRemove(event.Uint32())
719	}
720}
721
722func (r *Registry) handleGlobal(name uint32, iface string, version uint32) {
723	r.mu.Lock()
724	r.globals[name] = Global{Name: name, Interface: iface, Version: version}
725	handlers := append([]GlobalHandler(nil), r.handlers[iface]...)
726	handlers = append(handlers, r.handlers["*"]...)
727	r.mu.Unlock()
728	for _, handler := range handlers {
729		handler(r, name, version)
730	}
731}
732
733func (r *Registry) handleGlobalRemove(name uint32) {
734	r.mu.Lock()
735	delete(r.globals, name)
736	handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
737	r.mu.Unlock()
738	for _, handler := range handlers {
739		handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name})
740	}
741}
742
743// AddHandler adds a handler for a specific interface, or "*" for every
744// interface. Handlers accumulate; each announced global calls all of them in
745// registration order.
746func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
747	if handler == nil {
748		return
749	}
750	r.mu.Lock()
751	defer r.mu.Unlock()
752	r.handlers[iface] = append(r.handlers[iface], handler)
753}
754
755// ErrGlobalNotFound reports that the compositor does not announce a global.
756var ErrGlobalNotFound = errors.New("wlturbo: global not announced")
757
758// BindNegotiated binds the announced global for iface at
759// min(announced version, supported) and returns that version. It returns
760// ErrGlobalNotFound when the global is absent, so callers can decide whether
761// the capability is optional.
762func (r *Registry) BindNegotiated(iface string, supported uint32, proxy Proxy) (uint32, error) {
763	if supported == 0 {
764		return 0, fmt.Errorf("wlturbo: supported version for %s must be at least 1", iface)
765	}
766	g, ok := r.FindGlobal(iface)
767	if !ok || g.Version == 0 {
768		return 0, fmt.Errorf("%w: %s", ErrGlobalNotFound, iface)
769	}
770	version := min(g.Version, supported)
771	if err := r.Bind(g.Name, iface, version, proxy); err != nil {
772		return 0, err
773	}
774	return version, nil
775}
776
777// Bind binds to a global object and returns a typed proxy
778func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
779	// Set the ID if not already set
780	if proxy.ID() == 0 {
781		proxy.SetID(r.display.allocateID())
782	}
783
784	// Bootstrap a generated proxy with this display's context.
785	if proxy.Context() == nil {
786		if baseProxy, ok := proxy.(*BaseProxy); ok {
787			baseProxy.SetContext(r.display.Context())
788		} else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {
789			setter.SetContext(r.display.Context())
790		} else {
791			return fmt.Errorf("proxy doesn't have context and can't set it")
792		}
793	}
794
795	if proxy.Context() != r.display.Context() {
796		return fmt.Errorf("proxy belongs to another display")
797	}
798	if proxy.ID() == displayObjectID || proxy.ID() == r.id {
799		return fmt.Errorf("reserved object ID")
800	}
801	if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy {
802		return fmt.Errorf("object ID %d already registered", proxy.ID())
803	}
804	if v, ok := proxy.(interface{ SetVersion(uint32) }); ok {
805		v.SetVersion(version)
806	}
807	// Register the proxy
808	proxy.Context().Register(proxy)
809
810	// Send bind request (opcode 0) with proper arguments
811	if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
812		proxy.Context().Unregister(proxy)
813		return err
814	}
815
816	return nil
817}
818
819// BindID binds to a global object and returns just the ID (compatibility method)
820func (r *Registry) BindID(name uint32, iface string, version uint32) (uint32, error) {
821	newID := r.display.allocateID()
822
823	// Send bind request (opcode 0) with proper arguments
824	if err := r.display.SendRequest(r.id, 0, name, iface, version, newID); err != nil {
825		return 0, err
826	}
827
828	return newID, nil
829}
830
831// GetGlobals returns all announced globals
832func (r *Registry) GetGlobals() map[uint32]Global {
833	r.mu.RLock()
834	defer r.mu.RUnlock()
835
836	globals := make(map[uint32]Global)
837	for k, v := range r.globals {
838		globals[k] = v
839	}
840	return globals
841}
842
843// FindGlobal finds a global by interface name
844func (r *Registry) FindGlobal(iface string) (Global, bool) {
845	r.mu.RLock()
846	defer r.mu.RUnlock()
847
848	for _, global := range r.globals {
849		if global.Interface == iface {
850			return global, true
851		}
852	}
853	return Global{}, false
854}

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 8

↑↓ moveenter openesc close