1
2
3
4 package template
5
6 import (
7 "encoding/json"
8 "fmt"
9 "io"
10 "math"
11 "strconv"
12 "strings"
13 "text/template"
14 "time"
15
16 "github.com/cli/go-gh/v2/pkg/tableprinter"
17 "github.com/cli/go-gh/v2/pkg/text"
18 color "github.com/mgutz/ansi"
19 )
20
21 const (
22 ellipsis = "..."
23 )
24
25
26 type Template struct {
27 colorEnabled bool
28 output io.Writer
29 tmpl *template.Template
30 tp tableprinter.TablePrinter
31 width int
32 funcs template.FuncMap
33 }
34
35
36 func New(w io.Writer, width int, colorEnabled bool) *Template {
37 return &Template{
38 colorEnabled: colorEnabled,
39 output: w,
40 tp: tableprinter.New(w, true, width),
41 width: width,
42 funcs: template.FuncMap{},
43 }
44 }
45
46
47
48
49
50 func (t *Template) Funcs(funcMap map[string]interface{}) *Template {
51 for name, f := range funcMap {
52 t.funcs[name] = f
53 }
54 return t
55 }
56
57
58 func (t *Template) Parse(tmpl string) error {
59 now := time.Now()
60 templateFuncs := map[string]interface{}{
61 "autocolor": colorFunc,
62 "color": colorFunc,
63 "hyperlink": hyperlinkFunc,
64 "join": joinFunc,
65 "pluck": pluckFunc,
66 "tablerender": func() (string, error) {
67
68
69 defer func() {
70 t.tp = tableprinter.New(t.output, true, t.width)
71 }()
72 return tableRenderFunc(t.tp)
73 },
74 "tablerow": func(fields ...interface{}) (string, error) {
75 return tableRowFunc(t.tp, fields...)
76 },
77 "timeago": func(input string) (string, error) {
78 return timeAgoFunc(now, input)
79 },
80 "timefmt": timeFormatFunc,
81 "truncate": truncateFunc,
82 }
83 if !t.colorEnabled {
84 templateFuncs["autocolor"] = autoColorFunc
85 }
86 for name, f := range t.funcs {
87 templateFuncs[name] = f
88 }
89 var err error
90 t.tmpl, err = template.New("").Funcs(templateFuncs).Parse(tmpl)
91 return err
92 }
93
94
95
96 func (t *Template) Execute(input io.Reader) error {
97 jsonData, err := io.ReadAll(input)
98 if err != nil {
99 return err
100 }
101
102 var data interface{}
103 if err := json.Unmarshal(jsonData, &data); err != nil {
104 return err
105 }
106
107 return t.tmpl.Execute(t.output, data)
108 }
109
110
111
112
113
114 func (t *Template) Flush() error {
115 if _, err := tableRenderFunc(t.tp); err != nil {
116 return err
117 }
118 return nil
119 }
120
121 func colorFunc(colorName string, input interface{}) (string, error) {
122 text, err := jsonScalarToString(input)
123 if err != nil {
124 return "", err
125 }
126 return color.Color(text, colorName), nil
127 }
128
129 func pluckFunc(field string, input []interface{}) []interface{} {
130 var results []interface{}
131 for _, item := range input {
132 obj := item.(map[string]interface{})
133 results = append(results, obj[field])
134 }
135 return results
136 }
137
138 func joinFunc(sep string, input []interface{}) (string, error) {
139 var results []string
140 for _, item := range input {
141 text, err := jsonScalarToString(item)
142 if err != nil {
143 return "", err
144 }
145 results = append(results, text)
146 }
147 return strings.Join(results, sep), nil
148 }
149
150 func timeFormatFunc(format, input string) (string, error) {
151 t, err := time.Parse(time.RFC3339, input)
152 if err != nil {
153 return "", err
154 }
155 return t.Format(format), nil
156 }
157
158 func timeAgoFunc(now time.Time, input string) (string, error) {
159 t, err := time.Parse(time.RFC3339, input)
160 if err != nil {
161 return "", err
162 }
163 return timeAgo(now.Sub(t)), nil
164 }
165
166 func truncateFunc(maxWidth int, v interface{}) (string, error) {
167 if v == nil {
168 return "", nil
169 }
170 if s, ok := v.(string); ok {
171 return text.Truncate(maxWidth, s), nil
172 }
173 return "", fmt.Errorf("invalid value; expected string, got %T", v)
174 }
175
176 func autoColorFunc(colorName string, input interface{}) (string, error) {
177 return jsonScalarToString(input)
178 }
179
180 func tableRowFunc(tp tableprinter.TablePrinter, fields ...interface{}) (string, error) {
181 if tp == nil {
182 return "", fmt.Errorf("failed to write table row: no table printer")
183 }
184 for _, e := range fields {
185 s, err := jsonScalarToString(e)
186 if err != nil {
187 return "", fmt.Errorf("failed to write table row: %v", err)
188 }
189 tp.AddField(s, tableprinter.WithTruncate(truncateMultiline))
190 }
191 tp.EndRow()
192 return "", nil
193 }
194
195 func tableRenderFunc(tp tableprinter.TablePrinter) (string, error) {
196 if tp == nil {
197 return "", fmt.Errorf("failed to render table: no table printer")
198 }
199 err := tp.Render()
200 if err != nil {
201 return "", fmt.Errorf("failed to render table: %v", err)
202 }
203 return "", nil
204 }
205
206 func jsonScalarToString(input interface{}) (string, error) {
207 switch tt := input.(type) {
208 case string:
209 return tt, nil
210 case float64:
211 if math.Trunc(tt) == tt {
212 return strconv.FormatFloat(tt, 'f', 0, 64), nil
213 } else {
214 return strconv.FormatFloat(tt, 'f', 2, 64), nil
215 }
216 case nil:
217 return "", nil
218 case bool:
219 return fmt.Sprintf("%v", tt), nil
220 default:
221 return "", fmt.Errorf("cannot convert type to string: %v", tt)
222 }
223 }
224
225 func timeAgo(ago time.Duration) string {
226 if ago < time.Minute {
227 return "just now"
228 }
229 if ago < time.Hour {
230 return text.Pluralize(int(ago.Minutes()), "minute") + " ago"
231 }
232 if ago < 24*time.Hour {
233 return text.Pluralize(int(ago.Hours()), "hour") + " ago"
234 }
235 if ago < 30*24*time.Hour {
236 return text.Pluralize(int(ago.Hours())/24, "day") + " ago"
237 }
238 if ago < 365*24*time.Hour {
239 return text.Pluralize(int(ago.Hours())/24/30, "month") + " ago"
240 }
241 return text.Pluralize(int(ago.Hours()/24/365), "year") + " ago"
242 }
243
244
245
246
247 func truncateMultiline(maxWidth int, s string) string {
248 if i := strings.IndexAny(s, "\r\n"); i >= 0 {
249 s = s[:i] + ellipsis
250 }
251 return text.Truncate(maxWidth, s)
252 }
253
254 func hyperlinkFunc(link, text string) string {
255 if text == "" {
256 text = link
257 }
258
259
260 return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", link, text)
261 }
262
View as plain text