v0.4.0 b6be3a8

seat.go RawHistoryFolder

1package wayland
2
3import (
4	"encoding/binary"
5	"fmt"
6	"time"
7
8	"github.com/bnema/go-wayland-bindings/server/inputtimestamps"
9	"github.com/bnema/go-wayland-bindings/server/wayland"
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/bnema/purego-libwayland/server"
12	"golang.org/x/sys/unix"
13)
14
15// seatState is the wl_seat state: who has keyboard and pointer focus, the
16// bound keyboards and pointers, the keymap they carry, and the last press.
17// It belongs to the display goroutine.
18type seatState struct {
19	focused      ports.WindowID
20	pointerFocus ports.WindowID
21	// pointerX, pointerY is the pointer as core placed it in pointerFocus;
22	// pointerSurface and surfacePoint turn it into surface coordinates.
23	pointerX, pointerY float64
24	// enters holds the serial of the enter event each wl_pointer received
25	// for pointerFocus; a pointer warp must name its pointer's serial.
26	enters      map[*server.Resource]uint32
27	wheelRest   [2]int32   // v120 not yet sent as axis_discrete, per axis
28	wheelHeld   [2]float64 // axis value held back with it for pre-v8 clients
29	pointers    map[server.Client][]*wayland.Pointer
30	keyboards   map[server.Client][]*wayland.Keyboard
31	modState    ports.ModState
32	heldKeys    map[uint32]bool
33	grabKeys    map[uint32]bool // pressed through the input method grab
34	keymapFD    int
35	keymapSize  uint32
36	keymapText  string           // the seat keymap, to compare virtual keymaps with
37	keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
38	repeatRate  int
39	repeatDelay int
40	// press is the serial of the last button or key press, sent to
41	// pressClient: popup grabs must come from it.
42	press       uint32
43	pressClient server.Client
44	// pressAt and focusAt date the last press and keyboard focus change,
45	// for xdg-activation tokens.
46	pressAt, focusAt time.Time
47	// keyStamps and pointerStamps hold the zwp_input_timestamps_v1 objects
48	// subscribed to each device; the lists are replaced, never mutated.
49	keyStamps, pointerStamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1
50}
51
52// applyInput delivers a core input command to the focused client.
53func (s *Server) applyInput(cmd ports.ClientCommand) {
54	if s.security != nil && s.security.Snapshot().Protected {
55		var target ports.WindowID
56		switch c := cmd.(type) {
57		case ports.SetKeymap:
58			// Configuration updates do not authorize normal-client input.
59		case ports.PointerFocus:
60			target = c.ID
61		case ports.PointerMotionTo:
62			target = c.ID
63		case ports.PointerButtonTo:
64			target = c.ID
65		case ports.PointerAxisTo:
66			target = c.ID
67		case ports.FocusWindow:
68			target = c.ID
69		case ports.ForwardKey:
70			target = c.ID
71		}
72		if target != 0 && !s.lockInputTarget(target) {
73			return
74		}
75	}
76	switch c := cmd.(type) {
77	case ports.PointerFocus:
78		s.changePointerFocus(c.ID, c.X, c.Y)
79	case ports.PointerMotionTo:
80		if l := s.lockSurfaces[c.ID]; l != nil && s.lockInputTarget(c.ID) && c.ID == s.seat.pointerFocus {
81			s.seat.pointerX, s.seat.pointerY = c.X, c.Y
82			for _, p := range s.clientPointers(l.resource.Client()) {
83				s.stampPointer(p, c.Time)
84				p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
85				pointerFrame(p)
86			}
87			return
88		}
89		if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus {
90			s.seat.pointerX, s.seat.pointerY = c.X, c.Y
91			for _, p := range s.layerPointers(l) {
92				s.stampPointer(p, c.Time)
93				p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
94				pointerFrame(p)
95			}
96			return
97		}
98		if w := s.windows[c.ID]; w != nil && c.ID == s.seat.pointerFocus {
99			if s.relativeMotion(w, c) && s.locked(w) {
100				for _, p := range s.windowPointers(w) {
101					pointerFrame(p)
102				}
103			}
104			if s.locked(w) {
105				// Only relative motion reaches a locked pointer.
106				return
107			}
108			x, y := w.surfacePoint(c.X, c.Y)
109			s.seat.pointerX, s.seat.pointerY = c.X, c.Y
110			// A constraint waits for the pointer to enter its region.
111			if s.constraint == nil {
112				s.updateConstraint()
113			}
114			for _, p := range s.windowPointers(w) {
115				s.stampPointer(p, c.Time)
116				p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y))
117				pointerFrame(p)
118			}
119		}
120	case ports.PointerButtonTo:
121		// Releases may target the implicit-grab window after focus has moved.
122		if client, pointers, ok := s.pointerTarget(c.ID); ok {
123			s.serial++
124			state := uint32(0)
125			if c.Pressed {
126				state = 1
127				s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
128			}
129			for _, p := range pointers {
130				s.stampPointer(p, c.Time)
131				p.SendButton(s.serial, wireMsec(c.Time), c.Button, state)
132				pointerFrame(p)
133			}
134		}
135	case ports.PointerAxisTo:
136		// Scroll applies to the entered surface only.
137		if c.ID != s.seat.pointerFocus {
138			return
139		}
140		steps, values := s.seat.wheelSteps(c.Axis)
141		_, pointers, _ := s.pointerTarget(c.ID)
142		for _, p := range pointers {
143			s.sendAxis(p, c.Axis, steps, values)
144		}
145	case ports.SetKeymap:
146		s.setKeymap(c)
147	case ports.FocusWindow:
148		if s.seat.focused != c.ID {
149			s.changeFocus(c.ID)
150		}
151	case ports.ForwardKey:
152		if !(s.security != nil && s.security.Snapshot().Protected) && s.grabKey(c) {
153			return
154		}
155		_, keyboards := s.focusTarget(c.ID)
156		if c.ID != s.seat.focused || len(keyboards) == 0 {
157			s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
158			return
159		}
160		s.useKeymap(nil)
161		if s.seat.heldKeys == nil {
162			s.seat.heldKeys = make(map[uint32]bool)
163		}
164		if c.Key.Pressed {
165			s.seat.heldKeys[c.Key.Keycode] = true
166		} else {
167			delete(s.seat.heldKeys, c.Key.Keycode)
168		}
169		s.serial++
170		state := uint32(0)
171		if c.Key.Pressed {
172			state = 1
173		}
174		if c.Key.Pressed {
175			if surf, _ := s.focusTarget(c.ID); surf != nil {
176				s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, surf.Client(), time.Now()
177			}
178		}
179		for _, k := range keyboards {
180			s.stampKeyboard(k, c.Key.Time)
181			k.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state)
182		}
183		if c.Key.State != s.seat.modState {
184			s.seat.modState = c.Key.State
185			s.serial++
186			for _, k := range keyboards {
187				s.sendModifiers(k)
188			}
189		}
190	}
191}
192
193func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
194	if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
195		return nil
196	}
197	return s.clientKeyboards(w.xdg.resource.Client())
198}
199
200// clientKeyboards returns c's wl_keyboards without allocating. The list holds
201// only live resources (OnDestroy removes a keyboard when it dies) and is
202// replaced, never mutated, so callers may iterate it while sending.
203//
204// This relies on purego-libwayland: destroyed() sets gone and runs OnDestroy
205// synchronously on the display goroutine, and PostEvent never re-enters Go, so
206// a list ranged over while sending is never changed under the caller.
207func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
208	return s.seat.keyboards[c]
209}
210
211// focusTarget resolves a window or layer ID to its wl_surface and keyboards.
212func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) {
213	if l := s.lockSurfaces[id]; l != nil && !l.closed && l.surface.wl.Alive() {
214		// Outgoing leaves also resolve a role whose OnDestroy resource is
215		// already dead. Admission is enforced before entering or delivering.
216		return l.surface.wl, s.clientKeyboards(l.surface.wl.Client())
217	}
218	if w := s.windows[id]; w != nil && w.mapped {
219		if surf := w.xdg.surfaceResource(); surf != nil {
220			return surf, s.windowKeyboards(w)
221		}
222	}
223	if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
224		if surf := l.surfaceResource(); surf != nil {
225			return surf, s.clientKeyboards(l.resource.Client())
226		}
227	}
228	return nil, nil
229}
230
231// pointerTarget resolves a mapped window or layer ID to its client and
232// live pointers.
233func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Pointer, bool) {
234	if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) {
235		return l.resource.Client(), s.clientPointers(l.resource.Client()), true
236	}
237	if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() {
238		return w.xdg.resource.Client(), s.windowPointers(w), true
239	}
240	if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
241		return l.resource.Client(), s.layerPointers(l), true
242	}
243	return server.Client{}, nil, false
244}
245
246// pointerSurface is the wl_surface of a mapped window or layer and the
247// point in its surface coordinates.
248func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surface, []*wayland.Pointer, float64, float64) {
249	if l := s.lockSurfaces[id]; l != nil && !l.closed && l.surface.wl.Alive() {
250		return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y
251	}
252	if w := s.windows[id]; w != nil {
253		if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() {
254			x, y := w.surfacePoint(x, y)
255			return surf, s.windowPointers(w), x, y
256		}
257	}
258	if l := s.layers[id]; l != nil && l.mapped {
259		if surf := l.surfaceResource(); surf != nil {
260			return surf, s.layerPointers(l), x, y
261		}
262	}
263	return nil, nil, 0, 0
264}
265
266// keymapFile writes a sealed memfd holding the NUL-terminated keymap.
267func keymapFile(keymap string) (int, uint32, error) {
268	if uint64(len(keymap))+1 > uint64(^uint32(0)) {
269		return -1, 0, fmt.Errorf("keymap too large")
270	}
271	fd, err := unix.MemfdCreate("neferwl-keymap", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
272	if err != nil {
273		return -1, 0, err
274	}
275	n, err := unix.Write(fd, append([]byte(keymap), 0))
276	if err == nil && n != len(keymap)+1 {
277		err = fmt.Errorf("short keymap write")
278	}
279	if err == nil {
280		_, 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)
281	}
282	if err != nil {
283		unix.Close(fd)
284		return -1, 0, err
285	}
286	return fd, uint32(n), nil
287}
288
289// setKeymap sends repeat info, and a new keymap when one is given, to every keyboard.
290// A new keymap resets held keys and modifiers: the focused client gets leave, then
291// enter with no keys.
292func (s *Server) setKeymap(c ports.SetKeymap) {
293	s.seat.repeatRate, s.seat.repeatDelay = c.RepeatRate, c.RepeatDelay
294	if c.Keymap == "" {
295		for _, list := range s.seat.keyboards {
296			for _, k := range list {
297				if k.Version() >= 4 {
298					k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
299				}
300			}
301		}
302		s.sendGrabKeymap(false)
303		s.log.Info().Int("rate", s.seat.repeatRate).Int("delay", s.seat.repeatDelay).Msg("repeat updated")
304		return
305	}
306	fd, size, err := keymapFile(c.Keymap)
307	if err != nil {
308		s.log.Warn().Err(err).Msg("keymap update failed")
309		return
310	}
311	if s.seat.keymapFD >= 0 {
312		unix.Close(s.seat.keymapFD)
313	}
314	s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, c.Keymap
315	s.seat.keymapOwner = nil
316	focused := s.seat.focused
317	s.changeFocus(0)
318	s.seat.heldKeys, s.seat.grabKeys = nil, nil
319	s.seat.modState = ports.ModState{}
320	for _, list := range s.seat.keyboards {
321		for _, k := range list {
322			k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
323			if k.Version() >= 4 {
324				k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
325			}
326		}
327	}
328	s.sendGrabKeymap(true)
329	s.changeFocus(focused)
330	s.log.Info().Uint32("size", size).Int("rate", s.seat.repeatRate).Int("delay", s.seat.repeatDelay).Msg("keymap updated")
331}
332
333func (s *Server) sendModifiers(k *wayland.Keyboard) {
334	m := s.seat.modState
335	k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
336}
337
338func (s *Server) changeFocus(id ports.WindowID) {
339	if s.security != nil && s.security.Snapshot().Protected && id != 0 && !s.lockInputTarget(id) {
340		return
341	}
342	// Enter carries seat held keys and modifiers: they need the seat keymap.
343	s.useKeymap(nil)
344	old := s.focusClient()
345	defer func() {
346		// Tokens of a client losing the focus die with it.
347		if now := s.focusClient(); now != old {
348			s.dropTokens(old)
349			s.seat.focusAt = time.Now()
350		}
351	}()
352	if surf, keyboards := s.focusTarget(s.seat.focused); surf != nil {
353		for _, k := range keyboards {
354			s.serial++
355			k.SendLeave(s.serial, surf)
356		}
357	}
358	s.seat.focused = 0
359	if surf, keyboards := s.focusTarget(id); surf != nil {
360		s.seat.focused = id
361		s.focusSelections()
362		for _, k := range keyboards {
363			s.serial++
364			var keys []byte
365			for code := range s.seat.heldKeys {
366				keys = binary.LittleEndian.AppendUint32(keys, code)
367			}
368			k.SendEnter(s.serial, surf, keys)
369			s.sendModifiers(k)
370		}
371	}
372	s.textInputFocus()
373	s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus")
374	s.updateConstraint()
375}
376
377// wheelSteps adds wheel v120 to the rest per axis and returns the whole
378// detents for axis_discrete (seat before v8). High-resolution wheels send
379// fractions of 120; a direction change drops the rest, as wlroots does.
380// wheelHeld keeps the axis value of the frames without a step; values is
381// what a pre-v8 client gets with a step: the held value plus this frame's.
382func (st *seatState) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]float64) {
383	if a.Source != ports.AxisWheel {
384		return steps, values
385	}
386	for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
387		if !ax.Set || ax.V120 == 0 {
388			continue
389		}
390		if (st.wheelRest[i] < 0) != (ax.V120 < 0) {
391			st.wheelRest[i], st.wheelHeld[i] = 0, 0
392		}
393		st.wheelRest[i] += ax.V120
394		st.wheelHeld[i] += ax.Value
395		steps[i] = st.wheelRest[i] / 120
396		st.wheelRest[i] -= steps[i] * 120
397		if steps[i] != 0 {
398			values[i], st.wheelHeld[i] = st.wheelHeld[i], 0
399		}
400	}
401	return steps, values
402}
403
404// sendAxis sends one scroll frame, each event gated by the pointer version.
405// steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
406// wheel axis only with a step, carrying values (all the value since the
407// last step), so it never counts smooth and discrete scroll.
408func (s *Server) sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
409	v := p.Version()
410	axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
411	for i := range axes {
412		if v < 8 && a.Source == ports.AxisWheel && axes[i].V120 != 0 && steps[i] == 0 {
413			axes[i].Set = false
414		}
415	}
416	if !axes[0].Set && !axes[1].Set {
417		return
418	}
419	if v >= 5 {
420		p.SendAxisSource(uint32(a.Source))
421	}
422	for axis, ax := range axes {
423		if !ax.Set {
424			continue
425		}
426		if ax.Stop {
427			if v >= 5 {
428				s.stampPointer(p, a.Time)
429				p.SendAxisStop(wireMsec(a.Time), uint32(axis))
430			}
431			continue
432		}
433		value := ax.Value
434		if a.Source == ports.AxisWheel && ax.V120 != 0 {
435			switch {
436			case v >= 8:
437				p.SendAxisValue120(uint32(axis), ax.V120)
438			default:
439				value = values[axis]
440				if v >= 5 {
441					p.SendAxisDiscrete(uint32(axis), steps[axis])
442				}
443			}
444		}
445		s.stampPointer(p, a.Time)
446		p.SendAxis(wireMsec(a.Time), uint32(axis), server.FixedFromFloat(value))
447	}
448	pointerFrame(p)
449}
450
451// wireMsec is a device timestamp as the 32-bit millisecond time of wl_pointer
452// and wl_keyboard events (wrapping, like the protocol).
453func wireMsec(t time.Duration) uint32 { return uint32(t / time.Millisecond) }
454
455func pointerFrame(p *wayland.Pointer) {
456	if p.Version() >= 5 {
457		p.SendFrame()
458	}
459}
460func (s *Server) windowPointers(w *window) []*wayland.Pointer {
461	if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
462		return nil
463	}
464	return s.clientPointers(w.xdg.resource.Client())
465}
466
467func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
468	if l == nil || !l.mapped || !l.resource.Resource.Alive() {
469		return nil
470	}
471	return s.clientPointers(l.resource.Client())
472}
473
474// clientPointers returns c's wl_pointers without allocating. The list holds
475// only live resources (OnDestroy removes a pointer when it dies) and is
476// replaced, never mutated, so callers may iterate it while sending. See
477// clientKeyboards for the libwayland invariant this relies on.
478func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {
479	return s.seat.pointers[c]
480}
481func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
482	if s.security != nil && s.security.Snapshot().Protected && id != 0 && !s.lockInputTarget(id) {
483		return
484	}
485	if id == s.seat.pointerFocus {
486		return
487	}
488	s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
489	// The new client sets its own cursor on enter; until then, the arrow.
490	s.cursorSurface = nil
491	s.setCursor(ports.CursorChange{})
492	if surface, pointers, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0); surface != nil {
493		for _, p := range pointers {
494			s.serial++
495			p.SendLeave(s.serial, surface)
496			pointerFrame(p)
497		}
498	}
499	s.seat.pointerFocus = 0
500	clear(s.seat.enters)
501	if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {
502		s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY = id, x, y
503		for _, p := range pointers {
504			s.serial++
505			s.seat.enters[p.Resource] = s.serial
506			p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))
507			pointerFrame(p)
508		}
509	}
510	s.updateConstraint()
511}

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