detached 9f23317

fix(core): apply taskbar fullscreen requests during the map grace

Foreign-toplevel requests come from the user, not the window: WindowFullscreenRequest.External skips fullscreenGrace. The map test now waits for the dialog in the scene.
commit
9f2331720e1c1ad05b24d3e0bae5a8e2fbfb2d5b
author
brice · · 2026-09-28 15:37 UTC
parent
f94690a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +39 -6
M internal/adapters/wayland/foreigntoplevel.go +2 -2
@@ -246,11 +246,11 @@ func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *w
246246 }
247247 func (t *toplevelRequests) SetFullscreen(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Output) {
248248 if w := t.window(); w != nil {
249- t.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true})
249+ t.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true, External: true})
250250 }
251251 }
252252 func (t *toplevelRequests) UnsetFullscreen(*wlr.ZwlrForeignToplevelHandleV1) {
253253 if w := t.window(); w != nil {
254- t.s.emit(ports.WindowFullscreenRequest{ID: w.id})
254+ t.s.emit(ports.WindowFullscreenRequest{ID: w.id, External: true})
255255 }
256256 }
@@ -246,11 +246,11 @@ func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *w
246 }246 }
247 func (t *toplevelRequests) SetFullscreen(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Output) {247 func (t *toplevelRequests) SetFullscreen(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Output) {
248 if w := t.window(); w != nil {248 if w := t.window(); w != nil {
249- t.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true})249+ t.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true, External: true})
250 }250 }
251 }251 }
252 func (t *toplevelRequests) UnsetFullscreen(*wlr.ZwlrForeignToplevelHandleV1) {252 func (t *toplevelRequests) UnsetFullscreen(*wlr.ZwlrForeignToplevelHandleV1) {
253 if w := t.window(); w != nil {253 if w := t.window(); w != nil {
254- t.s.emit(ports.WindowFullscreenRequest{ID: w.id})254+ t.s.emit(ports.WindowFullscreenRequest{ID: w.id, External: true})
255 }255 }
256 }256 }
M internal/adapters/wayland/foreigntoplevel_test.go +2 -2
@@ -129,11 +129,11 @@ func TestForeignToplevel(t *testing.T) {
129129
130130 // Requests reach core as the xdg-shell and xdg-activation messages.
131131 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestSetFullscreen, uint32(0))
132- if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID, Fullscreen: true}) {
132+ if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID, Fullscreen: true, External: true}) {
133133 t.Fatalf("set_fullscreen = %#v", ev)
134134 }
135135 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestUnsetFullscreen)
136- if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID}) {
136+ if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID, External: true}) {
137137 t.Fatalf("unset_fullscreen = %#v", ev)
138138 }
139139 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestActivate, seat)
@@ -129,11 +129,11 @@ func TestForeignToplevel(t *testing.T) {
129 129
130 // Requests reach core as the xdg-shell and xdg-activation messages.130 // Requests reach core as the xdg-shell and xdg-activation messages.
131 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestSetFullscreen, uint32(0))131 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestSetFullscreen, uint32(0))
132- if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID, Fullscreen: true}) {132+ if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID, Fullscreen: true, External: true}) {
133 t.Fatalf("set_fullscreen = %#v", ev)133 t.Fatalf("set_fullscreen = %#v", ev)
134 }134 }
135 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestUnsetFullscreen)135 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestUnsetFullscreen)
136- if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID}) {136+ if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID, External: true}) {
137 t.Fatalf("unset_fullscreen = %#v", ev)137 t.Fatalf("unset_fullscreen = %#v", ev)
138 }138 }
139 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestActivate, seat)139 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestActivate, seat)
M internal/core/core.go +2 -2
@@ -51,7 +51,7 @@ type Channels struct {
5151 // is ignored. Wine asks for fullscreen whenever a window has the monitor's
5252 // size, so a launcher that remembered a fullscreen size would open
5353 // fullscreen and hide the windows it opens next. The user can still
54-// fullscreen it, and an app can ask again later.
54+// fullscreen it (bind or taskbar), and an app can ask again later.
5555 const fullscreenGrace = time.Second
5656 type binding struct {
5757 mods ports.Mods
@@ -788,7 +788,7 @@ func (c *Core) Run(ctx context.Context) error {
788788 case ports.PointerConstrained:
789789 c.constrained = v
790790 case ports.WindowFullscreenRequest:
791- if v.Fullscreen && c.now().Sub(c.mappedAt[v.ID]) < fullscreenGrace {
791+ if v.Fullscreen && !v.External && c.now().Sub(c.mappedAt[v.ID]) < fullscreenGrace {
792792 continue
793793 }
794794 if s, _ := c.screenOf(v.ID); s != nil {
@@ -51,7 +51,7 @@ type Channels struct {
51 // is ignored. Wine asks for fullscreen whenever a window has the monitor's51 // is ignored. Wine asks for fullscreen whenever a window has the monitor's
52 // size, so a launcher that remembered a fullscreen size would open52 // size, so a launcher that remembered a fullscreen size would open
53 // fullscreen and hide the windows it opens next. The user can still53 // fullscreen and hide the windows it opens next. The user can still
54-// fullscreen it, and an app can ask again later.54+// fullscreen it (bind or taskbar), and an app can ask again later.
55 const fullscreenGrace = time.Second55 const fullscreenGrace = time.Second
56 type binding struct {56 type binding struct {
57 mods ports.Mods57 mods ports.Mods
@@ -788,7 +788,7 @@ func (c *Core) Run(ctx context.Context) error {
788 case ports.PointerConstrained:788 case ports.PointerConstrained:
789 c.constrained = v789 c.constrained = v
790 case ports.WindowFullscreenRequest:790 case ports.WindowFullscreenRequest:
791- if v.Fullscreen && c.now().Sub(c.mappedAt[v.ID]) < fullscreenGrace {791+ if v.Fullscreen && !v.External && c.now().Sub(c.mappedAt[v.ID]) < fullscreenGrace {
792 continue792 continue
793 }793 }
794 if s, _ := c.screenOf(v.ID); s != nil {794 if s, _ := c.screenOf(v.ID); s != nil {
M internal/core/core_test.go +30 -0
@@ -774,6 +774,9 @@ func TestFullscreenAtMapIgnored(t *testing.T) {
774774 client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}
775775 client <- ports.WindowMapped{ID: 3, Floating: true, Width: 20, Height: 10}
776776 s := scene(t, scenes)
777+ for !slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == 3 }) {
778+ s = scene(t, scenes)
779+ }
777780 for _, w := range s.Windows {
778781 if w.Fullscreen || w.Hidden {
779782 t.Fatalf("startup fullscreen applied: %+v", s.Windows)
@@ -789,6 +792,33 @@ func TestFullscreenAtMapIgnored(t *testing.T) {
789792 }
790793 }
791794
795+// A taskbar's fullscreen request is the user's: it applies at once.
796+func TestExternalFullscreenAtMapApplies(t *testing.T) {
797+ cfg := config.Defaults()
798+ client := make(chan ports.ClientEvent, 8)
799+ output := make(chan ports.OutputEvent, 8)
800+ commands := make(chan ports.ClientCommand, 64)
801+ scenes := make(chan []ports.Scene, 1)
802+ clock := portsmocks.NewMockClock(t)
803+ clock.EXPECT().Now().Return(time.Unix(0, 0))
804+ c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: clock})
805+ if err != nil {
806+ t.Fatal(err)
807+ }
808+ ctx, cancel := context.WithCancel(context.Background())
809+ defer cancel()
810+ go c.Run(ctx)
811+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
812+ scene(t, scenes)
813+ client <- ports.WindowMapped{ID: 1}
814+ scene(t, scenes)
815+ client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true, External: true}
816+ s := scene(t, scenes)
817+ if len(s.Windows) != 1 || !s.Windows[0].Fullscreen {
818+ t.Fatalf("taskbar fullscreen ignored: %+v", s.Windows)
819+ }
820+}
821+
792822 // An activated window on another workspace comes on screen with focus.
793823 func TestWindowActivateShowsAndFocuses(t *testing.T) {
794824 cfg := config.Defaults()
@@ -774,6 +774,9 @@ func TestFullscreenAtMapIgnored(t *testing.T) {
774 client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}774 client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}
775 client <- ports.WindowMapped{ID: 3, Floating: true, Width: 20, Height: 10}775 client <- ports.WindowMapped{ID: 3, Floating: true, Width: 20, Height: 10}
776 s := scene(t, scenes)776 s := scene(t, scenes)
777+ for !slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == 3 }) {
778+ s = scene(t, scenes)
779+ }
777 for _, w := range s.Windows {780 for _, w := range s.Windows {
778 if w.Fullscreen || w.Hidden {781 if w.Fullscreen || w.Hidden {
779 t.Fatalf("startup fullscreen applied: %+v", s.Windows)782 t.Fatalf("startup fullscreen applied: %+v", s.Windows)
@@ -789,6 +792,33 @@ func TestFullscreenAtMapIgnored(t *testing.T) {
789 }792 }
790 }793 }
791 794
795+// A taskbar's fullscreen request is the user's: it applies at once.
796+func TestExternalFullscreenAtMapApplies(t *testing.T) {
797+ cfg := config.Defaults()
798+ client := make(chan ports.ClientEvent, 8)
799+ output := make(chan ports.OutputEvent, 8)
800+ commands := make(chan ports.ClientCommand, 64)
801+ scenes := make(chan []ports.Scene, 1)
802+ clock := portsmocks.NewMockClock(t)
803+ clock.EXPECT().Now().Return(time.Unix(0, 0))
804+ c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: clock})
805+ if err != nil {
806+ t.Fatal(err)
807+ }
808+ ctx, cancel := context.WithCancel(context.Background())
809+ defer cancel()
810+ go c.Run(ctx)
811+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
812+ scene(t, scenes)
813+ client <- ports.WindowMapped{ID: 1}
814+ scene(t, scenes)
815+ client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true, External: true}
816+ s := scene(t, scenes)
817+ if len(s.Windows) != 1 || !s.Windows[0].Fullscreen {
818+ t.Fatalf("taskbar fullscreen ignored: %+v", s.Windows)
819+ }
820+}
821+
792 // An activated window on another workspace comes on screen with focus.822 // An activated window on another workspace comes on screen with focus.
793 func TestWindowActivateShowsAndFocuses(t *testing.T) {823 func TestWindowActivateShowsAndFocuses(t *testing.T) {
794 cfg := config.Defaults()824 cfg := config.Defaults()
M internal/ports/messages.go +3 -0
@@ -94,9 +94,12 @@ type OutputPower struct {
9494 func (OutputPower) clientEvent() {}
9595
9696 // WindowFullscreenRequest carries wayland → core fullscreen requests.
97+// External is set when another client (a taskbar, through foreign
98+// toplevel) asks for it, not the window itself.
9799 type WindowFullscreenRequest struct {
98100 ID WindowID
99101 Fullscreen bool
102+ External bool
100103 }
101104
102105 func (WindowFullscreenRequest) clientEvent() {}
@@ -94,9 +94,12 @@ type OutputPower struct {
94 func (OutputPower) clientEvent() {}94 func (OutputPower) clientEvent() {}
95 95
96 // WindowFullscreenRequest carries wayland → core fullscreen requests.96 // WindowFullscreenRequest carries wayland → core fullscreen requests.
97+// External is set when another client (a taskbar, through foreign
98+// toplevel) asks for it, not the window itself.
97 type WindowFullscreenRequest struct {99 type WindowFullscreenRequest struct {
98 ID WindowID100 ID WindowID
99 Fullscreen bool101 Fullscreen bool
102+ External bool
100 }103 }
101 104
102 func (WindowFullscreenRequest) clientEvent() {}105 func (WindowFullscreenRequest) clientEvent() {}

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