@@ -16,13 +16,15 @@ import (
1616 "suah.dev/protect"
1717
1818 "github.com/olekukonko/tablewriter"
19+ "github.com/olekukonko/tablewriter/pkg/twwidth"
1920 "github.com/olekukonko/tablewriter/renderer"
2021 "github.com/olekukonko/tablewriter/tw"
2122 "github.com/yggdrasil-network/yggdrasil-go/src/admin"
2223 "github.com/yggdrasil-network/yggdrasil-go/src/core"
2324 "github.com/yggdrasil-network/yggdrasil-go/src/multicast"
2425 "github.com/yggdrasil-network/yggdrasil-go/src/tun"
2526 "github.com/yggdrasil-network/yggdrasil-go/src/version"
27+ "golang.org/x/term"
2628)
2729
2830func main () {
@@ -143,6 +145,11 @@ func run() int {
143145 return 0
144146 }
145147
148+ width , _ , err := term .GetSize (int (os .Stdout .Fd ()))
149+ if err != nil {
150+ width = 0 // not a terminal (piped); render at natural width
151+ }
152+
146153 opts := []tablewriter.Option {
147154 tablewriter .WithRowAlignment (tw .AlignLeft ),
148155 tablewriter .WithHeaderAlignment (tw .AlignCenter ),
@@ -158,7 +165,10 @@ func run() int {
158165 },
159166 })))
160167 }
161- table := tablewriter .NewTable (os .Stdout , opts ... )
168+ if cmdLineEnv .compact {
169+ opts = append (opts , tablewriter .WithHeaderAutoWrap (tw .WrapTruncate ), tablewriter .WithRowAutoWrap (tw .WrapTruncate ))
170+ }
171+ table := & fittedTable {width : width , borders : cmdLineEnv .borders , compact : cmdLineEnv .compact , opts : opts }
162172
163173 switch strings .ToLower (send .Name ) {
164174 case "list" :
@@ -339,3 +349,126 @@ func run() int {
339349
340350 return 0
341351}
352+
353+ // fittedTable buffers header and rows so column widths can be computed from the
354+ // content before the table is built.
355+ type fittedTable struct {
356+ width int
357+ borders bool
358+ compact bool
359+ opts []tablewriter.Option
360+ header []string
361+ rows [][]string
362+ }
363+
364+ func (t * fittedTable ) Header (header []string ) { t .header = header }
365+
366+ func (t * fittedTable ) Append (row []string ) error {
367+ t .rows = append (t .rows , row )
368+ return nil
369+ }
370+
371+ func (t * fittedTable ) Render () error {
372+ opts := t .opts
373+ if t .compact {
374+ if widths := fitColumnWidths (t .width , t .borders , t .header , t .rows ); widths != nil {
375+ opts = append (opts , tablewriter .WithColumnWidths (widths ))
376+ }
377+ }
378+ table := tablewriter .NewTable (os .Stdout , opts ... )
379+ if len (t .header ) > 0 {
380+ table .Header (t .header )
381+ }
382+ for _ , row := range t .rows {
383+ _ = table .Append (row )
384+ }
385+ return table .Render ()
386+ }
387+
388+ // fitColumnWidths returns per-column widths (content plus padding)
389+ // that fill the terminal, truncating only columns that must overflow;
390+ // nil means no constraint.
391+ func fitColumnWidths (width int , borders bool , header []string , rows [][]string ) tw.Mapper [int , int ] {
392+ numCols := len (header )
393+ for _ , row := range rows {
394+ if len (row ) > numCols {
395+ numCols = len (row )
396+ }
397+ }
398+ if numCols == 0 || width <= 0 {
399+ return nil
400+ }
401+
402+ const padding = 2 // tw.PaddingDefault: one space on each side
403+ cell := make ([]int , numCols )
404+ for i := range cell {
405+ content := 0
406+ if i < len (header ) {
407+ content = twwidth .Width (header [i ])
408+ }
409+ for _ , row := range rows {
410+ if i >= len (row ) {
411+ continue
412+ }
413+ if w := twwidth .Width (row [i ]); w > content {
414+ content = w
415+ }
416+ }
417+ cell [i ] = content + padding
418+ }
419+
420+ // Bordered rendering adds a vertical rule between and around columns
421+ overhead := 0
422+ if borders {
423+ overhead = numCols + 1
424+ }
425+ budget := width - overhead
426+
427+ natural := 0
428+ for _ , c := range cell {
429+ natural += c
430+ }
431+ if budget <= 0 || natural <= budget {
432+ return nil
433+ }
434+
435+ // Keep columns that fit their fair share, then split what
436+ // remains evenly among the columns that must overflow
437+ const minCell = 3 // one content column plus padding, enough for an ellipsis
438+ widths := tw .NewMapper [int , int ]()
439+ pool := make ([]int , numCols )
440+ for i := range pool {
441+ pool [i ] = i
442+ }
443+ poolBudget := budget
444+ var share int
445+ for {
446+ share = poolBudget / len (pool )
447+ reserved := false
448+ overflow := pool [:0 ] // filter in place
449+ for _ , i := range pool {
450+ if cell [i ] <= share {
451+ widths .Set (i , cell [i ])
452+ poolBudget -= cell [i ]
453+ reserved = true
454+ } else {
455+ overflow = append (overflow , i )
456+ }
457+ }
458+ pool = overflow
459+ if len (pool ) == 0 || ! reserved {
460+ break
461+ }
462+ }
463+
464+ extra := poolBudget - share * len (pool ) // spread the rounding remainder
465+ for n , i := range pool {
466+ w := max (share , minCell )
467+ if n < extra {
468+ w ++
469+ }
470+ widths .Set (i , w )
471+ }
472+
473+ return widths
474+ }
0 commit comments