From 2aae7c9a9c41d6c5cc639e9ae57940c2afbf5c59 Mon Sep 17 00:00:00 2001 From: Adrien Tay Pamart Date: Tue, 8 Oct 2024 09:04:14 +0100 Subject: [PATCH 01/16] Fix for issue #230 now set/unsetwriter behave in a symmetric manner wrt color support detection --- color.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/color.go b/color.go index ee39b408..b2795fed 100644 --- a/color.go +++ b/color.go @@ -235,10 +235,6 @@ func (c *Color) UnsetWriter(w io.Writer) { return } - if NoColor { - return - } - fmt.Fprintf(w, "%s[%dm", escape, Reset) } From be536a2b567c465a5be477a16fab4a1b1a4168fc Mon Sep 17 00:00:00 2001 From: Sasha Melentyev Date: Sun, 27 Oct 2024 11:32:39 +0300 Subject: [PATCH 02/16] chore: go mod cleanup Signed-off-by: Sasha Melentyev --- go.mod | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 9f448d99..162fd62f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,5 @@ go 1.17 require ( github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 + golang.org/x/sys v0.25.0 ) - -require golang.org/x/sys v0.25.0 From 6e1bb0d896cf05029c367950a73fd47828a17df5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 01:06:40 +0000 Subject: [PATCH 03/16] Bump golang.org/x/sys from 0.25.0 to 0.28.0 Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.25.0 to 0.28.0. - [Commits](https://github.com/golang/sys/compare/v0.25.0...v0.28.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9f448d99..31ed5a21 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( github.com/mattn/go-isatty v0.0.20 ) -require golang.org/x/sys v0.25.0 +require golang.org/x/sys v0.28.0 diff --git a/go.sum b/go.sum index bca13090..ccfb81c3 100644 --- a/go.sum +++ b/go.sum @@ -5,5 +5,5 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= From 8ad1f030ed39b766ebb555e75acbae0cff941ed8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 01:27:02 +0000 Subject: [PATCH 04/16] Bump golang.org/x/sys from 0.28.0 to 0.30.0 Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.28.0 to 0.30.0. - [Commits](https://github.com/golang/sys/compare/v0.28.0...v0.30.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8847b77a..5fba2e83 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,5 @@ go 1.17 require ( github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 - golang.org/x/sys v0.28.0 + golang.org/x/sys v0.30.0 ) diff --git a/go.sum b/go.sum index ccfb81c3..9c800613 100644 --- a/go.sum +++ b/go.sum @@ -5,5 +5,5 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= From 8beb4b6981fca6eadbaa560f74bceb00d74fc351 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 04:43:51 +0000 Subject: [PATCH 05/16] Bump github.com/mattn/go-colorable from 0.1.13 to 0.1.14 Bumps [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable) from 0.1.13 to 0.1.14. - [Commits](https://github.com/mattn/go-colorable/compare/v0.1.13...v0.1.14) --- updated-dependencies: - dependency-name: github.com/mattn/go-colorable dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 5fba2e83..906512ad 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/fatih/color go 1.17 require ( - github.com/mattn/go-colorable v0.1.13 + github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 golang.org/x/sys v0.30.0 ) diff --git a/go.sum b/go.sum index 9c800613..382e582c 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,8 @@ -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= From db343c1eff4f395c27c0e4a55e6a763ee84d312e Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Thu, 27 Mar 2025 08:54:03 +0300 Subject: [PATCH 06/16] Update CI and go deps Signed-off-by: Fatih Arslan --- .github/workflows/go.yml | 4 ++-- go.mod | 4 ++-- go.sum | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 133cda1a..2c24f5be 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '>=1.22.0' + go-version-file: go.mod - name: Run go mod tidy run: | @@ -41,7 +41,7 @@ jobs: - name: Staticcheck uses: dominikh/staticcheck-action@v1.3.1 with: - version: "2024.1.1" + version: "2025.1.1" install-go: false - name: Build diff --git a/go.mod b/go.mod index 906512ad..de615e6e 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/fatih/color -go 1.17 +go 1.24.1 require ( github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 - golang.org/x/sys v0.30.0 + golang.org/x/sys v0.31.0 ) diff --git a/go.sum b/go.sum index 382e582c..a178e794 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,5 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= From bb5a0574c93ea74b3c905d47e3567f2a35543264 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 01:01:18 +0000 Subject: [PATCH 07/16] Bump golang.org/x/sys from 0.31.0 to 0.37.0 Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.31.0 to 0.37.0. - [Commits](https://github.com/golang/sys/compare/v0.31.0...v0.37.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index de615e6e..e18ca4ee 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,5 @@ go 1.24.1 require ( github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 - golang.org/x/sys v0.31.0 + golang.org/x/sys v0.37.0 ) diff --git a/go.sum b/go.sum index a178e794..3174cb19 100644 --- a/go.sum +++ b/go.sum @@ -3,5 +3,5 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= From 73e3e224ab2b19de6b9e509359768b9846750001 Mon Sep 17 00:00:00 2001 From: unsubble Date: Thu, 9 Oct 2025 18:03:40 +0300 Subject: [PATCH 08/16] =?UTF-8?q?Improve=20Color.Equals=20performance=20fr?= =?UTF-8?q?om=20O(n=C2=B2)=20to=20O(n)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- color.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/color.go b/color.go index b2795fed..70d1afb8 100644 --- a/color.go +++ b/color.go @@ -471,27 +471,24 @@ func (c *Color) Equals(c2 *Color) bool { 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 { - if !c2.attrExists(attr) { - return false - } + counts[attr]++ } - return true -} - -func (c *Color) attrExists(a Attribute) bool { - for _, attr := range c.params { - if attr == a { - return true + for _, attr := range c2.params { + if counts[attr] == 0 { + return false } + counts[attr]-- } - return false + return true } func boolPtr(v bool) *bool { From f4c524d2eaa23556a729880b72b04f58e9d53ce1 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Thu, 1 Jan 2026 17:05:41 +0800 Subject: [PATCH 09/16] fix: add nil check for os.Stdout to prevent panic on Windows services Signed-off-by: majiayu000 <1835304752@qq.com> --- color.go | 34 ++++++++++++++++++++++++++++++---- color_windows.go | 3 +++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/color.go b/color.go index b2795fed..3f65f058 100644 --- a/color.go +++ b/color.go @@ -19,15 +19,14 @@ var ( // 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" || - (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) + NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || !stdoutIsTerminal() // Output defines the standard output of the print functions. By default, // os.Stdout is used. - Output = colorable.NewColorableStdout() + Output = initOutput() // Error defines a color supporting writer for os.Stderr. - Error = colorable.NewColorableStderr() + Error = initError() // colorsCache is used to reduce the count of created Color objects and // allows to reuse already created objects with required Attribute. @@ -40,6 +39,33 @@ 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()) +} + +// initOutput returns a writer for color output. +// Returns io.Discard if os.Stdout is nil (e.g., when running as a Windows service). +func initOutput() io.Writer { + if os.Stdout == nil { + return io.Discard + } + return colorable.NewColorableStdout() +} + +// initError returns a writer for color error output. +// Returns io.Discard if os.Stderr is nil (e.g., when running as a Windows service). +func initError() 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 diff --git a/color_windows.go b/color_windows.go index be01c558..97e5a765 100644 --- a/color_windows.go +++ b/color_windows.go @@ -9,6 +9,9 @@ import ( func init() { // Opt-in for ansi color support for current process. // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences + if os.Stdout == nil { + return + } var outMode uint32 out := windows.Handle(os.Stdout.Fd()) if err := windows.GetConsoleMode(out, &outMode); err != nil { From cbda2c3dd1e540a099716dcd28d4820fe45968ca Mon Sep 17 00:00:00 2001 From: Matthew Hall Date: Wed, 4 Mar 2026 17:49:09 -0700 Subject: [PATCH 10/16] fix: include escape codes in byte counts from `Fprint`, `Fprintf` Fix the `n` byte count returned from `Color.Fprint` and `Color.Fprintf` to include the bytes in ANSI escape sequences. Both methods delegate to `SetWriter` and `UnsetWriter` methods to start and stop the excape sequence. Since both these methods are public and do not return the byte counts needed, I extracted private versions of these methods `setWriter` and `unsetWriter`, and redirected the `Fprint` and `Fprintf` to get accurate byte counts without breaking API compatibility. Finally, `unsetWriter()` is no longer deferred. If we encounter an error at any point, stop writing and return the error. --- color.go | 50 +++++++++++++++++++++++++++++++++---------- color_test.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/color.go b/color.go index b2795fed..39f4a7e7 100644 --- a/color.go +++ b/color.go @@ -220,22 +220,30 @@ func (c *Color) unset() { // 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 c + return 0, nil } - fmt.Fprint(w, c.format()) - return c + 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 + return 0, nil } - fmt.Fprintf(w, "%s[%dm", escape, Reset) + return fmt.Fprintf(w, "%s[%dm", escape, Reset) } // Add is used to chain SGR parameters. Use as many as parameters to combine @@ -251,10 +259,20 @@ func (c *Color) Add(value ...Attribute) *Color { // 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) { - c.SetWriter(w) - defer c.UnsetWriter(w) + n, err = c.setWriter(w) + if err != nil { + return n, err + } + + nn, err := fmt.Fprint(w, a...) + n += nn + if err != nil { + return + } - return fmt.Fprint(w, a...) + nn, err = c.unsetWriter(w) + n += nn + return n, err } // Print formats using the default formats for its operands and writes to @@ -274,10 +292,20 @@ func (c *Color) Print(a ...interface{}) (n int, err error) { // 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) { - c.SetWriter(w) - defer c.UnsetWriter(w) + n, err = c.setWriter(w) + if err != nil { + return n, err + } + + nn, err := fmt.Fprintf(w, format, a...) + n += nn + if err != nil { + return + } - return fmt.Fprintf(w, format, a...) + nn, err = c.unsetWriter(w) + n += nn + return n, err } // Printf formats according to a format specifier and writes to standard output. diff --git a/color_test.go b/color_test.go index 586039b4..880dcfb3 100644 --- a/color_test.go +++ b/color_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" "testing" "github.com/mattn/go-colorable" @@ -446,6 +447,64 @@ func TestColor_Sprintln_Newline(t *testing.T) { } } +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) From de54a6886e5a39967952fa15a2ffdd06da235b61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:13:56 +0000 Subject: [PATCH 11/16] Bump golang.org/x/sys from 0.37.0 to 0.40.0 Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.37.0 to 0.40.0. - [Commits](https://github.com/golang/sys/compare/v0.37.0...v0.40.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-version: 0.40.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index e18ca4ee..6c4911b4 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/fatih/color -go 1.24.1 +go 1.25.0 require ( github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 - golang.org/x/sys v0.37.0 + golang.org/x/sys v0.42.0 ) diff --git a/go.sum b/go.sum index 3174cb19..c7896917 100644 --- a/go.sum +++ b/go.sum @@ -3,5 +3,5 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= From 3a0d3b0a963c2f87a6c5c058c2ba4614e1e3e702 Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Fri, 20 Mar 2026 11:30:37 +0300 Subject: [PATCH 12/16] color: rename std stream helpers Problem: The nil-safe writer helpers in this PR are named and , which reads like package init work instead of standard stream accessors. The related docs also still describe the old defaults. Solution: Rename the helpers to and , and update the nearby doc comments so and describe the actual default writers. --- color.go | 17 +++++++++-------- color_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/color.go b/color.go index c8d4c7d5..ada7604e 100644 --- a/color.go +++ b/color.go @@ -22,11 +22,12 @@ var ( NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || !stdoutIsTerminal() // Output defines the standard output of the print functions. By default, - // os.Stdout is used. - Output = initOutput() + // stdOut() is used. + Output = stdOut() - // Error defines a color supporting writer for os.Stderr. - Error = initError() + // 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. @@ -48,18 +49,18 @@ func stdoutIsTerminal() bool { return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) } -// initOutput returns a writer for color output. +// stdOut returns a writer for color output. // Returns io.Discard if os.Stdout is nil (e.g., when running as a Windows service). -func initOutput() io.Writer { +func stdOut() io.Writer { if os.Stdout == nil { return io.Discard } return colorable.NewColorableStdout() } -// initError returns a writer for color error output. +// 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 initError() io.Writer { +func stdErr() io.Writer { if os.Stderr == nil { return io.Discard } diff --git a/color_test.go b/color_test.go index 880dcfb3..103d6cbb 100644 --- a/color_test.go +++ b/color_test.go @@ -236,6 +236,42 @@ func Test_noColorIsSet(t *testing.T) { } } +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() From 239a88f715e8e35f40492da7a1e08f7173e78e05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:33:55 +0000 Subject: [PATCH 13/16] Bump dominikh/staticcheck-action from 1.3.1 to 1.4.0 --- updated-dependencies: - dependency-name: dominikh/staticcheck-action dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2c24f5be..4186e2e2 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -39,7 +39,7 @@ jobs: run: "go vet ./..." - name: Staticcheck - uses: dominikh/staticcheck-action@v1.3.1 + uses: dominikh/staticcheck-action@v1.4.1 with: version: "2025.1.1" install-go: false From 7f812f029c41eddd3ac7fbbdf6cc78e4b175944b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:36:57 +0000 Subject: [PATCH 14/16] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4186e2e2..a2a6c643 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Check out code into the Go module directory - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v5 From 4c2cd3443934693bd8892fc0f7bb5bbec8e3788a Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Fri, 20 Mar 2026 11:44:17 +0300 Subject: [PATCH 15/16] Add tests Signed-off-by: Fatih Arslan --- color_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/color_test.go b/color_test.go index 586039b4..a396b2a4 100644 --- a/color_test.go +++ b/color_test.go @@ -104,6 +104,20 @@ func TestColorEquals(t *testing.T) { } } +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 From 120598440a16510564204450092d1e7925fad9ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:51:08 +0000 Subject: [PATCH 16/16] Bump actions/setup-go from 5 to 6 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a2a6c643..401edbc9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod