feat(wayland): add wl_keyboard with focus and key forwarding
- commit
- 90a317c29f0f188632e689874aa2cd6790de064f
- author
- brice · · 2026-09-23 15:43 UTC
- parent
- 80c9df5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 50 | 50 | }, | |
| 51 | 51 | func() error { | |
| 52 | 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 | 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 | 60 | if v >= 2 { | |
| 57 | 61 | r.SendName("seat0") | |
| 58 | 62 | } | |
| @@ -237,13 +241,44 @@ type output struct{} | |||
| 237 | 241 | ||
| 238 | 242 | func (output) Release(*wayland.Output) {} | |
| 239 | 243 | ||
| 240 | - | type seat struct{} | |
| 244 | + | type seat struct{ server *Server } | |
| 241 | 245 | ||
| 242 | 246 | func (seat) GetPointer(r *wayland.Seat, id uint32) { | |
| 243 | 247 | wayland.NewPointer(r.Client(), r.Version(), id, pointer{}) | |
| 244 | 248 | } | |
| 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 | + | } | |
| 247 | 282 | } | |
| 248 | 283 | func (seat) GetTouch(r *wayland.Seat, id uint32) { | |
| 249 | 284 | 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 ( | |||
| 19 | 19 | type Options struct { | |
| 20 | 20 | RuntimeDir string | |
| 21 | 21 | OutputWidth, OutputHeight int | |
| 22 | + | Keymap string | |
| 23 | + | RepeatRate, RepeatDelay int | |
| 22 | 24 | } | |
| 23 | 25 | ||
| 24 | 26 | // Channels carries client notifications and commands. Events may be unbuffered. | |
| @@ -28,21 +30,26 @@ type Channels struct { | |||
| 28 | 30 | Commands <-chan ports.ClientCommand | |
| 29 | 31 | } | |
| 30 | 32 | 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 | |
| 46 | 53 | // eventMu protects only the notification queue, not display-owned window state. | |
| 47 | 54 | // The event queue is unbounded by design: it grows only if core stops draining | |
| 48 | 55 | // 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 | 87 | d.Close() | |
| 81 | 88 | 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 | 120 | if err = registerGlobals(d, opts, s); err != nil { | |
| 85 | - | cleanup() | |
| 121 | + | s.cleanup() | |
| 86 | 122 | d.Close() | |
| 87 | 123 | return nil, err | |
| 88 | 124 | } | |
| @@ -257,9 +293,30 @@ func (s *Server) forwardContents(ctx context.Context) { | |||
| 257 | 293 | func (s *Server) apply(cmd ports.ClientCommand) { | |
| 258 | 294 | switch c := cmd.(type) { | |
| 259 | 295 | case ports.FocusWindow: | |
| 260 | - | s.focused = c.ID | |
| 296 | + | if s.focused != c.ID { | |
| 297 | + | s.changeFocus(c.ID) | |
| 298 | + | } | |
| 261 | 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 | 320 | case ports.ConfigureWindow: | |
| 264 | 321 | w := s.windows[c.ID] | |
| 265 | 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 | 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 | + | } | |
| @@ -19,6 +19,8 @@ import ( | |||||
| 19 | type Options struct { | 19 | type Options struct { | ||
| 20 | RuntimeDir string | 20 | RuntimeDir string | ||
| 21 | OutputWidth, OutputHeight int | 21 | 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.ClientCommand | 30 | Commands <-chan ports.ClientCommand | ||
| 29 | } | 31 | } | ||
| 30 | type Server struct { | 32 | type Server struct { | ||
| 31 | - | display *server.Display | 33 | + | display *server.Display |
| 32 | - | name string | 34 | + | name string |
| 33 | - | cleanup func() | 35 | + | cleanup func() |
| 34 | - | log zerowrap.Logger | 36 | + | log zerowrap.Logger |
| 35 | - | channels Channels | 37 | + | channels Channels |
| 36 | - | awaiting []*wayland.Callback | 38 | + | awaiting []*wayland.Callback |
| 37 | - | frames uint64 | 39 | + | frames uint64 |
| 38 | - | started time.Time | 40 | + | started time.Time |
| 39 | - | surfaces map[*server.Resource]*surface | 41 | + | surfaces map[*server.Resource]*surface |
| 40 | - | buffers map[*server.Resource]*buffer | 42 | + | buffers map[*server.Resource]*buffer |
| 41 | - | serial uint32 | 43 | + | serial uint32 |
| 42 | - | ctx context.Context | 44 | + | ctx context.Context |
| 43 | - | windows map[ports.WindowID]*window | 45 | + | windows map[ports.WindowID]*window |
| 44 | - | nextWindow ports.WindowID | 46 | + | nextWindow ports.WindowID |
| 45 | - | focused 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 | |||
| 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 draining | 54 | // 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). A | 55 | // 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, err | 88 | 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, err | 123 | 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.ID | 296 | + | 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 { | |||
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | 82 | func (w *window) unmap() { | |
| 83 | + | if w.xdg.server.focused == w.id { | |
| 84 | + | w.xdg.server.changeFocus(0) | |
| 85 | + | } | |
| 83 | 86 | if !w.mapped { | |
| 84 | 87 | return | |
| 85 | 88 | } | |
| @@ -188,3 +191,12 @@ type popup struct{} | |||
| 188 | 191 | func (popup) Destroy(*xdgshell.Popup) {} | |
| 189 | 192 | func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {} | |
| 190 | 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 | + | } | |
| @@ -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 | return | 87 | 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 | + | } | |||