Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/milestones/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func StatusCmd() *cobra.Command {

ui.PrintSuccess(ui.EmojiMilestone, fmt.Sprintf("Active Milestone: %s", ui.Bold(activeMilestone.Name)))
ui.PrintInfo(4, "Project", projectName)
ui.PrintInfo(4, "Started", settings.FormatTime(activeMilestone.StartTime))
ui.PrintInfo(4, "Started", settings.FormatDateTime(activeMilestone.StartTime))
ui.PrintInfo(4, "Duration", ui.FormatDuration(activeMilestone.Duration()))
ui.PrintInfo(4, "Entries", fmt.Sprintf("%d", len(entries)))
ui.PrintInfo(4, "Total Time", ui.FormatDuration(totalTime))
Expand Down
28 changes: 26 additions & 2 deletions internal/settings/global_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func DefaultGlobalConfig() *GlobalConfig {
}
}

// GetGlobalConfigPath returns the path to the global config file. If
// the TMPO_DEV environment variable is set to 1 or true, a
// developer-specific config path is returned.
func GetGlobalConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
Expand All @@ -42,6 +45,8 @@ func GetGlobalConfigPath() (string, error) {
return filepath.Join(tmpoDir, "config.yaml"), nil
}

// LoadGlobalConfig loads the global config from disk. If the config
// file does not exist the default config is returned.
func LoadGlobalConfig() (*GlobalConfig, error) {
configPath, err := GetGlobalConfigPath()
if err != nil {
Expand Down Expand Up @@ -69,6 +74,8 @@ func LoadGlobalConfig() (*GlobalConfig, error) {
return &config, nil
}

// Save writes the GlobalConfig to the config file, creating the
// config directory if necessary.
func (gc *GlobalConfig) Save() error {
configPath, err := GetGlobalConfigPath()
if err != nil {
Expand All @@ -92,7 +99,8 @@ func (gc *GlobalConfig) Save() error {
return nil
}

// GetDisplayTimezone returns the user's configured timezone or local timezone as fallback
// GetDisplayTimezone returns the user's configured timezone or the
// local timezone as a fallback.
func GetDisplayTimezone() *time.Location {
cfg, err := LoadGlobalConfig()
if err != nil || cfg.Timezone == "" {
Expand All @@ -107,11 +115,14 @@ func GetDisplayTimezone() *time.Location {
return loc
}

// ToDisplayTime converts a UTC time to the user's display timezone
// ToDisplayTime converts the provided time to the user's display
// timezone.
func ToDisplayTime(t time.Time) time.Time {
return t.In(GetDisplayTimezone())
}

// FormatTime formats t according to the user's configured time
// format. Falls back to 12-hour format "3:04 PM".
func FormatTime(t time.Time) string {
t = ToDisplayTime(t)

Expand All @@ -127,6 +138,8 @@ func FormatTime(t time.Time) string {
return t.Format("3:04 PM")
}

// FormatTimePadded formats t with a padded hour for 12-hour formats
// ("03:04 PM") or returns the 24-hour format when configured.
func FormatTimePadded(t time.Time) string {
t = ToDisplayTime(t)

Expand All @@ -142,6 +155,8 @@ func FormatTimePadded(t time.Time) string {
return t.Format("03:04 PM")
}

// FormatDate formats t according to the user's configured date
// format. Defaults to "01/02/2006" (MM/DD/YYYY).
func FormatDate(t time.Time) string {
t = ToDisplayTime(t)

Expand All @@ -162,6 +177,7 @@ func FormatDate(t time.Time) string {
}
}

// FormatDateDashed formats t using dashes between date components.
func FormatDateDashed(t time.Time) string {
t = ToDisplayTime(t)

Expand All @@ -182,20 +198,28 @@ func FormatDateDashed(t time.Time) string {
}
}

// FormatDateTime returns the date and time formatted according to
// user preferences.
func FormatDateTime(t time.Time) string {
return FormatDate(t) + " " + FormatTime(t)
}

// FormatDateTimeDashed returns the dashed date and time formatted
// according to user preferences.
func FormatDateTimeDashed(t time.Time) string {
return FormatDateDashed(t) + " " + FormatTime(t)
}

// FormatDateLong returns a long human-readable date string like
// "Mon, Jan 2, 2006".
func FormatDateLong(t time.Time) string {
t = ToDisplayTime(t)

return t.Format("Mon, Jan 2, 2006")
}

// FormatDateTimeLong returns a long human-readable date/time string
// honoring the user's time format preference.
func FormatDateTimeLong(t time.Time) string {
t = ToDisplayTime(t)

Expand Down
14 changes: 9 additions & 5 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,15 @@ func FormatFileSize(bytes int64) string {
}

func FormatDuration(d time.Duration) string {
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60

if hours > 0 {
totalSeconds := int(d.Seconds())
days := totalSeconds / 86400
hours := (totalSeconds % 86400) / 3600
minutes := (totalSeconds % 3600) / 60
seconds := totalSeconds % 60

if days > 0 {
return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds)
} else if hours > 0 {
return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds)
} else if minutes > 0 {
return fmt.Sprintf("%dm %ds", minutes, seconds)
Expand Down
14 changes: 12 additions & 2 deletions internal/ui/ui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,19 @@ func TestFormatDuration(t *testing.T) {
expected: "1s",
},
{
name: "large duration",
name: "large duration under 24h",
duration: 23*time.Hour + 45*time.Minute + 30*time.Second,
expected: "23h 45m 30s",
},
{
name: "duration over 24h shows days",
duration: 25*time.Hour + 45*time.Minute + 30*time.Second,
expected: "25h 45m 30s",
expected: "1d 1h 45m 30s",
},
{
name: "multi-day duration",
duration: 688*time.Hour + 30*time.Minute + 39*time.Second,
expected: "28d 16h 30m 39s",
},
}

Expand Down
Loading