...
1
2
3
4
5 package tableprinter
6
7 import (
8 "fmt"
9 "io"
10
11 "github.com/cli/go-gh/v2/pkg/text"
12 )
13
14 type fieldOption func(*tableField)
15
16 type TablePrinter interface {
17 AddHeader([]string, ...fieldOption)
18 AddField(string, ...fieldOption)
19 EndRow()
20 Render() error
21 }
22
23
24
25
26
27 func WithTruncate(fn func(int, string) string) fieldOption {
28 return func(f *tableField) {
29 f.truncateFunc = fn
30 }
31 }
32
33
34
35
36
37 func WithPadding(fn func(int, string) string) fieldOption {
38 return func(f *tableField) {
39 f.paddingFunc = fn
40 }
41 }
42
43
44
45
46 func WithColor(fn func(string) string) fieldOption {
47 return func(f *tableField) {
48 f.colorFunc = fn
49 }
50 }
51
52
53
54
55 func New(w io.Writer, isTTY bool, maxWidth int) TablePrinter {
56 if isTTY {
57 return &ttyTablePrinter{
58 out: w,
59 maxWidth: maxWidth,
60 }
61 }
62
63 return &tsvTablePrinter{
64 out: w,
65 }
66 }
67
68 type tableField struct {
69 text string
70 truncateFunc func(int, string) string
71 paddingFunc func(int, string) string
72 colorFunc func(string) string
73 }
74
75 type ttyTablePrinter struct {
76 out io.Writer
77 maxWidth int
78 hasHeaders bool
79 rows [][]tableField
80 }
81
82 func (t *ttyTablePrinter) AddHeader(columns []string, opts ...fieldOption) {
83 if t.hasHeaders {
84 return
85 }
86
87 t.hasHeaders = true
88 for _, column := range columns {
89 t.AddField(column, opts...)
90 }
91 t.EndRow()
92 }
93
94 func (t *ttyTablePrinter) AddField(s string, opts ...fieldOption) {
95 if t.rows == nil {
96 t.rows = make([][]tableField, 1)
97 }
98 rowI := len(t.rows) - 1
99 field := tableField{
100 text: s,
101 truncateFunc: text.Truncate,
102 }
103 for _, opt := range opts {
104 opt(&field)
105 }
106 t.rows[rowI] = append(t.rows[rowI], field)
107 }
108
109 func (t *ttyTablePrinter) EndRow() {
110 t.rows = append(t.rows, []tableField{})
111 }
112
113 func (t *ttyTablePrinter) Render() error {
114 if len(t.rows) == 0 {
115 return nil
116 }
117
118 delim := " "
119 numCols := len(t.rows[0])
120 colWidths := t.calculateColumnWidths(len(delim))
121
122 for _, row := range t.rows {
123 for col, field := range row {
124 if col > 0 {
125 _, err := fmt.Fprint(t.out, delim)
126 if err != nil {
127 return err
128 }
129 }
130 truncVal := field.text
131 if field.truncateFunc != nil {
132 truncVal = field.truncateFunc(colWidths[col], field.text)
133 }
134 if field.paddingFunc != nil {
135 truncVal = field.paddingFunc(colWidths[col], truncVal)
136 } else if col < numCols-1 {
137 truncVal = text.PadRight(colWidths[col], truncVal)
138 }
139 if field.colorFunc != nil {
140 truncVal = field.colorFunc(truncVal)
141 }
142 _, err := fmt.Fprint(t.out, truncVal)
143 if err != nil {
144 return err
145 }
146 }
147 if len(row) > 0 {
148 _, err := fmt.Fprint(t.out, "\n")
149 if err != nil {
150 return err
151 }
152 }
153 }
154 return nil
155 }
156
157 func (t *ttyTablePrinter) calculateColumnWidths(delimSize int) []int {
158 numCols := len(t.rows[0])
159 maxColWidths := make([]int, numCols)
160 colWidths := make([]int, numCols)
161
162 for _, row := range t.rows {
163 for col, field := range row {
164 w := text.DisplayWidth(field.text)
165 if w > maxColWidths[col] {
166 maxColWidths[col] = w
167 }
168
169 if field.truncateFunc == nil && w > colWidths[col] {
170 colWidths[col] = w
171 }
172 }
173 }
174
175 availWidth := func() int {
176 setWidths := 0
177 for col := 0; col < numCols; col++ {
178 setWidths += colWidths[col]
179 }
180 return t.maxWidth - delimSize*(numCols-1) - setWidths
181 }
182 numFixedCols := func() int {
183 fixedCols := 0
184 for col := 0; col < numCols; col++ {
185 if colWidths[col] > 0 {
186 fixedCols++
187 }
188 }
189 return fixedCols
190 }
191
192
193 if w := availWidth(); w > 0 {
194 if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 {
195 perColumn := w / numFlexColumns
196 for col := 0; col < numCols; col++ {
197 if max := maxColWidths[col]; max < perColumn {
198 colWidths[col] = max
199 }
200 }
201 }
202 }
203
204
205 if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 {
206 perColumn := availWidth() / numFlexColumns
207 for col := 0; col < numCols; col++ {
208 if colWidths[col] == 0 {
209 if max := maxColWidths[col]; max < perColumn {
210 colWidths[col] = max
211 } else if perColumn > 0 {
212 colWidths[col] = perColumn
213 }
214 }
215 }
216 }
217
218
219 if w := availWidth(); w > 0 {
220 for col := 0; col < numCols; col++ {
221 d := maxColWidths[col] - colWidths[col]
222 toAdd := w
223 if d < toAdd {
224 toAdd = d
225 }
226 colWidths[col] += toAdd
227 w -= toAdd
228 if w <= 0 {
229 break
230 }
231 }
232 }
233
234 return colWidths
235 }
236
237 type tsvTablePrinter struct {
238 out io.Writer
239 currentCol int
240 }
241
242 func (t *tsvTablePrinter) AddHeader(_ []string, _ ...fieldOption) {}
243
244 func (t *tsvTablePrinter) AddField(text string, _ ...fieldOption) {
245 if t.currentCol > 0 {
246 fmt.Fprint(t.out, "\t")
247 }
248 fmt.Fprint(t.out, text)
249 t.currentCol++
250 }
251
252 func (t *tsvTablePrinter) EndRow() {
253 fmt.Fprint(t.out, "\n")
254 t.currentCol = 0
255 }
256
257 func (t *tsvTablePrinter) Render() error {
258 return nil
259 }
260
View as plain text