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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"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{
|
||||
|
|
@ -59,9 +99,19 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
targetDB = "@" + dbName
|
||||
}
|
||||
|
||||
flags, err := enrichFlags()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
if listNoKeys && listNoValues && !listTTL {
|
||||
return fmt.Errorf("cannot ls '%s': no columns selected; disable --no-keys/--no-values or pass --ttl", targetDB)
|
||||
}
|
||||
|
||||
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")
|
||||
|
|
@ -77,29 +127,19 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
|
||||
columnKinds, err := requireColumns(flags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ls '%s': %v", targetDB, err)
|
||||
}
|
||||
|
||||
showValues := !listNoValues
|
||||
output := cmd.OutOrStdout()
|
||||
tw := table.NewWriter()
|
||||
tw.SetOutputMirror(output)
|
||||
tw.SetStyle(table.StyleDefault)
|
||||
// Should these be settable flags?
|
||||
tw.Style().Options.SeparateHeader = false
|
||||
tw.Style().Options.SeparateFooter = false
|
||||
tw.Style().Options.DrawBorder = false
|
||||
tw.Style().Options.SeparateRows = false
|
||||
tw.Style().Options.SeparateColumns = false
|
||||
|
||||
var maxContentWidths []int
|
||||
maxContentWidths = make([]int, len(columnKinds))
|
||||
|
||||
if flags.header {
|
||||
header := buildHeaderCells(columnKinds)
|
||||
updateMaxContentWidths(maxContentWidths, header)
|
||||
tw.AppendHeader(stringSliceToRow(header))
|
||||
if listHeader {
|
||||
tw.AppendHeader(headerRow(columns))
|
||||
}
|
||||
|
||||
placeholder := "**********"
|
||||
|
|
@ -111,7 +151,7 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
transact: func(tx *badger.Txn, k []byte) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchSize = 10
|
||||
opts.PrefetchValues = flags.value
|
||||
opts.PrefetchValues = showValues
|
||||
it := tx.NewIterator(opts)
|
||||
defer it.Close()
|
||||
var valueBuf []byte
|
||||
|
|
@ -126,33 +166,32 @@ func list(cmd *cobra.Command, args []string) error {
|
|||
isSecret := meta&metaSecret != 0
|
||||
|
||||
var valueStr string
|
||||
if flags.value && (!isSecret || flags.secrets) {
|
||||
if showValues && (!isSecret || listSecret) {
|
||||
if err := item.Value(func(v []byte) error {
|
||||
valueBuf = append(valueBuf[:0], v...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
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))
|
||||
for _, column := range columnKinds {
|
||||
switch column {
|
||||
row := make(table.Row, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
switch col {
|
||||
case columnKey:
|
||||
columns = append(columns, key)
|
||||
row = append(row, key)
|
||||
case columnValue:
|
||||
if isSecret && !flags.secrets {
|
||||
columns = append(columns, placeholder)
|
||||
if isSecret && !listSecret {
|
||||
row = append(row, placeholder)
|
||||
} else {
|
||||
columns = append(columns, valueStr)
|
||||
row = append(row, valueStr)
|
||||
}
|
||||
case columnTTL:
|
||||
columns = append(columns, formatExpiry(item.ExpiresAt()))
|
||||
row = append(row, formatExpiry(item.ExpiresAt()))
|
||||
}
|
||||
}
|
||||
updateMaxContentWidths(maxContentWidths, columns)
|
||||
tw.AppendRow(stringSliceToRow(columns))
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
applyColumnConstraints(tw, columnKinds, output, maxContentWidths)
|
||||
|
||||
flags.render(tw)
|
||||
applyColumnWidths(tw, columns, output)
|
||||
renderTable(tw)
|
||||
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() {
|
||||
listCmd.Flags().BoolVarP(&binary, "binary", "b", false, "include binary data in text output")
|
||||
listCmd.Flags().BoolVarP(&secret, "secret", "S", false, "display values marked as secret")
|
||||
listCmd.Flags().BoolVar(&noKeys, "no-keys", false, "suppress the key column")
|
||||
listCmd.Flags().BoolVar(&noValues, "no-values", false, "suppress the value column")
|
||||
listCmd.Flags().BoolVarP(&ttl, "ttl", "t", false, "append a TTL column when entries expire")
|
||||
listCmd.Flags().BoolVar(&header, "header", false, "include header row")
|
||||
listCmd.Flags().VarP(&format, "format", "o", "output format (table|tsv|csv|markdown|html)")
|
||||
listCmd.Flags().BoolVarP(&listBinary, "binary", "b", false, "include binary data in text output")
|
||||
listCmd.Flags().BoolVarP(&listSecret, "secret", "S", false, "display values marked as secret")
|
||||
listCmd.Flags().BoolVar(&listNoKeys, "no-keys", false, "suppress the key column")
|
||||
listCmd.Flags().BoolVar(&listNoValues, "no-values", false, "suppress the value column")
|
||||
listCmd.Flags().BoolVarP(&listTTL, "ttl", "t", false, "append a TTL column when entries expire")
|
||||
listCmd.Flags().BoolVar(&listHeader, "header", false, "include header row")
|
||||
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().String("glob-sep", "", fmt.Sprintf("Characters treated as separators for globbing (default %q)", defaultGlobSeparatorsDisplay()))
|
||||
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