detached 57c5e51

Merge c55955b353be20c11e800406ae8f1b964b88a0cf into f2d166b7fd4fa830ec06f487fcdcc64f1eacb4c9

commit
57c5e51c199019308ffad26c789098b588210cf6
author
ermite · · 2026-10-22 20:26 UTC
merge of
f2d166b + c55955b · diff against the first parent
signature
unsigned
8 files · +416 -20
M docs/config.md +2 -0
@@ -307,6 +307,8 @@ The default layout. Columns sit side by side in bands of up to `max-columns`, st
307307 - A new window joins the focused band when it has room, else it gets a new band right below.
308308 - `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.
309309 - `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.
310+- A fullscreen window gets a band of its own right below the band it was in (expelled into its own column first when it shares one), so the other columns stay put and focus changes among them leave the fullscreen output alone. Leaving fullscreen keeps the column in that band.
311+- `move-column-left/right` on a fullscreen window (tiled or floating) sends it to the neighbor monitor, where it stays fullscreen and focused; with no neighbor monitor nothing moves.
310312 - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it.
311313
312314 ### Column widths
@@ -307,6 +307,8 @@ The default layout. Columns sit side by side in bands of up to `max-columns`, st
307 - A new window joins the focused band when it has room, else it gets a new band right below.307 - A new window joins the focused band when it has room, else it gets a new band right below.
308 - `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.308 - `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.
309 - `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.309 - `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.
310+- A fullscreen window gets a band of its own right below the band it was in (expelled into its own column first when it shares one), so the other columns stay put and focus changes among them leave the fullscreen output alone. Leaving fullscreen keeps the column in that band.
311+- `move-column-left/right` on a fullscreen window (tiled or floating) sends it to the neighbor monitor, where it stays fullscreen and focused; with no neighbor monitor nothing moves.
310 - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it.312 - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it.
311 313
312 ### Column widths314 ### Column widths
M internal/core/action.go +47 -1
@@ -388,7 +388,39 @@ func (c *Core) dispatchAction(a Action) Effect {
388388 if a == ActionMoveColumnLeft {
389389 dir, d = -1, dirLeft
390390 }
391- if i := c.neighbor(d); w.leavesBand(w.Focus+dir) && i >= 0 {
391+ // A fullscreen window (tiled or floating) leaves for the neighbor
392+ // screen at once and stays fullscreen there; with no screen,
393+ // nothing moves.
394+ i := c.neighbor(d)
395+ if fid, ok := w.Focused(); ok && w.fullscreen == fid && (w.floatIndex(fid) >= 0 || !w.onFloat()) {
396+ if i < 0 {
397+ return Effect{}
398+ }
399+ var col Column
400+ if w.floatIndex(fid) < 0 {
401+ if col, ok = w.takeColumn(); !ok {
402+ return Effect{}
403+ }
404+ }
405+ w.reconcileFloats() // floats the fullscreen kept demoted show again
406+ from := c.cur()
407+ c.focusScreen = i
408+ to := c.cur().mon.Current()
409+ if len(col.Windows) == 0 {
410+ moveFloat(w, to, fid)
411+ } else {
412+ at := 0
413+ if d == dirLeft {
414+ at = len(to.Columns)
415+ }
416+ to.receive(col, at)
417+ to.SetFullscreen(fid, true)
418+ }
419+ from.mon.normalize()
420+ c.cur().mon.normalize()
421+ return Effect{}
422+ }
423+ if i >= 0 && w.leavesBand(w.Focus+dir) {
392424 col, ok := w.takeColumn()
393425 if !ok {
394426 return Effect{}
@@ -608,3 +640,17 @@ func (w *Workspace) Apply(a Action) Effect {
608640 }
609641 return Effect{}
610642 }
643+
644+// moveFloat moves float id from workspace cur to to, as MoveToWorkspace
645+// does, focuses it there and keeps it fullscreen.
646+func moveFloat(cur, to *Workspace, id WindowID) {
647+ fl := cur.Floats[cur.floatIndex(id)]
648+ if fl.free && fl.back != nil {
649+ fl = fl.rehome()
650+ }
651+ cur.RemoveWindow(id)
652+ to.AddFloating(id, fl.W, fl.H)
653+ to.Floats[len(to.Floats)-1] = fl
654+ to.Activate(id)
655+ to.SetFullscreen(id, true)
656+}
@@ -388,7 +388,39 @@ func (c *Core) dispatchAction(a Action) Effect {
388 if a == ActionMoveColumnLeft {388 if a == ActionMoveColumnLeft {
389 dir, d = -1, dirLeft389 dir, d = -1, dirLeft
390 }390 }
391- if i := c.neighbor(d); w.leavesBand(w.Focus+dir) && i >= 0 {391+ // A fullscreen window (tiled or floating) leaves for the neighbor
392+ // screen at once and stays fullscreen there; with no screen,
393+ // nothing moves.
394+ i := c.neighbor(d)
395+ if fid, ok := w.Focused(); ok && w.fullscreen == fid && (w.floatIndex(fid) >= 0 || !w.onFloat()) {
396+ if i < 0 {
397+ return Effect{}
398+ }
399+ var col Column
400+ if w.floatIndex(fid) < 0 {
401+ if col, ok = w.takeColumn(); !ok {
402+ return Effect{}
403+ }
404+ }
405+ w.reconcileFloats() // floats the fullscreen kept demoted show again
406+ from := c.cur()
407+ c.focusScreen = i
408+ to := c.cur().mon.Current()
409+ if len(col.Windows) == 0 {
410+ moveFloat(w, to, fid)
411+ } else {
412+ at := 0
413+ if d == dirLeft {
414+ at = len(to.Columns)
415+ }
416+ to.receive(col, at)
417+ to.SetFullscreen(fid, true)
418+ }
419+ from.mon.normalize()
420+ c.cur().mon.normalize()
421+ return Effect{}
422+ }
423+ if i >= 0 && w.leavesBand(w.Focus+dir) {
392 col, ok := w.takeColumn()424 col, ok := w.takeColumn()
393 if !ok {425 if !ok {
394 return Effect{}426 return Effect{}
@@ -608,3 +640,17 @@ func (w *Workspace) Apply(a Action) Effect {
608 }640 }
609 return Effect{}641 return Effect{}
610 }642 }
643+
644+// moveFloat moves float id from workspace cur to to, as MoveToWorkspace
645+// does, focuses it there and keeps it fullscreen.
646+func moveFloat(cur, to *Workspace, id WindowID) {
647+ fl := cur.Floats[cur.floatIndex(id)]
648+ if fl.free && fl.back != nil {
649+ fl = fl.rehome()
650+ }
651+ cur.RemoveWindow(id)
652+ to.AddFloating(id, fl.W, fl.H)
653+ to.Floats[len(to.Floats)-1] = fl
654+ to.Activate(id)
655+ to.SetFullscreen(id, true)
656+}
M internal/core/cascade.go +73 -14
@@ -419,6 +419,52 @@ func (w *Workspace) isolateBand() {
419419 w.setBands(bands, id)
420420 }
421421
422+// fullscreenBand gives tiled window id, about to go fullscreen, a band of
423+// its own right below the band it is in, so the other columns keep their
424+// band and a focus change among them never touches the fullscreen output.
425+// A window stacked in a column is expelled into a column of its own first.
426+// Leaving fullscreen does not move it back: the column keeps its band, so
427+// nothing jumps when the user leaves. Only cascade layouts do this; the
428+// focus stays on the same window.
429+func (w *Workspace) fullscreenBand(id WindowID) {
430+ i := w.columnOf(id)
431+ if !w.policy().wraps || i < 0 {
432+ return
433+ }
434+ b := w.band(i)
435+ first, last := w.bandBounds(b)
436+ if len(w.Columns[i].Windows) == 1 && first == last {
437+ return
438+ }
439+ fc := w.Columns[w.Focus]
440+ focus := fc.Windows[fc.Focus]
441+ bands := w.bandList()
442+ col := bands[b][i-first]
443+ nc := col
444+ if len(col.Windows) == 1 {
445+ // The column leaves its lane, as deleteColumn: manual widths of the
446+ // lane no longer add up. It keeps its slot and its own width.
447+ w.clearSpans(i)
448+ bands[b] = slices.Delete(bands[b], i-first, i-first+1)
449+ } else {
450+ j := slices.Index(col.Windows, id)
451+ // The expelled window gets a plain column: no Span (a manual width
452+ // is the stack's, and the new band is alone), no shares.
453+ nc = Column{Windows: []WindowID{id}, Cells: col.Cells}
454+ col.Windows = slices.Delete(slices.Clone(col.Windows), j, j+1)
455+ col.Shares = nil
456+ if j == 0 {
457+ col.Slot = 0 // the slot window left
458+ }
459+ if col.Focus > j || col.Focus >= len(col.Windows) {
460+ col.Focus = max(min(col.Focus-1, len(col.Windows)-1), 0)
461+ }
462+ bands[b][i-first] = col
463+ }
464+ bands = slices.Insert(bands, b+1, []Column{nc})
465+ w.setBands(bands, focus)
466+}
467+
422468 // pushNeighbor makes room in the focused column's full cascade band for a
423469 // wider column: the column at the far end of the band leaves it. The
424470 // rightmost goes to the band below; from the last column, the leftmost
@@ -457,13 +503,10 @@ func (w *Workspace) restoreBand() bool {
457503 bands := w.bandList()
458504 b := w.band(w.Focus)
459505 room := max(max(w.MaxColumns, 1)-len(bands[b]), 0)
460- // take removes from band nb the remembered columns found there, at
461- // most room of them, in the order of ids walked from the focused
506+ // take removes from the bands nbs the remembered columns found there,
507+ // at most room of them, in the order of ids walked from the focused
462508 // column outward (rev: ids end nearest).
463- take := func(nb int, ids [][]WindowID, rev bool) []Column {
464- if nb < 0 || nb >= len(bands) {
465- return nil
466- }
509+ take := func(nbs []int, ids [][]WindowID, rev bool) []Column {
467510 var got []Column
468511 for k := range ids {
469512 if room == 0 {
@@ -481,21 +524,37 @@ func (w *Workspace) restoreBand() bool {
481524 if head < 0 {
482525 continue
483526 }
484- i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] })
485- if i < 0 {
486- continue
527+ for _, nb := range nbs {
528+ i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] })
529+ if i < 0 {
530+ continue
531+ }
532+ got = append(got, bands[nb][i])
533+ bands[nb] = slices.Delete(bands[nb], i, i+1)
534+ room--
535+ break
487536 }
488- got = append(got, bands[nb][i])
489- bands[nb] = slices.Delete(bands[nb], i, i+1)
490- room--
491537 }
492538 if rev {
493539 slices.Reverse(got)
494540 }
495541 return got
496542 }
497- above := take(b-1, up, true)
498- below := take(b+1, down, false)
543+ // 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.
546+ reach := func(dir int) []int {
547+ var nbs []int
548+ for n := b + dir; n >= 0 && n < len(bands); n += dir {
549+ nbs = append(nbs, n)
550+ if len(bands[n]) != 1 || len(nbs) == 2 {
551+ break
552+ }
553+ }
554+ return nbs
555+ }
556+ above := take(reach(-1), up, true)
557+ below := take(reach(1), down, false)
499558 bands[b] = slices.Concat(above, bands[b], below)
500559 w.setBands(bands, id)
501560 return true
@@ -419,6 +419,52 @@ func (w *Workspace) isolateBand() {
419 w.setBands(bands, id)419 w.setBands(bands, id)
420 }420 }
421 421
422+// fullscreenBand gives tiled window id, about to go fullscreen, a band of
423+// its own right below the band it is in, so the other columns keep their
424+// band and a focus change among them never touches the fullscreen output.
425+// A window stacked in a column is expelled into a column of its own first.
426+// Leaving fullscreen does not move it back: the column keeps its band, so
427+// nothing jumps when the user leaves. Only cascade layouts do this; the
428+// focus stays on the same window.
429+func (w *Workspace) fullscreenBand(id WindowID) {
430+ i := w.columnOf(id)
431+ if !w.policy().wraps || i < 0 {
432+ return
433+ }
434+ b := w.band(i)
435+ first, last := w.bandBounds(b)
436+ if len(w.Columns[i].Windows) == 1 && first == last {
437+ return
438+ }
439+ fc := w.Columns[w.Focus]
440+ focus := fc.Windows[fc.Focus]
441+ bands := w.bandList()
442+ col := bands[b][i-first]
443+ nc := col
444+ if len(col.Windows) == 1 {
445+ // The column leaves its lane, as deleteColumn: manual widths of the
446+ // lane no longer add up. It keeps its slot and its own width.
447+ w.clearSpans(i)
448+ bands[b] = slices.Delete(bands[b], i-first, i-first+1)
449+ } else {
450+ j := slices.Index(col.Windows, id)
451+ // The expelled window gets a plain column: no Span (a manual width
452+ // is the stack's, and the new band is alone), no shares.
453+ nc = Column{Windows: []WindowID{id}, Cells: col.Cells}
454+ col.Windows = slices.Delete(slices.Clone(col.Windows), j, j+1)
455+ col.Shares = nil
456+ if j == 0 {
457+ col.Slot = 0 // the slot window left
458+ }
459+ if col.Focus > j || col.Focus >= len(col.Windows) {
460+ col.Focus = max(min(col.Focus-1, len(col.Windows)-1), 0)
461+ }
462+ bands[b][i-first] = col
463+ }
464+ bands = slices.Insert(bands, b+1, []Column{nc})
465+ w.setBands(bands, focus)
466+}
467+
422 // pushNeighbor makes room in the focused column's full cascade band for a468 // pushNeighbor makes room in the focused column's full cascade band for a
423 // wider column: the column at the far end of the band leaves it. The469 // wider column: the column at the far end of the band leaves it. The
424 // rightmost goes to the band below; from the last column, the leftmost470 // rightmost goes to the band below; from the last column, the leftmost
@@ -457,13 +503,10 @@ func (w *Workspace) restoreBand() bool {
457 bands := w.bandList()503 bands := w.bandList()
458 b := w.band(w.Focus)504 b := w.band(w.Focus)
459 room := max(max(w.MaxColumns, 1)-len(bands[b]), 0)505 room := max(max(w.MaxColumns, 1)-len(bands[b]), 0)
460- // take removes from band nb the remembered columns found there, at506+ // take removes from the bands nbs the remembered columns found there,
461- // most room of them, in the order of ids walked from the focused507+ // at most room of them, in the order of ids walked from the focused
462 // column outward (rev: ids end nearest).508 // column outward (rev: ids end nearest).
463- take := func(nb int, ids [][]WindowID, rev bool) []Column {509+ take := func(nbs []int, ids [][]WindowID, rev bool) []Column {
464- if nb < 0 || nb >= len(bands) {
465- return nil
466- }
467 var got []Column510 var got []Column
468 for k := range ids {511 for k := range ids {
469 if room == 0 {512 if room == 0 {
@@ -481,21 +524,37 @@ func (w *Workspace) restoreBand() bool {
481 if head < 0 {524 if head < 0 {
482 continue525 continue
483 }526 }
484- i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] })527+ for _, nb := range nbs {
485- if i < 0 {528+ i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] })
486- continue529+ if i < 0 {
530+ continue
531+ }
532+ got = append(got, bands[nb][i])
533+ bands[nb] = slices.Delete(bands[nb], i, i+1)
534+ room--
535+ break
487 }536 }
488- got = append(got, bands[nb][i])
489- bands[nb] = slices.Delete(bands[nb], i, i+1)
490- room--
491 }537 }
492 if rev {538 if rev {
493 slices.Reverse(got)539 slices.Reverse(got)
494 }540 }
495 return got541 return got
496 }542 }
497- above := take(b-1, up, true)543+ // reach lists the bands dir away to look in: the adjacent one, and the
498- below := take(b+1, down, false)544+ // next when the adjacent holds a single column, as a fullscreen column
545+ // split off a band of its own meanwhile leaves.
546+ reach := func(dir int) []int {
547+ var nbs []int
548+ for n := b + dir; n >= 0 && n < len(bands); n += dir {
549+ nbs = append(nbs, n)
550+ if len(bands[n]) != 1 || len(nbs) == 2 {
551+ break
552+ }
553+ }
554+ return nbs
555+ }
556+ above := take(reach(-1), up, true)
557+ below := take(reach(1), down, false)
499 bands[b] = slices.Concat(above, bands[b], below)558 bands[b] = slices.Concat(above, bands[b], below)
500 w.setBands(bands, id)559 w.setBands(bands, id)
501 return true560 return true
A internal/core/cascade_fullscreen_band_test.go +283 -0
@@ -0,0 +1,283 @@
1+package core
2+
3+import "testing"
4+
5+// Entering fullscreen gives a cascade column a band below its own; the
6+// others stay. Leaving keeps it there.
7+func TestCascadeFullscreenOwnBand(t *testing.T) {
8+ for _, tc := range []struct {
9+ name string
10+ do func(w *Workspace)
11+ }{
12+ {"toggle", func(w *Workspace) { w.ToggleFullscreen() }},
13+ {"client request", func(w *Workspace) { w.SetFullscreen(3, true) }},
14+ } {
15+ t.Run(tc.name, func(t *testing.T) {
16+ c, w := cascadeMovesCore(t, 3, 3, false)
17+ wantBands(t, w, []WindowID{1, 2, 3})
18+ tc.do(w)
19+ wantBands(t, w, []WindowID{1, 2}, []WindowID{3})
20+ wantFocus(t, c, "M", 3)
21+ if w.fullscreen != 3 {
22+ t.Fatalf("fullscreen %d, want 3", w.fullscreen)
23+ }
24+ w.ToggleFullscreen()
25+ wantBands(t, w, []WindowID{1, 2}, []WindowID{3})
26+ })
27+ }
28+}
29+
30+func TestCascadeFullscreenMiddleAndAlone(t *testing.T) {
31+ c, w := cascadeMovesCore(t, 3, 3, false)
32+ w.FocusID(2)
33+ w.ToggleFullscreen()
34+ wantBands(t, w, []WindowID{1, 3}, []WindowID{2})
35+ wantFocus(t, c, "M", 2)
36+
37+ _, w = cascadeMovesCore(t, 1, 3, false)
38+ w.ToggleFullscreen()
39+ wantBands(t, w, []WindowID{1})
40+}
41+
42+func TestCascadeFullscreenExpelsFromStack(t *testing.T) {
43+ c, w := cascadeMovesCore(t, 2, 3, false)
44+ w.FocusID(2)
45+ c.applyAction(ActionConsumeOrExpelLeft) // 2 stacks under 1
46+ wantColumns(t, w, 1)
47+ w.ToggleFullscreen()
48+ wantBands(t, w, []WindowID{1}, []WindowID{2})
49+ if len(w.Columns[0].Windows) != 1 || w.fullscreen != 2 {
50+ t.Fatalf("columns %v fullscreen %d", w.Columns, w.fullscreen)
51+ }
52+ wantFocus(t, c, "M", 2)
53+}
54+
55+func TestScrollFullscreenKeepsColumns(t *testing.T) {
56+ _, w := cascadeMovesCore(t, 3, 3, false)
57+ w.Overflow = OverflowScroll
58+ w.forgetBands()
59+ w.ToggleFullscreen()
60+ if len(w.Columns) != 3 {
61+ t.Fatalf("columns %d", len(w.Columns))
62+ }
63+}
64+
65+func TestFullscreenColumnMovesToNeighborMonitor(t *testing.T) {
66+ for _, tc := range []struct {
67+ action Action
68+ screen string
69+ }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} {
70+ t.Run(tc.screen, func(t *testing.T) {
71+ c, w := cascadeMovesCore(t, 3, 3, true)
72+ w.ToggleFullscreen()
73+ c.applyAction(tc.action)
74+ wantColumns(t, w, 1, 2)
75+ dst := screenWorkspace(c, tc.screen)
76+ wantColumns(t, dst, 3)
77+ wantFocus(t, c, tc.screen, 3)
78+ if dst.fullscreen != 3 || w.fullscreen != 0 {
79+ t.Fatalf("fullscreen dst %d src %d", dst.fullscreen, w.fullscreen)
80+ }
81+ })
82+ }
83+}
84+
85+func TestFullscreenColumnNoNeighborStays(t *testing.T) {
86+ c, w := cascadeMovesCore(t, 3, 3, false)
87+ w.ToggleFullscreen()
88+ c.applyAction(ActionMoveColumnLeft)
89+ c.applyAction(ActionMoveColumnRight)
90+ wantBands(t, w, []WindowID{1, 2}, []WindowID{3})
91+ if w.fullscreen != 3 {
92+ t.Fatalf("fullscreen %d", w.fullscreen)
93+ }
94+}
95+
96+func TestFullscreenFloatMovesToNeighborMonitor(t *testing.T) {
97+ for _, tc := range []struct {
98+ action Action
99+ screen string
100+ }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} {
101+ t.Run(tc.screen, func(t *testing.T) {
102+ c, w := cascadeMovesCore(t, 2, 3, true)
103+ w.AddFloating(9, 5120, 2160)
104+ w.SetFullscreen(9, true)
105+ c.applyAction(tc.action)
106+ dst := screenWorkspace(c, tc.screen)
107+ if w.floatIndex(9) >= 0 || dst.floatIndex(9) < 0 || w.fullscreen != 0 || dst.fullscreen != 9 {
108+ t.Fatalf("float on src %v dst %v, fullscreen src %d dst %d", w.Floats, dst.Floats, w.fullscreen, dst.fullscreen)
109+ }
110+ wantFocus(t, c, tc.screen, 9)
111+ wantColumns(t, w, 1, 2)
112+ })
113+ }
114+}
115+
116+func TestFullscreenFloatNoNeighborStays(t *testing.T) {
117+ c, w := cascadeMovesCore(t, 2, 3, false)
118+ w.AddFloating(9, 800, 600)
119+ w.SetFullscreen(9, true)
120+ c.applyAction(ActionMoveColumnLeft)
121+ if w.floatIndex(9) < 0 || w.fullscreen != 9 {
122+ t.Fatalf("float moved: %v fullscreen %d", w.Floats, w.fullscreen)
123+ }
124+}
125+
126+func TestCascadeFullscreenRepeatedRequestKeepsBands(t *testing.T) {
127+ _, w := cascadeMovesCore(t, 3, 3, false)
128+ w.SetFullscreen(3, true)
129+ w.SetFullscreen(3, true)
130+ w.SetFullscreen(3, true)
131+ wantBands(t, w, []WindowID{1, 2}, []WindowID{3})
132+}
133+
134+func TestCascadeFullscreenStackedFixups(t *testing.T) {
135+ for _, tc := range []struct {
136+ name string
137+ focus WindowID
138+ rest WindowID
139+ slotKept bool
140+ }{{"last row", 3, 2, true}, {"first row", 2, 3, false}} {
141+ t.Run(tc.name, func(t *testing.T) {
142+ c, w := cascadeMovesCore(t, 3, 3, false)
143+ w.FocusID(3)
144+ c.applyAction(ActionConsumeOrExpelLeft) // 3 stacks under 2
145+ w.FocusID(tc.focus)
146+ w.Columns[1].Shares = []int{5000, 5000}
147+ w.Columns[1].Span = 3000
148+ w.Columns[1].Slot = 4
149+ w.SetFullscreen(tc.focus, true)
150+ wantBands(t, w, []WindowID{1, tc.rest}, []WindowID{tc.focus})
151+ col := w.Columns[1]
152+ if len(col.Windows) != 1 || col.Windows[0] != tc.rest || col.Shares != nil || col.Focus != 0 || (col.Slot == 4) != tc.slotKept {
153+ t.Fatalf("remaining column %+v", col)
154+ }
155+ if got, _ := w.Focused(); got != tc.focus {
156+ t.Fatalf("focus %d, want %d", got, tc.focus)
157+ }
158+ if w.Columns[2].Span != 0 || w.Columns[2].Slot != 0 {
159+ t.Fatalf("expelled column %+v", w.Columns[2])
160+ }
161+ })
162+ }
163+}
164+
165+func TestCascadeFullscreenKeepsSlotOfIntactColumn(t *testing.T) {
166+ _, w := cascadeMovesCore(t, 3, 3, false)
167+ w.Columns[2].Slot = 4
168+ w.ToggleFullscreen()
169+ if i := w.columnOf(3); w.Columns[i].Slot != 4 {
170+ t.Fatalf("slot %d, want 4", w.Columns[i].Slot)
171+ }
172+}
173+
174+func TestCascadeFullscreenAfterMaximizeRestores(t *testing.T) {
175+ _, w := cascadeMovesCore(t, 3, 3, false)
176+ w.FocusID(1)
177+ w.ToggleFullWidth()
178+ w.FocusID(3)
179+ w.ToggleFullscreen()
180+ w.ToggleFullscreen()
181+ w.FocusID(1)
182+ w.ToggleFullWidth()
183+ wantBands(t, w, []WindowID{1, 2, 3})
184+}
185+
186+func TestFullscreenMoveEdges(t *testing.T) {
187+ t.Run("destination fullscreen elsewhere", func(t *testing.T) {
188+ c, w := cascadeMovesCore(t, 2, 3, true)
189+ dst := screenWorkspace(c, "R")
190+ dst.AddWindow(20)
191+ dst.SetFullscreen(20, true)
192+ w.ToggleFullscreen()
193+ c.applyAction(ActionMoveColumnRight)
194+ if dst.fullscreen != 2 {
195+ t.Fatalf("fullscreen %d, want 2", dst.fullscreen)
196+ }
197+ wantFocus(t, c, "R", 2)
198+ })
199+ t.Run("scroll layout", func(t *testing.T) {
200+ c, w := cascadeMovesCore(t, 2, 3, true)
201+ w.Overflow = OverflowScroll
202+ w.ToggleFullscreen()
203+ c.applyAction(ActionMoveColumnLeft)
204+ dst := screenWorkspace(c, "L")
205+ wantColumns(t, dst, 2)
206+ if dst.fullscreen != 2 || w.fullscreen != 0 {
207+ t.Fatalf("fullscreen dst %d src %d", dst.fullscreen, w.fullscreen)
208+ }
209+ })
210+ t.Run("source floats promoted", func(t *testing.T) {
211+ c, w := cascadeMovesCore(t, 2, 3, true)
212+ w.AddFloating(9, 100, 100)
213+ w.FocusID(2)
214+ w.ToggleFullscreen()
215+ // A fullscreen keeps a float demoted without it covering the output.
216+ w.Floats[0].below = true
217+ if len(w.Floats) != 1 || !w.Floats[0].below || w.fullscreen != 2 {
218+ t.Fatalf("setup: float not demoted: %+v", w.Floats)
219+ }
220+ c.applyAction(ActionMoveColumnRight)
221+ if len(w.Floats) != 1 || w.Floats[0].below {
222+ t.Fatalf("float still demoted: %+v", w.Floats)
223+ }
224+ })
225+}
226+
227+func TestCascadeFullscreenRestoresAcrossBand(t *testing.T) {
228+ _, w := cascadeMovesCore(t, 3, 3, false)
229+ w.FocusID(1)
230+ w.ToggleFullWidth() // X=1 alone; 2 and 3 pushed below
231+ wantBands(t, w, []WindowID{1}, []WindowID{2, 3})
232+ w.FocusID(2)
233+ w.ToggleFullscreen()
234+ wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2})
235+ w.ToggleFullscreen()
236+ w.FocusID(1)
237+ w.ToggleFullWidth()
238+ wantBands(t, w, []WindowID{1, 2, 3})
239+}
240+
241+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.
245+ c, w := cascadeMovesCore(t, 2, 3, true)
246+ dst := screenWorkspace(c, "R")
247+ dst.Overflow = OverflowFixed
248+ dst.left = 2
249+ w.ToggleFullscreen()
250+ c.applyAction(ActionMoveColumnRight)
251+ wantFocus(t, c, "R", 2)
252+ if dst.fullscreen != 2 {
253+ t.Fatalf("fullscreen %d, want 2", dst.fullscreen)
254+ }
255+ })
256+ t.Run("stacked column in a band", func(t *testing.T) {
257+ c, w := cascadeMovesCore(t, 3, 3, true)
258+ w.FocusID(3)
259+ c.applyAction(ActionConsumeOrExpelLeft) // 3 under 2
260+ w.ToggleFullscreen() // 3 expelled to its own band
261+ c.applyAction(ActionMoveColumnLeft)
262+ wantColumns(t, w, 1, 2)
263+ dst := screenWorkspace(c, "L")
264+ wantColumns(t, dst, 3)
265+ if dst.fullscreen != 3 || len(w.Columns[1].Windows) != 1 {
266+ t.Fatalf("fullscreen %d, source column %v", dst.fullscreen, w.Columns[1].Windows)
267+ }
268+ })
269+ t.Run("destination with columns gets its own band", func(t *testing.T) {
270+ c, w := cascadeMovesCore(t, 1, 3, true)
271+ dst := screenWorkspace(c, "R")
272+ dst.Overflow = OverflowCascade
273+ dst.SetMaxColumns(3)
274+ dst.AddWindow(20)
275+ dst.AddWindow(21)
276+ w.ToggleFullscreen()
277+ c.applyAction(ActionMoveColumnRight)
278+ wantBands(t, dst, []WindowID{20, 21}, []WindowID{1})
279+ if dst.fullscreen != 1 {
280+ t.Fatalf("fullscreen %d", dst.fullscreen)
281+ }
282+ })
283+}
@@ -0,0 +1,283 @@
1+package core
2+
3+import "testing"
4+
5+// Entering fullscreen gives a cascade column a band below its own; the
6+// others stay. Leaving keeps it there.
7+func TestCascadeFullscreenOwnBand(t *testing.T) {
8+ for _, tc := range []struct {
9+ name string
10+ do func(w *Workspace)
11+ }{
12+ {"toggle", func(w *Workspace) { w.ToggleFullscreen() }},
13+ {"client request", func(w *Workspace) { w.SetFullscreen(3, true) }},
14+ } {
15+ t.Run(tc.name, func(t *testing.T) {
16+ c, w := cascadeMovesCore(t, 3, 3, false)
17+ wantBands(t, w, []WindowID{1, 2, 3})
18+ tc.do(w)
19+ wantBands(t, w, []WindowID{1, 2}, []WindowID{3})
20+ wantFocus(t, c, "M", 3)
21+ if w.fullscreen != 3 {
22+ t.Fatalf("fullscreen %d, want 3", w.fullscreen)
23+ }
24+ w.ToggleFullscreen()
25+ wantBands(t, w, []WindowID{1, 2}, []WindowID{3})
26+ })
27+ }
28+}
29+
30+func TestCascadeFullscreenMiddleAndAlone(t *testing.T) {
31+ c, w := cascadeMovesCore(t, 3, 3, false)
32+ w.FocusID(2)
33+ w.ToggleFullscreen()
34+ wantBands(t, w, []WindowID{1, 3}, []WindowID{2})
35+ wantFocus(t, c, "M", 2)
36+
37+ _, w = cascadeMovesCore(t, 1, 3, false)
38+ w.ToggleFullscreen()
39+ wantBands(t, w, []WindowID{1})
40+}
41+
42+func TestCascadeFullscreenExpelsFromStack(t *testing.T) {
43+ c, w := cascadeMovesCore(t, 2, 3, false)
44+ w.FocusID(2)
45+ c.applyAction(ActionConsumeOrExpelLeft) // 2 stacks under 1
46+ wantColumns(t, w, 1)
47+ w.ToggleFullscreen()
48+ wantBands(t, w, []WindowID{1}, []WindowID{2})
49+ if len(w.Columns[0].Windows) != 1 || w.fullscreen != 2 {
50+ t.Fatalf("columns %v fullscreen %d", w.Columns, w.fullscreen)
51+ }
52+ wantFocus(t, c, "M", 2)
53+}
54+
55+func TestScrollFullscreenKeepsColumns(t *testing.T) {
56+ _, w := cascadeMovesCore(t, 3, 3, false)
57+ w.Overflow = OverflowScroll
58+ w.forgetBands()
59+ w.ToggleFullscreen()
60+ if len(w.Columns) != 3 {
61+ t.Fatalf("columns %d", len(w.Columns))
62+ }
63+}
64+
65+func TestFullscreenColumnMovesToNeighborMonitor(t *testing.T) {
66+ for _, tc := range []struct {
67+ action Action
68+ screen string
69+ }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} {
70+ t.Run(tc.screen, func(t *testing.T) {
71+ c, w := cascadeMovesCore(t, 3, 3, true)
72+ w.ToggleFullscreen()
73+ c.applyAction(tc.action)
74+ wantColumns(t, w, 1, 2)
75+ dst := screenWorkspace(c, tc.screen)
76+ wantColumns(t, dst, 3)
77+ wantFocus(t, c, tc.screen, 3)
78+ if dst.fullscreen != 3 || w.fullscreen != 0 {
79+ t.Fatalf("fullscreen dst %d src %d", dst.fullscreen, w.fullscreen)
80+ }
81+ })
82+ }
83+}
84+
85+func TestFullscreenColumnNoNeighborStays(t *testing.T) {
86+ c, w := cascadeMovesCore(t, 3, 3, false)
87+ w.ToggleFullscreen()
88+ c.applyAction(ActionMoveColumnLeft)
89+ c.applyAction(ActionMoveColumnRight)
90+ wantBands(t, w, []WindowID{1, 2}, []WindowID{3})
91+ if w.fullscreen != 3 {
92+ t.Fatalf("fullscreen %d", w.fullscreen)
93+ }
94+}
95+
96+func TestFullscreenFloatMovesToNeighborMonitor(t *testing.T) {
97+ for _, tc := range []struct {
98+ action Action
99+ screen string
100+ }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} {
101+ t.Run(tc.screen, func(t *testing.T) {
102+ c, w := cascadeMovesCore(t, 2, 3, true)
103+ w.AddFloating(9, 5120, 2160)
104+ w.SetFullscreen(9, true)
105+ c.applyAction(tc.action)
106+ dst := screenWorkspace(c, tc.screen)
107+ if w.floatIndex(9) >= 0 || dst.floatIndex(9) < 0 || w.fullscreen != 0 || dst.fullscreen != 9 {
108+ t.Fatalf("float on src %v dst %v, fullscreen src %d dst %d", w.Floats, dst.Floats, w.fullscreen, dst.fullscreen)
109+ }
110+ wantFocus(t, c, tc.screen, 9)
111+ wantColumns(t, w, 1, 2)
112+ })
113+ }
114+}
115+
116+func TestFullscreenFloatNoNeighborStays(t *testing.T) {
117+ c, w := cascadeMovesCore(t, 2, 3, false)
118+ w.AddFloating(9, 800, 600)
119+ w.SetFullscreen(9, true)
120+ c.applyAction(ActionMoveColumnLeft)
121+ if w.floatIndex(9) < 0 || w.fullscreen != 9 {
122+ t.Fatalf("float moved: %v fullscreen %d", w.Floats, w.fullscreen)
123+ }
124+}
125+
126+func TestCascadeFullscreenRepeatedRequestKeepsBands(t *testing.T) {
127+ _, w := cascadeMovesCore(t, 3, 3, false)
128+ w.SetFullscreen(3, true)
129+ w.SetFullscreen(3, true)
130+ w.SetFullscreen(3, true)
131+ wantBands(t, w, []WindowID{1, 2}, []WindowID{3})
132+}
133+
134+func TestCascadeFullscreenStackedFixups(t *testing.T) {
135+ for _, tc := range []struct {
136+ name string
137+ focus WindowID
138+ rest WindowID
139+ slotKept bool
140+ }{{"last row", 3, 2, true}, {"first row", 2, 3, false}} {
141+ t.Run(tc.name, func(t *testing.T) {
142+ c, w := cascadeMovesCore(t, 3, 3, false)
143+ w.FocusID(3)
144+ c.applyAction(ActionConsumeOrExpelLeft) // 3 stacks under 2
145+ w.FocusID(tc.focus)
146+ w.Columns[1].Shares = []int{5000, 5000}
147+ w.Columns[1].Span = 3000
148+ w.Columns[1].Slot = 4
149+ w.SetFullscreen(tc.focus, true)
150+ wantBands(t, w, []WindowID{1, tc.rest}, []WindowID{tc.focus})
151+ col := w.Columns[1]
152+ if len(col.Windows) != 1 || col.Windows[0] != tc.rest || col.Shares != nil || col.Focus != 0 || (col.Slot == 4) != tc.slotKept {
153+ t.Fatalf("remaining column %+v", col)
154+ }
155+ if got, _ := w.Focused(); got != tc.focus {
156+ t.Fatalf("focus %d, want %d", got, tc.focus)
157+ }
158+ if w.Columns[2].Span != 0 || w.Columns[2].Slot != 0 {
159+ t.Fatalf("expelled column %+v", w.Columns[2])
160+ }
161+ })
162+ }
163+}
164+
165+func TestCascadeFullscreenKeepsSlotOfIntactColumn(t *testing.T) {
166+ _, w := cascadeMovesCore(t, 3, 3, false)
167+ w.Columns[2].Slot = 4
168+ w.ToggleFullscreen()
169+ if i := w.columnOf(3); w.Columns[i].Slot != 4 {
170+ t.Fatalf("slot %d, want 4", w.Columns[i].Slot)
171+ }
172+}
173+
174+func TestCascadeFullscreenAfterMaximizeRestores(t *testing.T) {
175+ _, w := cascadeMovesCore(t, 3, 3, false)
176+ w.FocusID(1)
177+ w.ToggleFullWidth()
178+ w.FocusID(3)
179+ w.ToggleFullscreen()
180+ w.ToggleFullscreen()
181+ w.FocusID(1)
182+ w.ToggleFullWidth()
183+ wantBands(t, w, []WindowID{1, 2, 3})
184+}
185+
186+func TestFullscreenMoveEdges(t *testing.T) {
187+ t.Run("destination fullscreen elsewhere", func(t *testing.T) {
188+ c, w := cascadeMovesCore(t, 2, 3, true)
189+ dst := screenWorkspace(c, "R")
190+ dst.AddWindow(20)
191+ dst.SetFullscreen(20, true)
192+ w.ToggleFullscreen()
193+ c.applyAction(ActionMoveColumnRight)
194+ if dst.fullscreen != 2 {
195+ t.Fatalf("fullscreen %d, want 2", dst.fullscreen)
196+ }
197+ wantFocus(t, c, "R", 2)
198+ })
199+ t.Run("scroll layout", func(t *testing.T) {
200+ c, w := cascadeMovesCore(t, 2, 3, true)
201+ w.Overflow = OverflowScroll
202+ w.ToggleFullscreen()
203+ c.applyAction(ActionMoveColumnLeft)
204+ dst := screenWorkspace(c, "L")
205+ wantColumns(t, dst, 2)
206+ if dst.fullscreen != 2 || w.fullscreen != 0 {
207+ t.Fatalf("fullscreen dst %d src %d", dst.fullscreen, w.fullscreen)
208+ }
209+ })
210+ t.Run("source floats promoted", func(t *testing.T) {
211+ c, w := cascadeMovesCore(t, 2, 3, true)
212+ w.AddFloating(9, 100, 100)
213+ w.FocusID(2)
214+ w.ToggleFullscreen()
215+ // A fullscreen keeps a float demoted without it covering the output.
216+ w.Floats[0].below = true
217+ if len(w.Floats) != 1 || !w.Floats[0].below || w.fullscreen != 2 {
218+ t.Fatalf("setup: float not demoted: %+v", w.Floats)
219+ }
220+ c.applyAction(ActionMoveColumnRight)
221+ if len(w.Floats) != 1 || w.Floats[0].below {
222+ t.Fatalf("float still demoted: %+v", w.Floats)
223+ }
224+ })
225+}
226+
227+func TestCascadeFullscreenRestoresAcrossBand(t *testing.T) {
228+ _, w := cascadeMovesCore(t, 3, 3, false)
229+ w.FocusID(1)
230+ w.ToggleFullWidth() // X=1 alone; 2 and 3 pushed below
231+ wantBands(t, w, []WindowID{1}, []WindowID{2, 3})
232+ w.FocusID(2)
233+ w.ToggleFullscreen()
234+ wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2})
235+ w.ToggleFullscreen()
236+ w.FocusID(1)
237+ w.ToggleFullWidth()
238+ wantBands(t, w, []WindowID{1, 2, 3})
239+}
240+
241+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.
245+ c, w := cascadeMovesCore(t, 2, 3, true)
246+ dst := screenWorkspace(c, "R")
247+ dst.Overflow = OverflowFixed
248+ dst.left = 2
249+ w.ToggleFullscreen()
250+ c.applyAction(ActionMoveColumnRight)
251+ wantFocus(t, c, "R", 2)
252+ if dst.fullscreen != 2 {
253+ t.Fatalf("fullscreen %d, want 2", dst.fullscreen)
254+ }
255+ })
256+ t.Run("stacked column in a band", func(t *testing.T) {
257+ c, w := cascadeMovesCore(t, 3, 3, true)
258+ w.FocusID(3)
259+ c.applyAction(ActionConsumeOrExpelLeft) // 3 under 2
260+ w.ToggleFullscreen() // 3 expelled to its own band
261+ c.applyAction(ActionMoveColumnLeft)
262+ wantColumns(t, w, 1, 2)
263+ dst := screenWorkspace(c, "L")
264+ wantColumns(t, dst, 3)
265+ if dst.fullscreen != 3 || len(w.Columns[1].Windows) != 1 {
266+ t.Fatalf("fullscreen %d, source column %v", dst.fullscreen, w.Columns[1].Windows)
267+ }
268+ })
269+ t.Run("destination with columns gets its own band", func(t *testing.T) {
270+ c, w := cascadeMovesCore(t, 1, 3, true)
271+ dst := screenWorkspace(c, "R")
272+ dst.Overflow = OverflowCascade
273+ dst.SetMaxColumns(3)
274+ dst.AddWindow(20)
275+ dst.AddWindow(21)
276+ w.ToggleFullscreen()
277+ c.applyAction(ActionMoveColumnRight)
278+ wantBands(t, dst, []WindowID{20, 21}, []WindowID{1})
279+ if dst.fullscreen != 1 {
280+ t.Fatalf("fullscreen %d", dst.fullscreen)
281+ }
282+ })
283+}
M internal/core/cascade_moves_test.go +3 -2
@@ -299,13 +299,14 @@ func TestCascadeMoveWindowStackedFirst(t *testing.T) {
299299 wantFocus(t, c, "M", 5)
300300 }
301301
302-// A fullscreen window keeps its band.
302+// A fullscreen window keeps its own band (it left {4 5} when it went
303+// fullscreen).
303304 func TestCascadeMoveWindowFullscreenStays(t *testing.T) {
304305 c, w := cascadeMovesCore(t, 5, 3, false)
305306 w.FocusID(4)
306307 w.ToggleFullscreen()
307308 c.applyAction(ActionMoveWindowUp)
308- wantColumns(t, w, 1, 2, 3, 4, 5)
309+ wantColumns(t, w, 1, 2, 3, 5, 4)
309310 }
310311
311312 // Scroll overflow has no bands: up/down stop at the column edge.
@@ -299,13 +299,14 @@ func TestCascadeMoveWindowStackedFirst(t *testing.T) {
299 wantFocus(t, c, "M", 5)299 wantFocus(t, c, "M", 5)
300 }300 }
301 301
302-// A fullscreen window keeps its band.302+// A fullscreen window keeps its own band (it left {4 5} when it went
303+// fullscreen).
303 func TestCascadeMoveWindowFullscreenStays(t *testing.T) {304 func TestCascadeMoveWindowFullscreenStays(t *testing.T) {
304 c, w := cascadeMovesCore(t, 5, 3, false)305 c, w := cascadeMovesCore(t, 5, 3, false)
305 w.FocusID(4)306 w.FocusID(4)
306 w.ToggleFullscreen()307 w.ToggleFullscreen()
307 c.applyAction(ActionMoveWindowUp)308 c.applyAction(ActionMoveWindowUp)
308- wantColumns(t, w, 1, 2, 3, 4, 5)309+ wantColumns(t, w, 1, 2, 3, 5, 4)
309 }310 }
310 311
311 // Scroll overflow has no bands: up/down stop at the column edge.312 // Scroll overflow has no bands: up/down stop at the column edge.
M internal/core/cascade_test.go +1 -1
@@ -195,7 +195,7 @@ func TestCascadeCovers(t *testing.T) {
195195 }
196196 w.ToggleFullscreen()
197197 w.FocusID(4)
198- if w.View != 600 {
198+ if w.View != 1200 {
199199 t.Fatal("fullscreen restore lost band reveal")
200200 }
201201 }
@@ -195,7 +195,7 @@ func TestCascadeCovers(t *testing.T) {
195 }195 }
196 w.ToggleFullscreen()196 w.ToggleFullscreen()
197 w.FocusID(4)197 w.FocusID(4)
198- if w.View != 600 {198+ if w.View != 1200 {
199 t.Fatal("fullscreen restore lost band reveal")199 t.Fatal("fullscreen restore lost band reveal")
200 }200 }
201 }201 }
M internal/core/layout.go +4 -0
@@ -1335,6 +1335,7 @@ func (w *Workspace) ToggleFullscreen() {
13351335 w.endFullscreen()
13361336 return
13371337 }
1338+ w.fullscreenBand(id)
13381339 w.fullscreen = id
13391340 w.scroll()
13401341 }
@@ -1444,6 +1445,9 @@ func (w *Workspace) SetFullscreen(id WindowID, on bool) {
14441445 // Another window covers: the override was for the old cover.
14451446 w.dropStashOver()
14461447 }
1448+ if !w.isFloat(id) && w.fullscreen != id {
1449+ w.fullscreenBand(id)
1450+ }
14471451 w.fullscreen = id
14481452 if !w.isFloat(id) {
14491453 w.scroll()
@@ -1335,6 +1335,7 @@ func (w *Workspace) ToggleFullscreen() {
1335 w.endFullscreen()1335 w.endFullscreen()
1336 return1336 return
1337 }1337 }
1338+ w.fullscreenBand(id)
1338 w.fullscreen = id1339 w.fullscreen = id
1339 w.scroll()1340 w.scroll()
1340 }1341 }
@@ -1444,6 +1445,9 @@ func (w *Workspace) SetFullscreen(id WindowID, on bool) {
1444 // Another window covers: the override was for the old cover.1445 // Another window covers: the override was for the old cover.
1445 w.dropStashOver()1446 w.dropStashOver()
1446 }1447 }
1448+ if !w.isFloat(id) && w.fullscreen != id {
1449+ w.fullscreenBand(id)
1450+ }
1447 w.fullscreen = id1451 w.fullscreen = id
1448 if !w.isFloat(id) {1452 if !w.isFloat(id) {
1449 w.scroll()1453 w.scroll()
M internal/core/overview_cascade_test.go +3 -2
@@ -728,7 +728,8 @@ func TestOverviewCascadeCoveringFloat(t *testing.T) {
728728 }
729729 }
730730
731-// T10b: a pinned fullscreen column keeps its Fullscreen tile in the band.
731+// T10b: a pinned fullscreen column keeps its Fullscreen tile, and the
732+// columns of the other bands stay shown.
732733 func TestOverviewCascadePinnedFullscreenTile(t *testing.T) {
733734 m := cascadeOverview()
734735 w := m.Current()
@@ -744,7 +745,7 @@ func TestOverviewCascadePinnedFullscreenTile(t *testing.T) {
744745 t.Fatalf("fullscreen tile: %+v", fs)
745746 }
746747 for _, id := range []WindowID{7, 9} {
747- if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen || p.Peek != (fs.Peek) {
748+ if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen {
748749 t.Fatalf("band tile %d: %+v", id, p)
749750 }
750751 }
@@ -728,7 +728,8 @@ func TestOverviewCascadeCoveringFloat(t *testing.T) {
728 }728 }
729 }729 }
730 730
731-// T10b: a pinned fullscreen column keeps its Fullscreen tile in the band.731+// T10b: a pinned fullscreen column keeps its Fullscreen tile, and the
732+// columns of the other bands stay shown.
732 func TestOverviewCascadePinnedFullscreenTile(t *testing.T) {733 func TestOverviewCascadePinnedFullscreenTile(t *testing.T) {
733 m := cascadeOverview()734 m := cascadeOverview()
734 w := m.Current()735 w := m.Current()
@@ -744,7 +745,7 @@ func TestOverviewCascadePinnedFullscreenTile(t *testing.T) {
744 t.Fatalf("fullscreen tile: %+v", fs)745 t.Fatalf("fullscreen tile: %+v", fs)
745 }746 }
746 for _, id := range []WindowID{7, 9} {747 for _, id := range []WindowID{7, 9} {
747- if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen || p.Peek != (fs.Peek) {748+ if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen {
748 t.Fatalf("band tile %d: %+v", id, p)749 t.Fatalf("band tile %d: %+v", id, p)
749 }750 }
750 }751 }

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