fix(wayland): copy virtual keymaps into sealed memfds, release held virtual keys on disconnect, restore seat keymap on focus change
- commit
- 56b509a0b220572f2ef6db797257d6c1f2ce04fa
- author
- brice · · 2026-09-26 09:09 UTC
- parent
- 4e14261
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +171 -27
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.1-0.20260926085626-ac96fe0bd3c3 | |
| 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.1-0.20260926085626-ac96fe0bd3c3 | 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 +2 -0 generated file: collapsed
M internal/adapters/wayland/server.go +2 -0
| @@ -624,6 +624,8 @@ func (s *Server) sendModifiers(k *wayland.Keyboard) { | |||
| 624 | 624 | } | |
| 625 | 625 | ||
| 626 | 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) | |
| 627 | 629 | old := s.focusClient() | |
| 628 | 630 | defer func() { | |
| 629 | 631 | // Tokens of a client losing the focus die with it. | |
| @@ -624,6 +624,8 @@ func (s *Server) sendModifiers(k *wayland.Keyboard) { | |||||
| 624 | } | 624 | } | ||
| 625 | 625 | ||||
| 626 | 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) | |||
| 627 | old := s.focusClient() | 629 | old := s.focusClient() | ||
| 628 | defer func() { | 630 | defer func() { | ||
| 629 | // Tokens of a client losing the focus die with it. | 631 | // Tokens of a client losing the focus die with it. | ||
M internal/adapters/wayland/virtualkeyboard.go +87 -26
| @@ -1,22 +1,34 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 4 | 8 | "github.com/bnema/purego-libwayland/protocol/virtualkeyboard" | |
| 5 | 9 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 6 | 10 | "github.com/bnema/purego-libwayland/server" | |
| 7 | 11 | "golang.org/x/sys/unix" | |
| 8 | 12 | ) | |
| 9 | 13 | ||
| 14 | + | // maxVirtualKeymap bounds a keymap read from a virtual keyboard client. | |
| 15 | + | const maxVirtualKeymap = 1 << 20 | |
| 16 | + | ||
| 10 | 17 | // virtualKeyboard is a zwp_virtual_keyboard_v1 (wtype, dictation tools). | |
| 11 | 18 | // Its keys go to the focused client. The focused client's keyboards carry | |
| 12 | 19 | // the keymap of whichever keyboard typed last: a virtual key switches them | |
| 13 | - | // to the virtual keymap, the next real key switches them back. | |
| 20 | + | // to the virtual keymap, the next real key or focus change switches back. | |
| 14 | 21 | type virtualKeyboard struct { | |
| 15 | 22 | server *Server | |
| 16 | - | fd int | |
| 23 | + | fd int // sealed copy of the client keymap, -1 before one | |
| 17 | 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 | |
| 18 | 28 | } | |
| 19 | 29 | ||
| 30 | + | // registerVirtualKeyboard exposes the global to every client, as wlroots | |
| 31 | + | // does by default: any local client may type into the focused window. | |
| 20 | 32 | func registerVirtualKeyboard(d *server.Display, s *Server) error { | |
| 21 | 33 | return virtualkeyboard.NewZwpVirtualKeyboardManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 22 | 34 | _, _ = virtualkeyboard.NewZwpVirtualKeyboardManagerV1(c, int32(v), id, virtualKeyboardManager{s}) | |
| @@ -26,7 +38,7 @@ func registerVirtualKeyboard(d *server.Display, s *Server) error { | |||
| 26 | 38 | type virtualKeyboardManager struct{ server *Server } | |
| 27 | 39 | ||
| 28 | 40 | func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirtualKeyboardManagerV1, _ *wayland.Seat, id uint32) { | |
| 29 | - | k := &virtualKeyboard{server: m.server, fd: -1} | |
| 41 | + | k := &virtualKeyboard{server: m.server, fd: -1, pressed: map[uint32]bool{}} | |
| 30 | 42 | res, err := virtualkeyboard.NewZwpVirtualKeyboardV1(r.Client(), r.Version(), id, k) | |
| 31 | 43 | if err != nil { | |
| 32 | 44 | return | |
| @@ -35,60 +47,109 @@ func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirt | |||
| 35 | 47 | m.server.log.Info().Int("pid", r.Client().PID()).Msg("virtual keyboard created") | |
| 36 | 48 | } | |
| 37 | 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. | |
| 38 | 52 | func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format uint32, fd int, size uint32) { | |
| 39 | - | if format != uint32(wayland.KeyboardKeymapFormatXkbV1) || size == 0 { | |
| 40 | - | unix.Close(fd) | |
| 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") | |
| 41 | 62 | return | |
| 42 | 63 | } | |
| 43 | 64 | if k.fd >= 0 { | |
| 44 | 65 | unix.Close(k.fd) | |
| 45 | 66 | } | |
| 46 | - | k.fd, k.size = fd, size | |
| 67 | + | k.fd, k.size = copyFD, copySize | |
| 47 | 68 | if s := k.server; s.keymapOwner == k { | |
| 48 | 69 | // Clients must see the new layout before the next key. | |
| 49 | - | s.sendKeymapAll(fd, size) | |
| 70 | + | s.sendKeymapAll(k.fd, k.size) | |
| 50 | 71 | } | |
| 51 | 72 | } | |
| 52 | 73 | ||
| 53 | - | func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) { | |
| 54 | - | if k.fd < 0 { | |
| 55 | - | r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "key before keymap") | |
| 56 | - | return | |
| 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 | |
| 57 | 80 | } | |
| 58 | - | s := k.server | |
| 59 | - | _, keyboards := s.focusTarget(s.focused) | |
| 60 | - | if len(keyboards) == 0 { | |
| 61 | - | return | |
| 81 | + | data := make([]byte, size) | |
| 82 | + | if n, err := unix.Pread(fd, data, 0); err != nil || n != int(size) { | |
| 83 | + | return "", false | |
| 62 | 84 | } | |
| 63 | - | s.useKeymap(k) | |
| 64 | - | s.serial++ | |
| 65 | - | for _, kb := range keyboards { | |
| 66 | - | kb.SendKey(s.serial, time, key, state) | |
| 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 | |
| 67 | 90 | } | |
| 91 | + | return string(data), true | |
| 68 | 92 | } | |
| 69 | 93 | ||
| 70 | - | func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) { | |
| 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 { | |
| 71 | 97 | if k.fd < 0 { | |
| 72 | - | r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "modifiers before keymap") | |
| 73 | - | return | |
| 98 | + | r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "no keymap") | |
| 99 | + | return nil | |
| 74 | 100 | } | |
| 75 | 101 | s := k.server | |
| 76 | 102 | _, keyboards := s.focusTarget(s.focused) | |
| 77 | 103 | if len(keyboards) == 0 { | |
| 78 | - | return | |
| 104 | + | return nil | |
| 105 | + | } | |
| 106 | + | if k.focus != s.focused { | |
| 107 | + | clear(k.pressed) | |
| 108 | + | k.focus = s.focused | |
| 79 | 109 | } | |
| 80 | 110 | s.useKeymap(k) | |
| 81 | 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 | + | } | |
| 82 | 125 | for _, kb := range keyboards { | |
| 83 | - | kb.SendModifiers(s.serial, depressed, latched, locked, group) | |
| 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) | |
| 84 | 133 | } | |
| 85 | 134 | } | |
| 86 | 135 | ||
| 87 | 136 | func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {} | |
| 88 | 137 | ||
| 89 | - | // release runs when the resource dies, by request or client disconnect. | |
| 138 | + | // release runs when the resource dies, by request or client disconnect: | |
| 139 | + | // keys still held on the focused client are released. | |
| 90 | 140 | func (k *virtualKeyboard) release() { | |
| 91 | - | if s := k.server; s.keymapOwner == k { | |
| 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 { | |
| 92 | 153 | s.useKeymap(nil) | |
| 93 | 154 | } | |
| 94 | 155 | if k.fd >= 0 { | |
| @@ -1,22 +1,34 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "bytes" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 4 | "github.com/bnema/purego-libwayland/protocol/virtualkeyboard" | 8 | "github.com/bnema/purego-libwayland/protocol/virtualkeyboard" | ||
| 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | 9 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 6 | "github.com/bnema/purego-libwayland/server" | 10 | "github.com/bnema/purego-libwayland/server" | ||
| 7 | "golang.org/x/sys/unix" | 11 | "golang.org/x/sys/unix" | ||
| 8 | ) | 12 | ) | ||
| 9 | 13 | ||||
| 14 | + | // maxVirtualKeymap bounds a keymap read from a virtual keyboard client. | |||
| 15 | + | const maxVirtualKeymap = 1 << 20 | |||
| 16 | + | ||||
| 10 | // virtualKeyboard is a zwp_virtual_keyboard_v1 (wtype, dictation tools). | 17 | // virtualKeyboard is a zwp_virtual_keyboard_v1 (wtype, dictation tools). | ||
| 11 | // Its keys go to the focused client. The focused client's keyboards carry | 18 | // Its keys go to the focused client. The focused client's keyboards carry | ||
| 12 | // the keymap of whichever keyboard typed last: a virtual key switches them | 19 | // the keymap of whichever keyboard typed last: a virtual key switches them | ||
| 13 | - | // to the virtual keymap, the next real key switches them back. | 20 | + | // to the virtual keymap, the next real key or focus change switches back. |
| 14 | type virtualKeyboard struct { | 21 | type virtualKeyboard struct { | ||
| 15 | server *Server | 22 | server *Server | ||
| 16 | - | fd int | 23 | + | fd int // sealed copy of the client keymap, -1 before one |
| 17 | size uint32 | 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 | |||
| 18 | } | 28 | } | ||
| 19 | 29 | ||||
| 30 | + | // registerVirtualKeyboard exposes the global to every client, as wlroots | |||
| 31 | + | // does by default: any local client may type into the focused window. | |||
| 20 | func registerVirtualKeyboard(d *server.Display, s *Server) error { | 32 | func registerVirtualKeyboard(d *server.Display, s *Server) error { | ||
| 21 | return virtualkeyboard.NewZwpVirtualKeyboardManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | 33 | return virtualkeyboard.NewZwpVirtualKeyboardManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | ||
| 22 | _, _ = virtualkeyboard.NewZwpVirtualKeyboardManagerV1(c, int32(v), id, virtualKeyboardManager{s}) | 34 | _, _ = virtualkeyboard.NewZwpVirtualKeyboardManagerV1(c, int32(v), id, virtualKeyboardManager{s}) | ||
| @@ -26,7 +38,7 @@ func registerVirtualKeyboard(d *server.Display, s *Server) error { | |||||
| 26 | type virtualKeyboardManager struct{ server *Server } | 38 | type virtualKeyboardManager struct{ server *Server } | ||
| 27 | 39 | ||||
| 28 | func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirtualKeyboardManagerV1, _ *wayland.Seat, id uint32) { | 40 | func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirtualKeyboardManagerV1, _ *wayland.Seat, id uint32) { | ||
| 29 | - | k := &virtualKeyboard{server: m.server, fd: -1} | 41 | + | k := &virtualKeyboard{server: m.server, fd: -1, pressed: map[uint32]bool{}} |
| 30 | res, err := virtualkeyboard.NewZwpVirtualKeyboardV1(r.Client(), r.Version(), id, k) | 42 | res, err := virtualkeyboard.NewZwpVirtualKeyboardV1(r.Client(), r.Version(), id, k) | ||
| 31 | if err != nil { | 43 | if err != nil { | ||
| 32 | return | 44 | return | ||
| @@ -35,60 +47,109 @@ func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirt | |||||
| 35 | m.server.log.Info().Int("pid", r.Client().PID()).Msg("virtual keyboard created") | 47 | m.server.log.Info().Int("pid", r.Client().PID()).Msg("virtual keyboard created") | ||
| 36 | } | 48 | } | ||
| 37 | 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. | |||
| 38 | func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format uint32, fd int, size uint32) { | 52 | func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format uint32, fd int, size uint32) { | ||
| 39 | - | if format != uint32(wayland.KeyboardKeymapFormatXkbV1) || size == 0 { | 53 | + | text, ok := readKeymap(fd, size) |
| 40 | - | unix.Close(fd) | 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") | |||
| 41 | return | 62 | return | ||
| 42 | } | 63 | } | ||
| 43 | if k.fd >= 0 { | 64 | if k.fd >= 0 { | ||
| 44 | unix.Close(k.fd) | 65 | unix.Close(k.fd) | ||
| 45 | } | 66 | } | ||
| 46 | - | k.fd, k.size = fd, size | 67 | + | k.fd, k.size = copyFD, copySize |
| 47 | if s := k.server; s.keymapOwner == k { | 68 | if s := k.server; s.keymapOwner == k { | ||
| 48 | // Clients must see the new layout before the next key. | 69 | // Clients must see the new layout before the next key. | ||
| 49 | - | s.sendKeymapAll(fd, size) | 70 | + | s.sendKeymapAll(k.fd, k.size) |
| 50 | } | 71 | } | ||
| 51 | } | 72 | } | ||
| 52 | 73 | ||||
| 53 | - | func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) { | 74 | + | // readKeymap reads a NUL-terminated xkb keymap of size bytes from a |
| 54 | - | if k.fd < 0 { | 75 | + | // regular file. |
| 55 | - | r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "key before keymap") | 76 | + | func readKeymap(fd int, size uint32) (string, bool) { |
| 56 | - | return | 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 | |||
| 57 | } | 80 | } | ||
| 58 | - | s := k.server | 81 | + | data := make([]byte, size) |
| 59 | - | _, keyboards := s.focusTarget(s.focused) | 82 | + | if n, err := unix.Pread(fd, data, 0); err != nil || n != int(size) { |
| 60 | - | if len(keyboards) == 0 { | 83 | + | return "", false |
| 61 | - | return | |||
| 62 | } | 84 | } | ||
| 63 | - | s.useKeymap(k) | 85 | + | if i := bytes.IndexByte(data, 0); i >= 0 { |
| 64 | - | s.serial++ | 86 | + | data = data[:i] |
| 65 | - | for _, kb := range keyboards { | 87 | + | } |
| 66 | - | kb.SendKey(s.serial, time, key, state) | 88 | + | if !bytes.Contains(data, []byte("xkb_keymap")) { |
| 89 | + | return "", false | |||
| 67 | } | 90 | } | ||
| 91 | + | return string(data), true | |||
| 68 | } | 92 | } | ||
| 69 | 93 | ||||
| 70 | - | func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) { | 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 { | |||
| 71 | if k.fd < 0 { | 97 | if k.fd < 0 { | ||
| 72 | - | r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "modifiers before keymap") | 98 | + | r.PostError(uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap), "no keymap") |
| 73 | - | return | 99 | + | return nil |
| 74 | } | 100 | } | ||
| 75 | s := k.server | 101 | s := k.server | ||
| 76 | _, keyboards := s.focusTarget(s.focused) | 102 | _, keyboards := s.focusTarget(s.focused) | ||
| 77 | if len(keyboards) == 0 { | 103 | if len(keyboards) == 0 { | ||
| 78 | - | return | 104 | + | return nil |
| 105 | + | } | |||
| 106 | + | if k.focus != s.focused { | |||
| 107 | + | clear(k.pressed) | |||
| 108 | + | k.focus = s.focused | |||
| 79 | } | 109 | } | ||
| 80 | s.useKeymap(k) | 110 | s.useKeymap(k) | ||
| 81 | s.serial++ | 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 | + | } | |||
| 82 | for _, kb := range keyboards { | 125 | for _, kb := range keyboards { | ||
| 83 | - | kb.SendModifiers(s.serial, depressed, latched, locked, group) | 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) | |||
| 84 | } | 133 | } | ||
| 85 | } | 134 | } | ||
| 86 | 135 | ||||
| 87 | func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {} | 136 | func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {} | ||
| 88 | 137 | ||||
| 89 | - | // release runs when the resource dies, by request or client disconnect. | 138 | + | // release runs when the resource dies, by request or client disconnect: |
| 139 | + | // keys still held on the focused client are released. | |||
| 90 | func (k *virtualKeyboard) release() { | 140 | func (k *virtualKeyboard) release() { | ||
| 91 | - | if s := k.server; s.keymapOwner == k { | 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 { | |||
| 92 | s.useKeymap(nil) | 153 | s.useKeymap(nil) | ||
| 93 | } | 154 | } | ||
| 94 | if k.fd >= 0 { | 155 | if k.fd >= 0 { | ||
M internal/adapters/wayland/virtualkeyboard_test.go +79 -0
| @@ -95,6 +95,85 @@ func TestVirtualKeyboardTypesIntoFocus(t *testing.T) { | |||
| 95 | 95 | } | |
| 96 | 96 | } | |
| 97 | 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 | + | ||
| 98 | 177 | func TestVirtualKeyBeforeKeymap(t *testing.T) { | |
| 99 | 178 | s, _, _, dir := keyboardServer(t) | |
| 100 | 179 | typer, kb := virtualTyper(t, s, dir) | |
| @@ -95,6 +95,85 @@ func TestVirtualKeyboardTypesIntoFocus(t *testing.T) { | |||||
| 95 | } | 95 | } | ||
| 96 | } | 96 | } | ||
| 97 | 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 | + | ||||
| 98 | func TestVirtualKeyBeforeKeymap(t *testing.T) { | 177 | func TestVirtualKeyBeforeKeymap(t *testing.T) { | ||
| 99 | s, _, _, dir := keyboardServer(t) | 178 | s, _, _, dir := keyboardServer(t) | ||
| 100 | typer, kb := virtualTyper(t, s, dir) | 179 | typer, kb := virtualTyper(t, s, dir) | ||