detached 264a8c7

fix(core): activate a stashed window's dialog through its window

A dialog of a hidden stashed window whose own fullscreen waited now brings that window fullscreen, as activating the window does, instead of leaving the cover for plain tiles. Activation tests cover every cover kind for returns and modal close, dialog z-order over the stash, and dialogs that leave the cover. Docs and comments state the exceptions.
commit
264a8c7537cb211ca3559ac801ca6b930c9c34c1
author
brice · · 2026-10-13 08:31 UTC
parent
e031711
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +113 -34
M docs/config.md +1 -1
@@ -258,7 +258,7 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l
258258 | `spawn <command>` | Run a program |
259259 | `spawn-terminal` | Run `terminal` |
260260 | `close-window` | Close the focused window |
261-| `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, except an activation of a window that can show over it: one of its dialogs, or a stashed window, which shows the stash over it as `toggle-stash-visible` does; the app's own fullscreen requests are then refused until it is focused again. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first |
261+| `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, and the app's own fullscreen requests are then refused until it is focused again. An activation of a window that can show over it keeps it fullscreen instead: one of its dialogs, or a stashed window or a dialog of one (and their dialogs), which shows the stash over it as `toggle-stash-visible` does, unless that stashed window asked for fullscreen while hidden: it then replaces the fullscreen window. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first |
262262 | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first |
263263 | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window |
264264 | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it |
@@ -258,7 +258,7 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l
258 | `spawn <command>` | Run a program |258 | `spawn <command>` | Run a program |
259 | `spawn-terminal` | Run `terminal` |259 | `spawn-terminal` | Run `terminal` |
260 | `close-window` | Close the focused window |260 | `close-window` | Close the focused window |
261-| `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, except an activation of a window that can show over it: one of its dialogs, or a stashed window, which shows the stash over it as `toggle-stash-visible` does; the app's own fullscreen requests are then refused until it is focused again. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first |261+| `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, and the app's own fullscreen requests are then refused until it is focused again. An activation of a window that can show over it keeps it fullscreen instead: one of its dialogs, or a stashed window or a dialog of one (and their dialogs), which shows the stash over it as `toggle-stash-visible` does, unless that stashed window asked for fullscreen while hidden: it then replaces the fullscreen window. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first |
262 | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first |262 | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first |
263 | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window |263 | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window |
264 | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it |264 | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it |
M internal/core/layout.go +12 -2
@@ -393,7 +393,8 @@ func (w *Workspace) focusModal(from WindowID) {
393393 if c := w.cover(); c != 0 && c != d {
394394 if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) {
395395 // Hidden under another window's fullscreen: leave it, as
396- // Activate does for a tile, so the dialog shows.
396+ // Activate does for a window not shown over the cover, so
397+ // the dialog shows.
397398 w.leaveFullscreen()
398399 }
399400 }
@@ -1266,13 +1267,22 @@ func (w *Workspace) leaveFullscreen() {
12661267 // window's fullscreen, which hides everything else. Over a covering
12671268 // window, a dialog drawn over it, a stashed window or a dialog of one
12681269 // shows over it instead, as ToggleStashVisible does: the cover stays
1269-// fullscreen behind, so a game never sees its fullscreen end.
1270+// fullscreen behind, so a game never sees its fullscreen end. A stashed
1271+// window whose own fullscreen waited while hidden replaces the cover
1272+// (coverStays); a dialog of a stashed window activates that window
1273+// first.
12701274 func (w *Workspace) Activate(id WindowID) {
12711275 if c := w.cover(); c != 0 && id != c {
12721276 owner := id
12731277 if f := w.floatIndex(id); f >= 0 {
12741278 owner = w.dialogOwner(w.Floats[f], c)
12751279 }
1280+ if owner != id && owner != c && w.stashIndex(owner) >= 0 && !w.coverStays(owner) {
1281+ w.Activate(owner)
1282+ w.FocusID(id)
1283+ w.focusModal(0)
1284+ return
1285+ }
12761286 if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) {
12771287 if !w.stashOverCover() {
12781288 w.showStashOver()
@@ -393,7 +393,8 @@ func (w *Workspace) focusModal(from WindowID) {
393 if c := w.cover(); c != 0 && c != d {393 if c := w.cover(); c != 0 && c != d {
394 if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) {394 if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) {
395 // Hidden under another window's fullscreen: leave it, as395 // Hidden under another window's fullscreen: leave it, as
396- // Activate does for a tile, so the dialog shows.396+ // Activate does for a window not shown over the cover, so
397+ // the dialog shows.
397 w.leaveFullscreen()398 w.leaveFullscreen()
398 }399 }
399 }400 }
@@ -1266,13 +1267,22 @@ func (w *Workspace) leaveFullscreen() {
1266 // window's fullscreen, which hides everything else. Over a covering1267 // window's fullscreen, which hides everything else. Over a covering
1267 // window, a dialog drawn over it, a stashed window or a dialog of one1268 // window, a dialog drawn over it, a stashed window or a dialog of one
1268 // shows over it instead, as ToggleStashVisible does: the cover stays1269 // shows over it instead, as ToggleStashVisible does: the cover stays
1269-// fullscreen behind, so a game never sees its fullscreen end.1270+// fullscreen behind, so a game never sees its fullscreen end. A stashed
1271+// window whose own fullscreen waited while hidden replaces the cover
1272+// (coverStays); a dialog of a stashed window activates that window
1273+// first.
1270 func (w *Workspace) Activate(id WindowID) {1274 func (w *Workspace) Activate(id WindowID) {
1271 if c := w.cover(); c != 0 && id != c {1275 if c := w.cover(); c != 0 && id != c {
1272 owner := id1276 owner := id
1273 if f := w.floatIndex(id); f >= 0 {1277 if f := w.floatIndex(id); f >= 0 {
1274 owner = w.dialogOwner(w.Floats[f], c)1278 owner = w.dialogOwner(w.Floats[f], c)
1275 }1279 }
1280+ if owner != id && owner != c && w.stashIndex(owner) >= 0 && !w.coverStays(owner) {
1281+ w.Activate(owner)
1282+ w.FocusID(id)
1283+ w.focusModal(0)
1284+ return
1285+ }
1276 if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) {1286 if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) {
1277 if !w.stashOverCover() {1287 if !w.stashOverCover() {
1278 w.showStashOver()1288 w.showStashOver()
M internal/core/stash_cover_test.go +100 -31
@@ -228,6 +228,12 @@ func TestActivateOverCover(t *testing.T) {
228228 t.Run(cv.name+"/"+tc.name, func(t *testing.T) {
229229 _, w, cover := stashCoverCore(t, cv.overflow, cv.float)
230230 id := tc.setup(w, cover)
231+ // A dialog of the hidden stash waits hidden under the cover.
232+ if i := slices.IndexFunc(w.Floats, func(f Float) bool { return f.over == 2 }); i >= 0 && !w.stashOverCover() {
233+ if p := previewOf(t, w.Layout(), w.Floats[i].ID); !p.Hidden {
234+ t.Fatalf("dialog of the hidden stash shown before activation: %+v", p)
235+ }
236+ }
231237 view := w.View
232238 w.Activate(id)
233239 if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over {
@@ -237,9 +243,13 @@ func TestActivateOverCover(t *testing.T) {
237243 if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen {
238244 t.Fatalf("cover %+v", p)
239245 }
240- if f, c := slices.IndexFunc(ps, isID(tc.focused)), slices.IndexFunc(ps, isID(cover)); f < c || ps[f].Hidden {
246+ f := slices.IndexFunc(ps, isID(tc.focused))
247+ if f < slices.IndexFunc(ps, isID(cover)) || ps[f].Hidden {
241248 t.Fatalf("focused window not over the cover: %+v", ps)
242249 }
250+ if tc.over && f < slices.IndexFunc(ps, isID(2)) {
251+ t.Fatalf("focused window under the stash: %+v", ps)
252+ }
243253 if !cv.float && w.View != view {
244254 t.Fatalf("view %d, was %d", w.View, view)
245255 }
@@ -249,40 +259,99 @@ func TestActivateOverCover(t *testing.T) {
249259 }
250260
251261 // After an activation shows the stash over the cover, hiding the stash
252-// or activating the cover returns to it, and a dialog of a stashed
253-// window mapping meanwhile shows with the focus.
262+// or activating the cover returns to it; a dialog of a stashed window,
263+// modal or mapping meanwhile, shows with the focus and closing it
264+// returns to the stash over the cover.
254265 func TestActivateOverCoverReturns(t *testing.T) {
255- _, w, cover := stashCoverCore(t, OverflowFixed, true)
256- w.Activate(2)
257- w.ToggleStashVisible()
258- if id, _ := w.Focused(); id != cover || w.stashShown() {
259- t.Fatalf("hide: focused %d shown %v", id, w.stashShown())
260- }
261- w.Activate(2)
262- w.Activate(cover)
263- if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
264- t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
265- }
266- w.Activate(2)
267- w.AddDialog(9, 2, 20, 10)
268- w.AddDialog(8, 9, 10, 5)
269- if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden {
270- t.Fatalf("mapped chain: focused %d", id)
271- }
272- // Closing the dialogs returns to the stash over the cover.
273- w.RemoveWindow(8)
274- w.RemoveWindow(9)
275- if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
276- t.Fatalf("closed: focused %d over %v", id, w.stashOverCover())
277- }
278- // The cover closes: the stash stays shown and focused, the override
279- // ends with it.
280- w.RemoveWindow(cover)
281- if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() {
282- t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown())
266+ for _, cv := range []struct {
267+ name string
268+ overflow Overflow
269+ float bool
270+ }{
271+ {"fixed tile", OverflowFixed, false},
272+ {"scroll tile", OverflowScroll, false},
273+ {"fixed float", OverflowFixed, true},
274+ {"scroll float", OverflowScroll, true},
275+ } {
276+ t.Run(cv.name, func(t *testing.T) {
277+ _, w, cover := stashCoverCore(t, cv.overflow, cv.float)
278+ w.Activate(2)
279+ w.ToggleStashVisible()
280+ if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
281+ t.Fatalf("hide: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
282+ }
283+ w.Activate(2)
284+ w.Activate(cover)
285+ if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
286+ t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
287+ }
288+ w.AddDialog(7, 2, 20, 10)
289+ w.SetModal(7, true)
290+ w.FocusID(cover)
291+ w.Activate(2)
292+ w.RemoveWindow(7)
293+ if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
294+ t.Fatalf("modal closed: focused %d over %v", id, w.stashOverCover())
295+ }
296+ w.AddDialog(9, 2, 20, 10)
297+ w.AddDialog(8, 9, 10, 5)
298+ if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden {
299+ t.Fatalf("mapped chain: focused %d", id)
300+ }
301+ w.RemoveWindow(8)
302+ w.RemoveWindow(9)
303+ if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
304+ t.Fatalf("closed: focused %d over %v", id, w.stashOverCover())
305+ }
306+ // The cover closes: the stash stays shown and focused, the
307+ // override ends with it.
308+ w.RemoveWindow(cover)
309+ if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() {
310+ t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown())
311+ }
312+ })
283313 }
284314 }
285315
316+// Activating a dialog that cannot show over the cover leaves fullscreen
317+// as any other window does; a dialog of a stashed window whose own
318+// fullscreen waited while hidden brings that window fullscreen, as
319+// activating the window itself does.
320+func TestActivateDialogLeavesCover(t *testing.T) {
321+ t.Run("dialog of a plain float", func(t *testing.T) {
322+ _, w, cover := stashCoverCore(t, OverflowFixed, false)
323+ w.AddFloating(5, 20, 10)
324+ w.AddDialog(9, 5, 10, 5)
325+ w.FocusID(cover)
326+ w.Activate(9)
327+ if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover {
328+ t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left)
329+ }
330+ })
331+ t.Run("dialog of another tile", func(t *testing.T) {
332+ _, w, cover := stashCoverCore(t, OverflowFixed, true)
333+ w.AddDialog(9, 1, 10, 5)
334+ w.FocusID(cover)
335+ w.Activate(9)
336+ if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover {
337+ t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left)
338+ }
339+ })
340+ t.Run("dialog of a stashed window with a pending fullscreen", func(t *testing.T) {
341+ _, w, cover := stashCoverCore(t, OverflowFixed, true)
342+ w.SetFullscreen(2, true)
343+ w.AddDialog(9, 2, 10, 5)
344+ w.FocusID(cover)
345+ w.Activate(9)
346+ if id, _ := w.Focused(); id != 9 || w.fullscreen != 2 || w.stashHidden || w.left != cover {
347+ t.Fatalf("focused %d fullscreen %d hidden %v left %d", id, w.fullscreen, w.stashHidden, w.left)
348+ }
349+ if p := previewOf(t, w.Layout(), 9); p.Hidden {
350+ t.Fatalf("dialog hidden: %+v", p)
351+ }
352+ })
353+}
354+
286355 // A dialog parent loop under a cover ends: neither dialog shows over it.
287356 func TestOverCoverDialogLoop(t *testing.T) {
288357 _, w, _ := stashCoverCore(t, OverflowFixed, true)
@@ -228,6 +228,12 @@ func TestActivateOverCover(t *testing.T) {
228 t.Run(cv.name+"/"+tc.name, func(t *testing.T) {228 t.Run(cv.name+"/"+tc.name, func(t *testing.T) {
229 _, w, cover := stashCoverCore(t, cv.overflow, cv.float)229 _, w, cover := stashCoverCore(t, cv.overflow, cv.float)
230 id := tc.setup(w, cover)230 id := tc.setup(w, cover)
231+ // A dialog of the hidden stash waits hidden under the cover.
232+ if i := slices.IndexFunc(w.Floats, func(f Float) bool { return f.over == 2 }); i >= 0 && !w.stashOverCover() {
233+ if p := previewOf(t, w.Layout(), w.Floats[i].ID); !p.Hidden {
234+ t.Fatalf("dialog of the hidden stash shown before activation: %+v", p)
235+ }
236+ }
231 view := w.View237 view := w.View
232 w.Activate(id)238 w.Activate(id)
233 if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over {239 if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over {
@@ -237,9 +243,13 @@ func TestActivateOverCover(t *testing.T) {
237 if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen {243 if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen {
238 t.Fatalf("cover %+v", p)244 t.Fatalf("cover %+v", p)
239 }245 }
240- if f, c := slices.IndexFunc(ps, isID(tc.focused)), slices.IndexFunc(ps, isID(cover)); f < c || ps[f].Hidden {246+ f := slices.IndexFunc(ps, isID(tc.focused))
247+ if f < slices.IndexFunc(ps, isID(cover)) || ps[f].Hidden {
241 t.Fatalf("focused window not over the cover: %+v", ps)248 t.Fatalf("focused window not over the cover: %+v", ps)
242 }249 }
250+ if tc.over && f < slices.IndexFunc(ps, isID(2)) {
251+ t.Fatalf("focused window under the stash: %+v", ps)
252+ }
243 if !cv.float && w.View != view {253 if !cv.float && w.View != view {
244 t.Fatalf("view %d, was %d", w.View, view)254 t.Fatalf("view %d, was %d", w.View, view)
245 }255 }
@@ -249,40 +259,99 @@ func TestActivateOverCover(t *testing.T) {
249 }259 }
250 260
251 // After an activation shows the stash over the cover, hiding the stash261 // After an activation shows the stash over the cover, hiding the stash
252-// or activating the cover returns to it, and a dialog of a stashed262+// or activating the cover returns to it; a dialog of a stashed window,
253-// window mapping meanwhile shows with the focus.263+// modal or mapping meanwhile, shows with the focus and closing it
264+// returns to the stash over the cover.
254 func TestActivateOverCoverReturns(t *testing.T) {265 func TestActivateOverCoverReturns(t *testing.T) {
255- _, w, cover := stashCoverCore(t, OverflowFixed, true)266+ for _, cv := range []struct {
256- w.Activate(2)267+ name string
257- w.ToggleStashVisible()268+ overflow Overflow
258- if id, _ := w.Focused(); id != cover || w.stashShown() {269+ float bool
259- t.Fatalf("hide: focused %d shown %v", id, w.stashShown())270+ }{
260- }271+ {"fixed tile", OverflowFixed, false},
261- w.Activate(2)272+ {"scroll tile", OverflowScroll, false},
262- w.Activate(cover)273+ {"fixed float", OverflowFixed, true},
263- if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {274+ {"scroll float", OverflowScroll, true},
264- t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)275+ } {
265- }276+ t.Run(cv.name, func(t *testing.T) {
266- w.Activate(2)277+ _, w, cover := stashCoverCore(t, cv.overflow, cv.float)
267- w.AddDialog(9, 2, 20, 10)278+ w.Activate(2)
268- w.AddDialog(8, 9, 10, 5)279+ w.ToggleStashVisible()
269- if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden {280+ if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
270- t.Fatalf("mapped chain: focused %d", id)281+ t.Fatalf("hide: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
271- }282+ }
272- // Closing the dialogs returns to the stash over the cover.283+ w.Activate(2)
273- w.RemoveWindow(8)284+ w.Activate(cover)
274- w.RemoveWindow(9)285+ if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
275- if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {286+ t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
276- t.Fatalf("closed: focused %d over %v", id, w.stashOverCover())287+ }
277- }288+ w.AddDialog(7, 2, 20, 10)
278- // The cover closes: the stash stays shown and focused, the override289+ w.SetModal(7, true)
279- // ends with it.290+ w.FocusID(cover)
280- w.RemoveWindow(cover)291+ w.Activate(2)
281- if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() {292+ w.RemoveWindow(7)
282- t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown())293+ if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
294+ t.Fatalf("modal closed: focused %d over %v", id, w.stashOverCover())
295+ }
296+ w.AddDialog(9, 2, 20, 10)
297+ w.AddDialog(8, 9, 10, 5)
298+ if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden {
299+ t.Fatalf("mapped chain: focused %d", id)
300+ }
301+ w.RemoveWindow(8)
302+ w.RemoveWindow(9)
303+ if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
304+ t.Fatalf("closed: focused %d over %v", id, w.stashOverCover())
305+ }
306+ // The cover closes: the stash stays shown and focused, the
307+ // override ends with it.
308+ w.RemoveWindow(cover)
309+ if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() {
310+ t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown())
311+ }
312+ })
283 }313 }
284 }314 }
285 315
316+// Activating a dialog that cannot show over the cover leaves fullscreen
317+// as any other window does; a dialog of a stashed window whose own
318+// fullscreen waited while hidden brings that window fullscreen, as
319+// activating the window itself does.
320+func TestActivateDialogLeavesCover(t *testing.T) {
321+ t.Run("dialog of a plain float", func(t *testing.T) {
322+ _, w, cover := stashCoverCore(t, OverflowFixed, false)
323+ w.AddFloating(5, 20, 10)
324+ w.AddDialog(9, 5, 10, 5)
325+ w.FocusID(cover)
326+ w.Activate(9)
327+ if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover {
328+ t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left)
329+ }
330+ })
331+ t.Run("dialog of another tile", func(t *testing.T) {
332+ _, w, cover := stashCoverCore(t, OverflowFixed, true)
333+ w.AddDialog(9, 1, 10, 5)
334+ w.FocusID(cover)
335+ w.Activate(9)
336+ if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover {
337+ t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left)
338+ }
339+ })
340+ t.Run("dialog of a stashed window with a pending fullscreen", func(t *testing.T) {
341+ _, w, cover := stashCoverCore(t, OverflowFixed, true)
342+ w.SetFullscreen(2, true)
343+ w.AddDialog(9, 2, 10, 5)
344+ w.FocusID(cover)
345+ w.Activate(9)
346+ if id, _ := w.Focused(); id != 9 || w.fullscreen != 2 || w.stashHidden || w.left != cover {
347+ t.Fatalf("focused %d fullscreen %d hidden %v left %d", id, w.fullscreen, w.stashHidden, w.left)
348+ }
349+ if p := previewOf(t, w.Layout(), 9); p.Hidden {
350+ t.Fatalf("dialog hidden: %+v", p)
351+ }
352+ })
353+}
354+
286 // A dialog parent loop under a cover ends: neither dialog shows over it.355 // A dialog parent loop under a cover ends: neither dialog shows over it.
287 func TestOverCoverDialogLoop(t *testing.T) {356 func TestOverCoverDialogLoop(t *testing.T) {
288 _, w, _ := stashCoverCore(t, OverflowFixed, true)357 _, w, _ := stashCoverCore(t, OverflowFixed, true)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close