merge: virtual-keyboard (zwp_virtual_keyboard_v1 for wtype)
6 files · +389 -2
M go.mod +1 -1
| @@ -4,7 +4,7 @@ go 1.27 | |||
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/bnema/purego v0.11.0-bnema.4 | |
| 7 | - | github.com/bnema/purego-libwayland v0.1.0 | |
| 7 | + | github.com/bnema/purego-libwayland v0.1.1 | |
| 8 | 8 | github.com/bnema/purego-vulkan v0.4.0 | |
| 9 | 9 | github.com/bnema/wlturbo v0.2.0 | |
| 10 | 10 | github.com/bnema/zerowrap v1.4.1 | |
| @@ -4,7 +4,7 @@ go 1.27 | |||||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | github.com/bnema/purego v0.11.0-bnema.4 | 6 | github.com/bnema/purego v0.11.0-bnema.4 | ||
| 7 | - | github.com/bnema/purego-libwayland v0.1.0 | 7 | + | github.com/bnema/purego-libwayland v0.1.1 |
| 8 | github.com/bnema/purego-vulkan v0.4.0 | 8 | github.com/bnema/purego-vulkan v0.4.0 | ||
| 9 | github.com/bnema/wlturbo v0.2.0 | 9 | github.com/bnema/wlturbo v0.2.0 | ||
| 10 | github.com/bnema/zerowrap v1.4.1 | 10 | github.com/bnema/zerowrap v1.4.1 | ||
M go.sum +4 -0 generated file: collapsed
M internal/adapters/wayland/globals.go +3 -1
| @@ -20,6 +20,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 20 | 20 | func() error { return registerTearing(d, s) }, | |
| 21 | 21 | func() error { return registerPresentation(d, s) }, | |
| 22 | 22 | func() error { return registerActivation(d, s) }, | |
| 23 | + | func() error { return registerVirtualKeyboard(d, s) }, | |
| 23 | 24 | func() error { | |
| 24 | 25 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | |
| 25 | 26 | }, | |
| @@ -393,7 +394,8 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | |||
| 393 | 394 | return | |
| 394 | 395 | } | |
| 395 | 396 | s.keyboards[r.Client()] = append(s.keyboards[r.Client()], k) | |
| 396 | - | k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize) | |
| 397 | + | fd, size := s.currentKeymap() | |
| 398 | + | k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size) | |
| 397 | 399 | if r.Version() >= 4 { | |
| 398 | 400 | k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay)) | |
| 399 | 401 | } | |
| @@ -20,6 +20,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 20 | func() error { return registerTearing(d, s) }, | 20 | func() error { return registerTearing(d, s) }, | ||
| 21 | func() error { return registerPresentation(d, s) }, | 21 | func() error { return registerPresentation(d, s) }, | ||
| 22 | func() error { return registerActivation(d, s) }, | 22 | func() error { return registerActivation(d, s) }, | ||
| 23 | + | func() error { return registerVirtualKeyboard(d, s) }, | |||
| 23 | func() error { | 24 | func() error { | ||
| 24 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | 25 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | ||
| 25 | }, | 26 | }, | ||
| @@ -393,7 +394,8 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | |||||
| 393 | return | 394 | return | ||
| 394 | } | 395 | } | ||
| 395 | s.keyboards[r.Client()] = append(s.keyboards[r.Client()], k) | 396 | s.keyboards[r.Client()] = append(s.keyboards[r.Client()], k) | ||
| 396 | - | k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize) | 397 | + | fd, size := s.currentKeymap() |
| 398 | + | k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size) | |||
| 397 | if r.Version() >= 4 { | 399 | if r.Version() >= 4 { | ||
| 398 | k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay)) | 400 | k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay)) | ||
| 399 | } | 401 | } | ||
M internal/adapters/wayland/server.go +5 -0
| @@ -91,6 +91,7 @@ type Server struct { | |||
| 91 | 91 | heldKeys map[uint32]bool | |
| 92 | 92 | keymapFD int | |
| 93 | 93 | keymapSize uint32 | |
| 94 | + | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | |
| 94 | 95 | repeatRate, repeatDelay int | |
| 95 | 96 | // eventMu protects only the notification queue, not display-owned window state. | |
| 96 | 97 | // The event queue is unbounded by design: it grows only if core stops draining | |
| @@ -428,6 +429,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 428 | 429 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | |
| 429 | 430 | return | |
| 430 | 431 | } | |
| 432 | + | s.useKeymap(nil) | |
| 431 | 433 | if s.heldKeys == nil { | |
| 432 | 434 | s.heldKeys = make(map[uint32]bool) | |
| 433 | 435 | } | |
| @@ -599,6 +601,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) { | |||
| 599 | 601 | unix.Close(s.keymapFD) | |
| 600 | 602 | } | |
| 601 | 603 | s.keymapFD, s.keymapSize = fd, size | |
| 604 | + | s.keymapOwner = nil | |
| 602 | 605 | focused := s.focused | |
| 603 | 606 | s.changeFocus(0) | |
| 604 | 607 | s.heldKeys = nil | |
| @@ -621,6 +624,8 @@ func (s *Server) sendModifiers(k *wayland.Keyboard) { | |||
| 621 | 624 | } | |
| 622 | 625 | ||
| 623 | 626 | func (s *Server) changeFocus(id ports.WindowID) { | |
| 627 | + | // Enter carries seat held keys and modifiers: they need the seat keymap. | |
| 628 | + | s.useKeymap(nil) | |
| 624 | 629 | old := s.focusClient() | |
| 625 | 630 | defer func() { | |
| 626 | 631 | // Tokens of a client losing the focus die with it. | |
| @@ -91,6 +91,7 @@ type Server struct { | |||||
| 91 | heldKeys map[uint32]bool | 91 | heldKeys map[uint32]bool | ||
| 92 | keymapFD int | 92 | keymapFD int | ||
| 93 | keymapSize uint32 | 93 | keymapSize uint32 | ||
| 94 | + | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | |||
| 94 | repeatRate, repeatDelay int | 95 | repeatRate, repeatDelay int | ||
| 95 | // eventMu protects only the notification queue, not display-owned window state. | 96 | // eventMu protects only the notification queue, not display-owned window state. | ||
| 96 | // The event queue is unbounded by design: it grows only if core stops draining | 97 | // The event queue is unbounded by design: it grows only if core stops draining | ||
| @@ -428,6 +429,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 428 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | 429 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | ||
| 429 | return | 430 | return | ||
| 430 | } | 431 | } | ||
| 432 | + | s.useKeymap(nil) | |||
| 431 | if s.heldKeys == nil { | 433 | if s.heldKeys == nil { | ||
| 432 | s.heldKeys = make(map[uint32]bool) | 434 | s.heldKeys = make(map[uint32]bool) | ||
| 433 | } | 435 | } | ||
| @@ -599,6 +601,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) { | |||||
| 599 | unix.Close(s.keymapFD) | 601 | unix.Close(s.keymapFD) | ||
| 600 | } | 602 | } | ||
| 601 | s.keymapFD, s.keymapSize = fd, size | 603 | s.keymapFD, s.keymapSize = fd, size | ||
| 604 | + | s.keymapOwner = nil | |||
| 602 | focused := s.focused | 605 | focused := s.focused | ||
| 603 | s.changeFocus(0) | 606 | s.changeFocus(0) | ||
| 604 | s.heldKeys = nil | 607 | s.heldKeys = nil | ||
| @@ -621,6 +624,8 @@ func (s *Server) sendModifiers(k *wayland.Keyboard) { | |||||
| 621 | } | 624 | } | ||
| 622 | 625 | ||||
| 623 | func (s *Server) changeFocus(id ports.WindowID) { | 626 | func (s *Server) changeFocus(id ports.WindowID) { | ||
| 627 | + | // Enter carries seat held keys and modifiers: they need the seat keymap. | |||
| 628 | + | s.useKeymap(nil) | |||
| 624 | old := s.focusClient() | 629 | old := s.focusClient() | ||
| 625 | defer func() { | 630 | defer func() { | ||
| 626 | // Tokens of a client losing the focus die with it. | 631 | // Tokens of a client losing the focus die with it. | ||
A internal/adapters/wayland/virtualkeyboard.go +194 -0
| @@ -0,0 +1,194 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/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. | |
| 15 | + | const 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. | |
| 21 | + | type virtualKeyboard struct { | |
| 22 | + | server *Server | |
| 23 | + | fd int // sealed copy of the client keymap, -1 before one | |
| 24 | + | size uint32 | |
| 25 | + | // pressed are keys held on focus; a focus change releases them by leave. | |
| 26 | + | pressed map[uint32]bool | |
| 27 | + | focus ports.WindowID | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // registerVirtualKeyboard exposes the global to every client, as wlroots | |
| 31 | + | // does by default: any local client may type into the focused window. | |
| 32 | + | func registerVirtualKeyboard(d *server.Display, s *Server) error { | |
| 33 | + | return virtualkeyboard.NewZwpVirtualKeyboardManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 34 | + | _, _ = virtualkeyboard.NewZwpVirtualKeyboardManagerV1(c, int32(v), id, virtualKeyboardManager{s}) | |
| 35 | + | }) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | type virtualKeyboardManager struct{ server *Server } | |
| 39 | + | ||
| 40 | + | func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirtualKeyboardManagerV1, _ *wayland.Seat, id uint32) { | |
| 41 | + | k := &virtualKeyboard{server: m.server, fd: -1, pressed: map[uint32]bool{}} | |
| 42 | + | res, err := virtualkeyboard.NewZwpVirtualKeyboardV1(r.Client(), r.Version(), id, k) | |
| 43 | + | if err != nil { | |
| 44 | + | return | |
| 45 | + | } | |
| 46 | + | res.OnDestroy = k.release | |
| 47 | + | m.server.log.Info().Int("pid", r.Client().PID()).Msg("virtual keyboard created") | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Keymap copies the client keymap into a sealed memfd: other clients never | |
| 51 | + | // see the client's fd. An unreadable or malformed keymap is ignored. | |
| 52 | + | func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format uint32, fd int, size uint32) { | |
| 53 | + | text, ok := readKeymap(fd, size) | |
| 54 | + | unix.Close(fd) | |
| 55 | + | if format != uint32(wayland.KeyboardKeymapFormatXkbV1) || !ok { | |
| 56 | + | k.server.log.Debug().Msg("virtual keymap rejected") | |
| 57 | + | return | |
| 58 | + | } | |
| 59 | + | copyFD, copySize, err := keymapFile(text) | |
| 60 | + | if err != nil { | |
| 61 | + | k.server.log.Warn().Err(err).Msg("virtual keymap copy failed") | |
| 62 | + | return | |
| 63 | + | } | |
| 64 | + | if k.fd >= 0 { | |
| 65 | + | unix.Close(k.fd) | |
| 66 | + | } | |
| 67 | + | k.fd, k.size = copyFD, copySize | |
| 68 | + | if s := k.server; s.keymapOwner == k { | |
| 69 | + | // Clients must see the new layout before the next key. | |
| 70 | + | s.sendKeymapAll(k.fd, k.size) | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // readKeymap reads a NUL-terminated xkb keymap of size bytes from a | |
| 75 | + | // regular file. | |
| 76 | + | func readKeymap(fd int, size uint32) (string, bool) { | |
| 77 | + | var st unix.Stat_t | |
| 78 | + | if size == 0 || size > maxVirtualKeymap || unix.Fstat(fd, &st) != nil || st.Mode&unix.S_IFMT != unix.S_IFREG || st.Size < int64(size) { | |
| 79 | + | return "", false | |
| 80 | + | } | |
| 81 | + | data := make([]byte, size) | |
| 82 | + | if n, err := unix.Pread(fd, data, 0); err != nil || n != int(size) { | |
| 83 | + | return "", false | |
| 84 | + | } | |
| 85 | + | if i := bytes.IndexByte(data, 0); i >= 0 { | |
| 86 | + | data = data[:i] | |
| 87 | + | } | |
| 88 | + | if !bytes.Contains(data, []byte("xkb_keymap")) { | |
| 89 | + | return "", false | |
| 90 | + | } | |
| 91 | + | return string(data), true | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // keyboards switches the focused client to k's keymap and returns its | |
| 95 | + | // keyboards, or nil after a no_keymap error or without focus. | |
| 96 | + | func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []*wayland.Keyboard { | |
| 97 | + | if k.fd < 0 { | |
| 98 | + | r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "no keymap") | |
| 99 | + | return nil | |
| 100 | + | } | |
| 101 | + | s := k.server | |
| 102 | + | _, keyboards := s.focusTarget(s.focused) | |
| 103 | + | if len(keyboards) == 0 { | |
| 104 | + | return nil | |
| 105 | + | } | |
| 106 | + | if k.focus != s.focused { | |
| 107 | + | clear(k.pressed) | |
| 108 | + | k.focus = s.focused | |
| 109 | + | } | |
| 110 | + | s.useKeymap(k) | |
| 111 | + | s.serial++ | |
| 112 | + | return keyboards | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) { | |
| 116 | + | keyboards := k.keyboards(r) | |
| 117 | + | if keyboards == nil { | |
| 118 | + | return | |
| 119 | + | } | |
| 120 | + | if state == 1 { | |
| 121 | + | k.pressed[key] = true | |
| 122 | + | } else { | |
| 123 | + | delete(k.pressed, key) | |
| 124 | + | } | |
| 125 | + | for _, kb := range keyboards { | |
| 126 | + | kb.SendKey(k.server.serial, time, key, state) | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) { | |
| 131 | + | for _, kb := range k.keyboards(r) { | |
| 132 | + | kb.SendModifiers(k.server.serial, depressed, latched, locked, group) | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {} | |
| 137 | + | ||
| 138 | + | // release runs when the resource dies, by request or client disconnect: | |
| 139 | + | // keys still held on the focused client are released. | |
| 140 | + | func (k *virtualKeyboard) release() { | |
| 141 | + | s := k.server | |
| 142 | + | if len(k.pressed) > 0 && k.focus == s.focused { | |
| 143 | + | _, keyboards := s.focusTarget(s.focused) | |
| 144 | + | now := uint32(time.Now().UnixMilli()) | |
| 145 | + | for key := range k.pressed { | |
| 146 | + | s.serial++ | |
| 147 | + | for _, kb := range keyboards { | |
| 148 | + | kb.SendKey(s.serial, now, key, 0) | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | if s.keymapOwner == k { | |
| 153 | + | s.useKeymap(nil) | |
| 154 | + | } | |
| 155 | + | if k.fd >= 0 { | |
| 156 | + | unix.Close(k.fd) | |
| 157 | + | k.fd = -1 | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // useKeymap makes every keyboard carry the keymap of k, or the seat keymap | |
| 162 | + | // when k is nil. Switching back restores the seat modifiers too. | |
| 163 | + | func (s *Server) useKeymap(k *virtualKeyboard) { | |
| 164 | + | if s.keymapOwner == k { | |
| 165 | + | return | |
| 166 | + | } | |
| 167 | + | s.keymapOwner = k | |
| 168 | + | s.sendKeymapAll(s.currentKeymap()) | |
| 169 | + | if k == nil { | |
| 170 | + | s.serial++ | |
| 171 | + | for _, kb := range s.clientKeyboards(s.focusClient()) { | |
| 172 | + | s.sendModifiers(kb) | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched") | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | func (s *Server) sendKeymapAll(fd int, size uint32) { | |
| 179 | + | for _, list := range s.keyboards { | |
| 180 | + | for _, kb := range list { | |
| 181 | + | if kb.Resource.Alive() { | |
| 182 | + | kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size) | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | // currentKeymap is the keymap a new keyboard must start with. | |
| 189 | + | func (s *Server) currentKeymap() (int, uint32) { | |
| 190 | + | if k := s.keymapOwner; k != nil { | |
| 191 | + | return k.fd, k.size | |
| 192 | + | } | |
| 193 | + | return s.keymapFD, s.keymapSize | |
| 194 | + | } | |
| @@ -0,0 +1,194 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/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. | |||
| 15 | + | const 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. | |||
| 21 | + | type virtualKeyboard struct { | |||
| 22 | + | server *Server | |||
| 23 | + | fd int // sealed copy of the client keymap, -1 before one | |||
| 24 | + | size uint32 | |||
| 25 | + | // pressed are keys held on focus; a focus change releases them by leave. | |||
| 26 | + | pressed map[uint32]bool | |||
| 27 | + | focus ports.WindowID | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // registerVirtualKeyboard exposes the global to every client, as wlroots | |||
| 31 | + | // does by default: any local client may type into the focused window. | |||
| 32 | + | func registerVirtualKeyboard(d *server.Display, s *Server) error { | |||
| 33 | + | return virtualkeyboard.NewZwpVirtualKeyboardManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 34 | + | _, _ = virtualkeyboard.NewZwpVirtualKeyboardManagerV1(c, int32(v), id, virtualKeyboardManager{s}) | |||
| 35 | + | }) | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | type virtualKeyboardManager struct{ server *Server } | |||
| 39 | + | ||||
| 40 | + | func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirtualKeyboardManagerV1, _ *wayland.Seat, id uint32) { | |||
| 41 | + | k := &virtualKeyboard{server: m.server, fd: -1, pressed: map[uint32]bool{}} | |||
| 42 | + | res, err := virtualkeyboard.NewZwpVirtualKeyboardV1(r.Client(), r.Version(), id, k) | |||
| 43 | + | if err != nil { | |||
| 44 | + | return | |||
| 45 | + | } | |||
| 46 | + | res.OnDestroy = k.release | |||
| 47 | + | m.server.log.Info().Int("pid", r.Client().PID()).Msg("virtual keyboard created") | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Keymap copies the client keymap into a sealed memfd: other clients never | |||
| 51 | + | // see the client's fd. An unreadable or malformed keymap is ignored. | |||
| 52 | + | func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format uint32, fd int, size uint32) { | |||
| 53 | + | text, ok := readKeymap(fd, size) | |||
| 54 | + | unix.Close(fd) | |||
| 55 | + | if format != uint32(wayland.KeyboardKeymapFormatXkbV1) || !ok { | |||
| 56 | + | k.server.log.Debug().Msg("virtual keymap rejected") | |||
| 57 | + | return | |||
| 58 | + | } | |||
| 59 | + | copyFD, copySize, err := keymapFile(text) | |||
| 60 | + | if err != nil { | |||
| 61 | + | k.server.log.Warn().Err(err).Msg("virtual keymap copy failed") | |||
| 62 | + | return | |||
| 63 | + | } | |||
| 64 | + | if k.fd >= 0 { | |||
| 65 | + | unix.Close(k.fd) | |||
| 66 | + | } | |||
| 67 | + | k.fd, k.size = copyFD, copySize | |||
| 68 | + | if s := k.server; s.keymapOwner == k { | |||
| 69 | + | // Clients must see the new layout before the next key. | |||
| 70 | + | s.sendKeymapAll(k.fd, k.size) | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // readKeymap reads a NUL-terminated xkb keymap of size bytes from a | |||
| 75 | + | // regular file. | |||
| 76 | + | func readKeymap(fd int, size uint32) (string, bool) { | |||
| 77 | + | var st unix.Stat_t | |||
| 78 | + | if size == 0 || size > maxVirtualKeymap || unix.Fstat(fd, &st) != nil || st.Mode&unix.S_IFMT != unix.S_IFREG || st.Size < int64(size) { | |||
| 79 | + | return "", false | |||
| 80 | + | } | |||
| 81 | + | data := make([]byte, size) | |||
| 82 | + | if n, err := unix.Pread(fd, data, 0); err != nil || n != int(size) { | |||
| 83 | + | return "", false | |||
| 84 | + | } | |||
| 85 | + | if i := bytes.IndexByte(data, 0); i >= 0 { | |||
| 86 | + | data = data[:i] | |||
| 87 | + | } | |||
| 88 | + | if !bytes.Contains(data, []byte("xkb_keymap")) { | |||
| 89 | + | return "", false | |||
| 90 | + | } | |||
| 91 | + | return string(data), true | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // keyboards switches the focused client to k's keymap and returns its | |||
| 95 | + | // keyboards, or nil after a no_keymap error or without focus. | |||
| 96 | + | func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []*wayland.Keyboard { | |||
| 97 | + | if k.fd < 0 { | |||
| 98 | + | r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "no keymap") | |||
| 99 | + | return nil | |||
| 100 | + | } | |||
| 101 | + | s := k.server | |||
| 102 | + | _, keyboards := s.focusTarget(s.focused) | |||
| 103 | + | if len(keyboards) == 0 { | |||
| 104 | + | return nil | |||
| 105 | + | } | |||
| 106 | + | if k.focus != s.focused { | |||
| 107 | + | clear(k.pressed) | |||
| 108 | + | k.focus = s.focused | |||
| 109 | + | } | |||
| 110 | + | s.useKeymap(k) | |||
| 111 | + | s.serial++ | |||
| 112 | + | return keyboards | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) { | |||
| 116 | + | keyboards := k.keyboards(r) | |||
| 117 | + | if keyboards == nil { | |||
| 118 | + | return | |||
| 119 | + | } | |||
| 120 | + | if state == 1 { | |||
| 121 | + | k.pressed[key] = true | |||
| 122 | + | } else { | |||
| 123 | + | delete(k.pressed, key) | |||
| 124 | + | } | |||
| 125 | + | for _, kb := range keyboards { | |||
| 126 | + | kb.SendKey(k.server.serial, time, key, state) | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) { | |||
| 131 | + | for _, kb := range k.keyboards(r) { | |||
| 132 | + | kb.SendModifiers(k.server.serial, depressed, latched, locked, group) | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {} | |||
| 137 | + | ||||
| 138 | + | // release runs when the resource dies, by request or client disconnect: | |||
| 139 | + | // keys still held on the focused client are released. | |||
| 140 | + | func (k *virtualKeyboard) release() { | |||
| 141 | + | s := k.server | |||
| 142 | + | if len(k.pressed) > 0 && k.focus == s.focused { | |||
| 143 | + | _, keyboards := s.focusTarget(s.focused) | |||
| 144 | + | now := uint32(time.Now().UnixMilli()) | |||
| 145 | + | for key := range k.pressed { | |||
| 146 | + | s.serial++ | |||
| 147 | + | for _, kb := range keyboards { | |||
| 148 | + | kb.SendKey(s.serial, now, key, 0) | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | } | |||
| 152 | + | if s.keymapOwner == k { | |||
| 153 | + | s.useKeymap(nil) | |||
| 154 | + | } | |||
| 155 | + | if k.fd >= 0 { | |||
| 156 | + | unix.Close(k.fd) | |||
| 157 | + | k.fd = -1 | |||
| 158 | + | } | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // useKeymap makes every keyboard carry the keymap of k, or the seat keymap | |||
| 162 | + | // when k is nil. Switching back restores the seat modifiers too. | |||
| 163 | + | func (s *Server) useKeymap(k *virtualKeyboard) { | |||
| 164 | + | if s.keymapOwner == k { | |||
| 165 | + | return | |||
| 166 | + | } | |||
| 167 | + | s.keymapOwner = k | |||
| 168 | + | s.sendKeymapAll(s.currentKeymap()) | |||
| 169 | + | if k == nil { | |||
| 170 | + | s.serial++ | |||
| 171 | + | for _, kb := range s.clientKeyboards(s.focusClient()) { | |||
| 172 | + | s.sendModifiers(kb) | |||
| 173 | + | } | |||
| 174 | + | } | |||
| 175 | + | s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched") | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | func (s *Server) sendKeymapAll(fd int, size uint32) { | |||
| 179 | + | for _, list := range s.keyboards { | |||
| 180 | + | for _, kb := range list { | |||
| 181 | + | if kb.Resource.Alive() { | |||
| 182 | + | kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size) | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | } | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | // currentKeymap is the keymap a new keyboard must start with. | |||
| 189 | + | func (s *Server) currentKeymap() (int, uint32) { | |||
| 190 | + | if k := s.keymapOwner; k != nil { | |||
| 191 | + | return k.fd, k.size | |||
| 192 | + | } | |||
| 193 | + | return s.keymapFD, s.keymapSize | |||
| 194 | + | } | |||
A internal/adapters/wayland/virtualkeyboard_test.go +182 -0
| @@ -0,0 +1,182 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "strings" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/virtualkeyboard" | |
| 11 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 12 | + | "github.com/bnema/wlturbo" | |
| 13 | + | "golang.org/x/sys/unix" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // virtualTyper binds a virtual keyboard on its own connection, as wtype does. | |
| 17 | + | func virtualTyper(t *testing.T, s *Server, dir string) (*wlturbo.Display, uint32) { | |
| 18 | + | t.Helper() | |
| 19 | + | c := protocolClient(t, s, dir) | |
| 20 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 21 | + | registerProtocol(t, c, seat) | |
| 22 | + | manager := bindProtocol(t, c, "zwp_virtual_keyboard_manager_v1") | |
| 23 | + | registerProtocol(t, c, manager) | |
| 24 | + | kb := c.AllocateID() | |
| 25 | + | registerProtocol(t, c, kb) | |
| 26 | + | requestProtocol(t, c, manager, virtualkeyboard.ZwpVirtualKeyboardManagerV1RequestCreateVirtualKeyboard, seat, kb) | |
| 27 | + | return c, kb | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | func sendVirtualKeymap(t *testing.T, c *wlturbo.Display, kb uint32, keymap string) { | |
| 31 | + | t.Helper() | |
| 32 | + | fd, size, err := keymapFile(keymap) | |
| 33 | + | if err != nil { | |
| 34 | + | t.Fatal(err) | |
| 35 | + | } | |
| 36 | + | defer unix.Close(fd) | |
| 37 | + | if err := c.SendRequestWithFDs(kb, uint16(virtualkeyboard.ZwpVirtualKeyboardV1RequestKeymap), []int{fd}, uint32(wayland.KeyboardKeymapFormatXkbV1), size); err != nil { | |
| 38 | + | t.Fatal(err) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // Virtual keys reach the focused client under the virtual keymap; the next | |
| 43 | + | // real key switches it back to the seat keymap. | |
| 44 | + | func TestVirtualKeyboardTypesIntoFocus(t *testing.T) { | |
| 45 | + | s, events, commands, dir := keyboardServer(t) | |
| 46 | + | target := protocolClient(t, s, dir) | |
| 47 | + | seat := bindProtocol(t, target, "wl_seat") | |
| 48 | + | registerProtocol(t, target, seat) | |
| 49 | + | proxy := &eventsProxy{} | |
| 50 | + | proxy.SetID(target.AllocateID()) | |
| 51 | + | target.Context().Register(proxy) | |
| 52 | + | requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, proxy.ID()) | |
| 53 | + | w := toplevelMapper(t, target, events)() | |
| 54 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 55 | + | waitFocus(t, s, w.ID) | |
| 56 | + | if err := target.Roundtrip(); err != nil { | |
| 57 | + | t.Fatal(err) | |
| 58 | + | } | |
| 59 | + | proxy.opcodes, proxy.keymaps = nil, nil | |
| 60 | + | ||
| 61 | + | typer, kb := virtualTyper(t, s, dir) | |
| 62 | + | virtual := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1) | |
| 63 | + | sendVirtualKeymap(t, typer, kb, virtual) | |
| 64 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |
| 65 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0)) | |
| 66 | + | if err := typer.Roundtrip(); err != nil { | |
| 67 | + | t.Fatal(err) | |
| 68 | + | } | |
| 69 | + | if err := target.Roundtrip(); err != nil { | |
| 70 | + | t.Fatal(err) | |
| 71 | + | } | |
| 72 | + | want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventKey), uint16(wayland.KeyboardEventKey)} | |
| 73 | + | if !slices.Equal(proxy.opcodes, want) { | |
| 74 | + | t.Fatalf("virtual events %v, want %v", proxy.opcodes, want) | |
| 75 | + | } | |
| 76 | + | if len(proxy.keymaps) != 1 || proxy.keymaps[0] != virtual { | |
| 77 | + | t.Fatal("focused client did not get the virtual keymap") | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | proxy.opcodes, proxy.keymaps = nil, nil | |
| 81 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 3}} | |
| 82 | + | deadline := time.Now().Add(2 * time.Second) | |
| 83 | + | for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) { | |
| 84 | + | if err := target.Roundtrip(); err != nil { | |
| 85 | + | t.Fatal(err) | |
| 86 | + | } | |
| 87 | + | time.Sleep(5 * time.Millisecond) | |
| 88 | + | } | |
| 89 | + | want = []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey)} | |
| 90 | + | if !slices.Equal(proxy.opcodes, want) { | |
| 91 | + | t.Fatalf("real key events %v, want %v", proxy.opcodes, want) | |
| 92 | + | } | |
| 93 | + | if len(proxy.keymaps) != 1 || proxy.keymaps[0] != keymapText(t) { | |
| 94 | + | t.Fatal("seat keymap not restored") | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // focusedTarget maps a focused toplevel with one keyboard and returns its | |
| 99 | + | // connection and recorder, drained of setup events. | |
| 100 | + | func focusedTarget(t *testing.T, s *Server, events chan ports.ClientEvent, commands chan ports.ClientCommand, dir string) (*wlturbo.Display, *eventsProxy) { | |
| 101 | + | t.Helper() | |
| 102 | + | target := protocolClient(t, s, dir) | |
| 103 | + | seat := bindProtocol(t, target, "wl_seat") | |
| 104 | + | registerProtocol(t, target, seat) | |
| 105 | + | proxy := &eventsProxy{} | |
| 106 | + | proxy.SetID(target.AllocateID()) | |
| 107 | + | target.Context().Register(proxy) | |
| 108 | + | requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, proxy.ID()) | |
| 109 | + | w := toplevelMapper(t, target, events)() | |
| 110 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 111 | + | waitFocus(t, s, w.ID) | |
| 112 | + | if err := target.Roundtrip(); err != nil { | |
| 113 | + | t.Fatal(err) | |
| 114 | + | } | |
| 115 | + | proxy.opcodes, proxy.keymaps = nil, nil | |
| 116 | + | return target, proxy | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func roundtrip(t *testing.T, cs ...*wlturbo.Display) { | |
| 120 | + | t.Helper() | |
| 121 | + | for _, c := range cs { | |
| 122 | + | if err := c.Roundtrip(); err != nil { | |
| 123 | + | t.Fatal(err) | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // A typer dying mid-key releases the key and restores the seat keymap. | |
| 129 | + | func TestVirtualKeyboardDisconnectReleases(t *testing.T) { | |
| 130 | + | s, events, commands, dir := keyboardServer(t) | |
| 131 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |
| 132 | + | typer, kb := virtualTyper(t, s, dir) | |
| 133 | + | sendVirtualKeymap(t, typer, kb, strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)) | |
| 134 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestModifiers, uint32(1), uint32(0), uint32(0), uint32(0)) | |
| 135 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |
| 136 | + | roundtrip(t, typer) | |
| 137 | + | _ = typer.Close() | |
| 138 | + | deadline := time.Now().Add(2 * time.Second) | |
| 139 | + | for len(proxy.keymaps) < 2 && time.Now().Before(deadline) { | |
| 140 | + | roundtrip(t, target) | |
| 141 | + | time.Sleep(5 * time.Millisecond) | |
| 142 | + | } | |
| 143 | + | k, m := uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers) | |
| 144 | + | key := uint16(wayland.KeyboardEventKey) | |
| 145 | + | want := []uint16{k, m, key, key, k, m} | |
| 146 | + | if !slices.Equal(proxy.opcodes, want) { | |
| 147 | + | t.Fatalf("events %v, want %v", proxy.opcodes, want) | |
| 148 | + | } | |
| 149 | + | if proxy.keymaps[1] != keymapText(t) { | |
| 150 | + | t.Fatal("seat keymap not restored") | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | // A keyboard created while a typer owns the keymap starts with it; a | |
| 155 | + | // malformed keymap is ignored and the last good one stays. | |
| 156 | + | func TestVirtualKeymapLateKeyboardAndRejected(t *testing.T) { | |
| 157 | + | s, events, commands, dir := keyboardServer(t) | |
| 158 | + | target, _ := focusedTarget(t, s, events, commands, dir) | |
| 159 | + | typer, kb := virtualTyper(t, s, dir) | |
| 160 | + | virtual := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1) | |
| 161 | + | sendVirtualKeymap(t, typer, kb, virtual) | |
| 162 | + | sendVirtualKeymap(t, typer, kb, "not a keymap") | |
| 163 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |
| 164 | + | roundtrip(t, typer, target) | |
| 165 | + | seat := bindProtocol(t, target, "wl_seat") | |
| 166 | + | registerProtocol(t, target, seat) | |
| 167 | + | late := &eventsProxy{} | |
| 168 | + | late.SetID(target.AllocateID()) | |
| 169 | + | target.Context().Register(late) | |
| 170 | + | requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, late.ID()) | |
| 171 | + | roundtrip(t, target) | |
| 172 | + | if len(late.keymaps) != 1 || late.keymaps[0] != virtual { | |
| 173 | + | t.Fatal("late keyboard did not get the virtual keymap") | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | func TestVirtualKeyBeforeKeymap(t *testing.T) { | |
| 178 | + | s, _, _, dir := keyboardServer(t) | |
| 179 | + | typer, kb := virtualTyper(t, s, dir) | |
| 180 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |
| 181 | + | expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap)) | |
| 182 | + | } | |
| @@ -0,0 +1,182 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "strings" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/virtualkeyboard" | |||
| 11 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 12 | + | "github.com/bnema/wlturbo" | |||
| 13 | + | "golang.org/x/sys/unix" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // virtualTyper binds a virtual keyboard on its own connection, as wtype does. | |||
| 17 | + | func virtualTyper(t *testing.T, s *Server, dir string) (*wlturbo.Display, uint32) { | |||
| 18 | + | t.Helper() | |||
| 19 | + | c := protocolClient(t, s, dir) | |||
| 20 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 21 | + | registerProtocol(t, c, seat) | |||
| 22 | + | manager := bindProtocol(t, c, "zwp_virtual_keyboard_manager_v1") | |||
| 23 | + | registerProtocol(t, c, manager) | |||
| 24 | + | kb := c.AllocateID() | |||
| 25 | + | registerProtocol(t, c, kb) | |||
| 26 | + | requestProtocol(t, c, manager, virtualkeyboard.ZwpVirtualKeyboardManagerV1RequestCreateVirtualKeyboard, seat, kb) | |||
| 27 | + | return c, kb | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | func sendVirtualKeymap(t *testing.T, c *wlturbo.Display, kb uint32, keymap string) { | |||
| 31 | + | t.Helper() | |||
| 32 | + | fd, size, err := keymapFile(keymap) | |||
| 33 | + | if err != nil { | |||
| 34 | + | t.Fatal(err) | |||
| 35 | + | } | |||
| 36 | + | defer unix.Close(fd) | |||
| 37 | + | if err := c.SendRequestWithFDs(kb, uint16(virtualkeyboard.ZwpVirtualKeyboardV1RequestKeymap), []int{fd}, uint32(wayland.KeyboardKeymapFormatXkbV1), size); err != nil { | |||
| 38 | + | t.Fatal(err) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // Virtual keys reach the focused client under the virtual keymap; the next | |||
| 43 | + | // real key switches it back to the seat keymap. | |||
| 44 | + | func TestVirtualKeyboardTypesIntoFocus(t *testing.T) { | |||
| 45 | + | s, events, commands, dir := keyboardServer(t) | |||
| 46 | + | target := protocolClient(t, s, dir) | |||
| 47 | + | seat := bindProtocol(t, target, "wl_seat") | |||
| 48 | + | registerProtocol(t, target, seat) | |||
| 49 | + | proxy := &eventsProxy{} | |||
| 50 | + | proxy.SetID(target.AllocateID()) | |||
| 51 | + | target.Context().Register(proxy) | |||
| 52 | + | requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, proxy.ID()) | |||
| 53 | + | w := toplevelMapper(t, target, events)() | |||
| 54 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 55 | + | waitFocus(t, s, w.ID) | |||
| 56 | + | if err := target.Roundtrip(); err != nil { | |||
| 57 | + | t.Fatal(err) | |||
| 58 | + | } | |||
| 59 | + | proxy.opcodes, proxy.keymaps = nil, nil | |||
| 60 | + | ||||
| 61 | + | typer, kb := virtualTyper(t, s, dir) | |||
| 62 | + | virtual := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1) | |||
| 63 | + | sendVirtualKeymap(t, typer, kb, virtual) | |||
| 64 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |||
| 65 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0)) | |||
| 66 | + | if err := typer.Roundtrip(); err != nil { | |||
| 67 | + | t.Fatal(err) | |||
| 68 | + | } | |||
| 69 | + | if err := target.Roundtrip(); err != nil { | |||
| 70 | + | t.Fatal(err) | |||
| 71 | + | } | |||
| 72 | + | want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventKey), uint16(wayland.KeyboardEventKey)} | |||
| 73 | + | if !slices.Equal(proxy.opcodes, want) { | |||
| 74 | + | t.Fatalf("virtual events %v, want %v", proxy.opcodes, want) | |||
| 75 | + | } | |||
| 76 | + | if len(proxy.keymaps) != 1 || proxy.keymaps[0] != virtual { | |||
| 77 | + | t.Fatal("focused client did not get the virtual keymap") | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | proxy.opcodes, proxy.keymaps = nil, nil | |||
| 81 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 3}} | |||
| 82 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 83 | + | for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) { | |||
| 84 | + | if err := target.Roundtrip(); err != nil { | |||
| 85 | + | t.Fatal(err) | |||
| 86 | + | } | |||
| 87 | + | time.Sleep(5 * time.Millisecond) | |||
| 88 | + | } | |||
| 89 | + | want = []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey)} | |||
| 90 | + | if !slices.Equal(proxy.opcodes, want) { | |||
| 91 | + | t.Fatalf("real key events %v, want %v", proxy.opcodes, want) | |||
| 92 | + | } | |||
| 93 | + | if len(proxy.keymaps) != 1 || proxy.keymaps[0] != keymapText(t) { | |||
| 94 | + | t.Fatal("seat keymap not restored") | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // focusedTarget maps a focused toplevel with one keyboard and returns its | |||
| 99 | + | // connection and recorder, drained of setup events. | |||
| 100 | + | func focusedTarget(t *testing.T, s *Server, events chan ports.ClientEvent, commands chan ports.ClientCommand, dir string) (*wlturbo.Display, *eventsProxy) { | |||
| 101 | + | t.Helper() | |||
| 102 | + | target := protocolClient(t, s, dir) | |||
| 103 | + | seat := bindProtocol(t, target, "wl_seat") | |||
| 104 | + | registerProtocol(t, target, seat) | |||
| 105 | + | proxy := &eventsProxy{} | |||
| 106 | + | proxy.SetID(target.AllocateID()) | |||
| 107 | + | target.Context().Register(proxy) | |||
| 108 | + | requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, proxy.ID()) | |||
| 109 | + | w := toplevelMapper(t, target, events)() | |||
| 110 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 111 | + | waitFocus(t, s, w.ID) | |||
| 112 | + | if err := target.Roundtrip(); err != nil { | |||
| 113 | + | t.Fatal(err) | |||
| 114 | + | } | |||
| 115 | + | proxy.opcodes, proxy.keymaps = nil, nil | |||
| 116 | + | return target, proxy | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | func roundtrip(t *testing.T, cs ...*wlturbo.Display) { | |||
| 120 | + | t.Helper() | |||
| 121 | + | for _, c := range cs { | |||
| 122 | + | if err := c.Roundtrip(); err != nil { | |||
| 123 | + | t.Fatal(err) | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // A typer dying mid-key releases the key and restores the seat keymap. | |||
| 129 | + | func TestVirtualKeyboardDisconnectReleases(t *testing.T) { | |||
| 130 | + | s, events, commands, dir := keyboardServer(t) | |||
| 131 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |||
| 132 | + | typer, kb := virtualTyper(t, s, dir) | |||
| 133 | + | sendVirtualKeymap(t, typer, kb, strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)) | |||
| 134 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestModifiers, uint32(1), uint32(0), uint32(0), uint32(0)) | |||
| 135 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |||
| 136 | + | roundtrip(t, typer) | |||
| 137 | + | _ = typer.Close() | |||
| 138 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 139 | + | for len(proxy.keymaps) < 2 && time.Now().Before(deadline) { | |||
| 140 | + | roundtrip(t, target) | |||
| 141 | + | time.Sleep(5 * time.Millisecond) | |||
| 142 | + | } | |||
| 143 | + | k, m := uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers) | |||
| 144 | + | key := uint16(wayland.KeyboardEventKey) | |||
| 145 | + | want := []uint16{k, m, key, key, k, m} | |||
| 146 | + | if !slices.Equal(proxy.opcodes, want) { | |||
| 147 | + | t.Fatalf("events %v, want %v", proxy.opcodes, want) | |||
| 148 | + | } | |||
| 149 | + | if proxy.keymaps[1] != keymapText(t) { | |||
| 150 | + | t.Fatal("seat keymap not restored") | |||
| 151 | + | } | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | // A keyboard created while a typer owns the keymap starts with it; a | |||
| 155 | + | // malformed keymap is ignored and the last good one stays. | |||
| 156 | + | func TestVirtualKeymapLateKeyboardAndRejected(t *testing.T) { | |||
| 157 | + | s, events, commands, dir := keyboardServer(t) | |||
| 158 | + | target, _ := focusedTarget(t, s, events, commands, dir) | |||
| 159 | + | typer, kb := virtualTyper(t, s, dir) | |||
| 160 | + | virtual := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1) | |||
| 161 | + | sendVirtualKeymap(t, typer, kb, virtual) | |||
| 162 | + | sendVirtualKeymap(t, typer, kb, "not a keymap") | |||
| 163 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |||
| 164 | + | roundtrip(t, typer, target) | |||
| 165 | + | seat := bindProtocol(t, target, "wl_seat") | |||
| 166 | + | registerProtocol(t, target, seat) | |||
| 167 | + | late := &eventsProxy{} | |||
| 168 | + | late.SetID(target.AllocateID()) | |||
| 169 | + | target.Context().Register(late) | |||
| 170 | + | requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, late.ID()) | |||
| 171 | + | roundtrip(t, target) | |||
| 172 | + | if len(late.keymaps) != 1 || late.keymaps[0] != virtual { | |||
| 173 | + | t.Fatal("late keyboard did not get the virtual keymap") | |||
| 174 | + | } | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | func TestVirtualKeyBeforeKeymap(t *testing.T) { | |||
| 178 | + | s, _, _, dir := keyboardServer(t) | |||
| 179 | + | typer, kb := virtualTyper(t, s, dir) | |||
| 180 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |||
| 181 | + | expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap)) | |||
| 182 | + | } | |||