fix(layout): return a client fullscreen window to its column with Cmd+F (#33)
Wine makes monitor-sized apps (Battle.net) fullscreen on map. Under fixed overflow this hid every other window and Cmd+F did nothing. maximize-column now leaves that fullscreen first; the next press maximizes the column.
- commit
- 41f250abc04b8848c5675e329eeffcedd094de04
- author
- bnema · · 2026-09-27 19:33 UTC
- committer
- GitHub · · 2026-09-27 19:33 UTC
- parent
- 3aaf9f2
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
3 files · +39 -1
M docs/config.md +1 -1
| @@ -75,7 +75,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||
| 75 | 75 | | `spawn-terminal` | Run `terminal` | | |
| 76 | 76 | | `close-window` | Close the focused window | | |
| 77 | 77 | | `toggle-fullscreen` | Fullscreen the focused window | | |
| 78 | - | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off | | |
| 78 | + | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column | | |
| 79 | 79 | | `cycle-column-width` | Step through `layout.presets` | | |
| 80 | 80 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor | | |
| 81 | 81 | | `focus-window-up/down` | Focus in the column; past the edge, the next workspace | | |
| @@ -75,7 +75,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||||
| 75 | | `spawn-terminal` | Run `terminal` | | 75 | | `spawn-terminal` | Run `terminal` | | ||
| 76 | | `close-window` | Close the focused window | | 76 | | `close-window` | Close the focused window | | ||
| 77 | | `toggle-fullscreen` | Fullscreen the focused window | | 77 | | `toggle-fullscreen` | Fullscreen the focused window | | ||
| 78 | - | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off | | 78 | + | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column | |
| 79 | | `cycle-column-width` | Step through `layout.presets` | | 79 | | `cycle-column-width` | Step through `layout.presets` | | ||
| 80 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor | | 80 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor | | ||
| 81 | | `focus-window-up/down` | Focus in the column; past the edge, the next workspace | | 81 | | `focus-window-up/down` | Focus in the column; past the edge, the next workspace | | ||
M internal/core/action.go +9 -0
| @@ -218,6 +218,15 @@ func (m *Monitor) Apply(a Action) Effect { | |||
| 218 | 218 | case ActionToggleFullscreen: | |
| 219 | 219 | m.ToggleFullscreen() | |
| 220 | 220 | return Effect{} | |
| 221 | + | case ActionMaximizeColumn: | |
| 222 | + | // A client that made itself fullscreen (Wine at monitor size) goes | |
| 223 | + | // back to its column width; the next press maximizes the column. | |
| 224 | + | if w := m.Current(); w.fullscreen != 0 { | |
| 225 | + | if id, ok := w.Focused(); ok && id == w.fullscreen { | |
| 226 | + | m.ToggleFullscreen() | |
| 227 | + | return Effect{} | |
| 228 | + | } | |
| 229 | + | } | |
| 221 | 230 | case ActionFocusWorkspaceUp, ActionFocusWorkspaceDown: | |
| 222 | 231 | if m.shown == nil && a == ActionFocusWorkspaceUp { | |
| 223 | 232 | m.Focus(m.Active - 1) | |
| @@ -218,6 +218,15 @@ func (m *Monitor) Apply(a Action) Effect { | |||||
| 218 | case ActionToggleFullscreen: | 218 | case ActionToggleFullscreen: | ||
| 219 | m.ToggleFullscreen() | 219 | m.ToggleFullscreen() | ||
| 220 | return Effect{} | 220 | return Effect{} | ||
| 221 | + | case ActionMaximizeColumn: | |||
| 222 | + | // A client that made itself fullscreen (Wine at monitor size) goes | |||
| 223 | + | // back to its column width; the next press maximizes the column. | |||
| 224 | + | if w := m.Current(); w.fullscreen != 0 { | |||
| 225 | + | if id, ok := w.Focused(); ok && id == w.fullscreen { | |||
| 226 | + | m.ToggleFullscreen() | |||
| 227 | + | return Effect{} | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 221 | case ActionFocusWorkspaceUp, ActionFocusWorkspaceDown: | 230 | case ActionFocusWorkspaceUp, ActionFocusWorkspaceDown: | ||
| 222 | if m.shown == nil && a == ActionFocusWorkspaceUp { | 231 | if m.shown == nil && a == ActionFocusWorkspaceUp { | ||
| 223 | m.Focus(m.Active - 1) | 232 | m.Focus(m.Active - 1) | ||
M internal/core/monitor_test.go +29 -0
| @@ -664,3 +664,32 @@ func TestMonitorFixedFullscreenEdges(t *testing.T) { | |||
| 664 | 664 | } | |
| 665 | 665 | }) | |
| 666 | 666 | } | |
| 667 | + | ||
| 668 | + | // Cmd+F on a window that made itself fullscreen returns it to its column | |
| 669 | + | // (Wine opens monitor-sized apps fullscreen); the next press maximizes. | |
| 670 | + | func TestMaximizeLeavesClientFullscreen(t *testing.T) { | |
| 671 | + | for _, o := range []Overflow{OverflowScroll, OverflowFixed} { | |
| 672 | + | t.Run(string(o), func(t *testing.T) { | |
| 673 | + | m := monitor() | |
| 674 | + | m.SetOverflow(o) | |
| 675 | + | m.AddWindow(1) | |
| 676 | + | m.AddWindow(2) | |
| 677 | + | m.SetFullscreen(2, true) | |
| 678 | + | m.Apply(ActionMaximizeColumn) | |
| 679 | + | if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {}}) || m.Active != 0 { | |
| 680 | + | t.Fatal(got, m.Active) | |
| 681 | + | } | |
| 682 | + | w := m.Current() | |
| 683 | + | if w.fullscreen != 0 || w.Columns[1].FullWidth { | |
| 684 | + | t.Fatal(w.fullscreen, w.Columns) | |
| 685 | + | } | |
| 686 | + | if id, _ := m.Focused(); id != 2 { | |
| 687 | + | t.Fatal("focus", id) | |
| 688 | + | } | |
| 689 | + | m.Apply(ActionMaximizeColumn) | |
| 690 | + | if !w.Columns[1].FullWidth { | |
| 691 | + | t.Fatal(w.Columns) | |
| 692 | + | } | |
| 693 | + | }) | |
| 694 | + | } | |
| 695 | + | } | |
| @@ -664,3 +664,32 @@ func TestMonitorFixedFullscreenEdges(t *testing.T) { | |||||
| 664 | } | 664 | } | ||
| 665 | }) | 665 | }) | ||
| 666 | } | 666 | } | ||
| 667 | + | ||||
| 668 | + | // Cmd+F on a window that made itself fullscreen returns it to its column | |||
| 669 | + | // (Wine opens monitor-sized apps fullscreen); the next press maximizes. | |||
| 670 | + | func TestMaximizeLeavesClientFullscreen(t *testing.T) { | |||
| 671 | + | for _, o := range []Overflow{OverflowScroll, OverflowFixed} { | |||
| 672 | + | t.Run(string(o), func(t *testing.T) { | |||
| 673 | + | m := monitor() | |||
| 674 | + | m.SetOverflow(o) | |||
| 675 | + | m.AddWindow(1) | |||
| 676 | + | m.AddWindow(2) | |||
| 677 | + | m.SetFullscreen(2, true) | |||
| 678 | + | m.Apply(ActionMaximizeColumn) | |||
| 679 | + | if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {}}) || m.Active != 0 { | |||
| 680 | + | t.Fatal(got, m.Active) | |||
| 681 | + | } | |||
| 682 | + | w := m.Current() | |||
| 683 | + | if w.fullscreen != 0 || w.Columns[1].FullWidth { | |||
| 684 | + | t.Fatal(w.fullscreen, w.Columns) | |||
| 685 | + | } | |||
| 686 | + | if id, _ := m.Focused(); id != 2 { | |||
| 687 | + | t.Fatal("focus", id) | |||
| 688 | + | } | |||
| 689 | + | m.Apply(ActionMaximizeColumn) | |||
| 690 | + | if !w.Columns[1].FullWidth { | |||
| 691 | + | t.Fatal(w.Columns) | |||
| 692 | + | } | |||
| 693 | + | }) | |||
| 694 | + | } | |||
| 695 | + | } | |||