detached f716fc9

fix(core): look past only fullscreen bands when restoring columns

fullscreenBand marks the column it puts in its own band; restoreBand
looks one band farther only for marked columns, so a column the user
moved past a band stays there.
commit
f716fc9d3b12837d7a9086ae1c96cbdcecb63320
author
brice · · 2026-10-23 00:56 UTC
parent
c55955b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +35 -10
M internal/core/cascade.go +12 -5
@@ -371,6 +371,9 @@ func (w *Workspace) shiftBand(lo, hi, dir, at int) bool {
371371 id, _ := w.Focused()
372372 bands := w.bandList()
373373 cols := slices.Clone(bands[b][lo-first : hi-first+1])
374+ for k := range cols {
375+ cols[k].ownBand = false
376+ }
374377 bands[b] = slices.Delete(bands[b], lo-first, hi-first+1)
375378 w.setBands(w.sendBand(bands, b, dir, at, cols), id)
376379 return true
@@ -461,6 +464,7 @@ func (w *Workspace) fullscreenBand(id WindowID) {
461464 }
462465 bands[b][i-first] = col
463466 }
467+ nc.ownBand = true
464468 bands = slices.Insert(bands, b+1, []Column{nc})
465469 w.setBands(bands, focus)
466470 }
@@ -524,8 +528,12 @@ func (w *Workspace) restoreBand() bool {
524528 if head < 0 {
525529 continue
526530 }
527- for _, nb := range nbs {
528- i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] })
531+ for k, nb := range nbs {
532+ // Past the adjacent band, only a column fullscreenBand put in
533+ // a band of its own is taken: one the user moved there stays.
534+ i := slices.IndexFunc(bands[nb], func(c Column) bool {
535+ return c.Windows[0] == e[head] && (k == 0 || c.ownBand)
536+ })
529537 if i < 0 {
530538 continue
531539 }
@@ -541,13 +549,12 @@ func (w *Workspace) restoreBand() bool {
541549 return got
542550 }
543551 // reach lists the bands dir away to look in: the adjacent one, and the
544- // next when the adjacent holds a single column, as a fullscreen column
545- // split off a band of its own meanwhile leaves.
552+ // next, where a column that fullscreenBand split off may lie.
546553 reach := func(dir int) []int {
547554 var nbs []int
548555 for n := b + dir; n >= 0 && n < len(bands); n += dir {
549556 nbs = append(nbs, n)
550- if len(bands[n]) != 1 || len(nbs) == 2 {
557+ if len(nbs) == 2 {
551558 break
552559 }
553560 }
@@ -371,6 +371,9 @@ func (w *Workspace) shiftBand(lo, hi, dir, at int) bool {
371 id, _ := w.Focused()371 id, _ := w.Focused()
372 bands := w.bandList()372 bands := w.bandList()
373 cols := slices.Clone(bands[b][lo-first : hi-first+1])373 cols := slices.Clone(bands[b][lo-first : hi-first+1])
374+ for k := range cols {
375+ cols[k].ownBand = false
376+ }
374 bands[b] = slices.Delete(bands[b], lo-first, hi-first+1)377 bands[b] = slices.Delete(bands[b], lo-first, hi-first+1)
375 w.setBands(w.sendBand(bands, b, dir, at, cols), id)378 w.setBands(w.sendBand(bands, b, dir, at, cols), id)
376 return true379 return true
@@ -461,6 +464,7 @@ func (w *Workspace) fullscreenBand(id WindowID) {
461 }464 }
462 bands[b][i-first] = col465 bands[b][i-first] = col
463 }466 }
467+ nc.ownBand = true
464 bands = slices.Insert(bands, b+1, []Column{nc})468 bands = slices.Insert(bands, b+1, []Column{nc})
465 w.setBands(bands, focus)469 w.setBands(bands, focus)
466 }470 }
@@ -524,8 +528,12 @@ func (w *Workspace) restoreBand() bool {
524 if head < 0 {528 if head < 0 {
525 continue529 continue
526 }530 }
527- for _, nb := range nbs {531+ for k, nb := range nbs {
528- i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] })532+ // Past the adjacent band, only a column fullscreenBand put in
533+ // a band of its own is taken: one the user moved there stays.
534+ i := slices.IndexFunc(bands[nb], func(c Column) bool {
535+ return c.Windows[0] == e[head] && (k == 0 || c.ownBand)
536+ })
529 if i < 0 {537 if i < 0 {
530 continue538 continue
531 }539 }
@@ -541,13 +549,12 @@ func (w *Workspace) restoreBand() bool {
541 return got549 return got
542 }550 }
543 // reach lists the bands dir away to look in: the adjacent one, and the551 // reach lists the bands dir away to look in: the adjacent one, and the
544- // next when the adjacent holds a single column, as a fullscreen column552+ // next, where a column that fullscreenBand split off may lie.
545- // split off a band of its own meanwhile leaves.
546 reach := func(dir int) []int {553 reach := func(dir int) []int {
547 var nbs []int554 var nbs []int
548 for n := b + dir; n >= 0 && n < len(bands); n += dir {555 for n := b + dir; n >= 0 && n < len(bands); n += dir {
549 nbs = append(nbs, n)556 nbs = append(nbs, n)
550- if len(bands[n]) != 1 || len(nbs) == 2 {557+ if len(nbs) == 2 {
551 break558 break
552 }559 }
553 }560 }
M internal/core/cascade_fullscreen_band_test.go +19 -5
@@ -239,12 +239,12 @@ func TestCascadeFullscreenRestoresAcrossBand(t *testing.T) {
239239 }
240240
241241 func TestFullscreenMoveDestinations(t *testing.T) {
242- t.Run("mayCover refuses", func(t *testing.T) {
243- // A fixed destination refuses a cover unless the window is focused;
244- // the arrival is focused, so it ends fullscreen.
242+ t.Run("arrival focused despite left mark", func(t *testing.T) {
243+ // The destination has a pending "left" mark on the window, which
244+ // mayCover would refuse unless it is focused; the arrival is
245+ // focused, so it ends fullscreen.
245246 c, w := cascadeMovesCore(t, 2, 3, true)
246247 dst := screenWorkspace(c, "R")
247- dst.Overflow = OverflowFixed
248248 dst.left = 2
249249 w.ToggleFullscreen()
250250 c.applyAction(ActionMoveColumnRight)
@@ -253,7 +253,7 @@ func TestFullscreenMoveDestinations(t *testing.T) {
253253 t.Fatalf("fullscreen %d, want 2", dst.fullscreen)
254254 }
255255 })
256- t.Run("stacked column in a band", func(t *testing.T) {
256+ t.Run("fullscreen window expelled from a stack then moved", func(t *testing.T) {
257257 c, w := cascadeMovesCore(t, 3, 3, true)
258258 w.FocusID(3)
259259 c.applyAction(ActionConsumeOrExpelLeft) // 3 under 2
@@ -281,3 +281,17 @@ func TestFullscreenMoveDestinations(t *testing.T) {
281281 }
282282 })
283283 }
284+
285+// A band the user shaped is not looked past: P, pushed down from F, then
286+// moved past Y on purpose, stays there.
287+func TestRestoreBandDoesNotLookPastUserBand(t *testing.T) {
288+ c, w := cascadeMovesCore(t, 3, 3, false)
289+ w.FocusID(1)
290+ w.ToggleFullWidth() // {1} {2 3}
291+ w.FocusID(2)
292+ c.applyAction(ActionMoveWindowDown) // {1} {3} {2}
293+ wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2})
294+ w.FocusID(1)
295+ w.ToggleFullWidth()
296+ wantBands(t, w, []WindowID{1, 3}, []WindowID{2})
297+}
@@ -239,12 +239,12 @@ func TestCascadeFullscreenRestoresAcrossBand(t *testing.T) {
239 }239 }
240 240
241 func TestFullscreenMoveDestinations(t *testing.T) {241 func TestFullscreenMoveDestinations(t *testing.T) {
242- t.Run("mayCover refuses", func(t *testing.T) {242+ t.Run("arrival focused despite left mark", func(t *testing.T) {
243- // A fixed destination refuses a cover unless the window is focused;243+ // The destination has a pending "left" mark on the window, which
244- // the arrival is focused, so it ends fullscreen.244+ // mayCover would refuse unless it is focused; the arrival is
245+ // focused, so it ends fullscreen.
245 c, w := cascadeMovesCore(t, 2, 3, true)246 c, w := cascadeMovesCore(t, 2, 3, true)
246 dst := screenWorkspace(c, "R")247 dst := screenWorkspace(c, "R")
247- dst.Overflow = OverflowFixed
248 dst.left = 2248 dst.left = 2
249 w.ToggleFullscreen()249 w.ToggleFullscreen()
250 c.applyAction(ActionMoveColumnRight)250 c.applyAction(ActionMoveColumnRight)
@@ -253,7 +253,7 @@ func TestFullscreenMoveDestinations(t *testing.T) {
253 t.Fatalf("fullscreen %d, want 2", dst.fullscreen)253 t.Fatalf("fullscreen %d, want 2", dst.fullscreen)
254 }254 }
255 })255 })
256- t.Run("stacked column in a band", func(t *testing.T) {256+ t.Run("fullscreen window expelled from a stack then moved", func(t *testing.T) {
257 c, w := cascadeMovesCore(t, 3, 3, true)257 c, w := cascadeMovesCore(t, 3, 3, true)
258 w.FocusID(3)258 w.FocusID(3)
259 c.applyAction(ActionConsumeOrExpelLeft) // 3 under 2259 c.applyAction(ActionConsumeOrExpelLeft) // 3 under 2
@@ -281,3 +281,17 @@ func TestFullscreenMoveDestinations(t *testing.T) {
281 }281 }
282 })282 })
283 }283 }
284+
285+// A band the user shaped is not looked past: P, pushed down from F, then
286+// moved past Y on purpose, stays there.
287+func TestRestoreBandDoesNotLookPastUserBand(t *testing.T) {
288+ c, w := cascadeMovesCore(t, 3, 3, false)
289+ w.FocusID(1)
290+ w.ToggleFullWidth() // {1} {2 3}
291+ w.FocusID(2)
292+ c.applyAction(ActionMoveWindowDown) // {1} {3} {2}
293+ wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2})
294+ w.FocusID(1)
295+ w.ToggleFullWidth()
296+ wantBands(t, w, []WindowID{1, 3}, []WindowID{2})
297+}
M internal/core/layout.go +4 -0
@@ -40,6 +40,10 @@ type Column struct {
4040 // maximize-column or cycle-column-width sent to the band above or
4141 // below; they come back when the column is restored.
4242 pushedUp, pushedDown [][]WindowID
43+ // ownBand marks a column fullscreenBand put in a band of its own:
44+ // restoreBand looks past such a band. shiftBand clears it when the
45+ // column moves to another band on purpose.
46+ ownBand bool
4347 }
4448 type Placement struct {
4549 ID WindowID
@@ -40,6 +40,10 @@ type Column struct {
40 // maximize-column or cycle-column-width sent to the band above or40 // maximize-column or cycle-column-width sent to the band above or
41 // below; they come back when the column is restored.41 // below; they come back when the column is restored.
42 pushedUp, pushedDown [][]WindowID42 pushedUp, pushedDown [][]WindowID
43+ // ownBand marks a column fullscreenBand put in a band of its own:
44+ // restoreBand looks past such a band. shiftBand clears it when the
45+ // column moves to another band on purpose.
46+ ownBand bool
43 }47 }
44 type Placement struct {48 type Placement struct {
45 ID WindowID49 ID WindowID

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