package formatter import ( "bytes" "encoding/json" "fmt" "io" "os" "strings" "text/tabwriter" "git.db.org.ai/dborg/internal/utils" ) const ( ColorReset = "\033[0m" ColorRed = "\033[31m" ColorGreen = "\033[32m" ColorYellow = "\033[33m" ColorBlue = "\033[34m" ColorMagenta = "\033[35m" ColorCyan = "\033[36m" ColorGray = "\033[90m" ColorWhite = "\033[37m" ColorBold = "\033[1m" ColorDim = "\033[2m" ) type Formatter interface { Format(data any) (string, error) Print(data any) error } type OutputMode int const ( ModeJSON OutputMode = iota ModePretty ) type BaseFormatter struct { mode OutputMode writer io.Writer } func NewFormatter(isJSON bool) *BaseFormatter { mode := ModePretty if isJSON { mode = ModeJSON } return &BaseFormatter{ mode: mode, writer: os.Stdout, } } func (f *BaseFormatter) IsJSON() bool { return f.mode == ModeJSON } func (f *BaseFormatter) FormatJSON(data any) error { return utils.PrintJSON(data) } func isTerminal() bool { fileInfo, err := os.Stdout.Stat() if err != nil { return false } return (fileInfo.Mode() & os.ModeCharDevice) != 0 } func GetTerminalWidth() int { return 80 } func Colorize(text string, color string) string { if !isTerminal() { return text } return color + text + ColorReset } func Bold(text string) string { return Colorize(text, ColorBold) } func Dim(text string) string { return Colorize(text, ColorDim) } func Red(text string) string { return Colorize(text, ColorRed) } func Green(text string) string { return Colorize(text, ColorGreen) } func Yellow(text string) string { return Colorize(text, ColorYellow) } func Blue(text string) string { return Colorize(text, ColorBlue) } func Cyan(text string) string { return Colorize(text, ColorCyan) } func Gray(text string) string { return Colorize(text, ColorGray) } func Magenta(text string) string { return Colorize(text, ColorMagenta) } type CreditsDisplay struct { Current int64 Used int64 Remaining int64 Operation string } func FormatCredits(credits int64) string { var color string switch { case credits > 1000: color = ColorGreen case credits > 100: color = ColorYellow default: color = ColorRed } return Colorize(fmt.Sprintf("%d", credits), color) } func FormatCreditsWithLabel(credits int64, label string) string { return fmt.Sprintf("%s: %s", Dim(label), FormatCredits(credits)) } func PrintCreditsInfo(display *CreditsDisplay) { if display == nil { return } fmt.Printf("%s\n", Bold("Credits Information")) if display.Operation != "" { fmt.Printf(" %s: %s\n", Dim("Operation"), display.Operation) } if display.Current > 0 { fmt.Printf(" %s\n", FormatCreditsWithLabel(display.Current, "Current")) } if display.Used > 0 { fmt.Printf(" %s\n", FormatCreditsWithLabel(display.Used, "Used")) } if display.Remaining > 0 { fmt.Printf(" %s\n", FormatCreditsWithLabel(display.Remaining, "Remaining")) } fmt.Println() } type ProgressBar struct { Total int Current int Width int Label string } func NewProgressBar(total int, label string) *ProgressBar { return &ProgressBar{ Total: total, Width: 40, Label: label, } } func (p *ProgressBar) Update(current int) { p.Current = current } func (p *ProgressBar) Render() string { if p.Total <= 0 { return "" } percentage := float64(p.Current) / float64(p.Total) filled := int(percentage * float64(p.Width)) if filled > p.Width { filled = p.Width } bar := strings.Repeat("█", filled) + strings.Repeat("░", p.Width-filled) percentStr := fmt.Sprintf("%.1f%%", percentage*100) var barColor string switch { case percentage >= 1.0: barColor = ColorGreen case percentage >= 0.5: barColor = ColorYellow default: barColor = ColorCyan } coloredBar := Colorize(bar, barColor) if p.Label != "" { return fmt.Sprintf("%s [%s] %s (%d/%d)", Dim(p.Label), coloredBar, Bold(percentStr), p.Current, p.Total) } return fmt.Sprintf("[%s] %s (%d/%d)", coloredBar, Bold(percentStr), p.Current, p.Total) } func (p *ProgressBar) Print() { fmt.Printf("\r%s", p.Render()) } func (p *ProgressBar) Finish() { p.Current = p.Total fmt.Printf("\r%s\n", p.Render()) } type TableFormatter struct { headers []string rows [][]string writer *tabwriter.Writer } func NewTable(headers []string) *TableFormatter { return &TableFormatter{ headers: headers, rows: make([][]string, 0), writer: tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0), } } func (t *TableFormatter) AddRow(columns ...string) { t.rows = append(t.rows, columns) } func (t *TableFormatter) AddRows(rows [][]string) { t.rows = append(t.rows, rows...) } func (t *TableFormatter) Render() string { var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0) for i, h := range t.headers { if i > 0 { fmt.Fprint(w, "\t") } fmt.Fprint(w, Bold(h)) } fmt.Fprintln(w) for _, row := range t.rows { for i, col := range row { if i > 0 { fmt.Fprint(w, "\t") } fmt.Fprint(w, col) } fmt.Fprintln(w) } w.Flush() return buf.String() } func (t *TableFormatter) Print() { fmt.Print(t.Render()) } func (t *TableFormatter) PrintJSON() error { data := make([]map[string]string, 0, len(t.rows)) for _, row := range t.rows { rowMap := make(map[string]string) for i, col := range row { if i < len(t.headers) { rowMap[t.headers[i]] = col } } data = append(data, rowMap) } return utils.PrintJSON(data) } type KeyValue struct { Key string Value string } func FormatKeyValue(key, value string) string { return fmt.Sprintf("%s: %s", Cyan(key), value) } func FormatKeyValueList(items []KeyValue) string { var buf bytes.Buffer for i, item := range items { if i > 0 { buf.WriteString("\n") } buf.WriteString(FormatKeyValue(item.Key, item.Value)) } return buf.String() } func PrintKeyValue(key, value string) { fmt.Println(FormatKeyValue(key, value)) } func PrintSection(title string) { fmt.Printf("\n%s\n%s\n", Bold(title), strings.Repeat("─", len(title))) } func PrintDivider() { if isTerminal() { fmt.Println(Dim(strings.Repeat("─", 80))) } else { fmt.Println(strings.Repeat("-", 80)) } } type StatusIndicator int const ( StatusSuccess StatusIndicator = iota StatusWarning StatusError StatusInfo StatusPending ) func (s StatusIndicator) String() string { switch s { case StatusSuccess: return Green("✓") case StatusWarning: return Yellow("⚠") case StatusError: return Red("✗") case StatusInfo: return Blue("ℹ") case StatusPending: return Gray("●") default: return "?" } } func FormatStatus(status StatusIndicator, message string) string { return fmt.Sprintf("%s %s", status.String(), message) } func PrintStatus(status StatusIndicator, message string) { fmt.Println(FormatStatus(status, message)) } func PrintSuccess(message string) { PrintStatus(StatusSuccess, message) } func PrintWarning(message string) { PrintStatus(StatusWarning, message) } func PrintError(message string) { PrintStatus(StatusError, message) } func PrintInfo(message string) { PrintStatus(StatusInfo, message) } func FormatBytes(bytes int64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := int64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } func FormatPercentage(value, total int64) string { if total == 0 { return "0.0%" } percentage := float64(value) / float64(total) * 100 return fmt.Sprintf("%.1f%%", percentage) } func TruncateString(s string, maxLen int) string { if len(s) <= maxLen { return s } if maxLen <= 3 { return s[:maxLen] } return s[:maxLen-3] + "..." } func PadRight(s string, width int) string { if len(s) >= width { return s } return s + strings.Repeat(" ", width-len(s)) } func PadLeft(s string, width int) string { if len(s) >= width { return s } return strings.Repeat(" ", width-len(s)) + s } func FormatList(items []string, bullet string) string { if bullet == "" { bullet = "•" } var buf bytes.Buffer for i, item := range items { if i > 0 { buf.WriteString("\n") } buf.WriteString(fmt.Sprintf("%s %s", Dim(bullet), item)) } return buf.String() } func PrintList(items []string) { fmt.Println(FormatList(items, "•")) } func StreamJSON(w io.Writer, data any) error { encoder := json.NewEncoder(w) encoder.SetIndent("", " ") return encoder.Encode(data) } func StreamNDJSON(w io.Writer, data any) error { encoder := json.NewEncoder(w) return encoder.Encode(data) } func PrintColorizedJSON(data any) error { return utils.PrintJSON(data) } func FormatColorizedJSON(data any) (string, error) { output, err := json.MarshalIndent(data, "", " ") if err != nil { return "", fmt.Errorf("failed to format JSON: %w", err) } return utils.ColorizeJSON(output), nil } func PrintJSONWithHeader(header string, data any) error { fmt.Fprintf(os.Stderr, "\n%s\n", Bold(header)) output, err := json.MarshalIndent(data, "", " ") if err != nil { return fmt.Errorf("failed to format JSON: %w", err) } fmt.Println(utils.ColorizeJSON(output)) return nil }