v0.1.0 d9218d6

server.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"encoding/binary"
6	"fmt"
7	"github.com/bnema/purego-libwayland/protocol/relativepointer"
8	"os"
9	"slices"
10	"sync"
11	"time"
12
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/bnema/purego-libwayland/protocol/fractionalscale"
15	"github.com/bnema/purego-libwayland/protocol/wayland"
16	"github.com/bnema/purego-libwayland/server"
17	"github.com/bnema/zerowrap"
18	"golang.org/x/sys/unix"
19)
20
21// Options configures the server. Outputs is the initial layout; core
22// replaces it with ports.SetOutputs.
23type Options struct {
24	RuntimeDir string
25	Outputs    ports.Layout
26	// DMABuf is what the renderer imports; empty disables linux-dmabuf.
27	DMABuf ports.DMABufSupport
28	// SyncobjNode is the render node (/dev/dri/renderD*) timeline
29	// syncobjs are imported on; empty or without timeline support, no
30	// explicit sync is offered.
31	SyncobjNode             string
32	Keymap                  string
33	RepeatRate, RepeatDelay int
34	// syncDev replaces the render node's syncobj interface (tests).
35	syncDev syncobjDevice
36}
37
38// Channels carries client notifications and commands. Events may be unbuffered.
39type Channels struct {
40	Events   chan<- ports.ClientEvent
41	Contents chan<- ports.SurfaceContent
42	Commands <-chan ports.ClientCommand
43	// Cursors receives the cursor the client under the pointer asks for.
44	Cursors chan<- ports.CursorChange
45	// Presented paces frame callbacks on the outputs' page flips; outputs
46	// that do not flip (idle, headless) are paced at their refresh rate.
47	Presented <-chan ports.OutputPresented
48	// OutputFormats are the outputs' direct scanout formats, offered in
49	// dmabuf feedback to fullscreen surfaces.
50	OutputFormats <-chan ports.OutputFormats
51}
52type Server struct {
53	display      *server.Display
54	env          procEnv
55	slotsPending bool // core waits for a slot window
56	name         string
57	cleanup      func()
58	log          zerowrap.Logger
59	channels     Channels
60	// awaiting holds frame callbacks by output name, due at its next
61	// frame (frameDue, or its page flip).
62	awaiting   map[string][]*wayland.Callback
63	frameDue   map[string]time.Time
64	frameReady chan struct{}
65	// held buffers wait until no output reads them (release.go); reports
66	// are the outputs' latest ports.OutputPresented.
67	held    []heldBuffer
68	reports map[string]ports.OutputPresented
69	frames  uint64
70	started time.Time
71	// fifoSurfaces have a fifo barrier or queued commits (fifo.go);
72	// lastFlip is each output's latest page flip.
73	fifoSurfaces map[*surface]struct{}
74	lastFlip     map[string]time.Time
75	// tokens are the issued xdg-activation tokens (activation.go).
76	tokens   map[string]activationToken
77	surfaces map[*server.Resource]*surface
78	buffers  map[*server.Resource]clientBuffer
79	dmabuf   *dmabufGlobal
80	serial   uint32
81	// press is the serial of the last button or key press, sent to
82	// pressClient: popup grabs must come from it.
83	press       uint32
84	pressClient server.Client
85	// pressAt and focusAt date the last press and keyboard focus change,
86	// for xdg-activation tokens.
87	pressAt, focusAt        time.Time
88	ctx                     context.Context
89	windows                 map[ports.WindowID]*window
90	layers                  map[ports.WindowID]*layerSurface
91	nextWindow              ports.WindowID
92	nextPool                uint64
93	focused                 ports.WindowID
94	pointerFocus            ports.WindowID
95	wheelRest               [2]int32   // v120 not yet sent as axis_discrete, per axis
96	wheelHeld               [2]float64 // axis value held back with it for pre-v8 clients
97	pointerX, pointerY      float64
98	pointers                map[server.Client][]*wayland.Pointer
99	modState                ports.ModState
100	keyboards               map[server.Client][]*wayland.Keyboard
101	heldKeys                map[uint32]bool
102	keymapFD                int
103	keymapSize              uint32
104	keymapText              string           // the seat keymap, to compare virtual keymaps with
105	keymapOwner             *virtualKeyboard // nil: keyboards carry the seat keymap
106	repeatRate, repeatDelay int
107	// eventMu protects only the notification queue, not display-owned window state.
108	// The event queue is unbounded by design: it grows only if core stops draining
109	// Events, which happens only at shutdown (core owns the receiving end). A
110	// stalled core is a bug surfaced by ctx cancellation, not a reason to block
111	// the display goroutine.
112	eventMu    sync.Mutex
113	events     []ports.ClientEvent
114	eventReady chan struct{}
115	contentMu  sync.Mutex
116	contents   map[ports.WindowID]ports.SurfaceContent
117	contentSeq map[ports.WindowID]uint64
118	// damage is each window's recent damage (contentMu).
119	damage map[ports.WindowID][]ports.SeqDamage
120	// feedbacks wait for the flip that shows their content (presentation.go).
121	feedbacks []feedbackWait
122	// Explicit sync (syncobj.go): the render node, imported timelines by
123	// resource, and the acquire point waiter.
124	syncDev   syncobjDevice
125	timelines map[*server.Resource]*timeline
126	syncWait  *syncWaiter
127	// flips is each output's latest flip (pacer, under Do).
128	flips map[string]ports.FlipInfo
129	// inhibitors are the live inhibitor objects (inhibit.go);
130	// shortcutWindows and idleWindows what core was told.
131	inhibitors                   []*inhibitor
132	shortcutWindows, idleWindows map[ports.WindowID]bool
133	contentNotify                chan struct{}
134	contentReady                 chan struct{}
135	outputs                      []*output
136	focusedOutput                string
137	fractions                    map[*surface]*fractionalscale.WpFractionalScaleV1
138	// cursorSurface is the wl_pointer.set_cursor surface in use.
139	cursorSurface *surface
140	// cursorMu guards only the latest cursor change for forwardCursors.
141	cursorMu     sync.Mutex
142	cursorLatest ports.CursorChange
143	cursorQueued bool
144	cursorReady  chan struct{}
145	// Clipboard and primary selection (clipboard.go).
146	selections                                  [2]*clipSource
147	clipDevices                                 []*clipDevice
148	dataSources, primarySources, controlSources map[*server.Resource]*clipSource
149	// Pointer constraints (constraints.go).
150	regions     map[*server.Resource]*region
151	relatives   map[server.Client][]*relativepointer.ZwpRelativePointerV1
152	constraints map[*surface]*constraint
153	constraint  *constraint // the active one
154	positioners map[*server.Resource]*positioner
155}
156
157func removeItem[T comparable](list []T, v T) []T {
158	for i, x := range list {
159		if x == v {
160			return append(list[:i], list[i+1:]...)
161		}
162	}
163	return list
164}
165
166func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
167	if opts.RuntimeDir == "" {
168		opts.RuntimeDir = os.Getenv("XDG_RUNTIME_DIR")
169	}
170	if opts.RuntimeDir == "" {
171		return nil, fmt.Errorf("XDG_RUNTIME_DIR is empty")
172	}
173	d, err := server.NewDisplay()
174	if err != nil {
175		return nil, err
176	}
177	name, fd, cleanup, err := listen(opts.RuntimeDir)
178	if err != nil {
179		d.Close()
180		return nil, err
181	}
182	if err = d.AddSocketFD(fd); err != nil {
183		unix.Close(fd)
184		cleanup()
185		d.Close()
186		return nil, err
187	}
188	s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
189	s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
190	s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
191	s.flips = map[string]ports.FlipInfo{}
192	s.timelines = map[*server.Resource]*timeline{}
193	var node *renderNode
194	if opts.syncDev != nil {
195		s.syncDev = opts.syncDev
196		if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
197			cleanup()
198			d.Close()
199			return nil, err
200		}
201	} else if opts.SyncobjNode != "" {
202		if node, err = openSyncobj(opts.SyncobjNode); err != nil {
203			log.Info().Str("component", "wayland").Err(err).Msg("explicit sync off")
204			node = nil
205		} else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
206			node.close()
207			node = nil
208		} else {
209			s.syncDev = node
210		}
211	}
212	s.tokens = map[string]activationToken{}
213	if opts.Keymap != "" {
214		fd, size, e := keymapFile(opts.Keymap)
215		if e != nil {
216			cleanup()
217			d.Close()
218			return nil, e
219		}
220		s.keymapFD, s.keymapSize, s.keymapText = fd, size, opts.Keymap
221	}
222	s.cleanup = func() {
223		if s.keymapFD >= 0 {
224			unix.Close(s.keymapFD)
225		}
226		s.dmabuf.close()
227		if s.syncWait != nil {
228			s.syncWait.close()
229		}
230		if node != nil {
231			node.close()
232		}
233		cleanup()
234	}
235	if err = registerGlobals(d, opts, s); err != nil {
236		s.cleanup()
237		d.Close()
238		return nil, err
239	}
240	for _, p := range opts.Outputs {
241		s.addOutput(p)
242	}
243	return s, nil
244}
245func (s *Server) SocketName() string { return s.name }
246func (s *Server) Run(ctx context.Context) error {
247	defer s.cleanup()
248	s.log.Info().Str("socket", s.name).Msg("starting wayland")
249	s.started = time.Now()
250	s.ctx = ctx
251	var wg sync.WaitGroup
252	wg.Add(6)
253	go func() { defer wg.Done(); s.forward(ctx) }()
254	go func() { defer wg.Done(); s.forwardCursors(ctx) }()
255	go func() { defer wg.Done(); s.forwardContents(ctx) }()
256	go func() {
257		defer wg.Done()
258		for {
259			select {
260			case <-ctx.Done():
261				return
262			case <-s.display.Stopped():
263				return
264			case cmd, ok := <-s.channels.Commands:
265				if !ok {
266					return
267				}
268				if ctx.Err() != nil {
269					return
270				}
271				// One display round trip per batch: Do waits for the event
272				// loop, so a round trip per motion capped pointer events at
273				// ~150/s and let a backlog of stale motions build up.
274				batch, open := drainCommands(cmd, s.channels.Commands)
275				if !s.display.Do(func() {
276					for _, c := range batch {
277						if ctx.Err() != nil {
278							return
279						}
280						s.apply(c)
281					}
282				}) || !open {
283					return
284				}
285			}
286		}
287	}()
288	go func() { defer wg.Done(); s.pace(ctx) }()
289	go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
290	if s.syncWait != nil {
291		wg.Add(1)
292		go func() { defer wg.Done(); s.syncWait.run(ctx) }()
293	}
294	err := s.display.Run(ctx)
295	wg.Wait()
296	return err
297}
298
299// damageHistory is how many contents a window's damage history covers:
300// enough for a renderer target a few frames old.
301const damageHistory = 4
302
303// forwardOutputFormats applies the outputs' scanout formats on the
304// display goroutine.
305func (s *Server) forwardOutputFormats(ctx context.Context) {
306	for {
307		select {
308		case <-ctx.Done():
309			return
310		case <-s.display.Stopped():
311			return
312		case f, ok := <-s.channels.OutputFormats:
313			if !ok {
314				return
315			}
316			if !s.display.Do(func() {
317				if s.dmabuf != nil {
318					s.dmabuf.setOutputFormats(f)
319				}
320			}) {
321				return
322			}
323		}
324	}
325}
326
327func (s *Server) emit(ev ports.ClientEvent) {
328	if s.channels.Events == nil {
329		return
330	}
331	s.eventMu.Lock()
332	s.events = append(s.events, ev)
333	s.eventMu.Unlock()
334	select {
335	case s.eventReady <- struct{}{}:
336	default:
337	}
338}
339
340func (s *Server) forward(ctx context.Context) {
341	for {
342		s.eventMu.Lock()
343		if len(s.events) == 0 {
344			s.eventMu.Unlock()
345			select {
346			case <-ctx.Done():
347				return
348			case <-s.eventReady:
349				continue
350			}
351		}
352		ev := s.events[0]
353		s.events[0] = nil
354		s.events = s.events[1:]
355		s.eventMu.Unlock()
356		select {
357		case <-ctx.Done():
358			return
359		case s.channels.Events <- ev:
360		}
361	}
362}
363
364func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
365	if s.channels.Contents == nil {
366		return
367	}
368	s.contentMu.Lock()
369	s.contentSeq[c.ID]++
370	c.Seq = s.contentSeq[c.ID]
371	// The window's damage history: its last damageHistory contents.
372	h := append(s.damage[c.ID], ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})
373	if len(h) > damageHistory {
374		h = h[len(h)-damageHistory:]
375	}
376	if c.Empty() {
377		delete(s.damage, c.ID)
378	} else {
379		s.damage[c.ID] = h
380	}
381	c.DamageHistory = slices.Clone(h)
382	s.contents[c.ID] = c
383	close(s.contentNotify)
384	s.contentNotify = make(chan struct{})
385	s.contentMu.Unlock()
386	select {
387	case s.contentReady <- struct{}{}:
388	default:
389	}
390}
391
392func (s *Server) forwardContents(ctx context.Context) {
393	for {
394		s.contentMu.Lock()
395		var id ports.WindowID
396		var c ports.SurfaceContent
397		found := false
398		var seq uint64
399		var notify <-chan struct{}
400		for id, c = range s.contents {
401			found = true
402			seq = s.contentSeq[id]
403			break
404		}
405		notify = s.contentNotify
406		s.contentMu.Unlock()
407		if !found {
408			select {
409			case <-ctx.Done():
410				return
411			case <-s.contentReady:
412				continue
413			}
414		}
415		// Do not hold a stale frame while the receiver is busy: wake on newer data.
416		select {
417		case <-ctx.Done():
418			return
419		case <-notify:
420			continue
421		case s.channels.Contents <- c:
422			s.contentMu.Lock()
423			if s.contentSeq[id] == seq {
424				delete(s.contents, id)
425			}
426			s.contentMu.Unlock()
427		}
428	}
429}
430
431// maxCommandBatch bounds how many queued commands one display round trip applies.
432const maxCommandBatch = 256
433
434// drainCommands returns first plus the commands already queued, with runs of
435// pointer motion for the same window collapsed into the latest one, their
436// relative deltas summed. open is
437// false once the channel is closed.
438func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (batch []ports.ClientCommand, open bool) {
439	batch = append(batch, first)
440	for len(batch) < maxCommandBatch {
441		select {
442		case c, ok := <-cmds:
443			if !ok {
444				return batch, false
445			}
446			if m, isMotion := c.(ports.PointerMotionTo); isMotion {
447				if last, lastMotion := batch[len(batch)-1].(ports.PointerMotionTo); lastMotion && last.ID == m.ID {
448					// The position is the latest; relative deltas add up so
449					// locked pointers (games) lose no movement.
450					m.DX += last.DX
451					m.DY += last.DY
452					m.UnaccelDX += last.UnaccelDX
453					m.UnaccelDY += last.UnaccelDY
454					batch[len(batch)-1] = m
455					continue
456				}
457			}
458			batch = append(batch, c)
459		default:
460			return batch, true
461		}
462	}
463	return batch, true
464}
465
466func (s *Server) apply(cmd ports.ClientCommand) {
467	switch c := cmd.(type) {
468	case ports.PointerFocus:
469		s.changePointerFocus(c.ID, c.X, c.Y)
470	case ports.PointerMotionTo:
471		if l := s.layers[c.ID]; l != nil && c.ID == s.pointerFocus {
472			s.pointerX, s.pointerY = c.X, c.Y
473			for _, p := range s.layerPointers(l) {
474				p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
475				pointerFrame(p)
476			}
477			return
478		}
479		if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {
480			if s.relativeMotion(w, c) && s.locked(w) {
481				for _, p := range s.windowPointers(w) {
482					pointerFrame(p)
483				}
484			}
485			if s.locked(w) {
486				// Only relative motion reaches a locked pointer.
487				return
488			}
489			x, y := w.surfacePoint(c.X, c.Y)
490			s.pointerX, s.pointerY = c.X, c.Y
491			// A constraint waits for the pointer to enter its region.
492			if s.constraint == nil {
493				s.updateConstraint()
494			}
495			for _, p := range s.windowPointers(w) {
496				p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
497				pointerFrame(p)
498			}
499		}
500	case ports.PointerButtonTo:
501		// Releases may target the implicit-grab window after focus has moved.
502		if client, pointers, ok := s.pointerTarget(c.ID); ok {
503			s.serial++
504			state := uint32(0)
505			if c.Pressed {
506				state = 1
507				s.press, s.pressClient, s.pressAt = s.serial, client, time.Now()
508			}
509			for _, p := range pointers {
510				p.SendButton(s.serial, c.TimeMsec, c.Button, state)
511				pointerFrame(p)
512			}
513		}
514	case ports.PointerAxisTo:
515		// Scroll applies to the entered surface only.
516		if c.ID != s.pointerFocus {
517			return
518		}
519		steps, values := s.wheelSteps(c.Axis)
520		_, pointers, _ := s.pointerTarget(c.ID)
521		for _, p := range pointers {
522			sendAxis(p, c.Axis, steps, values)
523		}
524	case ports.SetKeymap:
525		s.setKeymap(c)
526	case ports.FocusWindow:
527		if s.focused != c.ID {
528			s.changeFocus(c.ID)
529		}
530	case ports.ForwardKey:
531		_, keyboards := s.focusTarget(c.ID)
532		if c.ID != s.focused || len(keyboards) == 0 {
533			s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
534			return
535		}
536		s.useKeymap(nil)
537		if s.heldKeys == nil {
538			s.heldKeys = make(map[uint32]bool)
539		}
540		if c.Key.Pressed {
541			s.heldKeys[c.Key.Keycode] = true
542		} else {
543			delete(s.heldKeys, c.Key.Keycode)
544		}
545		s.serial++
546		state := uint32(0)
547		if c.Key.Pressed {
548			state = 1
549		}
550		if c.Key.Pressed {
551			if surf, _ := s.focusTarget(c.ID); surf != nil {
552				s.press, s.pressClient, s.pressAt = s.serial, surf.Client(), time.Now()
553			}
554		}
555		for _, k := range keyboards {
556			k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
557		}
558		if c.Key.State != s.modState {
559			s.modState = c.Key.State
560			s.serial++
561			for _, k := range keyboards {
562				s.sendModifiers(k)
563			}
564		}
565	case ports.ConfigurePopup:
566		if w := s.windows[c.ID]; w != nil && w.popup != nil {
567			w.popup.configure(c)
568		}
569	case ports.ClosePopup:
570		if w := s.windows[c.ID]; w != nil && w.popup != nil {
571			w.popup.dismiss()
572		}
573	case ports.ConfigureWindow:
574		w := s.windows[c.ID]
575		if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {
576			s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")
577			return
578		}
579		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")
580		scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output
581		w.last, w.hasLast = c, true
582		w.sendConfigure()
583		if scanoutChanged && w.xdg.surface != nil {
584			s.dmabuf.resendSurface(w.xdg.surface)
585		}
586		if surf := w.xdg.surface; surf != nil && c.Output != "" {
587			surf.sendTreeScale()
588		}
589	case ports.SetOutputs:
590		s.setOutputs(c)
591	case ports.SlotsPending:
592		s.slotsPending = c.Pending
593	case ports.ShortcutsInhibitState:
594		s.setShortcutsInhibit(c)
595	case ports.CloseWindow:
596		w := s.windows[c.ID]
597		if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() {
598			s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window")
599			return
600		}
601		w.toplevel.SendClose()
602	}
603}
604
605func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
606	if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
607		return nil
608	}
609	return s.clientKeyboards(w.xdg.resource.Client())
610}
611
612func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
613	var alive []*wayland.Keyboard
614	for _, k := range s.keyboards[c] {
615		if k.Resource.Alive() {
616			alive = append(alive, k)
617		}
618	}
619	return alive
620}
621
622// focusTarget resolves a window or layer ID to its wl_surface and keyboards.
623func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) {
624	if w := s.windows[id]; w != nil && w.mapped {
625		if surf := w.xdg.surfaceResource(); surf != nil {
626			return surf, s.windowKeyboards(w)
627		}
628	}
629	if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
630		if surf := l.surfaceResource(); surf != nil {
631			return surf, s.clientKeyboards(l.resource.Client())
632		}
633	}
634	return nil, nil
635}
636
637// pointerTarget resolves a mapped window or layer ID to its client and
638// live pointers.
639func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Pointer, bool) {
640	if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() {
641		return w.xdg.resource.Client(), s.windowPointers(w), true
642	}
643	if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
644		return l.resource.Client(), s.layerPointers(l), true
645	}
646	return server.Client{}, nil, false
647}
648
649// pointerSurface is the wl_surface of a mapped window or layer and the
650// point in its surface coordinates.
651func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surface, []*wayland.Pointer, float64, float64) {
652	if w := s.windows[id]; w != nil {
653		if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() {
654			x, y := w.surfacePoint(x, y)
655			return surf, s.windowPointers(w), x, y
656		}
657	}
658	if l := s.layers[id]; l != nil && l.mapped {
659		if surf := l.surfaceResource(); surf != nil {
660			return surf, s.layerPointers(l), x, y
661		}
662	}
663	return nil, nil, 0, 0
664}
665
666// keymapFile writes a sealed memfd holding the NUL-terminated keymap.
667func keymapFile(keymap string) (int, uint32, error) {
668	if uint64(len(keymap))+1 > uint64(^uint32(0)) {
669		return -1, 0, fmt.Errorf("keymap too large")
670	}
671	fd, err := unix.MemfdCreate("neferwl-keymap", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
672	if err != nil {
673		return -1, 0, err
674	}
675	n, err := unix.Write(fd, append([]byte(keymap), 0))
676	if err == nil && n != len(keymap)+1 {
677		err = fmt.Errorf("short keymap write")
678	}
679	if err == nil {
680		_, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL)
681	}
682	if err != nil {
683		unix.Close(fd)
684		return -1, 0, err
685	}
686	return fd, uint32(n), nil
687}
688
689// setKeymap sends repeat info, and a new keymap when one is given, to every keyboard.
690// A new keymap resets held keys and modifiers: the focused client gets leave, then
691// enter with no keys.
692func (s *Server) setKeymap(c ports.SetKeymap) {
693	s.repeatRate, s.repeatDelay = c.RepeatRate, c.RepeatDelay
694	if c.Keymap == "" {
695		for _, list := range s.keyboards {
696			for _, k := range list {
697				if k.Version() >= 4 {
698					k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
699				}
700			}
701		}
702		s.log.Info().Int("rate", s.repeatRate).Int("delay", s.repeatDelay).Msg("repeat updated")
703		return
704	}
705	fd, size, err := keymapFile(c.Keymap)
706	if err != nil {
707		s.log.Warn().Err(err).Msg("keymap update failed")
708		return
709	}
710	if s.keymapFD >= 0 {
711		unix.Close(s.keymapFD)
712	}
713	s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap
714	s.keymapOwner = nil
715	focused := s.focused
716	s.changeFocus(0)
717	s.heldKeys = nil
718	s.modState = ports.ModState{}
719	for _, list := range s.keyboards {
720		for _, k := range list {
721			k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)
722			if k.Version() >= 4 {
723				k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
724			}
725		}
726	}
727	s.changeFocus(focused)
728	s.log.Info().Uint32("size", size).Int("rate", s.repeatRate).Int("delay", s.repeatDelay).Msg("keymap updated")
729}
730
731func (s *Server) sendModifiers(k *wayland.Keyboard) {
732	m := s.modState
733	k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
734}
735
736func (s *Server) changeFocus(id ports.WindowID) {
737	// Enter carries seat held keys and modifiers: they need the seat keymap.
738	s.useKeymap(nil)
739	old := s.focusClient()
740	defer func() {
741		// Tokens of a client losing the focus die with it.
742		if now := s.focusClient(); now != old {
743			s.dropTokens(old)
744			s.focusAt = time.Now()
745		}
746	}()
747	if surf, keyboards := s.focusTarget(s.focused); surf != nil {
748		for _, k := range keyboards {
749			s.serial++
750			k.SendLeave(s.serial, surf)
751		}
752	}
753	s.focused = 0
754	if surf, keyboards := s.focusTarget(id); surf != nil {
755		s.focused = id
756		s.focusSelections()
757		for _, k := range keyboards {
758			s.serial++
759			var keys []byte
760			for code := range s.heldKeys {
761				keys = binary.LittleEndian.AppendUint32(keys, code)
762			}
763			k.SendEnter(s.serial, surf, keys)
764			s.sendModifiers(k)
765		}
766	}
767	s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus")
768	s.updateConstraint()
769}
770
771// wheelSteps adds wheel v120 to the rest per axis and returns the whole
772// detents for axis_discrete (seat before v8). High-resolution wheels send
773// fractions of 120; a direction change drops the rest, as wlroots does.
774// wheelHeld keeps the axis value of the frames without a step; values is
775// what a pre-v8 client gets with a step: the held value plus this frame's.
776func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]float64) {
777	if a.Source != ports.AxisWheel {
778		return steps, values
779	}
780	for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
781		if !ax.Set || ax.V120 == 0 {
782			continue
783		}
784		if (s.wheelRest[i] < 0) != (ax.V120 < 0) {
785			s.wheelRest[i], s.wheelHeld[i] = 0, 0
786		}
787		s.wheelRest[i] += ax.V120
788		s.wheelHeld[i] += ax.Value
789		steps[i] = s.wheelRest[i] / 120
790		s.wheelRest[i] -= steps[i] * 120
791		if steps[i] != 0 {
792			values[i], s.wheelHeld[i] = s.wheelHeld[i], 0
793		}
794	}
795	return steps, values
796}
797
798// sendAxis sends one scroll frame, each event gated by the pointer version.
799// steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
800// wheel axis only with a step, carrying values (all the value since the
801// last step), so it never counts smooth and discrete scroll.
802func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
803	v := p.Version()
804	axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
805	for i := range axes {
806		if v < 8 && a.Source == ports.AxisWheel && axes[i].V120 != 0 && steps[i] == 0 {
807			axes[i].Set = false
808		}
809	}
810	if !axes[0].Set && !axes[1].Set {
811		return
812	}
813	if v >= 5 {
814		p.SendAxisSource(uint32(a.Source))
815	}
816	for axis, s := range axes {
817		if !s.Set {
818			continue
819		}
820		if s.Stop {
821			if v >= 5 {
822				p.SendAxisStop(a.TimeMsec, uint32(axis))
823			}
824			continue
825		}
826		value := s.Value
827		if a.Source == ports.AxisWheel && s.V120 != 0 {
828			switch {
829			case v >= 8:
830				p.SendAxisValue120(uint32(axis), s.V120)
831			default:
832				value = values[axis]
833				if v >= 5 {
834					p.SendAxisDiscrete(uint32(axis), steps[axis])
835				}
836			}
837		}
838		p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))
839	}
840	pointerFrame(p)
841}
842
843func pointerFrame(p *wayland.Pointer) {
844	if p.Version() >= 5 {
845		p.SendFrame()
846	}
847}
848func (s *Server) windowPointers(w *window) []*wayland.Pointer {
849	if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
850		return nil
851	}
852	return s.clientPointers(w.xdg.resource.Client())
853}
854
855func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
856	if l == nil || !l.mapped || !l.resource.Resource.Alive() {
857		return nil
858	}
859	return s.clientPointers(l.resource.Client())
860}
861
862func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {
863	var result []*wayland.Pointer
864	for _, p := range s.pointers[c] {
865		if p.Resource.Alive() {
866			result = append(result, p)
867		}
868	}
869	return result
870}
871func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
872	if id == s.pointerFocus {
873		return
874	}
875	s.wheelRest, s.wheelHeld = [2]int32{}, [2]float64{}
876	// The new client sets its own cursor on enter; until then, the arrow.
877	s.cursorSurface = nil
878	s.setCursor(ports.CursorChange{})
879	if surface, pointers, _, _ := s.pointerSurface(s.pointerFocus, 0, 0); surface != nil {
880		for _, p := range pointers {
881			s.serial++
882			p.SendLeave(s.serial, surface)
883			pointerFrame(p)
884		}
885	}
886	s.pointerFocus = 0
887	if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {
888		s.pointerFocus, s.pointerX, s.pointerY = id, x, y
889		for _, p := range pointers {
890			s.serial++
891			p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))
892			pointerFrame(p)
893		}
894	}
895	s.updateConstraint()
896}

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