detached 9ff3433

feat(core): hide the shown stash on a four-finger swipe up

commit
9ff34338ebf488140d6b0d05c1c5756837a75d56
author
brice · · 2026-10-05 03:55 UTC
committer
brice · · 2026-10-05 03:55 UTC
parent
03ba80b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +97 -7
M internal/core/gesture.go +8 -7
@@ -36,8 +36,9 @@ const (
3636 swipeWorkspaces
3737 swipeDiscrete
3838 // swipeOverview is a vertical four-finger swipe: up opens the
39- // overview, down closes it on the selection (niri's gesture); down
40- // with the overview closed shows or hides the stash.
39+ // overview, down closes it on the selection (niri's gesture). With
40+ // the overview closed, down shows or hides the stash, and up hides
41+ // it when it is shown, else opens the overview.
4142 swipeOverview
4243 // swipeDropped ignores the rest of a swipe whose workspace changed.
4344 swipeDropped
@@ -255,7 +256,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
255256 if step == 0 {
256257 return false
257258 }
258- if step > 0 && !m.ov.open {
259+ if !m.ov.open && (step > 0 || m.Current().stashShown()) {
259260 return c.swipeStash(g, now)
260261 }
261262 if (step < 0) == m.ov.open {
@@ -301,10 +302,10 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
301302 return shown
302303 }
303304
304-// swipeStash runs toggle-stash-visible for a four-finger swipe down with the
305-// overview closed, on the swipe's screen unless the pointer took the focus
306-// to another output meanwhile. It reports whether the stash showed or
307-// hid: an empty stash stays as it is.
305+// swipeStash runs toggle-stash-visible for a four-finger swipe down, or up
306+// while the stash is shown, with the overview closed, on the swipe's screen
307+// unless the pointer took the focus to another output meanwhile. It reports
308+// whether the stash showed or hid: an empty stash stays as it is.
308309 func (c *Core) swipeStash(g *swipeGesture, now time.Time) bool {
309310 if c.cur() != g.screen {
310311 return false
@@ -36,8 +36,9 @@ const (
36 swipeWorkspaces36 swipeWorkspaces
37 swipeDiscrete37 swipeDiscrete
38 // swipeOverview is a vertical four-finger swipe: up opens the38 // swipeOverview is a vertical four-finger swipe: up opens the
39- // overview, down closes it on the selection (niri's gesture); down39+ // overview, down closes it on the selection (niri's gesture). With
40- // with the overview closed shows or hides the stash.40+ // the overview closed, down shows or hides the stash, and up hides
41+ // it when it is shown, else opens the overview.
41 swipeOverview42 swipeOverview
42 // swipeDropped ignores the rest of a swipe whose workspace changed.43 // swipeDropped ignores the rest of a swipe whose workspace changed.
43 swipeDropped44 swipeDropped
@@ -255,7 +256,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
255 if step == 0 {256 if step == 0 {
256 return false257 return false
257 }258 }
258- if step > 0 && !m.ov.open {259+ if !m.ov.open && (step > 0 || m.Current().stashShown()) {
259 return c.swipeStash(g, now)260 return c.swipeStash(g, now)
260 }261 }
261 if (step < 0) == m.ov.open {262 if (step < 0) == m.ov.open {
@@ -301,10 +302,10 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
301 return shown302 return shown
302 }303 }
303 304
304-// swipeStash runs toggle-stash-visible for a four-finger swipe down with the305+// swipeStash runs toggle-stash-visible for a four-finger swipe down, or up
305-// overview closed, on the swipe's screen unless the pointer took the focus306+// while the stash is shown, with the overview closed, on the swipe's screen
306-// to another output meanwhile. It reports whether the stash showed or307+// unless the pointer took the focus to another output meanwhile. It reports
307-// hid: an empty stash stays as it is.308+// whether the stash showed or hid: an empty stash stays as it is.
308 func (c *Core) swipeStash(g *swipeGesture, now time.Time) bool {309 func (c *Core) swipeStash(g *swipeGesture, now time.Time) bool {
309 if c.cur() != g.screen {310 if c.cur() != g.screen {
310 return false311 return false
M internal/core/stash.go +6 -0
@@ -191,6 +191,12 @@ func (w *Workspace) stashOverCover() bool {
191191 return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0
192192 }
193193
194+// stashShown reports whether the stash is on screen: it holds windows, is
195+// not hidden, and no covering fullscreen window hides it.
196+func (w *Workspace) stashShown() bool {
197+ return len(w.Stash) > 0 && !w.stashHidden && (w.cover() == 0 || w.stashOverCover())
198+}
199+
194200 // dropStashOver ends the stash override when the focus leaves it: the
195201 // stash hides again under the covering window, as it was.
196202 func (w *Workspace) dropStashOver() {
@@ -191,6 +191,12 @@ func (w *Workspace) stashOverCover() bool {
191 return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0191 return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0
192 }192 }
193 193
194+// stashShown reports whether the stash is on screen: it holds windows, is
195+// not hidden, and no covering fullscreen window hides it.
196+func (w *Workspace) stashShown() bool {
197+ return len(w.Stash) > 0 && !w.stashHidden && (w.cover() == 0 || w.stashOverCover())
198+}
199+
194 // dropStashOver ends the stash override when the focus leaves it: the200 // dropStashOver ends the stash override when the focus leaves it: the
195 // stash hides again under the covering window, as it was.201 // stash hides again under the covering window, as it was.
196 func (w *Workspace) dropStashOver() {202 func (w *Workspace) dropStashOver() {
M internal/core/swipe_test.go +83 -0
@@ -723,6 +723,89 @@ func TestFourFingerSwipeDownTogglesStash(t *testing.T) {
723723 }
724724 }
725725
726+// A four-finger swipe up hides the shown stash, with the same fade as a
727+// swipe down, instead of opening the overview.
728+func TestFourFingerSwipeUpHidesShownStash(t *testing.T) {
729+ r := startSwipe(t, nil)
730+ stashedThird(t, r)
731+ s := r.four(t, 0, -40, 10)
732+ for _, w := range s.Windows {
733+ if w.Preview > 0 {
734+ t.Fatal("swipe up opened the overview with the stash shown")
735+ }
736+ }
737+ if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) {
738+ t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade)
739+ }
740+ s = r.settle(t)
741+ if w := windowOf(t, s, 3); !w.Hidden {
742+ t.Fatalf("swipe up left the stash shown: %+v", w)
743+ }
744+ for _, w := range s.Windows {
745+ if w.Preview > 0 {
746+ t.Fatal("overview open after the stash hid")
747+ }
748+ }
749+}
750+
751+// With the stash hidden or empty a swipe up opens the overview.
752+func TestFourFingerSwipeUpOpensOverviewWithoutShownStash(t *testing.T) {
753+ inOverview := func(s ports.Scene) bool {
754+ for _, w := range s.Windows {
755+ if w.Preview > 0 {
756+ return true
757+ }
758+ }
759+ return false
760+ }
761+ for _, name := range []string{"hidden", "empty"} {
762+ r := startSwipe(t, nil)
763+ if name == "hidden" {
764+ stashedThird(t, r)
765+ r.key(t, "s", ports.ModAlt)
766+ r.settle(t)
767+ } else {
768+ threeColumns(t, r)
769+ }
770+ r.four(t, 0, -40, 10)
771+ if !inOverview(r.settle(t)) {
772+ t.Fatalf("%s stash: swipe up did not open the overview", name)
773+ }
774+ }
775+}
776+
777+// The stash shown over a covering fullscreen window is on screen: a swipe
778+// up hides it and leaves the window fullscreen.
779+func TestFourFingerSwipeUpHidesStashOverCover(t *testing.T) {
780+ r := startSwipe(t, nil)
781+ stashedThird(t, r)
782+ r.key(t, "s", ports.ModAlt) // hide
783+ r.settle(t)
784+ r.key(t, "f", ports.ModAlt|ports.ModShift)
785+ r.settleAll(t, r.outs...)
786+ shown := r.key(t, "s", ports.ModAlt)[0] // show over the cover
787+ sc, _ := r.settleAll(t, r.outs...)
788+ if len(sc) == 0 {
789+ sc = []ports.Scene{shown}
790+ }
791+ if w := windowOf(t, sc[0], 3); w.Hidden {
792+ t.Fatalf("setup: the stash is hidden over the cover: %+v", w)
793+ }
794+ s := r.four(t, 0, -40, 10)
795+ for _, w := range s.Windows {
796+ if w.Preview > 0 {
797+ t.Fatal("swipe up opened the overview with the stash over the cover")
798+ }
799+ }
800+ // Over a cover the stash hides without a fade: the scene is final.
801+ if w := windowOf(t, s, 3); !w.Hidden {
802+ t.Fatalf("swipe up left the stash over the cover: %+v", w)
803+ }
804+ if w := windowOf(t, s, 2); !w.Fullscreen {
805+ t.Fatalf("the covering window left fullscreen: %+v", w)
806+ }
807+}
808+
726809 // With nothing in the stash a swipe down changes nothing: the scene is the
727810 // same one, with no new Seq and no animation.
728811 func TestFourFingerSwipeDownEmptyStash(t *testing.T) {
@@ -723,6 +723,89 @@ func TestFourFingerSwipeDownTogglesStash(t *testing.T) {
723 }723 }
724 }724 }
725 725
726+// A four-finger swipe up hides the shown stash, with the same fade as a
727+// swipe down, instead of opening the overview.
728+func TestFourFingerSwipeUpHidesShownStash(t *testing.T) {
729+ r := startSwipe(t, nil)
730+ stashedThird(t, r)
731+ s := r.four(t, 0, -40, 10)
732+ for _, w := range s.Windows {
733+ if w.Preview > 0 {
734+ t.Fatal("swipe up opened the overview with the stash shown")
735+ }
736+ }
737+ if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) {
738+ t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade)
739+ }
740+ s = r.settle(t)
741+ if w := windowOf(t, s, 3); !w.Hidden {
742+ t.Fatalf("swipe up left the stash shown: %+v", w)
743+ }
744+ for _, w := range s.Windows {
745+ if w.Preview > 0 {
746+ t.Fatal("overview open after the stash hid")
747+ }
748+ }
749+}
750+
751+// With the stash hidden or empty a swipe up opens the overview.
752+func TestFourFingerSwipeUpOpensOverviewWithoutShownStash(t *testing.T) {
753+ inOverview := func(s ports.Scene) bool {
754+ for _, w := range s.Windows {
755+ if w.Preview > 0 {
756+ return true
757+ }
758+ }
759+ return false
760+ }
761+ for _, name := range []string{"hidden", "empty"} {
762+ r := startSwipe(t, nil)
763+ if name == "hidden" {
764+ stashedThird(t, r)
765+ r.key(t, "s", ports.ModAlt)
766+ r.settle(t)
767+ } else {
768+ threeColumns(t, r)
769+ }
770+ r.four(t, 0, -40, 10)
771+ if !inOverview(r.settle(t)) {
772+ t.Fatalf("%s stash: swipe up did not open the overview", name)
773+ }
774+ }
775+}
776+
777+// The stash shown over a covering fullscreen window is on screen: a swipe
778+// up hides it and leaves the window fullscreen.
779+func TestFourFingerSwipeUpHidesStashOverCover(t *testing.T) {
780+ r := startSwipe(t, nil)
781+ stashedThird(t, r)
782+ r.key(t, "s", ports.ModAlt) // hide
783+ r.settle(t)
784+ r.key(t, "f", ports.ModAlt|ports.ModShift)
785+ r.settleAll(t, r.outs...)
786+ shown := r.key(t, "s", ports.ModAlt)[0] // show over the cover
787+ sc, _ := r.settleAll(t, r.outs...)
788+ if len(sc) == 0 {
789+ sc = []ports.Scene{shown}
790+ }
791+ if w := windowOf(t, sc[0], 3); w.Hidden {
792+ t.Fatalf("setup: the stash is hidden over the cover: %+v", w)
793+ }
794+ s := r.four(t, 0, -40, 10)
795+ for _, w := range s.Windows {
796+ if w.Preview > 0 {
797+ t.Fatal("swipe up opened the overview with the stash over the cover")
798+ }
799+ }
800+ // Over a cover the stash hides without a fade: the scene is final.
801+ if w := windowOf(t, s, 3); !w.Hidden {
802+ t.Fatalf("swipe up left the stash over the cover: %+v", w)
803+ }
804+ if w := windowOf(t, s, 2); !w.Fullscreen {
805+ t.Fatalf("the covering window left fullscreen: %+v", w)
806+ }
807+}
808+
726 // With nothing in the stash a swipe down changes nothing: the scene is the809 // With nothing in the stash a swipe down changes nothing: the scene is the
727 // same one, with no new Seq and no animation.810 // same one, with no new Seq and no animation.
728 func TestFourFingerSwipeDownEmptyStash(t *testing.T) {811 func TestFourFingerSwipeDownEmptyStash(t *testing.T) {

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