diff --git a/color.go b/color.go index 39f4a7e7..ada7604e 100644 --- a/color.go +++ b/color.go @@ -19,15 +19,15 @@ 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() + // stdOut() is used. + Output = stdOut() - // Error defines a color supporting writer for os.Stderr. - Error = colorable.NewColorableStderr() + // 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. @@ -40,6 +40,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()) +} + +// 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 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() 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 {