/* Copyright © 2026 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 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 . */ package printer import ( "encoding/json" "fmt" "regexp" "sort" "strconv" "strings" "codeberg.org/scip/esctl/pkg/cfg" "github.com/seeruk/go-wordwrap" "gopkg.in/yaml.v3" ) type Table struct { Mode string // tsv, json, yaml Headers []string Entries [][]any rows [][]string // representation used for printing processed bool lenHeaders []int alignInts bool maxwidth int } func NewTable(conf *cfg.Config, columns, rows int) *Table { table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()} table.Headers = make([]string, columns) table.Entries = make([][]any, rows) table.lenHeaders = make([]int, columns) table.alignInts = conf.AlignInts return &table } func NewTableEmpty(conf *cfg.Config) *Table { table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()} table.alignInts = conf.AlignInts return &table } func (table *Table) WithHeaders(headers ...string) *Table { count := len(headers) table.Entries = [][]any{} table.lenHeaders = make([]int, count) table.Headers = make([]string, count) table.Addheaders(headers...) return table } func (table *Table) Print() error { switch table.Mode { case "json": return table.PrintJSON() case "yaml": return table.PrintYAML() default: return table.PrintTSV() } } var ( ansiCtrlSeq = regexp.MustCompile(`\033.[0-9;]+m`) ) func (table *Table) PrintYAML() error { raw := table.toMap() body, err := yaml.Marshal(raw) if err != nil { return fmt.Errorf("failed to produce YAML output: %w", err) } fmt.Println(string(body)) return nil } func (table *Table) PrintJSON() error { raw := table.toMap() body, err := json.MarshalIndent(raw, "", " ") if err != nil { return fmt.Errorf("failed to produce JSON output: %w", err) } fmt.Println(string(body)) return nil } func (table *Table) PrintTSV() error { // length's, convert cell types table.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) } if idx < len(table.Headers)-1 { fmt.Print(" ") } } fmt.Println() for _, entries := range table.rows { currentWidth := 0 for idx, entry := range entries { length := visibleLen(entry) if length+currentWidth > table.maxwidth && table.maxwidth-currentWidth > 1 { // text is too wide to be put into one line, wrap it wrapper := wordwrap.Wrapper(table.maxwidth-currentWidth, false) wrapped := wrapper(entry) // and indent it for idx, line := range strings.Split(wrapped, "\n") { if idx == 0 { entry = line } else { entry += "\n " + strings.Repeat(" ", currentWidth) + line } } } currentWidth += table.lenHeaders[idx] switch { case isInt(entry) && table.alignInts: // align right fmt.Print(strings.Repeat(" ", table.lenHeaders[idx]-length), entry) case length < table.lenHeaders[idx] && idx+1 != len(entries): // pad right, if required fmt.Print(entry, strings.Repeat(" ", table.lenHeaders[idx]-length)) default: // no padding for last entry fmt.Print(entry) } if idx < len(table.Headers)-1 { fmt.Print(" ") } } fmt.Println() } 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), " ", "-")) } } } func (table *Table) AddRow(fields ...any) { table.Entries = append(table.Entries, fields) } // needed for json and yaml output func (table *Table) toMap() []map[string]any { raw := make([]map[string]any, len(table.Entries)) for idx, entries := range table.Entries { raw[idx] = make(map[string]any, len(table.Headers)) for eidx, entry := range entries { raw[idx][table.Headers[eidx]] = entry } } return raw } // return the length of a string but only visible chars, w/o ansi color escapes func visibleLen(word string) int { return len(ansiCtrlSeq.ReplaceAllLiteralString(word, "")) } func isInt(num string) bool { if _, err := strconv.Atoi(num); err == nil { return true } return false }