v0.3.0 96f3aeb

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

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