// Package presenter renders scan results as human- or machine-readable // output: a colorized terminal table, indented JSON, or a compact // one-liner format. package presenter import ( "fmt" "io" "os" "path/filepath" "strings" "gitea.oblak.solutions/dimitar/gitFlow/pkg/status" ) // ColorMode controls ANSI color rendering. type ColorMode int const ( ColorAuto ColorMode = iota // color only when writing to a terminal ColorAlways // always emit ANSI color ColorNever // never emit ANSI color ) // ParseColorMode converts a CLI string to a ColorMode; anything unknown // falls back to auto. func ParseColorMode(s string) ColorMode { switch s { case "always": return ColorAlways case "never": return ColorNever default: return ColorAuto } } // ThemeMode selects the ANSI palette used for colored output. type ThemeMode int const ( ThemeDark ThemeMode = iota // classic dark-terminal colors ThemeLight // brighter hues for light backgrounds ) // ParseTheme converts a CLI string to a ThemeMode; anything unknown falls // back to dark. func ParseTheme(s string) ThemeMode { if s == "light" { return ThemeLight } return ThemeDark } // Options control rendering behaviour. type Options struct { Color ColorMode Theme ThemeMode } // Formatter renders a ScanResult to a writer. type Formatter interface { Format(w io.Writer, result status.ScanResult) error } // For returns the formatter matching a format name ("table", "json", or // "compact"). func For(format string, opts Options) (Formatter, error) { switch format { case "table": return &TableFormatter{opts: opts}, nil case "json": return &JSONFormatter{}, nil case "compact": return &CompactFormatter{opts: opts}, nil default: return nil, fmt.Errorf("presenter: unsupported format %q (want table, json, or compact)", format) } } // Present renders result to w in the given format. func Present(w io.Writer, format string, result status.ScanResult, opts Options) error { formatter, err := For(format, opts) if err != nil { return err } return formatter.Format(w, result) } // renderer resolves ANSI escape codes according to the color mode, the // destination, and the NO_COLOR convention (https://no-color.org). type renderer struct { green string yellow string red string cyan string blue string reset string } // newRendererTheme resolves ANSI escape codes according to the color mode, // the theme, the destination, and the NO_COLOR convention // (https://no-color.org). func newRendererTheme(mode ColorMode, theme ThemeMode, w io.Writer) renderer { useColor := false switch mode { case ColorAlways: useColor = true case ColorNever: useColor = false default: // ColorAuto useColor = isTTY(w) && os.Getenv("NO_COLOR") == "" } if !useColor { return renderer{} } if theme == ThemeLight { // Bright variants (90-97) stay legible on light backgrounds. return renderer{ green: "\x1b[92m", yellow: "\x1b[93m", red: "\x1b[91m", cyan: "\x1b[96m", blue: "\x1b[94m", reset: "\x1b[0m", } } return renderer{ green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", blue: "\x1b[34m", reset: "\x1b[0m", } } // paint wraps text in the color for the given status. func (r renderer) paint(s status.RepoStatus, text string) string { var code string switch s { case status.StatusClean: code = r.green case status.StatusModified: code = r.yellow case status.StatusDiverged, status.StatusError: code = r.red case status.StatusAhead: code = r.blue case status.StatusBehind: code = r.yellow case status.StatusDetached, status.StatusBare: code = r.cyan } if code == "" { return text } return code + text + r.reset } func isTTY(w io.Writer) bool { f, ok := w.(*os.File) if !ok { return false } info, err := f.Stat() if err != nil { return false } return info.Mode()&os.ModeCharDevice != 0 } // ShortPath renders a path with the home directory collapsed to "~". func ShortPath(p string) string { home, err := os.UserHomeDir() if err != nil || home == "" { return p } rel, err := filepath.Rel(home, p) if err != nil || rel == "." || strings.HasPrefix(rel, "..") { return p } return filepath.Join("~", rel) } // shortPath is the internal name used by formatters; kept for compatibility. func shortPath(p string) string { return ShortPath(p) }