detached f4b2d68

fix(core): push the far-end cascade column on cycle-column-width

A full band now makes room from its far end: the rightmost column goes down,
or the leftmost goes up from the last column, instead of the direct
neighbor. Restored columns go back to the band's ends.
commit
f4b2d684179d4c4f09b44af4d4bb003dbb24261c
author
brice · · 2026-10-16 18:34 UTC
parent
1b76e21
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +29 -26
M docs/config.md +1 -1
@@ -247,7 +247,7 @@ The default layout. Columns sit side by side in bands of up to `max-columns`, st
247247 - Up/down moves through the windows of a column, then to the band above or below.
248248 - `move-window-up/down` past the column's top or bottom sends the whole column to the band above or below. It joins that band when it has room, else it gets a new band between the two.
249249 - A new window joins the focused band when it has room, else it gets a new band right below.
250-- `cycle-column-width` in a full band makes room: the right neighbor goes to the band below (the left one to the band above from the last column). The end of the cycle brings it back.
250+- `cycle-column-width` in a full band makes room: the rightmost column goes to the band below (from the last column, the leftmost goes to the band above). The end of the cycle brings it back.
251251 - `maximize-column` gives the column a band of its own: the columns on its left go to the band above, the ones on its right to the band below. Maximizing again forms the band back.
252252 - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it.
253253
@@ -247,7 +247,7 @@ The default layout. Columns sit side by side in bands of up to `max-columns`, st
247 - Up/down moves through the windows of a column, then to the band above or below.247 - Up/down moves through the windows of a column, then to the band above or below.
248 - `move-window-up/down` past the column's top or bottom sends the whole column to the band above or below. It joins that band when it has room, else it gets a new band between the two.248 - `move-window-up/down` past the column's top or bottom sends the whole column to the band above or below. It joins that band when it has room, else it gets a new band between the two.
249 - A new window joins the focused band when it has room, else it gets a new band right below.249 - A new window joins the focused band when it has room, else it gets a new band right below.
250-- `cycle-column-width` in a full band makes room: the right neighbor goes to the band below (the left one to the band above from the last column). The end of the cycle brings it back.250+- `cycle-column-width` in a full band makes room: the rightmost column goes to the band below (from the last column, the leftmost goes to the band above). The end of the cycle brings it back.
251 - `maximize-column` gives the column a band of its own: the columns on its left go to the band above, the ones on its right to the band below. Maximizing again forms the band back.251 - `maximize-column` gives the column a band of its own: the columns on its left go to the band above, the ones on its right to the band below. Maximizing again forms the band back.
252 - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it.252 - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it.
253 253
M internal/core/cascade.go +17 -16
@@ -378,11 +378,11 @@ func (w *Workspace) isolateBand() {
378378 left := slices.Clone(bands[b][:p])
379379 right := slices.Clone(bands[b][p+1:])
380380 c := bands[b][p]
381- // Earlier pushes (cycle-column-width) stay remembered and were nearer
382- // at first, so the new ones go past them: pushedUp ends nearest,
383- // pushedDown starts nearest.
384- c.pushedUp = append(columnIDs(left), c.pushedUp...)
385- c.pushedDown = append(c.pushedDown, columnIDs(right)...)
381+ // Both lists keep the band's left-to-right order. Earlier pushes
382+ // (cycle-column-width) took the far ends of the band, so the new ones
383+ // were nearer the column.
384+ c.pushedUp = append(c.pushedUp, columnIDs(left)...)
385+ c.pushedDown = append(columnIDs(right), c.pushedDown...)
386386 bands[b] = []Column{c}
387387 if len(right) > 0 {
388388 bands = w.sendBand(bands, b, 1, 0, right)
@@ -394,8 +394,9 @@ func (w *Workspace) isolateBand() {
394394 }
395395
396396 // pushNeighbor makes room in the focused column's full cascade band for a
397-// wider column: its right neighbor goes to the band below, or its left
398-// one to the band above at the band's right end. The column remembers it.
397+// wider column: the column at the far end of the band leaves it. The
398+// rightmost goes to the band below; from the last column, the leftmost
399+// goes to the band above, at its right end. The column remembers it.
399400 func (w *Workspace) pushNeighbor() {
400401 first, last := w.bandBounds(w.band(w.Focus))
401402 if first == last {
@@ -404,20 +405,21 @@ func (w *Workspace) pushNeighbor() {
404405 id, _ := w.Focused()
405406 c := &w.Columns[w.Focus]
406407 if w.Focus < last {
407- c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[w.Focus+1:w.Focus+2])...)
408- w.shiftBand(w.Focus+1, w.Focus+1, 1, 0)
408+ c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[last:last+1])...)
409+ w.shiftBand(last, last, 1, 0)
409410 } else {
410- c.pushedUp = append(columnIDs(w.Columns[w.Focus-1:w.Focus]), c.pushedUp...)
411- w.shiftBand(w.Focus-1, w.Focus-1, -1, max(w.MaxColumns, 1))
411+ c.pushedUp = append(columnIDs(w.Columns[first:first+1]), c.pushedUp...)
412+ w.shiftBand(first, first, -1, max(w.MaxColumns, 1))
412413 }
413414 w.Focus = w.columnOf(id)
414415 }
415416
416417 // restoreBand brings back the columns isolateBand or pushNeighbor sent
417418 // away, those still in the adjacent band, as many as the band has room
418-// for, nearest to the focused column first: the ones sent up before it,
419-// the ones sent down after it. The others stay where they are. It reports
420-// whether the column remembered any.
419+// for, nearest to the focused column first: the ones sent up at the start
420+// of the band, the ones sent down at its end, in their former order. The
421+// others stay where they are. It reports whether the column remembered
422+// any.
421423 func (w *Workspace) restoreBand() bool {
422424 c := &w.Columns[w.Focus]
423425 up, down := c.pushedUp, c.pushedDown
@@ -468,8 +470,7 @@ func (w *Workspace) restoreBand() bool {
468470 }
469471 above := take(b-1, up, true)
470472 below := take(b+1, down, false)
471- p := slices.IndexFunc(bands[b], func(c Column) bool { return slices.Contains(c.Windows, id) })
472- bands[b] = slices.Concat(bands[b][:p], above, bands[b][p:p+1], below, bands[b][p+1:])
473+ bands[b] = slices.Concat(above, bands[b], below)
473474 w.setBands(bands, id)
474475 return true
475476 }
@@ -378,11 +378,11 @@ func (w *Workspace) isolateBand() {
378 left := slices.Clone(bands[b][:p])378 left := slices.Clone(bands[b][:p])
379 right := slices.Clone(bands[b][p+1:])379 right := slices.Clone(bands[b][p+1:])
380 c := bands[b][p]380 c := bands[b][p]
381- // Earlier pushes (cycle-column-width) stay remembered and were nearer381+ // Both lists keep the band's left-to-right order. Earlier pushes
382- // at first, so the new ones go past them: pushedUp ends nearest,382+ // (cycle-column-width) took the far ends of the band, so the new ones
383- // pushedDown starts nearest.383+ // were nearer the column.
384- c.pushedUp = append(columnIDs(left), c.pushedUp...)384+ c.pushedUp = append(c.pushedUp, columnIDs(left)...)
385- c.pushedDown = append(c.pushedDown, columnIDs(right)...)385+ c.pushedDown = append(columnIDs(right), c.pushedDown...)
386 bands[b] = []Column{c}386 bands[b] = []Column{c}
387 if len(right) > 0 {387 if len(right) > 0 {
388 bands = w.sendBand(bands, b, 1, 0, right)388 bands = w.sendBand(bands, b, 1, 0, right)
@@ -394,8 +394,9 @@ func (w *Workspace) isolateBand() {
394 }394 }
395 395
396 // pushNeighbor makes room in the focused column's full cascade band for a396 // pushNeighbor makes room in the focused column's full cascade band for a
397-// wider column: its right neighbor goes to the band below, or its left397+// wider column: the column at the far end of the band leaves it. The
398-// one to the band above at the band's right end. The column remembers it.398+// rightmost goes to the band below; from the last column, the leftmost
399+// goes to the band above, at its right end. The column remembers it.
399 func (w *Workspace) pushNeighbor() {400 func (w *Workspace) pushNeighbor() {
400 first, last := w.bandBounds(w.band(w.Focus))401 first, last := w.bandBounds(w.band(w.Focus))
401 if first == last {402 if first == last {
@@ -404,20 +405,21 @@ func (w *Workspace) pushNeighbor() {
404 id, _ := w.Focused()405 id, _ := w.Focused()
405 c := &w.Columns[w.Focus]406 c := &w.Columns[w.Focus]
406 if w.Focus < last {407 if w.Focus < last {
407- c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[w.Focus+1:w.Focus+2])...)408+ c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[last:last+1])...)
408- w.shiftBand(w.Focus+1, w.Focus+1, 1, 0)409+ w.shiftBand(last, last, 1, 0)
409 } else {410 } else {
410- c.pushedUp = append(columnIDs(w.Columns[w.Focus-1:w.Focus]), c.pushedUp...)411+ c.pushedUp = append(columnIDs(w.Columns[first:first+1]), c.pushedUp...)
411- w.shiftBand(w.Focus-1, w.Focus-1, -1, max(w.MaxColumns, 1))412+ w.shiftBand(first, first, -1, max(w.MaxColumns, 1))
412 }413 }
413 w.Focus = w.columnOf(id)414 w.Focus = w.columnOf(id)
414 }415 }
415 416
416 // restoreBand brings back the columns isolateBand or pushNeighbor sent417 // restoreBand brings back the columns isolateBand or pushNeighbor sent
417 // away, those still in the adjacent band, as many as the band has room418 // away, those still in the adjacent band, as many as the band has room
418-// for, nearest to the focused column first: the ones sent up before it,419+// for, nearest to the focused column first: the ones sent up at the start
419-// the ones sent down after it. The others stay where they are. It reports420+// of the band, the ones sent down at its end, in their former order. The
420-// whether the column remembered any.421+// others stay where they are. It reports whether the column remembered
422+// any.
421 func (w *Workspace) restoreBand() bool {423 func (w *Workspace) restoreBand() bool {
422 c := &w.Columns[w.Focus]424 c := &w.Columns[w.Focus]
423 up, down := c.pushedUp, c.pushedDown425 up, down := c.pushedUp, c.pushedDown
@@ -468,8 +470,7 @@ func (w *Workspace) restoreBand() bool {
468 }470 }
469 above := take(b-1, up, true)471 above := take(b-1, up, true)
470 below := take(b+1, down, false)472 below := take(b+1, down, false)
471- p := slices.IndexFunc(bands[b], func(c Column) bool { return slices.Contains(c.Windows, id) })473+ bands[b] = slices.Concat(above, bands[b], below)
472- bands[b] = slices.Concat(bands[b][:p], above, bands[b][p:p+1], below, bands[b][p+1:])
473 w.setBands(bands, id)474 w.setBands(bands, id)
474 return true475 return true
475 }476 }
M internal/core/cascade_bands_test.go +11 -9
@@ -5,23 +5,25 @@ import (
55 "testing"
66 )
77
8-// cycle-column-width in a full band pushes the right neighbor to the band
9-// below, or the left one up from the last column; the end of the cycle
10-// brings it back.
8+// cycle-column-width in a full band pushes the column at the far end of
9+// the band out: the rightmost down, or the leftmost up from the last
10+// column; the end of the cycle brings it back.
1111 func TestCascadeCyclePushesNeighbor(t *testing.T) {
1212 for _, tc := range []struct {
1313 name string
14- n int
14+ n, k int
1515 focus WindowID
1616 pushed [][]WindowID
1717 }{
18- {"middle pushes right down", 3, 2, [][]WindowID{{1, 2}, {3}}},
19- {"left pushes right down", 3, 1, [][]WindowID{{1, 3}, {2}}},
20- {"last pushes left up", 3, 3, [][]WindowID{{2}, {1, 3}}},
21- {"joins the band below", 5, 2, [][]WindowID{{1, 2}, {3, 4, 5}}},
18+ {"middle pushes rightmost down", 3, 3, 2, [][]WindowID{{1, 2}, {3}}},
19+ {"first pushes rightmost down", 3, 3, 1, [][]WindowID{{1, 2}, {3}}},
20+ {"last pushes leftmost up", 3, 3, 3, [][]WindowID{{1}, {2, 3}}},
21+ {"last of four pushes leftmost up", 4, 4, 4, [][]WindowID{{1}, {2, 3, 4}}},
22+ {"second of four pushes rightmost down", 4, 4, 2, [][]WindowID{{1, 2, 3}, {4}}},
23+ {"joins the band below", 5, 3, 2, [][]WindowID{{1, 2}, {3, 4, 5}}},
2224 } {
2325 t.Run(tc.name, func(t *testing.T) {
24- _, w := cascadeMovesCore(t, tc.n, 3, false)
26+ _, w := cascadeMovesCore(t, tc.n, tc.k, false)
2527 w.FocusID(tc.focus)
2628 before := bandIDs(w)
2729 w.Apply(ActionCycleColumnWidth)
@@ -5,23 +5,25 @@ import (
5 "testing"5 "testing"
6 )6 )
7 7
8-// cycle-column-width in a full band pushes the right neighbor to the band8+// cycle-column-width in a full band pushes the column at the far end of
9-// below, or the left one up from the last column; the end of the cycle9+// the band out: the rightmost down, or the leftmost up from the last
10-// brings it back.10+// column; the end of the cycle brings it back.
11 func TestCascadeCyclePushesNeighbor(t *testing.T) {11 func TestCascadeCyclePushesNeighbor(t *testing.T) {
12 for _, tc := range []struct {12 for _, tc := range []struct {
13 name string13 name string
14- n int14+ n, k int
15 focus WindowID15 focus WindowID
16 pushed [][]WindowID16 pushed [][]WindowID
17 }{17 }{
18- {"middle pushes right down", 3, 2, [][]WindowID{{1, 2}, {3}}},18+ {"middle pushes rightmost down", 3, 3, 2, [][]WindowID{{1, 2}, {3}}},
19- {"left pushes right down", 3, 1, [][]WindowID{{1, 3}, {2}}},19+ {"first pushes rightmost down", 3, 3, 1, [][]WindowID{{1, 2}, {3}}},
20- {"last pushes left up", 3, 3, [][]WindowID{{2}, {1, 3}}},20+ {"last pushes leftmost up", 3, 3, 3, [][]WindowID{{1}, {2, 3}}},
21- {"joins the band below", 5, 2, [][]WindowID{{1, 2}, {3, 4, 5}}},21+ {"last of four pushes leftmost up", 4, 4, 4, [][]WindowID{{1}, {2, 3, 4}}},
22+ {"second of four pushes rightmost down", 4, 4, 2, [][]WindowID{{1, 2, 3}, {4}}},
23+ {"joins the band below", 5, 3, 2, [][]WindowID{{1, 2}, {3, 4, 5}}},
22 } {24 } {
23 t.Run(tc.name, func(t *testing.T) {25 t.Run(tc.name, func(t *testing.T) {
24- _, w := cascadeMovesCore(t, tc.n, 3, false)26+ _, w := cascadeMovesCore(t, tc.n, tc.k, false)
25 w.FocusID(tc.focus)27 w.FocusID(tc.focus)
26 before := bandIDs(w)28 before := bandIDs(w)
27 w.Apply(ActionCycleColumnWidth)29 w.Apply(ActionCycleColumnWidth)

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