blob: ce1f7fa180dc46df1a28fa15836c50589c7da0ac [file] [log] [blame]
Serge Bazanskie0c06172023-09-19 12:28:16 +00001// Package clitable implements tabular display for command line tools.
2//
3// The generated tables are vaguely reminiscent of the output of 'kubectl'. For
4// example:
5//
6// NAME ADDRESS STATUS
7// foo 1.2.3.4 Healthy
8// bar 1.2.3.12 Timeout
9//
10// The tables are sparse by design, with each Entry having a number of Key/Value
11// entries. Then, all keys for all entries get unified into columns, and entries
12// without a given key are rendered spare (i.e. the rendered cell is empty).
13package clitable
14
15import (
16 "fmt"
17 "io"
18 "strings"
19)
20
21// Table is a list of entries that form a table with sparse columns. Each entry
22// defines its own columns.
23type Table struct {
24 entries []Entry
25}
26
27// Add an entry to the table.
28func (t *Table) Add(e Entry) {
29 t.entries = append(t.entries, e)
30}
31
32// An Entry is made up of column key -> value pairs.
33type Entry struct {
34 columns []entryColumn
35}
36
37// Add a key/value pair to the entry.
38func (e *Entry) Add(key, value string) {
39 e.columns = append(e.columns, entryColumn{
40 key: key,
41 value: value,
42 })
43}
44
45// Get a value from a given key, returning zero string if not set.
46func (e *Entry) Get(key string) string {
47 for _, col := range e.columns {
48 if col.key == key {
49 return col.value
50 }
51 }
52 return ""
53}
54
55// An entryColumn is a pair for table column key and entry column value.
56type entryColumn struct {
57 key string
58 value string
59}
60
61// Columns returns the keys and widths of columns that are present in the table's
62// entries.
63func (t *Table) Columns() Columns {
64 var res Columns
65 for _, e := range t.entries {
66 for _, c := range e.columns {
67 tc := res.upsert(c.key)
68 if len(c.value) > tc.width {
69 tc.width = len(c.value)
70 }
71 }
72 }
73 return res
74}
75
76type Columns []*Column
77
78// A Column in a table, not containing all entries that make up this table, but
79// containing their maximum width (for layout purposes).
80type Column struct {
81 // key is the column key.
82 key string
83 // width is the maximum width (in runes) of all the entries' data in this column.
84 width int
85}
86
87// upsert a key into a list of columns, returning the upserted column.
88func (c *Columns) upsert(key string) *Column {
89 for _, col := range *c {
90 if col.key == key {
91 return col
92 }
93 }
94 col := &Column{
95 key: key,
96 width: len(key),
97 }
98 *c = append(*c, col)
99 return col
100}
101
102// filter returns a copy of columns where the only columns present are the ones
103// whose onlyColumns values are true. If only columns is nil, no filtering takes
104// place (all columns are returned).
105func (c Columns) filter(onlyColumns map[string]bool) Columns {
106 var res []*Column
107 for _, cc := range c {
108 if onlyColumns != nil && !onlyColumns[cc.key] {
109 continue
110 }
111 res = append(res, cc)
112 }
113 return res
114}
115
116// PrintHeader writes a table-like header to the given file, keeping margin
117// spaces between columns.
118func (c Columns) printHeader(f io.Writer, margin int) {
119 for _, cc := range c {
120 fmt.Fprintf(f, "%-*s", cc.width+margin, strings.ToUpper(cc.key))
121 }
122 fmt.Fprintf(f, "\n")
123}
124
125// Print writes a table-like representation of this table to the given file,
126// first filtering the columns by onlyColumns (if not set, no filtering takes
127// place).
128func (t *Table) Print(f io.Writer, onlyColumns map[string]bool) {
129 margin := 3
130 cols := t.Columns().filter(onlyColumns)
131 cols.printHeader(f, margin)
132
133 for _, e := range t.entries {
134 for _, c := range cols {
135 v := e.Get(c.key)
136 fmt.Fprintf(f, "%-*s", c.width+margin, v)
137 }
138 fmt.Fprintf(f, "\n")
139 }
140}