detached 3adce45

fix(core): tile windows opened over a fixed fullscreen on the home workspace (#62)

* fix(core): tile windows opened over a fixed fullscreen on the home workspace

Under fixed overflow a new tiled window (normal, targeted terminal or slot)
opening while a fullscreen window holds its workspace was hidden forever.
It now tiles on the home workspace; a fullscreen window still in place moves
to its own workspace first. The view and keyboard stay on the fullscreen
window; an xdg-activation shows the new window.

* test(core): cover slot, named workspace and float arrivals over a fixed fullscreen
commit
3adce452eb0e37e13ba01b17633e4bdbe18fa0c8
author
bnema · · 2026-09-29 10:22 UTC
committer
GitHub · · 2026-09-29 10:22 UTC
parent
6648be1
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
5 files · +140 -18
M docs/config.md +1 -1
@@ -106,7 +106,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
106106 | `spawn <command>` | Run a program |
107107 | `spawn-terminal` | Run `terminal` |
108108 | `close-window` | Close the focused window |
109-| `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, dialogs included, wait hidden until it leaves; activating one of them leaves fullscreen |
109+| `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, dialogs included, wait hidden until it leaves; activating one of them leaves fullscreen. In fixed overflow, the window gets its own workspace below its home one; a window opening while it is on screen tiles on the home workspace and the view stays on the fullscreen window; activating it shows the home workspace |
110110 | `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 or expanded state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first |
111111 | `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. Does nothing under a fullscreen window |
112112 | `toggle-overview` | Open the [overview](#overview), or close it on the selected window |
@@ -106,7 +106,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
106 | `spawn <command>` | Run a program |106 | `spawn <command>` | Run a program |
107 | `spawn-terminal` | Run `terminal` |107 | `spawn-terminal` | Run `terminal` |
108 | `close-window` | Close the focused window |108 | `close-window` | Close the focused window |
109-| `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, dialogs included, wait hidden until it leaves; activating one of them leaves fullscreen |109+| `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, dialogs included, wait hidden until it leaves; activating one of them leaves fullscreen. In fixed overflow, the window gets its own workspace below its home one; a window opening while it is on screen tiles on the home workspace and the view stays on the fullscreen window; activating it shows the home workspace |
110 | `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 or expanded state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first |110 | `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 or expanded state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first |
111 | `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. Does nothing under a fullscreen window |111 | `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. Does nothing under a fullscreen window |
112 | `toggle-overview` | Open the [overview](#overview), or close it on the selected window |112 | `toggle-overview` | Open the [overview](#overview), or close it on the selected window |
M internal/core/core_test.go +32 -0
@@ -3,6 +3,7 @@ package core_test
33 import (
44 "context"
55 "errors"
6+ "reflect"
67 "slices"
78 "sync/atomic"
89 "testing"
@@ -845,6 +846,37 @@ func TestFullscreenAtMapIgnored(t *testing.T) {
845846 }
846847 }
847848
849+// Under fixed overflow, a window opening over a fullscreen game tiles on
850+// its home workspace: the game keeps the screen and the keyboard, and an
851+// activation (a user action in the new client) shows the window there.
852+func TestFixedFullscreenArrivalTilesAtHome(t *testing.T) {
853+ r := startMulti(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" }, left)
854+ r.mapWindow(t, 1)
855+ r.key(t, "f", ports.ModAlt|ports.ModShift)
856+ set := r.mapWindow(t, 2)
857+ for len(r.commands) > 0 {
858+ <-r.commands
859+ }
860+ if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{1}) || focused != 1 {
861+ t.Fatal("game disturbed:", got, focused)
862+ }
863+ r.client <- ports.WindowActivate{ID: 2}
864+ set = receive(t, r.scenes)
865+ if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{2}) || focused != 2 {
866+ t.Fatal("after activate:", got, focused)
867+ }
868+ for {
869+ if f, ok := command(t, r.commands).(ports.FocusWindow); ok && f.ID == 2 {
870+ break
871+ }
872+ }
873+ // The game still waits fullscreen one workspace down.
874+ set = r.key(t, "Next", ports.ModAlt)
875+ if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{1}) || focused != 1 {
876+ t.Fatal("game workspace:", got, focused)
877+ }
878+}
879+
848880 // A taskbar's fullscreen request is the user's: it applies at once.
849881 func TestExternalFullscreenAtMapApplies(t *testing.T) {
850882 cfg := config.Defaults()
@@ -3,6 +3,7 @@ package core_test
3 import (3 import (
4 "context"4 "context"
5 "errors"5 "errors"
6+ "reflect"
6 "slices"7 "slices"
7 "sync/atomic"8 "sync/atomic"
8 "testing"9 "testing"
@@ -845,6 +846,37 @@ func TestFullscreenAtMapIgnored(t *testing.T) {
845 }846 }
846 }847 }
847 848
849+// Under fixed overflow, a window opening over a fullscreen game tiles on
850+// its home workspace: the game keeps the screen and the keyboard, and an
851+// activation (a user action in the new client) shows the window there.
852+func TestFixedFullscreenArrivalTilesAtHome(t *testing.T) {
853+ r := startMulti(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" }, left)
854+ r.mapWindow(t, 1)
855+ r.key(t, "f", ports.ModAlt|ports.ModShift)
856+ set := r.mapWindow(t, 2)
857+ for len(r.commands) > 0 {
858+ <-r.commands
859+ }
860+ if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{1}) || focused != 1 {
861+ t.Fatal("game disturbed:", got, focused)
862+ }
863+ r.client <- ports.WindowActivate{ID: 2}
864+ set = receive(t, r.scenes)
865+ if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{2}) || focused != 2 {
866+ t.Fatal("after activate:", got, focused)
867+ }
868+ for {
869+ if f, ok := command(t, r.commands).(ports.FocusWindow); ok && f.ID == 2 {
870+ break
871+ }
872+ }
873+ // The game still waits fullscreen one workspace down.
874+ set = r.key(t, "Next", ports.ModAlt)
875+ if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{1}) || focused != 1 {
876+ t.Fatal("game workspace:", got, focused)
877+ }
878+}
879+
848 // A taskbar's fullscreen request is the user's: it applies at once.880 // A taskbar's fullscreen request is the user's: it applies at once.
849 func TestExternalFullscreenAtMapApplies(t *testing.T) {881 func TestExternalFullscreenAtMapApplies(t *testing.T) {
850 cfg := config.Defaults()882 cfg := config.Defaults()
M internal/core/monitor.go +20 -8
@@ -260,19 +260,31 @@ func (m *Monitor) find(id WindowID) (*Workspace, int) {
260260 return nil, -1
261261 }
262262
263-// AddWindow places a new window on the workspace on screen. On a
264-// fullscreen workspace it floats, hidden until the fullscreen window
265-// leaves, then tiles after it at home.
263+// AddWindow places a new window on the workspace on screen (see arrive).
266264 func (m *Monitor) AddWindow(id WindowID) {
267265 if w, _ := m.find(id); w != nil {
268266 return
269267 }
270- if cur := m.Current(); cur.origin != nil {
271- cur.joinFullscreen(id)
272- m.normalize()
273- return
268+ m.arrive(m.Current(), func(w *Workspace) { w.AddWindow(id) })
269+}
270+
271+// arrive adds a new tiled window to w with add. Under fixed overflow a
272+// fullscreen window hides every other one of its workspace and keeps the
273+// view (ADR 011), so the new window goes to its home workspace instead:
274+// the origin of a fullscreen workspace, or w itself once a fullscreen
275+// window still in place there has moved to its own workspace.
276+func (m *Monitor) arrive(w *Workspace, add func(*Workspace)) {
277+ switch {
278+ case w.origin != nil:
279+ add(w.origin)
280+ case w.Overflow == OverflowFixed && w.fullscreen != 0 && m.has(w):
281+ // Added first, hidden and unfocused: w is never left empty,
282+ // which normalize would drop.
283+ add(w)
284+ m.enterFullscreen(w, w.fullscreen, m.Current() == w)
285+ default:
286+ add(w)
274287 }
275- m.Current().AddWindow(id)
276288 m.normalize()
277289 }
278290
@@ -260,19 +260,31 @@ func (m *Monitor) find(id WindowID) (*Workspace, int) {
260 return nil, -1260 return nil, -1
261 }261 }
262 262
263-// AddWindow places a new window on the workspace on screen. On a263+// AddWindow places a new window on the workspace on screen (see arrive).
264-// fullscreen workspace it floats, hidden until the fullscreen window
265-// leaves, then tiles after it at home.
266 func (m *Monitor) AddWindow(id WindowID) {264 func (m *Monitor) AddWindow(id WindowID) {
267 if w, _ := m.find(id); w != nil {265 if w, _ := m.find(id); w != nil {
268 return266 return
269 }267 }
270- if cur := m.Current(); cur.origin != nil {268+ m.arrive(m.Current(), func(w *Workspace) { w.AddWindow(id) })
271- cur.joinFullscreen(id)269+}
272- m.normalize()270+
273- return271+// arrive adds a new tiled window to w with add. Under fixed overflow a
272+// fullscreen window hides every other one of its workspace and keeps the
273+// view (ADR 011), so the new window goes to its home workspace instead:
274+// the origin of a fullscreen workspace, or w itself once a fullscreen
275+// window still in place there has moved to its own workspace.
276+func (m *Monitor) arrive(w *Workspace, add func(*Workspace)) {
277+ switch {
278+ case w.origin != nil:
279+ add(w.origin)
280+ case w.Overflow == OverflowFixed && w.fullscreen != 0 && m.has(w):
281+ // Added first, hidden and unfocused: w is never left empty,
282+ // which normalize would drop.
283+ add(w)
284+ m.enterFullscreen(w, w.fullscreen, m.Current() == w)
285+ default:
286+ add(w)
274 }287 }
275- m.Current().AddWindow(id)
276 m.normalize()288 m.normalize()
277 }289 }
278 290
M internal/core/monitor_test.go +85 -5
@@ -523,26 +523,106 @@ func TestMonitorFixedFullscreenEdges(t *testing.T) {
523523 }
524524 return m
525525 }
526- t.Run("a window opened there floats, then tiles after it at home", func(t *testing.T) {
526+ t.Run("a window opened there tiles at home, the view stays", func(t *testing.T) {
527527 m := fixed()
528528 m.Current().FocusID(2)
529529 m.ToggleFullscreen()
530530 m.AddWindow(4)
531- if fs := m.Current(); fs.floatIndex(4) != 0 || fs.fullscreen != 2 {
532- t.Fatal(fs.Floats, fs.fullscreen)
531+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 3, 4}, {2}, {}}) || m.Active != 1 {
532+ t.Fatal(got, m.Active)
533+ }
534+ if id, _ := m.Focused(); id != 2 {
535+ t.Fatal("focus", id)
533536 }
534537 m.SetFullscreen(2, false)
535- if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 4, 3}, {}}) || m.Active != 0 {
538+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3, 4}, {}}) || m.Active != 0 {
536539 t.Fatal(got, m.Active)
537540 }
538541 })
542+ t.Run("a window opened over a lone fullscreen moves it to its own workspace", func(t *testing.T) {
543+ for _, client := range []bool{false, true} {
544+ m := monitor()
545+ m.SetOverflow(OverflowFixed)
546+ m.AddWindow(1)
547+ if client {
548+ m.SetFullscreen(1, true)
549+ } else {
550+ m.ToggleFullscreen()
551+ }
552+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {}}) || m.Current().fullscreen != 1 {
553+ t.Fatal("in place", got)
554+ }
555+ m.AddWindow(2)
556+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{2}, {1}, {}}) || m.Active != 1 {
557+ t.Fatal(client, got, m.Active)
558+ }
559+ if id, _ := m.Focused(); id != 1 || m.Current().cover() != 1 {
560+ t.Fatal("focus", id)
561+ }
562+ m.SetFullscreen(1, false)
563+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {}}) || m.Active != 0 {
564+ t.Fatal(client, got, m.Active)
565+ }
566+ if id, _ := m.Focused(); id != 1 {
567+ t.Fatal("focus after", id)
568+ }
569+ }
570+ })
571+ t.Run("a window opened over a lone fullscreen off screen stays there", func(t *testing.T) {
572+ m := monitor()
573+ m.SetOverflow(OverflowFixed)
574+ m.AddWindow(1)
575+ m.ToggleFullscreen()
576+ m.Focus(1)
577+ m.AddWindow(2)
578+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {2}, {}}) || m.Active != 1 {
579+ t.Fatal(got, m.Active)
580+ }
581+ })
582+ t.Run("arrivals over a lone fullscreen: slot, named workspace, float", func(t *testing.T) {
583+ for _, tc := range []struct {
584+ name string
585+ setup func(m *Monitor) *Workspace
586+ add func(w *Workspace)
587+ }{
588+ {"slot", func(m *Monitor) *Workspace { m.AddWindow(1); return m.Current() },
589+ func(w *Workspace) { w.AddSlotWindow(2, 1, Width{}) }},
590+ {"hidden named", func(m *Monitor) *Workspace {
591+ m.SetNamed([]NamedWorkspace{{Name: "dev", Overflow: OverflowFixed}})
592+ m.Apply("workspace dev")
593+ m.AddWindow(1)
594+ return m.Current()
595+ }, func(w *Workspace) { w.AddWindow(2) }},
596+ {"float", func(m *Monitor) *Workspace { m.AddFloating(1, 10, 10); return m.Current() },
597+ func(w *Workspace) { w.AddWindow(2) }},
598+ } {
599+ m := monitor()
600+ m.SetOverflow(OverflowFixed)
601+ home := tc.setup(m)
602+ m.SetFullscreen(1, true)
603+ m.arrive(home, tc.add)
604+ if fs := m.Current(); fs == home || fs.cover() != 1 || fs.origin != home {
605+ t.Fatal(tc.name, "fullscreen not on its own workspace", windows(m))
606+ }
607+ if id, _ := m.Focused(); id != 1 || !home.has(2) || home.has(1) {
608+ t.Fatal(tc.name, "focus", id, home.windows())
609+ }
610+ m.SetFullscreen(1, false)
611+ if m.Current() != home || !home.has(1) || !home.has(2) {
612+ t.Fatal(tc.name, "not home", windows(m), home.windows())
613+ }
614+ if id, _ := m.Focused(); id != 1 {
615+ t.Fatal(tc.name, "focus after", id)
616+ }
617+ }
618+ })
539619 t.Run("a joined window lands after its stack when columns shifted", func(t *testing.T) {
540620 m := fixed()
541621 w := m.Current()
542622 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{5}}, {Windows: []WindowID{2, 3}}, {Windows: []WindowID{6}}}
543623 w.FocusID(3)
544624 m.ToggleFullscreen()
545- m.AddWindow(7)
625+ m.Current().joinFullscreen(7)
546626 m.RemoveWindow(1)
547627 m.ToggleFullscreen()
548628 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{5, 2, 3, 7, 6}, {}}) {
@@ -523,26 +523,106 @@ func TestMonitorFixedFullscreenEdges(t *testing.T) {
523 }523 }
524 return m524 return m
525 }525 }
526- t.Run("a window opened there floats, then tiles after it at home", func(t *testing.T) {526+ t.Run("a window opened there tiles at home, the view stays", func(t *testing.T) {
527 m := fixed()527 m := fixed()
528 m.Current().FocusID(2)528 m.Current().FocusID(2)
529 m.ToggleFullscreen()529 m.ToggleFullscreen()
530 m.AddWindow(4)530 m.AddWindow(4)
531- if fs := m.Current(); fs.floatIndex(4) != 0 || fs.fullscreen != 2 {531+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 3, 4}, {2}, {}}) || m.Active != 1 {
532- t.Fatal(fs.Floats, fs.fullscreen)532+ t.Fatal(got, m.Active)
533+ }
534+ if id, _ := m.Focused(); id != 2 {
535+ t.Fatal("focus", id)
533 }536 }
534 m.SetFullscreen(2, false)537 m.SetFullscreen(2, false)
535- if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 4, 3}, {}}) || m.Active != 0 {538+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3, 4}, {}}) || m.Active != 0 {
536 t.Fatal(got, m.Active)539 t.Fatal(got, m.Active)
537 }540 }
538 })541 })
542+ t.Run("a window opened over a lone fullscreen moves it to its own workspace", func(t *testing.T) {
543+ for _, client := range []bool{false, true} {
544+ m := monitor()
545+ m.SetOverflow(OverflowFixed)
546+ m.AddWindow(1)
547+ if client {
548+ m.SetFullscreen(1, true)
549+ } else {
550+ m.ToggleFullscreen()
551+ }
552+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {}}) || m.Current().fullscreen != 1 {
553+ t.Fatal("in place", got)
554+ }
555+ m.AddWindow(2)
556+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{2}, {1}, {}}) || m.Active != 1 {
557+ t.Fatal(client, got, m.Active)
558+ }
559+ if id, _ := m.Focused(); id != 1 || m.Current().cover() != 1 {
560+ t.Fatal("focus", id)
561+ }
562+ m.SetFullscreen(1, false)
563+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {}}) || m.Active != 0 {
564+ t.Fatal(client, got, m.Active)
565+ }
566+ if id, _ := m.Focused(); id != 1 {
567+ t.Fatal("focus after", id)
568+ }
569+ }
570+ })
571+ t.Run("a window opened over a lone fullscreen off screen stays there", func(t *testing.T) {
572+ m := monitor()
573+ m.SetOverflow(OverflowFixed)
574+ m.AddWindow(1)
575+ m.ToggleFullscreen()
576+ m.Focus(1)
577+ m.AddWindow(2)
578+ if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {2}, {}}) || m.Active != 1 {
579+ t.Fatal(got, m.Active)
580+ }
581+ })
582+ t.Run("arrivals over a lone fullscreen: slot, named workspace, float", func(t *testing.T) {
583+ for _, tc := range []struct {
584+ name string
585+ setup func(m *Monitor) *Workspace
586+ add func(w *Workspace)
587+ }{
588+ {"slot", func(m *Monitor) *Workspace { m.AddWindow(1); return m.Current() },
589+ func(w *Workspace) { w.AddSlotWindow(2, 1, Width{}) }},
590+ {"hidden named", func(m *Monitor) *Workspace {
591+ m.SetNamed([]NamedWorkspace{{Name: "dev", Overflow: OverflowFixed}})
592+ m.Apply("workspace dev")
593+ m.AddWindow(1)
594+ return m.Current()
595+ }, func(w *Workspace) { w.AddWindow(2) }},
596+ {"float", func(m *Monitor) *Workspace { m.AddFloating(1, 10, 10); return m.Current() },
597+ func(w *Workspace) { w.AddWindow(2) }},
598+ } {
599+ m := monitor()
600+ m.SetOverflow(OverflowFixed)
601+ home := tc.setup(m)
602+ m.SetFullscreen(1, true)
603+ m.arrive(home, tc.add)
604+ if fs := m.Current(); fs == home || fs.cover() != 1 || fs.origin != home {
605+ t.Fatal(tc.name, "fullscreen not on its own workspace", windows(m))
606+ }
607+ if id, _ := m.Focused(); id != 1 || !home.has(2) || home.has(1) {
608+ t.Fatal(tc.name, "focus", id, home.windows())
609+ }
610+ m.SetFullscreen(1, false)
611+ if m.Current() != home || !home.has(1) || !home.has(2) {
612+ t.Fatal(tc.name, "not home", windows(m), home.windows())
613+ }
614+ if id, _ := m.Focused(); id != 1 {
615+ t.Fatal(tc.name, "focus after", id)
616+ }
617+ }
618+ })
539 t.Run("a joined window lands after its stack when columns shifted", func(t *testing.T) {619 t.Run("a joined window lands after its stack when columns shifted", func(t *testing.T) {
540 m := fixed()620 m := fixed()
541 w := m.Current()621 w := m.Current()
542 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{5}}, {Windows: []WindowID{2, 3}}, {Windows: []WindowID{6}}}622 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{5}}, {Windows: []WindowID{2, 3}}, {Windows: []WindowID{6}}}
543 w.FocusID(3)623 w.FocusID(3)
544 m.ToggleFullscreen()624 m.ToggleFullscreen()
545- m.AddWindow(7)625+ m.Current().joinFullscreen(7)
546 m.RemoveWindow(1)626 m.RemoveWindow(1)
547 m.ToggleFullscreen()627 m.ToggleFullscreen()
548 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{5, 2, 3, 7, 6}, {}}) {628 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{5, 2, 3, 7, 6}, {}}) {
M internal/core/spawn_placement.go +2 -4
@@ -84,8 +84,7 @@ func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) {
8484 delete(p.pending, v.Slot)
8585 for _, s := range c.screens {
8686 if slices.Contains(s.mon.all(), target.terminal) {
87- target.terminal.AddWindow(v.ID)
88- s.mon.normalize()
87+ s.mon.arrive(target.terminal, func(w *Workspace) { w.AddWindow(v.ID) })
8988 return
9089 }
9190 }
@@ -93,8 +92,7 @@ func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) {
9392 if sc, w := c.byName(target.slot.workspace); w != nil {
9493 p.dropSlot(target.slot)
9594 st.window = v.ID
96- w.AddSlotWindow(v.ID, target.slot.index, st.width)
97- sc.mon.normalize()
95+ sc.mon.arrive(w, func(w *Workspace) { w.AddSlotWindow(v.ID, target.slot.index, st.width) })
9896 return
9997 }
10098 }
@@ -84,8 +84,7 @@ func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) {
84 delete(p.pending, v.Slot)84 delete(p.pending, v.Slot)
85 for _, s := range c.screens {85 for _, s := range c.screens {
86 if slices.Contains(s.mon.all(), target.terminal) {86 if slices.Contains(s.mon.all(), target.terminal) {
87- target.terminal.AddWindow(v.ID)87+ s.mon.arrive(target.terminal, func(w *Workspace) { w.AddWindow(v.ID) })
88- s.mon.normalize()
89 return88 return
90 }89 }
91 }90 }
@@ -93,8 +92,7 @@ func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) {
93 if sc, w := c.byName(target.slot.workspace); w != nil {92 if sc, w := c.byName(target.slot.workspace); w != nil {
94 p.dropSlot(target.slot)93 p.dropSlot(target.slot)
95 st.window = v.ID94 st.window = v.ID
96- w.AddSlotWindow(v.ID, target.slot.index, st.width)95+ sc.mon.arrive(w, func(w *Workspace) { w.AddSlotWindow(v.ID, target.slot.index, st.width) })
97- sc.mon.normalize()
98 return96 return
99 }97 }
100 }98 }

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