commit baec7b1239ff91f8d4c1b8b3e3213394c5d34c54 Author: upstream Date: Fri Sep 4 08:25:41 2026 +0200 Upstream gookit/color state of the touched files before a10b061e0b diff --git a/color.go b/color.go new file mode 100644 index 0000000..d3906bf --- /dev/null +++ b/color.go @@ -0,0 +1,733 @@ +package color + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" + "sync" + + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" +) + +var ( + // NoColor defines if the output is colorized or not. It's dynamically set to + // false or true based on the stdout's file descriptor referring to a terminal + // or not. It's also set to true if the NO_COLOR environment variable is + // set (regardless of its value). This is a global option and affects all + // colors. For more control over each color block use the methods + // DisableColor() individually. + NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || !stdoutIsTerminal() + + // Output defines the standard output of the print functions. By default, + // stdOut() is used. + Output = stdOut() + + // Error defines the standard error of the print functions. By default, + // stdErr() is used. + Error = stdErr() + + // colorsCache is used to reduce the count of created Color objects and + // allows to reuse already created objects with required Attribute. + colorsCache = make(map[Attribute]*Color) + colorsCacheMu sync.Mutex // protects colorsCache +) + +// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string. +func noColorIsSet() bool { + return os.Getenv("NO_COLOR") != "" +} + +// stdoutIsTerminal returns true if os.Stdout is a terminal. +// Returns false if os.Stdout is nil (e.g., when running as a Windows service). +func stdoutIsTerminal() bool { + if os.Stdout == nil { + return false + } + return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) +} + +// stdOut returns a writer for color output. +// Returns io.Discard if os.Stdout is nil (e.g., when running as a Windows service). +func stdOut() io.Writer { + if os.Stdout == nil { + return io.Discard + } + return colorable.NewColorableStdout() +} + +// stdErr returns a writer for color error output. +// Returns io.Discard if os.Stderr is nil (e.g., when running as a Windows service). +func stdErr() io.Writer { + if os.Stderr == nil { + return io.Discard + } + return colorable.NewColorableStderr() +} + +// Color defines a custom color object which is defined by SGR parameters. +type Color struct { + params []Attribute + noColor *bool +} + +// Attribute defines a single SGR Code +type Attribute int + +const escape = "\x1b" + +// Base attributes +const ( + Reset Attribute = iota + Bold + Faint + Italic + Underline + BlinkSlow + BlinkRapid + ReverseVideo + Concealed + CrossedOut +) + +const ( + ResetBold Attribute = iota + 22 + ResetItalic + ResetUnderline + ResetBlinking + _ + ResetReversed + ResetConcealed + ResetCrossedOut +) + +var mapResetAttributes map[Attribute]Attribute = map[Attribute]Attribute{ + Bold: ResetBold, + Faint: ResetBold, + Italic: ResetItalic, + Underline: ResetUnderline, + BlinkSlow: ResetBlinking, + BlinkRapid: ResetBlinking, + ReverseVideo: ResetReversed, + Concealed: ResetConcealed, + CrossedOut: ResetCrossedOut, +} + +// Foreground text colors +const ( + FgBlack Attribute = iota + 30 + FgRed + FgGreen + FgYellow + FgBlue + FgMagenta + FgCyan + FgWhite + + // used internally for 256 and 24-bit coloring + foreground +) + +// Foreground Hi-Intensity text colors +const ( + FgHiBlack Attribute = iota + 90 + FgHiRed + FgHiGreen + FgHiYellow + FgHiBlue + FgHiMagenta + FgHiCyan + FgHiWhite +) + +// Background text colors +const ( + BgBlack Attribute = iota + 40 + BgRed + BgGreen + BgYellow + BgBlue + BgMagenta + BgCyan + BgWhite + + // used internally for 256 and 24-bit coloring + background +) + +// Background Hi-Intensity text colors +const ( + BgHiBlack Attribute = iota + 100 + BgHiRed + BgHiGreen + BgHiYellow + BgHiBlue + BgHiMagenta + BgHiCyan + BgHiWhite +) + +// New returns a newly created color object. +func New(value ...Attribute) *Color { + c := &Color{ + params: make([]Attribute, 0), + } + + if noColorIsSet() { + c.noColor = boolPtr(true) + } + + c.Add(value...) + return c +} + +// RGB returns a new foreground color in 24-bit RGB. +func RGB(r, g, b int) *Color { + return New(foreground, 2, Attribute(r), Attribute(g), Attribute(b)) +} + +// BgRGB returns a new background color in 24-bit RGB. +func BgRGB(r, g, b int) *Color { + return New(background, 2, Attribute(r), Attribute(g), Attribute(b)) +} + +// AddRGB is used to chain foreground RGB SGR parameters. Use as many as parameters to combine +// and create custom color objects. Example: .Add(34, 0, 12).Add(255, 128, 0). +func (c *Color) AddRGB(r, g, b int) *Color { + c.params = append(c.params, foreground, 2, Attribute(r), Attribute(g), Attribute(b)) + return c +} + +// AddRGB is used to chain background RGB SGR parameters. Use as many as parameters to combine +// and create custom color objects. Example: .Add(34, 0, 12).Add(255, 128, 0). +func (c *Color) AddBgRGB(r, g, b int) *Color { + c.params = append(c.params, background, 2, Attribute(r), Attribute(g), Attribute(b)) + return c +} + +// Set sets the given parameters immediately. It will change the color of +// output with the given SGR parameters until color.Unset() is called. +func Set(p ...Attribute) *Color { + c := New(p...) + c.Set() + return c +} + +// Unset resets all escape attributes and clears the output. Usually should +// be called after Set(). +func Unset() { + if NoColor { + return + } + + fmt.Fprintf(Output, "%s[%dm", escape, Reset) +} + +// Set sets the SGR sequence. +func (c *Color) Set() *Color { + if c.isNoColorSet() { + return c + } + + fmt.Fprint(Output, c.format()) + return c +} + +func (c *Color) unset() { + if c.isNoColorSet() { + return + } + + Unset() +} + +// SetWriter is used to set the SGR sequence with the given io.Writer. This is +// a low-level function, and users should use the higher-level functions, such +// as color.Fprint, color.Print, etc. +func (c *Color) SetWriter(w io.Writer) *Color { + _, _ = c.setWriter(w) + return c +} + +func (c *Color) setWriter(w io.Writer) (int, error) { + if c.isNoColorSet() { + return 0, nil + } + + return fmt.Fprint(w, c.format()) +} + +// UnsetWriter resets all escape attributes and clears the output with the give +// io.Writer. Usually should be called after SetWriter(). +func (c *Color) UnsetWriter(w io.Writer) { + _, _ = c.unsetWriter(w) +} + +func (c *Color) unsetWriter(w io.Writer) (int, error) { + if c.isNoColorSet() { + return 0, nil + } + + return fmt.Fprintf(w, "%s[%dm", escape, Reset) +} + +// Add is used to chain SGR parameters. Use as many as parameters to combine +// and create custom color objects. Example: Add(color.FgRed, color.Underline). +func (c *Color) Add(value ...Attribute) *Color { + c.params = append(c.params, value...) + return c +} + +// Fprint formats using the default formats for its operands and writes to w. +// Spaces are added between operands when neither is a string. +// It returns the number of bytes written and any write error encountered. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) { + n, err = c.setWriter(w) + if err != nil { + return n, err + } + + nn, err := fmt.Fprint(w, a...) + n += nn + if err != nil { + return + } + + nn, err = c.unsetWriter(w) + n += nn + return n, err +} + +// Print formats using the default formats for its operands and writes to +// standard output. Spaces are added between operands when neither is a +// string. It returns the number of bytes written and any write error +// encountered. This is the standard fmt.Print() method wrapped with the given +// color. +func (c *Color) Print(a ...interface{}) (n int, err error) { + c.Set() + defer c.unset() + + return fmt.Fprint(Output, a...) +} + +// Fprintf formats according to a format specifier and writes to w. +// It returns the number of bytes written and any write error encountered. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + n, err = c.setWriter(w) + if err != nil { + return n, err + } + + nn, err := fmt.Fprintf(w, format, a...) + n += nn + if err != nil { + return + } + + nn, err = c.unsetWriter(w) + n += nn + return n, err +} + +// Printf formats according to a format specifier and writes to standard output. +// It returns the number of bytes written and any write error encountered. +// This is the standard fmt.Printf() method wrapped with the given color. +func (c *Color) Printf(format string, a ...interface{}) (n int, err error) { + c.Set() + defer c.unset() + + return fmt.Fprintf(Output, format, a...) +} + +// Fprintln formats using the default formats for its operands and writes to w. +// Spaces are always added between operands and a newline is appended. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, c.wrap(sprintln(a...))) +} + +// Println formats using the default formats for its operands and writes to +// standard output. Spaces are always added between operands and a newline is +// appended. It returns the number of bytes written and any write error +// encountered. This is the standard fmt.Print() method wrapped with the given +// color. +func (c *Color) Println(a ...interface{}) (n int, err error) { + return fmt.Fprintln(Output, c.wrap(sprintln(a...))) +} + +// Sprint is just like Print, but returns a string instead of printing it. +func (c *Color) Sprint(a ...interface{}) string { + return c.wrap(fmt.Sprint(a...)) +} + +// Sprintln is just like Println, but returns a string instead of printing it. +func (c *Color) Sprintln(a ...interface{}) string { + return c.wrap(sprintln(a...)) + "\n" +} + +// Sprintf is just like Printf, but returns a string instead of printing it. +func (c *Color) Sprintf(format string, a ...interface{}) string { + return c.wrap(fmt.Sprintf(format, a...)) +} + +// FprintFunc returns a new function that prints the passed arguments as +// colorized with color.Fprint(). +func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) { + return func(w io.Writer, a ...interface{}) { + c.Fprint(w, a...) + } +} + +// PrintFunc returns a new function that prints the passed arguments as +// colorized with color.Print(). +func (c *Color) PrintFunc() func(a ...interface{}) { + return func(a ...interface{}) { + c.Print(a...) + } +} + +// FprintfFunc returns a new function that prints the passed arguments as +// colorized with color.Fprintf(). +func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) { + return func(w io.Writer, format string, a ...interface{}) { + c.Fprintf(w, format, a...) + } +} + +// PrintfFunc returns a new function that prints the passed arguments as +// colorized with color.Printf(). +func (c *Color) PrintfFunc() func(format string, a ...interface{}) { + return func(format string, a ...interface{}) { + c.Printf(format, a...) + } +} + +// FprintlnFunc returns a new function that prints the passed arguments as +// colorized with color.Fprintln(). +func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) { + return func(w io.Writer, a ...interface{}) { + c.Fprintln(w, a...) + } +} + +// PrintlnFunc returns a new function that prints the passed arguments as +// colorized with color.Println(). +func (c *Color) PrintlnFunc() func(a ...interface{}) { + return func(a ...interface{}) { + c.Println(a...) + } +} + +// SprintFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprint(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output, example: +// +// put := New(FgYellow).SprintFunc() +// fmt.Fprintf(color.Output, "This is a %s", put("warning")) +func (c *Color) SprintFunc() func(a ...interface{}) string { + return func(a ...interface{}) string { + return c.wrap(fmt.Sprint(a...)) + } +} + +// SprintfFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprintf(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output. +func (c *Color) SprintfFunc() func(format string, a ...interface{}) string { + return func(format string, a ...interface{}) string { + return c.wrap(fmt.Sprintf(format, a...)) + } +} + +// SprintlnFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprintln(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output. +func (c *Color) SprintlnFunc() func(a ...interface{}) string { + return func(a ...interface{}) string { + return c.wrap(sprintln(a...)) + "\n" + } +} + +// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m" +// an example output might be: "1;36" -> bold cyan +func (c *Color) sequence() string { + format := make([]string, len(c.params)) + for i, v := range c.params { + format[i] = strconv.Itoa(int(v)) + } + + return strings.Join(format, ";") +} + +// wrap wraps the s string with the colors attributes. The string is ready to +// be printed. +func (c *Color) wrap(s string) string { + if c.isNoColorSet() { + return s + } + + return c.format() + s + c.unformat() +} + +func (c *Color) format() string { + return fmt.Sprintf("%s[%sm", escape, c.sequence()) +} + +func (c *Color) unformat() string { + //return fmt.Sprintf("%s[%dm", escape, Reset) + //for each element in sequence let's use the specific reset escape, or the generic one if not found + format := make([]string, len(c.params)) + for i, v := range c.params { + format[i] = strconv.Itoa(int(Reset)) + ra, ok := mapResetAttributes[v] + if ok { + format[i] = strconv.Itoa(int(ra)) + } + } + + return fmt.Sprintf("%s[%sm", escape, strings.Join(format, ";")) +} + +// DisableColor disables the color output. Useful to not change any existing +// code and still being able to output. Can be used for flags like +// "--no-color". To enable back use EnableColor() method. +func (c *Color) DisableColor() { + c.noColor = boolPtr(true) +} + +// EnableColor enables the color output. Use it in conjunction with +// DisableColor(). Otherwise, this method has no side effects. +func (c *Color) EnableColor() { + c.noColor = boolPtr(false) +} + +func (c *Color) isNoColorSet() bool { + // check first if we have user set action + if c.noColor != nil { + return *c.noColor + } + + // if not return the global option, which is disabled by default + return NoColor +} + +// Equals returns a boolean value indicating whether two colors are equal. +func (c *Color) Equals(c2 *Color) bool { + if c == nil && c2 == nil { + return true + } + if c == nil || c2 == nil { + return false + } + + if len(c.params) != len(c2.params) { + return false + } + + counts := make(map[Attribute]int, len(c.params)) + for _, attr := range c.params { + counts[attr]++ + } + + for _, attr := range c2.params { + if counts[attr] == 0 { + return false + } + counts[attr]-- + } + + return true +} + +func boolPtr(v bool) *bool { + return &v +} + +func getCachedColor(p Attribute) *Color { + colorsCacheMu.Lock() + defer colorsCacheMu.Unlock() + + c, ok := colorsCache[p] + if !ok { + c = New(p) + colorsCache[p] = c + } + + return c +} + +func colorPrint(format string, p Attribute, a ...interface{}) { + c := getCachedColor(p) + + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + + if len(a) == 0 { + c.Print(format) + } else { + c.Printf(format, a...) + } +} + +func colorString(format string, p Attribute, a ...interface{}) string { + c := getCachedColor(p) + + if len(a) == 0 { + return c.SprintFunc()(format) + } + + return c.SprintfFunc()(format, a...) +} + +// Black is a convenient helper function to print with black foreground. A +// newline is appended to format by default. +func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) } + +// Red is a convenient helper function to print with red foreground. A +// newline is appended to format by default. +func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) } + +// Green is a convenient helper function to print with green foreground. A +// newline is appended to format by default. +func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) } + +// Yellow is a convenient helper function to print with yellow foreground. +// A newline is appended to format by default. +func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) } + +// Blue is a convenient helper function to print with blue foreground. A +// newline is appended to format by default. +func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) } + +// Magenta is a convenient helper function to print with magenta foreground. +// A newline is appended to format by default. +func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) } + +// Cyan is a convenient helper function to print with cyan foreground. A +// newline is appended to format by default. +func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) } + +// White is a convenient helper function to print with white foreground. A +// newline is appended to format by default. +func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) } + +// BlackString is a convenient helper function to return a string with black +// foreground. +func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) } + +// RedString is a convenient helper function to return a string with red +// foreground. +func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) } + +// GreenString is a convenient helper function to return a string with green +// foreground. +func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) } + +// YellowString is a convenient helper function to return a string with yellow +// foreground. +func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) } + +// BlueString is a convenient helper function to return a string with blue +// foreground. +func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) } + +// MagentaString is a convenient helper function to return a string with magenta +// foreground. +func MagentaString(format string, a ...interface{}) string { + return colorString(format, FgMagenta, a...) +} + +// CyanString is a convenient helper function to return a string with cyan +// foreground. +func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) } + +// WhiteString is a convenient helper function to return a string with white +// foreground. +func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) } + +// HiBlack is a convenient helper function to print with hi-intensity black foreground. A +// newline is appended to format by default. +func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) } + +// HiRed is a convenient helper function to print with hi-intensity red foreground. A +// newline is appended to format by default. +func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) } + +// HiGreen is a convenient helper function to print with hi-intensity green foreground. A +// newline is appended to format by default. +func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) } + +// HiYellow is a convenient helper function to print with hi-intensity yellow foreground. +// A newline is appended to format by default. +func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) } + +// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A +// newline is appended to format by default. +func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) } + +// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground. +// A newline is appended to format by default. +func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) } + +// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A +// newline is appended to format by default. +func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) } + +// HiWhite is a convenient helper function to print with hi-intensity white foreground. A +// newline is appended to format by default. +func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) } + +// HiBlackString is a convenient helper function to return a string with hi-intensity black +// foreground. +func HiBlackString(format string, a ...interface{}) string { + return colorString(format, FgHiBlack, a...) +} + +// HiRedString is a convenient helper function to return a string with hi-intensity red +// foreground. +func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) } + +// HiGreenString is a convenient helper function to return a string with hi-intensity green +// foreground. +func HiGreenString(format string, a ...interface{}) string { + return colorString(format, FgHiGreen, a...) +} + +// HiYellowString is a convenient helper function to return a string with hi-intensity yellow +// foreground. +func HiYellowString(format string, a ...interface{}) string { + return colorString(format, FgHiYellow, a...) +} + +// HiBlueString is a convenient helper function to return a string with hi-intensity blue +// foreground. +func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) } + +// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta +// foreground. +func HiMagentaString(format string, a ...interface{}) string { + return colorString(format, FgHiMagenta, a...) +} + +// HiCyanString is a convenient helper function to return a string with hi-intensity cyan +// foreground. +func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) } + +// HiWhiteString is a convenient helper function to return a string with hi-intensity white +// foreground. +func HiWhiteString(format string, a ...interface{}) string { + return colorString(format, FgHiWhite, a...) +} + +// sprintln is a helper function to format a string with fmt.Sprintln and trim the trailing newline. +func sprintln(a ...interface{}) string { + return strings.TrimSuffix(fmt.Sprintln(a...), "\n") +} diff --git a/color_test.go b/color_test.go new file mode 100644 index 0000000..e22af6b --- /dev/null +++ b/color_test.go @@ -0,0 +1,664 @@ +package color + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "testing" + + "github.com/mattn/go-colorable" +) + +// Testing colors is kinda different. First we test for given colors and their +// escaped formatted results. Next we create some visual tests to be tested. +// Each visual test includes the color name to be compared. +func TestColor(t *testing.T) { + rb := new(bytes.Buffer) + Output = rb + + NoColor = false + + testColors := []struct { + text string + code Attribute + }{ + {text: "black", code: FgBlack}, + {text: "red", code: FgRed}, + {text: "green", code: FgGreen}, + {text: "yellow", code: FgYellow}, + {text: "blue", code: FgBlue}, + {text: "magent", code: FgMagenta}, + {text: "cyan", code: FgCyan}, + {text: "white", code: FgWhite}, + {text: "hblack", code: FgHiBlack}, + {text: "hred", code: FgHiRed}, + {text: "hgreen", code: FgHiGreen}, + {text: "hyellow", code: FgHiYellow}, + {text: "hblue", code: FgHiBlue}, + {text: "hmagent", code: FgHiMagenta}, + {text: "hcyan", code: FgHiCyan}, + {text: "hwhite", code: FgHiWhite}, + } + + for _, c := range testColors { + New(c.code).Print(c.text) + + line, _ := rb.ReadString('\n') + scannedLine := fmt.Sprintf("%q", line) + colored := fmt.Sprintf("\x1b[%dm%s\x1b[0m", c.code, c.text) + escapedForm := fmt.Sprintf("%q", colored) + + fmt.Printf("%s\t: %s\n", c.text, line) + + if scannedLine != escapedForm { + t.Errorf("Expecting %s, got '%s'\n", escapedForm, scannedLine) + } + } + + for _, c := range testColors { + line := New(c.code).Sprintf("%s", c.text) + scannedLine := fmt.Sprintf("%q", line) + colored := fmt.Sprintf("\x1b[%dm%s\x1b[0m", c.code, c.text) + escapedForm := fmt.Sprintf("%q", colored) + + fmt.Printf("%s\t: %s\n", c.text, line) + + if scannedLine != escapedForm { + t.Errorf("Expecting %s, got '%s'\n", escapedForm, scannedLine) + } + } +} + +func TestColorEquals(t *testing.T) { + fgblack1 := New(FgBlack) + fgblack2 := New(FgBlack) + bgblack := New(BgBlack) + fgbgblack := New(FgBlack, BgBlack) + fgblackbgred := New(FgBlack, BgRed) + fgred := New(FgRed) + bgred := New(BgRed) + + if !fgblack1.Equals(fgblack2) { + t.Error("Two black colors are not equal") + } + + if fgblack1.Equals(bgblack) { + t.Error("Fg and bg black colors are equal") + } + + if fgblack1.Equals(fgbgblack) { + t.Error("Fg black equals fg/bg black color") + } + + if fgblack1.Equals(fgred) { + t.Error("Fg black equals Fg red") + } + + if fgblack1.Equals(bgred) { + t.Error("Fg black equals Bg red") + } + + if fgblack1.Equals(fgblackbgred) { + t.Error("Fg black equals fg black bg red") + } +} + +func TestColorEquals_DuplicateAttributes(t *testing.T) { + ordered := New(FgRed, Bold).Add(FgRed) + reordered := New(Bold, FgRed).Add(FgRed) + differentCounts := New(FgRed, Bold).Add(Bold) + + if !ordered.Equals(reordered) { + t.Error("Colors with the same attributes in different orders are not equal") + } + + if ordered.Equals(differentCounts) { + t.Error("Colors with different duplicate attribute counts are equal") + } +} + +func TestNoColor(t *testing.T) { + rb := new(bytes.Buffer) + Output = rb + + testColors := []struct { + text string + code Attribute + }{ + {text: "black", code: FgBlack}, + {text: "red", code: FgRed}, + {text: "green", code: FgGreen}, + {text: "yellow", code: FgYellow}, + {text: "blue", code: FgBlue}, + {text: "magent", code: FgMagenta}, + {text: "cyan", code: FgCyan}, + {text: "white", code: FgWhite}, + {text: "hblack", code: FgHiBlack}, + {text: "hred", code: FgHiRed}, + {text: "hgreen", code: FgHiGreen}, + {text: "hyellow", code: FgHiYellow}, + {text: "hblue", code: FgHiBlue}, + {text: "hmagent", code: FgHiMagenta}, + {text: "hcyan", code: FgHiCyan}, + {text: "hwhite", code: FgHiWhite}, + } + + for _, c := range testColors { + p := New(c.code) + p.DisableColor() + p.Print(c.text) + + line, _ := rb.ReadString('\n') + if line != c.text { + t.Errorf("Expecting %s, got '%s'\n", c.text, line) + } + } + + // global check + NoColor = true + t.Cleanup(func() { + NoColor = false + }) + + for _, c := range testColors { + p := New(c.code) + p.Print(c.text) + + line, _ := rb.ReadString('\n') + if line != c.text { + t.Errorf("Expecting %s, got '%s'\n", c.text, line) + } + } +} + +func TestNoColor_Env(t *testing.T) { + rb := new(bytes.Buffer) + Output = rb + + testColors := []struct { + text string + code Attribute + }{ + {text: "black", code: FgBlack}, + {text: "red", code: FgRed}, + {text: "green", code: FgGreen}, + {text: "yellow", code: FgYellow}, + {text: "blue", code: FgBlue}, + {text: "magent", code: FgMagenta}, + {text: "cyan", code: FgCyan}, + {text: "white", code: FgWhite}, + {text: "hblack", code: FgHiBlack}, + {text: "hred", code: FgHiRed}, + {text: "hgreen", code: FgHiGreen}, + {text: "hyellow", code: FgHiYellow}, + {text: "hblue", code: FgHiBlue}, + {text: "hmagent", code: FgHiMagenta}, + {text: "hcyan", code: FgHiCyan}, + {text: "hwhite", code: FgHiWhite}, + } + + os.Setenv("NO_COLOR", "1") + t.Cleanup(func() { + os.Unsetenv("NO_COLOR") + }) + + for _, c := range testColors { + p := New(c.code) + p.Print(c.text) + + line, _ := rb.ReadString('\n') + if line != c.text { + t.Errorf("Expecting %s, got '%s'\n", c.text, line) + } + } +} + +func Test_noColorIsSet(t *testing.T) { + tests := []struct { + name string + act func() + want bool + }{ + { + name: "default", + act: func() {}, + want: false, + }, + { + name: "NO_COLOR=1", + act: func() { os.Setenv("NO_COLOR", "1") }, + want: true, + }, + { + name: "NO_COLOR=", + act: func() { os.Setenv("NO_COLOR", "") }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + os.Unsetenv("NO_COLOR") + }) + tt.act() + if got := noColorIsSet(); got != tt.want { + t.Errorf("noColorIsSet() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStdoutIsTerminal_NilStdout(t *testing.T) { + stdout := os.Stdout + os.Stdout = nil + t.Cleanup(func() { + os.Stdout = stdout + }) + + if stdoutIsTerminal() { + t.Fatal("stdoutIsTerminal() = true, want false") + } +} + +func TestStdOut_NilStdout(t *testing.T) { + stdout := os.Stdout + os.Stdout = nil + t.Cleanup(func() { + os.Stdout = stdout + }) + + if got := stdOut(); got != io.Discard { + t.Fatalf("stdOut() = %v, want %v", got, io.Discard) + } +} + +func TestStdErr_NilStderr(t *testing.T) { + stderr := os.Stderr + os.Stderr = nil + t.Cleanup(func() { + os.Stderr = stderr + }) + + if got := stdErr(); got != io.Discard { + t.Fatalf("stdErr() = %v, want %v", got, io.Discard) + } +} + +func TestColorVisual(t *testing.T) { + // First Visual Test + Output = colorable.NewColorableStdout() + + New(FgRed).Printf("red\t") + New(BgRed).Print(" ") + New(FgRed, Bold).Println(" red") + + New(FgGreen).Printf("green\t") + New(BgGreen).Print(" ") + New(FgGreen, Bold).Println(" green") + + New(FgYellow).Printf("yellow\t") + New(BgYellow).Print(" ") + New(FgYellow, Bold).Println(" yellow") + + New(FgBlue).Printf("blue\t") + New(BgBlue).Print(" ") + New(FgBlue, Bold).Println(" blue") + + New(FgMagenta).Printf("magenta\t") + New(BgMagenta).Print(" ") + New(FgMagenta, Bold).Println(" magenta") + + New(FgCyan).Printf("cyan\t") + New(BgCyan).Print(" ") + New(FgCyan, Bold).Println(" cyan") + + New(FgWhite).Printf("white\t") + New(BgWhite).Print(" ") + New(FgWhite, Bold).Println(" white") + fmt.Println("") + + // Second Visual test + Black("black") + Red("red") + Green("green") + Yellow("yellow") + Blue("blue") + Magenta("magenta") + Cyan("cyan") + White("white") + HiBlack("hblack") + HiRed("hred") + HiGreen("hgreen") + HiYellow("hyellow") + HiBlue("hblue") + HiMagenta("hmagenta") + HiCyan("hcyan") + HiWhite("hwhite") + + // Third visual test + fmt.Println() + Set(FgBlue) + fmt.Println("is this blue?") + Unset() + + Set(FgMagenta) + fmt.Println("and this magenta?") + Unset() + + // Fourth Visual test + fmt.Println() + blue := New(FgBlue).PrintlnFunc() + blue("blue text with custom print func") + + red := New(FgRed).PrintfFunc() + red("red text with a printf func: %d\n", 123) + + put := New(FgYellow).SprintFunc() + warn := New(FgRed).SprintFunc() + + fmt.Fprintf(Output, "this is a %s and this is %s.\n", put("warning"), warn("error")) + + info := New(FgWhite, BgGreen).SprintFunc() + fmt.Fprintf(Output, "this %s rocks!\n", info("package")) + + notice := New(FgBlue).FprintFunc() + notice(os.Stderr, "just a blue notice to stderr") + + // Fifth Visual Test + fmt.Println() + + fmt.Fprintln(Output, BlackString("black")) + fmt.Fprintln(Output, RedString("red")) + fmt.Fprintln(Output, GreenString("green")) + fmt.Fprintln(Output, YellowString("yellow")) + fmt.Fprintln(Output, BlueString("blue")) + fmt.Fprintln(Output, MagentaString("magenta")) + fmt.Fprintln(Output, CyanString("cyan")) + fmt.Fprintln(Output, WhiteString("white")) + fmt.Fprintln(Output, HiBlackString("hblack")) + fmt.Fprintln(Output, HiRedString("hred")) + fmt.Fprintln(Output, HiGreenString("hgreen")) + fmt.Fprintln(Output, HiYellowString("hyellow")) + fmt.Fprintln(Output, HiBlueString("hblue")) + fmt.Fprintln(Output, HiMagentaString("hmagenta")) + fmt.Fprintln(Output, HiCyanString("hcyan")) + fmt.Fprintln(Output, HiWhiteString("hwhite")) +} + +func TestNoFormat(t *testing.T) { + fmt.Printf("%s %%s = ", BlackString("Black")) + Black("%s") + + fmt.Printf("%s %%s = ", RedString("Red")) + Red("%s") + + fmt.Printf("%s %%s = ", GreenString("Green")) + Green("%s") + + fmt.Printf("%s %%s = ", YellowString("Yellow")) + Yellow("%s") + + fmt.Printf("%s %%s = ", BlueString("Blue")) + Blue("%s") + + fmt.Printf("%s %%s = ", MagentaString("Magenta")) + Magenta("%s") + + fmt.Printf("%s %%s = ", CyanString("Cyan")) + Cyan("%s") + + fmt.Printf("%s %%s = ", WhiteString("White")) + White("%s") + + fmt.Printf("%s %%s = ", HiBlackString("HiBlack")) + HiBlack("%s") + + fmt.Printf("%s %%s = ", HiRedString("HiRed")) + HiRed("%s") + + fmt.Printf("%s %%s = ", HiGreenString("HiGreen")) + HiGreen("%s") + + fmt.Printf("%s %%s = ", HiYellowString("HiYellow")) + HiYellow("%s") + + fmt.Printf("%s %%s = ", HiBlueString("HiBlue")) + HiBlue("%s") + + fmt.Printf("%s %%s = ", HiMagentaString("HiMagenta")) + HiMagenta("%s") + + fmt.Printf("%s %%s = ", HiCyanString("HiCyan")) + HiCyan("%s") + + fmt.Printf("%s %%s = ", HiWhiteString("HiWhite")) + HiWhite("%s") +} + +func TestNoFormatString(t *testing.T) { + tests := []struct { + f func(string, ...interface{}) string + format string + args []interface{} + want string + }{ + {BlackString, "%s", nil, "\x1b[30m%s\x1b[0m"}, + {RedString, "%s", nil, "\x1b[31m%s\x1b[0m"}, + {GreenString, "%s", nil, "\x1b[32m%s\x1b[0m"}, + {YellowString, "%s", nil, "\x1b[33m%s\x1b[0m"}, + {BlueString, "%s", nil, "\x1b[34m%s\x1b[0m"}, + {MagentaString, "%s", nil, "\x1b[35m%s\x1b[0m"}, + {CyanString, "%s", nil, "\x1b[36m%s\x1b[0m"}, + {WhiteString, "%s", nil, "\x1b[37m%s\x1b[0m"}, + {HiBlackString, "%s", nil, "\x1b[90m%s\x1b[0m"}, + {HiRedString, "%s", nil, "\x1b[91m%s\x1b[0m"}, + {HiGreenString, "%s", nil, "\x1b[92m%s\x1b[0m"}, + {HiYellowString, "%s", nil, "\x1b[93m%s\x1b[0m"}, + {HiBlueString, "%s", nil, "\x1b[94m%s\x1b[0m"}, + {HiMagentaString, "%s", nil, "\x1b[95m%s\x1b[0m"}, + {HiCyanString, "%s", nil, "\x1b[96m%s\x1b[0m"}, + {HiWhiteString, "%s", nil, "\x1b[97m%s\x1b[0m"}, + } + + for i, test := range tests { + s := test.f(test.format, test.args...) + + if s != test.want { + t.Errorf("[%d] want: %q, got: %q", i, test.want, s) + } + } +} + +func TestColor_Println_Newline(t *testing.T) { + rb := new(bytes.Buffer) + Output = rb + + c := New(FgRed) + c.Println("foo") + + got := readRaw(t, rb) + want := "\x1b[31mfoo\x1b[0m\n" + + if want != got { + t.Errorf("Println newline error\n\nwant: %q\n got: %q", want, got) + } +} + +func TestColor_Sprintln_Newline(t *testing.T) { + c := New(FgRed) + + got := c.Sprintln("foo") + want := "\x1b[31mfoo\x1b[0m\n" + + if want != got { + t.Errorf("Println newline error\n\nwant: %q\n got: %q", want, got) + } +} + +func TestColor_Fprint(t *testing.T) { + rb := new(strings.Builder) + c := New(FgRed) + + n, err := c.Fprint(rb, "foo", "bar") + if err != nil { + t.Errorf("Fprint error: %v", err) + } + got := rb.String() + want := "\x1b[31mfoobar\x1b[0m" + + if want != got { + t.Errorf("Fprint error\n\nwant: %q\n got: %q", want, got) + } + if n != len(got) { + t.Errorf("Fprint byte count does not match actual bytes written\n\nwant: %d\n got: %d", len(got), n) + } +} + +func TestColor_Fprintln(t *testing.T) { + rb := new(strings.Builder) + c := New(FgRed) + + n, err := c.Fprintln(rb, "foo", "bar") + if err != nil { + t.Errorf("Fprint error: %v", err) + } + got := rb.String() + want := "\x1b[31mfoo bar\x1b[0m\n" + + if want != got { + t.Errorf("Fprintln error\n\nwant: %q\n got: %q", want, got) + } + if n != len(got) { + t.Errorf("Fprintln byte count does not match actual bytes written\n\nwant: %d\n got: %d", len(got), n) + } +} + +func TestColor_Fprintf(t *testing.T) { + rb := new(strings.Builder) + c := New(FgRed) + + n, err := c.Fprintf(rb, "%-7s %-7s %5d\n", "hello", "world", 123) + if err != nil { + t.Errorf("Fprint error: %v", err) + } + + want := "\x1b[31mhello world 123\n\x1b[0m" + + got := rb.String() + if want != got { + t.Errorf("Fprintf error\n\nwant: %q\n got: %q", want, got) + } + if n != len(got) { + t.Errorf("Fprintf byte count does not match actual bytes written\n\nwant: %d\n got: %d", len(got), n) + } +} + +func TestColor_Fprintln_Newline(t *testing.T) { + rb := new(bytes.Buffer) + c := New(FgRed) + c.Fprintln(rb, "foo") + + got := readRaw(t, rb) + want := "\x1b[31mfoo\x1b[0m\n" + + if want != got { + t.Errorf("Println newline error\n\nwant: %q\n got: %q", want, got) + } +} + +func readRaw(t *testing.T, r io.Reader) string { + t.Helper() + + out, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } + + return string(out) +} + +func TestIssue206_1(t *testing.T) { + // visual test, go test -v . + // to see the string with escape codes, use go test -v . > c:\temp\test.txt + underline := New(Underline).Sprint + + line := fmt.Sprintf("%s %s %s %s", "word1", underline("word2"), "word3", underline("word4")) + + line = CyanString(line) + + fmt.Println(line) + + result := fmt.Sprintf("%v", line) + const expectedResult = "\x1b[36mword1 \x1b[4mword2\x1b[24m word3 \x1b[4mword4\x1b[24m\x1b[0m" + + if !bytes.Equal([]byte(result), []byte(expectedResult)) { + t.Errorf("Expecting %v, got '%v'\n", expectedResult, result) + } +} + +func TestIssue206_2(t *testing.T) { + underline := New(Underline).Sprint + bold := New(Bold).Sprint + + line := fmt.Sprintf("%s %s", GreenString(underline("underlined regular green")), RedString(bold("bold red"))) + + fmt.Println(line) + + result := fmt.Sprintf("%v", line) + const expectedResult = "\x1b[32m\x1b[4munderlined regular green\x1b[24m\x1b[0m \x1b[31m\x1b[1mbold red\x1b[22m\x1b[0m" + + if !bytes.Equal([]byte(result), []byte(expectedResult)) { + t.Errorf("Expecting %v, got '%v'\n", expectedResult, result) + } +} + +func TestIssue218(t *testing.T) { + // Adds a newline to the end of the last string to make sure it isn't trimmed. + params := []interface{}{"word1", "word2", "word3", "word4\n"} + + c := New(FgCyan) + c.Println(params...) + + result := c.Sprintln(params...) + fmt.Println(params...) + fmt.Print(result) + + const expectedResult = "\x1b[36mword1 word2 word3 word4\n\x1b[0m\n" + + if !bytes.Equal([]byte(result), []byte(expectedResult)) { + t.Errorf("Sprintln: Expecting %v (%v), got '%v (%v)'\n", expectedResult, []byte(expectedResult), result, []byte(result)) + } + + fn := c.SprintlnFunc() + result = fn(params...) + if !bytes.Equal([]byte(result), []byte(expectedResult)) { + t.Errorf("SprintlnFunc: Expecting %v (%v), got '%v (%v)'\n", expectedResult, []byte(expectedResult), result, []byte(result)) + } + + var buf bytes.Buffer + c.Fprintln(&buf, params...) + result = buf.String() + if !bytes.Equal([]byte(result), []byte(expectedResult)) { + t.Errorf("Fprintln: Expecting %v (%v), got '%v (%v)'\n", expectedResult, []byte(expectedResult), result, []byte(result)) + } +} + +func TestRGB(t *testing.T) { + tests := []struct { + r, g, b int + }{ + {255, 128, 0}, // orange + {230, 42, 42}, // red + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + RGB(tt.r, tt.g, tt.b).Println("foreground") + RGB(tt.r, tt.g, tt.b).AddBgRGB(0, 0, 0).Println("with background") + BgRGB(tt.r, tt.g, tt.b).Println("background") + BgRGB(tt.r, tt.g, tt.b).AddRGB(255, 255, 255).Println("with foreground") + }) + } +}