further table streamlining: work with new table api everywhere

This commit is contained in:
2026-08-09 21:48:03 +02:00
parent b49153bd38
commit 2841a94fe5
27 changed files with 271 additions and 249 deletions

View File

@@ -1,3 +1,5 @@
package printer
/*
Copyright © 2026 Thomas von Dein
@@ -14,15 +16,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package printer
import "strings"
// a wrapper around strings.Builder for convenience
// Builder is a wrapper around strings.Builder for convenience
type Builder struct {
strings.Builder
}
func (builder *Builder) WriteStringLine(in string) {
builder.WriteString(in + "\n")
builder.WriteString(in)
builder.WriteString("\n")
}

View File

@@ -1,3 +1,5 @@
package printer
/*
Copyright © 2026 Thomas von Dein
@@ -14,10 +16,11 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package printer
import "github.com/dustin/go-humanize"
// ByteSize converts uint64 bytes into human readable size
// e.g. 1500 => 1.5kb
type ByteSize struct {
size uint64
}

View File

@@ -28,9 +28,9 @@ import (
// stringer returns the string representation of different types of
// values
func stringer(in any) string {
func stringer(cell any) string {
//nolint:gocritic
switch val := in.(type) {
switch val := cell.(type) {
case string:
return val
case bool:
@@ -60,12 +60,13 @@ func stringer(in any) string {
case *types.Float64:
// ignore err here, because types.Float64.MarshalJSON() never returns one
f, _ := val.MarshalJSON()
return string(f)
default:
// Caution: this may cause a panic if the [unknown] type does
// not implement fmt.Stringer. In this case add another case
// to the type switch for it above.
return in.(fmt.Stringer).String()
return cell.(fmt.Stringer).String()
}
}

View File

@@ -1,3 +1,5 @@
package printer
/*
Copyright © 2026 Thomas von Dein
@@ -15,8 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package printer
import (
"os"

View File

@@ -1,3 +1,5 @@
package printer
/*
Copyright © 2026 Thomas von Dein
@@ -14,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package printer
import (
"fmt"

View File

@@ -1,3 +1,5 @@
package printer
/*
Copyright © 2026 Thomas von Dein
@@ -14,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package printer
import (
"fmt"

View File

@@ -1,3 +1,5 @@
package printer
/*
Copyright © 2026 Thomas von Dein
@@ -14,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package printer
// pager setup using bubbletea
// file shamlelessly copied from:

View File

@@ -1,3 +1,5 @@
package printer
/*
Copyright © 2026 Thomas von Dein
@@ -14,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package printer
import (
"encoding/json"
@@ -28,13 +29,20 @@ import (
"gopkg.in/yaml.v3"
)
type Table struct {
Mode string // tsv, json, yaml
Headers []string
RawHeaders []string
Entries [][]any
var (
// ansiCtrlSeq is being used to remove ANSI control sequences so
// that we can properly determine string lenght's
ansiCtrlSeq = regexp.MustCompile(`\033.[0-9;]+m`)
)
rows [][]string // representation used for printing
// Table stores tabular data for printing
type Table struct {
Mode string // tsv, json, yaml
Headers []string // colored headers
RawHeaders []string // plain string headers
Entries [][]any // rows of cells as fed in by pkg/es
rows [][]string // representation used for printing, cells are stringified
processed bool
lenHeaders []int
alignInts bool
@@ -42,6 +50,7 @@ type Table struct {
debugGoRoutines bool
}
// NewTable returns a new empty table object with unaligned storage
func NewTable(conf *cfg.Config) *Table {
return new(Table{
Mode: conf.Output,
@@ -54,55 +63,52 @@ func NewTable(conf *cfg.Config) *Table {
})
}
func (table *Table) WithSize(columns, rows int) *Table {
// WithSize configures the dimensions of the table, allocs aligned storage
func (t *Table) WithSize(columns, rows int) *Table {
if columns > 0 {
table.Headers = make([]string, columns)
table.RawHeaders = make([]string, columns)
table.lenHeaders = make([]int, columns)
t.Headers = make([]string, columns)
t.RawHeaders = make([]string, columns)
t.lenHeaders = make([]int, columns)
}
if rows > 0 {
table.Entries = make([][]any, rows)
t.Entries = make([][]any, rows)
}
return table
return t
}
func (table *Table) WithHeaders(headers ...string) *Table {
// WithHeaders sets table headers
func (t *Table) WithHeaders(headers ...string) *Table {
count := len(headers)
table.WithSize(count, 0).Addheaders(headers...)
return table
return t.WithSize(count, 0).formatHeaders(headers...)
}
func (table *Table) Print() error {
// Print outputs the tabular data according to Table.Mode
func (t *Table) Print() error {
var err error
switch table.Mode {
switch t.Mode {
case "json":
err = table.PrintJSON()
err = t.PrintJSON()
case "yaml":
err = table.PrintYAML()
err = t.PrintYAML()
case "csv":
err = table.PrintCSV()
err = t.PrintCSV()
default:
err = table.PrintTSV()
err = t.PrintTSV()
}
if table.debugGoRoutines {
if t.debugGoRoutines {
printGoRoutineMetrics()
}
return err
}
var (
ansiCtrlSeq = regexp.MustCompile(`\033.[0-9;]+m`)
)
func (table *Table) PrintYAML() error {
raw := table.toMap()
func (t *Table) PrintYAML() error {
raw := t.toMap()
body, err := yaml.Marshal(raw)
if err != nil {
@@ -114,8 +120,8 @@ func (table *Table) PrintYAML() error {
return nil
}
func (table *Table) PrintJSON() error {
raw := table.toMap()
func (t *Table) PrintJSON() error {
raw := t.toMap()
body, err := json.MarshalIndent(raw, "", " ")
if err != nil {
@@ -127,56 +133,101 @@ func (table *Table) PrintJSON() error {
return nil
}
func (table *Table) PrintTSV() error {
// length's, convert cell types
table.preprocessRows()
func (t *Table) PrintCSV() error {
t.preprocessRows()
// output headers
for idx, header := range table.Headers {
if idx+1 != len(table.Headers) {
fmt.Print(header, strings.Repeat(" ", table.lenHeaders[idx]-visibleLen(header)))
} else {
// no padding for last header
fmt.Print(header)
fmt.Println(strings.Join(t.RawHeaders, ","))
for _, entries := range t.rows {
row := make([]string, len(entries))
for idx, entry := range entries {
if strings.Contains(entry, " ") || strings.Contains(entry, ",") {
row[idx] = `"` + entry + `"`
} else {
row[idx] = entry
}
}
if idx < len(table.Headers)-1 {
fmt.Print(" ")
}
fmt.Println(strings.Join(row, ","))
}
fmt.Println()
return nil
}
for _, entries := range table.rows {
// Sort tabular data by first column
func (t *Table) Sort() {
// sanity checks
if len(t.Entries) == 0 {
return
}
t.preprocessRows()
sort.Slice(t.rows, func(i, j int) bool {
return t.rows[i][0] < t.rows[j][0]
})
}
func (t *Table) AddRow(fields ...any) {
t.Entries = append(t.Entries, fields)
}
func (t *Table) AddRowLate(fields ...any) {
t.AddRow(fields)
if !t.processed {
return
}
row := make([]string, len(fields))
for idx, field := range fields {
row[idx] = stringer(field)
}
t.rows = append(t.rows, row)
}
// PrintTSV is the default printer, it outputs in tab-separated-value format
func (t *Table) PrintTSV() error {
t.preprocessRows()
t.printTsvHeaders()
return t.printTsvRows()
}
func (t *Table) printTsvRows() error {
for _, entries := range t.rows {
currentWidth := 0
columns := len(entries)
for idx, entry := range entries {
length := visibleLen(entry)
if length+currentWidth > table.maxwidth &&
table.maxwidth-currentWidth > 1 &&
if length+currentWidth > t.maxwidth &&
t.maxwidth-currentWidth > 1 &&
idx == columns-1 {
// text is too wide to be put into one line, and
// it's the last cell, so wrap it
entry = wrap(table.maxwidth-currentWidth, currentWidth+2, entry)
entry = wrap(t.maxwidth-currentWidth, currentWidth+2, entry)
}
currentWidth += table.lenHeaders[idx]
currentWidth += t.lenHeaders[idx]
switch {
case isInt(entry) && table.alignInts:
case isInt(entry) && t.alignInts:
// align right
fmt.Print(strings.Repeat(" ", table.lenHeaders[idx]-length), entry)
case length < table.lenHeaders[idx] && idx+1 != len(entries):
fmt.Print(strings.Repeat(" ", t.lenHeaders[idx]-length), entry)
case length < t.lenHeaders[idx] && idx+1 != len(entries):
// pad right, if required
fmt.Print(entry, strings.Repeat(" ", table.lenHeaders[idx]-length))
fmt.Print(entry, strings.Repeat(" ", t.lenHeaders[idx]-length))
default:
// no padding for last entry
fmt.Print(entry)
}
if idx < len(table.Headers)-1 {
if idx < len(t.Headers)-1 {
fmt.Print(" ")
}
}
@@ -187,6 +238,40 @@ func (table *Table) PrintTSV() error {
return nil
}
// printTsvHeaders outputs TSV headers
func (t *Table) printTsvHeaders() {
for idx, header := range t.Headers {
if idx+1 != len(t.Headers) {
fmt.Print(header, strings.Repeat(" ", t.lenHeaders[idx]-visibleLen(header)))
} else {
// no padding for last header
fmt.Print(header)
}
if idx < len(t.Headers)-1 {
fmt.Print(" ")
}
}
fmt.Println()
}
// formatHeaders formats header fields according to output mode
func (t *Table) formatHeaders(headers ...string) *Table {
for idx, header := range headers {
switch t.Mode {
case "json", "yaml":
t.Headers[idx] = strings.ReplaceAll(strings.ToLower(header), " ", "_")
default:
t.Headers[idx] = Bold(strings.ReplaceAll(strings.ToUpper(header), " ", "-"))
}
t.RawHeaders[idx] = header
}
return t
}
// Wrap a text into multiple lines, first line is not indented, all
// further lines will be indented. Used within Print() to print large
// cell text.
@@ -229,129 +314,61 @@ func wrap(width, indent int, text string) string {
return wrapped
}
func (table *Table) PrintCSV() error {
table.preprocessRows()
fmt.Println(strings.Join(table.RawHeaders, ","))
for _, entries := range table.rows {
row := make([]string, len(entries))
for idx, entry := range entries {
if strings.Contains(entry, " ") || strings.Contains(entry, ",") {
row[idx] = `"` + entry + `"`
} else {
row[idx] = entry
}
}
fmt.Println(strings.Join(row, ","))
}
return nil
}
func (table *Table) Sort() {
// sanity checks
if len(table.Entries) == 0 {
return
}
table.preprocessRows()
sort.Slice(table.rows, func(i, j int) bool {
return table.rows[i][0] < table.rows[j][0]
})
}
func (table *Table) Addheaders(headers ...string) {
for idx, header := range headers {
switch table.Mode {
case "json", "yaml":
table.Headers[idx] = strings.ReplaceAll(strings.ToLower(header), " ", "_")
default:
table.Headers[idx] = Bold(strings.ReplaceAll(strings.ToUpper(header), " ", "-"))
}
table.RawHeaders[idx] = header
}
}
func (table *Table) AddRow(fields ...any) {
table.Entries = append(table.Entries, fields)
}
func (table *Table) AddRowLate(fields ...any) {
table.AddRow(fields)
if !table.processed {
return
}
row := make([]string, len(fields))
for idx, field := range fields {
row[idx] = stringer(field)
}
table.rows = append(table.rows, row)
}
// needed for json and yaml output
func (table *Table) toMap() []map[string]any {
raw := make([]map[string]any, len(table.Entries))
func (t *Table) toMap() []map[string]any {
raw := make([]map[string]any, len(t.Entries))
for idx, entries := range table.Entries {
raw[idx] = make(map[string]any, len(table.Headers))
for idx, entries := range t.Entries {
raw[idx] = make(map[string]any, len(t.Headers))
for eidx, entry := range entries {
raw[idx][table.Headers[eidx]] = entry
raw[idx][t.Headers[eidx]] = entry
}
}
return raw
}
func (data *Table) preprocessRows() {
if data.processed {
func (t *Table) preprocessRows() {
if t.processed {
// only do it once
return
}
// convert entries to strings
data.rows = make([][]string, len(data.Entries))
for rowidx, entries := range data.Entries {
data.rows[rowidx] = make([]string, len(entries))
t.rows = make([][]string, len(t.Entries))
for rowidx, entries := range t.Entries {
t.rows[rowidx] = make([]string, len(entries))
for colidx, entry := range data.Entries[rowidx] {
data.rows[rowidx][colidx] = stringer(entry)
for colidx, entry := range t.Entries[rowidx] {
t.rows[rowidx][colidx] = stringer(entry)
}
}
// determine header lenght's
for idx, head := range data.Headers {
data.lenHeaders[idx] = visibleLen(head)
for idx, head := range t.Headers {
t.lenHeaders[idx] = visibleLen(head)
}
// determine max width per column
for _, entries := range data.rows {
for _, entries := range t.rows {
currentWidth := 0
for idx, entry := range entries {
length := visibleLen(entry)
if data.lenHeaders[idx] < length {
if length > currentWidth+data.maxwidth {
data.lenHeaders[idx] = data.maxwidth - currentWidth
if t.lenHeaders[idx] < length {
if length > currentWidth+t.maxwidth {
t.lenHeaders[idx] = t.maxwidth - currentWidth
} else {
data.lenHeaders[idx] = length
t.lenHeaders[idx] = length
}
}
currentWidth += data.lenHeaders[idx]
currentWidth += t.lenHeaders[idx]
}
}
data.processed = true
t.processed = true
}
func isInt(num string) bool {