Skip to content

Commit 848e633

Browse files
authored
Merge branch 'main' into main
2 parents 4c2cd34 + b7fc9f9 commit 848e633

File tree

6 files changed

+175
-22
lines changed

6 files changed

+175
-22
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
run: "go vet ./..."
4040

4141
- name: Staticcheck
42-
uses: dominikh/staticcheck-action@v1.3.1
42+
uses: dominikh/staticcheck-action@v1.4.1
4343
with:
4444
version: "2025.1.1"
4545
install-go: false

color.go

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ var (
1919
// set (regardless of its value). This is a global option and affects all
2020
// colors. For more control over each color block use the methods
2121
// DisableColor() individually.
22-
NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" ||
23-
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
22+
NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || !stdoutIsTerminal()
2423

2524
// Output defines the standard output of the print functions. By default,
26-
// os.Stdout is used.
27-
Output = colorable.NewColorableStdout()
25+
// stdOut() is used.
26+
Output = stdOut()
2827

29-
// Error defines a color supporting writer for os.Stderr.
30-
Error = colorable.NewColorableStderr()
28+
// Error defines the standard error of the print functions. By default,
29+
// stdErr() is used.
30+
Error = stdErr()
3131

3232
// colorsCache is used to reduce the count of created Color objects and
3333
// allows to reuse already created objects with required Attribute.
@@ -40,6 +40,33 @@ func noColorIsSet() bool {
4040
return os.Getenv("NO_COLOR") != ""
4141
}
4242

43+
// stdoutIsTerminal returns true if os.Stdout is a terminal.
44+
// Returns false if os.Stdout is nil (e.g., when running as a Windows service).
45+
func stdoutIsTerminal() bool {
46+
if os.Stdout == nil {
47+
return false
48+
}
49+
return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
50+
}
51+
52+
// stdOut returns a writer for color output.
53+
// Returns io.Discard if os.Stdout is nil (e.g., when running as a Windows service).
54+
func stdOut() io.Writer {
55+
if os.Stdout == nil {
56+
return io.Discard
57+
}
58+
return colorable.NewColorableStdout()
59+
}
60+
61+
// stdErr returns a writer for color error output.
62+
// Returns io.Discard if os.Stderr is nil (e.g., when running as a Windows service).
63+
func stdErr() io.Writer {
64+
if os.Stderr == nil {
65+
return io.Discard
66+
}
67+
return colorable.NewColorableStderr()
68+
}
69+
4370
// Color defines a custom color object which is defined by SGR parameters.
4471
type Color struct {
4572
params []Attribute
@@ -220,22 +247,30 @@ func (c *Color) unset() {
220247
// a low-level function, and users should use the higher-level functions, such
221248
// as color.Fprint, color.Print, etc.
222249
func (c *Color) SetWriter(w io.Writer) *Color {
250+
_, _ = c.setWriter(w)
251+
return c
252+
}
253+
254+
func (c *Color) setWriter(w io.Writer) (int, error) {
223255
if c.isNoColorSet() {
224-
return c
256+
return 0, nil
225257
}
226258

227-
fmt.Fprint(w, c.format())
228-
return c
259+
return fmt.Fprint(w, c.format())
229260
}
230261

231262
// UnsetWriter resets all escape attributes and clears the output with the give
232263
// io.Writer. Usually should be called after SetWriter().
233264
func (c *Color) UnsetWriter(w io.Writer) {
265+
_, _ = c.unsetWriter(w)
266+
}
267+
268+
func (c *Color) unsetWriter(w io.Writer) (int, error) {
234269
if c.isNoColorSet() {
235-
return
270+
return 0, nil
236271
}
237272

238-
fmt.Fprintf(w, "%s[%dm", escape, Reset)
273+
return fmt.Fprintf(w, "%s[%dm", escape, Reset)
239274
}
240275

241276
// Add is used to chain SGR parameters. Use as many as parameters to combine
@@ -251,10 +286,20 @@ func (c *Color) Add(value ...Attribute) *Color {
251286
// On Windows, users should wrap w with colorable.NewColorable() if w is of
252287
// type *os.File.
253288
func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
254-
c.SetWriter(w)
255-
defer c.UnsetWriter(w)
289+
n, err = c.setWriter(w)
290+
if err != nil {
291+
return n, err
292+
}
256293

257-
return fmt.Fprint(w, a...)
294+
nn, err := fmt.Fprint(w, a...)
295+
n += nn
296+
if err != nil {
297+
return
298+
}
299+
300+
nn, err = c.unsetWriter(w)
301+
n += nn
302+
return n, err
258303
}
259304

260305
// Print formats using the default formats for its operands and writes to
@@ -274,10 +319,20 @@ func (c *Color) Print(a ...interface{}) (n int, err error) {
274319
// On Windows, users should wrap w with colorable.NewColorable() if w is of
275320
// type *os.File.
276321
func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
277-
c.SetWriter(w)
278-
defer c.UnsetWriter(w)
322+
n, err = c.setWriter(w)
323+
if err != nil {
324+
return n, err
325+
}
326+
327+
nn, err := fmt.Fprintf(w, format, a...)
328+
n += nn
329+
if err != nil {
330+
return
331+
}
279332

280-
return fmt.Fprintf(w, format, a...)
333+
nn, err = c.unsetWriter(w)
334+
n += nn
335+
return n, err
281336
}
282337

283338
// Printf formats according to a format specifier and writes to standard output.

color_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"os"
8+
"strings"
89
"testing"
910

1011
"github.com/mattn/go-colorable"
@@ -249,6 +250,42 @@ func Test_noColorIsSet(t *testing.T) {
249250
}
250251
}
251252

253+
func TestStdoutIsTerminal_NilStdout(t *testing.T) {
254+
stdout := os.Stdout
255+
os.Stdout = nil
256+
t.Cleanup(func() {
257+
os.Stdout = stdout
258+
})
259+
260+
if stdoutIsTerminal() {
261+
t.Fatal("stdoutIsTerminal() = true, want false")
262+
}
263+
}
264+
265+
func TestStdOut_NilStdout(t *testing.T) {
266+
stdout := os.Stdout
267+
os.Stdout = nil
268+
t.Cleanup(func() {
269+
os.Stdout = stdout
270+
})
271+
272+
if got := stdOut(); got != io.Discard {
273+
t.Fatalf("stdOut() = %v, want %v", got, io.Discard)
274+
}
275+
}
276+
277+
func TestStdErr_NilStderr(t *testing.T) {
278+
stderr := os.Stderr
279+
os.Stderr = nil
280+
t.Cleanup(func() {
281+
os.Stderr = stderr
282+
})
283+
284+
if got := stdErr(); got != io.Discard {
285+
t.Fatalf("stdErr() = %v, want %v", got, io.Discard)
286+
}
287+
}
288+
252289
func TestColorVisual(t *testing.T) {
253290
// First Visual Test
254291
Output = colorable.NewColorableStdout()
@@ -460,6 +497,64 @@ func TestColor_Sprintln_Newline(t *testing.T) {
460497
}
461498
}
462499

500+
func TestColor_Fprint(t *testing.T) {
501+
rb := new(strings.Builder)
502+
c := New(FgRed)
503+
504+
n, err := c.Fprint(rb, "foo", "bar")
505+
if err != nil {
506+
t.Errorf("Fprint error: %v", err)
507+
}
508+
got := rb.String()
509+
want := "\x1b[31mfoobar\x1b[0m"
510+
511+
if want != got {
512+
t.Errorf("Fprint error\n\nwant: %q\n got: %q", want, got)
513+
}
514+
if n != len(got) {
515+
t.Errorf("Fprint byte count does not match actual bytes written\n\nwant: %d\n got: %d", len(got), n)
516+
}
517+
}
518+
519+
func TestColor_Fprintln(t *testing.T) {
520+
rb := new(strings.Builder)
521+
c := New(FgRed)
522+
523+
n, err := c.Fprintln(rb, "foo", "bar")
524+
if err != nil {
525+
t.Errorf("Fprint error: %v", err)
526+
}
527+
got := rb.String()
528+
want := "\x1b[31mfoo bar\x1b[0m\n"
529+
530+
if want != got {
531+
t.Errorf("Fprintln error\n\nwant: %q\n got: %q", want, got)
532+
}
533+
if n != len(got) {
534+
t.Errorf("Fprintln byte count does not match actual bytes written\n\nwant: %d\n got: %d", len(got), n)
535+
}
536+
}
537+
538+
func TestColor_Fprintf(t *testing.T) {
539+
rb := new(strings.Builder)
540+
c := New(FgRed)
541+
542+
n, err := c.Fprintf(rb, "%-7s %-7s %5d\n", "hello", "world", 123)
543+
if err != nil {
544+
t.Errorf("Fprint error: %v", err)
545+
}
546+
547+
want := "\x1b[31mhello world 123\n\x1b[0m"
548+
549+
got := rb.String()
550+
if want != got {
551+
t.Errorf("Fprintf error\n\nwant: %q\n got: %q", want, got)
552+
}
553+
if n != len(got) {
554+
t.Errorf("Fprintf byte count does not match actual bytes written\n\nwant: %d\n got: %d", len(got), n)
555+
}
556+
}
557+
463558
func TestColor_Fprintln_Newline(t *testing.T) {
464559
rb := new(bytes.Buffer)
465560
c := New(FgRed)

color_windows.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import (
99
func init() {
1010
// Opt-in for ansi color support for current process.
1111
// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences
12+
if os.Stdout == nil {
13+
return
14+
}
1215
var outMode uint32
1316
out := windows.Handle(os.Stdout.Fd())
1417
if err := windows.GetConsoleMode(out, &outMode); err != nil {

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
module github.com/fatih/color
22

3-
go 1.24.1
3+
go 1.25.0
44

55
require (
66
github.com/mattn/go-colorable v0.1.14
77
github.com/mattn/go-isatty v0.0.20
8-
golang.org/x/sys v0.31.0
8+
golang.org/x/sys v0.42.0
99
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
33
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
44
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
55
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
7-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
6+
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
7+
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

0 commit comments

Comments
 (0)