detached 461a201

fix(layout): return only the fullscreen window, restack by surviving neighbours

commit
461a201d525ace2192bbd64aa0a283b6aca71d2a
author
brice · · 2026-09-25 20:17 UTC
parent
b49bcb3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +53 -19
M internal/core/layout.go +3 -3
@@ -146,9 +146,9 @@ type Workspace struct {
146146 // column, the column's width and slot, or its floating size.
147147 type origPlace struct {
148148 col, row, slot int
149- // stacked holds a window left in the column: the window returns to its
150- // row only while that window still marks the column.
151- stacked WindowID
149+ // stacked holds the windows left in the column: the window returns to
150+ // its row in the column that still holds one of them.
151+ stacked []WindowID
152152 width Width
153153 float *Float
154154 }
@@ -146,9 +146,9 @@ type Workspace struct {
146 // column, the column's width and slot, or its floating size.146 // column, the column's width and slot, or its floating size.
147 type origPlace struct {147 type origPlace struct {
148 col, row, slot int148 col, row, slot int
149- // stacked holds a window left in the column: the window returns to its149+ // stacked holds the windows left in the column: the window returns to
150- // row only while that window still marks the column.150+ // its row in the column that still holds one of them.
151- stacked WindowID151+ stacked []WindowID
152 width Width152 width Width
153 float *Float153 float *Float
154 }154 }
M internal/core/monitor.go +28 -13
@@ -320,8 +320,10 @@ func (m *Monitor) enterFullscreen(w *Workspace, id WindowID, show bool) {
320320 col := slices.IndexFunc(w.Columns, func(c Column) bool { return slices.Contains(c.Windows, id) })
321321 c := w.Columns[col]
322322 fs.back = origPlace{col: col, row: slices.Index(c.Windows, id), slot: c.Slot, width: c.Width}
323- if i := slices.IndexFunc(c.Windows, func(v WindowID) bool { return v != id }); i >= 0 {
324- fs.back.stacked = c.Windows[i]
323+ for _, v := range c.Windows {
324+ if v != id {
325+ fs.back.stacked = append(fs.back.stacked, v)
326+ }
325327 }
326328 w.RemoveWindow(id)
327329 fs.AddWindow(id)
@@ -358,10 +360,17 @@ func (m *Monitor) leaveFullscreen(fs *Workspace, focus bool) {
358360 case back.float != nil:
359361 // At the bottom: the top float, maybe focused, stays on top.
360362 origin.Floats = slices.Insert(origin.Floats, 0, Float{ID: id, W: back.float.W, H: back.float.H})
361- case back.stacked != 0 && slices.ContainsFunc(origin.Columns, func(c Column) bool { return slices.Contains(c.Windows, back.stacked) }):
362- back.col = slices.IndexFunc(origin.Columns, func(c Column) bool { return slices.Contains(c.Windows, back.stacked) })
363+ case slices.ContainsFunc(origin.Columns, back.holdsStack):
364+ back.col = slices.IndexFunc(origin.Columns, back.holdsStack)
363365 c := &origin.Columns[back.col]
364- row := min(back.row, len(c.Windows))
366+ // Below the windows that were above it and are still there.
367+ row := 0
368+ for _, v := range back.stacked[:back.row] {
369+ if slices.Contains(c.Windows, v) {
370+ row++
371+ }
372+ }
373+ row = min(row, len(c.Windows))
365374 c.Windows = slices.Insert(c.Windows, row, id)
366375 if focus {
367376 origin.Focus, c.Focus, origin.floatFocus = back.col, row, false
@@ -381,11 +390,12 @@ func (m *Monitor) leaveFullscreen(fs *Workspace, focus bool) {
381390 }
382391 }
383392 origin.scroll()
384- shown := m.Current() == fs
385- m.take(fs)
386- if shown {
393+ if m.Current() == fs && (focus || fs.empty()) {
387394 m.show(origin)
388395 }
396+ if fs.empty() {
397+ m.take(fs)
398+ }
389399 }
390400
391401 // awayInSlot reports whether id is away from slot n of w, fullscreen in a
@@ -399,17 +409,22 @@ func (m *Monitor) awayInSlot(w *Workspace, id WindowID, n int) bool {
399409 return false
400410 }
401411
412+// holdsStack reports whether c holds a window of the stack left behind.
413+func (p origPlace) holdsStack(c Column) bool {
414+ return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(p.stacked, v) })
415+}
416+
402417 // fullscreenHome keeps each fullscreen workspace linked to its origin only
403-// while it holds just its fullscreen window and the origin is still here.
404-// Anything else (another window opened or moved in, the window activated
405-// away from fullscreen, the origin gone to another monitor) makes it a
406-// normal workspace, so a later exit never moves the wrong window.
418+// while its window is still there and fullscreen, and the origin is still
419+// on this monitor. Otherwise (the window moved out or activated away from
420+// fullscreen, the origin gone to another monitor) it is a normal workspace.
421+// Other windows opened there (a dialog) stay when the window returns.
407422 func (m *Monitor) fullscreenHome() {
408423 for _, w := range m.all() {
409424 if w.origin == nil {
410425 continue
411426 }
412- if !m.has(w.origin) || w.fullscreen == 0 || len(w.windows()) != 1 || w.windows()[0] != w.fullscreen {
427+ if !m.has(w.origin) || w.fullscreen == 0 || !w.has(w.fullscreen) {
413428 w.origin, w.back = nil, origPlace{}
414429 }
415430 }
@@ -320,8 +320,10 @@ func (m *Monitor) enterFullscreen(w *Workspace, id WindowID, show bool) {
320 col := slices.IndexFunc(w.Columns, func(c Column) bool { return slices.Contains(c.Windows, id) })320 col := slices.IndexFunc(w.Columns, func(c Column) bool { return slices.Contains(c.Windows, id) })
321 c := w.Columns[col]321 c := w.Columns[col]
322 fs.back = origPlace{col: col, row: slices.Index(c.Windows, id), slot: c.Slot, width: c.Width}322 fs.back = origPlace{col: col, row: slices.Index(c.Windows, id), slot: c.Slot, width: c.Width}
323- if i := slices.IndexFunc(c.Windows, func(v WindowID) bool { return v != id }); i >= 0 {323+ for _, v := range c.Windows {
324- fs.back.stacked = c.Windows[i]324+ if v != id {
325+ fs.back.stacked = append(fs.back.stacked, v)
326+ }
325 }327 }
326 w.RemoveWindow(id)328 w.RemoveWindow(id)
327 fs.AddWindow(id)329 fs.AddWindow(id)
@@ -358,10 +360,17 @@ func (m *Monitor) leaveFullscreen(fs *Workspace, focus bool) {
358 case back.float != nil:360 case back.float != nil:
359 // At the bottom: the top float, maybe focused, stays on top.361 // At the bottom: the top float, maybe focused, stays on top.
360 origin.Floats = slices.Insert(origin.Floats, 0, Float{ID: id, W: back.float.W, H: back.float.H})362 origin.Floats = slices.Insert(origin.Floats, 0, Float{ID: id, W: back.float.W, H: back.float.H})
361- case back.stacked != 0 && slices.ContainsFunc(origin.Columns, func(c Column) bool { return slices.Contains(c.Windows, back.stacked) }):363+ case slices.ContainsFunc(origin.Columns, back.holdsStack):
362- back.col = slices.IndexFunc(origin.Columns, func(c Column) bool { return slices.Contains(c.Windows, back.stacked) })364+ back.col = slices.IndexFunc(origin.Columns, back.holdsStack)
363 c := &origin.Columns[back.col]365 c := &origin.Columns[back.col]
364- row := min(back.row, len(c.Windows))366+ // Below the windows that were above it and are still there.
367+ row := 0
368+ for _, v := range back.stacked[:back.row] {
369+ if slices.Contains(c.Windows, v) {
370+ row++
371+ }
372+ }
373+ row = min(row, len(c.Windows))
365 c.Windows = slices.Insert(c.Windows, row, id)374 c.Windows = slices.Insert(c.Windows, row, id)
366 if focus {375 if focus {
367 origin.Focus, c.Focus, origin.floatFocus = back.col, row, false376 origin.Focus, c.Focus, origin.floatFocus = back.col, row, false
@@ -381,11 +390,12 @@ func (m *Monitor) leaveFullscreen(fs *Workspace, focus bool) {
381 }390 }
382 }391 }
383 origin.scroll()392 origin.scroll()
384- shown := m.Current() == fs393+ if m.Current() == fs && (focus || fs.empty()) {
385- m.take(fs)
386- if shown {
387 m.show(origin)394 m.show(origin)
388 }395 }
396+ if fs.empty() {
397+ m.take(fs)
398+ }
389 }399 }
390 400
391 // awayInSlot reports whether id is away from slot n of w, fullscreen in a401 // awayInSlot reports whether id is away from slot n of w, fullscreen in a
@@ -399,17 +409,22 @@ func (m *Monitor) awayInSlot(w *Workspace, id WindowID, n int) bool {
399 return false409 return false
400 }410 }
401 411
412+// holdsStack reports whether c holds a window of the stack left behind.
413+func (p origPlace) holdsStack(c Column) bool {
414+ return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(p.stacked, v) })
415+}
416+
402 // fullscreenHome keeps each fullscreen workspace linked to its origin only417 // fullscreenHome keeps each fullscreen workspace linked to its origin only
403-// while it holds just its fullscreen window and the origin is still here.418+// while its window is still there and fullscreen, and the origin is still
404-// Anything else (another window opened or moved in, the window activated419+// on this monitor. Otherwise (the window moved out or activated away from
405-// away from fullscreen, the origin gone to another monitor) makes it a420+// fullscreen, the origin gone to another monitor) it is a normal workspace.
406-// normal workspace, so a later exit never moves the wrong window.421+// Other windows opened there (a dialog) stay when the window returns.
407 func (m *Monitor) fullscreenHome() {422 func (m *Monitor) fullscreenHome() {
408 for _, w := range m.all() {423 for _, w := range m.all() {
409 if w.origin == nil {424 if w.origin == nil {
410 continue425 continue
411 }426 }
412- if !m.has(w.origin) || w.fullscreen == 0 || len(w.windows()) != 1 || w.windows()[0] != w.fullscreen {427+ if !m.has(w.origin) || w.fullscreen == 0 || !w.has(w.fullscreen) {
413 w.origin, w.back = nil, origPlace{}428 w.origin, w.back = nil, origPlace{}
414 }429 }
415 }430 }
M internal/core/monitor_test.go +16 -3
@@ -487,14 +487,27 @@ func TestMonitorFixedFullscreenEdges(t *testing.T) {
487487 }
488488 return m
489489 }
490- t.Run("another window opened there unlinks it", func(t *testing.T) {
490+ t.Run("a window opened there stays when it returns", func(t *testing.T) {
491491 m := fixed()
492492 m.Current().FocusID(2)
493493 m.ToggleFullscreen()
494494 m.AddWindow(4)
495+ m.Current().MoveColumn(-1)
495496 m.SetFullscreen(2, false)
496- if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 3}, {2, 4}, {}}) {
497- t.Fatal(got)
497+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3}, {4}, {}}) || m.Active != 0 {
498+ t.Fatal(got, m.Active)
499+ }
500+ })
501+ t.Run("stack of three keeps its column", func(t *testing.T) {
502+ m := fixed()
503+ w := m.Current()
504+ w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 3, 5}}}
505+ w.FocusID(3)
506+ m.ToggleFullscreen()
507+ m.RemoveWindow(2)
508+ m.ToggleFullscreen()
509+ if len(w.Columns) != 2 || !slices.Equal(w.Columns[1].Windows, []WindowID{3, 5}) {
510+ t.Fatal(w.Columns)
498511 }
499512 })
500513 t.Run("client exit keeps the user's focus", func(t *testing.T) {
@@ -487,14 +487,27 @@ func TestMonitorFixedFullscreenEdges(t *testing.T) {
487 }487 }
488 return m488 return m
489 }489 }
490- t.Run("another window opened there unlinks it", func(t *testing.T) {490+ t.Run("a window opened there stays when it returns", func(t *testing.T) {
491 m := fixed()491 m := fixed()
492 m.Current().FocusID(2)492 m.Current().FocusID(2)
493 m.ToggleFullscreen()493 m.ToggleFullscreen()
494 m.AddWindow(4)494 m.AddWindow(4)
495+ m.Current().MoveColumn(-1)
495 m.SetFullscreen(2, false)496 m.SetFullscreen(2, false)
496- if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 3}, {2, 4}, {}}) {497+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3}, {4}, {}}) || m.Active != 0 {
497- t.Fatal(got)498+ t.Fatal(got, m.Active)
499+ }
500+ })
501+ t.Run("stack of three keeps its column", func(t *testing.T) {
502+ m := fixed()
503+ w := m.Current()
504+ w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 3, 5}}}
505+ w.FocusID(3)
506+ m.ToggleFullscreen()
507+ m.RemoveWindow(2)
508+ m.ToggleFullscreen()
509+ if len(w.Columns) != 2 || !slices.Equal(w.Columns[1].Windows, []WindowID{3, 5}) {
510+ t.Fatal(w.Columns)
498 }511 }
499 })512 })
500 t.Run("client exit keeps the user's focus", func(t *testing.T) {513 t.Run("client exit keeps the user's focus", func(t *testing.T) {
M internal/core/slots_test.go +6 -0
@@ -384,4 +384,10 @@ func TestSlotKeptWhileFullscreenAway(t *testing.T) {
384384 r.press(t, "d") // leave and show dev while code is away
385385 r.press(t, "d")
386386 noSpawn(t, r.spawn)
387+ // code opens a second window while fullscreen: the slot stays.
388+ r.client <- ports.WindowMapped{ID: 9}
389+ scene(t, r.scenes)
390+ r.press(t, "d")
391+ r.press(t, "d")
392+ noSpawn(t, r.spawn)
387393 }
@@ -384,4 +384,10 @@ func TestSlotKeptWhileFullscreenAway(t *testing.T) {
384 r.press(t, "d") // leave and show dev while code is away384 r.press(t, "d") // leave and show dev while code is away
385 r.press(t, "d")385 r.press(t, "d")
386 noSpawn(t, r.spawn)386 noSpawn(t, r.spawn)
387+ // code opens a second window while fullscreen: the slot stays.
388+ r.client <- ports.WindowMapped{ID: 9}
389+ scene(t, r.scenes)
390+ r.press(t, "d")
391+ r.press(t, "d")
392+ noSpawn(t, r.spawn)
387 }393 }

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