v0.3.0 cf370a0

virtualkeyboard.go RawHistoryFolder

1package wayland
2
3import (
4	"bytes"
5	"time"
6
7	"github.com/bnema/go-wayland-bindings/server/virtualkeyboard"
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// maxVirtualKeymap bounds a keymap read from a virtual keyboard client.
15const maxVirtualKeymap = 1 << 20
16
17// virtualKeyboard is a zwp_virtual_keyboard_v1 (wtype, dictation tools).
18// Its keys go to the focused client. The focused client's keyboards carry
19// the keymap of whichever keyboard typed last: a virtual key switches them
20// to the virtual keymap, the next real key or focus change switches back.
21type virtualKeyboard struct {
22	server *Server
23	fd     int // sealed copy of the client keymap, -1 before one
24	size   uint32
25	text   string         // the keymap, compared with the active one
26	mods   ports.ModState // last modifiers request, zero before one
27	// pressed are keys held on focus; a focus change releases them by leave.
28	pressed      map[uint32]bool
29	focus        ports.WindowID
30	generation   ports.LockGeneration // epoch of held keys; never release across protection
31	dropReleases bool                 // after an epoch change, ignore releases without a new press
32}
33
34// registerVirtualKeyboard exposes the global to every client, as wlroots
35// does by default: any local client may type into the focused window.
36func registerVirtualKeyboard(d *server.Display, s *Server) error {
37	return virtualkeyboard.NewZwpVirtualKeyboardManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
38		_, _ = virtualkeyboard.NewZwpVirtualKeyboardManagerV1(c, int32(v), id, virtualKeyboardManager{s})
39	})
40}
41
42type virtualKeyboardManager struct{ server *Server }
43
44func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirtualKeyboardManagerV1, _ *wayland.Seat, id uint32) {
45	k := &virtualKeyboard{server: m.server, fd: -1, pressed: map[uint32]bool{}}
46	res, err := virtualkeyboard.NewZwpVirtualKeyboardV1(r.Client(), r.Version(), id, k)
47	if err != nil {
48		return
49	}
50	res.OnDestroy = k.release
51	m.server.log.Info().Int("pid", r.Client().PID()).Msg("virtual keyboard created")
52}
53
54// Keymap copies the client keymap into a sealed memfd: other clients never
55// see the client's fd. An unreadable or malformed keymap is ignored.
56func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format uint32, fd int, size uint32) {
57	if k.server.protected() {
58		unix.Close(fd)
59		k.admitted() // discard held state and ownership without delivery
60		if k.fd >= 0 {
61			unix.Close(k.fd)
62		}
63		k.fd, k.size, k.text = -1, 0, ""
64		// A rejected replacement must not leave an old layout usable after
65		// unlock. The client must send a fresh keymap before typing again.
66		return
67	}
68	text, ok := readKeymap(fd, size)
69	unix.Close(fd)
70	if format != uint32(wayland.KeyboardKeymapFormatXkbV1) || !ok {
71		k.server.log.Debug().Msg("virtual keymap rejected")
72		return
73	}
74	if wide, from := widenKeycodes(text); from != 0 {
75		k.server.log.Debug().Int("maximum", from).Int("widened", minKeycodeMax).Msg("virtual keymap keycode range widened")
76		text = wide
77	}
78	copyFD, copySize, err := keymapFile(text)
79	if err != nil {
80		k.server.log.Warn().Err(err).Msg("virtual keymap copy failed")
81		return
82	}
83	if k.fd >= 0 {
84		unix.Close(k.fd)
85	}
86	k.fd, k.size = copyFD, copySize
87	old := k.server.activeKeymapText()
88	k.text = text
89	if s := k.server; s.seat.keymapOwner == k && text != old {
90		// Clients must see the new layout before the next key.
91		s.sendKeymapAll(k.fd, k.size)
92	}
93}
94
95// readKeymap reads a NUL-terminated xkb keymap of size bytes from a
96// regular file.
97func readKeymap(fd int, size uint32) (string, bool) {
98	var st unix.Stat_t
99	if size == 0 || size > maxVirtualKeymap || unix.Fstat(fd, &st) != nil || st.Mode&unix.S_IFMT != unix.S_IFREG || st.Size < int64(size) {
100		return "", false
101	}
102	data := make([]byte, size)
103	if n, err := unix.Pread(fd, data, 0); err != nil || n != int(size) {
104		return "", false
105	}
106	if i := bytes.IndexByte(data, 0); i >= 0 {
107		data = data[:i]
108	}
109	if !bytes.Contains(data, []byte("xkb_keymap")) {
110		return "", false
111	}
112	return string(data), true
113}
114
115// keyboards switches the focused client to k's keymap and returns its
116// keyboards, or nil after a no_keymap error or without focus.
117func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []*wayland.Keyboard {
118	if !k.admitted() {
119		return nil
120	}
121	if k.fd < 0 {
122		r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "no keymap")
123		return nil
124	}
125	s := k.server
126	_, keyboards := s.focusTarget(s.seat.focused)
127	if len(keyboards) == 0 {
128		return nil
129	}
130	if k.focus != s.seat.focused {
131		clear(k.pressed)
132		k.focus = s.seat.focused
133	}
134	s.useKeymap(k)
135	s.serial++
136	return keyboards
137}
138
139func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, msec, key, state uint32) {
140	if !k.admitted() || state != 1 && k.dropReleases && !k.pressed[key] {
141		return
142	}
143	keyboards := k.keyboards(r)
144	if keyboards == nil {
145		return
146	}
147	if state == 1 {
148		k.pressed[key] = true
149	} else {
150		delete(k.pressed, key)
151	}
152	// The client's millisecond time is all the timestamp there is.
153	stamp := time.Duration(msec) * time.Millisecond
154	for _, kb := range keyboards {
155		k.server.stampKeyboard(kb, stamp)
156		kb.SendKey(k.server.serial, msec, key, state)
157	}
158}
159
160func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) {
161	if !k.admitted() {
162		return
163	}
164	k.mods = ports.ModState{Depressed: depressed, Latched: latched, Locked: locked, Group: group}
165	for _, kb := range k.keyboards(r) {
166		kb.SendModifiers(k.server.serial, depressed, latched, locked, group)
167	}
168}
169
170// admitted forgets old held state even if protection began and ended between
171// this keyboard's requests. Snapshot is coherent; DISPLAY owns admission.
172func (k *virtualKeyboard) admitted() bool {
173	state := ports.SecurityState{}
174	if k.server.security != nil {
175		state = k.server.security.Snapshot()
176	}
177	if state.Protected || state.Generation != k.generation {
178		clear(k.pressed)
179		k.mods = ports.ModState{}
180		k.focus = 0
181		k.generation = state.Generation
182		k.dropReleases = true
183		if k.server.seat.keymapOwner == k {
184			// Forget ownership silently; the parent resets focus and restores
185			// the seat layout at the lock transition.
186			k.server.seat.keymapOwner = nil
187		}
188	}
189	return !state.Protected
190}
191
192func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {}
193
194// release runs when the resource dies, by request or client disconnect:
195// keys still held on the focused client are released.
196func (k *virtualKeyboard) release() {
197	s := k.server
198	generation := k.generation
199	admitted := k.admitted() && generation == k.generation
200	if admitted && len(k.pressed) > 0 && k.focus == s.seat.focused {
201		_, keyboards := s.focusTarget(s.seat.focused)
202		var ts unix.Timespec
203		_ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
204		// Input events carry CLOCK_MONOTONIC (libinput's clock).
205		now := time.Duration(ts.Nano())
206		for key := range k.pressed {
207			s.serial++
208			for _, kb := range keyboards {
209				s.stampKeyboard(kb, now)
210				kb.SendKey(s.serial, wireMsec(now), key, 0)
211			}
212		}
213	}
214	if s.seat.keymapOwner == k {
215		if admitted {
216			s.useKeymap(nil)
217		} else {
218			// Drop ownership without delivering keymap/modifiers into the locker.
219			s.seat.keymapOwner = nil
220		}
221	}
222	if k.fd >= 0 {
223		unix.Close(k.fd)
224		k.fd = -1
225	}
226}
227
228// useKeymap makes every keyboard carry the keymap of k, or the seat keymap
229// when k is nil. Switching back restores the seat modifiers too.
230func (s *Server) useKeymap(k *virtualKeyboard) {
231	if s.seat.keymapOwner == k {
232		return
233	}
234	old := s.activeKeymapText()
235	s.seat.keymapOwner = k
236	// Like wlroots and smithay, a keymap equal to the active one is not
237	// sent again: tools such as dictation apps create a virtual keyboard
238	// with the seat layout per use, and each resend makes every client
239	// (Xwayland included) recompile its keymap.
240	if s.activeKeymapText() != old {
241		s.sendKeymapAll(s.currentKeymap())
242	}
243	// Modifiers follow the keyboard that types: a skipped keymap no longer
244	// resets them, so a virtual keyboard never types through seat Shift or
245	// Caps Lock, and the seat gets its own back.
246	m := s.seat.modState
247	if k != nil {
248		m = k.mods
249	}
250	s.serial++
251	for _, kb := range s.clientKeyboards(s.focusClient()) {
252		kb.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
253	}
254	s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")
255}
256
257func (s *Server) sendKeymapAll(fd int, size uint32) {
258	for _, list := range s.seat.keyboards {
259		for _, kb := range list {
260			if kb.Resource.Alive() {
261				kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)
262			}
263		}
264	}
265}
266
267// activeKeymapText is the text of the keymap keyboards carry.
268func (s *Server) activeKeymapText() string {
269	if k := s.seat.keymapOwner; k != nil {
270		return k.text
271	}
272	return s.seat.keymapText
273}
274
275// currentKeymap is the keymap a new keyboard must start with.
276func (s *Server) currentKeymap() (int, uint32) {
277	if k := s.seat.keymapOwner; k != nil {
278		return k.fd, k.size
279	}
280	return s.seat.keymapFD, s.seat.keymapSize
281}

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