@@ -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+
261275func (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
404510var (
@@ -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.
458553func lenAnsi (s string ) int {
459554 length := 0
0 commit comments