detached 67280b4

refactor(core): extract output placement and config lookup

commit
67280b43a7940b62c3057ba3b721b14bd2d18e26
author
brice · · 2026-10-03 13:55 UTC
parent
819389e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +189 -68
M internal/adapters/config/config.go +37 -1
@@ -64,7 +64,43 @@ var defaultBinds = []struct{ combo, action string }{
6464 {"cmd+ctrl+shift+down", "move-workspace-down"},
6565 {"cmd+shift+space", "toggle-floating"},
6666 }
67-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, "toggle-window-stash": true, "toggle-stash-visible": true, "toggle-overview": true, "close-window": true, "quit": true, "move-window-up": true, "move-window-down": true, "move-workspace-up": true, "move-workspace-down": true, "toggle-floating": true}
67+var actions = map[string]bool{
68+ "focus-monitor-left": true,
69+ "focus-monitor-right": true,
70+ "move-workspace-to-monitor-left": true,
71+ "move-workspace-to-monitor-right": true,
72+ "scale-up": true,
73+ "scale-down": true,
74+ "focus-workspace-up": true,
75+ "focus-workspace-down": true,
76+ "move-column-to-workspace-up": true,
77+ "move-column-to-workspace-down": true,
78+ "move-window-to-workspace-up": true,
79+ "move-window-to-workspace-down": true,
80+ "none": true,
81+ "consume-or-expel-window-left": true,
82+ "consume-or-expel-window-right": true,
83+ "spawn-terminal": true,
84+ "focus-column-left": true,
85+ "focus-column-right": true,
86+ "focus-window-up": true,
87+ "focus-window-down": true,
88+ "move-column-left": true,
89+ "move-column-right": true,
90+ "cycle-column-width": true,
91+ "maximize-column": true,
92+ "toggle-fullscreen": true,
93+ "toggle-window-stash": true,
94+ "toggle-stash-visible": true,
95+ "toggle-overview": true,
96+ "close-window": true,
97+ "quit": true,
98+ "move-window-up": true,
99+ "move-window-down": true,
100+ "move-workspace-up": true,
101+ "move-workspace-down": true,
102+ "toggle-floating": true,
103+}
68104 var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true}
69105 var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
70106
@@ -64,7 +64,43 @@ var defaultBinds = []struct{ combo, action string }{
64 {"cmd+ctrl+shift+down", "move-workspace-down"},64 {"cmd+ctrl+shift+down", "move-workspace-down"},
65 {"cmd+shift+space", "toggle-floating"},65 {"cmd+shift+space", "toggle-floating"},
66 }66 }
67-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, "toggle-window-stash": true, "toggle-stash-visible": true, "toggle-overview": true, "close-window": true, "quit": true, "move-window-up": true, "move-window-down": true, "move-workspace-up": true, "move-workspace-down": true, "toggle-floating": true}67+var actions = map[string]bool{
68+ "focus-monitor-left": true,
69+ "focus-monitor-right": true,
70+ "move-workspace-to-monitor-left": true,
71+ "move-workspace-to-monitor-right": true,
72+ "scale-up": true,
73+ "scale-down": true,
74+ "focus-workspace-up": true,
75+ "focus-workspace-down": true,
76+ "move-column-to-workspace-up": true,
77+ "move-column-to-workspace-down": true,
78+ "move-window-to-workspace-up": true,
79+ "move-window-to-workspace-down": true,
80+ "none": true,
81+ "consume-or-expel-window-left": true,
82+ "consume-or-expel-window-right": true,
83+ "spawn-terminal": true,
84+ "focus-column-left": true,
85+ "focus-column-right": true,
86+ "focus-window-up": true,
87+ "focus-window-down": true,
88+ "move-column-left": true,
89+ "move-column-right": true,
90+ "cycle-column-width": true,
91+ "maximize-column": true,
92+ "toggle-fullscreen": true,
93+ "toggle-window-stash": true,
94+ "toggle-stash-visible": true,
95+ "toggle-overview": true,
96+ "close-window": true,
97+ "quit": true,
98+ "move-window-up": true,
99+ "move-window-down": true,
100+ "move-workspace-up": true,
101+ "move-workspace-down": true,
102+ "toggle-floating": true,
103+}
68 var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true}104 var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true}
69 var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)105 var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
70 106
M internal/core/action.go +22 -22
@@ -173,35 +173,35 @@ func (c *Core) applyAction(a Action) Effect {
173173 if name, ok := NamedArg(a); ok && c.bringNamed(name) {
174174 return Effect{}
175175 }
176- dir := 0
177- switch a {
178- case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft:
179- dir = -1
180- case ActionFocusMonitorRight, ActionMoveWorkspaceRight:
181- dir = 1
182- }
183176 switch a {
184177 case ActionFocusMonitorLeft, ActionFocusMonitorRight:
185- if i := c.neighbor(dir); i >= 0 {
178+ d := dirLeft
179+ if a == ActionFocusMonitorRight {
180+ d = dirRight
181+ }
182+ if i := c.neighbor(d); i >= 0 {
186183 c.focusScreen = i
187184 }
188185 return Effect{}
189186 case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight:
190- c.moveWorkspace(dir)
187+ d := dirLeft
188+ if a == ActionMoveWorkspaceRight {
189+ d = dirRight
190+ }
191+ c.moveWorkspace(d)
191192 return Effect{}
192193 case ActionFocusColumnLeft, ActionFocusColumnRight:
193194 w := c.cur().mon.Current()
195+ dir, d := 1, dirRight
194196 if a == ActionFocusColumnLeft {
195- dir = -1
196- } else {
197- dir = 1
197+ dir, d = -1, dirLeft
198198 }
199199 // A covering fullscreen window is left for the window on that side;
200200 // with none (the workspace or stash edge), the move goes to the
201201 // neighbor monitor and fullscreen stays.
202202 if w.pinned() {
203203 if !w.FocusColumn(dir) {
204- if i := c.neighbor(dir); i >= 0 {
204+ if i := c.neighbor(d); i >= 0 {
205205 c.focusScreen = i
206206 }
207207 }
@@ -211,14 +211,14 @@ func (c *Core) applyAction(a Action) Effect {
211211 // a native float leaves for the stash or the columns, and the stash
212212 // keeps the focus at its ends.
213213 edge := w.columnToward(dir) < 0 && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat())
214- if i := c.neighbor(dir); edge && i >= 0 {
214+ if i := c.neighbor(d); edge && i >= 0 {
215215 c.focusScreen = i
216216 return Effect{}
217217 }
218218 case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
219- dir = 1
219+ dir, d := 1, dirRight
220220 if a == ActionConsumeOrExpelLeft {
221- dir = -1
221+ dir, d = -1, dirLeft
222222 }
223223 from := c.cur()
224224 if from.mon.Current().ConsumeOrExpel(dir) {
@@ -227,7 +227,7 @@ func (c *Core) applyAction(a Action) Effect {
227227 }
228228 // Full fixed workspace, stacked edge column: one hop to the
229229 // neighbor monitor, if any; focus follows the window.
230- i := c.neighbor(dir)
230+ i := c.neighbor(d)
231231 if i < 0 {
232232 return Effect{}
233233 }
@@ -244,12 +244,12 @@ func (c *Core) applyAction(a Action) Effect {
244244 // Past the edge the column moves to the neighbor screen, on the
245245 // side facing this one, and focus follows it.
246246 w := c.cur().mon.Current()
247- dir = 1
247+ d := dirRight
248248 if a == ActionMoveColumnLeft {
249- dir = -1
249+ d = dirLeft
250250 }
251- edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1)
252- if i := c.neighbor(dir); edge && i >= 0 {
251+ edge := (d == dirLeft && w.Focus == 0) || (d == dirRight && w.Focus == len(w.Columns)-1)
252+ if i := c.neighbor(d); edge && i >= 0 {
253253 col, ok := w.takeColumn()
254254 if !ok {
255255 return Effect{}
@@ -258,7 +258,7 @@ func (c *Core) applyAction(a Action) Effect {
258258 c.focusScreen = i
259259 to := c.cur().mon.Current()
260260 at := 0
261- if dir < 0 {
261+ if d == dirLeft {
262262 at = len(to.Columns)
263263 }
264264 to.receive(col, at)
@@ -173,35 +173,35 @@ func (c *Core) applyAction(a Action) Effect {
173 if name, ok := NamedArg(a); ok && c.bringNamed(name) {173 if name, ok := NamedArg(a); ok && c.bringNamed(name) {
174 return Effect{}174 return Effect{}
175 }175 }
176- dir := 0
177- switch a {
178- case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft:
179- dir = -1
180- case ActionFocusMonitorRight, ActionMoveWorkspaceRight:
181- dir = 1
182- }
183 switch a {176 switch a {
184 case ActionFocusMonitorLeft, ActionFocusMonitorRight:177 case ActionFocusMonitorLeft, ActionFocusMonitorRight:
185- if i := c.neighbor(dir); i >= 0 {178+ d := dirLeft
179+ if a == ActionFocusMonitorRight {
180+ d = dirRight
181+ }
182+ if i := c.neighbor(d); i >= 0 {
186 c.focusScreen = i183 c.focusScreen = i
187 }184 }
188 return Effect{}185 return Effect{}
189 case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight:186 case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight:
190- c.moveWorkspace(dir)187+ d := dirLeft
188+ if a == ActionMoveWorkspaceRight {
189+ d = dirRight
190+ }
191+ c.moveWorkspace(d)
191 return Effect{}192 return Effect{}
192 case ActionFocusColumnLeft, ActionFocusColumnRight:193 case ActionFocusColumnLeft, ActionFocusColumnRight:
193 w := c.cur().mon.Current()194 w := c.cur().mon.Current()
195+ dir, d := 1, dirRight
194 if a == ActionFocusColumnLeft {196 if a == ActionFocusColumnLeft {
195- dir = -1197+ dir, d = -1, dirLeft
196- } else {
197- dir = 1
198 }198 }
199 // A covering fullscreen window is left for the window on that side;199 // A covering fullscreen window is left for the window on that side;
200 // with none (the workspace or stash edge), the move goes to the200 // with none (the workspace or stash edge), the move goes to the
201 // neighbor monitor and fullscreen stays.201 // neighbor monitor and fullscreen stays.
202 if w.pinned() {202 if w.pinned() {
203 if !w.FocusColumn(dir) {203 if !w.FocusColumn(dir) {
204- if i := c.neighbor(dir); i >= 0 {204+ if i := c.neighbor(d); i >= 0 {
205 c.focusScreen = i205 c.focusScreen = i
206 }206 }
207 }207 }
@@ -211,14 +211,14 @@ func (c *Core) applyAction(a Action) Effect {
211 // a native float leaves for the stash or the columns, and the stash211 // a native float leaves for the stash or the columns, and the stash
212 // keeps the focus at its ends.212 // keeps the focus at its ends.
213 edge := w.columnToward(dir) < 0 && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat())213 edge := w.columnToward(dir) < 0 && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat())
214- if i := c.neighbor(dir); edge && i >= 0 {214+ if i := c.neighbor(d); edge && i >= 0 {
215 c.focusScreen = i215 c.focusScreen = i
216 return Effect{}216 return Effect{}
217 }217 }
218 case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:218 case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
219- dir = 1219+ dir, d := 1, dirRight
220 if a == ActionConsumeOrExpelLeft {220 if a == ActionConsumeOrExpelLeft {
221- dir = -1221+ dir, d = -1, dirLeft
222 }222 }
223 from := c.cur()223 from := c.cur()
224 if from.mon.Current().ConsumeOrExpel(dir) {224 if from.mon.Current().ConsumeOrExpel(dir) {
@@ -227,7 +227,7 @@ func (c *Core) applyAction(a Action) Effect {
227 }227 }
228 // Full fixed workspace, stacked edge column: one hop to the228 // Full fixed workspace, stacked edge column: one hop to the
229 // neighbor monitor, if any; focus follows the window.229 // neighbor monitor, if any; focus follows the window.
230- i := c.neighbor(dir)230+ i := c.neighbor(d)
231 if i < 0 {231 if i < 0 {
232 return Effect{}232 return Effect{}
233 }233 }
@@ -244,12 +244,12 @@ func (c *Core) applyAction(a Action) Effect {
244 // Past the edge the column moves to the neighbor screen, on the244 // Past the edge the column moves to the neighbor screen, on the
245 // side facing this one, and focus follows it.245 // side facing this one, and focus follows it.
246 w := c.cur().mon.Current()246 w := c.cur().mon.Current()
247- dir = 1247+ d := dirRight
248 if a == ActionMoveColumnLeft {248 if a == ActionMoveColumnLeft {
249- dir = -1249+ d = dirLeft
250 }250 }
251- edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1)251+ edge := (d == dirLeft && w.Focus == 0) || (d == dirRight && w.Focus == len(w.Columns)-1)
252- if i := c.neighbor(dir); edge && i >= 0 {252+ if i := c.neighbor(d); edge && i >= 0 {
253 col, ok := w.takeColumn()253 col, ok := w.takeColumn()
254 if !ok {254 if !ok {
255 return Effect{}255 return Effect{}
@@ -258,7 +258,7 @@ func (c *Core) applyAction(a Action) Effect {
258 c.focusScreen = i258 c.focusScreen = i
259 to := c.cur().mon.Current()259 to := c.cur().mon.Current()
260 at := 0260 at := 0
261- if dir < 0 {261+ if d == dirLeft {
262 at = len(to.Columns)262 at = len(to.Columns)
263 }263 }
264 to.receive(col, at)264 to.receive(col, at)
A internal/core/placement.go +32 -0
@@ -0,0 +1,32 @@
1+package core
2+
3+import "image"
4+
5+// placeItem is one output to place: its connector name, logical size and, when set, the
6+// explicit origin from output management (output.<name> position).
7+type placeItem struct {
8+ name string
9+ w, h int
10+ pos *image.Point
11+}
12+
13+// placeOutputs returns the logical origin of every item, in item order.
14+// Items with an explicit position keep it; the others follow, in order, from
15+// the rightmost edge placed so far (never left of 0), at y=0.
16+func placeOutputs(items []placeItem) []image.Point {
17+ out := make([]image.Point, len(items))
18+ x := 0
19+ for i, it := range items {
20+ if it.pos != nil {
21+ out[i] = *it.pos
22+ x = max(x, it.pos.X+it.w)
23+ }
24+ }
25+ for i, it := range items {
26+ if it.pos == nil {
27+ out[i] = image.Point{X: x}
28+ x += it.w
29+ }
30+ }
31+ return out
32+}
@@ -0,0 +1,32 @@
1+package core
2+
3+import "image"
4+
5+// placeItem is one output to place: its connector name, logical size and, when set, the
6+// explicit origin from output management (output.<name> position).
7+type placeItem struct {
8+ name string
9+ w, h int
10+ pos *image.Point
11+}
12+
13+// placeOutputs returns the logical origin of every item, in item order.
14+// Items with an explicit position keep it; the others follow, in order, from
15+// the rightmost edge placed so far (never left of 0), at y=0.
16+func placeOutputs(items []placeItem) []image.Point {
17+ out := make([]image.Point, len(items))
18+ x := 0
19+ for i, it := range items {
20+ if it.pos != nil {
21+ out[i] = *it.pos
22+ x = max(x, it.pos.X+it.w)
23+ }
24+ }
25+ for i, it := range items {
26+ if it.pos == nil {
27+ out[i] = image.Point{X: x}
28+ x += it.w
29+ }
30+ }
31+ return out
32+}
A internal/core/placement_test.go +46 -0
@@ -0,0 +1,46 @@
1+package core
2+
3+import (
4+ "image"
5+ "slices"
6+ "testing"
7+)
8+
9+func TestPlaceOutputs(t *testing.T) {
10+ at := func(x, y int) *image.Point { return &image.Point{X: x, Y: y} }
11+ tests := []struct {
12+ name string
13+ items []placeItem
14+ want []image.Point
15+ }{
16+ {"none", nil, []image.Point{}},
17+ {"one automatic", []placeItem{{name: "A", w: 400, h: 300}}, []image.Point{{X: 0, Y: 0}}},
18+ {
19+ "three automatic",
20+ []placeItem{{name: "A", w: 400, h: 300}, {name: "B", w: 200, h: 300}, {name: "C", w: 300, h: 300}},
21+ []image.Point{{X: 0, Y: 0}, {X: 400, Y: 0}, {X: 600, Y: 0}},
22+ },
23+ {
24+ "explicit position then automatic",
25+ []placeItem{{name: "DP-1", w: 400, h: 300}, {name: "DP-2", w: 400, h: 300, pos: at(100, 40)}},
26+ []image.Point{{X: 500, Y: 0}, {X: 100, Y: 40}},
27+ },
28+ {
29+ "explicit negative x leaves automatic at zero",
30+ []placeItem{{name: "A", w: 300, h: 300}, {name: "B", w: 400, h: 300, pos: at(-500, 0)}},
31+ []image.Point{{X: 0, Y: 0}, {X: -500, Y: 0}},
32+ },
33+ {
34+ "all explicit with negative position",
35+ []placeItem{{name: "A", w: 400, h: 300, pos: at(-200, 12)}, {name: "B", w: 400, h: 300, pos: at(20, -120)}},
36+ []image.Point{{X: -200, Y: 12}, {X: 20, Y: -120}},
37+ },
38+ }
39+ for _, tc := range tests {
40+ t.Run(tc.name, func(t *testing.T) {
41+ if got := placeOutputs(tc.items); !slices.Equal(got, tc.want) {
42+ t.Fatalf("got %v, want %v", got, tc.want)
43+ }
44+ })
45+ }
46+}
@@ -0,0 +1,46 @@
1+package core
2+
3+import (
4+ "image"
5+ "slices"
6+ "testing"
7+)
8+
9+func TestPlaceOutputs(t *testing.T) {
10+ at := func(x, y int) *image.Point { return &image.Point{X: x, Y: y} }
11+ tests := []struct {
12+ name string
13+ items []placeItem
14+ want []image.Point
15+ }{
16+ {"none", nil, []image.Point{}},
17+ {"one automatic", []placeItem{{name: "A", w: 400, h: 300}}, []image.Point{{X: 0, Y: 0}}},
18+ {
19+ "three automatic",
20+ []placeItem{{name: "A", w: 400, h: 300}, {name: "B", w: 200, h: 300}, {name: "C", w: 300, h: 300}},
21+ []image.Point{{X: 0, Y: 0}, {X: 400, Y: 0}, {X: 600, Y: 0}},
22+ },
23+ {
24+ "explicit position then automatic",
25+ []placeItem{{name: "DP-1", w: 400, h: 300}, {name: "DP-2", w: 400, h: 300, pos: at(100, 40)}},
26+ []image.Point{{X: 500, Y: 0}, {X: 100, Y: 40}},
27+ },
28+ {
29+ "explicit negative x leaves automatic at zero",
30+ []placeItem{{name: "A", w: 300, h: 300}, {name: "B", w: 400, h: 300, pos: at(-500, 0)}},
31+ []image.Point{{X: 0, Y: 0}, {X: -500, Y: 0}},
32+ },
33+ {
34+ "all explicit with negative position",
35+ []placeItem{{name: "A", w: 400, h: 300, pos: at(-200, 12)}, {name: "B", w: 400, h: 300, pos: at(20, -120)}},
36+ []image.Point{{X: -200, Y: 12}, {X: 20, Y: -120}},
37+ },
38+ }
39+ for _, tc := range tests {
40+ t.Run(tc.name, func(t *testing.T) {
41+ if got := placeOutputs(tc.items); !slices.Equal(got, tc.want) {
42+ t.Fatalf("got %v, want %v", got, tc.want)
43+ }
44+ })
45+ }
46+}
M internal/core/screens.go +52 -45
@@ -7,8 +7,8 @@ import (
77 )
88
99 // screen is one connected output: its monitor tree, scale, layer surfaces and
10-// place in the global layout (ADR 011). Outputs are placed left to right, in
11-// config order (output.<name>.*), then in connection order.
10+// place in the global layout (ADR 011). Outputs are ordered by config
11+// (output.<name>.*), then by connection; order places them.
1212 type screen struct {
1313 info ports.OutputInfo
1414 mon *Monitor
@@ -49,44 +49,39 @@ func (s *screen) arrange() {
4949 s.mon.SetUsable(usable)
5050 }
5151
52-// order sorts screens by config order, then connection order, and assigns x.
52+// order sorts screens by config order, then connection order, and places them
53+// in the global layout (placeOutputs).
5354 func (c *Core) order() {
5455 rank := func(s *screen) int {
55- for i, o := range c.cfg.Outputs {
56- if o.Name == s.name() {
57- return i
58- }
56+ if i := slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == s.name() }); i >= 0 {
57+ return i
5958 }
6059 return len(c.cfg.Outputs)
6160 }
6261 focused := c.cur()
6362 slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) })
6463 c.focusScreen = slices.Index(c.screens, focused)
65- // Explicitly placed outputs reserve their position first; auto-placed
66- // outputs follow the rightmost explicit edge in configuration order.
67- x := 0
68- for _, s := range c.screens {
69- for _, o := range c.cfg.Outputs {
70- if o.Name == s.name() && o.Pos != nil {
71- s.x, s.y = o.Pos.X, o.Pos.Y
72- x = max(x, s.x+s.mon.Output().W)
73- break
74- }
64+ items := make([]placeItem, len(c.screens))
65+ for i, s := range c.screens {
66+ o := s.mon.Output()
67+ items[i].name, items[i].w, items[i].h = s.name(), o.W, o.H
68+ if cfg, ok := c.outputConfig(s.name()); ok {
69+ items[i].pos = cfg.Pos
7570 }
7671 }
77- for _, s := range c.screens {
78- explicit := false
79- for _, o := range c.cfg.Outputs {
80- if o.Name == s.name() && o.Pos != nil {
81- explicit = true
82- break
83- }
84- }
85- if !explicit {
86- s.x, s.y = x, 0
87- x += s.mon.Output().W
88- }
72+ for i, p := range placeOutputs(items) {
73+ c.screens[i].x, c.screens[i].y = p.X, p.Y
74+ }
75+}
76+
77+// outputConfig returns the output.<name> entry; names are unique in the
78+// config.
79+func (c *Core) outputConfig(name string) (ports.OutputConfig, bool) {
80+ i := slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == name })
81+ if i < 0 {
82+ return ports.OutputConfig{}, false
8983 }
84+ return c.cfg.Outputs[i], true
9085 }
9186
9287 // layout lists the connected outputs; the placeholder is not one.
@@ -129,12 +124,8 @@ func (c *Core) byName(name string) (*screen, *Workspace) {
129124
130125 // isPrimary reports output.<name>.primary.
131126 func (c *Core) isPrimary(name string) bool {
132- for _, o := range c.cfg.Outputs {
133- if o.Name == name && o.Primary {
134- return true
135- }
136- }
137- return false
127+ o, _ := c.outputConfig(name)
128+ return o.Primary
138129 }
139130
140131 // anyWindow reports whether any workspace holds a window.
@@ -152,10 +143,8 @@ func (c *Core) anyWindow() bool {
152143 // configScale is output.<name>.scale, else 1.
153144 func (c *Core) configScale(name string) float64 {
154145 v := 1.0
155- for _, o := range c.cfg.Outputs {
156- if o.Name == name && o.Scale != 0 {
157- v = o.Scale
158- }
146+ if o, ok := c.outputConfig(name); ok && o.Scale != 0 {
147+ v = o.Scale
159148 }
160149 return SnapScale(v)
161150 }
@@ -407,10 +396,28 @@ func (c *Core) settings(m *Monitor) {
407396 m.SetFollowMove(c.cfg.Focus.FollowMove)
408397 }
409398
410-// neighbor returns the screen index left (-1) or right (+1) of the focused
411-// one, or -1 at the edge.
412-func (c *Core) neighbor(dir int) int {
413- i := c.focusScreen + dir
399+// direction is where a monitor action looks for a neighbor screen.
400+type direction uint8
401+
402+const (
403+ dirLeft direction = iota
404+ dirRight
405+ dirUp
406+ dirDown
407+)
408+
409+// neighbor returns the index of the screen next to the focused one in
410+// direction d, or -1 at the edge.
411+func (c *Core) neighbor(d direction) int {
412+ var i int
413+ switch d {
414+ case dirLeft:
415+ i = c.focusScreen - 1
416+ case dirRight:
417+ i = c.focusScreen + 1
418+ default:
419+ return -1
420+ }
414421 if i < 0 || i >= len(c.screens) {
415422 return -1
416423 }
@@ -419,8 +426,8 @@ func (c *Core) neighbor(dir int) int {
419426
420427 // moveWorkspace moves the workspace on screen to the neighbor screen, where
421428 // it is appended and shown; it becomes its new home (ADR 011).
422-func (c *Core) moveWorkspace(dir int) {
423- to := c.neighbor(dir)
429+func (c *Core) moveWorkspace(d direction) {
430+ to := c.neighbor(d)
424431 if to < 0 {
425432 return
426433 }
@@ -7,8 +7,8 @@ import (
7 )7 )
8 8
9 // screen is one connected output: its monitor tree, scale, layer surfaces and9 // screen is one connected output: its monitor tree, scale, layer surfaces and
10-// place in the global layout (ADR 011). Outputs are placed left to right, in10+// place in the global layout (ADR 011). Outputs are ordered by config
11-// config order (output.<name>.*), then in connection order.11+// (output.<name>.*), then by connection; order places them.
12 type screen struct {12 type screen struct {
13 info ports.OutputInfo13 info ports.OutputInfo
14 mon *Monitor14 mon *Monitor
@@ -49,44 +49,39 @@ func (s *screen) arrange() {
49 s.mon.SetUsable(usable)49 s.mon.SetUsable(usable)
50 }50 }
51 51
52-// order sorts screens by config order, then connection order, and assigns x.52+// order sorts screens by config order, then connection order, and places them
53+// in the global layout (placeOutputs).
53 func (c *Core) order() {54 func (c *Core) order() {
54 rank := func(s *screen) int {55 rank := func(s *screen) int {
55- for i, o := range c.cfg.Outputs {56+ if i := slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == s.name() }); i >= 0 {
56- if o.Name == s.name() {57+ return i
57- return i
58- }
59 }58 }
60 return len(c.cfg.Outputs)59 return len(c.cfg.Outputs)
61 }60 }
62 focused := c.cur()61 focused := c.cur()
63 slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) })62 slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) })
64 c.focusScreen = slices.Index(c.screens, focused)63 c.focusScreen = slices.Index(c.screens, focused)
65- // Explicitly placed outputs reserve their position first; auto-placed64+ items := make([]placeItem, len(c.screens))
66- // outputs follow the rightmost explicit edge in configuration order.65+ for i, s := range c.screens {
67- x := 066+ o := s.mon.Output()
68- for _, s := range c.screens {67+ items[i].name, items[i].w, items[i].h = s.name(), o.W, o.H
69- for _, o := range c.cfg.Outputs {68+ if cfg, ok := c.outputConfig(s.name()); ok {
70- if o.Name == s.name() && o.Pos != nil {69+ items[i].pos = cfg.Pos
71- s.x, s.y = o.Pos.X, o.Pos.Y
72- x = max(x, s.x+s.mon.Output().W)
73- break
74- }
75 }70 }
76 }71 }
77- for _, s := range c.screens {72+ for i, p := range placeOutputs(items) {
78- explicit := false73+ c.screens[i].x, c.screens[i].y = p.X, p.Y
79- for _, o := range c.cfg.Outputs {74+ }
80- if o.Name == s.name() && o.Pos != nil {75+}
81- explicit = true76+
82- break77+// outputConfig returns the output.<name> entry; names are unique in the
83- }78+// config.
84- }79+func (c *Core) outputConfig(name string) (ports.OutputConfig, bool) {
85- if !explicit {80+ i := slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == name })
86- s.x, s.y = x, 081+ if i < 0 {
87- x += s.mon.Output().W82+ return ports.OutputConfig{}, false
88- }
89 }83 }
84+ return c.cfg.Outputs[i], true
90 }85 }
91 86
92 // layout lists the connected outputs; the placeholder is not one.87 // layout lists the connected outputs; the placeholder is not one.
@@ -129,12 +124,8 @@ func (c *Core) byName(name string) (*screen, *Workspace) {
129 124
130 // isPrimary reports output.<name>.primary.125 // isPrimary reports output.<name>.primary.
131 func (c *Core) isPrimary(name string) bool {126 func (c *Core) isPrimary(name string) bool {
132- for _, o := range c.cfg.Outputs {127+ o, _ := c.outputConfig(name)
133- if o.Name == name && o.Primary {128+ return o.Primary
134- return true
135- }
136- }
137- return false
138 }129 }
139 130
140 // anyWindow reports whether any workspace holds a window.131 // anyWindow reports whether any workspace holds a window.
@@ -152,10 +143,8 @@ func (c *Core) anyWindow() bool {
152 // configScale is output.<name>.scale, else 1.143 // configScale is output.<name>.scale, else 1.
153 func (c *Core) configScale(name string) float64 {144 func (c *Core) configScale(name string) float64 {
154 v := 1.0145 v := 1.0
155- for _, o := range c.cfg.Outputs {146+ if o, ok := c.outputConfig(name); ok && o.Scale != 0 {
156- if o.Name == name && o.Scale != 0 {147+ v = o.Scale
157- v = o.Scale
158- }
159 }148 }
160 return SnapScale(v)149 return SnapScale(v)
161 }150 }
@@ -407,10 +396,28 @@ func (c *Core) settings(m *Monitor) {
407 m.SetFollowMove(c.cfg.Focus.FollowMove)396 m.SetFollowMove(c.cfg.Focus.FollowMove)
408 }397 }
409 398
410-// neighbor returns the screen index left (-1) or right (+1) of the focused399+// direction is where a monitor action looks for a neighbor screen.
411-// one, or -1 at the edge.400+type direction uint8
412-func (c *Core) neighbor(dir int) int {401+
413- i := c.focusScreen + dir402+const (
403+ dirLeft direction = iota
404+ dirRight
405+ dirUp
406+ dirDown
407+)
408+
409+// neighbor returns the index of the screen next to the focused one in
410+// direction d, or -1 at the edge.
411+func (c *Core) neighbor(d direction) int {
412+ var i int
413+ switch d {
414+ case dirLeft:
415+ i = c.focusScreen - 1
416+ case dirRight:
417+ i = c.focusScreen + 1
418+ default:
419+ return -1
420+ }
414 if i < 0 || i >= len(c.screens) {421 if i < 0 || i >= len(c.screens) {
415 return -1422 return -1
416 }423 }
@@ -419,8 +426,8 @@ func (c *Core) neighbor(dir int) int {
419 426
420 // moveWorkspace moves the workspace on screen to the neighbor screen, where427 // moveWorkspace moves the workspace on screen to the neighbor screen, where
421 // it is appended and shown; it becomes its new home (ADR 011).428 // it is appended and shown; it becomes its new home (ADR 011).
422-func (c *Core) moveWorkspace(dir int) {429+func (c *Core) moveWorkspace(d direction) {
423- to := c.neighbor(dir)430+ to := c.neighbor(d)
424 if to < 0 {431 if to < 0 {
425 return432 return
426 }433 }

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