detached 1ddb3e5

fix(core): keep a column moved onto a fullscreen workspace visible

Cross-monitor move-column and move-column/window-to-workspace now share
Workspace.receive: windows float above a fixed-overflow fullscreen
workspace, and an in-place fullscreen is left so the moved window shows.
commit
1ddb3e5e483103aa8f179fd4ab0ad5572699b6cb
author
brice · · 2026-09-26 14:40 UTC
parent
503de12
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +41 -9
M internal/core/action.go +1 -1
@@ -156,7 +156,7 @@ func (c *Core) applyAction(a Action) Effect {
156156 if dir < 0 {
157157 at = len(to.Columns)
158158 }
159- to.insertColumn(at, col)
159+ to.receive(col, at)
160160 from.mon.normalize()
161161 c.cur().mon.normalize()
162162 return Effect{}
@@ -156,7 +156,7 @@ func (c *Core) applyAction(a Action) Effect {
156 if dir < 0 {156 if dir < 0 {
157 at = len(to.Columns)157 at = len(to.Columns)
158 }158 }
159- to.insertColumn(at, col)159+ to.receive(col, at)
160 from.mon.normalize()160 from.mon.normalize()
161 c.cur().mon.normalize()161 c.cur().mon.normalize()
162 return Effect{}162 return Effect{}
M internal/core/monitor.go +21 -8
@@ -493,6 +493,7 @@ func (m *Monitor) MoveToWorkspace(i int, column bool) {
493493 fl := cur.Floats[f]
494494 cur.RemoveWindow(id)
495495 to.AddFloating(id, fl.W, fl.H)
496+ to.FocusID(id)
496497 } else {
497498 col := Column{Windows: []WindowID{id}}
498499 if column {
@@ -500,22 +501,34 @@ func (m *Monitor) MoveToWorkspace(i int, column bool) {
500501 } else {
501502 cur.RemoveWindow(id)
502503 }
503- if to.origin != nil {
504- for _, w := range col.Windows {
505- to.joinFullscreen(w)
506- }
507- } else {
508- to.addColumn(col)
509- }
504+ to.receive(col, -1)
510505 }
511506 if m.followMove {
512507 m.Active = i
513508 m.shown = nil
514- to.FocusID(id)
515509 }
516510 m.normalize()
517511 }
518512
513+// receive adds a column moved in from another workspace and focuses it: at
514+// index at, or where a new window goes when at < 0. It stays visible: on a
515+// fixed-overflow fullscreen workspace its windows float above, and an
516+// in-place fullscreen is left.
517+func (w *Workspace) receive(col Column, at int) {
518+ id := col.Windows[col.Focus]
519+ switch {
520+ case w.origin != nil:
521+ for _, v := range col.Windows {
522+ w.joinFullscreen(v)
523+ }
524+ case at < 0:
525+ w.addColumn(col)
526+ default:
527+ w.insertColumn(at, col)
528+ }
529+ w.Activate(id)
530+}
531+
519532 // joinFullscreen adds a tiled window to a fullscreen workspace fs: it floats
520533 // above the fullscreen window, which would hide it (fixed overflow), and
521534 // tiles again once home.
@@ -493,6 +493,7 @@ func (m *Monitor) MoveToWorkspace(i int, column bool) {
493 fl := cur.Floats[f]493 fl := cur.Floats[f]
494 cur.RemoveWindow(id)494 cur.RemoveWindow(id)
495 to.AddFloating(id, fl.W, fl.H)495 to.AddFloating(id, fl.W, fl.H)
496+ to.FocusID(id)
496 } else {497 } else {
497 col := Column{Windows: []WindowID{id}}498 col := Column{Windows: []WindowID{id}}
498 if column {499 if column {
@@ -500,22 +501,34 @@ func (m *Monitor) MoveToWorkspace(i int, column bool) {
500 } else {501 } else {
501 cur.RemoveWindow(id)502 cur.RemoveWindow(id)
502 }503 }
503- if to.origin != nil {504+ to.receive(col, -1)
504- for _, w := range col.Windows {
505- to.joinFullscreen(w)
506- }
507- } else {
508- to.addColumn(col)
509- }
510 }505 }
511 if m.followMove {506 if m.followMove {
512 m.Active = i507 m.Active = i
513 m.shown = nil508 m.shown = nil
514- to.FocusID(id)
515 }509 }
516 m.normalize()510 m.normalize()
517 }511 }
518 512
513+// receive adds a column moved in from another workspace and focuses it: at
514+// index at, or where a new window goes when at < 0. It stays visible: on a
515+// fixed-overflow fullscreen workspace its windows float above, and an
516+// in-place fullscreen is left.
517+func (w *Workspace) receive(col Column, at int) {
518+ id := col.Windows[col.Focus]
519+ switch {
520+ case w.origin != nil:
521+ for _, v := range col.Windows {
522+ w.joinFullscreen(v)
523+ }
524+ case at < 0:
525+ w.addColumn(col)
526+ default:
527+ w.insertColumn(at, col)
528+ }
529+ w.Activate(id)
530+}
531+
519 // joinFullscreen adds a tiled window to a fullscreen workspace fs: it floats532 // joinFullscreen adds a tiled window to a fullscreen workspace fs: it floats
520 // above the fullscreen window, which would hide it (fixed overflow), and533 // above the fullscreen window, which would hide it (fixed overflow), and
521 // tiles again once home.534 // tiles again once home.
M internal/core/screens_test.go +19 -0
@@ -504,3 +504,22 @@ func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) {
504504 t.Fatalf("%+v", st)
505505 }
506506 }
507+
508+func TestMoveColumnOntoFullscreenStaysVisible(t *testing.T) {
509+ r := startMulti(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" }, left, right)
510+ r.mapWindow(t, 1)
511+ r.mapWindow(t, 2)
512+ // Window 2 goes to DP-2, alone, and goes fullscreen in place.
513+ r.key(t, "Right", ports.ModAlt|ports.ModShift)
514+ r.key(t, "f", ports.ModAlt|ports.ModShift)
515+ r.key(t, "Left", ports.ModAlt|ports.ModCtrl)
516+ // Window 1 joins from DP-1: the fullscreen ends so it is visible and focused.
517+ set := r.key(t, "Right", ports.ModAlt|ports.ModShift)
518+ for _, s := range set {
519+ for _, w := range s.Windows {
520+ if w.ID == 1 && (s.Output != "DP-2" || w.Hidden || !w.Focused) {
521+ t.Fatalf("%s %+v", s.Output, w)
522+ }
523+ }
524+ }
525+}
@@ -504,3 +504,22 @@ func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) {
504 t.Fatalf("%+v", st)504 t.Fatalf("%+v", st)
505 }505 }
506 }506 }
507+
508+func TestMoveColumnOntoFullscreenStaysVisible(t *testing.T) {
509+ r := startMulti(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" }, left, right)
510+ r.mapWindow(t, 1)
511+ r.mapWindow(t, 2)
512+ // Window 2 goes to DP-2, alone, and goes fullscreen in place.
513+ r.key(t, "Right", ports.ModAlt|ports.ModShift)
514+ r.key(t, "f", ports.ModAlt|ports.ModShift)
515+ r.key(t, "Left", ports.ModAlt|ports.ModCtrl)
516+ // Window 1 joins from DP-1: the fullscreen ends so it is visible and focused.
517+ set := r.key(t, "Right", ports.ModAlt|ports.ModShift)
518+ for _, s := range set {
519+ for _, w := range s.Windows {
520+ if w.ID == 1 && (s.Output != "DP-2" || w.Hidden || !w.Focused) {
521+ t.Fatalf("%s %+v", s.Output, w)
522+ }
523+ }
524+ }
525+}

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