v0.5.1 31f302d

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

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