@@ -4,20 +4,31 @@ import (
44 "context"
55 "fmt"
66 "io"
7+ "os"
8+ "strconv"
79 "strings"
810 "time"
911
1012 "github.com/kernel/hypeman-go"
13+ "golang.org/x/term"
1114)
1215
13- // TableWriter provides simple table formatting for CLI output
16+ // TableWriter provides simple table formatting for CLI output with
17+ // terminal-width-aware column sizing.
1418type TableWriter struct {
1519 w io.Writer
1620 headers []string
17- widths []int
21+ widths []int // natural widths (max of header and cell values)
1822 rows [][]string
23+
24+ // TruncOrder specifies column indices in truncation priority order.
25+ // The first index in the slice is truncated first when the table is
26+ // too wide for the terminal. Columns not listed are never truncated.
27+ TruncOrder []int
1928}
2029
30+ const columnGap = 2 // spaces between columns
31+
2132// NewTableWriter creates a new table writer
2233func NewTableWriter (w io.Writer , headers ... string ) * TableWriter {
2334 widths := make ([]int , len (headers ))
@@ -46,23 +57,112 @@ func (t *TableWriter) AddRow(cells ...string) {
4657 t .rows = append (t .rows , row )
4758}
4859
49- // Render outputs the table
60+ // getTerminalWidth returns the terminal width. It tries the stdout
61+ // file descriptor first, then the COLUMNS env var, then defaults to 80.
62+ func getTerminalWidth () int {
63+ if w , _ , err := term .GetSize (int (os .Stdout .Fd ())); err == nil && w > 0 {
64+ return w
65+ }
66+ if cols := os .Getenv ("COLUMNS" ); cols != "" {
67+ if w , err := strconv .Atoi (cols ); err == nil && w > 0 {
68+ return w
69+ }
70+ }
71+ return 80
72+ }
73+
74+ // renderWidths computes the final column widths, shrinking columns in
75+ // TruncOrder as needed to fit within the terminal width.
76+ func (t * TableWriter ) renderWidths () []int {
77+ n := len (t .headers )
78+ widths := make ([]int , n )
79+ copy (widths , t .widths )
80+
81+ termWidth := getTerminalWidth ()
82+
83+ // Total space: column widths + gaps (no trailing gap on last column)
84+ total := func () int {
85+ s := 0
86+ for _ , w := range widths {
87+ s += w
88+ }
89+ s += columnGap * (n - 1 )
90+ return s
91+ }
92+
93+ if total () <= termWidth {
94+ return widths
95+ }
96+
97+ // Shrink columns in TruncOrder until the table fits
98+ for _ , col := range t .TruncOrder {
99+ if col < 0 || col >= n {
100+ continue
101+ }
102+ excess := total () - termWidth
103+ if excess <= 0 {
104+ break
105+ }
106+ // Minimum width: at least the header length, but no less than 5
107+ minW := len (t .headers [col ])
108+ if minW < 5 {
109+ minW = 5
110+ }
111+ canShrink := widths [col ] - minW
112+ if canShrink <= 0 {
113+ continue
114+ }
115+ shrink := excess
116+ if shrink > canShrink {
117+ shrink = canShrink
118+ }
119+ widths [col ] -= shrink
120+ }
121+
122+ return widths
123+ }
124+
125+ // Render outputs the table, dynamically fitting columns to the terminal width.
50126func (t * TableWriter ) Render () {
127+ widths := t .renderWidths ()
128+ last := len (t .headers ) - 1
129+
51130 // Print headers
52131 for i , h := range t .headers {
53- fmt .Fprintf (t .w , "%-*s" , t .widths [i ]+ 2 , h )
132+ cell := truncateCell (h , widths [i ])
133+ if i < last {
134+ fmt .Fprintf (t .w , "%-*s" , widths [i ]+ columnGap , cell )
135+ } else {
136+ fmt .Fprint (t .w , cell )
137+ }
54138 }
55139 fmt .Fprintln (t .w )
56140
57141 // Print rows
58142 for _ , row := range t .rows {
59143 for i , cell := range row {
60- fmt .Fprintf (t .w , "%-*s" , t .widths [i ]+ 2 , cell )
144+ cell = truncateCell (cell , widths [i ])
145+ if i < last {
146+ fmt .Fprintf (t .w , "%-*s" , widths [i ]+ columnGap , cell )
147+ } else {
148+ fmt .Fprint (t .w , cell )
149+ }
61150 }
62151 fmt .Fprintln (t .w )
63152 }
64153}
65154
155+ // truncateCell truncates s to fit within maxWidth, appending "..." if needed.
156+ func truncateCell (s string , maxWidth int ) string {
157+ if len (s ) <= maxWidth {
158+ return s
159+ }
160+ if maxWidth <= 3 {
161+ return s [:maxWidth ]
162+ }
163+ return s [:maxWidth - 3 ] + "..."
164+ }
165+
66166// FormatTimeAgo formats a time as "X ago" string
67167func FormatTimeAgo (t time.Time ) string {
68168 if t .IsZero () {
0 commit comments