Skip to content

Commit 173335f

Browse files
committed
output: format timestamps and durations in --human
1 parent b5968a7 commit 173335f

2 files changed

Lines changed: 84 additions & 4 deletions

File tree

internal/output/human_formatter.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"sort"
99
"strconv"
1010
"strings"
11+
"time"
1112

1213
"github.com/charmbracelet/lipgloss"
1314
"github.com/heygen-com/heygen-cli/internal/command"
@@ -98,7 +99,7 @@ func (f *HumanFormatter) renderTable(rows []map[string]any, columns []command.Co
9899

99100
for _, row := range rows {
100101
for i, col := range columns {
101-
cell := formatValue(fieldValue(row, col.Field))
102+
cell := formatCell(fieldValue(row, col.Field), col.Field)
102103
if w := lipgloss.Width(cell); w > widths[i] {
103104
widths[i] = w
104105
}
@@ -111,7 +112,7 @@ func (f *HumanFormatter) renderTable(rows []map[string]any, columns []command.Co
111112
for _, row := range rows {
112113
values := make([]string, len(columns))
113114
for i, col := range columns {
114-
values[i] = formatValue(fieldValue(row, col.Field))
115+
values[i] = formatCell(fieldValue(row, col.Field), col.Field)
115116
}
116117
if _, err := fmt.Fprintln(f.out, renderTableLine(values, widths, true)); err != nil {
117118
return err
@@ -159,7 +160,7 @@ func (f *HumanFormatter) renderKeyValue(obj map[string]any) error {
159160
for _, key := range keys {
160161
label := humanizeKey(key)
161162
padding := strings.Repeat(" ", max(0, maxLabelWidth-lipgloss.Width(label)))
162-
if _, err := fmt.Fprintf(f.out, "%s:%s %s\n", label, padding, formatValue(obj[key])); err != nil {
163+
if _, err := fmt.Fprintf(f.out, "%s:%s %s\n", label, padding, formatCell(obj[key], key)); err != nil {
163164
return err
164165
}
165166
}
@@ -232,6 +233,18 @@ func fieldValue(row map[string]any, path string) any {
232233
return current
233234
}
234235

236+
func formatCell(v any, fieldName string) string {
237+
if value, ok := v.(float64); ok {
238+
if isUnixTimestamp(value) {
239+
return time.Unix(int64(value), 0).UTC().Format("2006-01-02 15:04")
240+
}
241+
if strings.Contains(strings.ToLower(fieldName), "duration") {
242+
return formatDuration(value)
243+
}
244+
}
245+
return formatValue(v)
246+
}
247+
235248
func formatValue(v any) string {
236249
switch value := v.(type) {
237250
case nil:
@@ -261,6 +274,24 @@ func formatValue(v any) string {
261274
}
262275
}
263276

277+
func isUnixTimestamp(value float64) bool {
278+
if value != float64(int64(value)) {
279+
return false
280+
}
281+
return value > 1.5e9 && value < 2.2e9
282+
}
283+
284+
func formatDuration(seconds float64) string {
285+
d := time.Duration(seconds * float64(time.Second))
286+
if d < time.Minute {
287+
return fmt.Sprintf("%ds", int(d.Seconds()))
288+
}
289+
if d < time.Hour {
290+
return fmt.Sprintf("%dm %ds", int(d.Minutes()), int(d.Seconds())%60)
291+
}
292+
return fmt.Sprintf("%dh %dm %ds", int(d.Hours()), int(d.Minutes())%60, int(d.Seconds())%60)
293+
}
294+
264295
func humanizeKey(key string) string {
265296
parts := strings.FieldsFunc(key, func(r rune) bool {
266297
return r == '_' || r == '-' || r == '.'
@@ -273,4 +304,3 @@ func humanizeKey(key string) string {
273304
}
274305
return strings.Join(parts, " ")
275306
}
276-

internal/output/human_formatter_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,56 @@ func TestHumanFormatter_Data_KeyValue(t *testing.T) {
7979
}
8080
}
8181

82+
func TestHumanFormatter_Data_TimestampFormatting(t *testing.T) {
83+
var out bytes.Buffer
84+
f := NewHumanFormatter(&out, &bytes.Buffer{})
85+
86+
input := json.RawMessage(`{"data":[{"id":"vid_123","created_at":1774712936}]}`)
87+
columns := []command.Column{
88+
{Header: "ID", Field: "id"},
89+
{Header: "Created", Field: "created_at"},
90+
}
91+
92+
if err := f.Data(input, "data", columns); err != nil {
93+
t.Fatalf("unexpected error: %v", err)
94+
}
95+
96+
got := stripANSI(out.String())
97+
if !strings.Contains(got, "2026-03-28 15:48") {
98+
t.Fatalf("timestamp was not formatted as UTC date/time:\n%s", got)
99+
}
100+
}
101+
102+
func TestHumanFormatter_Data_DurationFormatting(t *testing.T) {
103+
var out bytes.Buffer
104+
f := NewHumanFormatter(&out, &bytes.Buffer{})
105+
106+
input := json.RawMessage(`{"data":{"duration":18.9649,"status":"completed"}}`)
107+
if err := f.Data(input, "data", nil); err != nil {
108+
t.Fatalf("unexpected error: %v", err)
109+
}
110+
111+
got := stripANSI(out.String())
112+
if !strings.Contains(got, "Duration: 18s") {
113+
t.Fatalf("duration was not formatted as seconds:\n%s", got)
114+
}
115+
}
116+
117+
func TestHumanFormatter_Data_RegularFloatUnaffected(t *testing.T) {
118+
var out bytes.Buffer
119+
f := NewHumanFormatter(&out, &bytes.Buffer{})
120+
121+
input := json.RawMessage(`{"data":{"score":3.14}}`)
122+
if err := f.Data(input, "data", nil); err != nil {
123+
t.Fatalf("unexpected error: %v", err)
124+
}
125+
126+
got := stripANSI(out.String())
127+
if !strings.Contains(got, "Score: 3.14") {
128+
t.Fatalf("regular float should remain unchanged:\n%s", got)
129+
}
130+
}
131+
82132
func TestHumanFormatter_Data_PrimitiveArray(t *testing.T) {
83133
var out bytes.Buffer
84134
f := NewHumanFormatter(&out, &bytes.Buffer{})

0 commit comments

Comments
 (0)