detached 56b509a

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +171 -27
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.1-0.20260926085626-ac96fe0bd3c3
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.1-0.20260926085626-ac96fe0bd3c37+ 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 +2 -0 generated file: collapsed
M internal/adapters/wayland/server.go +2 -0
@@ -624,6 +624,8 @@ func (s *Server) sendModifiers(k *wayland.Keyboard) {
624624 }
625625
626626 func (s *Server) changeFocus(id ports.WindowID) {
627+ // Enter carries seat held keys and modifiers: they need the seat keymap.
628+ s.useKeymap(nil)
627629 old := s.focusClient()
628630 defer func() {
629631 // 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 @@
11 package wayland
22
33 import (
4+ "bytes"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
48 "github.com/bnema/purego-libwayland/protocol/virtualkeyboard"
59 "github.com/bnema/purego-libwayland/protocol/wayland"
610 "github.com/bnema/purego-libwayland/server"
711 "golang.org/x/sys/unix"
812 )
913
14+// maxVirtualKeymap bounds a keymap read from a virtual keyboard client.
15+const maxVirtualKeymap = 1 << 20
16+
1017 // virtualKeyboard is a zwp_virtual_keyboard_v1 (wtype, dictation tools).
1118 // Its keys go to the focused client. The focused client's keyboards carry
1219 // 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.
1421 type virtualKeyboard struct {
1522 server *Server
16- fd int
23+ fd int // sealed copy of the client keymap, -1 before one
1724 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
1828 }
1929
30+// registerVirtualKeyboard exposes the global to every client, as wlroots
31+// does by default: any local client may type into the focused window.
2032 func registerVirtualKeyboard(d *server.Display, s *Server) error {
2133 return virtualkeyboard.NewZwpVirtualKeyboardManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
2234 _, _ = virtualkeyboard.NewZwpVirtualKeyboardManagerV1(c, int32(v), id, virtualKeyboardManager{s})
@@ -26,7 +38,7 @@ func registerVirtualKeyboard(d *server.Display, s *Server) error {
2638 type virtualKeyboardManager struct{ server *Server }
2739
2840 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{}}
3042 res, err := virtualkeyboard.NewZwpVirtualKeyboardV1(r.Client(), r.Version(), id, k)
3143 if err != nil {
3244 return
@@ -35,60 +47,109 @@ func (m virtualKeyboardManager) CreateVirtualKeyboard(r *virtualkeyboard.ZwpVirt
3547 m.server.log.Info().Int("pid", r.Client().PID()).Msg("virtual keyboard created")
3648 }
3749
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.
3852 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")
4162 return
4263 }
4364 if k.fd >= 0 {
4465 unix.Close(k.fd)
4566 }
46- k.fd, k.size = fd, size
67+ k.fd, k.size = copyFD, copySize
4768 if s := k.server; s.keymapOwner == k {
4869 // Clients must see the new layout before the next key.
49- s.sendKeymapAll(fd, size)
70+ s.sendKeymapAll(k.fd, k.size)
5071 }
5172 }
5273
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
5780 }
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
6284 }
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
6790 }
91+ return string(data), true
6892 }
6993
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 {
7197 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
74100 }
75101 s := k.server
76102 _, keyboards := s.focusTarget(s.focused)
77103 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
79109 }
80110 s.useKeymap(k)
81111 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+ }
82125 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)
84133 }
85134 }
86135
87136 func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {}
88137
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.
90140 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 {
92153 s.useKeymap(nil)
93154 }
94155 if k.fd >= 0 {
@@ -1,22 +1,34 @@
1 package wayland1 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 carry18 // 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 them19 // 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 *Server22 server *Server
16- fd int23+ fd int // sealed copy of the client keymap, -1 before one
17 size uint3224 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 return44 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 return62 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, size67+ 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- return77+ 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.server81+ 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- return99+ return nil
74 }100 }
75 s := k.server101 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- return104+ 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) {
9595 }
9696 }
9797
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+
98177 func TestVirtualKeyBeforeKeymap(t *testing.T) {
99178 s, _, _, dir := keyboardServer(t)
100179 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)

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