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