detached 111a4b4

fix(selection): place output details at the top right

commit
111a4b4cb837d0b22e4d697e962ea9ae99c6c749
author
brice · · 2026-09-30 13:51 UTC
committer
brice · · 2026-09-30 14:48 UTC
parent
2c1e060
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +30 -3
M internal/adapters/selection/readability_test.go +29 -2
@@ -71,11 +71,38 @@ func TestWorkspaceLabelAvoidsOutputHeader(t *testing.T) {
7171 m.input(key("w"))
7272 s := m.scene()
7373 assert.Equal(t, "640 × 360", m.label.text)
74- assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h)
75- assert.GreaterOrEqual(t, s.label.y, 60.0, "label falls inside the workspace")
74+ assert.True(t, s.label.x+s.label.w <= s.header.x || s.label.x >= s.header.x+s.header.w ||
75+ s.label.y+s.label.h <= s.header.y || s.label.y >= s.header.y+s.header.h, "label and header must not overlap")
7676 assert.LessOrEqual(t, s.label.y+s.label.h, 480.0)
7777 }
7878
79+func TestRightWorkspaceLabelAvoidsOutputHeader(t *testing.T) {
80+ m, _ := newBareModel()
81+ m.setWorkspaces([]ports.Workspace{{ID: 11, OutputID: 7, Active: true, Region: ports.Region{X: 470, Y: 60, Width: 170, Height: 360}}})
82+ m.resize(640, 480, 1)
83+ m.input(key("w"))
84+ s := m.scene()
85+ assert.Greater(t, s.label.x+s.label.w, s.header.x, "label shares the header's horizontal range")
86+ assert.Less(t, s.label.x, s.header.x+s.header.w)
87+ assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h, "collision moves dimensions below the header")
88+ assert.GreaterOrEqual(t, s.label.y, 60.0, "dimensions move inside the workspace")
89+}
90+
91+func TestOutputHeaderAtTopRight(t *testing.T) {
92+ for _, width := range []int{1920, 640, 200, 20} {
93+ m := newTestModel(t)
94+ m.resize(width, 480, 1)
95+ s := m.scene()
96+ wantRightGap := min(margin, float64(width)-s.header.w)
97+ assert.Equal(t, float64(width)-wantRightGap, s.header.x+s.header.w)
98+ assert.GreaterOrEqual(t, s.header.x, 0.0)
99+ }
100+ m := newTestModel(t)
101+ m.input(key("m"))
102+ s := m.scene()
103+ assert.Less(t, s.label.x+s.label.w, s.header.x, "monitor dimensions stay left of the output details")
104+}
105+
79106 func TestFooterUnchangedAllocations(t *testing.T) {
80107 if raceEnabled {
81108 t.Skip("the race detector adds allocations")
@@ -71,11 +71,38 @@ func TestWorkspaceLabelAvoidsOutputHeader(t *testing.T) {
71 m.input(key("w"))71 m.input(key("w"))
72 s := m.scene()72 s := m.scene()
73 assert.Equal(t, "640 × 360", m.label.text)73 assert.Equal(t, "640 × 360", m.label.text)
74- assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h)74+ assert.True(t, s.label.x+s.label.w <= s.header.x || s.label.x >= s.header.x+s.header.w ||
75- assert.GreaterOrEqual(t, s.label.y, 60.0, "label falls inside the workspace")75+ s.label.y+s.label.h <= s.header.y || s.label.y >= s.header.y+s.header.h, "label and header must not overlap")
76 assert.LessOrEqual(t, s.label.y+s.label.h, 480.0)76 assert.LessOrEqual(t, s.label.y+s.label.h, 480.0)
77 }77 }
78 78
79+func TestRightWorkspaceLabelAvoidsOutputHeader(t *testing.T) {
80+ m, _ := newBareModel()
81+ m.setWorkspaces([]ports.Workspace{{ID: 11, OutputID: 7, Active: true, Region: ports.Region{X: 470, Y: 60, Width: 170, Height: 360}}})
82+ m.resize(640, 480, 1)
83+ m.input(key("w"))
84+ s := m.scene()
85+ assert.Greater(t, s.label.x+s.label.w, s.header.x, "label shares the header's horizontal range")
86+ assert.Less(t, s.label.x, s.header.x+s.header.w)
87+ assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h, "collision moves dimensions below the header")
88+ assert.GreaterOrEqual(t, s.label.y, 60.0, "dimensions move inside the workspace")
89+}
90+
91+func TestOutputHeaderAtTopRight(t *testing.T) {
92+ for _, width := range []int{1920, 640, 200, 20} {
93+ m := newTestModel(t)
94+ m.resize(width, 480, 1)
95+ s := m.scene()
96+ wantRightGap := min(margin, float64(width)-s.header.w)
97+ assert.Equal(t, float64(width)-wantRightGap, s.header.x+s.header.w)
98+ assert.GreaterOrEqual(t, s.header.x, 0.0)
99+ }
100+ m := newTestModel(t)
101+ m.input(key("m"))
102+ s := m.scene()
103+ assert.Less(t, s.label.x+s.label.w, s.header.x, "monitor dimensions stay left of the output details")
104+}
105+
79 func TestFooterUnchangedAllocations(t *testing.T) {106 func TestFooterUnchangedAllocations(t *testing.T) {
80 if raceEnabled {107 if raceEnabled {
81 t.Skip("the race detector adds allocations")108 t.Skip("the race detector adds allocations")
M internal/adapters/selection/scene.go +1 -1
@@ -104,7 +104,7 @@ func (m *model) scene() scene {
104104 s.label = labelRect(fr, w, h)
105105 }
106106 hw, hh := min(headerWidth(m.header), w), min(headerH, h)
107- s.header = frect{min(margin, max(w-hw, 0)), min(margin, max(h-hh, 0)), hw, hh}
107+ s.header = frect{max(w-hw-margin, 0), min(margin, max(h-hh, 0)), hw, hh}
108108 // Keep workspace dimensions clear of the output header; region and
109109 // monitor labels retain their independent placement.
110110 if m.picker.Kind() == core.PickWorkspace && s.label.w > 0 &&
@@ -104,7 +104,7 @@ func (m *model) scene() scene {
104 s.label = labelRect(fr, w, h)104 s.label = labelRect(fr, w, h)
105 }105 }
106 hw, hh := min(headerWidth(m.header), w), min(headerH, h)106 hw, hh := min(headerWidth(m.header), w), min(headerH, h)
107- s.header = frect{min(margin, max(w-hw, 0)), min(margin, max(h-hh, 0)), hw, hh}107+ s.header = frect{max(w-hw-margin, 0), min(margin, max(h-hh, 0)), hw, hh}
108 // Keep workspace dimensions clear of the output header; region and108 // Keep workspace dimensions clear of the output header; region and
109 // monitor labels retain their independent placement.109 // monitor labels retain their independent placement.
110 if m.picker.Kind() == core.PickWorkspace && s.label.w > 0 &&110 if m.picker.Kind() == core.PickWorkspace && s.label.w > 0 &&

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 2

Tags 9

↑↓ moveenter openesc close