Adds three small but high-leverage presentation tweaks for v0.1: 1. internal/cli/style is a new ~70 LOC package with Pass/Fail/Warn/ Dim/Bold helpers. Each is TTY-gated and obeys NO_COLOR. No external dep. Wired into the doctor PASS/FAIL/WARN status, the "banger:" error prefix on stderr, and the dim 'ready in <elapsed>' line. 2. internal/cli/errors translates rpc.ErrorResponse into user-facing text. operation_failed becomes invisible (the message wins); not_found, already_exists, bad_request, bad_version, unauthorized, unknown_method get short labels; unknown codes pass through. The daemon-attached op_id lands in dim parens — paste into journalctl --grep to find the daemon log line that produced the failure. 3. Tabwriter config converges on (0, 8, 2, ' ', 0) across every list/table command. The vm prune confirmation table picked up the right config; system install + system status switched from bare "key: value\n" lines to tabular form. printVMSpecLine drops its Unicode middle dot for an ASCII '|' so terminals without UTF-8 render cleanly. Tests cover translateRPCError for every code, style helpers no-op on non-TTY and under NO_COLOR. Smoke status greps switch from "key: value" to "key value" to match the new format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2 KiB
Go
70 lines
2 KiB
Go
// Package style provides a tiny, conservative ANSI-color helper for
|
|
// banger's CLI. The contract:
|
|
//
|
|
// - Each helper takes the writer the styled string is going to and
|
|
// returns either the wrapped string or the plain one.
|
|
// - "Wrapped" only happens when the writer is a TTY AND the
|
|
// NO_COLOR environment variable is unset.
|
|
// - No 256-color or truecolor; no theme system; no external dep.
|
|
//
|
|
// Banger's CLI uses these for status (pass/fail/warn), error
|
|
// prefixes, and dim secondary text. Anything richer belongs in a
|
|
// dedicated TUI layer that this package isn't.
|
|
package style
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// ANSI escape sequences. Kept private — callers compose meaning via
|
|
// the named helpers (Pass/Fail/Warn/...), not raw codes.
|
|
const (
|
|
ansiReset = "\x1b[0m"
|
|
ansiBold = "\x1b[1m"
|
|
ansiDim = "\x1b[2m"
|
|
ansiRed = "\x1b[31m"
|
|
ansiGreen = "\x1b[32m"
|
|
ansiYel = "\x1b[33m"
|
|
)
|
|
|
|
// Pass wraps s in green when w is a TTY and NO_COLOR is unset.
|
|
func Pass(w io.Writer, s string) string { return wrap(w, ansiGreen, s) }
|
|
|
|
// Fail wraps s in red.
|
|
func Fail(w io.Writer, s string) string { return wrap(w, ansiRed, s) }
|
|
|
|
// Warn wraps s in yellow.
|
|
func Warn(w io.Writer, s string) string { return wrap(w, ansiYel, s) }
|
|
|
|
// Dim wraps s in dim.
|
|
func Dim(w io.Writer, s string) string { return wrap(w, ansiDim, s) }
|
|
|
|
// Bold wraps s in bold.
|
|
func Bold(w io.Writer, s string) string { return wrap(w, ansiBold, s) }
|
|
|
|
// SupportsColor reports whether colored output should be emitted to
|
|
// w. Exposed so callers that build multi-segment strings can avoid
|
|
// duplicating the gate per call.
|
|
func SupportsColor(w io.Writer) bool {
|
|
if strings.TrimSpace(os.Getenv("NO_COLOR")) != "" {
|
|
return false
|
|
}
|
|
file, ok := w.(*os.File)
|
|
if !ok {
|
|
return false
|
|
}
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return info.Mode()&os.ModeCharDevice != 0
|
|
}
|
|
|
|
func wrap(w io.Writer, code, s string) string {
|
|
if !SupportsColor(w) {
|
|
return s
|
|
}
|
|
return code + s + ansiReset
|
|
}
|