detached b752a4a

fix(layout): center the middle zone on the middle of the bar, not of the space between the side zones

commit
b752a4a8f858459b31c7ac884aa32430b133d123
author
brice · · 2026-10-05 16:04 UTC
parent
a3516e3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +44 -2
M internal/layout/layout.go +7 -2
@@ -234,10 +234,15 @@ func (l *Layout) Compose() []gpu.Cell {
234234 x += copy(l.row[x:limit], s.cells)
235235 }
236236 }
237- // Center zone in the free gap.
237+ // Center zone: on the middle of the bar, and only as far from it as the
238+ // left and right zones force. Centering it in the space they leave would
239+ // shift it whenever the two zones differ in width.
238240 gap := l.cols - right - left
239241 if center = min(center, gap); center > 0 {
240- x, limit = left+(gap-center)/2, left+(gap-center)/2+center
242+ start := (l.cols - center) / 2 // exactly on the middle
243+ start = max(start, left) // not over the left zone
244+ start = min(start, l.cols-right-center) // not over the right zone
245+ x, limit = start, start+center
241246 for _, s := range l.sources {
242247 if s.M.Zone == module.Center && x < limit {
243248 x += copy(l.row[x:limit], s.cells)
@@ -234,10 +234,15 @@ func (l *Layout) Compose() []gpu.Cell {
234 x += copy(l.row[x:limit], s.cells)234 x += copy(l.row[x:limit], s.cells)
235 }235 }
236 }236 }
237- // Center zone in the free gap.237+ // Center zone: on the middle of the bar, and only as far from it as the
238+ // left and right zones force. Centering it in the space they leave would
239+ // shift it whenever the two zones differ in width.
238 gap := l.cols - right - left240 gap := l.cols - right - left
239 if center = min(center, gap); center > 0 {241 if center = min(center, gap); center > 0 {
240- x, limit = left+(gap-center)/2, left+(gap-center)/2+center242+ start := (l.cols - center) / 2 // exactly on the middle
243+ start = max(start, left) // not over the left zone
244+ start = min(start, l.cols-right-center) // not over the right zone
245+ x, limit = start, start+center
241 for _, s := range l.sources {246 for _, s := range l.sources {
242 if s.M.Zone == module.Center && x < limit {247 if s.M.Zone == module.Center && x < limit {
243 x += copy(l.row[x:limit], s.cells)248 x += copy(l.row[x:limit], s.cells)
M internal/layout/layout_test.go +37 -0
@@ -184,3 +184,40 @@ func TestBrokenEscapeDoesNotSwallowNextFrames(t *testing.T) {
184184 }
185185 }
186186 }
187+
188+// The center zone sits on the middle of the bar, not on the middle of the
189+// space left between a wide left zone and a narrow right one.
190+func TestCenterIsOnTheMiddleOfTheBar(t *testing.T) {
191+ l, m := newTestLayout(40)
192+ publish(m[0], "LLLLLLLLLL") // 10 cells on the left
193+ publish(m[1], "cccc") // 4 in the middle
194+ publish(m[2], "RRRR") // 4 on the right
195+ l.Update()
196+ got := rowText(l)
197+ if i := indexOf(got, "cccc"); i != 18 {
198+ t.Fatalf("center starts at cell %d, want 18 (the bar is 40 cells wide): %q", i, got)
199+ }
200+}
201+
202+// When the middle of the bar is taken, the center moves just enough to fit.
203+func TestCenterMovesOnlyAsFarAsNeeded(t *testing.T) {
204+ l, m := newTestLayout(40)
205+ publish(m[0], "LLLLLLLLLLLLLLLLLLLL") // 20 cells: reaches past the ideal start of 18
206+ publish(m[1], "cccc")
207+ publish(m[2], "RRRR")
208+ l.Update()
209+ got := rowText(l)
210+ if i := indexOf(got, "cccc"); i != 20 {
211+ t.Fatalf("center starts at cell %d, want 20 (right after the left zone): %q", i, got)
212+ }
213+ // And the other way round: a wide right zone pushes it left.
214+ l, m = newTestLayout(40)
215+ publish(m[0], "LL")
216+ publish(m[1], "cccc")
217+ publish(m[2], "RRRRRRRRRRRRRRRRRRRR") // 20 cells: the center must end by cell 20
218+ l.Update()
219+ got = rowText(l)
220+ if i := indexOf(got, "cccc"); i != 16 {
221+ t.Fatalf("center starts at cell %d, want 16 (ending where the right zone starts): %q", i, got)
222+ }
223+}
@@ -184,3 +184,40 @@ func TestBrokenEscapeDoesNotSwallowNextFrames(t *testing.T) {
184 }184 }
185 }185 }
186 }186 }
187+
188+// The center zone sits on the middle of the bar, not on the middle of the
189+// space left between a wide left zone and a narrow right one.
190+func TestCenterIsOnTheMiddleOfTheBar(t *testing.T) {
191+ l, m := newTestLayout(40)
192+ publish(m[0], "LLLLLLLLLL") // 10 cells on the left
193+ publish(m[1], "cccc") // 4 in the middle
194+ publish(m[2], "RRRR") // 4 on the right
195+ l.Update()
196+ got := rowText(l)
197+ if i := indexOf(got, "cccc"); i != 18 {
198+ t.Fatalf("center starts at cell %d, want 18 (the bar is 40 cells wide): %q", i, got)
199+ }
200+}
201+
202+// When the middle of the bar is taken, the center moves just enough to fit.
203+func TestCenterMovesOnlyAsFarAsNeeded(t *testing.T) {
204+ l, m := newTestLayout(40)
205+ publish(m[0], "LLLLLLLLLLLLLLLLLLLL") // 20 cells: reaches past the ideal start of 18
206+ publish(m[1], "cccc")
207+ publish(m[2], "RRRR")
208+ l.Update()
209+ got := rowText(l)
210+ if i := indexOf(got, "cccc"); i != 20 {
211+ t.Fatalf("center starts at cell %d, want 20 (right after the left zone): %q", i, got)
212+ }
213+ // And the other way round: a wide right zone pushes it left.
214+ l, m = newTestLayout(40)
215+ publish(m[0], "LL")
216+ publish(m[1], "cccc")
217+ publish(m[2], "RRRRRRRRRRRRRRRRRRRR") // 20 cells: the center must end by cell 20
218+ l.Update()
219+ got = rowText(l)
220+ if i := indexOf(got, "cccc"); i != 16 {
221+ t.Fatalf("center starts at cell %d, want 16 (ending where the right zone starts): %q", i, got)
222+ }
223+}

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 5

Tags 4

↑↓ moveenter openesc close