@@ -21,6 +21,7 @@ import (
2121 "fmt"
2222 "io"
2323 "iter"
24+ "slices"
2425 "strings"
2526 "sync"
2627 "time"
@@ -258,13 +259,39 @@ 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+ statusColor colorFunc
274+ }
275+
261276func (w * ttyWriter ) print () {
277+ terminalWidth := goterm .Width ()
278+ terminalHeight := goterm .Height ()
279+ if terminalWidth <= 0 {
280+ terminalWidth = 80
281+ }
282+ if terminalHeight <= 0 {
283+ terminalHeight = 24
284+ }
285+ w .printWithDimensions (terminalWidth , terminalHeight )
286+ }
287+
288+ func (w * ttyWriter ) printWithDimensions (terminalWidth , terminalHeight int ) {
262289 w .mtx .Lock ()
263290 defer w .mtx .Unlock ()
264291 if len (w .tasks ) == 0 {
265292 return
266293 }
267- terminalWidth := goterm . Width ()
294+
268295 up := w .numLines + 1
269296 if ! w .repeated {
270297 up --
@@ -283,48 +310,197 @@ func (w *ttyWriter) print() {
283310 firstLine := fmt .Sprintf ("[+] %s %d/%d" , w .operation , numDone (w .tasks ), len (w .tasks ))
284311 _ , _ = fmt .Fprintln (w .out , firstLine )
285312
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
313+ // Collect parent tasks in original order
314+ allTasks := slices .Collect (w .parentTasks ())
315+
316+ // Available lines: terminal height - 2 (header line + potential "more" line)
317+ maxLines := terminalHeight - 2
318+ if maxLines < 1 {
319+ maxLines = 1
320+ }
321+
322+ showMore := len (allTasks ) > maxLines
323+ tasksToShow := allTasks
324+ if showMore {
325+ tasksToShow = allTasks [:maxLines - 1 ] // Reserve one line for "more" message
326+ }
327+
328+ // collect line data and compute timerLen
329+ lines := make ([]lineData , len (tasksToShow ))
330+ var timerLen int
331+ for i , t := range tasksToShow {
332+ lines [i ] = w .prepareLineData (t )
333+ if len (lines [i ].timer ) > timerLen {
334+ timerLen = len (lines [i ].timer )
291335 }
292336 }
293337
294- skipChildEvents := len (w .tasks ) > goterm .Height ()- 2
338+ // shorten details/taskID to fit terminal width
339+ w .adjustLineWidth (lines , timerLen , terminalWidth )
340+
341+ // compute padding
342+ w .applyPadding (lines , terminalWidth , timerLen )
343+
344+ // Render lines
295345 numLines := 0
296- for t := range w .parentTasks () {
297- line := w .lineText (t , "" , terminalWidth , statusPadding , w .dryRun )
298- _ , _ = fmt .Fprint (w .out , line )
346+ for _ , l := range lines {
347+ _ , _ = fmt .Fprint (w .out , lineText (l ))
299348 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 ++
349+ }
350+
351+ if showMore {
352+ moreCount := len (allTasks ) - len (tasksToShow )
353+ moreText := fmt .Sprintf (" ... %d more" , moreCount )
354+ pad := terminalWidth - len (moreText )
355+ if pad < 0 {
356+ pad = 0
307357 }
358+ _ , _ = fmt .Fprintf (w .out , "%s%s\n " , moreText , strings .Repeat (" " , pad ))
359+ numLines ++
308360 }
361+
362+ // Clear any remaining lines from previous render
309363 for i := numLines ; i < w .numLines ; i ++ {
310- if numLines < goterm .Height ()- 2 {
311- _ , _ = fmt .Fprintln (w .out , strings .Repeat (" " , terminalWidth ))
312- numLines ++
313- }
364+ _ , _ = fmt .Fprintln (w .out , strings .Repeat (" " , terminalWidth ))
365+ numLines ++
314366 }
315367 w .numLines = numLines
316368}
317369
318- func (w * ttyWriter ) lineText (t * task , pad string , terminalWidth , statusPadding int , dryRun bool ) string {
370+ func (w * ttyWriter ) applyPadding (lines []lineData , terminalWidth int , timerLen int ) {
371+ var maxBeforeStatus int
372+ for i := range lines {
373+ l := & lines [i ]
374+ // Width before status: space(1) + spinner(1) + prefix + space(1) + taskID + [progress] + space(1)
375+ beforeStatus := 4 + lenAnsi (l .prefix ) + len (l .taskID ) + lenAnsi (l .progress )
376+ if beforeStatus > maxBeforeStatus {
377+ maxBeforeStatus = beforeStatus
378+ }
379+ }
380+
381+ for i , l := range lines {
382+ // Position before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress + space(1)
383+ beforeStatus := 4 + lenAnsi (l .prefix ) + len (l .taskID ) + lenAnsi (l .progress )
384+ // statusPad aligns status; lineText adds 1 more space after statusPad
385+ l .statusPad = maxBeforeStatus - beforeStatus
386+
387+ // Format: space(1) + spinner(1) + prefix + space(1) + taskID + progress + statusPad + space(1) + status
388+ lineLen := beforeStatus + l .statusPad + 1 + len (l .status )
389+ if l .details != "" {
390+ lineLen += 1 + len (l .details )
391+ }
392+ l .timerPad = terminalWidth - lineLen - timerLen
393+ if l .timerPad < 1 {
394+ l .timerPad = 1
395+ }
396+ lines [i ] = l
397+
398+ }
399+ }
400+
401+ func (w * ttyWriter ) adjustLineWidth (lines []lineData , timerLen int , terminalWidth int ) {
402+ const minIDLen = 10
403+
404+ // Find the maximum status length for proper width calculation
405+ var maxStatusLen int
406+ for i := range lines {
407+ if len (lines [i ].status ) > maxStatusLen {
408+ maxStatusLen = len (lines [i ].status )
409+ }
410+ }
411+
412+ // Iteratively truncate until all lines fit
413+ for iteration := 0 ; iteration < 100 ; iteration ++ { // safety limit
414+ // Compute maxBeforeStatus
415+ var maxBeforeStatus int
416+ for i := range lines {
417+ l := & lines [i ]
418+ beforeStatus := 4 + lenAnsi (l .prefix ) + len (l .taskID ) + lenAnsi (l .progress )
419+ if beforeStatus > maxBeforeStatus {
420+ maxBeforeStatus = beforeStatus
421+ }
422+ }
423+
424+ // Check if any line overflows and find max overflow
425+ maxOverflow := 0
426+ for i := range lines {
427+ l := & lines [i ]
428+ detailsLen := len (l .details )
429+ if detailsLen > 0 {
430+ detailsLen ++
431+ }
432+ // Line width: maxBeforeStatus + 1 (space) + status + details + 1 (min timerPad) + timer
433+ lineWidth := maxBeforeStatus + 1 + maxStatusLen + detailsLen + 1 + timerLen
434+ overflow := lineWidth - terminalWidth
435+ if overflow > maxOverflow {
436+ maxOverflow = overflow
437+ }
438+ }
439+
440+ if maxOverflow <= 0 {
441+ break // All lines fit
442+ }
443+
444+ // First try to truncate details
445+ truncated := false
446+ for i := range lines {
447+ l := & lines [i ]
448+ if len (l .details ) > 3 {
449+ reduction := maxOverflow
450+ if reduction > len (l .details )- 3 {
451+ reduction = len (l .details ) - 3
452+ }
453+ l .details = l .details [:len (l .details )- reduction - 3 ] + "..."
454+ truncated = true
455+ break
456+ } else if len (l .details ) > 0 {
457+ l .details = ""
458+ truncated = true
459+ break
460+ }
461+ }
462+
463+ if truncated {
464+ continue
465+ }
466+
467+ // No details left to truncate, find the line with longest taskID and truncate it
468+ longestIdx := - 1
469+ longestLen := minIDLen
470+ for i := range lines {
471+ if len (lines [i ].taskID ) > longestLen {
472+ longestLen = len (lines [i ].taskID )
473+ longestIdx = i
474+ }
475+ }
476+
477+ if longestIdx >= 0 {
478+ l := & lines [longestIdx ]
479+ reduction := maxOverflow + 3 // account for "..."
480+ newLen := len (l .taskID ) - reduction
481+ if newLen < minIDLen - 3 {
482+ newLen = minIDLen - 3
483+ }
484+ if newLen > 0 {
485+ l .taskID = l .taskID [:newLen ] + "..."
486+ }
487+ } else {
488+ break // Can't truncate further
489+ }
490+ }
491+ }
492+
493+ func (w * ttyWriter ) prepareLineData (t * task ) lineData {
319494 endTime := time .Now ()
320495 if t .status != api .Working {
321496 endTime = t .startTime
322497 if (t .endTime != time.Time {}) {
323498 endTime = t .endTime
324499 }
325500 }
501+
326502 prefix := ""
327- if dryRun {
503+ if w . dryRun {
328504 prefix = PrefixColor (DRYRUN_PREFIX )
329505 }
330506
@@ -338,11 +514,9 @@ func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding i
338514 )
339515
340516 // 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 ) {
517+ if t .status == api .Working {
518+ for child := range w .childrenTasks (t .ID ) {
343519 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
346520 hideDetails = true
347521 }
348522 total += child .total
@@ -356,49 +530,49 @@ func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding i
356530 }
357531 }
358532
359- // don't try to show detailed progress if we don't have any idea
360533 if total == 0 {
361534 hideDetails = true
362535 }
363536
364- txt := t . ID
537+ var progress string
365538 if len (completion ) > 0 {
366- var progress string
539+ progress = " [" + SuccessColor ( strings . Join ( completion , "" )) + "]"
367540 if ! hideDetails {
368- progress = fmt .Sprintf (" %7s / %-7s" , units .HumanSize (float64 (current )), units .HumanSize (float64 (total )))
541+ progress + = fmt .Sprintf (" %7s / %-7s" , units .HumanSize (float64 (current )), units .HumanSize (float64 (total )))
369542 }
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 )
543+ }
400544
401- return o
545+ return lineData {
546+ spinner : spinner (t ),
547+ prefix : prefix ,
548+ taskID : t .ID ,
549+ progress : progress ,
550+ status : t .text ,
551+ statusColor : colorFn (t .status ),
552+ details : t .details ,
553+ timer : fmt .Sprintf ("%.1fs" , elapsed ),
554+ }
555+ }
556+
557+ func lineText (l lineData ) string {
558+ var sb strings.Builder
559+ sb .WriteString (" " )
560+ sb .WriteString (l .spinner )
561+ sb .WriteString (l .prefix )
562+ sb .WriteString (" " )
563+ sb .WriteString (l .taskID )
564+ sb .WriteString (l .progress )
565+ sb .WriteString (strings .Repeat (" " , l .statusPad ))
566+ sb .WriteString (" " )
567+ sb .WriteString (l .statusColor (l .status ))
568+ if l .details != "" {
569+ sb .WriteString (" " )
570+ sb .WriteString (l .details )
571+ }
572+ sb .WriteString (strings .Repeat (" " , l .timerPad ))
573+ sb .WriteString (TimerColor (l .timer ))
574+ sb .WriteString ("\n " )
575+ return sb .String ()
402576}
403577
404578var (
@@ -443,17 +617,6 @@ func numDone(tasks map[string]*task) int {
443617 return i
444618}
445619
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-
457620// lenAnsi count of user-perceived characters in ANSI string.
458621func lenAnsi (s string ) int {
459622 length := 0
0 commit comments