fix(tui): preserve phase progress in ellipses
- commit
- a2b3a32d0d8ed772d23692ea94eacce48fc806d3
- author
- brice · · 2026-08-05 13:42 UTC
- parent
- a2c470e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 77 | 77 | ||
| 78 | 78 | content := make([]string, 0, height) | |
| 79 | 79 | 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 | + | } | |
| 82 | 86 | if visible := visibleWidth(text); visible < width { | |
| 83 | 87 | text += strings.Repeat(" ", width-visible) | |
| 84 | 88 | } | |
| 85 | 89 | return text | |
| 86 | 90 | } | |
| 87 | 91 | paintLine := func(text string, color int, dim, phase bool) string { | |
| 88 | - | text = padLine(text) | |
| 92 | + | text = padLine(text, phase) | |
| 89 | 93 | if color < 0 { | |
| 90 | 94 | return text | |
| 91 | 95 | } | |
| @@ -201,6 +205,23 @@ func truncateRightBarLine(text string, width int) string { | |||
| 201 | 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 | 225 | func rightBarChecklistIndent(lines []string) int { | |
| 205 | 226 | indent := -1 | |
| 206 | 227 | 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 := false | 79 | 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 text | 89 | 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 text | 94 | 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 := -1 | 226 | 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) { | |||
| 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 | 91 | func TestRenderRightBarUsesChecklistIndentEllipsisAndBoldPhases(t *testing.T) { | |
| 75 | 92 | lines := RenderRightBar(Dark, []RightBarWidget{{ | |
| 76 | 93 | 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", | ||