From 34970ac9d90a84e4118397e8188cb6a538c25610 Mon Sep 17 00:00:00 2001 From: lew Date: Tue, 10 Feb 2026 22:17:55 +0000 Subject: [PATCH] refactor: consolidates all list files together --- cmd/list.go | 214 +++++++++++++++++++++++++++++------- cmd/list_flags.go | 101 ----------------- cmd/list_table.go | 270 ---------------------------------------------- 3 files changed, 175 insertions(+), 410 deletions(-) delete mode 100644 cmd/list_flags.go delete mode 100644 cmd/list_table.go diff --git a/cmd/list.go b/cmd/list.go index 3dd83a8..3a2bd4b 100644 --- a/cmd/list.go +++ b/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) diff --git a/cmd/list_flags.go b/cmd/list_flags.go deleted file mode 100644 index 91f29aa..0000000 --- a/cmd/list_flags.go +++ /dev/null @@ -1,101 +0,0 @@ -/* -Copyright © 2025 Lewis Wynne - -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 -} diff --git a/cmd/list_table.go b/cmd/list_table.go deleted file mode 100644 index 427c1a0..0000000 --- a/cmd/list_table.go +++ /dev/null @@ -1,270 +0,0 @@ -/* -Copyright © 2025 Lewis Wynne - -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 - } -}