detached f92d01c

style(status): reserve room beside recent sessions

commit
f92d01ca2db0e68f8b7555df9d79eaf705166706
author
brice · · 2026-07-05 14:31 UTC
parent
dae2682
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +78 -2
M internal/usecase/daemon/status.go +29 -2
@@ -207,8 +207,8 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe
207207 if feedback != "" {
208208 copyReserve = len([]rune(feedback)) + 2
209209 }
210- budget := rowLen - leftUsed - copyReserve
211- if budget <= 0 || len(entries) == 0 {
210+ physicalBudget := rowLen - leftUsed - copyReserve
211+ if physicalBudget <= 0 || len(entries) == 0 {
212212 return nil
213213 }
214214 cost := func(e mruSession) int {
@@ -221,6 +221,19 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe
221221 }
222222 return n
223223 }
224+
225+ budget := physicalBudget
226+ if feedback == "" {
227+ budget -= mruFutureRightReserve(rowLen)
228+ // Keep at least one recent session when it physically fits; the reserved
229+ // right side is only a budget preference, not a reason to hide all recents.
230+ if firstCost := cost(entries[0]); firstCost <= physicalBudget && budget < firstCost {
231+ budget = firstCost
232+ }
233+ }
234+ if budget <= 0 {
235+ return nil
236+ }
224237 used := 0
225238 for i, e := range entries {
226239 used += cost(e)
@@ -231,6 +244,20 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe
231244 return entries
232245 }
233246
247+func mruFutureRightReserve(rowLen int) int {
248+ if rowLen < 40 {
249+ return 0
250+ }
251+ reserve := rowLen / 4
252+ if reserve < 12 {
253+ return 12
254+ }
255+ if reserve > 24 {
256+ return 24
257+ }
258+ return reserve
259+}
260+
234261 func mruStyle(base renderer.Style, t themeui.Theme, i, count int) renderer.Style {
235262 if count <= 1 || !base.HasForegroundRGB || !base.HasBackgroundRGB || !t.HasBG {
236263 return base
@@ -207,8 +207,8 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe
207 if feedback != "" {207 if feedback != "" {
208 copyReserve = len([]rune(feedback)) + 2208 copyReserve = len([]rune(feedback)) + 2
209 }209 }
210- budget := rowLen - leftUsed - copyReserve210+ physicalBudget := rowLen - leftUsed - copyReserve
211- if budget <= 0 || len(entries) == 0 {211+ if physicalBudget <= 0 || len(entries) == 0 {
212 return nil212 return nil
213 }213 }
214 cost := func(e mruSession) int {214 cost := func(e mruSession) int {
@@ -221,6 +221,19 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe
221 }221 }
222 return n222 return n
223 }223 }
224+
225+ budget := physicalBudget
226+ if feedback == "" {
227+ budget -= mruFutureRightReserve(rowLen)
228+ // Keep at least one recent session when it physically fits; the reserved
229+ // right side is only a budget preference, not a reason to hide all recents.
230+ if firstCost := cost(entries[0]); firstCost <= physicalBudget && budget < firstCost {
231+ budget = firstCost
232+ }
233+ }
234+ if budget <= 0 {
235+ return nil
236+ }
224 used := 0237 used := 0
225 for i, e := range entries {238 for i, e := range entries {
226 used += cost(e)239 used += cost(e)
@@ -231,6 +244,20 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe
231 return entries244 return entries
232 }245 }
233 246
247+func mruFutureRightReserve(rowLen int) int {
248+ if rowLen < 40 {
249+ return 0
250+ }
251+ reserve := rowLen / 4
252+ if reserve < 12 {
253+ return 12
254+ }
255+ if reserve > 24 {
256+ return 24
257+ }
258+ return reserve
259+}
260+
234 func mruStyle(base renderer.Style, t themeui.Theme, i, count int) renderer.Style {261 func mruStyle(base renderer.Style, t themeui.Theme, i, count int) renderer.Style {
235 if count <= 1 || !base.HasForegroundRGB || !base.HasBackgroundRGB || !t.HasBG {262 if count <= 1 || !base.HasForegroundRGB || !base.HasBackgroundRGB || !t.HasBG {
236 return base263 return base
M internal/usecase/daemon/status_test.go +49 -0
@@ -395,6 +395,55 @@ func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {
395395 require.NotContains(t, text, "old")
396396 }
397397
398+func TestStatusBarMRUWidthAwareBudget(t *testing.T) {
399+ mru := make([]mruSession, 0, maxMRUSessions)
400+ for i := 1; i <= maxMRUSessions; i++ {
401+ mru = append(mru, mruSession{name: "recent" + strconv.Itoa(i)})
402+ }
403+ state := barState{status: statusSnapshot{session: "cur"}, mru: mru}
404+
405+ tests := []struct {
406+ name string
407+ cols int
408+ wantShown []string
409+ wantHidden []string
410+ }{
411+ {
412+ name: "narrow keeps first recent when it physically fits",
413+ cols: 15,
414+ wantShown: []string{"cur", "recent1"},
415+ wantHidden: []string{"recent2"},
416+ },
417+ {
418+ name: "medium reserves right-side room instead of filling all recents",
419+ cols: 80,
420+ wantShown: []string{"recent1", "recent6"},
421+ wantHidden: []string{"recent7", "recent9"},
422+ },
423+ {
424+ name: "wide shows more recents while still keeping a right-side reserve",
425+ cols: 120,
426+ wantShown: []string{"recent1", "recent9"},
427+ wantHidden: nil,
428+ },
429+ }
430+ for _, tt := range tests {
431+ t.Run(tt.name, func(t *testing.T) {
432+ row := make([]renderer.Cell, tt.cols)
433+
434+ drawStatusBarState(row, state, resolveThemeStyles(nil))
435+
436+ text := rowText(row)
437+ for _, want := range tt.wantShown {
438+ require.Contains(t, text, want)
439+ }
440+ for _, hidden := range tt.wantHidden {
441+ require.NotContains(t, text, hidden)
442+ }
443+ })
444+ }
445+}
446+
398447 func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {
399448 state := barState{status: statusSnapshot{session: "cur"}, copyFeedback: "copied", mru: []mruSession{{name: "a"}, {name: "b"}, {name: "c"}}}
400449 row := make([]renderer.Cell, 20)
@@ -395,6 +395,55 @@ func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {
395 require.NotContains(t, text, "old")395 require.NotContains(t, text, "old")
396 }396 }
397 397
398+func TestStatusBarMRUWidthAwareBudget(t *testing.T) {
399+ mru := make([]mruSession, 0, maxMRUSessions)
400+ for i := 1; i <= maxMRUSessions; i++ {
401+ mru = append(mru, mruSession{name: "recent" + strconv.Itoa(i)})
402+ }
403+ state := barState{status: statusSnapshot{session: "cur"}, mru: mru}
404+
405+ tests := []struct {
406+ name string
407+ cols int
408+ wantShown []string
409+ wantHidden []string
410+ }{
411+ {
412+ name: "narrow keeps first recent when it physically fits",
413+ cols: 15,
414+ wantShown: []string{"cur", "recent1"},
415+ wantHidden: []string{"recent2"},
416+ },
417+ {
418+ name: "medium reserves right-side room instead of filling all recents",
419+ cols: 80,
420+ wantShown: []string{"recent1", "recent6"},
421+ wantHidden: []string{"recent7", "recent9"},
422+ },
423+ {
424+ name: "wide shows more recents while still keeping a right-side reserve",
425+ cols: 120,
426+ wantShown: []string{"recent1", "recent9"},
427+ wantHidden: nil,
428+ },
429+ }
430+ for _, tt := range tests {
431+ t.Run(tt.name, func(t *testing.T) {
432+ row := make([]renderer.Cell, tt.cols)
433+
434+ drawStatusBarState(row, state, resolveThemeStyles(nil))
435+
436+ text := rowText(row)
437+ for _, want := range tt.wantShown {
438+ require.Contains(t, text, want)
439+ }
440+ for _, hidden := range tt.wantHidden {
441+ require.NotContains(t, text, hidden)
442+ }
443+ })
444+ }
445+}
446+
398 func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {447 func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {
399 state := barState{status: statusSnapshot{session: "cur"}, copyFeedback: "copied", mru: []mruSession{{name: "a"}, {name: "b"}, {name: "c"}}}448 state := barState{status: statusSnapshot{session: "cur"}, copyFeedback: "copied", mru: []mruSession{{name: "a"}, {name: "b"}, {name: "c"}}}
400 row := make([]renderer.Cell, 20)449 row := make([]renderer.Cell, 20)

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 12

Tags 11

↑↓ moveenter openesc close