From 344a6f6415c3c1b593677adec3b8844e0839971b Mon Sep 17 00:00:00 2001 From: s Date: Thu, 13 Nov 2025 14:43:15 -0500 Subject: created pretty printing for all commands --- internal/formatter/formatter.go | 478 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 internal/formatter/formatter.go (limited to 'internal/formatter/formatter.go') diff --git a/internal/formatter/formatter.go b/internal/formatter/formatter.go new file mode 100644 index 0000000..4d65c60 --- /dev/null +++ b/internal/formatter/formatter.go @@ -0,0 +1,478 @@ +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 +} -- cgit v1.2.3