v0.1.0 d9218d6

virtualkeyboard.go RawHistoryFolder

1package wayland
2
3import (
4	"bytes"
5	"time"
6
7	"github.com/bnema/neferwl/internal/ports"
8	"github.com/bnema/purego-libwayland/protocol/virtualkeyboard"
9	"github.com/bnema/purego-libwayland/protocol/wayland"
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}
31
32// registerVirtualKeyboard exposes the global to every client, as wlroots
33// does by default: any local client may type into the focused window.
34func registerVirtualKeyboard(d *server.Display, s *Server) error {
35	return virtualkeyboard.NewZwpVirtualKeyboardManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
36		_, _ = virtualkeyboard.NewZwpVirtualKeyboardManagerV1(c, int32(v), id, virtualKeyboardManager{s})
37	})
38}
39
40type virtualKeyboardManager struct{ server *Server }
41
42func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirtualKeyboardManagerV1, _ *wayland.Seat, id uint32) {
43	k := &virtualKeyboard{server: m.server, fd: -1, pressed: map[uint32]bool{}}
44	res, err := virtualkeyboard.NewZwpVirtualKeyboardV1(r.Client(), r.Version(), id, k)
45	if err != nil {
46		return
47	}
48	res.OnDestroy = k.release
49	m.server.log.Info().Int("pid", r.Client().PID()).Msg("virtual keyboard created")
50}
51
52// Keymap copies the client keymap into a sealed memfd: other clients never
53// see the client's fd. An unreadable or malformed keymap is ignored.
54func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format uint32, fd int, size uint32) {
55	text, ok := readKeymap(fd, size)
56	unix.Close(fd)
57	if format != uint32(wayland.KeyboardKeymapFormatXkbV1) || !ok {
58		k.server.log.Debug().Msg("virtual keymap rejected")
59		return
60	}
61	if wide, from := widenKeycodes(text); from != 0 {
62		k.server.log.Debug().Int("maximum", from).Int("widened", minKeycodeMax).Msg("virtual keymap keycode range widened")
63		text = wide
64	}
65	copyFD, copySize, err := keymapFile(text)
66	if err != nil {
67		k.server.log.Warn().Err(err).Msg("virtual keymap copy failed")
68		return
69	}
70	if k.fd >= 0 {
71		unix.Close(k.fd)
72	}
73	k.fd, k.size = copyFD, copySize
74	old := k.server.activeKeymapText()
75	k.text = text
76	if s := k.server; s.keymapOwner == k && text != old {
77		// Clients must see the new layout before the next key.
78		s.sendKeymapAll(k.fd, k.size)
79	}
80}
81
82// readKeymap reads a NUL-terminated xkb keymap of size bytes from a
83// regular file.
84func readKeymap(fd int, size uint32) (string, bool) {
85	var st unix.Stat_t
86	if size == 0 || size > maxVirtualKeymap || unix.Fstat(fd, &st) != nil || st.Mode&unix.S_IFMT != unix.S_IFREG || st.Size < int64(size) {
87		return "", false
88	}
89	data := make([]byte, size)
90	if n, err := unix.Pread(fd, data, 0); err != nil || n != int(size) {
91		return "", false
92	}
93	if i := bytes.IndexByte(data, 0); i >= 0 {
94		data = data[:i]
95	}
96	if !bytes.Contains(data, []byte("xkb_keymap")) {
97		return "", false
98	}
99	return string(data), true
100}
101
102// keyboards switches the focused client to k's keymap and returns its
103// keyboards, or nil after a no_keymap error or without focus.
104func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []*wayland.Keyboard {
105	if k.fd < 0 {
106		r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "no keymap")
107		return nil
108	}
109	s := k.server
110	_, keyboards := s.focusTarget(s.focused)
111	if len(keyboards) == 0 {
112		return nil
113	}
114	if k.focus != s.focused {
115		clear(k.pressed)
116		k.focus = s.focused
117	}
118	s.useKeymap(k)
119	s.serial++
120	return keyboards
121}
122
123func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) {
124	keyboards := k.keyboards(r)
125	if keyboards == nil {
126		return
127	}
128	if state == 1 {
129		k.pressed[key] = true
130	} else {
131		delete(k.pressed, key)
132	}
133	for _, kb := range keyboards {
134		kb.SendKey(k.server.serial, time, key, state)
135	}
136}
137
138func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) {
139	k.mods = ports.ModState{Depressed: depressed, Latched: latched, Locked: locked, Group: group}
140	for _, kb := range k.keyboards(r) {
141		kb.SendModifiers(k.server.serial, depressed, latched, locked, group)
142	}
143}
144
145func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {}
146
147// release runs when the resource dies, by request or client disconnect:
148// keys still held on the focused client are released.
149func (k *virtualKeyboard) release() {
150	s := k.server
151	if len(k.pressed) > 0 && k.focus == s.focused {
152		_, keyboards := s.focusTarget(s.focused)
153		now := uint32(time.Now().UnixMilli())
154		for key := range k.pressed {
155			s.serial++
156			for _, kb := range keyboards {
157				kb.SendKey(s.serial, now, key, 0)
158			}
159		}
160	}
161	if s.keymapOwner == k {
162		s.useKeymap(nil)
163	}
164	if k.fd >= 0 {
165		unix.Close(k.fd)
166		k.fd = -1
167	}
168}
169
170// useKeymap makes every keyboard carry the keymap of k, or the seat keymap
171// when k is nil. Switching back restores the seat modifiers too.
172func (s *Server) useKeymap(k *virtualKeyboard) {
173	if s.keymapOwner == k {
174		return
175	}
176	old := s.activeKeymapText()
177	s.keymapOwner = k
178	// Like wlroots and smithay, a keymap equal to the active one is not
179	// sent again: tools such as dictation apps create a virtual keyboard
180	// with the seat layout per use, and each resend makes every client
181	// (Xwayland included) recompile its keymap.
182	if s.activeKeymapText() != old {
183		s.sendKeymapAll(s.currentKeymap())
184	}
185	// Modifiers follow the keyboard that types: a skipped keymap no longer
186	// resets them, so a virtual keyboard never types through seat Shift or
187	// Caps Lock, and the seat gets its own back.
188	m := s.modState
189	if k != nil {
190		m = k.mods
191	}
192	s.serial++
193	for _, kb := range s.clientKeyboards(s.focusClient()) {
194		kb.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
195	}
196	s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")
197}
198
199func (s *Server) sendKeymapAll(fd int, size uint32) {
200	for _, list := range s.keyboards {
201		for _, kb := range list {
202			if kb.Resource.Alive() {
203				kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)
204			}
205		}
206	}
207}
208
209// activeKeymapText is the text of the keymap keyboards carry.
210func (s *Server) activeKeymapText() string {
211	if k := s.keymapOwner; k != nil {
212		return k.text
213	}
214	return s.keymapText
215}
216
217// currentKeymap is the keymap a new keyboard must start with.
218func (s *Server) currentKeymap() (int, uint32) {
219	if k := s.keymapOwner; k != nil {
220		return k.fd, k.size
221	}
222	return s.keymapFD, s.keymapSize
223}

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