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

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 25

Tags 10

↑↓ moveenter openesc close