Serge Bazanski | e0c0617 | 2023-09-19 12:28:16 +0000 | [diff] [blame^] | 1 | // 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). |
| 13 | package clitable |
| 14 | |
| 15 | import ( |
| 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. |
| 23 | type Table struct { |
| 24 | entries []Entry |
| 25 | } |
| 26 | |
| 27 | // Add an entry to the table. |
| 28 | func (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. |
| 33 | type Entry struct { |
| 34 | columns []entryColumn |
| 35 | } |
| 36 | |
| 37 | // Add a key/value pair to the entry. |
| 38 | func (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. |
| 46 | func (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. |
| 56 | type 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. |
| 63 | func (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 | |
| 76 | type 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). |
| 80 | type 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. |
| 88 | func (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). |
| 105 | func (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. |
| 118 | func (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). |
| 128 | func (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 | } |