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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 247 | 247 | - Up/down moves through the windows of a column, then to the band above or below. | |
| 248 | 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 | 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 | 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 | 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 | ||
| @@ -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() { | |||
| 378 | 378 | left := slices.Clone(bands[b][:p]) | |
| 379 | 379 | right := slices.Clone(bands[b][p+1:]) | |
| 380 | 380 | 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...) | |
| 386 | 386 | bands[b] = []Column{c} | |
| 387 | 387 | if len(right) > 0 { | |
| 388 | 388 | bands = w.sendBand(bands, b, 1, 0, right) | |
| @@ -394,8 +394,9 @@ func (w *Workspace) isolateBand() { | |||
| 394 | 394 | } | |
| 395 | 395 | ||
| 396 | 396 | // 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. | |
| 399 | 400 | func (w *Workspace) pushNeighbor() { | |
| 400 | 401 | first, last := w.bandBounds(w.band(w.Focus)) | |
| 401 | 402 | if first == last { | |
| @@ -404,20 +405,21 @@ func (w *Workspace) pushNeighbor() { | |||
| 404 | 405 | id, _ := w.Focused() | |
| 405 | 406 | c := &w.Columns[w.Focus] | |
| 406 | 407 | 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) | |
| 409 | 410 | } 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)) | |
| 412 | 413 | } | |
| 413 | 414 | w.Focus = w.columnOf(id) | |
| 414 | 415 | } | |
| 415 | 416 | ||
| 416 | 417 | // restoreBand brings back the columns isolateBand or pushNeighbor sent | |
| 417 | 418 | // 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. | |
| 421 | 423 | func (w *Workspace) restoreBand() bool { | |
| 422 | 424 | c := &w.Columns[w.Focus] | |
| 423 | 425 | up, down := c.pushedUp, c.pushedDown | |
| @@ -468,8 +470,7 @@ func (w *Workspace) restoreBand() bool { | |||
| 468 | 470 | } | |
| 469 | 471 | above := take(b-1, up, true) | |
| 470 | 472 | 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) | |
| 473 | 474 | w.setBands(bands, id) | |
| 474 | 475 | return true | |
| 475 | 476 | } | |
| @@ -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 nearer | 381 | + | // 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 a | 396 | // 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 | 397 | + | // 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 sent | 417 | // restoreBand brings back the columns isolateBand or pushNeighbor sent | ||
| 417 | // away, those still in the adjacent band, as many as the band has room | 418 | // 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 reports | 420 | + | // 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.pushedDown | 425 | 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 true | 475 | return true | ||
| 475 | } | 476 | } | ||
M internal/core/cascade_bands_test.go +11 -9
| @@ -5,23 +5,25 @@ import ( | |||
| 5 | 5 | "testing" | |
| 6 | 6 | ) | |
| 7 | 7 | ||
| 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. | |
| 11 | 11 | func TestCascadeCyclePushesNeighbor(t *testing.T) { | |
| 12 | 12 | for _, tc := range []struct { | |
| 13 | 13 | name string | |
| 14 | - | n int | |
| 14 | + | n, k int | |
| 15 | 15 | focus WindowID | |
| 16 | 16 | pushed [][]WindowID | |
| 17 | 17 | }{ | |
| 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}}}, | |
| 22 | 24 | } { | |
| 23 | 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 | 27 | w.FocusID(tc.focus) | |
| 26 | 28 | before := bandIDs(w) | |
| 27 | 29 | 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 band | 8 | + | // 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 cycle | 9 | + | // 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 string | 13 | name string | ||
| 14 | - | n int | 14 | + | n, k int |
| 15 | focus WindowID | 15 | focus WindowID | ||
| 16 | pushed [][]WindowID | 16 | 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) | ||