v0.2.0 13c6a90

server.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"fmt"
6	"os"
7	"sync"
8	"sync/atomic"
9	"time"
10
11	"github.com/bnema/go-wayland-bindings/server/relativepointer"
12
13	"github.com/bnema/go-wayland-bindings/server/fractionalscale"
14	"github.com/bnema/go-wayland-bindings/server/wayland"
15	"github.com/bnema/neferwl/internal/adapters/captureallow"
16	"github.com/bnema/neferwl/internal/adapters/clock"
17	"github.com/bnema/neferwl/internal/adapters/workspaceid"
18	"github.com/bnema/neferwl/internal/ports"
19	"github.com/bnema/neferwl/internal/sessionlock"
20	"github.com/bnema/purego-libwayland/server"
21	"github.com/bnema/zerowrap"
22	"golang.org/x/sys/unix"
23)
24
25// Options configures the server. Outputs is the initial layout; core
26// replaces it with ports.SetOutputs.
27type Options struct {
28	RuntimeDir string
29	Outputs    ports.Layout
30	// DMABuf is what the renderer imports; empty disables linux-dmabuf.
31	DMABuf ports.DMABufSupport
32	// SyncobjNode is the render node (/dev/dri/renderD*) timeline
33	// syncobjs are imported on; empty or without timeline support, no
34	// explicit sync is offered.
35	SyncobjNode             string
36	Keymap                  string
37	RepeatRate, RepeatDelay int
38	// Clock runs idle notification timers; nil is the system clock.
39	Clock ports.Clock
40	// Security is display-owned protection control. Nil leaves session-lock
41	// unadvertised; independent protocol tests remain unlocked.
42	Security ports.SessionSecurityController
43	// WorkspaceIDs names workspaces (ext-workspace id); nil draws a fresh
44	// launch prefix. The state file must share the one of the session.
45	WorkspaceIDs *workspaceid.IDs
46	// CaptureAllow is the executable allowlist for screen capture (captureauth.go);
47	// nil lets every client capture, unless the session is protected. The
48	// caller keeps it current (captureallow.Store.Run).
49	CaptureAllow *captureallow.Store
50	// syncDev replaces the render node's syncobj interface (tests).
51	syncDev syncobjDevice
52	// peer replaces the peer executable lookup (tests).
53	peer peerExe
54}
55
56// Channels carries client notifications and commands. Events may be unbuffered.
57type Channels struct {
58	// SecurityChanges requests ordered backend protection/release. The
59	// backend echoes actual inventory/proofs/barriers on SecurityEvents.
60	SecurityChanges chan<- ports.SecurityState
61	SecurityEvents  <-chan ports.SecurityBackendEvent
62	LeaseRequests   chan<- ports.LeaseMessage
63	LeaseEvents     <-chan ports.LeaseMessage
64	Events          chan<- ports.ClientEvent
65	Contents        chan<- ports.SurfaceContent
66	Commands        <-chan ports.ClientCommand
67	Workspaces      <-chan ports.Workspaces
68	// Cursors receives the cursor the client under the pointer asks for.
69	Cursors chan<- ports.CursorChange
70	// Presented paces frame callbacks on the outputs' page flips; outputs
71	// that do not flip (idle, headless) are paced at their refresh rate.
72	Presented <-chan ports.OutputPresented
73	// Captures carries accepted capture requests, never blocking. Each
74	// accepted request holds one of ports.MaxCaptureInflight credits until its
75	// CaptureDone arrives on Captured, whether or not the client is still
76	// there; the backend must complete every request it takes. In production
77	// both channels have capacity >= ports.MaxCaptureInflight, so replies
78	// never block the backend; standalone tests may use smaller channels.
79	Captures chan<- ports.CaptureRequest
80	Captured <-chan ports.CaptureDone
81	// OutputFormats are the outputs' direct scanout formats, offered in
82	// dmabuf feedback to fullscreen surfaces.
83	OutputFormats <-chan ports.OutputFormats
84	OutputHeads   <-chan ports.OutputHeads
85	OutputApply   chan<- ports.OutputApply
86	OutputApplied <-chan ports.OutputApplied
87}
88
89type Server struct {
90	security           ports.SessionSecurity
91	securityController ports.SessionSecurityController
92	securityPending    []ports.SecurityState // display-owned ordered backend outbox
93	securityWake       chan struct{}
94	sessionLock        *sessionLock
95	lockReadiness      sessionlock.Readiness
96	lockSurfaces       map[ports.WindowID]*lockSurface
97	leaseDevices       map[string]*leaseDevice
98	pendingLeases      map[uint64]*leaseObject
99	pendingLeaseCards  map[uint64]string
100	activeLeases       map[leaseKey]*leaseObject
101	leaseMu            sync.Mutex // only the outbound lease queue; never window state
102	leaseOut           []ports.LeaseMessage
103	leaseReady         chan struct{}
104	nextLeaseID        uint64
105	nextCapture        uint64
106	captureReplies     map[uint64]func(ports.CaptureDone)
107	// captureInflight counts accepted captures until the backend completes
108	// them, independent of client resources; only the display loop touches it.
109	captureInflight map[uint64]struct{}
110	captureSources  map[*server.Resource]*captureSource
111	captureSessions map[*captureSession]struct{}
112	// sessionsByID are the sessions registered with core (imagecopy.go);
113	// excl is the one live exclusion (exclusion.go); ownUID the compositor's.
114	sessionsByID map[uint64]*captureSession
115	nextSession  uint64
116	excl         *exclusion
117	ownUID       uint32
118	// captureAllow, peer and peers decide mayCapture (captureauth.go):
119	// peers keeps each client's pidfd until it is destroyed.
120	captureAllow *captureallow.Store
121	peer         peerExe
122	peers        map[server.Client]*peerID
123	// noPidfdWarned: the missing SO_PEERPIDFD is logged once.
124	noPidfdWarned bool
125	// sandboxed are the clients that connected through a security context
126	// (securitycontext.go); display goroutine only. securityStop is
127	// written once at shutdown to end the listeners, securityWG waits for
128	// them, securityLive counts them.
129	sandboxed    map[server.Client]*sandbox
130	securityStop int
131	securityWG   sync.WaitGroup
132	securityLive atomic.Int32
133	display      *server.Display
134	env          procEnv
135	slotsPending bool // core waits for a slot window
136	name         string
137	cleanup      func()
138	log          zerowrap.Logger
139	channels     Channels
140	// awaiting holds frame callbacks by output name, due at its next
141	// frame (frameDue, or its page flip).
142	awaiting    map[string][]*wayland.Callback
143	frameOwners map[*wayland.Callback]*surface
144	frameDue    map[string]time.Time
145	frameReady  chan struct{}
146	// Pacer scratch: periods is used on the display goroutine, while
147	// flipped and frameReports belong to pace and its synchronous Do call.
148	framePeriods map[string]time.Duration
149	flipped      map[string]bool
150	frameReports []ports.OutputPresented
151	// held buffers wait until no output reads them (release.go); reports
152	// are the outputs' latest ports.OutputPresented.
153	held    []heldBuffer
154	reports map[string]ports.OutputPresented
155	frames  uint64
156	started time.Time
157	// fifoSurfaces have a fifo barrier or queued commits (fifo.go);
158	// lastFlip is each output's latest page flip.
159	fifoSurfaces        map[*surface]struct{}
160	applyingGraph       bool
161	graphDrawn          bool
162	graphFeedback       []graphFeedback
163	readinessGeneration uint64
164	lastFlip            map[string]time.Time
165	// tokens are the issued xdg-activation tokens (activation.go).
166	tokens   map[string]activationToken
167	surfaces map[*server.Resource]*surface
168	buffers  map[*server.Resource]clientBuffer
169	dmabuf   *dmabufGlobal
170	// hdrOutputs records confirmed DRM modesets; absent outputs are SDR.
171	hdrOutputs        map[string]ports.OutputHDR
172	colorID           uint64
173	colorIdentity     map[string]uint64
174	colorDescriptions map[*server.Resource]colorDescription
175	colorOutputs      map[*colorOutput]struct{}
176	colorFeedbacks    map[*colorFeedback]struct{}
177	serial            uint32
178	ctx               context.Context
179	windows           map[ports.WindowID]*window
180	layers            map[ports.WindowID]*layerSurface
181	nextWindow        ports.WindowID
182	nextPool          uint64
183	nextSurface       uint64
184	// seat is the keyboard and pointer state (seat.go).
185	seat seatState
186	// eventMu protects only the notification queue, not display-owned window state.
187	// The event queue is unbounded by design: it grows only if core stops draining
188	// Events, which happens only at shutdown (core owns the receiving end). A
189	// stalled core is a bug surfaced by ctx cancellation, not a reason to block
190	// the display goroutine.
191	eventMu    sync.Mutex
192	events     []ports.ClientEvent
193	eventReady chan struct{}
194	contentMu  sync.Mutex
195	contents   map[ports.WindowID]ports.SurfaceContent
196	contentSeq map[ports.WindowID]uint64
197	// damage is each window's recent damage (contentMu).
198	damage map[ports.WindowID][]ports.SeqDamage
199	// feedbacks wait for the flip that shows their content (presentation.go).
200	feedbacks []feedbackWait
201	// Explicit sync (syncobj.go): the render node, imported timelines by
202	// resource, and the acquire point waiter.
203	syncDev   syncobjDevice
204	timelines map[*server.Resource]*timeline
205	syncWait  *syncWaiter
206	// flips is each output's latest flip (pacer, under Do).
207	flips map[string]ports.FlipInfo
208	// inhibitors are the live inhibitor objects (inhibit.go);
209	// shortcutWindows and idleWindows what core was told.
210	inhibitors                   []*inhibitor
211	shortcutWindows, idleWindows map[ports.WindowID]bool
212	// idleNotes and powers are the idle notifications and output power
213	// objects (idle.go); outputsOff the outputs core turned off.
214	idleNotes         []*idleNotification
215	clock             ports.Clock
216	powers            []*outputPower
217	outputsOff        map[string]bool
218	contentNotify     chan struct{}
219	contentReady      chan struct{}
220	outputs           []*output
221	outputManagers    []*outputManager
222	workspaceManagers []*workspaceManager
223	toplevelManagers  []*toplevelManager
224	workspaceSnapshot ports.Workspaces
225	workspaceFrames   []*workspaceFrame
226	workspaceIDs      *workspaceid.IDs
227	outputHeads       ports.OutputHeads
228	outputPlaces      ports.Layout
229	managementSerial  uint32
230	nextOutputApply   uint64
231	outputReplies     map[uint64]*outputConfiguration
232	focusedOutput     string
233	fractions         map[*surface]*fractionalscale.WpFractionalScaleV1
234	// cursorSurface is the wl_pointer.set_cursor surface in use.
235	cursorSurface *surface
236	// cursorMu guards only the latest cursor change for forwardCursors.
237	cursorMu     sync.Mutex
238	cursorLatest ports.CursorChange
239	cursorQueued bool
240	cursorReady  chan struct{}
241	// Clipboard and primary selection (clipboard.go).
242	selections                                  [2]*clipSource
243	clipDevices                                 []*clipDevice
244	dataSources, primarySources, controlSources map[*server.Resource]*clipSource
245	// Pointer constraints (constraints.go).
246	regions     map[*server.Resource]*region
247	relatives   map[server.Client][]*relativepointer.ZwpRelativePointerV1
248	constraints map[*surface]*constraint
249	constraint  *constraint // the active one
250	positioners map[*server.Resource]*positioner
251	// Text input and input method (textinput.go): activeText is the
252	// enabled text input the input method serves.
253	textInputs  []*textInput
254	activeText  *textInput
255	inputMethod *inputMethod
256}
257
258func removeItem[T comparable](list []T, v T) []T {
259	for i, x := range list {
260		if x == v {
261			return append(list[:i], list[i+1:]...)
262		}
263	}
264	return list
265}
266
267func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
268	if opts.RuntimeDir == "" {
269		opts.RuntimeDir = os.Getenv("XDG_RUNTIME_DIR")
270	}
271	if opts.RuntimeDir == "" {
272		return nil, fmt.Errorf("XDG_RUNTIME_DIR is empty")
273	}
274	d, err := server.NewDisplay()
275	if err != nil {
276		return nil, err
277	}
278	name, fd, cleanup, err := listen(opts.RuntimeDir)
279	if err != nil {
280		d.Close()
281		return nil, err
282	}
283	if err = d.AddSocketFD(fd); err != nil {
284		unix.Close(fd)
285		cleanup()
286		d.Close()
287		return nil, err
288	}
289	s := &Server{
290		securityWake:       make(chan struct{}, 1),
291		security:           opts.Security,
292		securityController: opts.Security,
293		display:            d,
294		awaiting:           map[string][]*wayland.Callback{},
295		frameDue:           map[string]time.Time{},
296		frameReady:         make(chan struct{}, 1),
297		reports:            map[string]ports.OutputPresented{},
298		env:                linuxProcEnv{},
299		name:               name,
300		cleanup:            cleanup,
301		log:                log,
302		channels:           ch,
303		surfaces:           make(map[*server.Resource]*surface),
304		buffers:            make(map[*server.Resource]clientBuffer),
305		windows:            make(map[ports.WindowID]*window),
306		layers:             make(map[ports.WindowID]*layerSurface),
307		nextWindow:         1,
308		eventReady:         make(chan struct{}, 1),
309		contents:           make(map[ports.WindowID]ports.SurfaceContent),
310		contentSeq:         make(map[ports.WindowID]uint64),
311		damage:             map[ports.WindowID][]ports.SeqDamage{},
312		contentReady:       make(chan struct{}, 1),
313		cursorReady:        make(chan struct{}, 1),
314		dataSources:        map[*server.Resource]*clipSource{},
315		primarySources:     map[*server.Resource]*clipSource{},
316		controlSources:     map[*server.Resource]*clipSource{},
317		contentNotify:      make(chan struct{}),
318		regions:            map[*server.Resource]*region{},
319		relatives:          map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
320		constraints:        map[*surface]*constraint{},
321		positioners:        map[*server.Resource]*positioner{},
322		seat:               seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
323	}
324	s.leaseDevices = map[string]*leaseDevice{}
325	s.pendingLeases = map[uint64]*leaseObject{}
326	s.pendingLeaseCards = map[uint64]string{}
327	s.activeLeases = map[leaseKey]*leaseObject{}
328	s.leaseReady = make(chan struct{}, 1)
329	s.workspaceIDs = opts.WorkspaceIDs
330	if s.workspaceIDs == nil {
331		s.workspaceIDs = workspaceid.New()
332	}
333	s.clock = opts.Clock
334	if s.clock == nil {
335		s.clock = clock.System{}
336	}
337	s.captureAllow, s.peer, s.peers = opts.CaptureAllow, opts.peer, map[server.Client]*peerID{}
338	if s.peer == nil {
339		s.peer = linuxPeerExe{}
340	}
341	s.sandboxed, s.securityStop = map[server.Client]*sandbox{}, -1
342	s.captureReplies = map[uint64]func(ports.CaptureDone){}
343	s.captureInflight = map[uint64]struct{}{}
344	s.outputReplies = map[uint64]*outputConfiguration{}
345	s.managementSerial = 1
346	s.captureSources = map[*server.Resource]*captureSource{}
347	s.sessionsByID = map[uint64]*captureSession{}
348	s.captureSessions = map[*captureSession]struct{}{}
349	s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
350	s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
351	s.flips = map[string]ports.FlipInfo{}
352	s.timelines = map[*server.Resource]*timeline{}
353	var node *renderNode
354	if opts.syncDev != nil {
355		s.syncDev = opts.syncDev
356		if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
357			cleanup()
358			d.Close()
359			return nil, err
360		}
361	} else if opts.SyncobjNode != "" {
362		if node, err = openSyncobj(opts.SyncobjNode); err != nil {
363			log.Info().Str("component", "wayland").Err(err).Msg("explicit sync off")
364			node = nil
365		} else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
366			node.close()
367			node = nil
368		} else {
369			s.syncDev = node
370		}
371	}
372	// Implicit dma-buf fences also need this waiter when explicit sync is off.
373	if s.syncWait == nil {
374		if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
375			cleanup()
376			d.Close()
377			return nil, err
378		}
379	}
380	s.colorIdentity = map[string]uint64{}
381	s.colorDescriptions = map[*server.Resource]colorDescription{}
382	s.colorOutputs = map[*colorOutput]struct{}{}
383	s.colorFeedbacks = map[*colorFeedback]struct{}{}
384	s.tokens = map[string]activationToken{}
385	if opts.Keymap != "" {
386		fd, size, e := keymapFile(opts.Keymap)
387		if e != nil {
388			cleanup()
389			d.Close()
390			return nil, e
391		}
392		s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap
393	}
394	s.syncWait.log = log
395	s.cleanup = func() {
396		if s.securityStop >= 0 {
397			unix.Close(s.securityStop)
398		}
399		if s.seat.keymapFD >= 0 {
400			unix.Close(s.seat.keymapFD)
401		}
402		s.dmabuf.close()
403		if s.syncWait != nil {
404			s.syncWait.close()
405		}
406		if node != nil {
407			node.close()
408		}
409		cleanup()
410	}
411	if s.securityStop, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
412		s.cleanup()
413		d.Close()
414		return nil, err
415	}
416	if err = registerGlobals(d, opts, s); err != nil {
417		s.cleanup()
418		d.Close()
419		return nil, err
420	}
421	for _, p := range opts.Outputs {
422		s.addOutput(p)
423	}
424	return s, nil
425}
426func (s *Server) SocketName() string { return s.name }
427func (s *Server) Run(ctx context.Context) error {
428	defer s.cleanup()
429	defer func() {
430		for _, device := range s.leaseDevices {
431			if device.fd != nil {
432				device.fd.Close()
433			}
434		}
435	}()
436	s.log.Info().Str("socket", s.name).Msg("starting wayland")
437	s.started = time.Now()
438	s.ctx = ctx
439	var wg sync.WaitGroup
440	wg.Add(14)
441	go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }()
442	go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }()
443	go func() { defer wg.Done(); s.forwardLeases(ctx) }()
444	go func() { defer wg.Done(); s.forwardLeaseRequests(ctx) }()
445	go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }()
446	go func() { defer wg.Done(); s.forward(ctx) }()
447	go func() { defer wg.Done(); s.forwardCursors(ctx) }()
448	go func() { defer wg.Done(); s.forwardContents(ctx) }()
449	go func() {
450		defer wg.Done()
451		for {
452			select {
453			case <-ctx.Done():
454				return
455			case <-s.display.Stopped():
456				return
457			case cmd, ok := <-s.channels.Commands:
458				if !ok {
459					return
460				}
461				if ctx.Err() != nil {
462					return
463				}
464				// One display round trip per batch: Do waits for the event
465				// loop, so a round trip per motion capped pointer events at
466				// ~150/s and let a backlog of stale motions build up.
467				batch, open := drainCommands(cmd, s.channels.Commands)
468				if !s.display.Do(func() {
469					for _, c := range batch {
470						if ctx.Err() != nil {
471							return
472						}
473						s.apply(c)
474					}
475				}) || !open {
476					return
477				}
478			}
479		}
480	}()
481	go func() { defer wg.Done(); s.pace(ctx) }()
482	go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
483	go func() { defer wg.Done(); s.forwardOutputHeads(ctx) }()
484	go func() { defer wg.Done(); s.forwardOutputApplied(ctx) }()
485	go func() { defer wg.Done(); s.forwardCaptured(ctx.Done()) }()
486	if s.syncWait != nil {
487		wg.Add(1)
488		go func() { defer wg.Done(); s.syncWait.run(ctx) }()
489	}
490	err := s.display.Run(ctx)
491	s.stopSecurityContexts()
492	wg.Wait()
493	return err
494}
495
496// forwardOutputHeads delivers backend inventory on the display owner goroutine.
497func (s *Server) forwardOutputHeads(ctx context.Context) {
498	for {
499		select {
500		case <-ctx.Done():
501			return
502		case <-s.display.Stopped():
503			return
504		case heads, ok := <-s.channels.OutputHeads:
505			if !ok {
506				return
507			}
508			if !s.display.Do(func() { s.setOutputHeads(heads) }) {
509				return
510			}
511		}
512	}
513}
514
515func (s *Server) forwardOutputApplied(ctx context.Context) {
516	for {
517		select {
518		case <-ctx.Done():
519			return
520		case <-s.display.Stopped():
521			return
522		case result, ok := <-s.channels.OutputApplied:
523			if !ok {
524				return
525			}
526			if !s.display.Do(func() {
527				c := s.outputReplies[result.ID]
528				delete(s.outputReplies, result.ID)
529				if c == nil || !c.res.Alive() {
530					return
531				}
532				if result.Err != nil {
533					c.res.SendFailed()
534				} else {
535					c.res.SendSucceeded()
536				}
537			}) {
538				return
539			}
540		}
541	}
542}
543
544// damageHistory is how many contents a window's damage history covers:
545// enough for a renderer target a few frames old.
546const damageHistory = 4
547
548// forwardOutputFormats applies the outputs' scanout formats on the
549// display goroutine.
550func (s *Server) forwardOutputFormats(ctx context.Context) {
551	for {
552		select {
553		case <-ctx.Done():
554			return
555		case <-s.display.Stopped():
556			return
557		case f, ok := <-s.channels.OutputFormats:
558			if !ok {
559				return
560			}
561			if !s.display.Do(func() {
562				s.setOutputHDR(f)
563				if s.dmabuf != nil {
564					s.dmabuf.setOutputFormats(f)
565				}
566			}) {
567				return
568			}
569		}
570	}
571}
572
573func (s *Server) emit(ev ports.ClientEvent) {
574	if s.channels.Events == nil {
575		return
576	}
577	s.eventMu.Lock()
578	s.events = append(s.events, ev)
579	s.eventMu.Unlock()
580	select {
581	case s.eventReady <- struct{}{}:
582	default:
583	}
584}
585
586func (s *Server) forward(ctx context.Context) {
587	for {
588		s.eventMu.Lock()
589		if len(s.events) == 0 {
590			s.eventMu.Unlock()
591			select {
592			case <-ctx.Done():
593				return
594			case <-s.eventReady:
595				continue
596			}
597		}
598		ev := s.events[0]
599		s.events[0] = nil
600		s.events = s.events[1:]
601		s.eventMu.Unlock()
602		select {
603		case <-ctx.Done():
604			return
605		case s.channels.Events <- ev:
606		}
607	}
608}
609
610func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
611	if s.channels.Contents == nil {
612		return
613	}
614	s.contentMu.Lock()
615	s.contentSeq[c.ID]++
616	c.Seq = s.contentSeq[c.ID]
617	// Copy-on-write: previous publications can still be in an output's hands.
618	old := s.damage[c.ID]
619	if len(old) >= damageHistory {
620		old = old[len(old)-damageHistory+1:]
621	}
622	h := make([]ports.SeqDamage, len(old)+1)
623	copy(h, old)
624	h[len(old)] = ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}
625	if c.Empty() {
626		delete(s.damage, c.ID)
627	} else {
628		s.damage[c.ID] = h
629	}
630	c.DamageHistory = h
631	s.contents[c.ID] = c
632	close(s.contentNotify)
633	s.contentNotify = make(chan struct{})
634	s.contentMu.Unlock()
635	select {
636	case s.contentReady <- struct{}{}:
637	default:
638	}
639}
640
641func (s *Server) forwardContents(ctx context.Context) {
642	for {
643		s.contentMu.Lock()
644		var id ports.WindowID
645		var c ports.SurfaceContent
646		found := false
647		var seq uint64
648		var notify <-chan struct{}
649		for id, c = range s.contents {
650			found = true
651			seq = s.contentSeq[id]
652			break
653		}
654		notify = s.contentNotify
655		s.contentMu.Unlock()
656		if !found {
657			select {
658			case <-ctx.Done():
659				return
660			case <-s.contentReady:
661				continue
662			}
663		}
664		// Do not hold a stale frame while the receiver is busy: wake on newer data.
665		select {
666		case <-ctx.Done():
667			return
668		case <-notify:
669			continue
670		case s.channels.Contents <- c:
671			s.contentMu.Lock()
672			if s.contentSeq[id] == seq {
673				delete(s.contents, id)
674			}
675			s.contentMu.Unlock()
676		}
677	}
678}
679
680// maxCommandBatch bounds how many queued commands one display round trip applies.
681const maxCommandBatch = 256
682
683// drainCommands returns first plus the commands already queued, with runs of
684// pointer motion for the same window collapsed into the latest one, their
685// relative deltas summed. open is
686// false once the channel is closed.
687func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (batch []ports.ClientCommand, open bool) {
688	batch = append(batch, first)
689	for len(batch) < maxCommandBatch {
690		select {
691		case c, ok := <-cmds:
692			if !ok {
693				return batch, false
694			}
695			if m, isMotion := c.(ports.PointerMotionTo); isMotion {
696				if last, lastMotion := batch[len(batch)-1].(ports.PointerMotionTo); lastMotion && last.ID == m.ID {
697					// The position is the latest; relative deltas add up so
698					// locked pointers (games) lose no movement.
699					m.DX += last.DX
700					m.DY += last.DY
701					m.UnaccelDX += last.UnaccelDX
702					m.UnaccelDY += last.UnaccelDY
703					batch[len(batch)-1] = m
704					continue
705				}
706			}
707			batch = append(batch, c)
708		default:
709			return batch, true
710		}
711	}
712	return batch, true
713}
714
715func (s *Server) apply(cmd ports.ClientCommand) {
716	if envelope, ok := cmd.(ports.SecurityCommand); ok {
717		if s.security == nil || envelope.State != s.security.Snapshot() {
718			return
719		}
720		cmd = envelope.Command
721	} else if s.security != nil && s.security.Snapshot().Generation != 0 {
722		// Only input/focus commands require an owner envelope. Config relay
723		// also produces SetKeymap and other epoch-independent commands.
724		switch cmd.(type) {
725		case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.FocusWindow, ports.ForwardKey:
726			return
727		}
728	}
729	switch c := cmd.(type) {
730	case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
731		s.applyInput(c)
732	case ports.ConfigurePopup:
733		if w := s.windows[c.ID]; w != nil && w.popup != nil {
734			w.popup.configure(c)
735		}
736	case ports.ClosePopup:
737		if w := s.windows[c.ID]; w != nil && w.popup != nil {
738			w.popup.dismiss()
739		}
740	case ports.ConfigureWindow:
741		w := s.windows[c.ID]
742		if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {
743			s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")
744			return
745		}
746		s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")
747		// Callbacks move when the tree stops or starts being throttled: on
748		// Visible or on Captured.
749		visibilityChanged := w.hasLast && (w.last.Visible != c.Visible || w.last.Captured != c.Captured)
750		scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible
751		w.last, w.hasLast = c, true
752		if visibilityChanged {
753			s.relocateCallbacks(w.xdg.surface)
754			s.dropFeedbacks(time.Now())
755			s.wakePacer() // reconsider FIFO barriers at the new period
756		}
757		w.sendConfigure()
758		s.toplevelChanged(w)
759		if scanoutChanged && w.xdg.surface != nil {
760			s.dmabuf.resendSurface(w.xdg.surface)
761		}
762		if surf := w.xdg.surface; surf != nil && c.Output != "" {
763			surf.sendTreeScale()
764		}
765		if scanoutChanged {
766			s.notifyColorFeedback()
767		}
768	case ports.SetOutputs:
769		s.setOutputs(c)
770		off := map[string]bool{}
771		for _, name := range c.Off {
772			off[name] = true
773		}
774		s.setOutputsOff(off)
775	case ports.CaptureSessionState:
776		s.captureState(c)
777	case ports.UserActivity:
778		s.userActivity()
779	case ports.SlotsPending:
780		s.slotsPending = c.Pending
781	case ports.ShortcutsInhibitState:
782		s.setShortcutsInhibit(c)
783	case ports.CloseWindow:
784		w := s.windows[c.ID]
785		if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() {
786			s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window")
787			return
788		}
789		w.toplevel.SendClose()
790	}
791}

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 23

Tags 10

↑↓ moveenter openesc close