v0.3.1 4e89021

client.go RawHistoryFolder

1// Package wayland owns a standard-protocol control connection. It never owns
2// lock surfaces or grabs input; visual processes own ext-session-lock.
3package wayland
4
5import (
6	"context"
7	"errors"
8	"fmt"
9	"math"
10	"net"
11	"sort"
12	"time"
13
14	idlenotify "github.com/bnema/go-wayland-bindings/client/extidlenotify"
15	core "github.com/bnema/go-wayland-bindings/client/wayland"
16	outputpower "github.com/bnema/go-wayland-bindings/client/wlroutputpowermanagement"
17	"github.com/bnema/neferafk/internal/ports"
18	"github.com/bnema/wlturbo"
19	"github.com/bnema/zerowrap"
20)
21
22const queueSize = 64
23
24var ErrEventOverflow = errors.New("wayland: event queue overflow")
25
26type Options struct {
27	Requirements ports.WaylandRequirements
28	Deadlines    []time.Duration
29	Generation   uint64
30}
31type notice struct {
32	kind         uint8
33	notification *notification
34	output       *output
35	name         string
36	mode         uint32
37}
38type notification struct {
39	proxy                    *idlenotify.ExtIdleNotification
40	token, cycle, generation uint64
41	seat                     uint32
42	after                    time.Duration
43	active, wake, idled      bool
44}
45type seat struct {
46	proxy         *core.Seat
47	notifications map[time.Duration]*notification
48	wake          *notification
49}
50type output struct {
51	proxy       *core.Output
52	power       *outputpower.OutputPower
53	lifetime    uint64
54	global      uint32
55	name        string
56	off, failed bool
57	mode        uint32
58	modeKnown   bool
59}
60type Client struct {
61	d                *wlturbo.Display
62	options          Options
63	log              zerowrap.Logger
64	notices          chan notice
65	overflow         chan struct{}
66	seats            map[uint32]*seat
67	outputs          map[uint32]*output
68	idle             *idlenotify.ExtIdleNotifier
69	idleGlobal       uint32
70	power            *outputpower.OutputPowerManager
71	powerGlobal      uint32
72	deadlines        map[time.Duration]bool
73	generation, next uint64
74	dirty            bool
75}
76
77// New takes ownership of conn. Run is called exactly once; no other reader or
78// caller may use the display. Supplying an already connected socket permits
79// real socketpair tests without a replacement transport.
80func New(conn net.Conn, options Options, log zerowrap.Logger) (*Client, error) {
81	deadlines, err := normalize(options.Deadlines)
82	if err != nil {
83		_ = conn.Close()
84		return nil, err
85	}
86	d, err := wlturbo.ConnectFromConn(conn)
87	if err != nil {
88		_ = conn.Close()
89		return nil, err
90	}
91	c := &Client{d: d, options: options, log: log.WithField("component", "wayland"), notices: make(chan notice, queueSize), overflow: make(chan struct{}, 1), seats: map[uint32]*seat{}, outputs: map[uint32]*output{}, deadlines: deadlines, generation: options.Generation}
92	d.Registry().AddHandler("*", func(*wlturbo.Registry, uint32, uint32) { c.enqueue(notice{kind: 1}) })
93	d.Registry().AddGlobalRemoveHandler(c)
94	return c, nil
95}
96func normalize(ds []time.Duration) (map[time.Duration]bool, error) {
97	out := map[time.Duration]bool{}
98	for _, d := range ds {
99		if d == 0 {
100			continue
101		}
102		if d < 0 || d%time.Millisecond != 0 || d/time.Millisecond > math.MaxUint32 {
103			return nil, fmt.Errorf("wayland: deadline must be positive whole milliseconds within uint32: %s", d)
104		}
105		out[d] = true
106	}
107	return out, nil
108}
109func (c *Client) HandleRegistryGlobalRemove(wlturbo.RegistryGlobalRemoveEvent) {
110	c.enqueue(notice{kind: 1})
111}
112func (c *Client) enqueue(n notice) {
113	select {
114	case c.notices <- n:
115	default:
116		select {
117		case c.overflow <- struct{}{}:
118		default:
119		}
120		_ = c.d.Close()
121	}
122}
123
124// Run uses one dispatch reader; callbacks only enqueue values. All policy,
125// notification, output and capability state belongs to the Run goroutine.
126func (c *Client) Run(ctx context.Context, commands <-chan ports.WaylandCommand, events chan<- ports.WaylandEvent) error {
127	defer c.d.Close()
128	stop := context.AfterFunc(ctx, func() { _ = c.d.Close() })
129	defer stop()
130	if err := c.d.Roundtrip(); err != nil {
131		if ctx.Err() != nil {
132			return nil
133		}
134		return err
135	}
136	// Bootstrap announcements are represented by the registry snapshot.
137	for len(c.notices) > 0 {
138		<-c.notices
139	}
140	if err := c.reconcile(); err != nil {
141		return err
142	}
143	// Initial seat/output metadata and output-power mode arrive before doctor.
144	if err := c.d.Roundtrip(); err != nil {
145		if ctx.Err() != nil {
146			return nil
147		}
148		return err
149	}
150	for len(c.notices) > 0 {
151		if err := c.handle(ctx, events, <-c.notices); err != nil {
152			return err
153		}
154	}
155	done := make(chan error, 1)
156	proceed := make(chan struct{})
157	readerStop := make(chan struct{})
158	readerExited := make(chan struct{})
159	go func() {
160		defer close(readerExited)
161		for {
162			err := c.d.Dispatch()
163			select {
164			case done <- err:
165			case <-readerStop:
166				return
167			}
168			if err != nil {
169				return
170			}
171			select {
172			case <-proceed:
173			case <-readerStop:
174				return
175			}
176		}
177	}()
178	defer func() {
179		close(readerStop)
180		_ = c.d.Close()
181		<-readerExited
182	}()
183	// Callbacks only enqueue immutable proxy references; state stays here.
184	var pendingCommands []ports.WaylandCommand
185	for {
186		if c.dirty {
187			if err := c.emit(ctx, events, ports.WaylandCapabilities{Capabilities: c.capabilities()}); err != nil {
188				return nil
189			}
190			c.dirty = false
191		}
192		select {
193		case <-ctx.Done():
194			return nil
195		case <-c.overflow:
196			return ErrEventOverflow
197		case err := <-done:
198			if err != nil {
199				if ctx.Err() != nil {
200					return nil
201				}
202				select {
203				case <-c.overflow:
204					return ErrEventOverflow
205				default:
206				}
207				return err
208			}
209			for len(c.notices) > 0 {
210				if err := c.handle(ctx, events, <-c.notices); err != nil {
211					return err
212				}
213			}
214			for _, cmd := range pendingCommands {
215				if err := c.command(cmd); err != nil {
216					return err
217				}
218			}
219			pendingCommands = nil
220			select {
221			case proceed <- struct{}{}:
222			case <-ctx.Done():
223				return nil
224			}
225		case cmd, ok := <-commands:
226			if !ok {
227				commands = nil
228				continue
229			}
230			// Process any completed wire frame before this command. This keeps
231			// queued old-generation idle and registry removals ahead of reload.
232			if len(c.notices) > 0 {
233				if len(pendingCommands) == queueSize {
234					return ErrEventOverflow
235				}
236				pendingCommands = append(pendingCommands, cmd)
237			} else if err := c.command(cmd); err != nil {
238				return err
239			}
240		}
241	}
242}
243func (c *Client) emit(ctx context.Context, out chan<- ports.WaylandEvent, event ports.WaylandEvent) error {
244	select {
245	case out <- event:
246		return nil
247	case <-ctx.Done():
248		return ctx.Err()
249	}
250}
251func (c *Client) handle(ctx context.Context, out chan<- ports.WaylandEvent, n notice) error {
252	switch n.kind {
253	case 1:
254		return c.reconcile()
255	case 2, 3:
256		p := n.notification
257		if !p.active {
258			return nil
259		}
260		if n.kind == 2 {
261			if p.idled {
262				return nil
263			}
264			p.idled = true
265			if !p.wake {
266				return c.emit(ctx, out, ports.WaylandIdle{Generation: p.generation, Proxy: p.token, Cycle: p.cycle, Seat: p.seat, After: p.after})
267			}
268		}
269		if n.kind == 3 {
270			if !p.idled {
271				return nil
272			}
273			p.idled = false
274			p.cycle++
275			if p.wake {
276				if err := c.setPower(true); err != nil {
277					return err
278				}
279				return c.emit(ctx, out, ports.WaylandActivity{Proxy: p.token, Cycle: p.cycle, Seat: p.seat})
280			}
281		}
282	case 4:
283		o := n.output
284		if c.outputs[o.global] != o {
285			return nil
286		}
287		o.name = n.name
288		c.dirty = true
289	case 6:
290		o := n.output
291		if c.outputs[o.global] != o || o.failed {
292			return nil
293		}
294		o.mode, o.modeKnown = n.mode, true
295		if n.mode == outputpower.MODE_ON {
296			o.off = false
297		}
298	case 5:
299		o := n.output
300		if c.outputs[o.global] != o || o.failed {
301			return nil
302		}
303		c.log.Warn().Uint64("output_lifetime", o.lifetime).Msg("output power control failed")
304		o.failed = true
305		o.off = false
306		if o.power != nil {
307			if err := o.power.Destroy(); err != nil {
308				return err
309			}
310			o.power = nil
311		}
312		c.dirty = true
313		return c.emit(ctx, out, ports.WaylandPowerFailed{Output: o.lifetime})
314	}
315	return nil
316}
317func (c *Client) bind(g wlturbo.Global, supported uint32, p wlturbo.Proxy) error {
318	return c.d.Registry().Bind(g.Name, g.Interface, min(g.Version, supported), p)
319}
320func (c *Client) reconcile() error {
321	globals := c.d.Registry().GetGlobals()
322	find := func(iface string, minVersion uint32) wlturbo.Global {
323		var selected wlturbo.Global
324		for _, g := range globals {
325			if g.Interface == iface && g.Version >= minVersion && (selected.Name == 0 || g.Name < selected.Name) {
326				selected = g
327			}
328		}
329		return selected
330	}
331	idle := find(idlenotify.ExtIdleNotifierInterface, 2)
332	wantIdle := c.options.Requirements.Idle || c.options.Requirements.InputWake || len(c.deadlines) > 0
333	if !wantIdle {
334		idle = wlturbo.Global{}
335	}
336	if c.idleGlobal != idle.Name {
337		for _, s := range c.seats {
338			for _, p := range s.notifications {
339				if err := c.destroyNotification(p); err != nil {
340					return err
341				}
342			}
343			s.notifications = map[time.Duration]*notification{}
344			if err := c.destroyNotification(s.wake); err != nil {
345				return err
346			}
347			s.wake = nil
348		}
349		if c.idle != nil {
350			if err := c.idle.Destroy(); err != nil {
351				return err
352			}
353			c.idle = nil
354		}
355		c.idleGlobal = idle.Name
356		if idle.Name != 0 {
357			c.idle = idlenotify.NewExtIdleNotifier(c.d.Context())
358			if err := c.bind(idle, 2, c.idle); err != nil {
359				return err
360			}
361		}
362	}
363	power := find(outputpower.OutputPowerManagerInterface, 1)
364	if !c.options.Requirements.OutputPower {
365		power = wlturbo.Global{}
366	}
367	if c.powerGlobal != power.Name {
368		for _, o := range c.outputs {
369			if o.power != nil {
370				if o.off {
371					if err := o.power.SetMode(outputpower.MODE_ON); err != nil {
372						return err
373					}
374				}
375				if err := o.power.Destroy(); err != nil {
376					return err
377				}
378				o.power = nil
379			}
380			o.off = false
381			o.failed = false
382		}
383		if c.power != nil {
384			if err := c.power.Destroy(); err != nil {
385				return err
386			}
387			c.power = nil
388		}
389		c.powerGlobal = power.Name
390		if power.Name != 0 {
391			c.power = outputpower.NewOutputPowerManager(c.d.Context())
392			if err := c.bind(power, 1, c.power); err != nil {
393				return err
394			}
395		}
396	}
397	for name, s := range c.seats {
398		if g, ok := globals[name]; !ok || g.Interface != core.SeatInterface {
399			for _, p := range s.notifications {
400				if err := c.destroyNotification(p); err != nil {
401					return err
402				}
403			}
404			if err := c.destroyNotification(s.wake); err != nil {
405				return err
406			}
407			if s.proxy.Version() >= 5 {
408				if err := s.proxy.Release(); err != nil {
409					return err
410				}
411			}
412			delete(c.seats, name)
413		}
414	}
415	for name, o := range c.outputs {
416		if g, ok := globals[name]; !ok || g.Interface != core.OutputInterface {
417			if o.power != nil {
418				if err := o.power.Destroy(); err != nil {
419					return err
420				}
421			}
422			if o.proxy.Version() >= 3 {
423				if err := o.proxy.Release(); err != nil {
424					return err
425				}
426			}
427			delete(c.outputs, name)
428		}
429	}
430	for name, g := range globals {
431		if g.Interface == core.SeatInterface && c.seats[name] == nil {
432			p := core.NewSeat(c.d.Context())
433			if err := c.bind(g, 5, p); err != nil {
434				return err
435			}
436			c.seats[name] = &seat{proxy: p, notifications: map[time.Duration]*notification{}}
437		}
438		if g.Interface == core.OutputInterface && c.outputs[name] == nil {
439			c.next++
440			o := &output{global: name, lifetime: c.next, proxy: core.NewOutput(c.d.Context())}
441			o.proxy.OnName(func(s string) { c.enqueue(notice{kind: 4, output: o, name: s}) })
442			if err := c.bind(g, 4, o.proxy); err != nil {
443				return err
444			}
445			c.outputs[name] = o
446		}
447	}
448	for name, s := range c.seats {
449		for d, p := range s.notifications {
450			if !c.deadlines[d] {
451				if err := c.destroyNotification(p); err != nil {
452					return err
453				}
454				delete(s.notifications, d)
455			} else {
456				p.generation = c.generation
457			}
458		}
459		if c.idle != nil {
460			for d := range c.deadlines {
461				if s.notifications[d] == nil {
462					p, err := c.newNotification(name, s, d, false)
463					if err != nil {
464						return err
465					}
466					s.notifications[d] = p
467				}
468			}
469			if c.options.Requirements.InputWake && s.wake == nil {
470				p, err := c.newNotification(name, s, 0, true)
471				if err != nil {
472					return err
473				}
474				s.wake = p
475			}
476		}
477	}
478	for _, o := range c.outputs {
479		if c.power != nil && o.power == nil && !o.failed {
480			p, err := c.power.GetOutputPower(o.proxy)
481			if err != nil {
482				return err
483			}
484			o.power = p
485			p.OnMode(func(mode uint32) { c.enqueue(notice{kind: 6, output: o, mode: mode}) })
486			p.OnFailed(func() { c.enqueue(notice{kind: 5, output: o}) })
487		}
488	}
489	c.dirty = true
490	return nil
491}
492func (c *Client) newNotification(name uint32, s *seat, d time.Duration, wake bool) (*notification, error) {
493	var proxy *idlenotify.ExtIdleNotification
494	var err error
495	if wake {
496		proxy, err = c.idle.GetInputIdleNotification(0, s.proxy)
497	} else {
498		proxy, err = c.idle.GetIdleNotification(uint32(d/time.Millisecond), s.proxy)
499	}
500	if err != nil {
501		return nil, err
502	}
503	c.next++
504	p := &notification{proxy: proxy, token: c.next, cycle: 1, generation: c.generation, seat: name, after: d, active: true, wake: wake}
505	proxy.OnIdled(func() { c.enqueue(notice{kind: 2, notification: p}) })
506	proxy.OnResumed(func() { c.enqueue(notice{kind: 3, notification: p}) })
507	return p, nil
508}
509func (c *Client) destroyNotification(p *notification) error {
510	if p == nil {
511		return nil
512	}
513	p.active = false
514	return p.proxy.Destroy()
515}
516func (c *Client) setPower(on bool) error {
517	for _, o := range c.outputs {
518		if o.power == nil || o.failed {
519			continue
520		}
521		if on && !o.off {
522			continue
523		}
524		if !on && (o.off || o.modeKnown && o.mode == outputpower.MODE_OFF) {
525			continue // never claim an output somebody else already turned off
526		}
527		mode := uint32(outputpower.MODE_OFF)
528		if on {
529			mode = outputpower.MODE_ON
530		}
531		if err := o.power.SetMode(mode); err != nil {
532			return err
533		}
534		o.off = !on
535	}
536	return nil
537}
538func (c *Client) capabilities() ports.Capabilities {
539	result := ports.Capabilities{Globals: map[string]uint32{}}
540	for _, g := range c.d.Registry().GetGlobals() {
541		result.Globals[g.Interface] = max(result.Globals[g.Interface], g.Version)
542	}
543	for name := range c.seats {
544		result.Seats = append(result.Seats, name)
545	}
546	sort.Slice(result.Seats, func(i, j int) bool { return result.Seats[i] < result.Seats[j] })
547	for _, o := range c.outputs {
548		result.Outputs = append(result.Outputs, ports.WaylandOutput{Lifetime: o.lifetime, Global: o.global, Name: o.name, PowerSupported: o.power != nil && !o.failed})
549	}
550	sort.Slice(result.Outputs, func(i, j int) bool { return result.Outputs[i].Global < result.Outputs[j].Global })
551	require := func(enabled bool, name string, version uint32) {
552		if enabled && result.Globals[name] < version {
553			result.Missing = append(result.Missing, fmt.Sprintf("%s >= %d", name, version))
554		}
555	}
556	r := c.options.Requirements
557	require(r.Idle || r.InputWake || len(c.deadlines) > 0, idlenotify.ExtIdleNotifierInterface, 2)
558	require(r.Idle || r.InputWake || len(c.deadlines) > 0, core.SeatInterface, 1)
559	require(r.OutputPower, outputpower.OutputPowerManagerInterface, 1)
560	require(r.OutputPower || r.Visual || r.Lock, core.OutputInterface, 1)
561	if r.OutputPower {
562		for _, o := range c.outputs {
563			if o.failed {
564				result.Missing = append(result.Missing, fmt.Sprintf("%s output lifetime %d", outputpower.OutputPowerInterface, o.lifetime))
565			}
566		}
567	}
568	require(r.Lock, "ext_session_lock_manager_v1", 1)
569	require(r.Visual, "wl_compositor", 1)
570	require(r.Visual, "zwp_linux_dmabuf_v1", 4)
571	require(r.Visual, "wp_linux_drm_syncobj_manager_v1", 1)
572	sort.Strings(result.Missing)
573	return result
574}
575
576func (c *Client) command(cmd ports.WaylandCommand) error {
577	switch v := cmd.(type) {
578	case ports.SetDeadlines:
579		ds, err := normalize(v.Deadlines)
580		if err != nil {
581			return err
582		}
583		c.deadlines, c.generation = ds, v.Generation
584		return c.reconcile()
585	case ports.OutputPower:
586		return c.setPower(v.On)
587	}
588	return nil
589}

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 2

Tags 9

↑↓ moveenter openesc close