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}