mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 11:04:18 +02:00
further table streamlining: work with new table api everywhere
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user