Files
gh-action-readme/internal/output.go
Ismo Vuorinen 7f80105ff5 feat: go 1.25.5, dependency updates, renamed internal/errors (#129)
* feat: rename internal/errors to internal/apperrors

* fix(tests): clear env values before using in tests

* feat: rename internal/errors to internal/apperrors

* chore(deps): update go and all dependencies

* chore: remove renovate from pre-commit, formatting

* chore: sonarcloud fixes

* feat: consolidate constants to appconstants/constants.go

* chore: sonarcloud fixes

* feat: simplification, deduplication, test utils

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: clean up

* fix: config discovery, const deduplication

* chore: fixes
2026-01-01 23:17:29 +02:00

263 lines
6.4 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package internal
import (
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/ivuorinen/gh-action-readme/appconstants"
"github.com/ivuorinen/gh-action-readme/internal/apperrors"
)
// ColoredOutput provides methods for colored terminal output.
// It implements all the focused interfaces for backward compatibility.
type ColoredOutput struct {
NoColor bool
Quiet bool
}
// Compile-time interface checks.
var (
_ MessageLogger = (*ColoredOutput)(nil)
_ ErrorReporter = (*ColoredOutput)(nil)
_ ErrorFormatter = (*ColoredOutput)(nil)
_ ProgressReporter = (*ColoredOutput)(nil)
_ OutputConfig = (*ColoredOutput)(nil)
_ CompleteOutput = (*ColoredOutput)(nil)
)
// NewColoredOutput creates a new colored output instance.
func NewColoredOutput(quiet bool) *ColoredOutput {
return &ColoredOutput{
NoColor: color.NoColor || os.Getenv("NO_COLOR") != "",
Quiet: quiet,
}
}
// IsQuiet returns whether the output is in quiet mode.
func (co *ColoredOutput) IsQuiet() bool {
return co.Quiet
}
// Success prints a success message in green.
func (co *ColoredOutput) Success(format string, args ...any) {
if co.Quiet {
return
}
if co.NoColor {
fmt.Printf("✅ "+format+"\n", args...)
} else {
color.Green("✅ "+format, args...)
}
}
// Error prints an error message in red to stderr.
func (co *ColoredOutput) Error(format string, args ...any) {
if co.NoColor {
fmt.Fprintf(os.Stderr, "❌ "+format+"\n", args...)
} else {
_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "❌ "+format+"\n", args...)
}
}
// Warning prints a warning message in yellow.
func (co *ColoredOutput) Warning(format string, args ...any) {
if co.Quiet {
return
}
if co.NoColor {
fmt.Printf("⚠️ "+format+"\n", args...)
} else {
color.Yellow("⚠️ "+format, args...)
}
}
// Info prints an info message in blue.
func (co *ColoredOutput) Info(format string, args ...any) {
if co.Quiet {
return
}
if co.NoColor {
fmt.Printf(" "+format+"\n", args...)
} else {
color.Blue(" "+format, args...)
}
}
// Progress prints a progress message in cyan.
func (co *ColoredOutput) Progress(format string, args ...any) {
if co.Quiet {
return
}
if co.NoColor {
fmt.Printf("🔄 "+format+"\n", args...)
} else {
color.Cyan("🔄 "+format, args...)
}
}
// Bold prints text in bold.
func (co *ColoredOutput) Bold(format string, args ...any) {
if co.Quiet {
return
}
if co.NoColor {
fmt.Printf(format+"\n", args...)
} else {
_, _ = color.New(color.Bold).Printf(format+"\n", args...)
}
}
// Printf prints without color formatting (respects quiet mode).
func (co *ColoredOutput) Printf(format string, args ...any) {
if co.Quiet {
return
}
fmt.Printf(format, args...)
}
// Fprintf prints to specified writer without color formatting.
func (co *ColoredOutput) Fprintf(w *os.File, format string, args ...any) {
_, _ = fmt.Fprintf(w, format, args...)
}
// ErrorWithSuggestions prints a ContextualError with suggestions and help.
func (co *ColoredOutput) ErrorWithSuggestions(err *apperrors.ContextualError) {
if err == nil {
return
}
// Print main error message
if co.NoColor {
fmt.Fprintf(os.Stderr, "❌ %s\n", err.Error())
} else {
color.Red("❌ %s", err.Error())
}
}
// ErrorWithContext creates and prints a contextual error with suggestions.
func (co *ColoredOutput) ErrorWithContext(
code appconstants.ErrorCode,
message string,
context map[string]string,
) {
suggestions := apperrors.GetSuggestions(code, context)
helpURL := apperrors.GetHelpURL(code)
contextualErr := apperrors.New(code, message).
WithSuggestions(suggestions...).
WithHelpURL(helpURL)
if len(context) > 0 {
contextualErr = contextualErr.WithDetails(context)
}
co.ErrorWithSuggestions(contextualErr)
}
// ErrorWithSimpleFix prints an error with a simple suggestion.
func (co *ColoredOutput) ErrorWithSimpleFix(message, suggestion string) {
contextualErr := apperrors.New(appconstants.ErrCodeUnknown, message).
WithSuggestions(suggestion)
co.ErrorWithSuggestions(contextualErr)
}
// FormatContextualError formats a ContextualError for display.
func (co *ColoredOutput) FormatContextualError(err *apperrors.ContextualError) string {
if err == nil {
return ""
}
var parts []string
// Add main error message
parts = append(parts, co.formatMainError(err))
// Add details section
if len(err.Details) > 0 {
parts = append(parts, co.formatDetailsSection(err.Details)...)
}
// Add suggestions section
if len(err.Suggestions) > 0 {
parts = append(parts, co.formatSuggestionsSection(err.Suggestions)...)
}
// Add help URL section
if err.HelpURL != "" {
parts = append(parts, co.formatHelpURLSection(err.HelpURL))
}
return strings.Join(parts, "\n")
}
// formatMainError formats the main error message with code.
func (co *ColoredOutput) formatMainError(err *apperrors.ContextualError) string {
mainMsg := fmt.Sprintf("%s [%s]", err.Error(), err.Code)
if co.NoColor {
return "❌ " + mainMsg
}
return color.RedString("❌ ") + mainMsg
}
// formatDetailsSection formats the details section.
func (co *ColoredOutput) formatDetailsSection(details map[string]string) []string {
var parts []string
if co.NoColor {
parts = append(parts, appconstants.SectionDetails)
} else {
parts = append(parts, color.New(color.Bold).Sprint(appconstants.SectionDetails))
}
for key, value := range details {
if co.NoColor {
parts = append(parts, fmt.Sprintf(appconstants.FormatDetailKeyValue, key, value))
} else {
parts = append(parts, fmt.Sprintf(appconstants.FormatDetailKeyValue,
color.CyanString(key),
color.WhiteString(value)))
}
}
return parts
}
// formatSuggestionsSection formats the suggestions section.
func (co *ColoredOutput) formatSuggestionsSection(suggestions []string) []string {
var parts []string
if co.NoColor {
parts = append(parts, appconstants.SectionSuggestions)
} else {
parts = append(parts, color.New(color.Bold).Sprint(appconstants.SectionSuggestions))
}
for _, suggestion := range suggestions {
if co.NoColor {
parts = append(parts, " • "+suggestion)
} else {
parts = append(parts, fmt.Sprintf(" %s %s",
color.YellowString("•"),
color.WhiteString(suggestion)))
}
}
return parts
}
// formatHelpURLSection formats the help URL section.
func (co *ColoredOutput) formatHelpURLSection(helpURL string) string {
if co.NoColor {
return "\nFor more help: " + helpURL
}
return fmt.Sprintf("\n%s: %s",
color.New(color.Bold).Sprint("For more help"),
color.BlueString(helpURL))
}