v0.8.0 446d1e3

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

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