diff --git a/cmd/gh/main.go b/cmd/gh/main.go index 73b153ffa..db329c3cd 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -10,6 +10,7 @@ import ( "path" "strings" + surveyCore "github.com/AlecAivazis/survey/v2/core" "github.com/cli/cli/api" "github.com/cli/cli/command" "github.com/cli/cli/internal/config" @@ -43,6 +44,23 @@ func main() { cmdFactory := factory.New(command.Version) stderr := cmdFactory.IOStreams.ErrOut + if !cmdFactory.IOStreams.ColorEnabled() { + surveyCore.DisableColor = true + } else { + // override survey's poor choice of color + surveyCore.TemplateFuncsWithColor["color"] = func(style string) string { + switch style { + case "white": + if cmdFactory.IOStreams.ColorSupport256() { + return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242) + } + return ansi.ColorCode("default") + default: + return ansi.ColorCode(style) + } + } + } + rootCmd := root.NewCmdRoot(cmdFactory, command.Version, command.BuildDate) cfg, err := cmdFactory.Config() diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index a1acf5a40..aacdfc5e5 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -28,12 +28,17 @@ func NewHelpTopic(topic string) *cobra.Command { DEBUG: set to any value to enable verbose output to standard error. Include values "api" or "oauth" to print detailed information about HTTP requests or authentication flow. - PAGER: a paging program to send standard output to, e.g. "less". + PAGER: a terminal paging program to send standard output to, e.g. "less". GLAMOUR_STYLE: the style to use for rendering Markdown. See https://github.com/charmbracelet/glamour#styles - NO_COLOR: avoid printing ANSI escape sequences for color output. + NO_COLOR: set to any value to avoid printing ANSI escape sequences for color output. + + CLICOLOR: set to "0" to disable printing ANSI colors in output. + + CLICOLOR_FORCE: set to a value other than "0" to keep ANSI colors in output + even when the output is piped. `) cmd := &cobra.Command{ diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 6bbc429c5..2efe93c8b 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -1,6 +1,12 @@ package iostreams -import "github.com/mgutz/ansi" +import ( + "fmt" + "os" + "strings" + + "github.com/mgutz/ansi" +) var ( magenta = ansi.ColorFunc("magenta") @@ -11,14 +17,42 @@ var ( green = ansi.ColorFunc("green") gray = ansi.ColorFunc("black+h") bold = ansi.ColorFunc("default+b") + + gray256 = func(t string) string { + return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t) + } ) -func NewColorScheme(enabled bool) *ColorScheme { - return &ColorScheme{enabled: enabled} +func EnvColorDisabled() bool { + return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0" +} + +func EnvColorForced() bool { + return os.Getenv("CLICOLOR_FORCE") != "" && os.Getenv("CLICOLOR_FORCE") != "0" +} + +func Is256ColorSupported() bool { + term := os.Getenv("TERM") + colorterm := os.Getenv("COLORTERM") + + return strings.Contains(term, "256") || + strings.Contains(term, "24bit") || + strings.Contains(term, "truecolor") || + strings.Contains(colorterm, "256") || + strings.Contains(colorterm, "24bit") || + strings.Contains(colorterm, "truecolor") +} + +func NewColorScheme(enabled, is256enabled bool) *ColorScheme { + return &ColorScheme{ + enabled: enabled, + is256enabled: is256enabled, + } } type ColorScheme struct { - enabled bool + enabled bool + is256enabled bool } func (c *ColorScheme) Bold(t string) string { @@ -53,6 +87,9 @@ func (c *ColorScheme) Gray(t string) string { if !c.enabled { return t } + if c.is256enabled { + return gray256(t) + } return gray(t) } diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go new file mode 100644 index 000000000..90b3ae024 --- /dev/null +++ b/pkg/iostreams/color_test.go @@ -0,0 +1,145 @@ +package iostreams + +import ( + "os" + "testing" +) + +func TestEnvColorDisabled(t *testing.T) { + orig_NO_COLOR := os.Getenv("NO_COLOR") + orig_CLICOLOR := os.Getenv("CLICOLOR") + orig_CLICOLOR_FORCE := os.Getenv("CLICOLOR_FORCE") + t.Cleanup(func() { + os.Setenv("NO_COLOR", orig_NO_COLOR) + os.Setenv("CLICOLOR", orig_CLICOLOR) + os.Setenv("CLICOLOR_FORCE", orig_CLICOLOR_FORCE) + }) + + tests := []struct { + name string + NO_COLOR string + CLICOLOR string + CLICOLOR_FORCE string + want bool + }{ + { + name: "pristine env", + NO_COLOR: "", + CLICOLOR: "", + CLICOLOR_FORCE: "", + want: false, + }, + { + name: "NO_COLOR enabled", + NO_COLOR: "1", + CLICOLOR: "", + CLICOLOR_FORCE: "", + want: true, + }, + { + name: "CLICOLOR disabled", + NO_COLOR: "", + CLICOLOR: "0", + CLICOLOR_FORCE: "", + want: true, + }, + { + name: "CLICOLOR enabled", + NO_COLOR: "", + CLICOLOR: "1", + CLICOLOR_FORCE: "", + want: false, + }, + { + name: "CLICOLOR_FORCE has no effect", + NO_COLOR: "", + CLICOLOR: "", + CLICOLOR_FORCE: "1", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("NO_COLOR", tt.NO_COLOR) + os.Setenv("CLICOLOR", tt.CLICOLOR) + os.Setenv("CLICOLOR_FORCE", tt.CLICOLOR_FORCE) + + if got := EnvColorDisabled(); got != tt.want { + t.Errorf("EnvColorDisabled(): want %v, got %v", tt.want, got) + } + }) + } +} + +func TestEnvColorForced(t *testing.T) { + orig_NO_COLOR := os.Getenv("NO_COLOR") + orig_CLICOLOR := os.Getenv("CLICOLOR") + orig_CLICOLOR_FORCE := os.Getenv("CLICOLOR_FORCE") + t.Cleanup(func() { + os.Setenv("NO_COLOR", orig_NO_COLOR) + os.Setenv("CLICOLOR", orig_CLICOLOR) + os.Setenv("CLICOLOR_FORCE", orig_CLICOLOR_FORCE) + }) + + tests := []struct { + name string + NO_COLOR string + CLICOLOR string + CLICOLOR_FORCE string + want bool + }{ + { + name: "pristine env", + NO_COLOR: "", + CLICOLOR: "", + CLICOLOR_FORCE: "", + want: false, + }, + { + name: "NO_COLOR enabled", + NO_COLOR: "1", + CLICOLOR: "", + CLICOLOR_FORCE: "", + want: false, + }, + { + name: "CLICOLOR disabled", + NO_COLOR: "", + CLICOLOR: "0", + CLICOLOR_FORCE: "", + want: false, + }, + { + name: "CLICOLOR enabled", + NO_COLOR: "", + CLICOLOR: "1", + CLICOLOR_FORCE: "", + want: false, + }, + { + name: "CLICOLOR_FORCE enabled", + NO_COLOR: "", + CLICOLOR: "", + CLICOLOR_FORCE: "1", + want: true, + }, + { + name: "CLICOLOR_FORCE disabled", + NO_COLOR: "", + CLICOLOR: "", + CLICOLOR_FORCE: "0", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("NO_COLOR", tt.NO_COLOR) + os.Setenv("CLICOLOR", tt.CLICOLOR) + os.Setenv("CLICOLOR_FORCE", tt.CLICOLOR_FORCE) + + if got := EnvColorForced(); got != tt.want { + t.Errorf("EnvColorForced(): want %v, got %v", tt.want, got) + } + }) + } +} diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index d6c8ae48f..968f06e06 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -26,6 +26,7 @@ type IOStreams struct { // the original (non-colorable) output stream originalOut io.Writer colorEnabled bool + is256enabled bool progressIndicatorEnabled bool progressIndicator *spinner.Spinner @@ -47,6 +48,10 @@ func (s *IOStreams) ColorEnabled() bool { return s.colorEnabled } +func (s *IOStreams) ColorSupport256() bool { + return s.is256enabled +} + func (s *IOStreams) SetStdinTTY(isTTY bool) { s.stdinTTYOverride = true s.stdinIsTTY = isTTY @@ -200,7 +205,7 @@ func (s *IOStreams) TerminalWidth() int { } func (s *IOStreams) ColorScheme() *ColorScheme { - return NewColorScheme(s.ColorEnabled()) + return NewColorScheme(s.ColorEnabled(), s.ColorSupport256()) } func System() *IOStreams { @@ -212,7 +217,8 @@ func System() *IOStreams { originalOut: os.Stdout, Out: colorable.NewColorable(os.Stdout), ErrOut: colorable.NewColorable(os.Stderr), - colorEnabled: os.Getenv("NO_COLOR") == "" && stdoutIsTTY, + colorEnabled: EnvColorForced() || (!EnvColorDisabled() && stdoutIsTTY), + is256enabled: Is256ColorSupported(), pagerCommand: os.Getenv("PAGER"), } diff --git a/utils/color.go b/utils/color.go index 123e0927c..3e875acc4 100644 --- a/utils/color.go +++ b/utils/color.go @@ -1,9 +1,11 @@ package utils import ( + "fmt" "io" "os" + "github.com/cli/cli/pkg/iostreams" "github.com/mattn/go-colorable" "github.com/mgutz/ansi" ) @@ -32,6 +34,9 @@ func makeColorFunc(color string) func(string) string { cf := ansi.ColorFunc(color) return func(arg string) string { if isColorEnabled() { + if color == "black+h" && iostreams.Is256ColorSupported() { + return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, arg) + } return cf(arg) } return arg @@ -39,7 +44,11 @@ func makeColorFunc(color string) func(string) string { } func isColorEnabled() bool { - if os.Getenv("NO_COLOR") != "" { + if iostreams.EnvColorForced() { + return true + } + + if iostreams.EnvColorDisabled() { return false }