Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/adrg/xdg v0.5.3
github.com/creativeprojects/go-selfupdate v1.5.1
github.com/hashicorp/go-version v1.7.0
github.com/olekukonko/tablewriter v1.1.2
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
Expand All @@ -15,9 +16,13 @@ require (
code.gitea.io/sdk/gitea v0.22.0 // indirect
github.com/42wim/httpsig v1.2.3 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
Expand All @@ -26,6 +31,12 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
Expand Down
19 changes: 19 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
Expand Down Expand Up @@ -50,8 +56,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
Expand Down Expand Up @@ -97,6 +114,8 @@ golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand Down
8 changes: 4 additions & 4 deletions internal/output/formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,10 @@ func TestTableFormatter_BooleanValues(t *testing.T) {

// Check boolean formatting
lines := strings.Split(output, "\n")
assert.Contains(t, lines[1], "true")
assert.Contains(t, lines[1], "false")
assert.Contains(t, lines[2], "false")
assert.Contains(t, lines[2], "true")
assert.Contains(t, lines[3], "true")
assert.Contains(t, lines[3], "false")
assert.Contains(t, lines[4], "false")
assert.Contains(t, lines[4], "true")
}

func TestTableFormatter_NilPointer(t *testing.T) {
Expand Down
52 changes: 23 additions & 29 deletions internal/output/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"fmt"
"reflect"
"strings"
"text/tabwriter"

"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
)

// TableFormatter formats output as a table
Expand All @@ -18,21 +20,16 @@ func NewTableFormatter(opts Options) *TableFormatter {
}

func (f *TableFormatter) Format(data any) (err error) {
w := tabwriter.NewWriter(f.opts.Writer, 0, 0, 2, ' ', tabwriter.Debug)
w := tablewriter.NewWriter(f.opts.Writer)
defer func() {
if flushErr := w.Flush(); flushErr != nil {
if err == nil {
err = fmt.Errorf("failed to flush table writer: %w", flushErr)
}
}
// Add a final newline nach table output, but only if no error occurred
if err == nil {
if _, nlErr := fmt.Fprintln(f.opts.Writer); nlErr != nil {
err = fmt.Errorf("failed to write trailing newline: %w", nlErr)
}
if renderErr := w.Render(); renderErr != nil && err == nil {
err = fmt.Errorf("failed to render ascii w: %w", renderErr)
}
}()

// disable ALL CAPS for column headers
w.Options(tablewriter.WithHeaderAutoFormat(tw.Off))

// Handle different data types
val := reflect.ValueOf(data)

Expand All @@ -54,9 +51,9 @@ func (f *TableFormatter) Format(data any) (err error) {
}

// formatSlice formats a slice of structs as a table
func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) error {
func (f *TableFormatter) formatSlice(w *tablewriter.Table, val reflect.Value) error {
if val.Len() == 0 {
if _, err := fmt.Fprintln(w, "No data"); err != nil {
if _, err := fmt.Fprintln(f.opts.Writer, "No data"); err != nil {
return fmt.Errorf("failed to write no data message: %w", err)
}
return nil
Expand All @@ -71,7 +68,9 @@ func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) err
if firstElem.Kind() != reflect.Struct {
// Simple slice (e.g., []string)
for i := 0; i < val.Len(); i++ {
if _, err := fmt.Fprintf(w, "%v\n", val.Index(i).Interface()); err != nil {
elem := val.Index(i)
row := []string{f.formatValue(elem)}
if err := w.Append(row); err != nil {
return fmt.Errorf("failed to write slice element: %w", err)
}
}
Expand All @@ -82,9 +81,7 @@ func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) err
headers := f.getHeaders(firstElem.Type())
// Add # as first column header
headersWithNum := append([]string{"#"}, headers...)
if _, err := fmt.Fprintln(w, strings.Join(headersWithNum, "\t")); err != nil {
return fmt.Errorf("failed to write table headers: %w", err)
}
w.Header(headersWithNum)

// Print rows
for i := 0; i < val.Len(); i++ {
Expand All @@ -95,7 +92,7 @@ func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) err
row := f.formatStructRow(elem)
// Add row number (1-indexed) as first column
rowWithNum := append([]string{fmt.Sprintf("%d", i+1)}, row...)
if _, err := fmt.Fprintln(w, strings.Join(rowWithNum, "\t")); err != nil {
if err := w.Append(rowWithNum); err != nil {
return fmt.Errorf("failed to write table row: %w", err)
}
}
Expand All @@ -104,33 +101,30 @@ func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) err
}

// formatStruct formats a single struct as a table (horizontal layout with headers)
func (f *TableFormatter) formatStruct(w *tabwriter.Writer, val reflect.Value) error {
func (f *TableFormatter) formatStruct(w *tablewriter.Table, val reflect.Value) error {
// Get headers
headers := f.getHeaders(val.Type())
if _, err := fmt.Fprintln(w, strings.Join(headers, "\t")); err != nil {
return fmt.Errorf("failed to write struct headers: %w", err)
}
w.Header(headers)

// Get row data
row := f.formatStructRow(val)
if _, err := fmt.Fprintln(w, strings.Join(row, "\t")); err != nil {
if err := w.Append(row); err != nil {
return fmt.Errorf("failed to write struct row: %w", err)
}

return nil
}

// formatMap formats a map as a table
func (f *TableFormatter) formatMap(w *tabwriter.Writer, val reflect.Value) error {
if _, err := fmt.Fprintln(w, "Key\tValue"); err != nil {
return fmt.Errorf("failed to write map headers: %w", err)
}
func (f *TableFormatter) formatMap(w *tablewriter.Table, val reflect.Value) error {
w.Header([]string{"Key", "Value"})

iter := val.MapRange()
for iter.Next() {
key := iter.Key()
value := iter.Value()
if _, err := fmt.Fprintf(w, "%v\t%v\n", key.Interface(), f.formatValue(value)); err != nil {
row := []string{f.formatValue(key), f.formatValue(value)}
if err := w.Append(row); err != nil {
return fmt.Errorf("failed to write map entry: %w", err)
}
}
Expand Down