detached 6b64d3c

fix(core): step the cascade workspace before the neighbor monitor at a band edge

commit
6b64d3c0a4313e82d82ac14965ce96aac1a5ae41
author
brice · · 2026-10-12 14:01 UTC
parent
de9da51
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +57 -21
M docs/config.md +3 -3
@@ -227,13 +227,13 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
227227
228228 Cascade is the default layout (`layout.overflow = cascade`); it can also be set per output or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its row of cells (see [Column widths](#column-widths)); auto columns share it equally, so a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations.
229229
230-Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go to the neighbor monitor on that side, or, with none, to the previous or next workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends.
230+Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go to the previous or next workspace, so the opposite key comes back; past the first or the trailing empty workspace, to the neighbor monitor on that side; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends.
231231
232232 Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour.
233233
234234 In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. A maximized column shows the columns it hides as dimmed cards behind it, peeking out on their side of the band: the nearest one on each side, then the next ones stacked at the band's edge. Selecting a card (left/right, a three-finger swipe or a click on a visible card) moves the maximization to it; Return keeps it there and Escape gives it back.
235235
236-In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the neighbor monitor at the band's edge, else to the previous or next workspace). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. Over a maximized column that hides cards, horizontal moves one step per gesture like left/right: the next card, then past the band's edge the stash or a workspace. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band.
236+In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the previous or next workspace at the band's edge, then to the neighbor monitor). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. Over a maximized column that hides cards, horizontal moves one step per gesture like left/right: the next card, then past the band's edge the stash or a workspace. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band.
237237
238238 Directional focus past the edge of the content switches workspace along the layout's workspace axis: up/down in scroll and fixed overflow, left/right in cascade.
239239
@@ -267,7 +267,7 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l
267267 | `switch-column-prev` | The same, in the opposite direction |
268268 | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column |
269269 | `cycle-column-width` | Grow the focused column by one cell, taking it from the widest other column of its row (see [Column widths](#column-widths)). When no column has a cell to give, every column of the row goes back to equal shares. In scroll overflow, the column grows to `max-columns` cells, then back to auto. A lone column or a full row cannot grow: it goes back to equal shares if its cells were set, and otherwise nothing changes. A spiral never changes |
270-| `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it with no monitor the previous or next workspace. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays |
270+| `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it comes the previous or next workspace, then, past the first or the trailing empty one, the neighbor monitor. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays |
271271 | `focus-window-up/down` | Focus in the column; past the edge, the previous or next workspace (in cascade overflow, the adjacent band, stopping at the first and last). In fixed overflow, the column on screen above or below comes first. Up at the top brings back a covering float put behind the columns. From a fullscreen window, a window above or below comes first and fullscreen ends |
272272 | `move-column-left/right` | Move the focused column; at the edge, to the neighbor monitor, focus following. In cascade overflow the edge is the band's; with no monitor there, the column crosses into the adjacent band. Fixed overflow follows column order, not the screen |
273273 | `move-window-up/down` | Move the focused window up or down in its column; focus follows it. In cascade overflow, past the column, the column moves to the adjacent band, where `focus-window-up/down` goes |
@@ -227,13 +227,13 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
227 227
228 Cascade is the default layout (`layout.overflow = cascade`); it can also be set per output or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its row of cells (see [Column widths](#column-widths)); auto columns share it equally, so a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations.228 Cascade is the default layout (`layout.overflow = cascade`); it can also be set per output or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its row of cells (see [Column widths](#column-widths)); auto columns share it equally, so a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations.
229 229
230-Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go to the neighbor monitor on that side, or, with none, to the previous or next workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends.230+Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go to the previous or next workspace, so the opposite key comes back; past the first or the trailing empty workspace, to the neighbor monitor on that side; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends.
231 231
232 Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour.232 Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour.
233 233
234 In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. A maximized column shows the columns it hides as dimmed cards behind it, peeking out on their side of the band: the nearest one on each side, then the next ones stacked at the band's edge. Selecting a card (left/right, a three-finger swipe or a click on a visible card) moves the maximization to it; Return keeps it there and Escape gives it back.234 In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. A maximized column shows the columns it hides as dimmed cards behind it, peeking out on their side of the band: the nearest one on each side, then the next ones stacked at the band's edge. Selecting a card (left/right, a three-finger swipe or a click on a visible card) moves the maximization to it; Return keeps it there and Escape gives it back.
235 235
236-In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the neighbor monitor at the band's edge, else to the previous or next workspace). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. Over a maximized column that hides cards, horizontal moves one step per gesture like left/right: the next card, then past the band's edge the stash or a workspace. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band.236+In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the previous or next workspace at the band's edge, then to the neighbor monitor). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. Over a maximized column that hides cards, horizontal moves one step per gesture like left/right: the next card, then past the band's edge the stash or a workspace. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band.
237 237
238 Directional focus past the edge of the content switches workspace along the layout's workspace axis: up/down in scroll and fixed overflow, left/right in cascade.238 Directional focus past the edge of the content switches workspace along the layout's workspace axis: up/down in scroll and fixed overflow, left/right in cascade.
239 239
@@ -267,7 +267,7 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l
267 | `switch-column-prev` | The same, in the opposite direction |267 | `switch-column-prev` | The same, in the opposite direction |
268 | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column |268 | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column |
269 | `cycle-column-width` | Grow the focused column by one cell, taking it from the widest other column of its row (see [Column widths](#column-widths)). When no column has a cell to give, every column of the row goes back to equal shares. In scroll overflow, the column grows to `max-columns` cells, then back to auto. A lone column or a full row cannot grow: it goes back to equal shares if its cells were set, and otherwise nothing changes. A spiral never changes |269 | `cycle-column-width` | Grow the focused column by one cell, taking it from the widest other column of its row (see [Column widths](#column-widths)). When no column has a cell to give, every column of the row goes back to equal shares. In scroll overflow, the column grows to `max-columns` cells, then back to auto. A lone column or a full row cannot grow: it goes back to equal shares if its cells were set, and otherwise nothing changes. A spiral never changes |
270-| `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it with no monitor the previous or next workspace. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays |270+| `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it comes the previous or next workspace, then, past the first or the trailing empty one, the neighbor monitor. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays |
271 | `focus-window-up/down` | Focus in the column; past the edge, the previous or next workspace (in cascade overflow, the adjacent band, stopping at the first and last). In fixed overflow, the column on screen above or below comes first. Up at the top brings back a covering float put behind the columns. From a fullscreen window, a window above or below comes first and fullscreen ends |271 | `focus-window-up/down` | Focus in the column; past the edge, the previous or next workspace (in cascade overflow, the adjacent band, stopping at the first and last). In fixed overflow, the column on screen above or below comes first. Up at the top brings back a covering float put behind the columns. From a fullscreen window, a window above or below comes first and fullscreen ends |
272 | `move-column-left/right` | Move the focused column; at the edge, to the neighbor monitor, focus following. In cascade overflow the edge is the band's; with no monitor there, the column crosses into the adjacent band. Fixed overflow follows column order, not the screen |272 | `move-column-left/right` | Move the focused column; at the edge, to the neighbor monitor, focus following. In cascade overflow the edge is the band's; with no monitor there, the column crosses into the adjacent band. Fixed overflow follows column order, not the screen |
273 | `move-window-up/down` | Move the focused window up or down in its column; focus follows it. In cascade overflow, past the column, the column moves to the adjacent band, where `focus-window-up/down` goes |273 | `move-window-up/down` | Move the focused window up or down in its column; focus follows it. In cascade overflow, past the column, the column moves to the adjacent band, where `focus-window-up/down` goes |
M internal/core/action.go +9 -10
@@ -230,30 +230,29 @@ func (c *Core) applyAction(a Action) Effect {
230230 dir, d = -1, dirLeft
231231 }
232232 // A covering fullscreen window is left for the window on that side;
233- // with none (the workspace, band or stash edge), the move goes to the
234- // neighbor monitor, else along the workspace axis, and fullscreen
233+ // with none (the workspace, band or stash edge), the move goes along
234+ // the workspace axis, else to the neighbor monitor, and fullscreen
235235 // stays.
236236 if w.pinned() && !w.stashOverCover() {
237- if !w.FocusColumn(dir) {
237+ if !w.FocusColumn(dir) && !c.cur().mon.stepWorkspace(horizontalAxis, dir) {
238238 if i := c.neighbor(d); i >= 0 {
239239 c.focusScreen = i
240- } else {
241- c.cur().mon.stepWorkspace(horizontalAxis, dir)
242240 }
243241 }
244242 return Effect{}
245243 }
246244 // The first move from a float stays here unless nothing is under it:
247245 // a native float leaves for the stash or the columns, and the stash
248- // keeps the focus at its ends. At the edge the move goes to the
249- // neighbor monitor, else along the workspace axis if it is horizontal.
246+ // keeps the focus at its ends. At the edge the move goes along the
247+ // workspace axis if it is horizontal, so the way back is the same
248+ // key; past the first or trailing workspace, to the neighbor monitor.
250249 edge := w.leavesBand(w.columnToward(dir)) && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat())
251250 if edge {
252- if i := c.neighbor(d); i >= 0 {
253- c.focusScreen = i
251+ if c.cur().mon.stepWorkspace(horizontalAxis, dir) {
254252 return Effect{}
255253 }
256- if c.cur().mon.stepWorkspace(horizontalAxis, dir) {
254+ if i := c.neighbor(d); i >= 0 {
255+ c.focusScreen = i
257256 return Effect{}
258257 }
259258 }
@@ -230,30 +230,29 @@ func (c *Core) applyAction(a Action) Effect {
230 dir, d = -1, dirLeft230 dir, d = -1, dirLeft
231 }231 }
232 // A covering fullscreen window is left for the window on that side;232 // A covering fullscreen window is left for the window on that side;
233- // with none (the workspace, band or stash edge), the move goes to the233+ // with none (the workspace, band or stash edge), the move goes along
234- // neighbor monitor, else along the workspace axis, and fullscreen234+ // the workspace axis, else to the neighbor monitor, and fullscreen
235 // stays.235 // stays.
236 if w.pinned() && !w.stashOverCover() {236 if w.pinned() && !w.stashOverCover() {
237- if !w.FocusColumn(dir) {237+ if !w.FocusColumn(dir) && !c.cur().mon.stepWorkspace(horizontalAxis, dir) {
238 if i := c.neighbor(d); i >= 0 {238 if i := c.neighbor(d); i >= 0 {
239 c.focusScreen = i239 c.focusScreen = i
240- } else {
241- c.cur().mon.stepWorkspace(horizontalAxis, dir)
242 }240 }
243 }241 }
244 return Effect{}242 return Effect{}
245 }243 }
246 // The first move from a float stays here unless nothing is under it:244 // The first move from a float stays here unless nothing is under it:
247 // a native float leaves for the stash or the columns, and the stash245 // a native float leaves for the stash or the columns, and the stash
248- // keeps the focus at its ends. At the edge the move goes to the246+ // keeps the focus at its ends. At the edge the move goes along the
249- // neighbor monitor, else along the workspace axis if it is horizontal.247+ // workspace axis if it is horizontal, so the way back is the same
248+ // key; past the first or trailing workspace, to the neighbor monitor.
250 edge := w.leavesBand(w.columnToward(dir)) && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat())249 edge := w.leavesBand(w.columnToward(dir)) && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat())
251 if edge {250 if edge {
252- if i := c.neighbor(d); i >= 0 {251+ if c.cur().mon.stepWorkspace(horizontalAxis, dir) {
253- c.focusScreen = i
254 return Effect{}252 return Effect{}
255 }253 }
256- if c.cur().mon.stepWorkspace(horizontalAxis, dir) {254+ if i := c.neighbor(d); i >= 0 {
255+ c.focusScreen = i
257 return Effect{}256 return Effect{}
258 }257 }
259 }258 }
M internal/core/cascade_moves_test.go +1 -0
@@ -107,6 +107,7 @@ func TestCascadeInsideBandStaysOnMonitor(t *testing.T) {
107107 func TestCascadeBandEdgeWithoutMonitorFocusStepsMoveCrosses(t *testing.T) {
108108 c, w := cascadeMovesCore(t, 5, 3, false)
109109 m := c.cur().mon
110+ m.normalize() // the trailing empty workspace, as in a running session
110111 w.FocusID(4)
111112 c.applyAction(ActionFocusColumnLeft)
112113 wantFocus(t, c, "M", 4)
@@ -107,6 +107,7 @@ func TestCascadeInsideBandStaysOnMonitor(t *testing.T) {
107 func TestCascadeBandEdgeWithoutMonitorFocusStepsMoveCrosses(t *testing.T) {107 func TestCascadeBandEdgeWithoutMonitorFocusStepsMoveCrosses(t *testing.T) {
108 c, w := cascadeMovesCore(t, 5, 3, false)108 c, w := cascadeMovesCore(t, 5, 3, false)
109 m := c.cur().mon109 m := c.cur().mon
110+ m.normalize() // the trailing empty workspace, as in a running session
110 w.FocusID(4)111 w.FocusID(4)
111 c.applyAction(ActionFocusColumnLeft)112 c.applyAction(ActionFocusColumnLeft)
112 wantFocus(t, c, "M", 4)113 wantFocus(t, c, "M", 4)
M internal/core/monitor.go +5 -3
@@ -206,14 +206,16 @@ func (m *Monitor) Focus(i int) {
206206 }
207207
208208 // stepWorkspace switches to the previous or next workspace (dir -1/1) when
209-// axis is the current layout's workspace axis, and reports whether it did. A
209+// axis is the current layout's workspace axis, and reports whether it did.
210+// Past the first or the trailing workspace there is none to step to. A
210211 // hidden named workspace is outside the numbered list: it never steps.
211212 func (m *Monitor) stepWorkspace(axis layoutAxis, dir int) bool {
212213 step := m.Current().policy().workspaceStep(axis, dir)
213- if m.shown != nil || step == 0 {
214+ to := m.Active + step
215+ if m.shown != nil || step == 0 || to < 0 || to >= len(m.Workspaces) {
214216 return false
215217 }
216- m.Focus(m.Active + step)
218+ m.Focus(to)
217219 return true
218220 }
219221
@@ -206,14 +206,16 @@ func (m *Monitor) Focus(i int) {
206 }206 }
207 207
208 // stepWorkspace switches to the previous or next workspace (dir -1/1) when208 // stepWorkspace switches to the previous or next workspace (dir -1/1) when
209-// axis is the current layout's workspace axis, and reports whether it did. A209+// axis is the current layout's workspace axis, and reports whether it did.
210+// Past the first or the trailing workspace there is none to step to. A
210 // hidden named workspace is outside the numbered list: it never steps.211 // hidden named workspace is outside the numbered list: it never steps.
211 func (m *Monitor) stepWorkspace(axis layoutAxis, dir int) bool {212 func (m *Monitor) stepWorkspace(axis layoutAxis, dir int) bool {
212 step := m.Current().policy().workspaceStep(axis, dir)213 step := m.Current().policy().workspaceStep(axis, dir)
213- if m.shown != nil || step == 0 {214+ to := m.Active + step
215+ if m.shown != nil || step == 0 || to < 0 || to >= len(m.Workspaces) {
214 return false216 return false
215 }217 }
216- m.Focus(m.Active + step)218+ m.Focus(to)
217 return true219 return true
218 }220 }
219 221
M internal/core/workspace_axis_test.go +39 -5
@@ -50,18 +50,52 @@ func TestCascadeFocusColumnStepsWorkspaceAtBandEdge(t *testing.T) {
5050 }
5151 }
5252
53-// AC2: a monitor on that side wins over the workspace step.
54-func TestCascadeFocusColumnPrefersNeighborMonitor(t *testing.T) {
53+// The workspace step wins over a monitor on that side, so the same key
54+// brings focus back: a neighbor monitor is reached only past the first or
55+// the trailing workspace.
56+func TestCascadeFocusColumnStepsWorkspaceBeforeMonitor(t *testing.T) {
5557 c, m := axisCore(t, true, OverflowCascade, 3)
5658 m.Current().FocusID(23)
5759 c.applyAction(ActionFocusColumnRight)
58- wantFocus(t, c, "R", 0)
60+ wantActive(t, m, 2)
61+ if got := c.cur().name(); got != "M" {
62+ t.Fatalf("focused screen %q, want M", got)
63+ }
64+ // Back with the opposite key, from the first column of workspace 3.
65+ m.Current().FocusID(31)
66+ c.applyAction(ActionFocusColumnLeft)
5967 wantActive(t, m, 1)
60- c.focusScreen = 1
61- m.Current().FocusID(24)
68+ if got := c.cur().name(); got != "M" {
69+ t.Fatalf("focused screen %q, want M", got)
70+ }
71+ // Past the first workspace: the monitor on the left.
72+ m.Focus(0)
73+ m.Current().FocusID(11)
6274 c.applyAction(ActionFocusColumnLeft)
6375 wantFocus(t, c, "L", 0)
76+ wantActive(t, m, 0)
77+ // Past the trailing empty workspace: the monitor on the right.
78+ c.focusScreen = 1
79+ m.Focus(len(m.Workspaces) - 1)
80+ c.applyAction(ActionFocusColumnRight)
81+ wantFocus(t, c, "R", 0)
82+}
83+
84+// The live regression: one monitor on the left only. Right steps to the
85+// next workspace, and left from its empty band comes back to the previous
86+// workspace instead of leaving for that monitor.
87+func TestCascadeStepRightThenLeftComesBack(t *testing.T) {
88+ c := layoutCore(1, tile{"L", 0, 0, 900, 600}, tile{"M", 900, 0, 900, 600})
89+ m := c.cur().mon
90+ m.SetOverflow(OverflowCascade)
91+ m.SetMaxColumns(3)
92+ m.AddWindow(1)
93+ m.AddWindow(5)
94+ c.applyAction(ActionFocusColumnRight)
6495 wantActive(t, m, 1)
96+ c.applyAction(ActionFocusColumnLeft)
97+ wantFocus(t, c, "M", 5)
98+ wantActive(t, m, 0)
6599 }
66100
67101 // AC3: up/down cross bands and stop at the last, with no workspace change.
@@ -50,18 +50,52 @@ func TestCascadeFocusColumnStepsWorkspaceAtBandEdge(t *testing.T) {
50 }50 }
51 }51 }
52 52
53-// AC2: a monitor on that side wins over the workspace step.53+// The workspace step wins over a monitor on that side, so the same key
54-func TestCascadeFocusColumnPrefersNeighborMonitor(t *testing.T) {54+// brings focus back: a neighbor monitor is reached only past the first or
55+// the trailing workspace.
56+func TestCascadeFocusColumnStepsWorkspaceBeforeMonitor(t *testing.T) {
55 c, m := axisCore(t, true, OverflowCascade, 3)57 c, m := axisCore(t, true, OverflowCascade, 3)
56 m.Current().FocusID(23)58 m.Current().FocusID(23)
57 c.applyAction(ActionFocusColumnRight)59 c.applyAction(ActionFocusColumnRight)
58- wantFocus(t, c, "R", 0)60+ wantActive(t, m, 2)
61+ if got := c.cur().name(); got != "M" {
62+ t.Fatalf("focused screen %q, want M", got)
63+ }
64+ // Back with the opposite key, from the first column of workspace 3.
65+ m.Current().FocusID(31)
66+ c.applyAction(ActionFocusColumnLeft)
59 wantActive(t, m, 1)67 wantActive(t, m, 1)
60- c.focusScreen = 168+ if got := c.cur().name(); got != "M" {
61- m.Current().FocusID(24)69+ t.Fatalf("focused screen %q, want M", got)
70+ }
71+ // Past the first workspace: the monitor on the left.
72+ m.Focus(0)
73+ m.Current().FocusID(11)
62 c.applyAction(ActionFocusColumnLeft)74 c.applyAction(ActionFocusColumnLeft)
63 wantFocus(t, c, "L", 0)75 wantFocus(t, c, "L", 0)
76+ wantActive(t, m, 0)
77+ // Past the trailing empty workspace: the monitor on the right.
78+ c.focusScreen = 1
79+ m.Focus(len(m.Workspaces) - 1)
80+ c.applyAction(ActionFocusColumnRight)
81+ wantFocus(t, c, "R", 0)
82+}
83+
84+// The live regression: one monitor on the left only. Right steps to the
85+// next workspace, and left from its empty band comes back to the previous
86+// workspace instead of leaving for that monitor.
87+func TestCascadeStepRightThenLeftComesBack(t *testing.T) {
88+ c := layoutCore(1, tile{"L", 0, 0, 900, 600}, tile{"M", 900, 0, 900, 600})
89+ m := c.cur().mon
90+ m.SetOverflow(OverflowCascade)
91+ m.SetMaxColumns(3)
92+ m.AddWindow(1)
93+ m.AddWindow(5)
94+ c.applyAction(ActionFocusColumnRight)
64 wantActive(t, m, 1)95 wantActive(t, m, 1)
96+ c.applyAction(ActionFocusColumnLeft)
97+ wantFocus(t, c, "M", 5)
98+ wantActive(t, m, 0)
65 }99 }
66 100
67 // AC3: up/down cross bands and stop at the last, with no workspace change.101 // AC3: up/down cross bands and stop at the last, with no workspace change.

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