v0.3.0 96f3aeb

client.go RawHistoryFolder

1// Package wlturbo is a Wayland client transport for Go: connection, framing,
2// descriptor passing, object lifecycle and the bootstrap registry. Protocol
3// bindings are generated into the protocol/ packages.
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)
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.
238func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}) error {
239	// Get buffer from pool
240	buf := bufferPool.Get().(*bytes.Buffer)
241	defer func() {
242		buf.Reset()
243		bufferPool.Put(buf)
244	}()
245
246	// Write header placeholder
247	var header [HeaderSize]byte
248	_, _ = buf.Write(header[:])
249
250	// Marshal arguments
251	for _, arg := range args {
252		if err := d.marshalArg(buf, arg); err != nil {
253			return fmt.Errorf("failed to marshal argument: %w", err)
254		}
255	}
256
257	// Update header with actual size in bytes
258	bufLen := buf.Len()
259	if bufLen > 0xFFFF || bufLen%4 != 0 {
260		return fmt.Errorf("invalid message size: %d bytes", bufLen)
261	}
262	size := uint32(bufLen) // Safe: checked above
263	binary.LittleEndian.PutUint32(header[0:4], objectID)
264	// Upper 16 bits = size, lower 16 bits = opcode
265	binary.LittleEndian.PutUint32(header[4:8], (size<<16)|uint32(opcode))
266
267	// Update buffer with correct header
268	data := buf.Bytes()
269	copy(data[0:8], header[:])
270
271	// Send message with optional file descriptors
272	return d.sendmsgWithFDs(data, fds, guard)
273}
274
275// putUint32 appends one little-endian wire word without allocating.
276func putUint32(buf *bytes.Buffer, v uint32) {
277	var w [4]byte
278	binary.LittleEndian.PutUint32(w[:], v)
279	_, _ = buf.Write(w[:])
280}
281
282// marshalArg marshals a single argument
283func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
284	switch v := arg.(type) {
285	case uint32:
286		putUint32(buf, v)
287	case int32:
288		putUint32(buf, uint32(v))
289	case Fixed:
290		putUint32(buf, uint32(v))
291	case string:
292		// String format: length (including null) + string + null + padding
293		strlen := len(v) + 1
294		if strlen > 0xffff-HeaderSize {
295			return fmt.Errorf("string too long: %d bytes", strlen)
296		}
297		putUint32(buf, uint32(strlen)) // Safe: checked above
298		_, _ = buf.WriteString(v)
299		_ = buf.WriteByte(0)
300		// Pad to 32-bit boundary
301		padding := (4 - (strlen % 4)) % 4
302		for i := 0; i < padding; i++ {
303			_ = buf.WriteByte(0)
304		}
305	case []byte:
306		// Array format: length + data + padding
307		arrlen := len(v)
308		if arrlen > 0xffff-HeaderSize {
309			return fmt.Errorf("array too long: %d bytes", arrlen)
310		}
311		putUint32(buf, uint32(arrlen))
312		_, _ = buf.Write(v)
313		// Pad to 32-bit boundary
314		padding := (4 - (arrlen % 4)) % 4
315		for i := 0; i < padding; i++ {
316			_ = buf.WriteByte(0)
317		}
318	case Object:
319		if v != nil {
320			putUint32(buf, v.ID())
321		} else {
322			putUint32(buf, 0)
323		}
324	case nil:
325		// Null object
326		putUint32(buf, 0)
327	case uintptr:
328		// File descriptor as uintptr - NO placeholder in message for neurlang compatibility
329		// FD is ONLY sent via SCM_RIGHTS, not in the message body
330		return nil
331	default:
332		return fmt.Errorf("unsupported argument type: %T", arg)
333	}
334	return nil
335}
336
337// Dispatch reads one complete Wayland message and delivers it to the object
338// that owns it.
339//
340// Read boundaries never define message boundaries: a header split across
341// several reads and several messages arriving in one read are both handled.
342// Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
343// malformed frame, io.ErrUnexpectedEOF when the peer closes mid-frame, and
344// os.ErrDeadlineExceeded (not sticky) when a read deadline expires.
345//
346// Event handlers run while Dispatch holds the dispatch lock, so a handler must
347// not call Dispatch or Roundtrip on the same Display: that call deadlocks.
348// Hand work that needs a roundtrip to another goroutine instead.
349func (d *Display) Dispatch() error {
350	d.dispatchMu.Lock()
351	defer d.dispatchMu.Unlock()
352	d.recvMu.Lock()
353
354	if d.closed.Load() {
355		d.recvMu.Unlock()
356		return net.ErrClosed
357	}
358
359	frame, err := d.nextFrame()
360	d.recvMu.Unlock()
361	if d.closed.Load() {
362		return net.ErrClosed
363	}
364	if err != nil {
365		return d.fail(err)
366	}
367	return d.fail(d.dispatchFrame(frame))
368}
369
370// fail releases resources that cannot be used once the connection is
371// unusable. A protocol violation invalidates the whole connection, so queued
372// descriptors are closed; transport errors may be transient and keep them.
373func (d *Display) fail(err error) error {
374	if err == nil {
375		return nil
376	}
377	d.recvMu.Lock()
378	defer d.recvMu.Unlock()
379	var perr *ProtocolError
380	if errors.As(err, &perr) {
381		d.closePendingFDs()
382	}
383	if errors.Is(err, io.ErrUnexpectedEOF) {
384		d.closePendingFDs()
385	}
386	return err
387}
388
389// maxMessageSize reports the largest accepted message size.
390func (d *Display) maxMessageSize() uint32 {
391	if d.maxMsgLen == 0 {
392		return DefaultMaxMessageSize
393	}
394	return d.maxMsgLen
395}
396
397// nextFrame returns the next complete frame, reading more bytes as needed.
398func (d *Display) nextFrame() (receivedFrame, error) {
399	for {
400		buf := d.rbuf[d.rpos:]
401		if len(buf) >= HeaderSize {
402			object, opcode, size, err := parseHeader(buf[:HeaderSize], d.maxMessageSize())
403			if err != nil {
404				return receivedFrame{}, err
405			}
406			if uint32(len(buf)) >= size {
407				// The body aliases the receive buffer. It stays valid until
408				// the next fill, which only happens in a later Dispatch.
409				frame := receivedFrame{object: object, opcode: opcode}
410				if size > HeaderSize {
411					frame.body = buf[HeaderSize:size:size]
412				}
413				d.rpos += int(size)
414				return frame, nil
415			}
416		}
417		if err := d.fill(); err != nil {
418			if d.closed.Load() {
419				return receivedFrame{}, net.ErrClosed
420			}
421			return receivedFrame{}, err
422		}
423	}
424}
425
426// unread reports whether received bytes remain that have not been framed.
427func (d *Display) unread() bool { return d.rpos < len(d.rbuf) }
428
429// fill reads one chunk from the connection into the receive buffer. Bytes that
430// arrive together with an error are kept so they are still parsed before the
431// error surfaces.
432func (d *Display) fill() error {
433	if d.readErr != nil {
434		return d.readErr
435	}
436	if d.recvBuf == nil {
437		d.recvBuf = make([]byte, readChunkSize)
438	}
439	// Compact once per read rather than once per frame, so framing cost does
440	// not grow with the number of buffered messages.
441	if d.rpos > 0 {
442		d.rbuf = d.rbuf[:copy(d.rbuf, d.rbuf[d.rpos:])]
443		d.rpos = 0
444	}
445	n, err := d.readChunk()
446	if n > 0 {
447		d.rbuf = append(d.rbuf, d.recvBuf[:n]...)
448	}
449	if err != nil {
450		if errors.Is(err, io.EOF) {
451			err = io.ErrUnexpectedEOF
452		}
453		// A missed deadline is not a broken stream: report it without making
454		// it sticky, so a later Dispatch can read again.
455		if errors.Is(err, os.ErrDeadlineExceeded) {
456			return err
457		}
458		d.readErr = err
459		// A control-message failure corrupts the descriptor stream, so it is
460		// reported immediately instead of after the buffered bytes are used.
461		var perr *ProtocolError
462		if n == 0 || errors.As(err, &perr) {
463			return err
464		}
465	}
466	return nil
467}
468
469// signatureProvider is implemented by generated objects; every opcode must
470// have a known signature before any descriptor can be assigned or dispatched.
471type signatureProvider interface{ EventSignature(uint16) (string, bool) }
472
473// dispatchFrame delivers one complete frame with precisely its signature's FDs.
474func (d *Display) dispatchFrame(f receivedFrame) error {
475	d.recvMu.Lock()
476	obj, ev, err := d.prepareFrame(f)
477	d.recvMu.Unlock()
478	if err != nil {
479		return err
480	}
481	if obj == nil {
482		return d.handleDisplayEvent(f.opcode, f.body)
483	}
484	defer func() {
485		for _, fd := range ev.fds {
486			_ = fd.Close()
487		}
488		*ev = Event{}
489		eventPool.Put(ev)
490	}()
491	if _, isZombie := obj.(*zombie); isZombie {
492		return nil
493	}
494	// Another goroutine may have destroyed the object after prepareFrame
495	// looked it up; a destroyed object receives no further events.
496	if current, _ := d.objects.Load(f.object); current != obj {
497		return nil
498	}
499	if r, ok := obj.(eventReceiver); ok {
500		r.Dispatch(ev)
501	}
502	return nil
503}
504
505// eventReceiver is any registered object that handles its own events:
506// generated proxies, callbacks and the registry.
507type eventReceiver interface{ Dispatch(*Event) }
508
509// prepareFrame assigns owned descriptors under recvMu, but never calls user code.
510// A nil object with a nil error means the frame targets wl_display.
511func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, error) {
512	if d.closed.Load() {
513		return nil, nil, net.ErrClosed
514	}
515	if f.object == displayObjectID {
516		if len(d.pendingFDs) != 0 && !d.unread() {
517			return nil, nil, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
518		}
519		return nil, nil, nil
520	}
521	obj, ok := d.objects.Load(f.object)
522	if !ok {
523		return nil, nil, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
524	}
525	sigObj := obj
526	z, isZombie := obj.(*zombie)
527	if isZombie {
528		sigObj = z.object
529	}
530	sig, valid := "", false
531	p, isGenerated := sigObj.(signatureProvider)
532	if isGenerated {
533		sig, valid = p.EventSignature(f.opcode)
534	}
535	// Legacy custom proxies may declare their signatures with RegisterEventSignature.
536	if !valid && !isGenerated {
537		if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok {
538			sig, valid = v.(string), true
539		}
540	}
541	if !valid {
542		return nil, nil, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
543	}
544	if err := validateEventBody(sig, f.body); err != nil {
545		return nil, nil, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
546	}
547	count := strings.Count(sig, "fd,")
548	if len(d.pendingFDs) < count {
549		return nil, nil, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
550	}
551	ev := eventPool.Get().(*Event)
552	*ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
553	if count != 0 {
554		ev.fds = make([]*OwnedFD, count)
555		for i := range ev.fds {
556			ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]}
557		}
558		d.pendingFDs = d.pendingFDs[count:]
559	}
560	// FDs from a single recvmsg can accompany multiple coalesced frames. Once
561	// all buffered bytes have been framed, surplus descriptors are not legal.
562	if !d.unread() && len(d.pendingFDs) != 0 {
563		for _, fd := range ev.fds {
564			_ = fd.Close()
565		}
566		*ev = Event{}
567		eventPool.Put(ev)
568		return nil, nil, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
569	}
570	// For a zombie the descriptors were still consumed above so the stream
571	// stays aligned; dispatchFrame closes them without running any handler.
572	return obj.(Object), ev, nil
573}
574
575type signatureKey struct {
576	object uint32
577	opcode uint16
578}
579
580// RegisterEventSignature defines a signature for a legacy proxy. Generated
581// proxies supply it themselves and cannot be overridden.
582func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) {
583	if object == 0 || object == displayObjectID {
584		return
585	}
586	d.signatures.Store(signatureKey{object, opcode}, signature)
587}
588
589// handleDisplayEvent handles events on the display object
590func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
591	switch opcode {
592	case 0: // error
593		if err := validateEventBody("object,uint,string,", data); err != nil {
594			return &ProtocolError{Kind: "malformed_display_error", Err: err}
595		}
596		event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
597		objectID := event.Uint32()
598		code := event.Uint32()
599		return &DisplayError{ObjectID: objectID, Code: code, Message: event.String()}
600
601	case 1: // delete_id
602		if len(data) != 4 {
603			return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
604		}
605		id := binary.LittleEndian.Uint32(data[0:4])
606		d.idMu.Lock()
607		obj, live := d.objects.Load(id)
608		if _, isZombie := obj.(*zombie); isZombie {
609			d.objects.CompareAndDelete(id, obj)
610			live = false
611		}
612		if id < 2 || id >= 0xff000000 || id >= d.nextID || live || d.retiredIDs[id] {
613			d.idMu.Unlock()
614			return &ProtocolError{Kind: "invalid_delete_id", Object: id, Err: ErrMalformedFrame}
615		}
616		if d.retiredIDs == nil {
617			d.retiredIDs = make(map[uint32]bool)
618		}
619		d.retiredIDs[id] = true
620		d.freeIDs = append(d.freeIDs, id)
621		d.idMu.Unlock()
622		return nil
623
624	default:
625		return &ProtocolError{Kind: "unknown_opcode", Object: displayObjectID, Opcode: opcode, Err: ErrUnknownOpcode}
626	}
627}
628
629// Roundtrip waits for one wl_display.sync callback. Concurrent event loops
630// must not call Roundtrip while another goroutine dispatches this connection.
631func (d *Display) Roundtrip() error {
632	callback, err := d.Sync()
633	if err != nil {
634		return err
635	}
636	for {
637		if err = d.Dispatch(); err != nil {
638			return err
639		}
640		if _, exists := d.objects.Load(callback.ID()); !exists {
641			return nil
642		}
643	}
644}
645
646// getRegistry sends wl_display.get_registry for the bootstrap registry.
647func (d *Display) getRegistry() error {
648	return d.SendRequest(displayObjectID, 1, d.registry.id)
649}
650
651// Registry returns the global registry
652func (d *Display) Registry() *Registry {
653	return d.registry
654}
655
656// ID returns the registry's object ID
657func (r *Registry) ID() uint32 {
658	return r.id
659}
660
661// EventSignature reports the wl_registry event signatures.
662func (r *Registry) EventSignature(opcode uint16) (string, bool) {
663	switch opcode {
664	case 0:
665		return "uint,string,uint,", true
666	case 1:
667		return "uint,", true
668	}
669	return "", false
670}
671
672// Dispatch handles wl_registry.global and wl_registry.global_remove. The
673// transport has already validated the body against EventSignature.
674func (r *Registry) Dispatch(event *Event) {
675	switch event.Opcode {
676	case 0:
677		r.handleGlobal(event.Uint32(), event.String(), event.Uint32())
678	case 1:
679		r.handleGlobalRemove(event.Uint32())
680	}
681}
682
683func (r *Registry) handleGlobal(name uint32, iface string, version uint32) {
684	r.mu.Lock()
685	r.globals[name] = Global{Name: name, Interface: iface, Version: version}
686	handlers := append([]GlobalHandler(nil), r.handlers[iface]...)
687	handlers = append(handlers, r.handlers["*"]...)
688	r.mu.Unlock()
689	for _, handler := range handlers {
690		handler(r, name, version)
691	}
692}
693
694func (r *Registry) handleGlobalRemove(name uint32) {
695	r.mu.Lock()
696	delete(r.globals, name)
697	handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
698	r.mu.Unlock()
699	for _, handler := range handlers {
700		handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name})
701	}
702}
703
704// AddHandler adds a handler for a specific interface, or "*" for every
705// interface. Handlers accumulate; each announced global calls all of them in
706// registration order.
707func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
708	if handler == nil {
709		return
710	}
711	r.mu.Lock()
712	defer r.mu.Unlock()
713	r.handlers[iface] = append(r.handlers[iface], handler)
714}
715
716// ErrGlobalNotFound reports that the compositor does not announce a global.
717var ErrGlobalNotFound = errors.New("wlturbo: global not announced")
718
719// BindNegotiated binds the announced global for iface at
720// min(announced version, supported) and returns that version. It returns
721// ErrGlobalNotFound when the global is absent, so callers can decide whether
722// the capability is optional.
723func (r *Registry) BindNegotiated(iface string, supported uint32, proxy Proxy) (uint32, error) {
724	if supported == 0 {
725		return 0, fmt.Errorf("wlturbo: supported version for %s must be at least 1", iface)
726	}
727	g, ok := r.FindGlobal(iface)
728	if !ok || g.Version == 0 {
729		return 0, fmt.Errorf("%w: %s", ErrGlobalNotFound, iface)
730	}
731	version := min(g.Version, supported)
732	if err := r.Bind(g.Name, iface, version, proxy); err != nil {
733		return 0, err
734	}
735	return version, nil
736}
737
738// Bind binds to a global object and returns a typed proxy
739func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
740	// Set the ID if not already set
741	if proxy.ID() == 0 {
742		proxy.SetID(r.display.allocateID())
743	}
744
745	// Bootstrap a generated proxy with this display's context.
746	if proxy.Context() == nil {
747		if baseProxy, ok := proxy.(*BaseProxy); ok {
748			baseProxy.SetContext(r.display.Context())
749		} else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {
750			setter.SetContext(r.display.Context())
751		} else {
752			return fmt.Errorf("proxy doesn't have context and can't set it")
753		}
754	}
755
756	if proxy.Context() != r.display.Context() {
757		return fmt.Errorf("proxy belongs to another display")
758	}
759	if proxy.ID() == displayObjectID || proxy.ID() == r.id {
760		return fmt.Errorf("reserved object ID")
761	}
762	if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy {
763		return fmt.Errorf("object ID %d already registered", proxy.ID())
764	}
765	if v, ok := proxy.(interface{ SetVersion(uint32) }); ok {
766		v.SetVersion(version)
767	}
768	// Register the proxy
769	proxy.Context().Register(proxy)
770
771	// Send bind request (opcode 0) with proper arguments
772	if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
773		proxy.Context().Unregister(proxy)
774		return err
775	}
776
777	return nil
778}
779
780// BindID binds to a global object and returns just the ID (compatibility method)
781func (r *Registry) BindID(name uint32, iface string, version uint32) (uint32, error) {
782	newID := r.display.allocateID()
783
784	// Send bind request (opcode 0) with proper arguments
785	if err := r.display.SendRequest(r.id, 0, name, iface, version, newID); err != nil {
786		return 0, err
787	}
788
789	return newID, nil
790}
791
792// GetGlobals returns all announced globals
793func (r *Registry) GetGlobals() map[uint32]Global {
794	r.mu.RLock()
795	defer r.mu.RUnlock()
796
797	globals := make(map[uint32]Global)
798	for k, v := range r.globals {
799		globals[k] = v
800	}
801	return globals
802}
803
804// FindGlobal finds a global by interface name
805func (r *Registry) FindGlobal(iface string) (Global, bool) {
806	r.mu.RLock()
807	defer r.mu.RUnlock()
808
809	for _, global := range r.globals {
810		if global.Interface == iface {
811			return global, true
812		}
813	}
814	return Global{}, false
815}

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