v0.4.0 2de4e1d

connection.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"context"
7	"errors"
8	"fmt"
9	"net"
10	"os"
11	"sort"
12	"sync"
13	"sync/atomic"
14	"time"
15
16	"github.com/bnema/wlturbo"
17	"github.com/bnema/wlturbo/protocol/core"
18	"github.com/bnema/wlturbo/protocol/extsessionlock"
19)
20
21// Connection owns one socket, one reader and one seat. All NewWindow, Apply,
22// Outputs, AcquireLock and Close calls belong to the same UI owner goroutine.
23// Reader callbacks post immutable events; they never mutate owner window state.
24type Connection struct {
25	Display     *wlturbo.Display
26	events      chan Event
27	stop        chan struct{}
28	done        chan struct{}
29	once        sync.Once
30	started     atomic.Bool
31	windows     map[uint32]*Window
32	routes      sync.Map // immutable surface->window identities published by owner
33	outputs     map[uint32]Output
34	seat        *Window // transport-only seat host, never a rendering window
35	lockManager *extsessionlock.ExtSessionLockManager
36	lock        *extsessionlock.ExtSessionLock
37	lockFinish  bool    // finished event applied
38	lockDone    bool    // locked event applied
39	held        []Event // lock/output events kept by drainStartup(true); owner only
40	closed      bool
41
42	// Reader pause gate. The owner pauses the reader to register handlers on
43	// freshly created objects (whose first events may arrive immediately) or to
44	// run its own roundtrip. While paused, post parks events instead of
45	// blocking, so neither side can wait on the bounded queue.
46	socket     net.Conn
47	gate       sync.Mutex
48	pausing    bool
49	pauseSig   chan struct{} // closed when a pause is requested
50	parked     []Event
51	pauseDepth int // owner goroutine only
52	pauseAck   chan struct{}
53	resume     chan struct{}
54}
55
56// Output identifies the registry lifetime, not merely a reusable monitor name.
57type Output struct {
58	Global uint32
59	Name   string
60	Scale  int32
61	proxy  *core.Output
62}
63
64func ConnectConnection(ctx context.Context, name string) (res *Connection, err error) {
65	if ctx == nil {
66		return nil, errors.New("nil context")
67	}
68	if err := ctx.Err(); err != nil {
69		return nil, err
70	}
71	path, err := waylandSocketPath(name)
72	if err != nil {
73		return nil, err
74	}
75	socket, err := (&net.Dialer{Timeout: dialTimeout}).DialContext(ctx, "unix", path)
76	if err != nil {
77		return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
78	}
79	display, err := wlturbo.ConnectFromConn(socket)
80	if err != nil {
81		return nil, err
82	}
83	c := &Connection{Display: display, socket: socket, events: make(chan Event, 256), stop: make(chan struct{}), windows: make(map[uint32]*Window), outputs: make(map[uint32]Output), pauseSig: make(chan struct{}), pauseAck: make(chan struct{}, 1), resume: make(chan struct{}, 1)}
84	watch := context.AfterFunc(ctx, func() { _ = display.Close() })
85	defer func() {
86		if !watch() && err == nil {
87			err = ctx.Err()
88		}
89		if err != nil {
90			_ = c.Close()
91			res = nil
92			if ctx.Err() != nil {
93				err = errors.Join(ctx.Err(), err)
94			}
95		}
96	}()
97	if err = display.Roundtrip(); err != nil {
98		return nil, err
99	}
100	c.seat = &Window{Display: display, connection: c, events: c.events, readerStop: c.stop}
101	if err = c.seat.BindSeat(); err != nil {
102		return nil, err
103	}
104	display.Registry().AddGlobalHandler(c)
105	display.Registry().AddGlobalRemoveHandler(c)
106	for name, g := range display.Registry().GetGlobals() {
107		if g.Interface == core.OutputInterface {
108			if err = c.bindOutput(name, g.Version); err != nil {
109				return nil, err
110			}
111		}
112	}
113	if err = display.Roundtrip(); err != nil {
114		return nil, err
115	}
116	if err = c.drainStartup(false); err != nil {
117		return nil, err
118	}
119	return c, nil
120}
121
122func (c *Connection) bindOutput(global, version uint32) error {
123	if version < 4 {
124		return &CapabilityError{Name: core.OutputInterface, Cause: fmt.Errorf("output names require version 4, announced %d", version)}
125	}
126	out := core.NewOutput(c.Display.Context())
127	if err := c.Display.Registry().Bind(global, core.OutputInterface, 4, out); err != nil {
128		c.Display.Context().Unregister(out)
129		return err
130	}
131	c.outputs[global] = Output{Global: global, Scale: 1, proxy: out}
132	out.OnName(func(name string) { c.post(Event{Kind: OutputNamed, Output: Output{Global: global, Name: name}}) })
133	out.OnScale(func(scale int32) { c.post(Event{Kind: OutputScale, Output: Output{Global: global, Scale: scale}}) })
134	out.OnDone(func() { c.post(Event{Kind: OutputAdded, Output: Output{Global: global}}) })
135	return nil
136}
137
138func (c *Connection) HandleRegistryGlobal(e wlturbo.RegistryGlobalEvent) {
139	if e.Interface == core.OutputInterface {
140		c.post(Event{Kind: OutputGlobal, Output: Output{Global: e.Name}, Version: e.Version})
141	}
142}
143func (c *Connection) HandleRegistryGlobalRemove(e wlturbo.RegistryGlobalRemoveEvent) {
144	c.post(Event{Kind: OutputRemoved, Output: Output{Global: e.Name}})
145}
146func (c *Connection) post(ev Event) {
147	c.gate.Lock()
148	if c.pausing {
149		c.parked = append(c.parked, ev)
150		c.gate.Unlock()
151		return
152	}
153	sig := c.pauseSig
154	c.gate.Unlock()
155	select {
156	case c.events <- ev:
157	case <-c.stop:
158		CloseEventFD(ev)
159	case <-sig:
160		c.gate.Lock()
161		c.parked = append(c.parked, ev)
162		c.gate.Unlock()
163	}
164}
165
166// pauseReader stops the reader between two Dispatch calls and returns once it
167// is parked. The nested calls of one owner goroutine share the first pause;
168// the returned func ends it. Without a running reader it does nothing.
169func (c *Connection) pauseReader() (resume func()) {
170	if !c.started.Load() || c.done == nil {
171		return func() {}
172	}
173	c.pauseDepth++
174	if c.pauseDepth > 1 {
175		return func() { c.pauseDepth-- }
176	}
177	c.gate.Lock()
178	c.pausing = true
179	close(c.pauseSig)
180	c.gate.Unlock()
181	if c.socket != nil {
182		_ = c.socket.SetReadDeadline(time.Unix(1, 0)) // wakes a blocked read
183	}
184	select {
185	case <-c.pauseAck:
186	case <-c.done:
187	}
188	if c.socket != nil {
189		_ = c.socket.SetReadDeadline(time.Time{})
190	}
191	return func() {
192		c.pauseDepth--
193		if c.pauseDepth > 0 {
194			return
195		}
196		c.gate.Lock()
197		c.pausing = false
198		c.pauseSig = make(chan struct{})
199		c.gate.Unlock()
200		select {
201		case c.resume <- struct{}{}:
202		default:
203		}
204	}
205}
206
207// flushParked re-posts events held while paused; reader goroutine only.
208func (c *Connection) flushParked() {
209	c.gate.Lock()
210	held := c.parked
211	c.parked = nil
212	c.gate.Unlock()
213	for _, ev := range held {
214		c.post(ev)
215	}
216}
217
218// ErrLockFinished is wrapped by platform errors when the compositor refused or
219// ended the session lock (ext_session_lock_v1.finished).
220var ErrLockFinished = errors.New("wayland: session lock finished")
221
222// holdDuringSetup reports events the lock owner must see through its own event
223// handling: lock state and output lifetimes. They are never applied by the
224// startup drain, which would hide them from the owner.
225func holdDuringSetup(k EventKind) bool {
226	switch k {
227	case LockAcquired, LockFinished, OutputGlobal, OutputNamed, OutputScale, OutputAdded, OutputRemoved:
228		return true
229	}
230	return false
231}
232
233// drainStartup applies queued events before the reader starts. With hold set
234// (window creation after the lock exists) lock and output events are kept, in
235// order, for TakeHeld instead of being applied; a held `finished` fails setup.
236func (c *Connection) drainStartup(hold bool) error {
237	for {
238		select {
239		case ev := <-c.events:
240			if hold && ev.Window == nil && holdDuringSetup(ev.Kind) {
241				c.held = append(c.held, ev)
242				if ev.Kind == LockFinished {
243					c.lockFinish = true // Close then sends the plain destroy
244					return ErrLockFinished
245				}
246				continue
247			}
248			if err := c.Apply(ev); err != nil {
249				return err
250			}
251		default:
252			return nil
253		}
254	}
255}
256
257// TakeHeld returns the lock and output events kept during window setup, oldest
258// first. The owner feeds them to its normal event handling right after it
259// starts the reader, so they precede everything the reader posts later.
260func (c *Connection) TakeHeld() []Event {
261	held := c.held
262	c.held = nil
263	return held
264}
265
266// Outputs returns a copy of owner-applied output lifetimes. Call after applying
267// OutputAdded/Removed events; names and scales are never read from the reader.
268func (c *Connection) Outputs() []Output {
269	result := make([]Output, 0, len(c.outputs))
270	for _, out := range c.outputs {
271		if out.Name != "" {
272			result = append(result, out)
273		}
274	}
275	sort.Slice(result, func(i, j int) bool { return result[i].Global < result[j].Global })
276	return result
277}
278
279// AcquireLock creates the sole lock object for this connection. Locked and
280// Finished are transport events, not an authentication or unlock policy.
281func (c *Connection) AcquireLock() error {
282	if c.closed || c.lock != nil {
283		return errors.New("connection closed or lock already acquired")
284	}
285	defer c.pauseReader()() // locked/finished may follow the request immediately
286	if c.lockManager == nil {
287		manager := extsessionlock.NewExtSessionLockManager(c.Display.Context())
288		if _, err := c.Display.Registry().BindNegotiated(extsessionlock.ExtSessionLockManagerInterface, 1, manager); err != nil {
289			c.Display.Context().Unregister(manager)
290			return &CapabilityError{Name: extsessionlock.ExtSessionLockManagerInterface, Cause: err}
291		}
292		c.lockManager = manager
293	}
294	lock, err := c.lockManager.Lock()
295	if err != nil {
296		return err
297	}
298	c.lock = lock
299	lock.OnLocked(func() { c.post(Event{Kind: LockAcquired}) })
300	lock.OnFinished(func() { c.post(Event{Kind: LockFinished}) })
301	if !c.started.Load() {
302		// The compositor answers with locked or finished immediately; queue it
303		// now so window setup can hold it for the owner (see drainStartup).
304		return c.Display.Roundtrip()
305	}
306	return nil
307}
308
309// Locked and Finished report the owner-applied lock state.
310func (c *Connection) Locked() bool   { return c.lockDone && !c.lockFinish }
311func (c *Connection) Finished() bool { return c.lockFinish }
312
313// UnlockAndDestroy sends unlock_and_destroy and roundtrips so the compositor
314// has processed it before the caller closes anything. It is only valid after
315// the locked event; the platform supplies no authorization policy. The reader
316// is paused for the roundtrip, so events it posts are parked, never lost.
317func (c *Connection) UnlockAndDestroy() error {
318	if c.lock == nil {
319		return errors.New("no session lock")
320	}
321	if !c.Locked() {
322		return errors.New("unlock before the locked event")
323	}
324	defer c.pauseReader()()
325	if err := c.lock.UnlockAndDestroy(); err != nil {
326		return err
327	}
328	c.lock, c.lockDone = nil, false
329	return c.Display.Roundtrip()
330}
331
332func (c *Connection) StartReader() <-chan Event {
333	c.once.Do(func() {
334		c.started.Store(true)
335		for _, w := range c.windows {
336			w.readerStarted.Store(true)
337		}
338		c.done = make(chan struct{})
339		go func() {
340			defer close(c.done)
341			for {
342				select {
343				case <-c.stop:
344					return
345				default:
346				}
347				c.gate.Lock()
348				paused := c.pausing
349				c.gate.Unlock()
350				if paused {
351					c.pauseAck <- struct{}{}
352					select {
353					case <-c.resume:
354						c.flushParked()
355						continue
356					case <-c.stop:
357						return
358					}
359				}
360				err := c.Display.Dispatch()
361				if errors.Is(err, os.ErrDeadlineExceeded) {
362					continue // a pause request interrupted the read
363				}
364				if err != nil {
365					c.post(Event{Kind: TransportError, Err: err})
366					return
367				}
368			}
369		}()
370	})
371	return c.events
372}
373func (c *Connection) Events() <-chan Event { return c.events }
374
375// Apply handles connection events or routes a tagged event to its window.
376func (c *Connection) Apply(ev Event) error {
377	if ev.Window != nil {
378		return ev.Window.Apply(ev)
379	}
380	switch ev.Kind {
381	case OutputGlobal:
382		if _, exists := c.outputs[ev.Output.Global]; !exists {
383			defer c.pauseReader()() // name/scale/done follow the bind
384			return c.bindOutput(ev.Output.Global, ev.Version)
385		}
386	case OutputNamed, OutputScale:
387		out, ok := c.outputs[ev.Output.Global]
388		if !ok {
389			return nil
390		}
391		if ev.Kind == OutputNamed {
392			out.Name = ev.Output.Name
393		} else if ev.Output.Scale > 0 {
394			out.Scale = ev.Output.Scale
395		}
396		c.outputs[out.Global] = out
397	case OutputAdded:
398	case OutputRemoved:
399		if out, ok := c.outputs[ev.Output.Global]; ok {
400			_ = out.proxy.Release()
401			delete(c.outputs, ev.Output.Global)
402		}
403	case SeatCapabilities, SeatRemoved:
404		oldPointer := c.seat.Pointer
405		defer c.pauseReader()() // keymap/capability events follow get_keyboard
406		if ev.Kind == SeatRemoved || (ev.Capabilities&1 == 0 && oldPointer != nil) {
407			// Shape devices die before the wl_pointer they were created from
408			// is released; their enter serials belonged to that pointer.
409			for _, w := range c.windows {
410				w.dropCursorDevice()
411			}
412		}
413		if err := c.seat.ApplySeat(ev); err != nil {
414			return err
415		}
416		for _, w := range c.windows {
417			if oldPointer != c.seat.Pointer {
418				w.dropCursorDevice()
419				w.queueInput([]Input{{Kind: "leave"}})
420			}
421			w.Seat, w.Pointer, w.WLKeyboard = c.seat.Seat, c.seat.Pointer, c.seat.WLKeyboard
422			if w.LockSurface != nil {
423				w.Seat = nil
424			}
425			w.pointerGen = c.seat.pointerGen
426			if ev.Kind == SeatRemoved || ev.Capabilities&2 == 0 {
427				w.ownerFocusOut()
428			}
429		}
430	case InputFocusOut:
431		for _, w := range c.windows {
432			w.ownerFocusOut()
433		}
434	case TransportError:
435		return ev.Err
436	case LockAcquired:
437		c.lockDone = true
438	case LockFinished:
439		c.lockFinish = true
440	default:
441		return fmt.Errorf("unrouted connection event %d", ev.Kind)
442	}
443	return nil
444}
445
446func (c *Connection) Close() error {
447	if c == nil || c.closed {
448		return nil
449	}
450	c.closed = true
451	close(c.stop)
452	for _, w := range c.windows {
453		_ = w.Close()
454	}
455	if c.lock != nil && c.lockFinish {
456		// Refused, or ended after locked: the compositor ended the lock, so a
457		// plain destroy is valid (unlock_and_destroy already cleared c.lock).
458		_ = c.lock.Destroy()
459	}
460	if c.lockManager != nil {
461		_ = c.lockManager.Destroy()
462	}
463	err := c.Display.Close() // closing an acquired owner never requests unlock
464	if c.done != nil {
465		<-c.done
466	}
467	if c.seat != nil {
468		_ = c.seat.releaseSeat()
469	}
470	for len(c.events) > 0 {
471		CloseEventFD(<-c.events)
472	}
473	for _, ev := range c.held {
474		CloseEventFD(ev)
475	}
476	c.held = nil
477	c.gate.Lock()
478	for _, ev := range c.parked {
479		CloseEventFD(ev)
480	}
481	c.parked = nil
482	c.gate.Unlock()
483	return err
484}

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 12

↑↓ moveenter openesc close