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, time, 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	for _, kb := range keyboards {
153		kb.SendKey(k.server.serial, time, key, state)
154	}
155}
156
157func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) {
158	if !k.admitted() {
159		return
160	}
161	k.mods = ports.ModState{Depressed: depressed, Latched: latched, Locked: locked, Group: group}
162	for _, kb := range k.keyboards(r) {
163		kb.SendModifiers(k.server.serial, depressed, latched, locked, group)
164	}
165}
166
167// admitted forgets old held state even if protection began and ended between
168// this keyboard's requests. Snapshot is coherent; DISPLAY owns admission.
169func (k *virtualKeyboard) admitted() bool {
170	state := ports.SecurityState{}
171	if k.server.security != nil {
172		state = k.server.security.Snapshot()
173	}
174	if state.Protected || state.Generation != k.generation {
175		clear(k.pressed)
176		k.mods = ports.ModState{}
177		k.focus = 0
178		k.generation = state.Generation
179		k.dropReleases = true
180		if k.server.seat.keymapOwner == k {
181			// Forget ownership silently; the parent resets focus and restores
182			// the seat layout at the lock transition.
183			k.server.seat.keymapOwner = nil
184		}
185	}
186	return !state.Protected
187}
188
189func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {}
190
191// release runs when the resource dies, by request or client disconnect:
192// keys still held on the focused client are released.
193func (k *virtualKeyboard) release() {
194	s := k.server
195	generation := k.generation
196	admitted := k.admitted() && generation == k.generation
197	if admitted && len(k.pressed) > 0 && k.focus == s.seat.focused {
198		_, keyboards := s.focusTarget(s.seat.focused)
199		var ts unix.Timespec
200		_ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
201		// Input events carry CLOCK_MONOTONIC milliseconds (libinput's clock).
202		now := uint32(ts.Nano() / int64(time.Millisecond))
203		for key := range k.pressed {
204			s.serial++
205			for _, kb := range keyboards {
206				kb.SendKey(s.serial, now, key, 0)
207			}
208		}
209	}
210	if s.seat.keymapOwner == k {
211		if admitted {
212			s.useKeymap(nil)
213		} else {
214			// Drop ownership without delivering keymap/modifiers into the locker.
215			s.seat.keymapOwner = nil
216		}
217	}
218	if k.fd >= 0 {
219		unix.Close(k.fd)
220		k.fd = -1
221	}
222}
223
224// useKeymap makes every keyboard carry the keymap of k, or the seat keymap
225// when k is nil. Switching back restores the seat modifiers too.
226func (s *Server) useKeymap(k *virtualKeyboard) {
227	if s.seat.keymapOwner == k {
228		return
229	}
230	old := s.activeKeymapText()
231	s.seat.keymapOwner = k
232	// Like wlroots and smithay, a keymap equal to the active one is not
233	// sent again: tools such as dictation apps create a virtual keyboard
234	// with the seat layout per use, and each resend makes every client
235	// (Xwayland included) recompile its keymap.
236	if s.activeKeymapText() != old {
237		s.sendKeymapAll(s.currentKeymap())
238	}
239	// Modifiers follow the keyboard that types: a skipped keymap no longer
240	// resets them, so a virtual keyboard never types through seat Shift or
241	// Caps Lock, and the seat gets its own back.
242	m := s.seat.modState
243	if k != nil {
244		m = k.mods
245	}
246	s.serial++
247	for _, kb := range s.clientKeyboards(s.focusClient()) {
248		kb.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
249	}
250	s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")
251}
252
253func (s *Server) sendKeymapAll(fd int, size uint32) {
254	for _, list := range s.seat.keyboards {
255		for _, kb := range list {
256			if kb.Resource.Alive() {
257				kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)
258			}
259		}
260	}
261}
262
263// activeKeymapText is the text of the keymap keyboards carry.
264func (s *Server) activeKeymapText() string {
265	if k := s.seat.keymapOwner; k != nil {
266		return k.text
267	}
268	return s.seat.keymapText
269}
270
271// currentKeymap is the keymap a new keyboard must start with.
272func (s *Server) currentKeymap() (int, uint32) {
273	if k := s.seat.keymapOwner; k != nil {
274		return k.fd, k.size
275	}
276	return s.seat.keymapFD, s.seat.keymapSize
277}

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