mirror of
https://github.com/neilotoole/sq.git
synced 2024-12-24 08:36:43 +03:00
7c56377b40
* Field alignment
590 lines
12 KiB
Go
590 lines
12 KiB
Go
// Copyright 2014 Oleku Konko All rights reserved.
|
|
// Use of this source code is governed by a MIT
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// This module is a Table writer API for the Go Programming Language.
|
|
// The protocols were written in pure Go and works on windows and unix systems
|
|
|
|
// Package tablewriter creates & generates text based table
|
|
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// MaxRowWidth defines maximum row width
|
|
const (
|
|
MaxRowWidth = 30
|
|
)
|
|
|
|
const (
|
|
// BorderCenterChar defines char at center of border
|
|
BorderCenterChar = "+"
|
|
// BorderRowChar defines char of row border
|
|
BorderRowChar = "-"
|
|
// BorderColumnChar defines char of column border
|
|
BorderColumnChar = "|"
|
|
// Empty defines no padding space or border
|
|
Empty = ""
|
|
// Space defines space char for padding and borders
|
|
Space = " "
|
|
)
|
|
|
|
// AlignDefault and other alignment constants
|
|
const (
|
|
AlignDefault = iota
|
|
AlignLeft
|
|
AlignCenter
|
|
AlignRight
|
|
)
|
|
|
|
var (
|
|
decimal = regexp.MustCompile(`^-*\d*\.?\d*$`)
|
|
percent = regexp.MustCompile(`^-*\d*\.?\d*$%$`)
|
|
)
|
|
|
|
// Border struct
|
|
type Border struct {
|
|
Left bool
|
|
Right bool
|
|
Top bool
|
|
Bottom bool
|
|
}
|
|
|
|
// Table struct
|
|
type Table struct {
|
|
out io.Writer
|
|
cs map[int]int
|
|
rs map[int]int
|
|
colTrans map[int]textTransFunc
|
|
cellTrans map[string]textTransFunc
|
|
headerTrans textTransFunc
|
|
pCenter string
|
|
pRow string
|
|
pColumn string
|
|
rows [][]string
|
|
lines [][][]string
|
|
headers []string
|
|
footers []string
|
|
mW int
|
|
tColumn int
|
|
tRow int
|
|
hAlign int
|
|
fAlign int
|
|
align int
|
|
colSize int
|
|
borders Border
|
|
autoFmt bool
|
|
autoWrap bool
|
|
rowLine bool
|
|
hdrLine bool
|
|
hdrDisable bool
|
|
}
|
|
|
|
// NewTable returns a new table that writes to writer.
|
|
func NewTable(writer io.Writer) *Table {
|
|
t := &Table{
|
|
out: writer,
|
|
rows: [][]string{},
|
|
lines: [][][]string{},
|
|
cs: make(map[int]int),
|
|
rs: make(map[int]int),
|
|
headers: []string{},
|
|
footers: []string{},
|
|
autoFmt: true,
|
|
autoWrap: true,
|
|
mW: MaxRowWidth,
|
|
pCenter: BorderCenterChar,
|
|
pRow: BorderRowChar,
|
|
pColumn: BorderColumnChar,
|
|
tColumn: -1,
|
|
tRow: -1,
|
|
hAlign: AlignDefault,
|
|
fAlign: AlignDefault,
|
|
align: AlignDefault,
|
|
rowLine: false,
|
|
hdrLine: false,
|
|
hdrDisable: false,
|
|
borders: Border{Left: true, Right: true, Bottom: true, Top: true},
|
|
colSize: -1,
|
|
colTrans: make(map[int]textTransFunc),
|
|
cellTrans: make(map[string]textTransFunc),
|
|
headerTrans: fmt.Sprint,
|
|
}
|
|
return t
|
|
}
|
|
|
|
// SetColTrans sets the column transformer.
|
|
func (t *Table) SetColTrans(col int, trans textTransFunc) {
|
|
t.colTrans[col] = trans
|
|
}
|
|
|
|
// SetCellTrans sets the cell transformer.
|
|
//
|
|
// REVISIT: does this even work? How does it interact with SetColTrans?
|
|
func (t *Table) SetCellTrans(row, col int, trans textTransFunc) {
|
|
t.cellTrans[fmt.Sprintf("[%v][%v]", row, col)] = trans
|
|
}
|
|
|
|
// SetHeaderTrans sets the transformer for the header row.
|
|
func (t *Table) SetHeaderTrans(trans textTransFunc) {
|
|
t.headerTrans = trans
|
|
}
|
|
|
|
func (t *Table) getCellTrans(row, col int) textTransFunc {
|
|
colTrans := t.getColTrans(col)
|
|
key := fmt.Sprintf("[%v][%v]", row, col)
|
|
cellTrans := t.cellTrans[key]
|
|
|
|
if cellTrans == nil {
|
|
cellTrans = func(val ...any) string {
|
|
return fmt.Sprint(val...)
|
|
}
|
|
}
|
|
|
|
return func(val ...any) string {
|
|
return cellTrans(colTrans(val...))
|
|
}
|
|
}
|
|
|
|
func (t *Table) getColTrans(col int) textTransFunc {
|
|
trans := t.colTrans[col]
|
|
if trans != nil {
|
|
return trans
|
|
}
|
|
|
|
return func(val ...any) string {
|
|
return fmt.Sprint(val...)
|
|
}
|
|
}
|
|
|
|
// RenderAll table output
|
|
func (t *Table) RenderAll(ctx context.Context) error {
|
|
if t.borders.Top {
|
|
t.printLine(true)
|
|
}
|
|
t.printHeading()
|
|
if err := t.printRows(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !t.rowLine && t.borders.Bottom {
|
|
t.printLine(true)
|
|
}
|
|
t.printFooter()
|
|
return nil
|
|
}
|
|
|
|
// SetHeader sets table header
|
|
func (t *Table) SetHeader(keys []string) {
|
|
t.colSize = len(keys)
|
|
for i, v := range keys {
|
|
t.parseDimension(v, i, -1)
|
|
t.headers = append(t.headers, v)
|
|
}
|
|
}
|
|
|
|
// SetFooter sets table Footer
|
|
func (t *Table) SetFooter(keys []string) {
|
|
// t.colSize = len(keys)
|
|
for i, v := range keys {
|
|
t.parseDimension(v, i, -1)
|
|
t.footers = append(t.footers, v)
|
|
}
|
|
}
|
|
|
|
// SetAutoFormatHeaders turns header autoformatting on/off. Default is on (true).
|
|
func (t *Table) SetAutoFormatHeaders(auto bool) {
|
|
t.autoFmt = auto
|
|
}
|
|
|
|
// SetAutoWrapText turns automatic multiline text adjustment on/off. Default is on (true).
|
|
func (t *Table) SetAutoWrapText(auto bool) {
|
|
t.autoWrap = auto
|
|
}
|
|
|
|
// SetColWidth sets the default column width
|
|
func (t *Table) SetColWidth(width int) {
|
|
t.mW = width
|
|
}
|
|
|
|
// SetColumnSeparator sets the Column Separator
|
|
func (t *Table) SetColumnSeparator(sep string) {
|
|
t.pColumn = sep
|
|
}
|
|
|
|
// SetRowSeparator sets the Row Separator
|
|
func (t *Table) SetRowSeparator(sep string) {
|
|
t.pRow = sep
|
|
}
|
|
|
|
// SetCenterSeparator sets the center Separator
|
|
func (t *Table) SetCenterSeparator(sep string) {
|
|
t.pCenter = sep
|
|
}
|
|
|
|
// SetHeaderAlignment sets Header Alignment
|
|
func (t *Table) SetHeaderAlignment(hAlign int) {
|
|
t.hAlign = hAlign
|
|
}
|
|
|
|
// SetFooterAlignment sets Footer Alignment
|
|
func (t *Table) SetFooterAlignment(fAlign int) {
|
|
t.fAlign = fAlign
|
|
}
|
|
|
|
// SetAlignment sets Table Alignment
|
|
func (t *Table) SetAlignment(align int) {
|
|
t.align = align
|
|
}
|
|
|
|
// SetHeaderLine enable / disable a line after the header
|
|
func (t *Table) SetHeaderLine(line bool) {
|
|
t.hdrLine = line
|
|
}
|
|
|
|
// SetDisableHeader enable / disable printing of headers
|
|
func (t *Table) SetHeaderDisable(disable bool) {
|
|
t.hdrDisable = disable
|
|
}
|
|
|
|
// SetRowLine enable / disable a line on each row of the table
|
|
func (t *Table) SetRowLine(line bool) {
|
|
t.rowLine = line
|
|
}
|
|
|
|
// SetBorder enable / disable line around the table
|
|
func (t *Table) SetBorder(border bool) {
|
|
t.SetBorders(Border{border, border, border, border})
|
|
}
|
|
|
|
// SetBorders sets borders
|
|
func (t *Table) SetBorders(border Border) {
|
|
t.borders = border
|
|
}
|
|
|
|
// Append row to table
|
|
func (t *Table) Append(row []string) {
|
|
rowSize := len(t.headers)
|
|
if rowSize > t.colSize {
|
|
t.colSize = rowSize
|
|
}
|
|
|
|
n := len(t.lines)
|
|
line := [][]string{}
|
|
for i, v := range row {
|
|
// Detect string width
|
|
// Detect String height
|
|
// Break strings into words
|
|
out := t.parseDimension(v, i, n)
|
|
|
|
// Append broken words
|
|
line = append(line, out)
|
|
}
|
|
t.lines = append(t.lines, line)
|
|
}
|
|
|
|
// AppendBulk allows support for Bulk Append
|
|
// Eliminates repeated for loops
|
|
func (t *Table) AppendBulk(rows [][]string) {
|
|
for _, row := range rows {
|
|
t.Append(row)
|
|
}
|
|
}
|
|
|
|
// Print line based on row width
|
|
func (t *Table) printLine(nl bool) {
|
|
fmt.Fprint(t.out, t.pCenter)
|
|
for i := 0; i < len(t.cs); i++ {
|
|
v := t.cs[i]
|
|
fmt.Fprintf(t.out, "%s%s%s%s",
|
|
t.pRow,
|
|
strings.Repeat(string(t.pRow), v),
|
|
t.pRow,
|
|
t.pCenter)
|
|
}
|
|
if nl {
|
|
fmt.Fprintln(t.out)
|
|
}
|
|
}
|
|
|
|
// Return the PadRight function if align is left, PadLeft if align is right,
|
|
// and Pad by default
|
|
func pad(align int) func(string, string, int) string {
|
|
padFunc := Pad
|
|
switch align {
|
|
case AlignLeft:
|
|
padFunc = PadRight
|
|
case AlignRight:
|
|
padFunc = PadLeft
|
|
}
|
|
return padFunc
|
|
}
|
|
|
|
// Print heading information
|
|
func (t *Table) printHeading() {
|
|
// Check if headers is available
|
|
if len(t.headers) < 1 || t.hdrDisable {
|
|
return
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
// Check if border is set
|
|
// Replace with space if not set
|
|
fmt.Fprint(buf, ConditionString(t.borders.Left, t.pColumn, Empty))
|
|
fmt.Fprint(t.out, strings.TrimRightFunc(buf.String(), unicode.IsSpace))
|
|
|
|
// Identify last column
|
|
end := len(t.cs) - 1
|
|
|
|
// Get pad function
|
|
padFunc := pad(t.hAlign)
|
|
|
|
// Print Heading column
|
|
for i := 0; i <= end; i++ {
|
|
v := t.cs[i]
|
|
h := t.headers[i]
|
|
if t.autoFmt {
|
|
h = Title(h)
|
|
}
|
|
pad := ConditionString(i == end && !t.borders.Left, Space, t.pColumn)
|
|
|
|
var head string
|
|
if i == end {
|
|
// Trim the padding from the final column
|
|
// head = strings.TrimRightFunc(head, unicode.IsPunct)
|
|
head = t.headerTrans(h)
|
|
} else {
|
|
head = t.headerTrans(fmt.Sprintf("%s %s ", padFunc(h, Space, v), pad))
|
|
}
|
|
fmt.Fprint(t.out, head)
|
|
|
|
}
|
|
// Next line
|
|
fmt.Fprintln(t.out)
|
|
if t.hdrLine {
|
|
t.printLine(true)
|
|
}
|
|
}
|
|
|
|
// Print heading information
|
|
func (t *Table) printFooter() {
|
|
// Check if headers is available
|
|
if len(t.footers) < 1 {
|
|
return
|
|
}
|
|
|
|
// Only print line if border is not set
|
|
if !t.borders.Bottom {
|
|
t.printLine(true)
|
|
}
|
|
// Check if border is set
|
|
// Replace with space if not set
|
|
fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, Space))
|
|
|
|
// Identify last column
|
|
end := len(t.cs) - 1
|
|
|
|
// Get pad function
|
|
padFunc := pad(t.fAlign)
|
|
|
|
// Print Heading column
|
|
for i := 0; i <= end; i++ {
|
|
v := t.cs[i]
|
|
f := t.footers[i]
|
|
if t.autoFmt {
|
|
f = Title(f)
|
|
}
|
|
pad := ConditionString(i == end && !t.borders.Top, Space, t.pColumn)
|
|
|
|
if len(t.footers[i]) == 0 {
|
|
pad = Space
|
|
}
|
|
fmt.Fprintf(t.out, " %s %s",
|
|
padFunc(f, Space, v),
|
|
pad)
|
|
}
|
|
// Next line
|
|
fmt.Fprintln(t.out)
|
|
|
|
hasPrinted := false
|
|
|
|
for i := 0; i <= end; i++ {
|
|
v := t.cs[i]
|
|
pad := t.pRow
|
|
center := t.pCenter
|
|
length := len(t.footers[i])
|
|
|
|
if length > 0 {
|
|
hasPrinted = true
|
|
}
|
|
|
|
// Set center to be space if length is 0
|
|
if length == 0 && !t.borders.Right {
|
|
center = Space
|
|
}
|
|
|
|
// Print first junction
|
|
if i == 0 {
|
|
fmt.Fprint(t.out, center)
|
|
}
|
|
|
|
// Pad With space of length is 0
|
|
if length == 0 {
|
|
pad = Space
|
|
}
|
|
// Ignore left space of it has printed before
|
|
if hasPrinted || t.borders.Left {
|
|
pad = t.pRow
|
|
center = t.pCenter
|
|
}
|
|
|
|
// Change Center start position
|
|
if center == Space {
|
|
if i < end && len(t.footers[i+1]) != 0 {
|
|
center = t.pCenter
|
|
}
|
|
}
|
|
|
|
// Print the footer
|
|
fmt.Fprintf(t.out, "%s%s%s%s",
|
|
pad,
|
|
strings.Repeat(string(pad), v),
|
|
pad,
|
|
center)
|
|
|
|
}
|
|
|
|
fmt.Fprintln(t.out)
|
|
}
|
|
|
|
func (t *Table) printRows(ctx context.Context) error {
|
|
for i, lines := range t.lines {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
t.printRow(lines, i)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Print Row Information
|
|
func (t *Table) printRow(columns [][]string, colKey int) {
|
|
// Get Maximum Height
|
|
max := t.rs[colKey]
|
|
total := len(columns)
|
|
|
|
// Pad Each Height
|
|
for i, line := range columns {
|
|
length := len(line)
|
|
pad := max - length
|
|
for n := 0; n < pad; n++ {
|
|
columns[i] = append(columns[i], " ")
|
|
}
|
|
}
|
|
|
|
for x := 0; x < max; x++ {
|
|
for y := 0; y < total; y++ {
|
|
// Check if border is set
|
|
fmt.Fprint(t.out, ConditionString(!t.borders.Left && y == 0, Empty, t.pColumn))
|
|
|
|
text := columns[y][x]
|
|
|
|
tran := t.getCellTrans(colKey, y)
|
|
// This would print alignment
|
|
// Default alignment would use multiple configuration
|
|
switch t.align {
|
|
case AlignCenter: //
|
|
fmt.Fprintf(t.out, "%s", Pad(text, Space, t.cs[y]))
|
|
case AlignRight:
|
|
fmt.Fprintf(t.out, "%s", PadLeft(text, Space, t.cs[y]))
|
|
case AlignLeft:
|
|
cellContent := text
|
|
if y != total-1 {
|
|
cellContent = PadRight(text, Space, t.cs[y])
|
|
fmt.Fprintf(t.out, tran("%s "), cellContent)
|
|
} else {
|
|
fmt.Fprintf(t.out, tran(strings.TrimRightFunc(cellContent, unicode.IsSpace)))
|
|
}
|
|
|
|
default:
|
|
if decimal.MatchString(strings.TrimSpace(text)) || percent.MatchString(strings.TrimSpace(text)) {
|
|
fmt.Fprintf(t.out, "%s", PadLeft(text, Space, t.cs[y]))
|
|
} else {
|
|
fmt.Fprintf(t.out, "%s", PadRight(text, Space, t.cs[y]))
|
|
}
|
|
}
|
|
}
|
|
// Check if border is set
|
|
// Replace with space if not set
|
|
// fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, Space))
|
|
fmt.Fprintln(t.out)
|
|
}
|
|
|
|
if t.rowLine {
|
|
t.printLine(true)
|
|
}
|
|
}
|
|
|
|
func (t *Table) parseDimension(str string, colKey, rowKey int) []string {
|
|
var (
|
|
raw []string
|
|
max int
|
|
)
|
|
w := DisplayWidth(str)
|
|
// Calculate Width
|
|
// Check if with is grater than maximum width
|
|
if w > t.mW {
|
|
w = t.mW
|
|
}
|
|
|
|
// Check if width exists
|
|
v, ok := t.cs[colKey]
|
|
if !ok || v < w || v == 0 {
|
|
t.cs[colKey] = w
|
|
}
|
|
|
|
if rowKey == -1 {
|
|
return raw
|
|
}
|
|
// Calculate Height
|
|
if t.autoWrap {
|
|
raw, _ = WrapString(str, t.cs[colKey])
|
|
} else {
|
|
raw = getLines(str)
|
|
}
|
|
|
|
for _, line := range raw {
|
|
if w := DisplayWidth(line); w > max {
|
|
max = w
|
|
}
|
|
}
|
|
|
|
// Make sure the with is the same length as maximum word
|
|
// Important for cases where the width is smaller than maxu word
|
|
if max > t.cs[colKey] {
|
|
t.cs[colKey] = max
|
|
}
|
|
|
|
h := len(raw)
|
|
v, ok = t.rs[rowKey]
|
|
|
|
if !ok || v < h || v == 0 {
|
|
t.rs[rowKey] = h
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// textTransFunc is a function that can transform text, typically
|
|
// to add color.
|
|
type textTransFunc func(a ...any) string
|