detached 18a2776

merge: wl_keyboard with focus and key forwarding

commit
18a27767131da846c1cc599cd04b912e8a5df2bf
author
brice · · 2026-09-23 15:43 UTC
merge of
cb9e8ca + 90a317c · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +2397 -24
M internal/adapters/wayland/globals.go +40 -5
@@ -50,9 +50,13 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
5050 },
5151 func() error {
5252 return wayland.NewSeatGlobal(d, 7, func(c server.Client, v, id uint32) {
53- r, e := wayland.NewSeat(c, int32(v), id, seat{})
53+ r, e := wayland.NewSeat(c, int32(v), id, seat{s})
5454 if e == nil {
55- r.SendCapabilities(0)
55+ capabilities := uint32(0)
56+ if s.keymapFD >= 0 {
57+ capabilities = uint32(wayland.SeatCapabilityKeyboard)
58+ }
59+ r.SendCapabilities(capabilities)
5660 if v >= 2 {
5761 r.SendName("seat0")
5862 }
@@ -237,13 +241,44 @@ type output struct{}
237241
238242 func (output) Release(*wayland.Output) {}
239243
240-type seat struct{}
244+type seat struct{ server *Server }
241245
242246 func (seat) GetPointer(r *wayland.Seat, id uint32) {
243247 wayland.NewPointer(r.Client(), r.Version(), id, pointer{})
244248 }
245-func (seat) GetKeyboard(r *wayland.Seat, id uint32) {
246- wayland.NewKeyboard(r.Client(), r.Version(), id, keyboard{})
249+func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
250+ k, err := wayland.NewKeyboard(r.Client(), r.Version(), id, keyboard{})
251+ if err != nil {
252+ return
253+ }
254+ s := h.server
255+ k.OnDestroy = func() {
256+ list := s.keyboards[r.Client()]
257+ for i, item := range list {
258+ if item == k {
259+ list = append(list[:i], list[i+1:]...)
260+ break
261+ }
262+ }
263+ if len(list) == 0 {
264+ delete(s.keyboards, r.Client())
265+ } else {
266+ s.keyboards[r.Client()] = list
267+ }
268+ }
269+ if s.keymapFD < 0 {
270+ return
271+ }
272+ s.keyboards[r.Client()] = append(s.keyboards[r.Client()], k)
273+ k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)
274+ if r.Version() >= 4 {
275+ k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
276+ }
277+ if w := s.windows[s.focused]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() {
278+ s.serial++
279+ k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{})
280+ s.sendModifiers(k)
281+ }
247282 }
248283 func (seat) GetTouch(r *wayland.Seat, id uint32) {
249284 wayland.NewTouch(r.Client(), r.Version(), id, touch{})
@@ -50,9 +50,13 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
50 },50 },
51 func() error {51 func() error {
52 return wayland.NewSeatGlobal(d, 7, func(c server.Client, v, id uint32) {52 return wayland.NewSeatGlobal(d, 7, func(c server.Client, v, id uint32) {
53- r, e := wayland.NewSeat(c, int32(v), id, seat{})53+ r, e := wayland.NewSeat(c, int32(v), id, seat{s})
54 if e == nil {54 if e == nil {
55- r.SendCapabilities(0)55+ capabilities := uint32(0)
56+ if s.keymapFD >= 0 {
57+ capabilities = uint32(wayland.SeatCapabilityKeyboard)
58+ }
59+ r.SendCapabilities(capabilities)
56 if v >= 2 {60 if v >= 2 {
57 r.SendName("seat0")61 r.SendName("seat0")
58 }62 }
@@ -237,13 +241,44 @@ type output struct{}
237 241
238 func (output) Release(*wayland.Output) {}242 func (output) Release(*wayland.Output) {}
239 243
240-type seat struct{}244+type seat struct{ server *Server }
241 245
242 func (seat) GetPointer(r *wayland.Seat, id uint32) {246 func (seat) GetPointer(r *wayland.Seat, id uint32) {
243 wayland.NewPointer(r.Client(), r.Version(), id, pointer{})247 wayland.NewPointer(r.Client(), r.Version(), id, pointer{})
244 }248 }
245-func (seat) GetKeyboard(r *wayland.Seat, id uint32) {249+func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
246- wayland.NewKeyboard(r.Client(), r.Version(), id, keyboard{})250+ k, err := wayland.NewKeyboard(r.Client(), r.Version(), id, keyboard{})
251+ if err != nil {
252+ return
253+ }
254+ s := h.server
255+ k.OnDestroy = func() {
256+ list := s.keyboards[r.Client()]
257+ for i, item := range list {
258+ if item == k {
259+ list = append(list[:i], list[i+1:]...)
260+ break
261+ }
262+ }
263+ if len(list) == 0 {
264+ delete(s.keyboards, r.Client())
265+ } else {
266+ s.keyboards[r.Client()] = list
267+ }
268+ }
269+ if s.keymapFD < 0 {
270+ return
271+ }
272+ s.keyboards[r.Client()] = append(s.keyboards[r.Client()], k)
273+ k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)
274+ if r.Version() >= 4 {
275+ k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
276+ }
277+ if w := s.windows[s.focused]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() {
278+ s.serial++
279+ k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{})
280+ s.sendModifiers(k)
281+ }
247 }282 }
248 func (seat) GetTouch(r *wayland.Seat, id uint32) {283 func (seat) GetTouch(r *wayland.Seat, id uint32) {
249 wayland.NewTouch(r.Client(), r.Version(), id, touch{})284 wayland.NewTouch(r.Client(), r.Version(), id, touch{})
A internal/adapters/wayland/keyboard_test.go +152 -0
@@ -0,0 +1,152 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "os/exec"
7+ "regexp"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/bnema/nefertty/internal/logging"
13+ "github.com/bnema/nefertty/internal/ports"
14+ "github.com/bnema/purego-libwayland/protocol/wayland"
15+ "github.com/bnema/wlturbo"
16+ "golang.org/x/sys/unix"
17+)
18+
19+func keymapText(t *testing.T) string {
20+ t.Helper()
21+ data, err := os.ReadFile("testdata/us.xkb")
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ return string(data)
26+}
27+
28+func keyboardServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, string) {
29+ t.Helper()
30+ dir := t.TempDir()
31+ events := make(chan ports.ClientEvent, 16)
32+ commands := make(chan ports.ClientCommand, 32)
33+ s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080, Keymap: keymapText(t), RepeatRate: 25, RepeatDelay: 600}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland"))
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ ctx, cancel := context.WithCancel(context.Background())
38+ done := make(chan error, 1)
39+ go func() { done <- s.Run(ctx) }()
40+ t.Cleanup(func() {
41+ cancel()
42+ select {
43+ case err := <-done:
44+ if err != nil {
45+ t.Errorf("Run: %v", err)
46+ }
47+ case <-time.After(2 * time.Second):
48+ t.Error("Run hung")
49+ }
50+ })
51+ return s, events, commands, dir
52+}
53+
54+func TestKeyboardInfo(t *testing.T) {
55+ tool, err := exec.LookPath("wayland-info")
56+ if err != nil {
57+ t.Skip("wayland-info not installed")
58+ }
59+ s, _, _, dir := keyboardServer(t)
60+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
61+ defer cancel()
62+ cmd := exec.CommandContext(ctx, tool)
63+ cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName())
64+ out, err := cmd.CombinedOutput()
65+ if err != nil {
66+ t.Fatalf("wayland-info: %v: %s", err, out)
67+ }
68+ for _, want := range []string{"capabilities: keyboard", "keyboard repeat rate: 25", "keyboard repeat delay: 600"} {
69+ if !strings.Contains(string(out), want) {
70+ t.Errorf("missing %q: %s", want, out)
71+ }
72+ }
73+}
74+
75+type keymapProxy struct {
76+ wlturbo.BaseProxy
77+ result chan error
78+}
79+
80+func (p *keymapProxy) Dispatch(e *wlturbo.Event) {
81+ if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
82+ return
83+ }
84+ format := e.Uint32()
85+ fd := int(e.Fd())
86+ size := e.Uint32()
87+ if fd <= 0 || size == 0 || format != uint32(wayland.KeyboardKeymapFormatXkbV1) {
88+ p.result <- os.ErrInvalid
89+ return
90+ }
91+ defer unix.Close(fd)
92+ data := make([]byte, size)
93+ _, err := unix.Pread(fd, data, 0)
94+ if err == nil && !strings.HasPrefix(string(data), "xkb_keymap") {
95+ err = os.ErrInvalid
96+ }
97+ p.result <- err
98+}
99+func TestKeyboardKeymapProtocol(t *testing.T) {
100+ s, _, _, dir := keyboardServer(t)
101+ c := protocolClient(t, s, dir)
102+ seat := bindProtocol(t, c, "wl_seat")
103+ registerProtocol(t, c, seat)
104+ id := c.AllocateID()
105+ proxy := &keymapProxy{result: make(chan error, 1)}
106+ proxy.SetID(id)
107+ c.Context().Register(proxy)
108+ requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id)
109+ if err := c.Roundtrip(); err != nil {
110+ t.Fatal(err)
111+ }
112+ select {
113+ case err := <-proxy.result:
114+ if err != nil {
115+ t.Fatal(err)
116+ }
117+ default:
118+ t.Fatal("missing keymap event")
119+ }
120+}
121+func TestFootKeyboard(t *testing.T) {
122+ tool, err := exec.LookPath("foot")
123+ if err != nil {
124+ t.Skip("foot not installed")
125+ }
126+ s, events, commands, dir := keyboardServer(t)
127+ _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + t.TempDir(), "WAYLAND_DEBUG=client"}, "-c", "/dev/null", "sh", "-c", "sleep 10")
128+ w := mapped(t, events, 10*time.Second)
129+ commands <- ports.FocusWindow{ID: w.ID}
130+ for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} {
131+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, TimeMsec: 1}}
132+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, TimeMsec: 2}}
133+ }
134+ enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`)
135+ key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`)
136+ deadline := time.After(4 * time.Second)
137+ tick := time.NewTicker(10 * time.Millisecond)
138+ defer tick.Stop()
139+ for {
140+ select {
141+ case err := <-done:
142+ t.Fatalf("foot exited: %v: %s", err, output.String())
143+ case <-deadline:
144+ t.Fatalf("keyboard events missing: %s", output.String())
145+ case <-tick.C:
146+ text := output.String()
147+ if enter.MatchString(text) && key.MatchString(text) {
148+ return
149+ }
150+ }
151+ }
152+}
@@ -0,0 +1,152 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "os/exec"
7+ "regexp"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/bnema/nefertty/internal/logging"
13+ "github.com/bnema/nefertty/internal/ports"
14+ "github.com/bnema/purego-libwayland/protocol/wayland"
15+ "github.com/bnema/wlturbo"
16+ "golang.org/x/sys/unix"
17+)
18+
19+func keymapText(t *testing.T) string {
20+ t.Helper()
21+ data, err := os.ReadFile("testdata/us.xkb")
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ return string(data)
26+}
27+
28+func keyboardServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, string) {
29+ t.Helper()
30+ dir := t.TempDir()
31+ events := make(chan ports.ClientEvent, 16)
32+ commands := make(chan ports.ClientCommand, 32)
33+ s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080, Keymap: keymapText(t), RepeatRate: 25, RepeatDelay: 600}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland"))
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ ctx, cancel := context.WithCancel(context.Background())
38+ done := make(chan error, 1)
39+ go func() { done <- s.Run(ctx) }()
40+ t.Cleanup(func() {
41+ cancel()
42+ select {
43+ case err := <-done:
44+ if err != nil {
45+ t.Errorf("Run: %v", err)
46+ }
47+ case <-time.After(2 * time.Second):
48+ t.Error("Run hung")
49+ }
50+ })
51+ return s, events, commands, dir
52+}
53+
54+func TestKeyboardInfo(t *testing.T) {
55+ tool, err := exec.LookPath("wayland-info")
56+ if err != nil {
57+ t.Skip("wayland-info not installed")
58+ }
59+ s, _, _, dir := keyboardServer(t)
60+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
61+ defer cancel()
62+ cmd := exec.CommandContext(ctx, tool)
63+ cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName())
64+ out, err := cmd.CombinedOutput()
65+ if err != nil {
66+ t.Fatalf("wayland-info: %v: %s", err, out)
67+ }
68+ for _, want := range []string{"capabilities: keyboard", "keyboard repeat rate: 25", "keyboard repeat delay: 600"} {
69+ if !strings.Contains(string(out), want) {
70+ t.Errorf("missing %q: %s", want, out)
71+ }
72+ }
73+}
74+
75+type keymapProxy struct {
76+ wlturbo.BaseProxy
77+ result chan error
78+}
79+
80+func (p *keymapProxy) Dispatch(e *wlturbo.Event) {
81+ if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
82+ return
83+ }
84+ format := e.Uint32()
85+ fd := int(e.Fd())
86+ size := e.Uint32()
87+ if fd <= 0 || size == 0 || format != uint32(wayland.KeyboardKeymapFormatXkbV1) {
88+ p.result <- os.ErrInvalid
89+ return
90+ }
91+ defer unix.Close(fd)
92+ data := make([]byte, size)
93+ _, err := unix.Pread(fd, data, 0)
94+ if err == nil && !strings.HasPrefix(string(data), "xkb_keymap") {
95+ err = os.ErrInvalid
96+ }
97+ p.result <- err
98+}
99+func TestKeyboardKeymapProtocol(t *testing.T) {
100+ s, _, _, dir := keyboardServer(t)
101+ c := protocolClient(t, s, dir)
102+ seat := bindProtocol(t, c, "wl_seat")
103+ registerProtocol(t, c, seat)
104+ id := c.AllocateID()
105+ proxy := &keymapProxy{result: make(chan error, 1)}
106+ proxy.SetID(id)
107+ c.Context().Register(proxy)
108+ requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id)
109+ if err := c.Roundtrip(); err != nil {
110+ t.Fatal(err)
111+ }
112+ select {
113+ case err := <-proxy.result:
114+ if err != nil {
115+ t.Fatal(err)
116+ }
117+ default:
118+ t.Fatal("missing keymap event")
119+ }
120+}
121+func TestFootKeyboard(t *testing.T) {
122+ tool, err := exec.LookPath("foot")
123+ if err != nil {
124+ t.Skip("foot not installed")
125+ }
126+ s, events, commands, dir := keyboardServer(t)
127+ _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + t.TempDir(), "WAYLAND_DEBUG=client"}, "-c", "/dev/null", "sh", "-c", "sleep 10")
128+ w := mapped(t, events, 10*time.Second)
129+ commands <- ports.FocusWindow{ID: w.ID}
130+ for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} {
131+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, TimeMsec: 1}}
132+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, TimeMsec: 2}}
133+ }
134+ enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`)
135+ key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`)
136+ deadline := time.After(4 * time.Second)
137+ tick := time.NewTicker(10 * time.Millisecond)
138+ defer tick.Stop()
139+ for {
140+ select {
141+ case err := <-done:
142+ t.Fatalf("foot exited: %v: %s", err, output.String())
143+ case <-deadline:
144+ t.Fatalf("keyboard events missing: %s", output.String())
145+ case <-tick.C:
146+ text := output.String()
147+ if enter.MatchString(text) && key.MatchString(text) {
148+ return
149+ }
150+ }
151+ }
152+}
M internal/adapters/wayland/server.go +112 -19
@@ -19,6 +19,8 @@ import (
1919 type Options struct {
2020 RuntimeDir string
2121 OutputWidth, OutputHeight int
22+ Keymap string
23+ RepeatRate, RepeatDelay int
2224 }
2325
2426 // Channels carries client notifications and commands. Events may be unbuffered.
@@ -28,21 +30,26 @@ type Channels struct {
2830 Commands <-chan ports.ClientCommand
2931 }
3032 type Server struct {
31- display *server.Display
32- name string
33- cleanup func()
34- log zerowrap.Logger
35- channels Channels
36- awaiting []*wayland.Callback
37- frames uint64
38- started time.Time
39- surfaces map[*server.Resource]*surface
40- buffers map[*server.Resource]*buffer
41- serial uint32
42- ctx context.Context
43- windows map[ports.WindowID]*window
44- nextWindow ports.WindowID
45- focused ports.WindowID
33+ display *server.Display
34+ name string
35+ cleanup func()
36+ log zerowrap.Logger
37+ channels Channels
38+ awaiting []*wayland.Callback
39+ frames uint64
40+ started time.Time
41+ surfaces map[*server.Resource]*surface
42+ buffers map[*server.Resource]*buffer
43+ serial uint32
44+ ctx context.Context
45+ windows map[ports.WindowID]*window
46+ nextWindow ports.WindowID
47+ focused ports.WindowID
48+ modState ports.ModState
49+ keyboards map[server.Client][]*wayland.Keyboard
50+ keymapFD int
51+ keymapSize uint32
52+ repeatRate, repeatDelay int
4653 // eventMu protects only the notification queue, not display-owned window state.
4754 // The event queue is unbounded by design: it grows only if core stops draining
4855 // Events, which happens only at shutdown (core owns the receiving end). A
@@ -80,9 +87,38 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
8087 d.Close()
8188 return nil, err
8289 }
83- s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{})}
90+ s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
91+ if opts.Keymap != "" {
92+ if uint64(len(opts.Keymap))+1 > uint64(^uint32(0)) {
93+ cleanup()
94+ d.Close()
95+ return nil, fmt.Errorf("keymap too large")
96+ }
97+ fd, e := unix.MemfdCreate("nefertty-keymap", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
98+ if e == nil {
99+ var n int
100+ n, e = unix.Write(fd, append([]byte(opts.Keymap), 0))
101+ if e == nil && n != len(opts.Keymap)+1 {
102+ e = fmt.Errorf("short keymap write")
103+ }
104+ if e == nil {
105+ _, e = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL)
106+ }
107+ if e != nil {
108+ unix.Close(fd)
109+ } else {
110+ s.keymapFD, s.keymapSize = fd, uint32(n)
111+ }
112+ }
113+ if e != nil {
114+ cleanup()
115+ d.Close()
116+ return nil, e
117+ }
118+ s.cleanup = func() { unix.Close(fd); cleanup() }
119+ }
84120 if err = registerGlobals(d, opts, s); err != nil {
85- cleanup()
121+ s.cleanup()
86122 d.Close()
87123 return nil, err
88124 }
@@ -257,9 +293,30 @@ func (s *Server) forwardContents(ctx context.Context) {
257293 func (s *Server) apply(cmd ports.ClientCommand) {
258294 switch c := cmd.(type) {
259295 case ports.FocusWindow:
260- s.focused = c.ID
296+ if s.focused != c.ID {
297+ s.changeFocus(c.ID)
298+ }
261299 case ports.ForwardKey:
262- s.log.Debug().Uint64("id", uint64(c.ID)).Msg("forward key")
300+ w := s.windows[c.ID]
301+ if c.ID != s.focused || w == nil || !w.mapped || len(s.windowKeyboards(w)) == 0 {
302+ s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
303+ return
304+ }
305+ s.serial++
306+ state := uint32(0)
307+ if c.Key.Pressed {
308+ state = 1
309+ }
310+ for _, k := range s.windowKeyboards(w) {
311+ k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
312+ }
313+ if c.Key.State != s.modState {
314+ s.modState = c.Key.State
315+ s.serial++
316+ for _, k := range s.windowKeyboards(w) {
317+ s.sendModifiers(k)
318+ }
319+ }
263320 case ports.ConfigureWindow:
264321 w := s.windows[c.ID]
265322 if w == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {
@@ -286,3 +343,39 @@ func (s *Server) apply(cmd ports.ClientCommand) {
286343 w.toplevel.SendClose()
287344 }
288345 }
346+
347+func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
348+ if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
349+ return nil
350+ }
351+ var alive []*wayland.Keyboard
352+ for _, k := range s.keyboards[w.xdg.resource.Client()] {
353+ if k.Resource.Alive() {
354+ alive = append(alive, k)
355+ }
356+ }
357+ return alive
358+}
359+
360+func (s *Server) sendModifiers(k *wayland.Keyboard) {
361+ m := s.modState
362+ k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
363+}
364+
365+func (s *Server) changeFocus(id ports.WindowID) {
366+ if old := s.windows[s.focused]; old != nil && old.xdg.surfaceResource() != nil {
367+ for _, k := range s.windowKeyboards(old) {
368+ s.serial++
369+ k.SendLeave(s.serial, old.xdg.surfaceResource())
370+ }
371+ }
372+ s.focused = 0
373+ if w := s.windows[id]; w != nil && w.mapped && w.xdg.surfaceResource() != nil {
374+ s.focused = id
375+ for _, k := range s.windowKeyboards(w) {
376+ s.serial++
377+ k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{})
378+ s.sendModifiers(k)
379+ }
380+ }
381+}
@@ -19,6 +19,8 @@ import (
19 type Options struct {19 type Options struct {
20 RuntimeDir string20 RuntimeDir string
21 OutputWidth, OutputHeight int21 OutputWidth, OutputHeight int
22+ Keymap string
23+ RepeatRate, RepeatDelay int
22 }24 }
23 25
24 // Channels carries client notifications and commands. Events may be unbuffered.26 // Channels carries client notifications and commands. Events may be unbuffered.
@@ -28,21 +30,26 @@ type Channels struct {
28 Commands <-chan ports.ClientCommand30 Commands <-chan ports.ClientCommand
29 }31 }
30 type Server struct {32 type Server struct {
31- display *server.Display33+ display *server.Display
32- name string34+ name string
33- cleanup func()35+ cleanup func()
34- log zerowrap.Logger36+ log zerowrap.Logger
35- channels Channels37+ channels Channels
36- awaiting []*wayland.Callback38+ awaiting []*wayland.Callback
37- frames uint6439+ frames uint64
38- started time.Time40+ started time.Time
39- surfaces map[*server.Resource]*surface41+ surfaces map[*server.Resource]*surface
40- buffers map[*server.Resource]*buffer42+ buffers map[*server.Resource]*buffer
41- serial uint3243+ serial uint32
42- ctx context.Context44+ ctx context.Context
43- windows map[ports.WindowID]*window45+ windows map[ports.WindowID]*window
44- nextWindow ports.WindowID46+ nextWindow ports.WindowID
45- focused ports.WindowID47+ focused ports.WindowID
48+ modState ports.ModState
49+ keyboards map[server.Client][]*wayland.Keyboard
50+ keymapFD int
51+ keymapSize uint32
52+ repeatRate, repeatDelay int
46 // eventMu protects only the notification queue, not display-owned window state.53 // eventMu protects only the notification queue, not display-owned window state.
47 // The event queue is unbounded by design: it grows only if core stops draining54 // The event queue is unbounded by design: it grows only if core stops draining
48 // Events, which happens only at shutdown (core owns the receiving end). A55 // Events, which happens only at shutdown (core owns the receiving end). A
@@ -80,9 +87,38 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
80 d.Close()87 d.Close()
81 return nil, err88 return nil, err
82 }89 }
83- s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{})}90+ s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
91+ if opts.Keymap != "" {
92+ if uint64(len(opts.Keymap))+1 > uint64(^uint32(0)) {
93+ cleanup()
94+ d.Close()
95+ return nil, fmt.Errorf("keymap too large")
96+ }
97+ fd, e := unix.MemfdCreate("nefertty-keymap", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
98+ if e == nil {
99+ var n int
100+ n, e = unix.Write(fd, append([]byte(opts.Keymap), 0))
101+ if e == nil && n != len(opts.Keymap)+1 {
102+ e = fmt.Errorf("short keymap write")
103+ }
104+ if e == nil {
105+ _, e = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL)
106+ }
107+ if e != nil {
108+ unix.Close(fd)
109+ } else {
110+ s.keymapFD, s.keymapSize = fd, uint32(n)
111+ }
112+ }
113+ if e != nil {
114+ cleanup()
115+ d.Close()
116+ return nil, e
117+ }
118+ s.cleanup = func() { unix.Close(fd); cleanup() }
119+ }
84 if err = registerGlobals(d, opts, s); err != nil {120 if err = registerGlobals(d, opts, s); err != nil {
85- cleanup()121+ s.cleanup()
86 d.Close()122 d.Close()
87 return nil, err123 return nil, err
88 }124 }
@@ -257,9 +293,30 @@ func (s *Server) forwardContents(ctx context.Context) {
257 func (s *Server) apply(cmd ports.ClientCommand) {293 func (s *Server) apply(cmd ports.ClientCommand) {
258 switch c := cmd.(type) {294 switch c := cmd.(type) {
259 case ports.FocusWindow:295 case ports.FocusWindow:
260- s.focused = c.ID296+ if s.focused != c.ID {
297+ s.changeFocus(c.ID)
298+ }
261 case ports.ForwardKey:299 case ports.ForwardKey:
262- s.log.Debug().Uint64("id", uint64(c.ID)).Msg("forward key")300+ w := s.windows[c.ID]
301+ if c.ID != s.focused || w == nil || !w.mapped || len(s.windowKeyboards(w)) == 0 {
302+ s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
303+ return
304+ }
305+ s.serial++
306+ state := uint32(0)
307+ if c.Key.Pressed {
308+ state = 1
309+ }
310+ for _, k := range s.windowKeyboards(w) {
311+ k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
312+ }
313+ if c.Key.State != s.modState {
314+ s.modState = c.Key.State
315+ s.serial++
316+ for _, k := range s.windowKeyboards(w) {
317+ s.sendModifiers(k)
318+ }
319+ }
263 case ports.ConfigureWindow:320 case ports.ConfigureWindow:
264 w := s.windows[c.ID]321 w := s.windows[c.ID]
265 if w == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {322 if w == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {
@@ -286,3 +343,39 @@ func (s *Server) apply(cmd ports.ClientCommand) {
286 w.toplevel.SendClose()343 w.toplevel.SendClose()
287 }344 }
288 }345 }
346+
347+func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
348+ if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
349+ return nil
350+ }
351+ var alive []*wayland.Keyboard
352+ for _, k := range s.keyboards[w.xdg.resource.Client()] {
353+ if k.Resource.Alive() {
354+ alive = append(alive, k)
355+ }
356+ }
357+ return alive
358+}
359+
360+func (s *Server) sendModifiers(k *wayland.Keyboard) {
361+ m := s.modState
362+ k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
363+}
364+
365+func (s *Server) changeFocus(id ports.WindowID) {
366+ if old := s.windows[s.focused]; old != nil && old.xdg.surfaceResource() != nil {
367+ for _, k := range s.windowKeyboards(old) {
368+ s.serial++
369+ k.SendLeave(s.serial, old.xdg.surfaceResource())
370+ }
371+ }
372+ s.focused = 0
373+ if w := s.windows[id]; w != nil && w.mapped && w.xdg.surfaceResource() != nil {
374+ s.focused = id
375+ for _, k := range s.windowKeyboards(w) {
376+ s.serial++
377+ k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{})
378+ s.sendModifiers(k)
379+ }
380+ }
381+}
A internal/adapters/wayland/testdata/us.xkb +2081 -0 large diff (2081 lines changed): collapsed
M internal/adapters/wayland/xdg.go +12 -0
@@ -80,6 +80,9 @@ type window struct {
8080 }
8181
8282 func (w *window) unmap() {
83+ if w.xdg.server.focused == w.id {
84+ w.xdg.server.changeFocus(0)
85+ }
8386 if !w.mapped {
8487 return
8588 }
@@ -188,3 +191,12 @@ type popup struct{}
188191 func (popup) Destroy(*xdgshell.Popup) {}
189192 func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {}
190193 func (popup) Reposition(*xdgshell.Popup, *xdgshell.Positioner, uint32) {}
194+
195+func (x *xdgSurface) surfaceResource() *wayland.Surface {
196+ for resource, state := range x.server.surfaces {
197+ if state == x.surface && resource.Alive() {
198+ return wayland.WrapSurface(resource)
199+ }
200+ }
201+ return nil
202+}
@@ -80,6 +80,9 @@ type window struct {
80 }80 }
81 81
82 func (w *window) unmap() {82 func (w *window) unmap() {
83+ if w.xdg.server.focused == w.id {
84+ w.xdg.server.changeFocus(0)
85+ }
83 if !w.mapped {86 if !w.mapped {
84 return87 return
85 }88 }
@@ -188,3 +191,12 @@ type popup struct{}
188 func (popup) Destroy(*xdgshell.Popup) {}191 func (popup) Destroy(*xdgshell.Popup) {}
189 func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {}192 func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {}
190 func (popup) Reposition(*xdgshell.Popup, *xdgshell.Positioner, uint32) {}193 func (popup) Reposition(*xdgshell.Popup, *xdgshell.Positioner, uint32) {}
194+
195+func (x *xdgSurface) surfaceResource() *wayland.Surface {
196+ for resource, state := range x.server.surfaces {
197+ if state == x.surface && resource.Alive() {
198+ return wayland.WrapSurface(resource)
199+ }
200+ }
201+ return nil
202+}

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 26

Tags 10

↑↓ moveenter openesc close