seat.go RawHistoryFolder

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

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