diff --git a/README.md b/README.md index bce272e..9e83ff6 100644 --- a/README.md +++ b/README.md @@ -421,12 +421,13 @@ Conventions: "Uploaded" for Complete. Never write the words "begin" or "complete" in the body — the marker color already conveys that. * All elapsed and remaining-time fields are explicitly scoped to their - subject: write "blob upload elapsed 30s, blob upload estimated remaining - time (14s), finish at 2026-06-17T03:15:00Z", never just "elapsed 30s, - ETA 14s". + subject: write "blob upload elapsed: 30s, blob upload ETA: 03:15:00 + (est remain 14s)", never just "elapsed 30s, ETA 14s". * "ETA" means an absolute clock time (when the operation will finish), not a remaining-duration. Use `ui.Time()` for the former and `ui.Duration()` for the latter, and label both. +* `ui.Time` formats same-day times as `HH:MM:SS` and other-day times as + `YYYY-MM-DD HH:MM:SS`. No timezone — local time is implied. Value colorizers in `internal/ui` colorize specific value types consistently. Compose messages from these helpers rather than embedding diff --git a/internal/snapshot/scanner.go b/internal/snapshot/scanner.go index bdb84df..81df80b 100644 --- a/internal/snapshot/scanner.go +++ b/internal/snapshot/scanner.go @@ -777,16 +777,16 @@ func (s *Scanner) printScanProgressLine(filesScanned int64, changedCount int, es eta = time.Duration(float64(remaining)/rate) * time.Second } if eta > 0 { - s.ui.Progress("Snapshot source files enumeration: %s files (~%s), %s changed or new, %.0f files/sec, enumeration elapsed %s, enumeration estimated remaining time (%s), finish at %s.", + s.ui.Progress("Snapshot source files enumeration: %s files (~%s), %s changed or new, %.0f files/sec, enumeration elapsed: %s, enumeration ETA: %s (est remain %s).", s.ui.Count(int(filesScanned)), s.ui.Percent(pct), s.ui.Count(changedCount), rate, s.ui.Duration(elapsed), - s.ui.Duration(eta), - s.ui.Time(time.Now().Add(eta))) + s.ui.Time(time.Now().Add(eta)), + s.ui.Duration(eta)) } else { - s.ui.Progress("Snapshot source files enumeration: %s files (~%s), %s changed or new, %.0f files/sec, enumeration elapsed %s.", + s.ui.Progress("Snapshot source files enumeration: %s files (~%s), %s changed or new, %.0f files/sec, enumeration elapsed: %s.", s.ui.Count(int(filesScanned)), s.ui.Percent(pct), s.ui.Count(changedCount), @@ -794,7 +794,7 @@ func (s *Scanner) printScanProgressLine(filesScanned int64, changedCount int, es s.ui.Duration(elapsed)) } } else { - s.ui.Progress("Snapshot source files enumeration: %s files seen, %s changed or new, %.0f files/sec, enumeration elapsed %s.", + s.ui.Progress("Snapshot source files enumeration: %s files seen, %s changed or new, %.0f files/sec, enumeration elapsed: %s.", s.ui.Count(int(filesScanned)), s.ui.Count(changedCount), rate, @@ -1067,7 +1067,7 @@ func (s *Scanner) printProcessingProgress(filesProcessed, totalFiles int, bytesP } if eta > 0 { - s.ui.Progress("Snapshot file processing: %s/%s files (%s), %s/%s, %s, %.0f files/sec, processing elapsed %s, processing estimated remaining time (%s), finish at %s.", + s.ui.Progress("Snapshot file processing: %s/%s files (%s), %s/%s, %s, %.0f files/sec, processing elapsed: %s, processing ETA: %s (est remain %s).", s.ui.Count(filesProcessed), s.ui.Count(totalFiles), s.ui.Percent(pct), @@ -1076,10 +1076,10 @@ func (s *Scanner) printProcessingProgress(filesProcessed, totalFiles int, bytesP s.ui.Speed(byteRate), fileRate, s.ui.Duration(elapsed), - s.ui.Duration(eta), - s.ui.Time(time.Now().Add(eta))) + s.ui.Time(time.Now().Add(eta)), + s.ui.Duration(eta)) } else { - s.ui.Progress("Snapshot file processing: %s/%s files (%s), %s/%s, %s, %.0f files/sec, processing elapsed %s.", + s.ui.Progress("Snapshot file processing: %s/%s files (%s), %s/%s, %s, %.0f files/sec, processing elapsed: %s.", s.ui.Count(filesProcessed), s.ui.Count(totalFiles), s.ui.Percent(pct), @@ -1251,15 +1251,15 @@ func (s *Scanner) makeUploadProgressCallback(ctx context.Context, finishedBlob * if avgSpeed > 0 { eta = time.Duration(float64(finishedBlob.Compressed-uploaded)/avgSpeed) * time.Second } - s.ui.Progress("Blob upload %s: %s / %s (%s) at %s, blob upload elapsed %s, blob upload estimated remaining time (%s), finish at %s.", + s.ui.Progress("Blob upload %s: %s / %s (%s) at %s, blob upload elapsed: %s, blob upload ETA: %s (est remain %s).", s.ui.Hex(finishedBlob.Hash), s.ui.Size(uploaded), s.ui.Size(finishedBlob.Compressed), s.ui.Percent(pct), s.ui.Speed(avgSpeed), s.ui.Duration(totalElapsed), - s.ui.Duration(eta), - s.ui.Time(now.Add(eta))) + s.ui.Time(now.Add(eta)), + s.ui.Duration(eta)) lastStdoutTime = now } diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 354949b..e05a99d 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -188,9 +188,17 @@ func (w *Writer) Duration(d time.Duration) string { return w.paint(ansiYellow, d.Round(time.Second).String()) } -// Time colorizes an absolute time (RFC3339, second precision). +// Time colorizes an absolute clock time. If t falls on today's local +// calendar date the output is "HH:MM:SS"; otherwise it is +// "YYYY-MM-DD HH:MM:SS". No timezone is included — values are +// displayed in the process's local zone. func (w *Writer) Time(t time.Time) string { - return w.paint(ansiYellow, t.Format(time.RFC3339)) + t = t.Local() + now := time.Now() + if t.Year() == now.Year() && t.YearDay() == now.YearDay() { + return w.paint(ansiYellow, t.Format("15:04:05")) + } + return w.paint(ansiYellow, t.Format("2006-01-02 15:04:05")) } // Count colorizes an integer count with thousands separators. diff --git a/internal/ui/ui_test.go b/internal/ui/ui_test.go index 08999df..0656093 100644 --- a/internal/ui/ui_test.go +++ b/internal/ui/ui_test.go @@ -72,6 +72,16 @@ func TestValueFormattersPlain(t *testing.T) { if got := w.Percent(12.34); got != "12.3%" { t.Errorf("Percent: got %q", got) } + + // Time format: today → HH:MM:SS, other day → YYYY-MM-DD HH:MM:SS. + today := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 14, 30, 45, 0, time.Local) + if got := w.Time(today); got != "14:30:45" { + t.Errorf("Time today: got %q, want 14:30:45", got) + } + other := time.Date(2030, 1, 2, 3, 4, 5, 0, time.Local) + if got := w.Time(other); got != "2030-01-02 03:04:05" { + t.Errorf("Time other day: got %q", got) + } } func TestValueFormattersColored(t *testing.T) { diff --git a/internal/vaultik/snapshot.go b/internal/vaultik/snapshot.go index aed6b81..f6dfef1 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -1277,6 +1277,9 @@ func (v *Vaultik) PruneDatabase() (*PruneResult, error) { result := &PruneResult{} + // Snapshot counts before deletion of incompletes. + snapshotCountBefore, _ := v.getTableCount("snapshots") + // First, delete any incomplete snapshots incompleteSnapshots, err := v.Repositories.Snapshots.GetIncompleteSnapshots(v.ctx) if err != nil { @@ -1329,8 +1332,12 @@ func (v *Vaultik) PruneDatabase() (*PruneResult, error) { "orphaned_blobs", result.BlobsDeleted, ) - v.UI.Complete("Pruned local index database: %d incomplete snapshots, %d orphaned files, %d orphaned chunks, %d orphaned blobs removed.", - result.SnapshotsDeleted, result.FilesDeleted, result.ChunksDeleted, result.BlobsDeleted) + snapshotCountAfter := snapshotCountBefore - result.SnapshotsDeleted + v.UI.Complete("Pruned local index database: %d incomplete snapshots removed (%d remain), %d orphaned files removed (%d remain), %d orphaned chunks removed (%d remain), %d orphaned blobs removed (%d remain).", + result.SnapshotsDeleted, snapshotCountAfter, + result.FilesDeleted, fileCountAfter, + result.ChunksDeleted, chunkCountAfter, + result.BlobsDeleted, blobCountAfter) return result, nil }