detached 2ca07b7

test(core): cover auto-fullscreen focus rules

commit
2ca07b7799ef7ad9e857a1c42c53c8c89dc7c9d4
author
brice · · 2026-10-23 00:55 UTC
parent
bc208ba
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +54 -5
M docs/config.md +1 -1
@@ -260,7 +260,7 @@ bind.XF86MonBrightnessDown = spawn brightnessctl set 5%-
260260 | `scale-up` / `scale-down` | Zoom the display; the scale is saved to the config |
261261 | `quit` | Exit NeferWL |
262262
263-A parentless window whose first committed size is exactly the logical size of the output it is on, without asking for fullscreen (a game in borderless-windowed mode), is put fullscreen as if by `toggle-fullscreen`, without being resized, when it is the focused window of the shown workspace. Later resizes do not.
263+Note on `toggle-fullscreen`: a parentless window whose first committed size is exactly the logical size of the output it is on, without asking for fullscreen (a game in borderless-windowed mode), is put fullscreen as if by `toggle-fullscreen`, without being resized, when it is the focused window of the shown workspace. Later resizes do not.
264264
265265 ### Default binds
266266
@@ -260,7 +260,7 @@ bind.XF86MonBrightnessDown = spawn brightnessctl set 5%-
260 | `scale-up` / `scale-down` | Zoom the display; the scale is saved to the config |260 | `scale-up` / `scale-down` | Zoom the display; the scale is saved to the config |
261 | `quit` | Exit NeferWL |261 | `quit` | Exit NeferWL |
262 262
263-A parentless window whose first committed size is exactly the logical size of the output it is on, without asking for fullscreen (a game in borderless-windowed mode), is put fullscreen as if by `toggle-fullscreen`, without being resized, when it is the focused window of the shown workspace. Later resizes do not.263+Note on `toggle-fullscreen`: a parentless window whose first committed size is exactly the logical size of the output it is on, without asking for fullscreen (a game in borderless-windowed mode), is put fullscreen as if by `toggle-fullscreen`, without being resized, when it is the focused window of the shown workspace. Later resizes do not.
264 264
265 ### Default binds265 ### Default binds
266 266
M internal/adapters/wayland/xdg.go +3 -3
@@ -418,10 +418,10 @@ func (w *window) afterCommit() {
418418 return
419419 }
420420 if !w.floating {
421- // Core is authoritative: this prefilter matches any output, and
422- // core compares with the output the window is on.
423421 // Core reads a tile's first size once, to spot a window that maps
424- // at its output's size without asking for fullscreen.
422+ // at its output's size without asking for fullscreen. This
423+ // prefilter accepts any output size; core compares with the output
424+ // the window is on.
425425 if fw, fh := w.size(); !w.sized && fw > 0 && fh > 0 {
426426 w.sized = true
427427 if !w.xdg.server.outputSized(fw, fh) {
@@ -418,10 +418,10 @@ func (w *window) afterCommit() {
418 return418 return
419 }419 }
420 if !w.floating {420 if !w.floating {
421- // Core is authoritative: this prefilter matches any output, and
422- // core compares with the output the window is on.
423 // Core reads a tile's first size once, to spot a window that maps421 // Core reads a tile's first size once, to spot a window that maps
424- // at its output's size without asking for fullscreen.422+ // at its output's size without asking for fullscreen. This
423+ // prefilter accepts any output size; core compares with the output
424+ // the window is on.
425 if fw, fh := w.size(); !w.sized && fw > 0 && fh > 0 {425 if fw, fh := w.size(); !w.sized && fw > 0 && fh > 0 {
426 w.sized = true426 w.sized = true
427 if !w.xdg.server.outputSized(fw, fh) {427 if !w.xdg.server.outputSized(fw, fh) {
M internal/core/legacy_fullscreen_internal_test.go +50 -1
@@ -120,7 +120,7 @@ func TestLegacyFullscreenRespectsFocus(t *testing.T) {
120120 ws := sc.mon.Current()
121121 sc.mon.Focus(1)
122122 if sc.mon.Current() == ws {
123- t.Skip("no second workspace")
123+ t.Fatal("setup: no second workspace")
124124 }
125125 o := sc.rect()
126126 c.resizeFloating(ports.WindowResized{ID: 1, Width: o.W, Height: o.H})
@@ -167,3 +167,52 @@ func TestLegacyFullscreenRemapAndOutputs(t *testing.T) {
167167 }
168168 })
169169 }
170+
171+// Only the focused window of the shown workspace is a candidate.
172+func TestLegacyFullscreenNeedsFocusAtFirstSize(t *testing.T) {
173+ t.Run("focus moved before the commit", func(t *testing.T) {
174+ c, _, _ := fadeCore(t)
175+ sc := c.cur()
176+ c.mapWindow(ports.WindowMapped{ID: 1})
177+ c.mapWindow(ports.WindowMapped{ID: 2})
178+ w := sc.mon.Current()
179+ w.FocusID(1)
180+ o := sc.rect()
181+ c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H})
182+ if w.fullscreen != 0 {
183+ t.Fatalf("unfocused window went fullscreen: %d", w.fullscreen)
184+ }
185+ if id, _ := w.Focused(); id != 1 {
186+ t.Fatalf("focus moved to %d", id)
187+ }
188+ })
189+ t.Run("quiet column", func(t *testing.T) {
190+ c, _, _ := fadeCore(t)
191+ sc := c.cur()
192+ c.mapWindow(ports.WindowMapped{ID: 1})
193+ c.windows.mapped(ports.WindowMapped{ID: 2}, c.now())
194+ w := sc.mon.Current()
195+ w.addColumnQuiet(Column{Windows: []WindowID{2}})
196+ o := sc.rect()
197+ c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H})
198+ if w.fullscreen != 0 {
199+ t.Fatalf("quiet window went fullscreen: %d", w.fullscreen)
200+ }
201+ if id, _ := w.Focused(); id != 1 {
202+ t.Fatalf("focus moved to %d", id)
203+ }
204+ })
205+ t.Run("quiet float", func(t *testing.T) {
206+ c, _, _ := fadeCore(t)
207+ sc := c.cur()
208+ c.mapWindow(ports.WindowMapped{ID: 1})
209+ c.windows.mapped(ports.WindowMapped{ID: 2, Floating: true}, c.now())
210+ w := sc.mon.Current()
211+ w.addFloat(Float{ID: 2}, true)
212+ o := sc.rect()
213+ c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H})
214+ if w.fullscreen != 0 {
215+ t.Fatalf("quiet float went fullscreen: %d", w.fullscreen)
216+ }
217+ })
218+}
@@ -120,7 +120,7 @@ func TestLegacyFullscreenRespectsFocus(t *testing.T) {
120 ws := sc.mon.Current()120 ws := sc.mon.Current()
121 sc.mon.Focus(1)121 sc.mon.Focus(1)
122 if sc.mon.Current() == ws {122 if sc.mon.Current() == ws {
123- t.Skip("no second workspace")123+ t.Fatal("setup: no second workspace")
124 }124 }
125 o := sc.rect()125 o := sc.rect()
126 c.resizeFloating(ports.WindowResized{ID: 1, Width: o.W, Height: o.H})126 c.resizeFloating(ports.WindowResized{ID: 1, Width: o.W, Height: o.H})
@@ -167,3 +167,52 @@ func TestLegacyFullscreenRemapAndOutputs(t *testing.T) {
167 }167 }
168 })168 })
169 }169 }
170+
171+// Only the focused window of the shown workspace is a candidate.
172+func TestLegacyFullscreenNeedsFocusAtFirstSize(t *testing.T) {
173+ t.Run("focus moved before the commit", func(t *testing.T) {
174+ c, _, _ := fadeCore(t)
175+ sc := c.cur()
176+ c.mapWindow(ports.WindowMapped{ID: 1})
177+ c.mapWindow(ports.WindowMapped{ID: 2})
178+ w := sc.mon.Current()
179+ w.FocusID(1)
180+ o := sc.rect()
181+ c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H})
182+ if w.fullscreen != 0 {
183+ t.Fatalf("unfocused window went fullscreen: %d", w.fullscreen)
184+ }
185+ if id, _ := w.Focused(); id != 1 {
186+ t.Fatalf("focus moved to %d", id)
187+ }
188+ })
189+ t.Run("quiet column", func(t *testing.T) {
190+ c, _, _ := fadeCore(t)
191+ sc := c.cur()
192+ c.mapWindow(ports.WindowMapped{ID: 1})
193+ c.windows.mapped(ports.WindowMapped{ID: 2}, c.now())
194+ w := sc.mon.Current()
195+ w.addColumnQuiet(Column{Windows: []WindowID{2}})
196+ o := sc.rect()
197+ c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H})
198+ if w.fullscreen != 0 {
199+ t.Fatalf("quiet window went fullscreen: %d", w.fullscreen)
200+ }
201+ if id, _ := w.Focused(); id != 1 {
202+ t.Fatalf("focus moved to %d", id)
203+ }
204+ })
205+ t.Run("quiet float", func(t *testing.T) {
206+ c, _, _ := fadeCore(t)
207+ sc := c.cur()
208+ c.mapWindow(ports.WindowMapped{ID: 1})
209+ c.windows.mapped(ports.WindowMapped{ID: 2, Floating: true}, c.now())
210+ w := sc.mon.Current()
211+ w.addFloat(Float{ID: 2}, true)
212+ o := sc.rect()
213+ c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H})
214+ if w.fullscreen != 0 {
215+ t.Fatalf("quiet float went fullscreen: %d", w.fullscreen)
216+ }
217+ })
218+}

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