table.go

v0.6.0
Doc Versions Source
1
package report
2
3
import (
4
	"io"
5
	"strings"
6
)
7
8
// Table is a passive aligned-column renderer for terminal output. Cells may
9
// carry ANSI color escapes (e.g. a colored status); column widths are measured
10
// on visible runes so a colored cell aligns exactly as its plain twin. It is
11
// pure data-in, text-out: no I/O beyond the io.Writer passed to Render, and no
12
// dependency on the legatus core, transport, or sync.
13
type Table struct {
14
	headers []string
15
	rows    [][]string
16
}
17
18
// NewTable returns a Table with the given column headers.
19
func NewTable(headers ...string) *Table {
20
	return &Table{headers: headers}
21
}
22
23
// AddRow appends a row and returns the table for chaining. A row need not match
24
// the header count: shorter rows render the missing cells empty, longer rows add
25
// unheadered columns — Render derives the column count from the widest input.
26
func (t *Table) AddRow(cells ...string) *Table {
27
	t.rows = append(t.rows, cells)
28
	return t
29
}
30
31
// Render writes the aligned table to w. Columns are left-aligned at the widest
32
// visible cell, separated by a two-space gutter, with no trailing whitespace and
33
// one newline per row. Output is deterministic for a given header+row set: the
34
// column order is construction order and no map iteration is involved. Write
35
// errors are ignored, matching TextReporter.
36
func (t *Table) Render(w io.Writer) {
37
	cols := len(t.headers)
38
	for _, r := range t.rows {
39
		if len(r) > cols {
40
			cols = len(r)
41
		}
42
	}
43
44
	widths := make([]int, cols)
45
	for c := 0; c < cols; c++ {
46
		widths[c] = visibleWidth(cell(t.headers, c))
47
		for _, r := range t.rows {
48
			if v := visibleWidth(cell(r, c)); v > widths[c] {
49
				widths[c] = v
50
			}
51
		}
52
	}
53
54
	writeRow(w, t.headers, widths)
55
	for _, r := range t.rows {
56
		writeRow(w, r, widths)
57
	}
58
}
59
60
// writeRow renders one row, padding each cell (except the trailing run of empty
61
// space) to its column width and joining with a two-space gutter.
62
func writeRow(w io.Writer, cells []string, widths []int) {
63
	var b strings.Builder
64
	for c := 0; c < len(widths); c++ {
65
		val := cell(cells, c)
66
		if c > 0 {
67
			b.WriteString("  ")
68
		}
69
		b.WriteString(val)
70
		// Pad to the column width on visible length, except the last column where
71
		// trailing padding would only add invisible trailing whitespace.
72
		if c < len(widths)-1 {
73
			b.WriteString(strings.Repeat(" ", widths[c]-visibleWidth(val)))
74
		}
75
	}
76
	out := strings.TrimRight(b.String(), " ")
77
	_, _ = io.WriteString(w, out+"\n")
78
}
79
80
// cell returns the c-th cell of row, or "" when the row is shorter.
81
func cell(row []string, c int) string {
82
	if c < len(row) {
83
		return row[c]
84
	}
85
	return ""
86
}
87
88
// visibleWidth counts the runes of s that are visible on a terminal, skipping
89
// ANSI CSI escape sequences (ESC '[' params final-byte). The final byte of a CSI
90
// sequence is in 0x40..0x7e; everything before it (parameters/intermediates) is
91
// skipped, so a colored cell measures the same width as its plain text.
92
func visibleWidth(s string) int {
93
	n, rs := 0, []rune(s)
94
	for i := 0; i < len(rs); i++ {
95
		if rs[i] == '\x1b' && i+1 < len(rs) && rs[i+1] == '[' {
96
			i += 2
97
			for i < len(rs) && (rs[i] < 0x40 || rs[i] > 0x7e) {
98
				i++ // skip parameter/intermediate bytes
99
			}
100
			continue // i is at the final byte; the loop's i++ steps past it
101
		}
102
		n++
103
	}
104
	return n
105
}
106

Source Files