v0.6.1 b45de7b

core.go RawHistoryFolder

1package wlturbo
2
3import (
4	"encoding/binary"
5	"errors"
6	"fmt"
7	"sync"
8	"sync/atomic"
9
10	"golang.org/x/sys/unix"
11)
12
13// Context provides a compatibility layer for wl.Context
14type Context struct {
15	display *Display
16	proxies sync.Map // map[uint32]Proxy
17	closed  atomic.Bool
18}
19
20// Proxy interface for Wayland protocol objects
21type Proxy interface {
22	Object
23	SetID(uint32)
24	Context() *Context
25	Dispatch(*Event)
26}
27
28// BaseProxy provides base implementation for protocol objects
29type BaseProxy struct {
30	id      uint32
31	context *Context
32	version uint32
33}
34
35// eventPool recycles Events across dispatches; an Event is only valid for
36// the duration of the handler call.
37var eventPool = sync.Pool{New: func() any { return &Event{} }}
38
39// Event represents a Wayland protocol event
40type Event struct {
41	ProxyID uint32
42	Opcode  uint16
43	data    []byte
44	offset  int
45	display *Display
46	fds     []*OwnedFD
47	fdIndex int
48}
49
50// Data returns the raw event body. It aliases the connection's receive buffer
51// and is only valid until the handler returns; copy it to keep it.
52func (e *Event) Data() []byte {
53	return e.data
54}
55
56// Offset returns the current read offset
57func (e *Event) Offset() int {
58	return e.offset
59}
60
61// NewContext creates a new context from a display
62func NewContext(display *Display) *Context {
63	return &Context{
64		display: display,
65	}
66}
67
68// SendRequest sends a request through the context. The proxy is validated
69// under the connection send lock, so no request reaches the wire after a
70// destructor for the same proxy.
71func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error {
72	return c.SendRequestWithFDs(proxy, opcode, nil, args...)
73}
74
75// SendRequestWithFDs sends a request with file descriptors through the context.
76// On error the FDs remain owned by the caller.
77func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
78	return c.sendChecked(proxy, opcode, fds, c.CheckProxy, args, nil)
79}
80
81// CheckProxy rejects stale and foreign proxies before any bytes are written.
82func (c *Context) CheckProxy(proxy Proxy) error {
83	if c.closed.Load() {
84		return errors.New("context is closed")
85	}
86	if proxy == nil || proxy.ID() == 0 || proxy.Context() != c {
87		return errors.New("invalid proxy")
88	}
89	registered, ok := c.proxies.Load(proxy.ID())
90	if !ok || registered != proxy {
91		return errors.New("proxy is not registered")
92	}
93	return nil
94}
95
96// Request describes one protocol request for Context.Request and
97// Context.RequestArgs.
98type Request struct {
99	Proxy      Proxy
100	Opcode     uint32
101	Name       string // interface.request, used in errors
102	Since      uint32 // version that introduced the request; 0 or 1 means always
103	Destructor bool   // claim the proxy exactly once, as SendDestructor does
104	Child      Proxy  // new_id object created by this request, or nil
105	FDs        []int  // descriptors to attach; closed after a successful send
106}
107
108// Request runs one request with its whole lifecycle: it checks the proxy and
109// version, allocates and registers Child (inheriting the parent's version),
110// sends, and then either closes the sent descriptors or, on error,
111// unregisters Child and leaves the descriptors with the caller. Child must
112// already have this context; it is passed in args at its wire position.
113//
114// Every argument is boxed into an interface, and a descriptor is a uintptr
115// placeholder that r.FDs is not checked against. Generated bindings use
116// RequestArgs instead, which marshals the same bytes without allocating.
117func (c *Context) Request(r Request, args ...interface{}) error {
118	return c.request(r, args, nil, false)
119}
120
121// RequestArgs is Request with typed arguments: same lifecycle, same wire
122// bytes, no per-argument interface boxing. Child, when set, must be passed as
123// an ArgObject at its wire position. The number of ArgFD markers must equal
124// len(r.FDs); a mismatch fails before the child is allocated, the send guard
125// runs or anything is written, and leaves the descriptors with the caller.
126func (c *Context) RequestArgs(r Request, args ...Arg) error {
127	return c.request(r, nil, args, true)
128}
129
130func (c *Context) request(r Request, args []interface{}, vals []Arg, typed bool) error {
131	if typed {
132		markers := 0
133		for i := range vals {
134			if vals[i].kind == argFD {
135				markers++
136			}
137		}
138		if markers != len(r.FDs) {
139			return fmt.Errorf("wlturbo: %s: %d fd arguments but %d descriptors", r.Name, markers, len(r.FDs))
140		}
141	}
142	if err := c.CheckProxy(r.Proxy); err != nil {
143		return err
144	}
145	if v, ok := r.Proxy.(interface{ Version() uint32 }); ok {
146		if err := CheckVersion(v.Version(), r.Since, r.Name); err != nil {
147			return err
148		}
149	}
150	if r.Child != nil {
151		r.Child.SetID(c.AllocateID())
152		if v, ok := r.Proxy.(interface{ Version() uint32 }); ok {
153			if s, ok := r.Child.(interface{ SetVersion(uint32) }); ok {
154				s.SetVersion(v.Version())
155			}
156		}
157		c.Register(r.Child)
158	}
159	var err error
160	if r.Destructor {
161		err = c.sendChecked(r.Proxy, r.Opcode, r.FDs, c.claimDestroy, args, vals)
162	} else {
163		err = c.sendChecked(r.Proxy, r.Opcode, r.FDs, c.CheckProxy, args, vals)
164	}
165	if err != nil {
166		if r.Child != nil {
167			c.Unregister(r.Child)
168		}
169		return err
170	}
171	for _, fd := range r.FDs {
172		_ = CloseSentFD(fd)
173	}
174	return nil
175}
176
177// sendChecked validates the proxy, then sends with check run under the send
178// lock (CheckProxy for plain requests, claimDestroy for destructors).
179func (c *Context) sendChecked(proxy Proxy, opcode uint32, fds []int, check func(Proxy) error, args []interface{}, vals []Arg) error {
180	if err := c.CheckProxy(proxy); err != nil {
181		return err
182	}
183	return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return check(proxy) }, args, vals)
184}
185
186// SendDestructor sends a destructor request exactly once. The proxy is
187// claimed (unregistered) under the connection send lock, after marshaling
188// and immediately before the write: concurrent destructors and later requests
189// on the proxy fail without writing. A marshaling error leaves the proxy
190// registered. A write error after the claim leaves the proxy unregistered;
191// such an error means the connection is broken and must be closed.
192func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error {
193	return c.SendDestructorWithFDs(proxy, opcode, nil, args...)
194}
195
196// SendDestructorWithFDs is SendDestructor for requests carrying FDs. On error
197// the FDs remain owned by the caller.
198func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
199	return c.sendChecked(proxy, opcode, fds, c.claimDestroy, args, nil)
200}
201
202func (c *Context) claimDestroy(proxy Proxy) error {
203	if err := c.CheckProxy(proxy); err != nil {
204		return err
205	}
206	if !c.proxies.CompareAndDelete(proxy.ID(), proxy) {
207		return errors.New("proxy is not registered")
208	}
209	// The compositor may already have sent events for this object. Keep a
210	// zombie until delete_id so those events are dropped, not fatal.
211	c.display.objects.CompareAndSwap(proxy.ID(), proxy, &zombie{object: proxy})
212	return nil
213}
214
215// Abandon stops delivering events to proxy without sending any request. Use
216// it for objects the compositor destroys itself, such as a wl_callback whose
217// done event may still be in flight: the object stays a zombie, so a late
218// event is discarded and the compositor's wl_display.delete_id retires the ID.
219// It returns an error when proxy is not registered on this context.
220func (c *Context) Abandon(proxy Proxy) error {
221	return c.claimDestroy(proxy)
222}
223
224// zombie stands in for an object the client destroyed until the compositor
225// acknowledges the destruction with wl_display.delete_id. Events that were in
226// flight for it are discarded; their descriptors are closed.
227type zombie struct{ object Object }
228
229func (z *zombie) ID() uint32 { return z.object.ID() }
230
231// Register registers a proxy object
232func (c *Context) Register(proxy Proxy) {
233	if proxy != nil && proxy.ID() != 0 && !c.closed.Load() {
234		if proxy.Context() != c {
235			return
236		}
237		c.proxies.Store(proxy.ID(), proxy)
238		c.display.objects.Store(proxy.ID(), proxy)
239	}
240}
241
242// Unregister removes a proxy object
243// Only this proxy is removed: a zombie or a newer object that took the ID
244// is left in place.
245func (c *Context) Unregister(proxy Proxy) {
246	if proxy != nil {
247		c.proxies.CompareAndDelete(proxy.ID(), proxy)
248		c.display.objects.CompareAndDelete(proxy.ID(), proxy)
249	}
250}
251
252// UnregisterID removes a proxy object by ID (overloaded for compatibility)
253func (c *Context) UnregisterID(id uint32) {
254	c.proxies.Delete(id)
255	c.display.objects.Delete(id)
256}
257
258// AllocateID allocates a new object ID
259func (c *Context) AllocateID() uint32 {
260	return c.display.AllocateID()
261}
262
263// Close closes the context
264func (c *Context) Close() error {
265	c.closed.Store(true)
266	return c.display.Close()
267}
268
269// RunTill runs the event loop until the callback fires
270func (c *Context) RunTill(callback Object) error {
271	if c.closed.Load() {
272		return errors.New("context is closed")
273	}
274
275	// Special case: if callback is a sync object, do a roundtrip
276	if _, ok := callback.(*callbackObject); ok {
277		return c.display.Roundtrip()
278	}
279
280	// Otherwise, process events until the callback is unregistered
281	// This assumes the callback will unregister itself when done
282	for {
283		if err := c.display.Dispatch(); err != nil {
284			return err
285		}
286
287		// Check if callback is still registered
288		if _, ok := c.proxies.Load(callback.ID()); !ok {
289			// Callback has been unregistered, we're done
290			return nil
291		}
292	}
293}
294
295// BaseProxy methods
296
297// ID returns the proxy's object ID
298func (p *BaseProxy) ID() uint32 {
299	return p.id
300}
301
302// SetId sets the proxy's object ID
303func (p *BaseProxy) SetID(id uint32) {
304	p.id = id
305}
306
307// Context returns the proxy's context
308func (p *BaseProxy) Context() *Context {
309	return p.context
310}
311
312// SetContext sets the proxy's context
313func (p *BaseProxy) SetContext(ctx *Context) {
314	p.context = ctx
315}
316
317// Version returns the protocol version the object was bound or created at,
318// or 0 when it is unknown (for example after Registry.BindID).
319func (p *BaseProxy) Version() uint32 {
320	return p.version
321}
322
323// SetVersion records the object's protocol version. Registry.Bind sets it for
324// globals and generated requests copy it from parent to child.
325func (p *BaseProxy) SetVersion(v uint32) {
326	p.version = v
327}
328
329// ErrVersionTooLow reports a request the bound object version does not have.
330var ErrVersionTooLow = errors.New("wlturbo: request needs a newer object version")
331
332// CheckVersion rejects a request introduced in version since when the
333// object's known version is lower. An unknown version (0) is not checked.
334func CheckVersion(version, since uint32, request string) error {
335	if version != 0 && version < since {
336		return fmt.Errorf("%w: %s needs version %d, object has %d", ErrVersionTooLow, request, since, version)
337	}
338	return nil
339}
340
341// Dispatch default implementation (does nothing)
342func (p *BaseProxy) Dispatch(event *Event) {
343	// Default implementation does nothing
344}
345
346// Event methods for extracting data
347
348// Uint32 reads a uint32 from the event
349func (e *Event) Uint32() uint32 {
350	if e.offset+4 > len(e.data) {
351		return 0
352	}
353	val := binary.LittleEndian.Uint32(e.data[e.offset:])
354	e.offset += 4
355	return val
356}
357
358// Int32 reads an int32 from the event
359func (e *Event) Int32() int32 {
360	return int32(e.Uint32())
361}
362
363// Fixed reads a fixed-point value from the event
364func (e *Event) Fixed() Fixed {
365	return Fixed(e.Int32())
366}
367
368// String reads a string from the event
369func (e *Event) String() string {
370	if e.offset+4 > len(e.data) {
371		return ""
372	}
373	strlen := e.Uint32()
374	if strlen == 0 || e.offset+int(strlen) > len(e.data) {
375		return ""
376	}
377	// String includes null terminator in length
378	str := string(e.data[e.offset : e.offset+int(strlen)-1])
379	// Advance offset including padding
380	totalLen := strlen
381	padding := (4 - (totalLen % 4)) % 4
382	e.offset += int(totalLen + padding)
383	return str
384}
385
386// Array reads a byte array from the event
387func (e *Event) Array() []byte {
388	if e.offset+4 > len(e.data) {
389		return nil
390	}
391	arrlen := e.Uint32()
392	if arrlen == 0 || e.offset+int(arrlen) > len(e.data) {
393		return nil
394	}
395	arr := make([]byte, arrlen)
396	copy(arr, e.data[e.offset:e.offset+int(arrlen)])
397	// Advance offset including padding
398	padding := (4 - (arrlen % 4)) % 4
399	e.offset += int(arrlen + padding)
400	return arr
401}
402
403// OwnedFD owns one received descriptor. Close is idempotent; Take transfers
404// ownership to the caller (who must close it). Descriptor zero is valid.
405type OwnedFD struct {
406	mu sync.Mutex
407	fd int
408}
409
410func (f *OwnedFD) Take() (int, error) {
411	if f == nil {
412		return -1, errors.New("missing descriptor")
413	}
414	f.mu.Lock()
415	defer f.mu.Unlock()
416	if f.fd < 0 {
417		return -1, errors.New("descriptor already taken or closed")
418	}
419	n := f.fd
420	f.fd = -1
421	return n, nil
422}
423func (f *OwnedFD) Close() error {
424	if f == nil {
425		return nil
426	}
427	f.mu.Lock()
428	defer f.mu.Unlock()
429	if f.fd < 0 {
430		return nil
431	}
432	n := f.fd
433	f.fd = -1
434	return unix.Close(n)
435}
436
437// FD transfers ownership of the next descriptor assigned to this event.
438// An unclaimed descriptor is closed when dispatch completes.
439func (e *Event) FD() *OwnedFD {
440	if e.fdIndex >= len(e.fds) {
441		return nil
442	}
443	fd := e.fds[e.fdIndex]
444	e.fds[e.fdIndex] = nil
445	e.fdIndex++
446	return fd
447}
448
449// Fd is the legacy raw descriptor API; prefer FD for explicit ownership.
450func (e *Event) Fd() uintptr {
451	fd := e.FD()
452	if fd == nil {
453		_ = e.Uint32()
454		return 0
455	}
456	n, _ := fd.Take()
457	return uintptr(n)
458}
459
460// NewId reads a new object ID from the event
461func (e *Event) NewID() Proxy {
462	id := e.Uint32()
463	// Return a basic proxy with just the ID
464	return &BaseProxy{id: id}
465}
466
467// Proxy reads an existing proxy reference from the event
468func (e *Event) Proxy() Proxy {
469	id := e.Uint32()
470	// Would need context reference to look up actual proxy
471	return &BaseProxy{id: id}
472}
473
474// Registry handler interfaces for compatibility
475
476// RegistryGlobalHandler interface
477type RegistryGlobalHandler interface {
478	HandleRegistryGlobal(event RegistryGlobalEvent)
479}
480
481// RegistryGlobalRemoveHandler interface
482type RegistryGlobalRemoveHandler interface {
483	HandleRegistryGlobalRemove(event RegistryGlobalRemoveEvent)
484}
485
486// RegistryGlobalEvent represents a registry global announcement
487type RegistryGlobalEvent struct {
488	Registry  *Registry
489	Name      uint32
490	Interface string
491	Version   uint32
492}
493
494// RegistryGlobalRemoveEvent represents a registry global removal
495type RegistryGlobalRemoveEvent struct {
496	Registry *Registry
497	Name     uint32
498}
499
500// AddGlobalHandler adds a global handler to the registry
501func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {
502	if handler == nil {
503		return
504	}
505	r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {
506		if global, ok := r.FindGlobalByName(name); ok {
507			handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version})
508		}
509	})
510}
511
512// AddGlobalRemoveHandler registers a handler for registry removals.
513func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {
514	if handler == nil {
515		return
516	}
517	r.mu.Lock()
518	r.removeHandlers = append(r.removeHandlers, handler)
519	r.mu.Unlock()
520}
521
522// FindGlobalByName finds a global by its name ID
523func (r *Registry) FindGlobalByName(name uint32) (Global, bool) {
524	r.mu.RLock()
525	defer r.mu.RUnlock()
526
527	if global, ok := r.globals[name]; ok {
528		return global, true
529	}
530	return Global{}, false
531}
532
533// Display compatibility methods
534
535// Context returns a context for this display
536func (d *Display) Context() *Context {
537	return d.context
538}
539
540// GetRegistry returns the registry (compatibility)
541func (d *Display) GetRegistry() *Registry {
542	return d.registry
543}
544
545// Sync creates a sync callback
546func (d *Display) Sync() (Object, error) {
547	callbackID := d.allocateID()
548	callback := &callbackObject{
549		BaseProxy: BaseProxy{
550			context: d.context,
551			id:      callbackID,
552		},
553		display: d,
554	}
555
556	// Store before sending: the compositor may reply immediately.
557	d.context.Register(callback)
558	if err := d.SendRequest(1, 0, callbackID); err != nil {
559		d.context.Unregister(callback)
560		return nil, err
561	}
562
563	return callback, nil
564}

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