detached a2b3a32

fix(tui): preserve phase progress in ellipses

commit
a2b3a32d0d8ed772d23692ea94eacce48fc806d3
author
brice · · 2026-08-05 13:42 UTC
parent
a2c470e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +41 -3
M packages/tui/rightbar.go +24 -3
@@ -77,15 +77,19 @@ func RenderRightBar(th Theme, widgets []RightBarWidget, width, height int) []str
7777
7878 content := make([]string, 0, height)
7979 truncated := false
80- padLine := func(text string) string {
81- text = truncateRightBarLine(text, width)
80+ padLine := func(text string, phase bool) string {
81+ if phase {
82+ text = truncateRightBarPhaseLine(text, width)
83+ } else {
84+ text = truncateRightBarLine(text, width)
85+ }
8286 if visible := visibleWidth(text); visible < width {
8387 text += strings.Repeat(" ", width-visible)
8488 }
8589 return text
8690 }
8791 paintLine := func(text string, color int, dim, phase bool) string {
88- text = padLine(text)
92+ text = padLine(text, phase)
8993 if color < 0 {
9094 return text
9195 }
@@ -201,6 +205,23 @@ func truncateRightBarLine(text string, width int) string {
201205 return truncateToWidth(text, width-3) + "..."
202206 }
203207
208+func truncateRightBarPhaseLine(text string, width int) string {
209+ if width <= 0 || visibleWidth(text) <= width {
210+ return text
211+ }
212+ nameStart, nameEnd, ok := rightBarChecklistPhaseName(text)
213+ if !ok {
214+ return truncateRightBarLine(text, width)
215+ }
216+ prefix := text[:nameStart]
217+ suffix := text[nameEnd:]
218+ nameWidth := width - visibleWidth(prefix) - visibleWidth(suffix)
219+ if nameWidth <= 0 {
220+ return truncateRightBarLine(text, width)
221+ }
222+ return prefix + truncateRightBarLine(text[nameStart:nameEnd], nameWidth) + suffix
223+}
224+
204225 func rightBarChecklistIndent(lines []string) int {
205226 indent := -1
206227 for _, line := range lines {
@@ -77,15 +77,19 @@ func RenderRightBar(th Theme, widgets []RightBarWidget, width, height int) []str
77 77
78 content := make([]string, 0, height)78 content := make([]string, 0, height)
79 truncated := false79 truncated := false
80- padLine := func(text string) string {80+ padLine := func(text string, phase bool) string {
81- text = truncateRightBarLine(text, width)81+ if phase {
82+ text = truncateRightBarPhaseLine(text, width)
83+ } else {
84+ text = truncateRightBarLine(text, width)
85+ }
82 if visible := visibleWidth(text); visible < width {86 if visible := visibleWidth(text); visible < width {
83 text += strings.Repeat(" ", width-visible)87 text += strings.Repeat(" ", width-visible)
84 }88 }
85 return text89 return text
86 }90 }
87 paintLine := func(text string, color int, dim, phase bool) string {91 paintLine := func(text string, color int, dim, phase bool) string {
88- text = padLine(text)92+ text = padLine(text, phase)
89 if color < 0 {93 if color < 0 {
90 return text94 return text
91 }95 }
@@ -201,6 +205,23 @@ func truncateRightBarLine(text string, width int) string {
201 return truncateToWidth(text, width-3) + "..."205 return truncateToWidth(text, width-3) + "..."
202 }206 }
203 207
208+func truncateRightBarPhaseLine(text string, width int) string {
209+ if width <= 0 || visibleWidth(text) <= width {
210+ return text
211+ }
212+ nameStart, nameEnd, ok := rightBarChecklistPhaseName(text)
213+ if !ok {
214+ return truncateRightBarLine(text, width)
215+ }
216+ prefix := text[:nameStart]
217+ suffix := text[nameEnd:]
218+ nameWidth := width - visibleWidth(prefix) - visibleWidth(suffix)
219+ if nameWidth <= 0 {
220+ return truncateRightBarLine(text, width)
221+ }
222+ return prefix + truncateRightBarLine(text[nameStart:nameEnd], nameWidth) + suffix
223+}
224+
204 func rightBarChecklistIndent(lines []string) int {225 func rightBarChecklistIndent(lines []string) int {
205 indent := -1226 indent := -1
206 for _, line := range lines {227 for _, line := range lines {
M packages/tui/rightbar_test.go +17 -0
@@ -71,6 +71,23 @@ func TestRenderRightBarClipsLongWidgetLinesWithEllipsis(t *testing.T) {
7171 }
7272 }
7373
74+func TestRenderRightBarPreservesPhaseProgressWhenTitleIsClipped(t *testing.T) {
75+ lines := RenderRightBar(Dark, []RightBarWidget{
76+ {
77+ Extension: "tasked-phases",
78+ Lines: []string{"[ ] A phase title that is much too long to fit 0/3"},
79+ },
80+ }, 24, 5)
81+ plainLines := strings.Split(stripANSI(strings.Join(lines, "\n")), "\n")
82+ if len(plainLines) < 3 {
83+ t.Fatalf("phase row is missing: %q", plainLines)
84+ }
85+ phase := strings.TrimRight(plainLines[2], " ")
86+ if !strings.HasSuffix(phase, "0/3") || !strings.Contains(phase, "... 0/3") {
87+ t.Fatalf("phase progress was clipped with its title: %q", phase)
88+ }
89+}
90+
7491 func TestRenderRightBarUsesChecklistIndentEllipsisAndBoldPhases(t *testing.T) {
7592 lines := RenderRightBar(Dark, []RightBarWidget{{
7693 Extension: "tasked-phases",
@@ -71,6 +71,23 @@ func TestRenderRightBarClipsLongWidgetLinesWithEllipsis(t *testing.T) {
71 }71 }
72 }72 }
73 73
74+func TestRenderRightBarPreservesPhaseProgressWhenTitleIsClipped(t *testing.T) {
75+ lines := RenderRightBar(Dark, []RightBarWidget{
76+ {
77+ Extension: "tasked-phases",
78+ Lines: []string{"[ ] A phase title that is much too long to fit 0/3"},
79+ },
80+ }, 24, 5)
81+ plainLines := strings.Split(stripANSI(strings.Join(lines, "\n")), "\n")
82+ if len(plainLines) < 3 {
83+ t.Fatalf("phase row is missing: %q", plainLines)
84+ }
85+ phase := strings.TrimRight(plainLines[2], " ")
86+ if !strings.HasSuffix(phase, "0/3") || !strings.Contains(phase, "... 0/3") {
87+ t.Fatalf("phase progress was clipped with its title: %q", phase)
88+ }
89+}
90+
74 func TestRenderRightBarUsesChecklistIndentEllipsisAndBoldPhases(t *testing.T) {91 func TestRenderRightBarUsesChecklistIndentEllipsisAndBoldPhases(t *testing.T) {
75 lines := RenderRightBar(Dark, []RightBarWidget{{92 lines := RenderRightBar(Dark, []RightBarWidget{{
76 Extension: "tasked-phases",93 Extension: "tasked-phases",

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 1

Tags 476

↑↓ moveenter openesc close