detached dbd9830

feat(core): give a fullscreen cascade window its own band

A cascade window going fullscreen leaves its band for a new one right
below, so focus changes among the other columns no longer recompose the
fullscreen output. move-column-left/right on a fullscreen window sends
it to the neighbor monitor, still fullscreen.
commit
dbd98309bcdd8e48b5bb98f4b9a3e6d6e1f0f2be
author
brice · · 2026-10-22 19:43 UTC
parent
fdd661a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +158 -6
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 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 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 +12 -1
@@ -388,7 +388,15 @@ 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 leaves for the neighbor screen at once, and
392+ // stays fullscreen there; with no screen, nothing moves.
393+ fid, _ := w.Focused()
394+ full := fid != 0 && !w.onFloat() && w.fullscreen == fid
395+ i := c.neighbor(d)
396+ if full && i < 0 {
397+ return Effect{}
398+ }
399+ if i >= 0 && (full || w.leavesBand(w.Focus+dir)) {
392400 col, ok := w.takeColumn()
393401 if !ok {
394402 return Effect{}
@@ -401,6 +409,9 @@ func (c *Core) dispatchAction(a Action) Effect {
401409 at = len(to.Columns)
402410 }
403411 to.receive(col, at)
412+ if full {
413+ to.ToggleFullscreen()
414+ }
404415 from.mon.normalize()
405416 c.cur().mon.normalize()
406417 return Effect{}
@@ -388,7 +388,15 @@ 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 leaves for the neighbor screen at once, and
392+ // stays fullscreen there; with no screen, nothing moves.
393+ fid, _ := w.Focused()
394+ full := fid != 0 && !w.onFloat() && w.fullscreen == fid
395+ i := c.neighbor(d)
396+ if full && i < 0 {
397+ return Effect{}
398+ }
399+ if i >= 0 && (full || w.leavesBand(w.Focus+dir)) {
392 col, ok := w.takeColumn()400 col, ok := w.takeColumn()
393 if !ok {401 if !ok {
394 return Effect{}402 return Effect{}
@@ -401,6 +409,9 @@ func (c *Core) dispatchAction(a Action) Effect {
401 at = len(to.Columns)409 at = len(to.Columns)
402 }410 }
403 to.receive(col, at)411 to.receive(col, at)
412+ if full {
413+ to.ToggleFullscreen()
414+ }
404 from.mon.normalize()415 from.mon.normalize()
405 c.cur().mon.normalize()416 c.cur().mon.normalize()
406 return Effect{}417 return Effect{}
M internal/core/cascade.go +39 -0
@@ -419,6 +419,45 @@ 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+ w.dropSlotOf(id)
442+ bands := w.bandList()
443+ col := bands[b][i-first]
444+ nc := col
445+ if len(col.Windows) == 1 {
446+ bands[b] = slices.Delete(bands[b], i-first, i-first+1)
447+ } else {
448+ j := slices.Index(col.Windows, id)
449+ nc = Column{Windows: []WindowID{id}, Cells: col.Cells}
450+ col.Windows = slices.Delete(slices.Clone(col.Windows), j, j+1)
451+ switch {
452+ case col.Focus > j, col.Focus >= len(col.Windows):
453+ col.Focus = max(min(col.Focus-1, len(col.Windows)-1), 0)
454+ }
455+ bands[b][i-first] = col
456+ }
457+ bands = slices.Insert(bands, b+1, []Column{nc})
458+ w.setBands(bands, focus)
459+}
460+
422461 // pushNeighbor makes room in the focused column's full cascade band for a
423462 // wider column: the column at the far end of the band leaves it. The
424463 // rightmost goes to the band below; from the last column, the leftmost
@@ -419,6 +419,45 @@ 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+ w.dropSlotOf(id)
442+ bands := w.bandList()
443+ col := bands[b][i-first]
444+ nc := col
445+ if len(col.Windows) == 1 {
446+ bands[b] = slices.Delete(bands[b], i-first, i-first+1)
447+ } else {
448+ j := slices.Index(col.Windows, id)
449+ nc = Column{Windows: []WindowID{id}, Cells: col.Cells}
450+ col.Windows = slices.Delete(slices.Clone(col.Windows), j, j+1)
451+ switch {
452+ case col.Focus > j, col.Focus >= len(col.Windows):
453+ col.Focus = max(min(col.Focus-1, len(col.Windows)-1), 0)
454+ }
455+ bands[b][i-first] = col
456+ }
457+ bands = slices.Insert(bands, b+1, []Column{nc})
458+ w.setBands(bands, focus)
459+}
460+
422 // pushNeighbor makes room in the focused column's full cascade band for a461 // 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. The462 // 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 leftmost463 // rightmost goes to the band below; from the last column, the leftmost
A internal/core/cascade_fullscreen_band_test.go +94 -0
@@ -0,0 +1,94 @@
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(c *Core, w *Workspace)
11+ }{
12+ {"toggle", func(_ *Core, w *Workspace) { w.ToggleFullscreen() }},
13+ {"client request", func(_ *Core, 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(c, 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+}
@@ -0,0 +1,94 @@
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(c *Core, w *Workspace)
11+ }{
12+ {"toggle", func(_ *Core, w *Workspace) { w.ToggleFullscreen() }},
13+ {"client request", func(_ *Core, 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(c, 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+}
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) {
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) {
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