Introduce internal/ui package and rewrite user-facing output

All user-facing output now goes through a single ui.Writer with a
uniform style:

  》 (white)     for begin / info / notice
  》 (green)     for complete / success
  Warning:      for warnings (orange)
  ERROR:        for errors (red)
    》          (indented) for progress heartbeats

Color is enabled when stdout is a TTY and NO_COLOR is unset.

Standards:
- Complete-sentence messages with fully qualified terms ("backup
  destination store", "local index database", "snapshot source
  files enumeration").
- Every Complete has a matching Begin.
- Natural verb tense conveys state ("Uploading" -> "Uploaded"). The
  words "begin"/"complete" never appear in message bodies; the marker
  color carries that information.
- ETA means clock time, not duration. Progress lines say "estimated
  remaining time (<dur>), finish at <time>" with both labeled.

Adds globals.CommitDate (populated by Makefile/Dockerfile/goreleaser
via ldflags from `git show -s --format=%cI HEAD`) and a startup banner
printed once per invocation.

Strips fx call-chain noise from startup errors so users see the actual
underlying error (e.g. "creating base path: mkdir /Volumes/BACKUPS:
permission denied" instead of three layers of "could not build
arguments for function ...").

README documents the output style and the ui package conventions.
This commit is contained in:
2026-06-17 04:32:05 +02:00
parent 8de8f8e5cc
commit 00d4b36e35
14 changed files with 533 additions and 187 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
@@ -20,6 +21,7 @@ import (
"sneak.berlin/go/vaultik/internal/pidlock"
"sneak.berlin/go/vaultik/internal/snapshot"
"sneak.berlin/go/vaultik/internal/storage"
"sneak.berlin/go/vaultik/internal/ui"
"sneak.berlin/go/vaultik/internal/vaultik"
)
@@ -33,11 +35,21 @@ type AppOptions struct {
Invokes []fx.Option
}
// setupGlobals sets up the globals with application startup time
func setupGlobals(lc fx.Lifecycle, g *globals.Globals) {
// setupGlobals records the startup time and prints the startup banner.
// In --cron mode the banner is suppressed (LogOptions.Cron == true).
func setupGlobals(lc fx.Lifecycle, g *globals.Globals, v *vaultik.Vaultik, opts log.LogOptions) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
g.StartTime = time.Now().UTC()
if opts.Cron || opts.Quiet {
// Replace UI writer with a discarding one so all
// user-facing output is suppressed.
v.UI = ui.NewWithColor(io.Discard, false)
} else {
v.UI.Banner("%s %s (commit %s, %s) invoked at %s by %s",
g.Appname, g.Version, g.ShortCommit(), g.CommitDate,
g.StartTime.Format(time.RFC3339), globals.Author)
}
return nil
},
})

View File

@@ -3,7 +3,6 @@ package cli
import (
"context"
"fmt"
"io"
"os"
"github.com/spf13/cobra"
@@ -73,9 +72,7 @@ specifying a path using --config or by setting VAULTIK_CONFIG to a path.`,
OnStart: func(ctx context.Context) error {
// Start the snapshot creation in a goroutine
go func() {
if opts.Cron {
v.Stdout = io.Discard
}
// --cron suppression is wired through v.UI by setupGlobals.
if err := v.CreateSnapshot(opts); err != nil {
if err != context.Canceled {
log.Error("Snapshot creation failed", "error", err)