detached cc9b1b3

merge: virtual-keyboard (zwp_virtual_keyboard_v1 for wtype)

commit
cc9b1b3b72506348a94ec710cab7577bbc4ed3cc
author
brice · · 2026-09-26 09:09 UTC
merge of
7e09e66 + 56b509a · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +389 -2
M go.mod +1 -1
@@ -4,7 +4,7 @@ go 1.27
44
55 require (
66 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
88 github.com/bnema/purego-vulkan v0.4.0
99 github.com/bnema/wlturbo v0.2.0
1010 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.46 github.com/bnema/purego v0.11.0-bnema.4
7- github.com/bnema/purego-libwayland v0.1.07+ github.com/bnema/purego-libwayland v0.1.1
8 github.com/bnema/purego-vulkan v0.4.08 github.com/bnema/purego-vulkan v0.4.0
9 github.com/bnema/wlturbo v0.2.09 github.com/bnema/wlturbo v0.2.0
10 github.com/bnema/zerowrap v1.4.110 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 {
2020 func() error { return registerTearing(d, s) },
2121 func() error { return registerPresentation(d, s) },
2222 func() error { return registerActivation(d, s) },
23+ func() error { return registerVirtualKeyboard(d, s) },
2324 func() error {
2425 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
2526 },
@@ -393,7 +394,8 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
393394 return
394395 }
395396 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)
397399 if r.Version() >= 4 {
398400 k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
399401 }
@@ -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 return394 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 {
9191 heldKeys map[uint32]bool
9292 keymapFD int
9393 keymapSize uint32
94+ keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
9495 repeatRate, repeatDelay int
9596 // eventMu protects only the notification queue, not display-owned window state.
9697 // 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) {
428429 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
429430 return
430431 }
432+ s.useKeymap(nil)
431433 if s.heldKeys == nil {
432434 s.heldKeys = make(map[uint32]bool)
433435 }
@@ -599,6 +601,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) {
599601 unix.Close(s.keymapFD)
600602 }
601603 s.keymapFD, s.keymapSize = fd, size
604+ s.keymapOwner = nil
602605 focused := s.focused
603606 s.changeFocus(0)
604607 s.heldKeys = nil
@@ -621,6 +624,8 @@ func (s *Server) sendModifiers(k *wayland.Keyboard) {
621624 }
622625
623626 func (s *Server) changeFocus(id ports.WindowID) {
627+ // Enter carries seat held keys and modifiers: they need the seat keymap.
628+ s.useKeymap(nil)
624629 old := s.focusClient()
625630 defer func() {
626631 // Tokens of a client losing the focus die with it.
@@ -91,6 +91,7 @@ type Server struct {
91 heldKeys map[uint32]bool91 heldKeys map[uint32]bool
92 keymapFD int92 keymapFD int
93 keymapSize uint3293 keymapSize uint32
94+ keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
94 repeatRate, repeatDelay int95 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 draining97 // 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 return430 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, size603 s.keymapFD, s.keymapSize = fd, size
604+ s.keymapOwner = nil
602 focused := s.focused605 focused := s.focused
603 s.changeFocus(0)606 s.changeFocus(0)
604 s.heldKeys = nil607 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close