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

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