Skip to content

Commit 1175c65

Browse files
committed
Fixed progress UI to adapt to terminal width
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent ef14cfc commit 1175c65

File tree

1 file changed

+168
-73
lines changed

1 file changed

+168
-73
lines changed

cmd/display/tty.go

Lines changed: 168 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"io"
2323
"iter"
24+
"slices"
2425
"strings"
2526
"sync"
2627
"time"
@@ -258,13 +259,34 @@ func (w *ttyWriter) childrenTasks(parent string) iter.Seq[*task] {
258259
}
259260
}
260261

262+
// lineData holds pre-computed formatting for a task line
263+
type lineData struct {
264+
spinner string // rendered spinner with color
265+
prefix string // dry-run prefix if any
266+
taskID string // possibly abbreviated
267+
progress string // progress bar and size info
268+
status string // rendered status with color
269+
details string // possibly abbreviated
270+
timer string // rendered timer with color
271+
statusPad int // padding before status to align
272+
timerPad int // padding before timer to align
273+
}
274+
261275
func (w *ttyWriter) print() {
262276
w.mtx.Lock()
263277
defer w.mtx.Unlock()
264278
if len(w.tasks) == 0 {
265279
return
266280
}
267281
terminalWidth := goterm.Width()
282+
terminalHeight := goterm.Height()
283+
if terminalWidth <= 0 {
284+
terminalWidth = 80
285+
}
286+
if terminalHeight <= 0 {
287+
terminalHeight = 24
288+
}
289+
268290
up := w.numLines + 1
269291
if !w.repeated {
270292
up--
@@ -283,48 +305,135 @@ func (w *ttyWriter) print() {
283305
firstLine := fmt.Sprintf("[+] %s %d/%d", w.operation, numDone(w.tasks), len(w.tasks))
284306
_, _ = fmt.Fprintln(w.out, firstLine)
285307

286-
var statusPadding int
287-
for _, t := range w.tasks {
288-
l := len(t.ID)
289-
if len(t.parents) == 0 && statusPadding < l {
290-
statusPadding = l
291-
}
308+
// Collect parent tasks in original order
309+
allTasks := slices.Collect(w.parentTasks())
310+
311+
// Available lines: terminal height - 2 (header line + potential "more" line)
312+
maxLines := terminalHeight - 2
313+
if maxLines < 1 {
314+
maxLines = 1
315+
}
316+
317+
showMore := len(allTasks) > maxLines
318+
tasksToShow := allTasks
319+
if showMore {
320+
tasksToShow = allTasks[:maxLines-1] // Reserve one line for "more" message
292321
}
293322

294-
skipChildEvents := len(w.tasks) > goterm.Height()-2
323+
// collect line data and compute timerLen
324+
lines := make([]lineData, len(tasksToShow))
325+
var timerLen int
326+
for i, t := range tasksToShow {
327+
lines[i] = w.prepareLineData(t)
328+
}
329+
330+
// shorten details/taskID to fit terminal width
331+
w.adjustLineWidth(lines, timerLen, terminalWidth)
332+
333+
// compute padding
334+
w.applyPadding(lines, terminalWidth, timerLen)
335+
336+
// Render lines
295337
numLines := 0
296-
for t := range w.parentTasks() {
297-
line := w.lineText(t, "", terminalWidth, statusPadding, w.dryRun)
298-
_, _ = fmt.Fprint(w.out, line)
338+
for _, l := range lines {
339+
_, _ = fmt.Fprint(w.out, lineText(l))
299340
numLines++
300-
if skipChildEvents {
301-
continue
302-
}
303-
for child := range w.childrenTasks(t.ID) {
304-
line := w.lineText(child, " ", terminalWidth, statusPadding-2, w.dryRun)
305-
_, _ = fmt.Fprint(w.out, line)
306-
numLines++
341+
}
342+
343+
if showMore {
344+
moreCount := len(allTasks) - len(tasksToShow)
345+
moreText := fmt.Sprintf(" ... %d more", moreCount)
346+
pad := terminalWidth - len(moreText)
347+
if pad < 0 {
348+
pad = 0
307349
}
350+
_, _ = fmt.Fprintf(w.out, "%s%s\n", moreText, strings.Repeat(" ", pad))
351+
numLines++
308352
}
353+
354+
// Clear any remaining lines from previous render
309355
for i := numLines; i < w.numLines; i++ {
310-
if numLines < goterm.Height()-2 {
311-
_, _ = fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
312-
numLines++
313-
}
356+
_, _ = fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
357+
numLines++
314358
}
315359
w.numLines = numLines
316360
}
317361

318-
func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding int, dryRun bool) string {
362+
func (w *ttyWriter) applyPadding(lines []lineData, terminalWidth int, timerLen int) {
363+
var maxBeforeStatus int
364+
for i := range lines {
365+
l := &lines[i]
366+
// Width before status: space(1) + spinner(1) + prefix + space(1) + taskID + [progress] + space(1)
367+
beforeStatus := 4 + lenAnsi(l.prefix) + len(l.taskID) + lenAnsi(l.progress)
368+
if beforeStatus > maxBeforeStatus {
369+
maxBeforeStatus = beforeStatus
370+
}
371+
}
372+
373+
for i, l := range lines {
374+
// Position before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress + space(1)
375+
beforeStatus := 4 + lenAnsi(l.prefix) + len(l.taskID) + lenAnsi(l.progress)
376+
// statusPad aligns status; lineText adds 1 more space after statusPad
377+
l.statusPad = maxBeforeStatus - beforeStatus
378+
379+
// Format: space(1) + spinner(1) + prefix + space(1) + taskID + progress + statusPad + space(1) + status
380+
lineLen := beforeStatus + l.statusPad + 1 + lenAnsi(l.status)
381+
if l.details != "" {
382+
lineLen += 1 + len(l.details)
383+
}
384+
l.timerPad = terminalWidth - lineLen - timerLen
385+
if l.timerPad < 1 {
386+
l.timerPad = 1
387+
}
388+
lines[i] = l
389+
390+
}
391+
}
392+
393+
func (w *ttyWriter) adjustLineWidth(lines []lineData, timerLen int, terminalWidth int) {
394+
for i := range lines {
395+
l := &lines[i]
396+
statusLen := lenAnsi(l.status)
397+
detailsLen := len(l.details)
398+
if detailsLen > 0 {
399+
detailsLen++ // details is printed with an additional space if present
400+
}
401+
// line: space(1) + spinner(1) + prefix + space(1) + taskID + progress + space(1) + [padding] + status + [space(1) + details] + [padding] + timer
402+
widthWithoutDetails := 5 + lenAnsi(l.prefix) + len(l.taskID) + lenAnsi(l.progress) + statusLen + timerLen
403+
overflow := widthWithoutDetails + detailsLen - terminalWidth
404+
405+
if overflow > 0 {
406+
// First, abbreviate details
407+
if detailsLen > overflow+3 {
408+
l.details = l.details[:detailsLen-overflow-3] + "..."
409+
} else {
410+
l.details = ""
411+
// Then abbreviate task ID if still overflowing
412+
overflow = widthWithoutDetails - terminalWidth
413+
if overflow > 0 {
414+
minIDLen := 10
415+
if len(l.taskID) > minIDLen+overflow {
416+
l.taskID = l.taskID[:len(l.taskID)-overflow-3] + "..."
417+
} else if len(l.taskID) > minIDLen {
418+
l.taskID = l.taskID[:minIDLen-3] + "..."
419+
}
420+
}
421+
}
422+
}
423+
}
424+
}
425+
426+
func (w *ttyWriter) prepareLineData(t *task) lineData {
319427
endTime := time.Now()
320428
if t.status != api.Working {
321429
endTime = t.startTime
322430
if (t.endTime != time.Time{}) {
323431
endTime = t.endTime
324432
}
325433
}
434+
326435
prefix := ""
327-
if dryRun {
436+
if w.dryRun {
328437
prefix = PrefixColor(DRYRUN_PREFIX)
329438
}
330439

@@ -338,11 +447,9 @@ func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding i
338447
)
339448

340449
// only show the aggregated progress while the root operation is in-progress
341-
if parent := t; parent.status == api.Working {
342-
for child := range w.childrenTasks(parent.ID) {
450+
if t.status == api.Working {
451+
for child := range w.childrenTasks(t.ID) {
343452
if child.status == api.Working && child.total == 0 {
344-
// we don't have totals available for all the child events
345-
// so don't show the total progress yet
346453
hideDetails = true
347454
}
348455
total += child.total
@@ -356,49 +463,48 @@ func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding i
356463
}
357464
}
358465

359-
// don't try to show detailed progress if we don't have any idea
360466
if total == 0 {
361467
hideDetails = true
362468
}
363469

364-
txt := t.ID
470+
var progress string
365471
if len(completion) > 0 {
366-
var progress string
472+
progress = " [" + SuccessColor(strings.Join(completion, "")) + "]"
367473
if !hideDetails {
368-
progress = fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
474+
progress += fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
369475
}
370-
txt = fmt.Sprintf("%s [%s]%s",
371-
t.ID,
372-
SuccessColor(strings.Join(completion, "")),
373-
progress,
374-
)
375-
}
376-
textLen := len(txt)
377-
padding := statusPadding - textLen
378-
if padding < 0 {
379-
padding = 0
380-
}
381-
// calculate the max length for the status text, on errors it
382-
// is 2-3 lines long and breaks the line formatting
383-
maxDetailsLen := terminalWidth - textLen - statusPadding - 15
384-
details := t.details
385-
// in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index
386-
if maxDetailsLen > 0 && len(details) > maxDetailsLen {
387-
details = details[:maxDetailsLen] + "..."
388-
}
389-
text := fmt.Sprintf("%s %s%s %s %s%s %s",
390-
pad,
391-
spinner(t),
392-
prefix,
393-
txt,
394-
strings.Repeat(" ", padding),
395-
colorFn(t.status)(t.text),
396-
details,
397-
)
398-
timer := fmt.Sprintf("%.1fs ", elapsed)
399-
o := align(text, TimerColor(timer), terminalWidth)
476+
}
400477

401-
return o
478+
return lineData{
479+
spinner: spinner(t),
480+
prefix: prefix,
481+
taskID: t.ID,
482+
progress: progress,
483+
status: colorFn(t.status)(t.text),
484+
details: t.details,
485+
timer: fmt.Sprintf("%.1fs", elapsed),
486+
}
487+
}
488+
489+
func lineText(l lineData) string {
490+
var sb strings.Builder
491+
sb.WriteString(" ")
492+
sb.WriteString(l.spinner)
493+
sb.WriteString(l.prefix)
494+
sb.WriteString(" ")
495+
sb.WriteString(l.taskID)
496+
sb.WriteString(l.progress)
497+
sb.WriteString(strings.Repeat(" ", l.statusPad))
498+
sb.WriteString(" ")
499+
sb.WriteString(l.status)
500+
if l.details != "" {
501+
sb.WriteString(" ")
502+
sb.WriteString(l.details)
503+
}
504+
sb.WriteString(strings.Repeat(" ", l.timerPad))
505+
sb.WriteString(TimerColor(l.timer))
506+
sb.WriteString("\n")
507+
return sb.String()
402508
}
403509

404510
var (
@@ -443,17 +549,6 @@ func numDone(tasks map[string]*task) int {
443549
return i
444550
}
445551

446-
func align(l, r string, w int) string {
447-
ll := lenAnsi(l)
448-
lr := lenAnsi(r)
449-
pad := ""
450-
count := w - ll - lr
451-
if count > 0 {
452-
pad = strings.Repeat(" ", count)
453-
}
454-
return fmt.Sprintf("%s%s%s\n", l, pad, r)
455-
}
456-
457552
// lenAnsi count of user-perceived characters in ANSI string.
458553
func lenAnsi(s string) int {
459554
length := 0

0 commit comments

Comments
 (0)