refactor: consolidates all list files together

This commit is contained in:
Lewis Wynne 2026-02-10 22:17:55 +00:00
parent 4dff61074d
commit 34970ac9d9
3 changed files with 175 additions and 410 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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
}
}