detached 4c23646

fix(layer): only exclusive layers take keyboard focus; send leave on layer unmap

commit
4c236465b0ab863606a4033d9f20dc79f891c0f3
author
brice · · 2026-09-24 09:46 UTC
parent
6347722
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +9 -6
M internal/adapters/wayland/layer.go +3 -0
@@ -123,6 +123,9 @@ func (l *layerSurface) unmap() {
123123 if !l.mapped {
124124 return
125125 }
126+ if s := l.shell.server; s.focused == l.id {
127+ s.changeFocus(0)
128+ }
126129 l.mapped = false
127130 l.configured = false
128131 l.acked = false
@@ -123,6 +123,9 @@ func (l *layerSurface) unmap() {
123 if !l.mapped {123 if !l.mapped {
124 return124 return
125 }125 }
126+ if s := l.shell.server; s.focused == l.id {
127+ s.changeFocus(0)
128+ }
126 l.mapped = false129 l.mapped = false
127 l.configured = false130 l.configured = false
128 l.acked = false131 l.acked = false
M internal/core/core.go +4 -4
@@ -161,13 +161,13 @@ 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.
164+// keyboardFocus is the mapped top/overlay layer with exclusive keyboard
165+// interactivity and the highest ID, else the focused window. On-demand layers
166+// never take focus automatically. When the layer unmaps, focus returns to the window.
167167 func (c *Core) keyboardFocus() WindowID {
168168 var layer WindowID
169169 for _, l := range c.layers {
170- if l.Keyboard != 0 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {
170+ if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {
171171 layer = l.ID
172172 }
173173 }
@@ -161,13 +161,13 @@ 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 keyboard164+// keyboardFocus is the mapped top/overlay layer with exclusive keyboard
165-// (exclusive or on-demand), else the focused window. When the layer unmaps,165+// interactivity and the highest ID, else the focused window. On-demand layers
166-// focus returns to the window.166+// never take focus automatically. When the layer unmaps, focus returns to the window.
167 func (c *Core) keyboardFocus() WindowID {167 func (c *Core) keyboardFocus() WindowID {
168 var layer WindowID168 var layer WindowID
169 for _, l := range c.layers {169 for _, l := range c.layers {
170- if l.Keyboard != 0 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {170+ if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {
171 layer = l.ID171 layer = l.ID
172 }172 }
173 }173 }
M internal/core/core_test.go +2 -2
@@ -312,9 +312,9 @@ func TestLayerKeyboardFocus(t *testing.T) {
312312 if id := focusOf(); id != 1 {
313313 t.Fatalf("window focus %d", id)
314314 }
315- // A bar without keyboard interactivity does not steal focus; a launcher does.
315+ // An on-demand bar does not steal focus; an exclusive launcher does.
316316 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}
317+ bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 5, Keyboard: 2}
318318 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}}
319319 receive(t, scenes)
320320 if id := focusOf(); id != 7 {
@@ -312,9 +312,9 @@ func TestLayerKeyboardFocus(t *testing.T) {
312 if id := focusOf(); id != 1 {312 if id := focusOf(); id != 1 {
313 t.Fatalf("window focus %d", id)313 t.Fatalf("window focus %d", id)
314 }314 }
315- // A bar without keyboard interactivity does not steal focus; a launcher does.315+ // An on-demand bar does not steal focus; an exclusive launcher does.
316 launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 40, Height: 20, Keyboard: 1}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}317+ bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 5, Keyboard: 2}
318 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}}318 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}}
319 receive(t, scenes)319 receive(t, scenes)
320 if id := focusOf(); id != 7 {320 if id := focusOf(); id != 7 {

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