detached 6347722

feat(layer): keyboard focus for interactive top/overlay layers, restored on unmap

commit
6347722b19dc24d2b83a7794817388bed0328e41
author
brice · · 2026-09-24 09:42 UTC
parent
039f0ba
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +110 -17
M internal/adapters/wayland/globals.go +2 -2
@@ -302,9 +302,9 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
302302 if r.Version() >= 4 {
303303 k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
304304 }
305- if w := s.windows[s.focused]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() {
305+ if surf, _ := s.focusTarget(s.focused); surf != nil && surf.Client() == r.Client() {
306306 s.serial++
307- k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{})
307+ k.SendEnter(s.serial, surf, []byte{})
308308 s.sendModifiers(k)
309309 }
310310 }
@@ -302,9 +302,9 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
302 if r.Version() >= 4 {302 if r.Version() >= 4 {
303 k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))303 k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
304 }304 }
305- if w := s.windows[s.focused]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() {305+ if surf, _ := s.focusTarget(s.focused); surf != nil && surf.Client() == r.Client() {
306 s.serial++306 s.serial++
307- k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{})307+ k.SendEnter(s.serial, surf, []byte{})
308 s.sendModifiers(k)308 s.sendModifiers(k)
309 }309 }
310 }310 }
M internal/adapters/wayland/layer.go +1 -2
@@ -88,7 +88,6 @@ func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, l
8888 l.pending.margin = [4]int32{t, r, b, left}
8989 }
9090
91-// Keyboard interactivity is deferred; layer-shell is capped at v4.
9291 func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) {
9392 if k > 2 {
9493 r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity")
@@ -137,7 +136,7 @@ func (s *Server) layerChanged() {
137136 if !l.mapped {
138137 continue
139138 }
140- v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace}
139+ v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace, Keyboard: l.current.keyboard}
141140 if l.surface.current != nil {
142141 if b := s.buffers[l.surface.current.Resource]; b != nil {
143142 v.Width, v.Height = b.width, b.height
@@ -88,7 +88,6 @@ func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, l
88 l.pending.margin = [4]int32{t, r, b, left}88 l.pending.margin = [4]int32{t, r, b, left}
89 }89 }
90 90
91-// Keyboard interactivity is deferred; layer-shell is capped at v4.
92 func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) {91 func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) {
93 if k > 2 {92 if k > 2 {
94 r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity")93 r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity")
@@ -137,7 +136,7 @@ func (s *Server) layerChanged() {
137 if !l.mapped {136 if !l.mapped {
138 continue137 continue
139 }138 }
140- v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace}139+ v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace, Keyboard: l.current.keyboard}
141 if l.surface.current != nil {140 if l.surface.current != nil {
142 if b := s.buffers[l.surface.current.Resource]; b != nil {141 if b := s.buffers[l.surface.current.Resource]; b != nil {
143 v.Width, v.Height = b.width, b.height142 v.Width, v.Height = b.width, b.height
M internal/adapters/wayland/server.go +33 -11
@@ -346,8 +346,8 @@ func (s *Server) apply(cmd ports.ClientCommand) {
346346 s.changeFocus(c.ID)
347347 }
348348 case ports.ForwardKey:
349- w := s.windows[c.ID]
350- if c.ID != s.focused || w == nil || !w.mapped || len(s.windowKeyboards(w)) == 0 {
349+ _, keyboards := s.focusTarget(c.ID)
350+ if c.ID != s.focused || len(keyboards) == 0 {
351351 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
352352 return
353353 }
@@ -364,13 +364,13 @@ func (s *Server) apply(cmd ports.ClientCommand) {
364364 if c.Key.Pressed {
365365 state = 1
366366 }
367- for _, k := range s.windowKeyboards(w) {
367+ for _, k := range keyboards {
368368 k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
369369 }
370370 if c.Key.State != s.modState {
371371 s.modState = c.Key.State
372372 s.serial++
373- for _, k := range s.windowKeyboards(w) {
373+ for _, k := range keyboards {
374374 s.sendModifiers(k)
375375 }
376376 }
@@ -397,8 +397,12 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
397397 if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
398398 return nil
399399 }
400+ return s.clientKeyboards(w.xdg.resource.Client())
401+}
402+
403+func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
400404 var alive []*wayland.Keyboard
401- for _, k := range s.keyboards[w.xdg.resource.Client()] {
405+ for _, k := range s.keyboards[c] {
402406 if k.Resource.Alive() {
403407 alive = append(alive, k)
404408 }
@@ -406,31 +410,49 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
406410 return alive
407411 }
408412
413+// focusTarget resolves a window or layer ID to its wl_surface and keyboards.
414+func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) {
415+ if w := s.windows[id]; w != nil && w.mapped {
416+ if surf := w.xdg.surfaceResource(); surf != nil {
417+ return surf, s.windowKeyboards(w)
418+ }
419+ }
420+ if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
421+ for resource, state := range s.surfaces {
422+ if state == l.surface && resource.Alive() {
423+ return wayland.WrapSurface(resource), s.clientKeyboards(l.resource.Client())
424+ }
425+ }
426+ }
427+ return nil, nil
428+}
429+
409430 func (s *Server) sendModifiers(k *wayland.Keyboard) {
410431 m := s.modState
411432 k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
412433 }
413434
414435 func (s *Server) changeFocus(id ports.WindowID) {
415- if old := s.windows[s.focused]; old != nil && old.xdg.surfaceResource() != nil {
416- for _, k := range s.windowKeyboards(old) {
436+ if surf, keyboards := s.focusTarget(s.focused); surf != nil {
437+ for _, k := range keyboards {
417438 s.serial++
418- k.SendLeave(s.serial, old.xdg.surfaceResource())
439+ k.SendLeave(s.serial, surf)
419440 }
420441 }
421442 s.focused = 0
422- if w := s.windows[id]; w != nil && w.mapped && w.xdg.surfaceResource() != nil {
443+ if surf, keyboards := s.focusTarget(id); surf != nil {
423444 s.focused = id
424- for _, k := range s.windowKeyboards(w) {
445+ for _, k := range keyboards {
425446 s.serial++
426447 var keys []byte
427448 for code := range s.heldKeys {
428449 keys = binary.LittleEndian.AppendUint32(keys, code)
429450 }
430- k.SendEnter(s.serial, w.xdg.surfaceResource(), keys)
451+ k.SendEnter(s.serial, surf, keys)
431452 s.sendModifiers(k)
432453 }
433454 }
455+ s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus")
434456 }
435457
436458 func pointerFrame(p *wayland.Pointer) {
@@ -346,8 +346,8 @@ func (s *Server) apply(cmd ports.ClientCommand) {
346 s.changeFocus(c.ID)346 s.changeFocus(c.ID)
347 }347 }
348 case ports.ForwardKey:348 case ports.ForwardKey:
349- w := s.windows[c.ID]349+ _, keyboards := s.focusTarget(c.ID)
350- if c.ID != s.focused || w == nil || !w.mapped || len(s.windowKeyboards(w)) == 0 {350+ if c.ID != s.focused || len(keyboards) == 0 {
351 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")351 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
352 return352 return
353 }353 }
@@ -364,13 +364,13 @@ func (s *Server) apply(cmd ports.ClientCommand) {
364 if c.Key.Pressed {364 if c.Key.Pressed {
365 state = 1365 state = 1
366 }366 }
367- for _, k := range s.windowKeyboards(w) {367+ for _, k := range keyboards {
368 k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)368 k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
369 }369 }
370 if c.Key.State != s.modState {370 if c.Key.State != s.modState {
371 s.modState = c.Key.State371 s.modState = c.Key.State
372 s.serial++372 s.serial++
373- for _, k := range s.windowKeyboards(w) {373+ for _, k := range keyboards {
374 s.sendModifiers(k)374 s.sendModifiers(k)
375 }375 }
376 }376 }
@@ -397,8 +397,12 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
397 if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {397 if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
398 return nil398 return nil
399 }399 }
400+ return s.clientKeyboards(w.xdg.resource.Client())
401+}
402+
403+func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
400 var alive []*wayland.Keyboard404 var alive []*wayland.Keyboard
401- for _, k := range s.keyboards[w.xdg.resource.Client()] {405+ for _, k := range s.keyboards[c] {
402 if k.Resource.Alive() {406 if k.Resource.Alive() {
403 alive = append(alive, k)407 alive = append(alive, k)
404 }408 }
@@ -406,31 +410,49 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
406 return alive410 return alive
407 }411 }
408 412
413+// focusTarget resolves a window or layer ID to its wl_surface and keyboards.
414+func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) {
415+ if w := s.windows[id]; w != nil && w.mapped {
416+ if surf := w.xdg.surfaceResource(); surf != nil {
417+ return surf, s.windowKeyboards(w)
418+ }
419+ }
420+ if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
421+ for resource, state := range s.surfaces {
422+ if state == l.surface && resource.Alive() {
423+ return wayland.WrapSurface(resource), s.clientKeyboards(l.resource.Client())
424+ }
425+ }
426+ }
427+ return nil, nil
428+}
429+
409 func (s *Server) sendModifiers(k *wayland.Keyboard) {430 func (s *Server) sendModifiers(k *wayland.Keyboard) {
410 m := s.modState431 m := s.modState
411 k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)432 k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
412 }433 }
413 434
414 func (s *Server) changeFocus(id ports.WindowID) {435 func (s *Server) changeFocus(id ports.WindowID) {
415- if old := s.windows[s.focused]; old != nil && old.xdg.surfaceResource() != nil {436+ if surf, keyboards := s.focusTarget(s.focused); surf != nil {
416- for _, k := range s.windowKeyboards(old) {437+ for _, k := range keyboards {
417 s.serial++438 s.serial++
418- k.SendLeave(s.serial, old.xdg.surfaceResource())439+ k.SendLeave(s.serial, surf)
419 }440 }
420 }441 }
421 s.focused = 0442 s.focused = 0
422- if w := s.windows[id]; w != nil && w.mapped && w.xdg.surfaceResource() != nil {443+ if surf, keyboards := s.focusTarget(id); surf != nil {
423 s.focused = id444 s.focused = id
424- for _, k := range s.windowKeyboards(w) {445+ for _, k := range keyboards {
425 s.serial++446 s.serial++
426 var keys []byte447 var keys []byte
427 for code := range s.heldKeys {448 for code := range s.heldKeys {
428 keys = binary.LittleEndian.AppendUint32(keys, code)449 keys = binary.LittleEndian.AppendUint32(keys, code)
429 }450 }
430- k.SendEnter(s.serial, w.xdg.surfaceResource(), keys)451+ k.SendEnter(s.serial, surf, keys)
431 s.sendModifiers(k)452 s.sendModifiers(k)
432 }453 }
433 }454 }
455+ s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus")
434 }456 }
435 457
436 func pointerFrame(p *wayland.Pointer) {458 func pointerFrame(p *wayland.Pointer) {
M internal/core/core.go +19 -2
@@ -161,11 +161,28 @@ func (c *Core) clientRect(p Placement) Rect {
161161 return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b}
162162 }
163163
164+// keyboardFocus is the newest mapped top/overlay layer that asks for the keyboard
165+// (exclusive or on-demand), else the focused window. When the layer unmaps,
166+// focus returns to the window.
167+func (c *Core) keyboardFocus() WindowID {
168+ var layer WindowID
169+ for _, l := range c.layers {
170+ if l.Keyboard != 0 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {
171+ layer = l.ID
172+ }
173+ }
174+ if layer != 0 {
175+ return layer
176+ }
177+ id, _ := c.ws.Focused()
178+ return id
179+}
180+
164181 func (c *Core) publish(ctx context.Context) error {
165182 c.seq++
166183 scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)}
167184 alive := map[WindowID]bool{}
168- focus, _ := c.ws.Focused()
185+ focus := c.keyboardFocus()
169186 for _, p := range c.ws.Layout() {
170187 alive[p.ID] = true
171188 scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: p.Focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden})
@@ -390,7 +407,7 @@ func (c *Core) Run(ctx context.Context) error {
390407 }
391408 c.pressed[name] = false
392409 }
393- if id, ok := c.ws.Focused(); ok {
410+ if id := c.keyboardFocus(); id != 0 {
394411 if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
395412 return nil
396413 }
@@ -161,11 +161,28 @@ func (c *Core) clientRect(p Placement) Rect {
161 return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b}161 return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b}
162 }162 }
163 163
164+// keyboardFocus is the newest mapped top/overlay layer that asks for the keyboard
165+// (exclusive or on-demand), else the focused window. When the layer unmaps,
166+// focus returns to the window.
167+func (c *Core) keyboardFocus() WindowID {
168+ var layer WindowID
169+ for _, l := range c.layers {
170+ if l.Keyboard != 0 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {
171+ layer = l.ID
172+ }
173+ }
174+ if layer != 0 {
175+ return layer
176+ }
177+ id, _ := c.ws.Focused()
178+ return id
179+}
180+
164 func (c *Core) publish(ctx context.Context) error {181 func (c *Core) publish(ctx context.Context) error {
165 c.seq++182 c.seq++
166 scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)}183 scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)}
167 alive := map[WindowID]bool{}184 alive := map[WindowID]bool{}
168- focus, _ := c.ws.Focused()185+ focus := c.keyboardFocus()
169 for _, p := range c.ws.Layout() {186 for _, p := range c.ws.Layout() {
170 alive[p.ID] = true187 alive[p.ID] = true
171 scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: p.Focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden})188 scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: p.Focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden})
@@ -390,7 +407,7 @@ func (c *Core) Run(ctx context.Context) error {
390 }407 }
391 c.pressed[name] = false408 c.pressed[name] = false
392 }409 }
393- if id, ok := c.ws.Focused(); ok {410+ if id := c.keyboardFocus(); id != 0 {
394 if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {411 if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
395 return nil412 return nil
396 }413 }
M internal/core/core_test.go +53 -0
@@ -282,3 +282,56 @@ func TestBorderInset(t *testing.T) {
282282 t.Fatal(v)
283283 }
284284 }
285+
286+func TestLayerKeyboardFocus(t *testing.T) {
287+ cfg := config.Defaults()
288+ client := make(chan ports.ClientEvent, 8)
289+ input := make(chan ports.InputEvent, 8)
290+ output := make(chan ports.OutputEvent, 8)
291+ commands := make(chan ports.ClientCommand, 64)
292+ scenes := make(chan ports.Scene, 1)
293+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
294+ if err != nil {
295+ t.Fatal(err)
296+ }
297+ ctx, cancel := context.WithCancel(context.Background())
298+ defer cancel()
299+ go c.Run(ctx)
300+ output <- ports.OutputMode{Width: 100, Height: 80}
301+ receive(t, scenes)
302+ client <- ports.WindowMapped{ID: 1}
303+ receive(t, scenes)
304+ focusOf := func() ports.WindowID {
305+ t.Helper()
306+ for {
307+ if v, ok := receive(t, commands).(ports.FocusWindow); ok {
308+ return v.ID
309+ }
310+ }
311+ }
312+ if id := focusOf(); id != 1 {
313+ t.Fatalf("window focus %d", id)
314+ }
315+ // A bar without keyboard interactivity does not steal focus; a launcher does.
316+ launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 40, Height: 20, Keyboard: 1}
317+ bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 5}
318+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}}
319+ receive(t, scenes)
320+ if id := focusOf(); id != 7 {
321+ t.Fatalf("layer focus %d", id)
322+ }
323+ input <- ports.KeyEvent{Keysym: "a", Keycode: 30, Pressed: true}
324+ for {
325+ if v, ok := receive(t, commands).(ports.ForwardKey); ok {
326+ if v.ID != 7 {
327+ t.Fatalf("key went to %d", v.ID)
328+ }
329+ break
330+ }
331+ }
332+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
333+ receive(t, scenes)
334+ if id := focusOf(); id != 1 {
335+ t.Fatalf("focus not restored: %d", id)
336+ }
337+}
@@ -282,3 +282,56 @@ func TestBorderInset(t *testing.T) {
282 t.Fatal(v)282 t.Fatal(v)
283 }283 }
284 }284 }
285+
286+func TestLayerKeyboardFocus(t *testing.T) {
287+ cfg := config.Defaults()
288+ client := make(chan ports.ClientEvent, 8)
289+ input := make(chan ports.InputEvent, 8)
290+ output := make(chan ports.OutputEvent, 8)
291+ commands := make(chan ports.ClientCommand, 64)
292+ scenes := make(chan ports.Scene, 1)
293+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
294+ if err != nil {
295+ t.Fatal(err)
296+ }
297+ ctx, cancel := context.WithCancel(context.Background())
298+ defer cancel()
299+ go c.Run(ctx)
300+ output <- ports.OutputMode{Width: 100, Height: 80}
301+ receive(t, scenes)
302+ client <- ports.WindowMapped{ID: 1}
303+ receive(t, scenes)
304+ focusOf := func() ports.WindowID {
305+ t.Helper()
306+ for {
307+ if v, ok := receive(t, commands).(ports.FocusWindow); ok {
308+ return v.ID
309+ }
310+ }
311+ }
312+ if id := focusOf(); id != 1 {
313+ t.Fatalf("window focus %d", id)
314+ }
315+ // A bar without keyboard interactivity does not steal focus; a launcher does.
316+ launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 40, Height: 20, Keyboard: 1}
317+ bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 5}
318+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}}
319+ receive(t, scenes)
320+ if id := focusOf(); id != 7 {
321+ t.Fatalf("layer focus %d", id)
322+ }
323+ input <- ports.KeyEvent{Keysym: "a", Keycode: 30, Pressed: true}
324+ for {
325+ if v, ok := receive(t, commands).(ports.ForwardKey); ok {
326+ if v.ID != 7 {
327+ t.Fatalf("key went to %d", v.ID)
328+ }
329+ break
330+ }
331+ }
332+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
333+ receive(t, scenes)
334+ if id := focusOf(); id != 1 {
335+ t.Fatalf("focus not restored: %d", id)
336+ }
337+}
M internal/ports/messages.go +2 -0
@@ -53,6 +53,8 @@ type LayerSurface struct {
5353 Margin [4]int32
5454 Width, Height int
5555 Namespace string
56+ // Keyboard is the requested interactivity: 0 none, 1 exclusive, 2 on-demand.
57+ Keyboard uint32
5658 }
5759
5860 // LayerChanged carries wayland → core the full list of mapped layer surfaces,
@@ -53,6 +53,8 @@ type LayerSurface struct {
53 Margin [4]int3253 Margin [4]int32
54 Width, Height int54 Width, Height int
55 Namespace string55 Namespace string
56+ // Keyboard is the requested interactivity: 0 none, 1 exclusive, 2 on-demand.
57+ Keyboard uint32
56 }58 }
57 59
58 // LayerChanged carries wayland → core the full list of mapped layer surfaces,60 // LayerChanged carries wayland → core the full list of mapped layer surfaces,

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 23

Tags 10

↑↓ moveenter openesc close