diff --git a/cmd/plugin.go b/cmd/plugin.go index 1bcbe1097d..32a84ca023 100644 --- a/cmd/plugin.go +++ b/cmd/plugin.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/olekukonko/tablewriter" + "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/turbot/steampipe-plugin-sdk/logging" "github.com/turbot/steampipe/cmdconfig" @@ -193,16 +193,14 @@ func runPluginListCmd(cmd *cobra.Command, args []string) { utils.ShowErrorWithMessage(err, fmt.Sprintf("Plugin Listing failed")) } - - table := tablewriter.NewWriter(os.Stdout) - table.SetBorder(true) - table.SetHeader([]string{"Name", "Version", "Connections"}) - + t := table.NewWriter() + t.SetStyle(table.StyleLight) + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Name", "Version", "Connections"}) for _, item := range list { - table.Append([]string{item.Name, item.Version, strings.Join(item.Connections, ",")}) + t.AppendRow(table.Row{item.Name, item.Version, strings.Join(item.Connections, ",")}) } - - table.Render() + t.Render() } func runPluginUninstallCmd(cmd *cobra.Command, args []string) { diff --git a/constants/app.go b/constants/app.go index 2f8f40de5f..6650651f63 100644 --- a/constants/app.go +++ b/constants/app.go @@ -1,10 +1,6 @@ package constants -import "time" - // Application constants const ( APPNAME = "steampipe" - // SpinnerShowTimeout :: duration after which spinner should be shown - SpinnerShowTimeout = 100 * time.Millisecond ) diff --git a/constants/display.go b/constants/display.go new file mode 100644 index 0000000000..68566aa425 --- /dev/null +++ b/constants/display.go @@ -0,0 +1,12 @@ +package constants + +import "time" + +// Display constants +const ( + // SpinnerShowTimeout :: duration after which spinner should be shown + SpinnerShowTimeout = 100 * time.Millisecond + + // Max Column Width + MaxColumnWidth = 1024 +) diff --git a/display/column.go b/display/column.go index ed7f941861..582e88aad0 100644 --- a/display/column.go +++ b/display/column.go @@ -19,6 +19,7 @@ func ColumnNames(columns []*sql.ColumnType) []string { return colNames } +// ColumnValuesAsString :: converts a slice of columns into strings func ColumnValuesAsString(values []interface{}, columns []*sql.ColumnType) ([]string, error) { rowAsString := make([]string, len(columns)) for idx, val := range values { diff --git a/display/display.go b/display/display.go index cd5c6fa5a7..4a0a2e19e2 100644 --- a/display/display.go +++ b/display/display.go @@ -7,12 +7,11 @@ import ( "fmt" "os" + "github.com/jedib0t/go-pretty/v6/table" "github.com/turbot/steampipe/cmdconfig" "github.com/turbot/steampipe/constants" "github.com/turbot/steampipe/db" "github.com/turbot/steampipe/utils" - - "github.com/olekukonko/tablewriter" ) // ShowOutput :: displays the output using the proper formatter as applicable @@ -68,13 +67,26 @@ func displayCSV(result *db.QueryResult) { func displayTable(result *db.QueryResult) { // the buffer to put the output data in outbuf := bytes.NewBufferString("") - table := tablewriter.NewWriter(outbuf) - table.SetAutoFormatHeaders(false) - table.SetAutoWrapText(false) - if cmdconfig.Viper().GetBool(constants.ArgHeader) { - table.SetHeader(ColumnNames(result.ColTypes)) + + // the table + t := table.NewWriter() + t.SetOutputMirror(outbuf) + t.SetStyle(table.StyleDefault) + + colConfigs := []table.ColumnConfig{} + headers := table.Row{} + + for idx, column := range result.ColTypes { + headers = append(headers, column.Name()) + colConfigs = append(colConfigs, table.ColumnConfig{ + Name: column.Name(), + Number: idx + 1, + WidthMax: constants.MaxColumnWidth, + }) } - table.SetBorder(true) + + t.SetColumnConfigs(colConfigs) + t.AppendHeader(headers) for { row := <-(*result.RowChan) @@ -83,20 +95,21 @@ func displayTable(result *db.QueryResult) { } // TODO how to handle error rowAsString, _ := ColumnValuesAsString(row, result.ColTypes) - table.Append(rowAsString) + rowObj := table.Row{} + for _, col := range rowAsString { + rowObj = append(rowObj, col) + } + t.AppendRow(rowObj) } - table.SetAutoWrapText(false) - // write out the table to the buffer - table.Render() - + t.Render() // if timer is turned on if cmdconfig.Viper().GetBool(constants.ArgTimer) { // put in the time information in the buffer outbuf.WriteString(fmt.Sprintf("\nTime: %v\n", <-result.Duration)) } - // spit it out! - displayPaged(outbuf.String()) + // page out the table + ShowPaged(outbuf.String()) } diff --git a/display/pager.go b/display/pager.go index e556053037..a4eccdfcf4 100644 --- a/display/pager.go +++ b/display/pager.go @@ -11,7 +11,8 @@ import ( "github.com/karrick/gows" ) -func displayPaged(content string) { +// ShowPaged :: displays the `content` in a system dependent pager +func ShowPaged(content string) { if isPagerNeeded(content) { switch runtime.GOOS { case "darwin", "linux": diff --git a/go.mod b/go.mod index 575840b8ec..d555c85d66 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/hashicorp/go-plugin v1.3.0 github.com/hashicorp/go-version v1.2.1 github.com/hashicorp/hcl/v2 v2.6.0 + github.com/jedib0t/go-pretty/v6 v6.0.6 github.com/karrick/gows v0.3.0 github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect diff --git a/go.sum b/go.sum index 4f629809c9..8e1032edb8 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,7 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/gertd/go-pluralize v0.1.7 h1:RgvJTJ5W7olOoAks97BOwOlekBFsLEyh00W48Z6ZEZY= @@ -241,6 +242,10 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jedib0t/go-pretty v1.0.0 h1:RbDCN8CAdLRirMDdk68J2WQJbLnlUK97+VHIUM8YHRw= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty/v6 v6.0.6 h1:hUOe8GJCG1gyGSUT8wiiFtB7TnkprPIkj5YmgcpR7lE= +github.com/jedib0t/go-pretty/v6 v6.0.6/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -355,6 +360,7 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -541,6 +547,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/metaquery/handlers.go b/metaquery/handlers.go index 46e71b6040..23d857d27b 100644 --- a/metaquery/handlers.go +++ b/metaquery/handlers.go @@ -8,13 +8,12 @@ import ( "strings" "github.com/c-bata/go-prompt" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/karrick/gows" + typeHelpers "github.com/turbot/go-kit/types" - "github.com/olekukonko/tablewriter" "github.com/turbot/steampipe/cmdconfig" "github.com/turbot/steampipe/connection_config" - - typeHelpers "github.com/turbot/go-kit/types" - "github.com/turbot/steampipe/constants" "github.com/turbot/steampipe/schema" "github.com/turbot/steampipe/utils" @@ -245,14 +244,72 @@ func inspectTable(connectionName string, tableName string, input *HandlerInput) return nil } -func writeTable(header []string, rows [][]string, autoMerge bool) { - table := tablewriter.NewWriter(os.Stdout) - table.SetAutoFormatHeaders(false) - table.SetHeader(header) - table.SetBorder(true) - table.SetAutoMergeCells(autoMerge) +func writeTable(headers []string, rows [][]string, autoMerge bool) { + t := table.NewWriter() + t.SetStyle(table.StyleDefault) + t.SetOutputMirror(os.Stdout) + + rowConfig := table.RowConfig{AutoMerge: autoMerge} + colConfigs, headerRow := getColumnSettings(headers, rows) + + t.SetColumnConfigs(colConfigs) + t.AppendHeader(headerRow) + for _, row := range rows { - table.Append(row) + rowObj := table.Row{} + for _, col := range row { + rowObj = append(rowObj, col) + } + t.AppendRow(rowObj, rowConfig) } - table.Render() + t.Render() +} + +// calculate and returns column configuration based on header and row content +func getColumnSettings(headers []string, rows [][]string) ([]table.ColumnConfig, table.Row) { + maxCols, _, _ := gows.GetWinSize() + colConfigs := make([]table.ColumnConfig, len(headers)) + headerRow := make(table.Row, len(headers)) + + sumOfAllCols := 0 + + // account for the spaces around the value of a column and separators + spaceAccounting := ((len(headers) * 3) + 1) + + for idx, colName := range headers { + headerRow[idx] = colName + + // get the maximum len of strings in this column + maxLen := 0 + for _, row := range rows { + colVal := row[idx] + if len(colVal) > maxLen { + maxLen = len(colVal) + } + if len(colName) > maxLen { + maxLen = len(colName) + } + } + colConfigs[idx] = table.ColumnConfig{ + Name: colName, + Number: idx + 1, + WidthMax: maxLen, + WidthMin: maxLen, + } + sumOfAllCols += maxLen + } + + // now that all columns are set to the widths that they need, + // set the last one to occupy as much as is available - no more - no less + sumOfRest := 0 + for idx, c := range colConfigs { + if idx == len(colConfigs)-1 { + continue + } + sumOfRest += c.WidthMax + } + colConfigs[len(colConfigs)-1].WidthMax = (maxCols - sumOfRest - spaceAccounting) + colConfigs[len(colConfigs)-1].WidthMin = (maxCols - sumOfRest - spaceAccounting) + + return colConfigs, headerRow }