refactor: consolidates all list files together
This commit is contained in:
parent
4dff61074d
commit
34970ac9d9
3 changed files with 175 additions and 410 deletions
214
cmd/list.go
214
cmd/list.go
|
|
@ -25,10 +25,50 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
"github.com/dgraph-io/badger/v4"
|
||||||
"github.com/jedib0t/go-pretty/v6/table"
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
|
"github.com/jedib0t/go-pretty/v6/text"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// formatEnum implements pflag.Value for format selection.
|
||||||
|
type formatEnum string
|
||||||
|
|
||||||
|
func (e *formatEnum) String() string { return string(*e) }
|
||||||
|
|
||||||
|
func (e *formatEnum) Set(v string) error {
|
||||||
|
switch v {
|
||||||
|
case "table", "tsv", "csv", "html", "markdown":
|
||||||
|
*e = formatEnum(v)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("must be one of \"table\", \"tsv\", \"csv\", \"html\", or \"markdown\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *formatEnum) Type() string { return "format" }
|
||||||
|
|
||||||
|
var (
|
||||||
|
listBinary bool
|
||||||
|
listSecret bool
|
||||||
|
listNoKeys bool
|
||||||
|
listNoValues bool
|
||||||
|
listTTL bool
|
||||||
|
listHeader bool
|
||||||
|
listFormat formatEnum = "table"
|
||||||
|
)
|
||||||
|
|
||||||
|
type columnKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
columnKey columnKind = iota
|
||||||
|
columnValue
|
||||||
|
columnTTL
|
||||||
)
|
)
|
||||||
|
|
||||||
var listCmd = &cobra.Command{
|
var listCmd = &cobra.Command{
|
||||||
|
|
@ -59,9 +99,19 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
targetDB = "@" + dbName
|
targetDB = "@" + dbName
|
||||||
}
|
}
|
||||||
|
|
||||||
flags, err := enrichFlags()
|
if listNoKeys && listNoValues && !listTTL {
|
||||||
if err != nil {
|
return fmt.Errorf("cannot ls '%s': no columns selected; disable --no-keys/--no-values or pass --ttl", targetDB)
|
||||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
}
|
||||||
|
|
||||||
|
var columns []columnKind
|
||||||
|
if !listNoKeys {
|
||||||
|
columns = append(columns, columnKey)
|
||||||
|
}
|
||||||
|
if !listNoValues {
|
||||||
|
columns = append(columns, columnValue)
|
||||||
|
}
|
||||||
|
if listTTL {
|
||||||
|
columns = append(columns, columnTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
globPatterns, err := cmd.Flags().GetStringSlice("glob")
|
globPatterns, err := cmd.Flags().GetStringSlice("glob")
|
||||||
|
|
@ -77,29 +127,19 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
columnKinds, err := requireColumns(flags)
|
showValues := !listNoValues
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output := cmd.OutOrStdout()
|
output := cmd.OutOrStdout()
|
||||||
tw := table.NewWriter()
|
tw := table.NewWriter()
|
||||||
tw.SetOutputMirror(output)
|
tw.SetOutputMirror(output)
|
||||||
tw.SetStyle(table.StyleDefault)
|
tw.SetStyle(table.StyleDefault)
|
||||||
// Should these be settable flags?
|
|
||||||
tw.Style().Options.SeparateHeader = false
|
tw.Style().Options.SeparateHeader = false
|
||||||
tw.Style().Options.SeparateFooter = false
|
tw.Style().Options.SeparateFooter = false
|
||||||
tw.Style().Options.DrawBorder = false
|
tw.Style().Options.DrawBorder = false
|
||||||
tw.Style().Options.SeparateRows = false
|
tw.Style().Options.SeparateRows = false
|
||||||
tw.Style().Options.SeparateColumns = false
|
tw.Style().Options.SeparateColumns = false
|
||||||
|
|
||||||
var maxContentWidths []int
|
if listHeader {
|
||||||
maxContentWidths = make([]int, len(columnKinds))
|
tw.AppendHeader(headerRow(columns))
|
||||||
|
|
||||||
if flags.header {
|
|
||||||
header := buildHeaderCells(columnKinds)
|
|
||||||
updateMaxContentWidths(maxContentWidths, header)
|
|
||||||
tw.AppendHeader(stringSliceToRow(header))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
placeholder := "**********"
|
placeholder := "**********"
|
||||||
|
|
@ -111,7 +151,7 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
transact: func(tx *badger.Txn, k []byte) error {
|
transact: func(tx *badger.Txn, k []byte) error {
|
||||||
opts := badger.DefaultIteratorOptions
|
opts := badger.DefaultIteratorOptions
|
||||||
opts.PrefetchSize = 10
|
opts.PrefetchSize = 10
|
||||||
opts.PrefetchValues = flags.value
|
opts.PrefetchValues = showValues
|
||||||
it := tx.NewIterator(opts)
|
it := tx.NewIterator(opts)
|
||||||
defer it.Close()
|
defer it.Close()
|
||||||
var valueBuf []byte
|
var valueBuf []byte
|
||||||
|
|
@ -126,33 +166,32 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
isSecret := meta&metaSecret != 0
|
isSecret := meta&metaSecret != 0
|
||||||
|
|
||||||
var valueStr string
|
var valueStr string
|
||||||
if flags.value && (!isSecret || flags.secrets) {
|
if showValues && (!isSecret || listSecret) {
|
||||||
if err := item.Value(func(v []byte) error {
|
if err := item.Value(func(v []byte) error {
|
||||||
valueBuf = append(valueBuf[:0], v...)
|
valueBuf = append(valueBuf[:0], v...)
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||||
}
|
}
|
||||||
valueStr = store.FormatBytes(flags.binary, valueBuf)
|
valueStr = store.FormatBytes(listBinary, valueBuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
columns := make([]string, 0, len(columnKinds))
|
row := make(table.Row, 0, len(columns))
|
||||||
for _, column := range columnKinds {
|
for _, col := range columns {
|
||||||
switch column {
|
switch col {
|
||||||
case columnKey:
|
case columnKey:
|
||||||
columns = append(columns, key)
|
row = append(row, key)
|
||||||
case columnValue:
|
case columnValue:
|
||||||
if isSecret && !flags.secrets {
|
if isSecret && !listSecret {
|
||||||
columns = append(columns, placeholder)
|
row = append(row, placeholder)
|
||||||
} else {
|
} else {
|
||||||
columns = append(columns, valueStr)
|
row = append(row, valueStr)
|
||||||
}
|
}
|
||||||
case columnTTL:
|
case columnTTL:
|
||||||
columns = append(columns, formatExpiry(item.ExpiresAt()))
|
row = append(row, formatExpiry(item.ExpiresAt()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateMaxContentWidths(maxContentWidths, columns)
|
tw.AppendRow(row)
|
||||||
tw.AppendRow(stringSliceToRow(columns))
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
@ -166,20 +205,117 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("cannot ls '%s': No matches for pattern %s", targetDB, formatGlobPatterns(globPatterns))
|
return fmt.Errorf("cannot ls '%s': No matches for pattern %s", targetDB, formatGlobPatterns(globPatterns))
|
||||||
}
|
}
|
||||||
|
|
||||||
applyColumnConstraints(tw, columnKinds, output, maxContentWidths)
|
applyColumnWidths(tw, columns, output)
|
||||||
|
renderTable(tw)
|
||||||
flags.render(tw)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func headerRow(columns []columnKind) table.Row {
|
||||||
|
row := make(table.Row, 0, len(columns))
|
||||||
|
for _, col := range columns {
|
||||||
|
switch col {
|
||||||
|
case columnKey:
|
||||||
|
row = append(row, "Key")
|
||||||
|
case columnValue:
|
||||||
|
row = append(row, "Value")
|
||||||
|
case columnTTL:
|
||||||
|
row = append(row, "TTL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyColumnWidths(tw table.Writer, columns []columnKind, out io.Writer) {
|
||||||
|
termWidth := detectTerminalWidth(out)
|
||||||
|
if termWidth <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tw.SetAllowedRowLength(termWidth)
|
||||||
|
|
||||||
|
// Padding per column: go-pretty's default is one space each side.
|
||||||
|
padding := len(columns) * 2
|
||||||
|
available := termWidth - padding
|
||||||
|
if available < len(columns) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give key and TTL columns a fixed budget; value gets the rest.
|
||||||
|
const keyWidth = 30
|
||||||
|
const ttlWidth = 40
|
||||||
|
valueWidth := available
|
||||||
|
for _, col := range columns {
|
||||||
|
switch col {
|
||||||
|
case columnKey:
|
||||||
|
valueWidth -= keyWidth
|
||||||
|
case columnTTL:
|
||||||
|
valueWidth -= ttlWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if valueWidth < 10 {
|
||||||
|
valueWidth = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs []table.ColumnConfig
|
||||||
|
for i, col := range columns {
|
||||||
|
var maxW int
|
||||||
|
switch col {
|
||||||
|
case columnKey:
|
||||||
|
maxW = keyWidth
|
||||||
|
case columnValue:
|
||||||
|
maxW = valueWidth
|
||||||
|
case columnTTL:
|
||||||
|
maxW = ttlWidth
|
||||||
|
}
|
||||||
|
configs = append(configs, table.ColumnConfig{
|
||||||
|
Number: i + 1,
|
||||||
|
WidthMax: maxW,
|
||||||
|
WidthMaxEnforcer: text.WrapText,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
tw.SetColumnConfigs(configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectTerminalWidth(out io.Writer) int {
|
||||||
|
type fd interface{ Fd() uintptr }
|
||||||
|
if f, ok := out.(fd); ok {
|
||||||
|
if w, _, err := term.GetSize(int(f.Fd())); err == nil && w > 0 {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
if cols := os.Getenv("COLUMNS"); cols != "" {
|
||||||
|
if parsed, err := strconv.Atoi(cols); err == nil && parsed > 0 {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTable(tw table.Writer) {
|
||||||
|
switch listFormat.String() {
|
||||||
|
case "tsv":
|
||||||
|
tw.RenderTSV()
|
||||||
|
case "csv":
|
||||||
|
tw.RenderCSV()
|
||||||
|
case "html":
|
||||||
|
tw.RenderHTML()
|
||||||
|
case "markdown":
|
||||||
|
tw.RenderMarkdown()
|
||||||
|
default:
|
||||||
|
tw.Render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
listCmd.Flags().BoolVarP(&binary, "binary", "b", false, "include binary data in text output")
|
listCmd.Flags().BoolVarP(&listBinary, "binary", "b", false, "include binary data in text output")
|
||||||
listCmd.Flags().BoolVarP(&secret, "secret", "S", false, "display values marked as secret")
|
listCmd.Flags().BoolVarP(&listSecret, "secret", "S", false, "display values marked as secret")
|
||||||
listCmd.Flags().BoolVar(&noKeys, "no-keys", false, "suppress the key column")
|
listCmd.Flags().BoolVar(&listNoKeys, "no-keys", false, "suppress the key column")
|
||||||
listCmd.Flags().BoolVar(&noValues, "no-values", false, "suppress the value column")
|
listCmd.Flags().BoolVar(&listNoValues, "no-values", false, "suppress the value column")
|
||||||
listCmd.Flags().BoolVarP(&ttl, "ttl", "t", false, "append a TTL column when entries expire")
|
listCmd.Flags().BoolVarP(&listTTL, "ttl", "t", false, "append a TTL column when entries expire")
|
||||||
listCmd.Flags().BoolVar(&header, "header", false, "include header row")
|
listCmd.Flags().BoolVar(&listHeader, "header", false, "include header row")
|
||||||
listCmd.Flags().VarP(&format, "format", "o", "output format (table|tsv|csv|markdown|html)")
|
listCmd.Flags().VarP(&listFormat, "format", "o", "output format (table|tsv|csv|markdown|html)")
|
||||||
listCmd.Flags().StringSliceP("glob", "g", nil, "Filter keys with glob pattern (repeatable)")
|
listCmd.Flags().StringSliceP("glob", "g", nil, "Filter keys with glob pattern (repeatable)")
|
||||||
listCmd.Flags().String("glob-sep", "", fmt.Sprintf("Characters treated as separators for globbing (default %q)", defaultGlobSeparatorsDisplay()))
|
listCmd.Flags().String("glob-sep", "", fmt.Sprintf("Characters treated as separators for globbing (default %q)", defaultGlobSeparatorsDisplay()))
|
||||||
rootCmd.AddCommand(listCmd)
|
rootCmd.AddCommand(listCmd)
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright © 2025 Lewis Wynne <lew@ily.rs>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/jedib0t/go-pretty/v6/table"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ListArgs tracks the resolved flag configuration for the list command.
|
|
||||||
type ListArgs struct {
|
|
||||||
header bool
|
|
||||||
key bool
|
|
||||||
value bool
|
|
||||||
ttl bool
|
|
||||||
binary bool
|
|
||||||
secrets bool
|
|
||||||
render func(table.Writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatEnum implements pflag.Value for format selection.
|
|
||||||
type formatEnum string
|
|
||||||
|
|
||||||
func (e *formatEnum) String() string {
|
|
||||||
return string(*e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *formatEnum) Set(v string) error {
|
|
||||||
switch v {
|
|
||||||
case "table", "tsv", "csv", "html", "markdown":
|
|
||||||
*e = formatEnum(v)
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("must be one of \"table\", \"tsv\", \"csv\", \"html\", or \"markdown\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *formatEnum) Type() string {
|
|
||||||
return "format"
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
binary bool = false
|
|
||||||
secret bool = false
|
|
||||||
noKeys bool = false
|
|
||||||
noValues bool = false
|
|
||||||
ttl bool = false
|
|
||||||
header bool = false
|
|
||||||
format formatEnum = "table"
|
|
||||||
)
|
|
||||||
|
|
||||||
func enrichFlags() (ListArgs, error) {
|
|
||||||
var renderFunc func(tw table.Writer)
|
|
||||||
switch format.String() {
|
|
||||||
case "tsv":
|
|
||||||
renderFunc = func(tw table.Writer) { tw.RenderTSV() }
|
|
||||||
case "csv":
|
|
||||||
renderFunc = func(tw table.Writer) { tw.RenderCSV() }
|
|
||||||
case "html":
|
|
||||||
renderFunc = func(tw table.Writer) { tw.RenderHTML() }
|
|
||||||
case "markdown":
|
|
||||||
renderFunc = func(tw table.Writer) { tw.RenderMarkdown() }
|
|
||||||
case "table":
|
|
||||||
renderFunc = func(tw table.Writer) { tw.Render() }
|
|
||||||
}
|
|
||||||
|
|
||||||
if noKeys && noValues && !ttl {
|
|
||||||
return ListArgs{}, fmt.Errorf("no columns selected; disable --no-keys/--no-values or pass --ttl")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListArgs{
|
|
||||||
header: header,
|
|
||||||
key: !noKeys,
|
|
||||||
value: !noValues,
|
|
||||||
ttl: ttl,
|
|
||||||
binary: binary,
|
|
||||||
render: renderFunc,
|
|
||||||
secrets: secret,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,270 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright © 2025 Lewis Wynne <lew@ily.rs>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/jedib0t/go-pretty/v6/table"
|
|
||||||
"github.com/jedib0t/go-pretty/v6/text"
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
type columnKind int
|
|
||||||
|
|
||||||
const (
|
|
||||||
columnKey columnKind = iota
|
|
||||||
columnValue
|
|
||||||
columnTTL
|
|
||||||
)
|
|
||||||
|
|
||||||
func requireColumns(args ListArgs) ([]columnKind, error) {
|
|
||||||
var columns []columnKind
|
|
||||||
if args.key {
|
|
||||||
columns = append(columns, columnKey)
|
|
||||||
}
|
|
||||||
if args.value {
|
|
||||||
columns = append(columns, columnValue)
|
|
||||||
}
|
|
||||||
if args.ttl {
|
|
||||||
columns = append(columns, columnTTL)
|
|
||||||
}
|
|
||||||
if len(columns) == 0 {
|
|
||||||
return nil, fmt.Errorf("no columns selected; enable key, value, or ttl output")
|
|
||||||
}
|
|
||||||
return columns, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildHeaderCells(columnKinds []columnKind) []string {
|
|
||||||
labels := make([]string, 0, len(columnKinds))
|
|
||||||
for _, column := range columnKinds {
|
|
||||||
switch column {
|
|
||||||
case columnKey:
|
|
||||||
labels = append(labels, "Key")
|
|
||||||
case columnValue:
|
|
||||||
labels = append(labels, "Value")
|
|
||||||
case columnTTL:
|
|
||||||
labels = append(labels, "TTL")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return labels
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringSliceToRow(values []string) table.Row {
|
|
||||||
row := make(table.Row, len(values))
|
|
||||||
for i, val := range values {
|
|
||||||
row[i] = val
|
|
||||||
}
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateMaxContentWidths(maxWidths []int, values []string) {
|
|
||||||
if len(maxWidths) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
limit := min(len(values), len(maxWidths))
|
|
||||||
for i := range limit {
|
|
||||||
width := text.LongestLineLen(values[i])
|
|
||||||
if width > maxWidths[i] {
|
|
||||||
maxWidths[i] = width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyColumnConstraints(tw table.Writer, columns []columnKind, out io.Writer, maxContentWidths []int) {
|
|
||||||
totalWidth := detectTerminalWidth(out)
|
|
||||||
if totalWidth <= 0 {
|
|
||||||
totalWidth = 100
|
|
||||||
}
|
|
||||||
contentWidth := contentWidthForStyle(totalWidth, tw, len(columns))
|
|
||||||
widths := distributeWidths(contentWidth, columns)
|
|
||||||
|
|
||||||
used := 0
|
|
||||||
for idx, width := range widths {
|
|
||||||
if width <= 0 {
|
|
||||||
width = 1
|
|
||||||
}
|
|
||||||
if idx < len(maxContentWidths) {
|
|
||||||
if actual := maxContentWidths[idx]; actual > 0 && width > actual {
|
|
||||||
width = actual
|
|
||||||
}
|
|
||||||
}
|
|
||||||
widths[idx] = width
|
|
||||||
used += width
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining := contentWidth - used
|
|
||||||
for remaining > 0 {
|
|
||||||
progressed := false
|
|
||||||
for idx := range widths {
|
|
||||||
actual := 0
|
|
||||||
if idx < len(maxContentWidths) {
|
|
||||||
actual = maxContentWidths[idx]
|
|
||||||
}
|
|
||||||
if actual > 0 && widths[idx] >= actual {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
widths[idx]++
|
|
||||||
remaining--
|
|
||||||
progressed = true
|
|
||||||
if remaining == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !progressed {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configs := make([]table.ColumnConfig, 0, len(columns))
|
|
||||||
for idx, width := range widths {
|
|
||||||
configs = append(configs, table.ColumnConfig{
|
|
||||||
Number: idx + 1,
|
|
||||||
WidthMax: width,
|
|
||||||
WidthMaxEnforcer: text.WrapText,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
tw.SetColumnConfigs(configs)
|
|
||||||
tw.SetAllowedRowLength(totalWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
func contentWidthForStyle(totalWidth int, tw table.Writer, columnCount int) int {
|
|
||||||
if columnCount == 0 {
|
|
||||||
return totalWidth
|
|
||||||
}
|
|
||||||
style := tw.Style()
|
|
||||||
if style != nil {
|
|
||||||
totalWidth -= tableRowOverhead(style, columnCount)
|
|
||||||
}
|
|
||||||
if totalWidth < columnCount {
|
|
||||||
totalWidth = columnCount
|
|
||||||
}
|
|
||||||
return totalWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableRowOverhead(style *table.Style, columnCount int) int {
|
|
||||||
if style == nil || columnCount == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
paddingWidth := text.StringWidthWithoutEscSequences(style.Box.PaddingLeft + style.Box.PaddingRight)
|
|
||||||
overhead := paddingWidth * columnCount
|
|
||||||
if style.Options.SeparateColumns && columnCount > 1 {
|
|
||||||
overhead += (columnCount - 1) * maxSeparatorWidth(style)
|
|
||||||
}
|
|
||||||
if style.Options.DrawBorder {
|
|
||||||
overhead += text.StringWidthWithoutEscSequences(style.Box.Left + style.Box.Right)
|
|
||||||
}
|
|
||||||
return overhead
|
|
||||||
}
|
|
||||||
|
|
||||||
func maxSeparatorWidth(style *table.Style) int {
|
|
||||||
widest := 0
|
|
||||||
separators := []string{
|
|
||||||
style.Box.MiddleSeparator,
|
|
||||||
style.Box.EmptySeparator,
|
|
||||||
style.Box.MiddleHorizontal,
|
|
||||||
style.Box.TopSeparator,
|
|
||||||
style.Box.BottomSeparator,
|
|
||||||
style.Box.MiddleVertical,
|
|
||||||
style.Box.LeftSeparator,
|
|
||||||
style.Box.RightSeparator,
|
|
||||||
}
|
|
||||||
for _, sep := range separators {
|
|
||||||
if width := text.StringWidthWithoutEscSequences(sep); width > widest {
|
|
||||||
widest = width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return widest
|
|
||||||
}
|
|
||||||
|
|
||||||
type fdWriter interface {
|
|
||||||
Fd() uintptr
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectTerminalWidth(out io.Writer) int {
|
|
||||||
if f, ok := out.(fdWriter); ok {
|
|
||||||
if w, _, err := term.GetSize(int(f.Fd())); err == nil && w > 0 {
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
if cols := os.Getenv("COLUMNS"); cols != "" {
|
|
||||||
if parsed, err := strconv.Atoi(cols); err == nil && parsed > 0 {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func distributeWidths(total int, columns []columnKind) []int {
|
|
||||||
if total <= 0 {
|
|
||||||
total = 100
|
|
||||||
}
|
|
||||||
hasTTL := slices.Contains(columns, columnTTL)
|
|
||||||
base := make([]float64, len(columns))
|
|
||||||
sum := 0.0
|
|
||||||
for i, c := range columns {
|
|
||||||
pct := basePercentageForColumn(c, hasTTL)
|
|
||||||
base[i] = pct
|
|
||||||
sum += pct
|
|
||||||
}
|
|
||||||
if sum == 0 {
|
|
||||||
sum = 1
|
|
||||||
}
|
|
||||||
widths := make([]int, len(columns))
|
|
||||||
remaining := total
|
|
||||||
const minColWidth = 10
|
|
||||||
for i := range columns {
|
|
||||||
width := max(int((base[i]/sum)*float64(total)), minColWidth)
|
|
||||||
widths[i] = width
|
|
||||||
remaining -= width
|
|
||||||
}
|
|
||||||
for i := 0; remaining > 0 && len(columns) > 0; i++ {
|
|
||||||
idx := i % len(columns)
|
|
||||||
widths[idx]++
|
|
||||||
remaining--
|
|
||||||
}
|
|
||||||
return widths
|
|
||||||
}
|
|
||||||
|
|
||||||
func basePercentageForColumn(c columnKind, hasTTL bool) float64 {
|
|
||||||
switch c {
|
|
||||||
case columnKey:
|
|
||||||
return 0.25
|
|
||||||
case columnValue:
|
|
||||||
if hasTTL {
|
|
||||||
return 0.5
|
|
||||||
}
|
|
||||||
return 0.75
|
|
||||||
case columnTTL:
|
|
||||||
return 0.25
|
|
||||||
default:
|
|
||||||
return 0.25
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue