v0.4.0 9c230c3

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// zombie stands in for an object the client destroyed until the compositor
216// acknowledges the destruction with wl_display.delete_id. Events that were in
217// flight for it are discarded; their descriptors are closed.
218type zombie struct{ object Object }
219
220func (z *zombie) ID() uint32 { return z.object.ID() }
221
222// Register registers a proxy object
223func (c *Context) Register(proxy Proxy) {
224	if proxy != nil && proxy.ID() != 0 && !c.closed.Load() {
225		if proxy.Context() != c {
226			return
227		}
228		c.proxies.Store(proxy.ID(), proxy)
229		c.display.objects.Store(proxy.ID(), proxy)
230	}
231}
232
233// Unregister removes a proxy object
234// Only this proxy is removed: a zombie or a newer object that took the ID
235// is left in place.
236func (c *Context) Unregister(proxy Proxy) {
237	if proxy != nil {
238		c.proxies.CompareAndDelete(proxy.ID(), proxy)
239		c.display.objects.CompareAndDelete(proxy.ID(), proxy)
240	}
241}
242
243// UnregisterID removes a proxy object by ID (overloaded for compatibility)
244func (c *Context) UnregisterID(id uint32) {
245	c.proxies.Delete(id)
246	c.display.objects.Delete(id)
247}
248
249// AllocateID allocates a new object ID
250func (c *Context) AllocateID() uint32 {
251	return c.display.AllocateID()
252}
253
254// Close closes the context
255func (c *Context) Close() error {
256	c.closed.Store(true)
257	return c.display.Close()
258}
259
260// RunTill runs the event loop until the callback fires
261func (c *Context) RunTill(callback Object) error {
262	if c.closed.Load() {
263		return errors.New("context is closed")
264	}
265
266	// Special case: if callback is a sync object, do a roundtrip
267	if _, ok := callback.(*callbackObject); ok {
268		return c.display.Roundtrip()
269	}
270
271	// Otherwise, process events until the callback is unregistered
272	// This assumes the callback will unregister itself when done
273	for {
274		if err := c.display.Dispatch(); err != nil {
275			return err
276		}
277
278		// Check if callback is still registered
279		if _, ok := c.proxies.Load(callback.ID()); !ok {
280			// Callback has been unregistered, we're done
281			return nil
282		}
283	}
284}
285
286// BaseProxy methods
287
288// ID returns the proxy's object ID
289func (p *BaseProxy) ID() uint32 {
290	return p.id
291}
292
293// SetId sets the proxy's object ID
294func (p *BaseProxy) SetID(id uint32) {
295	p.id = id
296}
297
298// Context returns the proxy's context
299func (p *BaseProxy) Context() *Context {
300	return p.context
301}
302
303// SetContext sets the proxy's context
304func (p *BaseProxy) SetContext(ctx *Context) {
305	p.context = ctx
306}
307
308// Version returns the protocol version the object was bound or created at,
309// or 0 when it is unknown (for example after Registry.BindID).
310func (p *BaseProxy) Version() uint32 {
311	return p.version
312}
313
314// SetVersion records the object's protocol version. Registry.Bind sets it for
315// globals and generated requests copy it from parent to child.
316func (p *BaseProxy) SetVersion(v uint32) {
317	p.version = v
318}
319
320// ErrVersionTooLow reports a request the bound object version does not have.
321var ErrVersionTooLow = errors.New("wlturbo: request needs a newer object version")
322
323// CheckVersion rejects a request introduced in version since when the
324// object's known version is lower. An unknown version (0) is not checked.
325func CheckVersion(version, since uint32, request string) error {
326	if version != 0 && version < since {
327		return fmt.Errorf("%w: %s needs version %d, object has %d", ErrVersionTooLow, request, since, version)
328	}
329	return nil
330}
331
332// Dispatch default implementation (does nothing)
333func (p *BaseProxy) Dispatch(event *Event) {
334	// Default implementation does nothing
335}
336
337// Event methods for extracting data
338
339// Uint32 reads a uint32 from the event
340func (e *Event) Uint32() uint32 {
341	if e.offset+4 > len(e.data) {
342		return 0
343	}
344	val := binary.LittleEndian.Uint32(e.data[e.offset:])
345	e.offset += 4
346	return val
347}
348
349// Int32 reads an int32 from the event
350func (e *Event) Int32() int32 {
351	return int32(e.Uint32())
352}
353
354// Fixed reads a fixed-point value from the event
355func (e *Event) Fixed() Fixed {
356	return Fixed(e.Int32())
357}
358
359// String reads a string from the event
360func (e *Event) String() string {
361	if e.offset+4 > len(e.data) {
362		return ""
363	}
364	strlen := e.Uint32()
365	if strlen == 0 || e.offset+int(strlen) > len(e.data) {
366		return ""
367	}
368	// String includes null terminator in length
369	str := string(e.data[e.offset : e.offset+int(strlen)-1])
370	// Advance offset including padding
371	totalLen := strlen
372	padding := (4 - (totalLen % 4)) % 4
373	e.offset += int(totalLen + padding)
374	return str
375}
376
377// Array reads a byte array from the event
378func (e *Event) Array() []byte {
379	if e.offset+4 > len(e.data) {
380		return nil
381	}
382	arrlen := e.Uint32()
383	if arrlen == 0 || e.offset+int(arrlen) > len(e.data) {
384		return nil
385	}
386	arr := make([]byte, arrlen)
387	copy(arr, e.data[e.offset:e.offset+int(arrlen)])
388	// Advance offset including padding
389	padding := (4 - (arrlen % 4)) % 4
390	e.offset += int(arrlen + padding)
391	return arr
392}
393
394// OwnedFD owns one received descriptor. Close is idempotent; Take transfers
395// ownership to the caller (who must close it). Descriptor zero is valid.
396type OwnedFD struct {
397	mu sync.Mutex
398	fd int
399}
400
401func (f *OwnedFD) Take() (int, error) {
402	if f == nil {
403		return -1, errors.New("missing descriptor")
404	}
405	f.mu.Lock()
406	defer f.mu.Unlock()
407	if f.fd < 0 {
408		return -1, errors.New("descriptor already taken or closed")
409	}
410	n := f.fd
411	f.fd = -1
412	return n, nil
413}
414func (f *OwnedFD) Close() error {
415	if f == nil {
416		return nil
417	}
418	f.mu.Lock()
419	defer f.mu.Unlock()
420	if f.fd < 0 {
421		return nil
422	}
423	n := f.fd
424	f.fd = -1
425	return unix.Close(n)
426}
427
428// FD transfers ownership of the next descriptor assigned to this event.
429// An unclaimed descriptor is closed when dispatch completes.
430func (e *Event) FD() *OwnedFD {
431	if e.fdIndex >= len(e.fds) {
432		return nil
433	}
434	fd := e.fds[e.fdIndex]
435	e.fds[e.fdIndex] = nil
436	e.fdIndex++
437	return fd
438}
439
440// Fd is the legacy raw descriptor API; prefer FD for explicit ownership.
441func (e *Event) Fd() uintptr {
442	fd := e.FD()
443	if fd == nil {
444		_ = e.Uint32()
445		return 0
446	}
447	n, _ := fd.Take()
448	return uintptr(n)
449}
450
451// NewId reads a new object ID from the event
452func (e *Event) NewID() Proxy {
453	id := e.Uint32()
454	// Return a basic proxy with just the ID
455	return &BaseProxy{id: id}
456}
457
458// Proxy reads an existing proxy reference from the event
459func (e *Event) Proxy() Proxy {
460	id := e.Uint32()
461	// Would need context reference to look up actual proxy
462	return &BaseProxy{id: id}
463}
464
465// Registry handler interfaces for compatibility
466
467// RegistryGlobalHandler interface
468type RegistryGlobalHandler interface {
469	HandleRegistryGlobal(event RegistryGlobalEvent)
470}
471
472// RegistryGlobalRemoveHandler interface
473type RegistryGlobalRemoveHandler interface {
474	HandleRegistryGlobalRemove(event RegistryGlobalRemoveEvent)
475}
476
477// RegistryGlobalEvent represents a registry global announcement
478type RegistryGlobalEvent struct {
479	Registry  *Registry
480	Name      uint32
481	Interface string
482	Version   uint32
483}
484
485// RegistryGlobalRemoveEvent represents a registry global removal
486type RegistryGlobalRemoveEvent struct {
487	Registry *Registry
488	Name     uint32
489}
490
491// AddGlobalHandler adds a global handler to the registry
492func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {
493	if handler == nil {
494		return
495	}
496	r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {
497		if global, ok := r.FindGlobalByName(name); ok {
498			handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version})
499		}
500	})
501}
502
503// AddGlobalRemoveHandler registers a handler for registry removals.
504func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {
505	if handler == nil {
506		return
507	}
508	r.mu.Lock()
509	r.removeHandlers = append(r.removeHandlers, handler)
510	r.mu.Unlock()
511}
512
513// FindGlobalByName finds a global by its name ID
514func (r *Registry) FindGlobalByName(name uint32) (Global, bool) {
515	r.mu.RLock()
516	defer r.mu.RUnlock()
517
518	if global, ok := r.globals[name]; ok {
519		return global, true
520	}
521	return Global{}, false
522}
523
524// Display compatibility methods
525
526// Context returns a context for this display
527func (d *Display) Context() *Context {
528	return d.context
529}
530
531// GetRegistry returns the registry (compatibility)
532func (d *Display) GetRegistry() *Registry {
533	return d.registry
534}
535
536// Sync creates a sync callback
537func (d *Display) Sync() (Object, error) {
538	callbackID := d.allocateID()
539	callback := &callbackObject{
540		BaseProxy: BaseProxy{
541			context: d.context,
542			id:      callbackID,
543		},
544		display: d,
545	}
546
547	// Store before sending: the compositor may reply immediately.
548	d.context.Register(callback)
549	if err := d.SendRequest(1, 0, callbackID); err != nil {
550		d.context.Unregister(callback)
551		return nil, err
552	}
553
554	return callback, nil
555}

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