From 6c3fa04c7278929646eeec5f4c4d72941eb0e21f Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Fri, 31 Jul 2026 02:34:20 -0400 Subject: [PATCH] cli: display times in localtime --- internal/cli/issue_view.go | 2 +- internal/cli/pipeline_view.go | 6 +++--- internal/cli/pr_view.go | 2 +- internal/cli/repo_view.go | 2 +- internal/cli/rows.go | 19 +++++++++++++++---- internal/cli/rows_test.go | 19 +++++++++++++++++++ internal/cli/string_view.go | 2 +- 7 files changed, 41 insertions(+), 11 deletions(-) diff --git a/internal/cli/issue_view.go b/internal/cli/issue_view.go index ee21475..2b11de6 100644 --- a/internal/cli/issue_view.go +++ b/internal/cli/issue_view.go @@ -31,7 +31,7 @@ directory's git origin remote.`, {"Title", view.Title}, {"Status", formatDetailState(cmd.OutOrStdout(), view.State)}, {"Author", view.Author.Handle}, - {"Created", view.CreatedAt}, + {"Created", localTimestamp(view.CreatedAt)}, } renderDetail(cmd.OutOrStdout(), fields, view.Body) }) diff --git a/internal/cli/pipeline_view.go b/internal/cli/pipeline_view.go index 2943b27..de11c36 100644 --- a/internal/cli/pipeline_view.go +++ b/internal/cli/pipeline_view.go @@ -42,7 +42,7 @@ func renderPipelineDetail(writer io.Writer, pipeline *app.Pipeline) { {"ID", pipeline.ID}, {"Commit", pipeline.Commit}, {"Trigger", pipelineTrigger(pipeline.Trigger)}, - {"Created", pipeline.CreatedAt}, + {"Created", localTimestamp(pipeline.CreatedAt)}, } if pipeline.SourceRepo != "" { fields = append(fields, detailField{"Source Repo", pipeline.SourceRepo}) @@ -58,8 +58,8 @@ func renderPipelineWorkflows(writer io.Writer, workflows []app.PipelineWorkflow) rows = append(rows, []string{ workflow.Name, workflowStatusLabel(workflow.Status), - workflow.StartedAt, - workflow.FinishedAt, + localTimestamp(workflow.StartedAt), + localTimestamp(workflow.FinishedAt), workflow.Error, }) } diff --git a/internal/cli/pr_view.go b/internal/cli/pr_view.go index 2e82287..8676eb1 100644 --- a/internal/cli/pr_view.go +++ b/internal/cli/pr_view.go @@ -31,7 +31,7 @@ directory's git origin remote.`, {"Title", view.Title}, {"Status", formatDetailState(cmd.OutOrStdout(), view.State)}, {"Author", view.Author.Handle}, - {"Created", view.CreatedAt}, + {"Created", localTimestamp(view.CreatedAt)}, {"Branch", view.SourceBranch + " → " + view.TargetBranch}, } renderDetail(cmd.OutOrStdout(), fields, view.Body) diff --git a/internal/cli/repo_view.go b/internal/cli/repo_view.go index 4b64ab1..bcd2239 100644 --- a/internal/cli/repo_view.go +++ b/internal/cli/repo_view.go @@ -34,7 +34,7 @@ func newRepoViewCommand(service *app.Service) *cobra.Command { {"Description", item.Description}, {"URI", item.URI}, {"Knot", item.Knot}, - {"Created", item.CreatedAt}, + {"Created", localTimestamp(item.CreatedAt)}, } if item.RepoDid != "" { fields = append(fields, detailField{"Repo DID", item.RepoDid}) diff --git a/internal/cli/rows.go b/internal/cli/rows.go index 971780d..c0b15f3 100644 --- a/internal/cli/rows.go +++ b/internal/cli/rows.go @@ -7,6 +7,7 @@ import ( "os" "strings" "text/tabwriter" + "time" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/table" @@ -14,12 +15,22 @@ import ( xterm "github.com/charmbracelet/x/term" ) -// shortDate trims an ISO 8601 timestamp to its YYYY-MM-DD prefix. +// shortDate renders an ISO 8601 timestamp as the date in the user's timezone. func shortDate(timestamp string) string { - if len(timestamp) > 10 { - return timestamp[:10] + parsed, err := time.Parse(time.RFC3339, timestamp) + if err != nil { + return timestamp } - return timestamp + return parsed.Local().Format("2006-01-02") +} + +// localTimestamp renders an ISO 8601 timestamp in the user's timezone. +func localTimestamp(timestamp string) string { + parsed, err := time.Parse(time.RFC3339, timestamp) + if err != nil { + return timestamp + } + return parsed.Local().Format("2006-01-02 15:04:05 MST") } // asFile returns w as an *os.File, or nil when w is not backed by a file. diff --git a/internal/cli/rows_test.go b/internal/cli/rows_test.go index 9f5dfa4..9dbea6c 100644 --- a/internal/cli/rows_test.go +++ b/internal/cli/rows_test.go @@ -5,17 +5,23 @@ import ( "io" "strings" "testing" + "time" "charm.land/lipgloss/v2" ) func TestShortDate(t *testing.T) { + originalLocation := time.Local + t.Cleanup(func() { time.Local = originalLocation }) + time.Local = time.FixedZone("Test", -5*60*60) + tests := []struct { name string timestamp string want string }{ {name: "iso timestamp", timestamp: "2026-07-21T10:30:00Z", want: "2026-07-21"}, + {name: "timezone crosses date boundary", timestamp: "2026-07-21T02:30:00Z", want: "2026-07-20"}, {name: "date only", timestamp: "2026-07-21", want: "2026-07-21"}, {name: "short string", timestamp: "2026", want: "2026"}, {name: "empty", timestamp: "", want: ""}, @@ -30,6 +36,19 @@ func TestShortDate(t *testing.T) { } } +func TestLocalTimestamp(t *testing.T) { + originalLocation := time.Local + t.Cleanup(func() { time.Local = originalLocation }) + time.Local = time.FixedZone("Test", -5*60*60) + + if got, want := localTimestamp("2026-07-21T10:30:00Z"), "2026-07-21 05:30:00 Test"; got != want { + t.Fatalf("got %q, want %q", got, want) + } + if got, want := localTimestamp("not a timestamp"), "not a timestamp"; got != want { + t.Fatalf("got %q, want %q", got, want) + } +} + func TestRenderTable(t *testing.T) { origTerm := isTerminal origWidth := terminalWidth diff --git a/internal/cli/string_view.go b/internal/cli/string_view.go index 2dfec07..a5346ed 100644 --- a/internal/cli/string_view.go +++ b/internal/cli/string_view.go @@ -32,7 +32,7 @@ If no handle is given, views the authenticated user's string fields := []detailField{ {"Filename", view.Filename}, {"Author", view.Author.Handle}, - {"Created", view.CreatedAt}, + {"Created", localTimestamp(view.CreatedAt)}, } if view.Description != "" { fields = append(fields, detailField{"Description", view.Description}) -- 2.51.2