detached 541e2c7

feat(layout): maximize focused column with Cmd+F (#29)

commit
541e2c75b912a1737c08b2e29275a398b16948ac
author
bnema · · 2026-09-27 18:32 UTC
committer
GitHub · · 2026-09-27 18:32 UTC
parent
77b3aae
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
8 files · +102 -10
M docs/config.md +2 -0
@@ -75,6 +75,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
7575 | `spawn-terminal` | Run `terminal` |
7676 | `close-window` | Close the focused window |
7777 | `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 |
7879 | `cycle-column-width` | Step through `layout.presets` |
7980 | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor |
8081 | `focus-window-up/down` | Focus in the column; past the edge, the next workspace |
@@ -109,6 +110,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
109110 | `cmd+bracketleft` | `consume-or-expel-window-left` |
110111 | `cmd+bracketright` | `consume-or-expel-window-right` |
111112 | `cmd+r` | `cycle-column-width` |
113+| `cmd+f` | `maximize-column` |
112114 | `cmd+shift+f` | `toggle-fullscreen` |
113115 | `cmd+q` | `close-window` |
114116 | `ctrl+alt+backspace` | `quit` |
@@ -75,6 +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 | `cycle-column-width` | Step through `layout.presets` |79 | `cycle-column-width` | Step through `layout.presets` |
79 | `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 |
80 | `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 |
@@ -109,6 +110,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
109 | `cmd+bracketleft` | `consume-or-expel-window-left` |110 | `cmd+bracketleft` | `consume-or-expel-window-left` |
110 | `cmd+bracketright` | `consume-or-expel-window-right` |111 | `cmd+bracketright` | `consume-or-expel-window-right` |
111 | `cmd+r` | `cycle-column-width` |112 | `cmd+r` | `cycle-column-width` |
113+| `cmd+f` | `maximize-column` |
112 | `cmd+shift+f` | `toggle-fullscreen` |114 | `cmd+shift+f` | `toggle-fullscreen` |
113 | `cmd+q` | `close-window` |115 | `cmd+q` | `close-window` |
114 | `ctrl+alt+backspace` | `quit` |116 | `ctrl+alt+backspace` | `quit` |
M examples/config +1 -0
@@ -108,6 +108,7 @@ bind.cmd+shift+l = move-column-right
108108 bind.cmd+bracketleft = consume-or-expel-window-left
109109 bind.cmd+bracketright = consume-or-expel-window-right
110110 bind.cmd+r = cycle-column-width
111+bind.cmd+f = maximize-column
111112 bind.cmd+shift+f = toggle-fullscreen
112113 bind.cmd+q = close-window
113114 bind.ctrl+alt+backspace = quit
@@ -108,6 +108,7 @@ bind.cmd+shift+l = move-column-right
108 bind.cmd+bracketleft = consume-or-expel-window-left108 bind.cmd+bracketleft = consume-or-expel-window-left
109 bind.cmd+bracketright = consume-or-expel-window-right109 bind.cmd+bracketright = consume-or-expel-window-right
110 bind.cmd+r = cycle-column-width110 bind.cmd+r = cycle-column-width
111+bind.cmd+f = maximize-column
111 bind.cmd+shift+f = toggle-fullscreen112 bind.cmd+shift+f = toggle-fullscreen
112 bind.cmd+q = close-window113 bind.cmd+q = close-window
113 bind.ctrl+alt+backspace = quit114 bind.ctrl+alt+backspace = quit
M internal/adapters/config/config.go +2 -1
@@ -30,6 +30,7 @@ var defaultBinds = []struct{ combo, action string }{
3030 {"cmd+bracketleft", "consume-or-expel-window-left"},
3131 {"cmd+bracketright", "consume-or-expel-window-right"},
3232 {"cmd+r", "cycle-column-width"},
33+ {"cmd+f", "maximize-column"},
3334 {"cmd+shift+f", "toggle-fullscreen"},
3435 {"cmd+q", "close-window"},
3536 {"ctrl+alt+backspace", "quit"},
@@ -43,7 +44,7 @@ var defaultBinds = []struct{ combo, action string }{
4344 {"cmd+ctrl+shift+left", "move-workspace-to-monitor-left"},
4445 {"cmd+ctrl+shift+right", "move-workspace-to-monitor-right"},
4546 }
46-var actions = map[string]bool{"focus-monitor-left": true, "focus-monitor-right": true, "move-workspace-to-monitor-left": true, "move-workspace-to-monitor-right": true, "scale-up": true, "scale-down": true, "focus-workspace-up": true, "focus-workspace-down": true, "move-column-to-workspace-up": true, "move-column-to-workspace-down": true, "move-window-to-workspace-up": true, "move-window-to-workspace-down": true, "none": true, "consume-or-expel-window-left": true, "consume-or-expel-window-right": true, "spawn-terminal": true, "focus-column-left": true, "focus-column-right": true, "focus-window-up": true, "focus-window-down": true, "move-column-left": true, "move-column-right": true, "cycle-column-width": true, "toggle-fullscreen": true, "close-window": true, "quit": true}
47+var actions = map[string]bool{"focus-monitor-left": true, "focus-monitor-right": true, "move-workspace-to-monitor-left": true, "move-workspace-to-monitor-right": true, "scale-up": true, "scale-down": true, "focus-workspace-up": true, "focus-workspace-down": true, "move-column-to-workspace-up": true, "move-column-to-workspace-down": true, "move-window-to-workspace-up": true, "move-window-to-workspace-down": true, "none": true, "consume-or-expel-window-left": true, "consume-or-expel-window-right": true, "spawn-terminal": true, "focus-column-left": true, "focus-column-right": true, "focus-window-up": true, "focus-window-down": true, "move-column-left": true, "move-column-right": true, "cycle-column-width": true, "maximize-column": true, "toggle-fullscreen": true, "close-window": true, "quit": true}
4748 var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true}
4849 var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
4950
@@ -30,6 +30,7 @@ var defaultBinds = []struct{ combo, action string }{
30 {"cmd+bracketleft", "consume-or-expel-window-left"},30 {"cmd+bracketleft", "consume-or-expel-window-left"},
31 {"cmd+bracketright", "consume-or-expel-window-right"},31 {"cmd+bracketright", "consume-or-expel-window-right"},
32 {"cmd+r", "cycle-column-width"},32 {"cmd+r", "cycle-column-width"},
33+ {"cmd+f", "maximize-column"},
33 {"cmd+shift+f", "toggle-fullscreen"},34 {"cmd+shift+f", "toggle-fullscreen"},
34 {"cmd+q", "close-window"},35 {"cmd+q", "close-window"},
35 {"ctrl+alt+backspace", "quit"},36 {"ctrl+alt+backspace", "quit"},
@@ -43,7 +44,7 @@ var defaultBinds = []struct{ combo, action string }{
43 {"cmd+ctrl+shift+left", "move-workspace-to-monitor-left"},44 {"cmd+ctrl+shift+left", "move-workspace-to-monitor-left"},
44 {"cmd+ctrl+shift+right", "move-workspace-to-monitor-right"},45 {"cmd+ctrl+shift+right", "move-workspace-to-monitor-right"},
45 }46 }
46-var actions = map[string]bool{"focus-monitor-left": true, "focus-monitor-right": true, "move-workspace-to-monitor-left": true, "move-workspace-to-monitor-right": true, "scale-up": true, "scale-down": true, "focus-workspace-up": true, "focus-workspace-down": true, "move-column-to-workspace-up": true, "move-column-to-workspace-down": true, "move-window-to-workspace-up": true, "move-window-to-workspace-down": true, "none": true, "consume-or-expel-window-left": true, "consume-or-expel-window-right": true, "spawn-terminal": true, "focus-column-left": true, "focus-column-right": true, "focus-window-up": true, "focus-window-down": true, "move-column-left": true, "move-column-right": true, "cycle-column-width": true, "toggle-fullscreen": true, "close-window": true, "quit": true}47+var actions = map[string]bool{"focus-monitor-left": true, "focus-monitor-right": true, "move-workspace-to-monitor-left": true, "move-workspace-to-monitor-right": true, "scale-up": true, "scale-down": true, "focus-workspace-up": true, "focus-workspace-down": true, "move-column-to-workspace-up": true, "move-column-to-workspace-down": true, "move-window-to-workspace-up": true, "move-window-to-workspace-down": true, "none": true, "consume-or-expel-window-left": true, "consume-or-expel-window-right": true, "spawn-terminal": true, "focus-column-left": true, "focus-column-right": true, "focus-window-up": true, "focus-window-down": true, "move-column-left": true, "move-column-right": true, "cycle-column-width": true, "maximize-column": true, "toggle-fullscreen": true, "close-window": true, "quit": true}
47 var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true}48 var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true}
48 var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)49 var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
49 50
M internal/adapters/config/config_test.go +2 -2
@@ -22,10 +22,10 @@ func parseString(t *testing.T, s string) (ports.Config, []Warning) {
2222 func TestDefaultsAndLoad(t *testing.T) {
2323 t.Setenv("TERMINAL", "")
2424 d := Defaults()
25- if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 52 || d.Layout.MaxColumns != 2 {
25+ if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 53 || d.Layout.MaxColumns != 2 {
2626 t.Fatalf("defaults: %+v", d)
2727 }
28- if d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove {
28+ if d.Binds["Cmd+f"] != "maximize-column" || d.Binds["Cmd+Shift+f"] != "toggle-fullscreen" || d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove {
2929 t.Fatal(d.Binds)
3030 }
3131 if d.Binds["Cmd+Ctrl+space"] != "spawn fuzzel" || d.Binds["Alt+Ctrl+BackSpace"] != "quit" {
@@ -22,10 +22,10 @@ func parseString(t *testing.T, s string) (ports.Config, []Warning) {
22 func TestDefaultsAndLoad(t *testing.T) {22 func TestDefaultsAndLoad(t *testing.T) {
23 t.Setenv("TERMINAL", "")23 t.Setenv("TERMINAL", "")
24 d := Defaults()24 d := Defaults()
25- if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 52 || d.Layout.MaxColumns != 2 {25+ if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 53 || d.Layout.MaxColumns != 2 {
26 t.Fatalf("defaults: %+v", d)26 t.Fatalf("defaults: %+v", d)
27 }27 }
28- if d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove {28+ if d.Binds["Cmd+f"] != "maximize-column" || d.Binds["Cmd+Shift+f"] != "toggle-fullscreen" || d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove {
29 t.Fatal(d.Binds)29 t.Fatal(d.Binds)
30 }30 }
31 if d.Binds["Cmd+Ctrl+space"] != "spawn fuzzel" || d.Binds["Alt+Ctrl+BackSpace"] != "quit" {31 if d.Binds["Cmd+Ctrl+space"] != "spawn fuzzel" || d.Binds["Alt+Ctrl+BackSpace"] != "quit" {
M internal/core/action.go +3 -0
@@ -16,6 +16,7 @@ const (
1616 ActionMoveColumnLeft Action = "move-column-left"
1717 ActionMoveColumnRight Action = "move-column-right"
1818 ActionCycleColumnWidth Action = "cycle-column-width"
19+ ActionMaximizeColumn Action = "maximize-column"
1920 ActionToggleFullscreen Action = "toggle-fullscreen"
2021 ActionCloseWindow Action = "close-window"
2122 ActionQuit Action = "quit"
@@ -260,6 +261,8 @@ func (w *Workspace) Apply(a Action) Effect {
260261 w.MoveColumn(1)
261262 case ActionCycleColumnWidth:
262263 w.CycleWidth()
264+ case ActionMaximizeColumn:
265+ w.ToggleFullWidth()
263266 case ActionToggleFullscreen:
264267 // In place only: binds go through Monitor.Apply, which gives a
265268 // fixed-overflow fullscreen its own workspace.
@@ -16,6 +16,7 @@ const (
16 ActionMoveColumnLeft Action = "move-column-left"16 ActionMoveColumnLeft Action = "move-column-left"
17 ActionMoveColumnRight Action = "move-column-right"17 ActionMoveColumnRight Action = "move-column-right"
18 ActionCycleColumnWidth Action = "cycle-column-width"18 ActionCycleColumnWidth Action = "cycle-column-width"
19+ ActionMaximizeColumn Action = "maximize-column"
19 ActionToggleFullscreen Action = "toggle-fullscreen"20 ActionToggleFullscreen Action = "toggle-fullscreen"
20 ActionCloseWindow Action = "close-window"21 ActionCloseWindow Action = "close-window"
21 ActionQuit Action = "quit"22 ActionQuit Action = "quit"
@@ -260,6 +261,8 @@ func (w *Workspace) Apply(a Action) Effect {
260 w.MoveColumn(1)261 w.MoveColumn(1)
261 case ActionCycleColumnWidth:262 case ActionCycleColumnWidth:
262 w.CycleWidth()263 w.CycleWidth()
264+ case ActionMaximizeColumn:
265+ w.ToggleFullWidth()
263 case ActionToggleFullscreen:266 case ActionToggleFullscreen:
264 // In place only: binds go through Monitor.Apply, which gives a267 // In place only: binds go through Monitor.Apply, which gives a
265 // fixed-overflow fullscreen its own workspace.268 // fixed-overflow fullscreen its own workspace.
M internal/core/core.go +1 -1
@@ -191,7 +191,7 @@ func (c *Core) apply(cfg ports.Config) error {
191191 continue
192192 }
193193 switch Action(a) {
194- case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
194+ case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionMaximizeColumn, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
195195 default:
196196 return fmt.Errorf("invalid action %q", a)
197197 }
@@ -191,7 +191,7 @@ func (c *Core) apply(cfg ports.Config) error {
191 continue191 continue
192 }192 }
193 switch Action(a) {193 switch Action(a) {
194- case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:194+ case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionMaximizeColumn, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
195 default:195 default:
196 return fmt.Errorf("invalid action %q", a)196 return fmt.Errorf("invalid action %q", a)
197 }197 }
M internal/core/layout.go +39 -6
@@ -80,9 +80,10 @@ func (v Width) Resolve(usableW, gaps int) int {
8080 // Column.Width is zero (auto) until the user picks a preset: auto columns share
8181 // the usable width equally, up to MaxColumns visible at once.
8282 type Column struct {
83- Windows []WindowID
84- Width Width
85- Focus int
83+ Windows []WindowID
84+ Width Width
85+ FullWidth bool
86+ Focus int
8687 // Slot is the declared column number (workspace.<name>.column.N) of a
8788 // slot window; 0 for normal columns.
8889 Slot int
@@ -246,6 +247,9 @@ func (w *Workspace) addColumn(col Column) {
246247 } else if len(w.Columns) > 0 {
247248 at = w.Focus + 1
248249 }
250+ if w.Overflow == OverflowFixed && len(w.Columns) > 0 {
251+ w.Columns[w.Focus].FullWidth = false
252+ }
249253 w.Columns = append(w.Columns, Column{})
250254 copy(w.Columns[at+1:], w.Columns[at:])
251255 w.Columns[at] = col
@@ -360,6 +364,9 @@ func (w *Workspace) FocusID(id WindowID) bool {
360364 for i := range w.Columns {
361365 for j, v := range w.Columns[i].Windows {
362366 if v == id {
367+ if w.Overflow == OverflowFixed && w.Focus != i && w.Focus < len(w.Columns) {
368+ w.Columns[w.Focus].FullWidth = false
369+ }
363370 w.Focus = i
364371 w.Columns[i].Focus = j
365372 w.scroll()
@@ -376,6 +383,9 @@ func (w *Workspace) FocusColumn(dir int) {
376383 return
377384 }
378385 if len(w.Columns) > 0 && (dir == -1 || dir == 1) && w.Focus+dir >= 0 && w.Focus+dir < len(w.Columns) {
386+ if w.Overflow == OverflowFixed {
387+ w.Columns[w.Focus].FullWidth = false
388+ }
379389 w.Focus += dir
380390 w.scroll()
381391 }
@@ -433,6 +443,9 @@ func (w *Workspace) takeColumn() (Column, bool) {
433443 // insertColumn adds a column at index at (clamped) and focuses it.
434444 func (w *Workspace) insertColumn(at int, col Column) {
435445 at = min(max(at, 0), len(w.Columns))
446+ if w.Overflow == OverflowFixed && len(w.Columns) > 0 {
447+ w.Columns[w.Focus].FullWidth = false
448+ }
436449 w.Columns = slices.Insert(w.Columns, at, col)
437450 w.Focus = at
438451 w.scroll()
@@ -455,8 +468,20 @@ func (w *Workspace) CycleWidth() {
455468 }
456469 }
457470 c.Width = next
471+ c.FullWidth = false
458472 w.scroll()
459473 }
474+
475+// ToggleFullWidth expands the focused tiled column without changing its saved width.
476+func (w *Workspace) ToggleFullWidth() {
477+ if len(w.Columns) == 0 || w.floatFocus {
478+ return
479+ }
480+ c := &w.Columns[w.Focus]
481+ c.FullWidth = !c.FullWidth
482+ w.scroll()
483+}
484+
460485 func (w *Workspace) ToggleFullscreen() {
461486 id, ok := w.Focused()
462487 if !ok {
@@ -551,6 +576,9 @@ func (w *Workspace) fullscreenColumn(i int) bool {
551576 return false
552577 }
553578 func (w *Workspace) columnWidth(i int) int {
579+ if w.Columns[i].FullWidth {
580+ return max(w.Usable.W-2*w.gap(), 0)
581+ }
554582 if w.fullscreenColumn(i) {
555583 return w.Output.W
556584 }
@@ -604,6 +632,10 @@ func (w *Workspace) columnRects() []Rect {
604632 for i := range w.Columns {
605633 rects[i] = Rect{X: w.columnX(i) - w.ViewX, Y: y, W: w.columnWidth(i), H: h}
606634 }
635+ if w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth {
636+ rects[w.Focus].X = w.Usable.X + g
637+ return rects
638+ }
607639 k := max(w.MaxColumns, 1)
608640 if w.Overflow != OverflowFixed || len(w.Columns) <= k {
609641 return rects
@@ -656,9 +688,10 @@ func (w *Workspace) Layout() []Placement {
656688 h = min(h, bottom-y)
657689 r := Rect{X: col.X, Y: y, W: col.W, H: h}
658690 full := w.fullscreen == id && id != 0
659- // Fixed overflow keeps every column on screen: hide them all
660- // behind a fullscreen window.
661- hidden := (fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full
691+ // Fixed overflow cannot scroll to other columns while one fills
692+ // the view. Keep them in place, but out of the scene.
693+ maximized := w.Overflow == OverflowFixed && w.Columns[w.Focus].FullWidth && i != w.Focus
694+ hidden := ((fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full) || maximized
662695 if full {
663696 // Scroll mode aligns the view on the column; fixed never scrolls.
664697 r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H}
@@ -80,9 +80,10 @@ func (v Width) Resolve(usableW, gaps int) int {
80 // Column.Width is zero (auto) until the user picks a preset: auto columns share80 // Column.Width is zero (auto) until the user picks a preset: auto columns share
81 // the usable width equally, up to MaxColumns visible at once.81 // the usable width equally, up to MaxColumns visible at once.
82 type Column struct {82 type Column struct {
83- Windows []WindowID83+ Windows []WindowID
84- Width Width84+ Width Width
85- Focus int85+ FullWidth bool
86+ Focus int
86 // Slot is the declared column number (workspace.<name>.column.N) of a87 // Slot is the declared column number (workspace.<name>.column.N) of a
87 // slot window; 0 for normal columns.88 // slot window; 0 for normal columns.
88 Slot int89 Slot int
@@ -246,6 +247,9 @@ func (w *Workspace) addColumn(col Column) {
246 } else if len(w.Columns) > 0 {247 } else if len(w.Columns) > 0 {
247 at = w.Focus + 1248 at = w.Focus + 1
248 }249 }
250+ if w.Overflow == OverflowFixed && len(w.Columns) > 0 {
251+ w.Columns[w.Focus].FullWidth = false
252+ }
249 w.Columns = append(w.Columns, Column{})253 w.Columns = append(w.Columns, Column{})
250 copy(w.Columns[at+1:], w.Columns[at:])254 copy(w.Columns[at+1:], w.Columns[at:])
251 w.Columns[at] = col255 w.Columns[at] = col
@@ -360,6 +364,9 @@ func (w *Workspace) FocusID(id WindowID) bool {
360 for i := range w.Columns {364 for i := range w.Columns {
361 for j, v := range w.Columns[i].Windows {365 for j, v := range w.Columns[i].Windows {
362 if v == id {366 if v == id {
367+ if w.Overflow == OverflowFixed && w.Focus != i && w.Focus < len(w.Columns) {
368+ w.Columns[w.Focus].FullWidth = false
369+ }
363 w.Focus = i370 w.Focus = i
364 w.Columns[i].Focus = j371 w.Columns[i].Focus = j
365 w.scroll()372 w.scroll()
@@ -376,6 +383,9 @@ func (w *Workspace) FocusColumn(dir int) {
376 return383 return
377 }384 }
378 if len(w.Columns) > 0 && (dir == -1 || dir == 1) && w.Focus+dir >= 0 && w.Focus+dir < len(w.Columns) {385 if len(w.Columns) > 0 && (dir == -1 || dir == 1) && w.Focus+dir >= 0 && w.Focus+dir < len(w.Columns) {
386+ if w.Overflow == OverflowFixed {
387+ w.Columns[w.Focus].FullWidth = false
388+ }
379 w.Focus += dir389 w.Focus += dir
380 w.scroll()390 w.scroll()
381 }391 }
@@ -433,6 +443,9 @@ func (w *Workspace) takeColumn() (Column, bool) {
433 // insertColumn adds a column at index at (clamped) and focuses it.443 // insertColumn adds a column at index at (clamped) and focuses it.
434 func (w *Workspace) insertColumn(at int, col Column) {444 func (w *Workspace) insertColumn(at int, col Column) {
435 at = min(max(at, 0), len(w.Columns))445 at = min(max(at, 0), len(w.Columns))
446+ if w.Overflow == OverflowFixed && len(w.Columns) > 0 {
447+ w.Columns[w.Focus].FullWidth = false
448+ }
436 w.Columns = slices.Insert(w.Columns, at, col)449 w.Columns = slices.Insert(w.Columns, at, col)
437 w.Focus = at450 w.Focus = at
438 w.scroll()451 w.scroll()
@@ -455,8 +468,20 @@ func (w *Workspace) CycleWidth() {
455 }468 }
456 }469 }
457 c.Width = next470 c.Width = next
471+ c.FullWidth = false
458 w.scroll()472 w.scroll()
459 }473 }
474+
475+// ToggleFullWidth expands the focused tiled column without changing its saved width.
476+func (w *Workspace) ToggleFullWidth() {
477+ if len(w.Columns) == 0 || w.floatFocus {
478+ return
479+ }
480+ c := &w.Columns[w.Focus]
481+ c.FullWidth = !c.FullWidth
482+ w.scroll()
483+}
484+
460 func (w *Workspace) ToggleFullscreen() {485 func (w *Workspace) ToggleFullscreen() {
461 id, ok := w.Focused()486 id, ok := w.Focused()
462 if !ok {487 if !ok {
@@ -551,6 +576,9 @@ func (w *Workspace) fullscreenColumn(i int) bool {
551 return false576 return false
552 }577 }
553 func (w *Workspace) columnWidth(i int) int {578 func (w *Workspace) columnWidth(i int) int {
579+ if w.Columns[i].FullWidth {
580+ return max(w.Usable.W-2*w.gap(), 0)
581+ }
554 if w.fullscreenColumn(i) {582 if w.fullscreenColumn(i) {
555 return w.Output.W583 return w.Output.W
556 }584 }
@@ -604,6 +632,10 @@ func (w *Workspace) columnRects() []Rect {
604 for i := range w.Columns {632 for i := range w.Columns {
605 rects[i] = Rect{X: w.columnX(i) - w.ViewX, Y: y, W: w.columnWidth(i), H: h}633 rects[i] = Rect{X: w.columnX(i) - w.ViewX, Y: y, W: w.columnWidth(i), H: h}
606 }634 }
635+ if w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth {
636+ rects[w.Focus].X = w.Usable.X + g
637+ return rects
638+ }
607 k := max(w.MaxColumns, 1)639 k := max(w.MaxColumns, 1)
608 if w.Overflow != OverflowFixed || len(w.Columns) <= k {640 if w.Overflow != OverflowFixed || len(w.Columns) <= k {
609 return rects641 return rects
@@ -656,9 +688,10 @@ func (w *Workspace) Layout() []Placement {
656 h = min(h, bottom-y)688 h = min(h, bottom-y)
657 r := Rect{X: col.X, Y: y, W: col.W, H: h}689 r := Rect{X: col.X, Y: y, W: col.W, H: h}
658 full := w.fullscreen == id && id != 0690 full := w.fullscreen == id && id != 0
659- // Fixed overflow keeps every column on screen: hide them all691+ // Fixed overflow cannot scroll to other columns while one fills
660- // behind a fullscreen window.692+ // the view. Keep them in place, but out of the scene.
661- hidden := (fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full693+ maximized := w.Overflow == OverflowFixed && w.Columns[w.Focus].FullWidth && i != w.Focus
694+ hidden := ((fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full) || maximized
662 if full {695 if full {
663 // Scroll mode aligns the view on the column; fixed never scrolls.696 // Scroll mode aligns the view on the column; fixed never scrolls.
664 r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H}697 r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H}
M internal/core/layout_test.go +52 -0
@@ -107,6 +107,58 @@ func TestStackAndLayout(t *testing.T) {
107107 t.Fatal(w.Usable)
108108 }
109109 }
110+func TestMaximizeColumnScroll(t *testing.T) {
111+ w := workspace()
112+ for id := WindowID(1); id <= 3; id++ {
113+ w.AddWindow(id)
114+ }
115+ w.Columns[1].Width = Width{Pixels: 30}
116+ w.FocusID(2)
117+ w.Apply(ActionMaximizeColumn)
118+ if !w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) {
119+ t.Fatal(w.Columns, w.Layout())
120+ }
121+ if w.Layout()[0].Rect.X >= 0 || w.Layout()[2].Rect.X < 100 || w.Layout()[1].Fullscreen {
122+ t.Fatal(w.Layout())
123+ }
124+ w.Apply(ActionMaximizeColumn)
125+ if w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect.W != 30 {
126+ t.Fatal(w.Columns, w.Layout())
127+ }
128+}
129+
130+func TestMaximizeColumnFixed(t *testing.T) {
131+ w := workspace()
132+ w.Overflow = OverflowFixed
133+ for id := WindowID(1); id <= 3; id++ {
134+ w.AddWindow(id)
135+ }
136+ w.FocusID(2)
137+ w.Apply(ActionMaximizeColumn)
138+ p := w.Layout()
139+ if p[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) || p[1].Fullscreen || !p[0].Hidden || !p[2].Hidden {
140+ t.Fatal(p)
141+ }
142+ w.FocusColumn(1)
143+ p = w.Layout()
144+ if w.Columns[1].FullWidth || p[2].Hidden || p[0].Hidden || p[1].Hidden {
145+ t.Fatal(w.Columns, p)
146+ }
147+}
148+
149+func TestMaximizeColumnFixedAddWindow(t *testing.T) {
150+ w := workspace()
151+ w.Overflow = OverflowFixed
152+ w.AddWindow(1)
153+ w.AddWindow(2)
154+ w.ToggleFullWidth()
155+ w.AddWindow(3)
156+ p := w.Layout()
157+ if w.Columns[1].FullWidth || p[0].Hidden || p[1].Hidden || p[2].Hidden || p[2].Rect.W == 0 {
158+ t.Fatal(w.Columns, p)
159+ }
160+}
161+
110162 func TestPresetsAndActions(t *testing.T) {
111163 w := workspace()
112164 w.AddWindow(1)
@@ -107,6 +107,58 @@ func TestStackAndLayout(t *testing.T) {
107 t.Fatal(w.Usable)107 t.Fatal(w.Usable)
108 }108 }
109 }109 }
110+func TestMaximizeColumnScroll(t *testing.T) {
111+ w := workspace()
112+ for id := WindowID(1); id <= 3; id++ {
113+ w.AddWindow(id)
114+ }
115+ w.Columns[1].Width = Width{Pixels: 30}
116+ w.FocusID(2)
117+ w.Apply(ActionMaximizeColumn)
118+ if !w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) {
119+ t.Fatal(w.Columns, w.Layout())
120+ }
121+ if w.Layout()[0].Rect.X >= 0 || w.Layout()[2].Rect.X < 100 || w.Layout()[1].Fullscreen {
122+ t.Fatal(w.Layout())
123+ }
124+ w.Apply(ActionMaximizeColumn)
125+ if w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect.W != 30 {
126+ t.Fatal(w.Columns, w.Layout())
127+ }
128+}
129+
130+func TestMaximizeColumnFixed(t *testing.T) {
131+ w := workspace()
132+ w.Overflow = OverflowFixed
133+ for id := WindowID(1); id <= 3; id++ {
134+ w.AddWindow(id)
135+ }
136+ w.FocusID(2)
137+ w.Apply(ActionMaximizeColumn)
138+ p := w.Layout()
139+ if p[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) || p[1].Fullscreen || !p[0].Hidden || !p[2].Hidden {
140+ t.Fatal(p)
141+ }
142+ w.FocusColumn(1)
143+ p = w.Layout()
144+ if w.Columns[1].FullWidth || p[2].Hidden || p[0].Hidden || p[1].Hidden {
145+ t.Fatal(w.Columns, p)
146+ }
147+}
148+
149+func TestMaximizeColumnFixedAddWindow(t *testing.T) {
150+ w := workspace()
151+ w.Overflow = OverflowFixed
152+ w.AddWindow(1)
153+ w.AddWindow(2)
154+ w.ToggleFullWidth()
155+ w.AddWindow(3)
156+ p := w.Layout()
157+ if w.Columns[1].FullWidth || p[0].Hidden || p[1].Hidden || p[2].Hidden || p[2].Rect.W == 0 {
158+ t.Fatal(w.Columns, p)
159+ }
160+}
161+
110 func TestPresetsAndActions(t *testing.T) {162 func TestPresetsAndActions(t *testing.T) {
111 w := workspace()163 w := workspace()
112 w.AddWindow(1)164 w.AddWindow(1)

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 26

Tags 10

↑↓ moveenter openesc close