detached f23fedf

refactor(core): document overlap reach limit and parse output relations without a map

commit
f23fedf30b1f82f58d437e22e9ea468701adc357
author
brice · · 2026-10-03 14:22 UTC
parent
ac43558
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +32 -16
M internal/adapters/config/config.go +14 -8
@@ -234,17 +234,23 @@ func Load(path string) (ports.Config, []Warning, error) {
234234 // real layout but small enough that placement arithmetic cannot overflow.
235235 const maxOutputOffset = 65536
236236
237+// outputRelations lists the relation suffixes, tried in order.
238+var outputRelations = [...]struct {
239+ suffix string
240+ rel ports.OutputRelation
241+}{
242+ {".right-of", ports.RelationRightOf},
243+ {".left-of", ports.RelationLeftOf},
244+ {".above", ports.RelationAbove},
245+ {".below", ports.RelationBelow},
246+}
247+
237248 // outputRelation splits "<output>.right-of" (left-of, above, below) into the
238249 // relation and the output name.
239250 func outputRelation(name string) (ports.OutputRelation, string, bool) {
240- for suffix, rel := range map[string]ports.OutputRelation{
241- ".right-of": ports.RelationRightOf,
242- ".left-of": ports.RelationLeftOf,
243- ".above": ports.RelationAbove,
244- ".below": ports.RelationBelow,
245- } {
246- if base, ok := strings.CutSuffix(name, suffix); ok {
247- return rel, base, true
251+ for _, r := range outputRelations {
252+ if base, ok := strings.CutSuffix(name, r.suffix); ok {
253+ return r.rel, base, true
248254 }
249255 }
250256 return ports.RelationNone, "", false
@@ -234,17 +234,23 @@ func Load(path string) (ports.Config, []Warning, error) {
234 // real layout but small enough that placement arithmetic cannot overflow.234 // real layout but small enough that placement arithmetic cannot overflow.
235 const maxOutputOffset = 65536235 const maxOutputOffset = 65536
236 236
237+// outputRelations lists the relation suffixes, tried in order.
238+var outputRelations = [...]struct {
239+ suffix string
240+ rel ports.OutputRelation
241+}{
242+ {".right-of", ports.RelationRightOf},
243+ {".left-of", ports.RelationLeftOf},
244+ {".above", ports.RelationAbove},
245+ {".below", ports.RelationBelow},
246+}
247+
237 // outputRelation splits "<output>.right-of" (left-of, above, below) into the248 // outputRelation splits "<output>.right-of" (left-of, above, below) into the
238 // relation and the output name.249 // relation and the output name.
239 func outputRelation(name string) (ports.OutputRelation, string, bool) {250 func outputRelation(name string) (ports.OutputRelation, string, bool) {
240- for suffix, rel := range map[string]ports.OutputRelation{251+ for _, r := range outputRelations {
241- ".right-of": ports.RelationRightOf,252+ if base, ok := strings.CutSuffix(name, r.suffix); ok {
242- ".left-of": ports.RelationLeftOf,253+ return r.rel, base, true
243- ".above": ports.RelationAbove,
244- ".below": ports.RelationBelow,
245- } {
246- if base, ok := strings.CutSuffix(name, suffix); ok {
247- return rel, base, true
248 }254 }
249 }255 }
250 return ports.RelationNone, "", false256 return ports.RelationNone, "", false
M internal/core/neighbor_test.go +5 -0
@@ -83,6 +83,11 @@ func TestNeighbor(t *testing.T) {
8383 // R is fully left of Y and wins over the closer, overlapping X.
8484 {"Y", [4]string{"R", "", "", "X"}},
8585 }},
86+ {"same-height outputs anchored to the same reference: one is unreachable by direction", []tile{{"R", 0, 0, 100, 100}, {"Y", 100, 0, 300, 100}, {"X", 100, 0, 100, 100}}, []step{
87+ {"R", [4]string{"", "Y", "", ""}},
88+ {"Y", [4]string{"R", "", "", ""}},
89+ {"X", [4]string{"R", "Y", "", ""}},
90+ }},
8691 {"strict candidate beats a closer overlapping one", []tile{{"A", 0, 0, 100, 100}, {"near", 50, 0, 100, 100}, {"far", 300, 0, 100, 100}}, []step{
8792 {"A", [4]string{"", "far", "", ""}},
8893 {"near", [4]string{"A", "far", "", ""}},
@@ -83,6 +83,11 @@ func TestNeighbor(t *testing.T) {
83 // R is fully left of Y and wins over the closer, overlapping X.83 // R is fully left of Y and wins over the closer, overlapping X.
84 {"Y", [4]string{"R", "", "", "X"}},84 {"Y", [4]string{"R", "", "", "X"}},
85 }},85 }},
86+ {"same-height outputs anchored to the same reference: one is unreachable by direction", []tile{{"R", 0, 0, 100, 100}, {"Y", 100, 0, 300, 100}, {"X", 100, 0, 100, 100}}, []step{
87+ {"R", [4]string{"", "Y", "", ""}},
88+ {"Y", [4]string{"R", "", "", ""}},
89+ {"X", [4]string{"R", "Y", "", ""}},
90+ }},
86 {"strict candidate beats a closer overlapping one", []tile{{"A", 0, 0, 100, 100}, {"near", 50, 0, 100, 100}, {"far", 300, 0, 100, 100}}, []step{91 {"strict candidate beats a closer overlapping one", []tile{{"A", 0, 0, 100, 100}, {"near", 50, 0, 100, 100}, {"far", 300, 0, 100, 100}}, []step{
87 {"A", [4]string{"", "far", "", ""}},92 {"A", [4]string{"", "far", "", ""}},
88 {"near", [4]string{"A", "far", "", ""}},93 {"near", [4]string{"A", "far", "", ""}},
M internal/core/screens.go +8 -6
@@ -415,12 +415,14 @@ const (
415415 // direction d, or -1 at the edge. Candidates lie fully beyond the focused
416416 // screen's edge; one sharing part of that edge wins over a diagonal one, then
417417 // the smallest gap, the largest shared edge, the closest centers and the
418-// lowest index decide. When none lies fully beyond, a second pass handles
419-// overlapping outputs (for instance two anchored to the same reference): it
420-// takes the screens whose rectangle intersects the focused one and whose
421-// center is strictly beyond the focused center, ranked the same way with a
422-// gap of 0, so they stay reachable. Screens with identical centers are not
423-// reachable by direction. There is no wrap.
418+// lowest index decide. Overlapping outputs (for instance two anchored to the
419+// same reference) are reached only when no screen lies fully beyond the edge
420+// in that direction: a second pass then takes the screens whose rectangle
421+// intersects the focused one and whose center is strictly beyond the focused
422+// center, ranked the same way with a gap of 0. A screen that does have a
423+// strict candidate never falls back, so an overlapping one can stay out of
424+// reach. Screens with the same center on the axis are unreachable by direction
425+// (the pointer still reaches them). There is no wrap.
424426 func (c *Core) neighbor(d direction) int {
425427 if d < dirLeft || d > dirDown {
426428 return -1
@@ -415,12 +415,14 @@ const (
415 // direction d, or -1 at the edge. Candidates lie fully beyond the focused415 // direction d, or -1 at the edge. Candidates lie fully beyond the focused
416 // screen's edge; one sharing part of that edge wins over a diagonal one, then416 // screen's edge; one sharing part of that edge wins over a diagonal one, then
417 // the smallest gap, the largest shared edge, the closest centers and the417 // the smallest gap, the largest shared edge, the closest centers and the
418-// lowest index decide. When none lies fully beyond, a second pass handles418+// lowest index decide. Overlapping outputs (for instance two anchored to the
419-// overlapping outputs (for instance two anchored to the same reference): it419+// same reference) are reached only when no screen lies fully beyond the edge
420-// takes the screens whose rectangle intersects the focused one and whose420+// in that direction: a second pass then takes the screens whose rectangle
421-// center is strictly beyond the focused center, ranked the same way with a421+// intersects the focused one and whose center is strictly beyond the focused
422-// gap of 0, so they stay reachable. Screens with identical centers are not422+// center, ranked the same way with a gap of 0. A screen that does have a
423-// reachable by direction. There is no wrap.423+// strict candidate never falls back, so an overlapping one can stay out of
424+// reach. Screens with the same center on the axis are unreachable by direction
425+// (the pointer still reaches them). There is no wrap.
424 func (c *Core) neighbor(d direction) int {426 func (c *Core) neighbor(d direction) int {
425 if d < dirLeft || d > dirDown {427 if d < dirLeft || d > dirDown {
426 return -1428 return -1
M internal/core/screens_test.go +5 -2
@@ -1398,8 +1398,11 @@ func TestPoweredOffScreenStaysNeighbor(t *testing.T) {
13981398 current := focusedOutput(r, lastOutputs(t, r.commands).Focused)
13991399 r.client <- ports.OutputPower{Output: "DP-2", On: false}
14001400 _ = receive(t, r.scenes)
1401- if out := lastOutputs(t, r.commands); current != "DP-1" || !slices.Equal(out.Off, []string{"DP-2"}) {
1402- t.Fatalf("focused %s, off %v", current, out.Off)
1401+ if current != "DP-1" {
1402+ t.Fatalf("focused %s, want DP-1", current)
1403+ }
1404+ if out := lastOutputs(t, r.commands); !slices.Equal(out.Off, []string{"DP-2"}) {
1405+ t.Fatalf("off %v, want [DP-2]", out.Off)
14031406 }
14041407 r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
14051408 if got := focusedOutput(r, current); got != "DP-2" {
@@ -1398,8 +1398,11 @@ func TestPoweredOffScreenStaysNeighbor(t *testing.T) {
1398 current := focusedOutput(r, lastOutputs(t, r.commands).Focused)1398 current := focusedOutput(r, lastOutputs(t, r.commands).Focused)
1399 r.client <- ports.OutputPower{Output: "DP-2", On: false}1399 r.client <- ports.OutputPower{Output: "DP-2", On: false}
1400 _ = receive(t, r.scenes)1400 _ = receive(t, r.scenes)
1401- if out := lastOutputs(t, r.commands); current != "DP-1" || !slices.Equal(out.Off, []string{"DP-2"}) {1401+ if current != "DP-1" {
1402- t.Fatalf("focused %s, off %v", current, out.Off)1402+ t.Fatalf("focused %s, want DP-1", current)
1403+ }
1404+ if out := lastOutputs(t, r.commands); !slices.Equal(out.Off, []string{"DP-2"}) {
1405+ t.Fatalf("off %v, want [DP-2]", out.Off)
1403 }1406 }
1404 r.key(t, "Right", ports.ModAlt|ports.ModCtrl)1407 r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
1405 if got := focusedOutput(r, current); got != "DP-2" {1408 if got := focusedOutput(r, current); got != "DP-2" {

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