Skip to content

Commit 5a46616

Browse files
committed
feat: add compact output mode to yggdrasilctl
1 parent 422836e commit 5a46616

4 files changed

Lines changed: 142 additions & 4 deletions

File tree

cmd/yggdrasilctl/cmd_line_env.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import (
1414
)
1515

1616
type CmdLineEnv struct {
17-
args []string
18-
endpoint, server string
19-
injson, borders, ver bool
17+
args []string
18+
endpoint, server string
19+
injson, borders, compact, ver bool
2020
}
2121

2222
func newCmdLineEnv() CmdLineEnv {
@@ -45,6 +45,7 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() {
4545
server := flag.String("endpoint", cmdLineEnv.endpoint, "Admin socket endpoint")
4646
injson := flag.Bool("json", false, "Output in JSON format (as opposed to pretty-print)")
4747
borders := flag.Bool("borders", true, "Output borders on tables")
48+
compact := flag.Bool("compact", false, "Truncate contents if needed to fit terminal width")
4849
ver := flag.Bool("version", false, "Prints the version of this build")
4950

5051
flag.Parse()
@@ -53,6 +54,7 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() {
5354
cmdLineEnv.server = *server
5455
cmdLineEnv.injson = *injson
5556
cmdLineEnv.borders = *borders
57+
cmdLineEnv.compact = *compact
5658
cmdLineEnv.ver = *ver
5759
}
5860

cmd/yggdrasilctl/main.go

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

2830
func 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+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ require (
3333
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
3434
github.com/olekukonko/errors v1.2.0 // indirect
3535
github.com/olekukonko/ll v0.1.6 // indirect
36+
golang.org/x/term v0.44.0 // indirect
3637
)
3738

3839
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7575
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
7676
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
7777
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
78+
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
79+
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
7880
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
7981
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
8082
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=

0 commit comments

Comments
 (0)