Skip to content

Commit 6d0da07

Browse files
authored
Merge pull request cli#1631 from cli/color-env
Add support for CLICOLOR standard
2 parents cd32ef8 + b2de27c commit 6d0da07

File tree

6 files changed

+229
-9
lines changed

6 files changed

+229
-9
lines changed

cmd/gh/main.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path"
1111
"strings"
1212

13+
surveyCore "github.com/AlecAivazis/survey/v2/core"
1314
"github.com/cli/cli/api"
1415
"github.com/cli/cli/command"
1516
"github.com/cli/cli/internal/config"
@@ -43,6 +44,23 @@ func main() {
4344

4445
cmdFactory := factory.New(command.Version)
4546
stderr := cmdFactory.IOStreams.ErrOut
47+
if !cmdFactory.IOStreams.ColorEnabled() {
48+
surveyCore.DisableColor = true
49+
} else {
50+
// override survey's poor choice of color
51+
surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
52+
switch style {
53+
case "white":
54+
if cmdFactory.IOStreams.ColorSupport256() {
55+
return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242)
56+
}
57+
return ansi.ColorCode("default")
58+
default:
59+
return ansi.ColorCode(style)
60+
}
61+
}
62+
}
63+
4664
rootCmd := root.NewCmdRoot(cmdFactory, command.Version, command.BuildDate)
4765

4866
cfg, err := cmdFactory.Config()

pkg/cmd/root/help_topic.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ func NewHelpTopic(topic string) *cobra.Command {
2828
DEBUG: set to any value to enable verbose output to standard error. Include values "api"
2929
or "oauth" to print detailed information about HTTP requests or authentication flow.
3030
31-
PAGER: a paging program to send standard output to, e.g. "less".
31+
PAGER: a terminal paging program to send standard output to, e.g. "less".
3232
3333
GLAMOUR_STYLE: the style to use for rendering Markdown. See
3434
https://github.com/charmbracelet/glamour#styles
3535
36-
NO_COLOR: avoid printing ANSI escape sequences for color output.
36+
NO_COLOR: set to any value to avoid printing ANSI escape sequences for color output.
37+
38+
CLICOLOR: set to "0" to disable printing ANSI colors in output.
39+
40+
CLICOLOR_FORCE: set to a value other than "0" to keep ANSI colors in output
41+
even when the output is piped.
3742
`)
3843

3944
cmd := &cobra.Command{

pkg/iostreams/color.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package iostreams
22

3-
import "github.com/mgutz/ansi"
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/mgutz/ansi"
9+
)
410

511
var (
612
magenta = ansi.ColorFunc("magenta")
@@ -11,14 +17,42 @@ var (
1117
green = ansi.ColorFunc("green")
1218
gray = ansi.ColorFunc("black+h")
1319
bold = ansi.ColorFunc("default+b")
20+
21+
gray256 = func(t string) string {
22+
return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t)
23+
}
1424
)
1525

16-
func NewColorScheme(enabled bool) *ColorScheme {
17-
return &ColorScheme{enabled: enabled}
26+
func EnvColorDisabled() bool {
27+
return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0"
28+
}
29+
30+
func EnvColorForced() bool {
31+
return os.Getenv("CLICOLOR_FORCE") != "" && os.Getenv("CLICOLOR_FORCE") != "0"
32+
}
33+
34+
func Is256ColorSupported() bool {
35+
term := os.Getenv("TERM")
36+
colorterm := os.Getenv("COLORTERM")
37+
38+
return strings.Contains(term, "256") ||
39+
strings.Contains(term, "24bit") ||
40+
strings.Contains(term, "truecolor") ||
41+
strings.Contains(colorterm, "256") ||
42+
strings.Contains(colorterm, "24bit") ||
43+
strings.Contains(colorterm, "truecolor")
44+
}
45+
46+
func NewColorScheme(enabled, is256enabled bool) *ColorScheme {
47+
return &ColorScheme{
48+
enabled: enabled,
49+
is256enabled: is256enabled,
50+
}
1851
}
1952

2053
type ColorScheme struct {
21-
enabled bool
54+
enabled bool
55+
is256enabled bool
2256
}
2357

2458
func (c *ColorScheme) Bold(t string) string {
@@ -53,6 +87,9 @@ func (c *ColorScheme) Gray(t string) string {
5387
if !c.enabled {
5488
return t
5589
}
90+
if c.is256enabled {
91+
return gray256(t)
92+
}
5693
return gray(t)
5794
}
5895

pkg/iostreams/color_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package iostreams
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func TestEnvColorDisabled(t *testing.T) {
9+
orig_NO_COLOR := os.Getenv("NO_COLOR")
10+
orig_CLICOLOR := os.Getenv("CLICOLOR")
11+
orig_CLICOLOR_FORCE := os.Getenv("CLICOLOR_FORCE")
12+
t.Cleanup(func() {
13+
os.Setenv("NO_COLOR", orig_NO_COLOR)
14+
os.Setenv("CLICOLOR", orig_CLICOLOR)
15+
os.Setenv("CLICOLOR_FORCE", orig_CLICOLOR_FORCE)
16+
})
17+
18+
tests := []struct {
19+
name string
20+
NO_COLOR string
21+
CLICOLOR string
22+
CLICOLOR_FORCE string
23+
want bool
24+
}{
25+
{
26+
name: "pristine env",
27+
NO_COLOR: "",
28+
CLICOLOR: "",
29+
CLICOLOR_FORCE: "",
30+
want: false,
31+
},
32+
{
33+
name: "NO_COLOR enabled",
34+
NO_COLOR: "1",
35+
CLICOLOR: "",
36+
CLICOLOR_FORCE: "",
37+
want: true,
38+
},
39+
{
40+
name: "CLICOLOR disabled",
41+
NO_COLOR: "",
42+
CLICOLOR: "0",
43+
CLICOLOR_FORCE: "",
44+
want: true,
45+
},
46+
{
47+
name: "CLICOLOR enabled",
48+
NO_COLOR: "",
49+
CLICOLOR: "1",
50+
CLICOLOR_FORCE: "",
51+
want: false,
52+
},
53+
{
54+
name: "CLICOLOR_FORCE has no effect",
55+
NO_COLOR: "",
56+
CLICOLOR: "",
57+
CLICOLOR_FORCE: "1",
58+
want: false,
59+
},
60+
}
61+
for _, tt := range tests {
62+
t.Run(tt.name, func(t *testing.T) {
63+
os.Setenv("NO_COLOR", tt.NO_COLOR)
64+
os.Setenv("CLICOLOR", tt.CLICOLOR)
65+
os.Setenv("CLICOLOR_FORCE", tt.CLICOLOR_FORCE)
66+
67+
if got := EnvColorDisabled(); got != tt.want {
68+
t.Errorf("EnvColorDisabled(): want %v, got %v", tt.want, got)
69+
}
70+
})
71+
}
72+
}
73+
74+
func TestEnvColorForced(t *testing.T) {
75+
orig_NO_COLOR := os.Getenv("NO_COLOR")
76+
orig_CLICOLOR := os.Getenv("CLICOLOR")
77+
orig_CLICOLOR_FORCE := os.Getenv("CLICOLOR_FORCE")
78+
t.Cleanup(func() {
79+
os.Setenv("NO_COLOR", orig_NO_COLOR)
80+
os.Setenv("CLICOLOR", orig_CLICOLOR)
81+
os.Setenv("CLICOLOR_FORCE", orig_CLICOLOR_FORCE)
82+
})
83+
84+
tests := []struct {
85+
name string
86+
NO_COLOR string
87+
CLICOLOR string
88+
CLICOLOR_FORCE string
89+
want bool
90+
}{
91+
{
92+
name: "pristine env",
93+
NO_COLOR: "",
94+
CLICOLOR: "",
95+
CLICOLOR_FORCE: "",
96+
want: false,
97+
},
98+
{
99+
name: "NO_COLOR enabled",
100+
NO_COLOR: "1",
101+
CLICOLOR: "",
102+
CLICOLOR_FORCE: "",
103+
want: false,
104+
},
105+
{
106+
name: "CLICOLOR disabled",
107+
NO_COLOR: "",
108+
CLICOLOR: "0",
109+
CLICOLOR_FORCE: "",
110+
want: false,
111+
},
112+
{
113+
name: "CLICOLOR enabled",
114+
NO_COLOR: "",
115+
CLICOLOR: "1",
116+
CLICOLOR_FORCE: "",
117+
want: false,
118+
},
119+
{
120+
name: "CLICOLOR_FORCE enabled",
121+
NO_COLOR: "",
122+
CLICOLOR: "",
123+
CLICOLOR_FORCE: "1",
124+
want: true,
125+
},
126+
{
127+
name: "CLICOLOR_FORCE disabled",
128+
NO_COLOR: "",
129+
CLICOLOR: "",
130+
CLICOLOR_FORCE: "0",
131+
want: false,
132+
},
133+
}
134+
for _, tt := range tests {
135+
t.Run(tt.name, func(t *testing.T) {
136+
os.Setenv("NO_COLOR", tt.NO_COLOR)
137+
os.Setenv("CLICOLOR", tt.CLICOLOR)
138+
os.Setenv("CLICOLOR_FORCE", tt.CLICOLOR_FORCE)
139+
140+
if got := EnvColorForced(); got != tt.want {
141+
t.Errorf("EnvColorForced(): want %v, got %v", tt.want, got)
142+
}
143+
})
144+
}
145+
}

pkg/iostreams/iostreams.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type IOStreams struct {
2626
// the original (non-colorable) output stream
2727
originalOut io.Writer
2828
colorEnabled bool
29+
is256enabled bool
2930

3031
progressIndicatorEnabled bool
3132
progressIndicator *spinner.Spinner
@@ -47,6 +48,10 @@ func (s *IOStreams) ColorEnabled() bool {
4748
return s.colorEnabled
4849
}
4950

51+
func (s *IOStreams) ColorSupport256() bool {
52+
return s.is256enabled
53+
}
54+
5055
func (s *IOStreams) SetStdinTTY(isTTY bool) {
5156
s.stdinTTYOverride = true
5257
s.stdinIsTTY = isTTY
@@ -200,7 +205,7 @@ func (s *IOStreams) TerminalWidth() int {
200205
}
201206

202207
func (s *IOStreams) ColorScheme() *ColorScheme {
203-
return NewColorScheme(s.ColorEnabled())
208+
return NewColorScheme(s.ColorEnabled(), s.ColorSupport256())
204209
}
205210

206211
func System() *IOStreams {
@@ -212,7 +217,8 @@ func System() *IOStreams {
212217
originalOut: os.Stdout,
213218
Out: colorable.NewColorable(os.Stdout),
214219
ErrOut: colorable.NewColorable(os.Stderr),
215-
colorEnabled: os.Getenv("NO_COLOR") == "" && stdoutIsTTY,
220+
colorEnabled: EnvColorForced() || (!EnvColorDisabled() && stdoutIsTTY),
221+
is256enabled: Is256ColorSupported(),
216222
pagerCommand: os.Getenv("PAGER"),
217223
}
218224

utils/color.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package utils
22

33
import (
4+
"fmt"
45
"io"
56
"os"
67

8+
"github.com/cli/cli/pkg/iostreams"
79
"github.com/mattn/go-colorable"
810
"github.com/mgutz/ansi"
911
)
@@ -32,14 +34,21 @@ func makeColorFunc(color string) func(string) string {
3234
cf := ansi.ColorFunc(color)
3335
return func(arg string) string {
3436
if isColorEnabled() {
37+
if color == "black+h" && iostreams.Is256ColorSupported() {
38+
return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, arg)
39+
}
3540
return cf(arg)
3641
}
3742
return arg
3843
}
3944
}
4045

4146
func isColorEnabled() bool {
42-
if os.Getenv("NO_COLOR") != "" {
47+
if iostreams.EnvColorForced() {
48+
return true
49+
}
50+
51+
if iostreams.EnvColorDisabled() {
4352
return false
4453
}
4554

0 commit comments

Comments
 (0)