mirror of https://gitlab.com/gitlab-org/cli.git
331 lines
7.7 KiB
Go
331 lines
7.7 KiB
Go
package tableprinter
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/spf13/cast"
|
|
"gitlab.com/gitlab-org/cli/pkg/text"
|
|
)
|
|
|
|
var tp *TablePrinter
|
|
|
|
func init() {
|
|
tp = &TablePrinter{
|
|
TotalRows: 0,
|
|
Wrap: false,
|
|
MaxColWidth: 0,
|
|
TTYSeparator: "\t",
|
|
NonTTYSeparator: "\t",
|
|
TerminalWidth: 80,
|
|
}
|
|
}
|
|
|
|
// TablePrinter represents a decorator that renders the data formatted in a tabular form.
|
|
type TablePrinter struct {
|
|
// Total number of records. Needed if AddRowFunc is used
|
|
TotalRows int
|
|
// Wrap when set to true wraps the contents of the columns when the length exceeds the MaxColWidth
|
|
Wrap bool
|
|
// MaxColWidth is the maximum allowed width for cells in the table
|
|
MaxColWidth int
|
|
// TTYSeparator is the separator for columns in the table on TTYs. Default is "\t"
|
|
TTYSeparator string
|
|
// NonTTYSeparator is the separator for columns in the table on non-TTYs. Default is "\t"
|
|
NonTTYSeparator string
|
|
// Rows is the collection of rows in the table
|
|
Rows []*TableRow
|
|
// TerminalWidth is the max width of the terminal
|
|
TerminalWidth int
|
|
// IsTTY indicates whether output is a TTY or non-TTY
|
|
IsTTY bool
|
|
}
|
|
|
|
type TableCell struct {
|
|
// Value in the cell
|
|
Value interface{}
|
|
// Width is the width of the cell
|
|
Width int
|
|
// Wrap when true wraps the contents of the cell when the length exceeds the width
|
|
Wrap bool
|
|
|
|
isaTTY bool
|
|
}
|
|
|
|
type TableRow struct {
|
|
Cells []*TableCell
|
|
// Separator is the separator for columns in the table. Default is " "
|
|
Separator string
|
|
}
|
|
|
|
func NewTablePrinter() *TablePrinter {
|
|
t := &TablePrinter{
|
|
TTYSeparator: tp.TTYSeparator,
|
|
NonTTYSeparator: tp.NonTTYSeparator,
|
|
MaxColWidth: tp.MaxColWidth,
|
|
Wrap: false,
|
|
TerminalWidth: tp.TerminalWidth,
|
|
IsTTY: tp.IsTTY,
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
func (t *TablePrinter) Separator() string {
|
|
if t.IsTTY {
|
|
return t.TTYSeparator
|
|
}
|
|
|
|
return t.NonTTYSeparator
|
|
}
|
|
|
|
// SetTerminalWidth sets the maximum width for the terminal
|
|
func SetTerminalWidth(width int) { tp.SetTerminalWidth(width) }
|
|
|
|
func (t *TablePrinter) SetTerminalWidth(width int) {
|
|
t.TerminalWidth = width
|
|
}
|
|
|
|
// SetIsTTY sets the IsTTY variable which indicates whether terminal
|
|
// output is a TTY or nonTTY
|
|
func SetIsTTY(isTTY bool) { tp.SetIsTTY(isTTY) }
|
|
|
|
func (t *TablePrinter) SetIsTTY(isTTY bool) {
|
|
t.IsTTY = isTTY
|
|
}
|
|
|
|
// SetTTYSeparator sets the separator for the columns in the table for TTYs
|
|
func SetTTYSeparator(s string) { tp.SetTTYSeparator(s) }
|
|
|
|
func (t *TablePrinter) SetTTYSeparator(s string) {
|
|
t.TTYSeparator = s
|
|
}
|
|
|
|
// SetNonTTYSeparator sets the separator for the columns in the table for non-ttys
|
|
func SetNonTTYSeparator(s string) { tp.SetNonTTYSeparator(s) }
|
|
|
|
func (t *TablePrinter) SetNonTTYSeparator(s string) {
|
|
t.NonTTYSeparator = s
|
|
}
|
|
|
|
func (t *TablePrinter) makeRow() {
|
|
if t.Rows == nil {
|
|
t.Rows = make([]*TableRow, 1)
|
|
t.Rows[0] = &TableRow{}
|
|
}
|
|
}
|
|
|
|
func (t *TablePrinter) AddCell(s interface{}) {
|
|
t.makeRow()
|
|
rowI := len(t.Rows) - 1
|
|
row := t.Rows[rowI]
|
|
|
|
cell := &TableCell{
|
|
Value: s,
|
|
isaTTY: t.IsTTY,
|
|
}
|
|
|
|
row.Separator = t.Separator()
|
|
|
|
row.Cells = append(row.Cells, cell)
|
|
}
|
|
|
|
// AddCellf formats according to a format specifier and adds cell to row
|
|
func (t *TablePrinter) AddCellf(s string, f ...interface{}) {
|
|
t.AddCell(fmt.Sprintf(s, f...))
|
|
}
|
|
|
|
func (t *TablePrinter) AddRow(str ...interface{}) {
|
|
for _, s := range str {
|
|
t.AddCell(s)
|
|
}
|
|
t.EndRow()
|
|
}
|
|
|
|
func (t *TablePrinter) AddRowFunc(f func(int, int) string) {
|
|
for ri := 0; ri < t.TotalRows; ri++ {
|
|
row := make([]interface{}, t.TotalRows)
|
|
for ci := range row {
|
|
row[ci] = f(ri, ci)
|
|
}
|
|
t.AddRow(row)
|
|
t.EndRow()
|
|
}
|
|
}
|
|
|
|
func (t *TablePrinter) EndRow() {
|
|
t.Rows = append(t.Rows, &TableRow{Cells: make([]*TableCell, 1)})
|
|
}
|
|
|
|
// Bytes returns the []byte value of table
|
|
func (t *TablePrinter) Bytes() []byte {
|
|
return []byte(t.String())
|
|
}
|
|
|
|
// String returns the string value of table. Alternative to Render()
|
|
func (t *TablePrinter) String() string {
|
|
return t.Render()
|
|
}
|
|
|
|
// String returns the string representation of the row
|
|
func (r *TableRow) String() string {
|
|
// get the max number of lines for each cell
|
|
var lc int // line count
|
|
for _, cell := range r.Cells {
|
|
if clc := len(strings.Split(cell.String(), "\n")); clc > lc {
|
|
lc = clc
|
|
}
|
|
}
|
|
|
|
// allocate a two-dimensional array of cells for each line and add size them
|
|
cells := make([][]*TableCell, lc)
|
|
for x := 0; x < lc; x++ {
|
|
cells[x] = make([]*TableCell, len(r.Cells))
|
|
for y := 0; y < len(r.Cells); y++ {
|
|
cells[x][y] = &TableCell{Width: r.Cells[y].Width}
|
|
}
|
|
}
|
|
|
|
// insert each line in a cell as new cell in the cells array
|
|
for y, cell := range r.Cells {
|
|
lines := strings.Split(cell.String(), "\n")
|
|
for x, line := range lines {
|
|
cells[x][y].Value = line
|
|
}
|
|
}
|
|
|
|
// format each line
|
|
lines := make([]string, lc)
|
|
for x := range lines {
|
|
line := make([]string, len(cells[x]))
|
|
for y := range cells[x] {
|
|
line[y] = cells[x][y].String()
|
|
}
|
|
lines[x] = text.Join(line, r.Separator)
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
// purgeRow removes nil cells and rows
|
|
func (t *TablePrinter) purgeRow() {
|
|
newSlice := make([]*TableRow, 0, len(t.Rows))
|
|
for _, row := range t.Rows {
|
|
var newRow *TableRow
|
|
if len(row.Cells) > 0 && row.Cells != nil {
|
|
var newCells []*TableCell
|
|
for _, cell := range row.Cells {
|
|
if cell != nil {
|
|
newCells = append(newCells, cell)
|
|
}
|
|
}
|
|
newRow = &TableRow{Cells: newCells}
|
|
}
|
|
|
|
if newRow != nil {
|
|
newSlice = append(newSlice, newRow)
|
|
}
|
|
}
|
|
t.Rows = newSlice
|
|
}
|
|
|
|
// Render builds and returns the string representation of the table
|
|
func (t *TablePrinter) Render() string {
|
|
if len(t.Rows) == 0 {
|
|
return ""
|
|
}
|
|
// remove nil cells and rows
|
|
t.purgeRow()
|
|
|
|
colWidths := t.colWidths()
|
|
|
|
var lines []string
|
|
for _, row := range t.Rows {
|
|
row.Separator = t.Separator()
|
|
for i, cell := range row.Cells {
|
|
cell.Width = colWidths[i]
|
|
cell.Wrap = t.Wrap
|
|
}
|
|
lines = append(lines, row.String())
|
|
}
|
|
return text.Join(lines, "\n")
|
|
}
|
|
|
|
// LineWidth returns the max width of all the lines in a cell
|
|
func (c *TableCell) LineWidth() int {
|
|
width := 0
|
|
for _, s := range strings.Split(c.String(), "\n") {
|
|
w := text.StringWidth(s)
|
|
if w > width {
|
|
width = w
|
|
}
|
|
}
|
|
return width
|
|
}
|
|
|
|
// String returns the string formatted representation of the cell
|
|
func (c *TableCell) String() string {
|
|
if c == nil {
|
|
return ""
|
|
}
|
|
if c.Value == nil {
|
|
return text.PadLeft(" ", c.Width, ' ')
|
|
}
|
|
|
|
s := cast.ToString(c.Value)
|
|
if c.Width > 0 && c.isaTTY {
|
|
if c.Wrap && len(s) > c.Width {
|
|
return text.WrapString(s, c.Width)
|
|
} else {
|
|
return text.Truncate(s, c.Width)
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// colWidths determine the width for each column (cell in a row)
|
|
func (t *TablePrinter) colWidths() []int {
|
|
var colWidths []int
|
|
for _, row := range t.Rows {
|
|
for i, cell := range row.Cells {
|
|
// resize colwidth array
|
|
if i+1 > len(colWidths) {
|
|
colWidths = append(colWidths, 0)
|
|
}
|
|
cellwidth := cell.LineWidth()
|
|
if t.MaxColWidth != 0 && cellwidth > t.MaxColWidth {
|
|
cellwidth = t.MaxColWidth
|
|
}
|
|
|
|
if cellwidth > colWidths[i] {
|
|
colWidths[i] = cellwidth
|
|
}
|
|
}
|
|
}
|
|
numCols := len(colWidths)
|
|
separatorWidth := (numCols - 1) * len(t.Separator())
|
|
totalWidth := separatorWidth
|
|
for _, width := range colWidths {
|
|
totalWidth += width
|
|
}
|
|
|
|
if t.MaxColWidth == 0 && totalWidth > t.TerminalWidth {
|
|
availWidth := t.TerminalWidth - colWidths[0] - separatorWidth
|
|
// add extra space from columns that are already narrower than threshold
|
|
for col := 1; col < numCols; col++ {
|
|
availColWidth := availWidth / (numCols - 1)
|
|
if extra := availColWidth - colWidths[col]; extra > 0 {
|
|
availWidth += extra
|
|
}
|
|
}
|
|
// cap all but first column to fit available terminal width
|
|
for col := 1; col < numCols; col++ {
|
|
availColWidth := availWidth / (numCols - 1)
|
|
if colWidths[col] > availColWidth {
|
|
colWidths[col] = availColWidth
|
|
}
|
|
}
|
|
}
|
|
|
|
return colWidths
|
|
}
|