Skip to content

#285 add color output for mage list #301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 8, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions mage/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,31 @@ func testmain(m *testing.M) int {
if err := os.Unsetenv(mg.IgnoreDefaultEnv); err != nil {
log.Fatal(err)
}
if err := os.Setenv(mg.CacheEnv, dir); err != nil {
log.Fatal(err)
}
if err := os.Unsetenv(mg.EnableColorEnv); err != nil {
log.Fatal(err)
}
if err := os.Unsetenv(mg.TargetColorEnv); err != nil {
log.Fatal(err)
}
resetTerm()
return m.Run()
}

func resetTerm() {
if term, exists := os.LookupEnv("TERM"); exists == true {
log.Printf("Current terminal: %s", term)
// unset TERM env var in order to disable color output to make the tests simpler
// there is a specific test for colorized output, so all the other tests can use non-colorized one
if err := os.Unsetenv("TERM"); err != nil {
log.Fatal(err)
}
}
os.Setenv(mg.EnableColorEnv, "false")
}

func TestTransitiveDepCache(t *testing.T) {
cache, err := internal.OutputDebug("go", "env", "GOCACHE")
if err != nil {
Expand Down Expand Up @@ -292,6 +314,7 @@ func TestListMagefilesLib(t *testing.T) {
}

func TestMixedMageImports(t *testing.T) {
resetTerm()
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
inv := Invocation{
Expand Down Expand Up @@ -420,7 +443,82 @@ Targets:
}
}

var terminals = []struct {
code string
supportsColor bool
}{
{"", true},
{"vt100", false},
{"cygwin", false},
{"xterm-mono", false},
{"xterm", true},
{"xterm-vt220", true},
{"xterm-16color", true},
{"xterm-256color", true},
{"screen-256color", true},
}

func TestListWithColor(t *testing.T) {
os.Setenv(mg.EnableColorEnv, "true")
os.Setenv(mg.TargetColorEnv, mg.Cyan.String())

expectedPlainText := `
This is a comment on the package which should get turned into output with the list of targets.

Targets:
somePig* This is the synopsis for SomePig.
testVerbose

* default target
`[1:]

// NOTE: using the literal string would be complicated because I would need to break it
// in the middle and join with a normal string for the target names,
// otherwise the single backslash would be taken literally and encoded as \\
expectedColorizedText := "" +
"This is a comment on the package which should get turned into output with the list of targets.\n" +
"\n" +
"Targets:\n" +
" \x1b[36msomePig*\x1b[0m This is the synopsis for SomePig.\n" +
" \x1b[36mtestVerbose\x1b[0m \n" +
"\n" +
"* default target\n"

for _, terminal := range terminals {
t.Run(terminal.code, func(t *testing.T) {
os.Setenv("TERM", terminal.code)

stdout := &bytes.Buffer{}
inv := Invocation{
Dir: "./testdata/list",
Stdout: stdout,
Stderr: ioutil.Discard,
List: true,
}

code := Invoke(inv)
if code != 0 {
t.Errorf("expected to exit with code 0, but got %v", code)
}
actual := stdout.String()
var expected string
if terminal.supportsColor {
expected = expectedColorizedText
} else {
expected = expectedPlainText
}

if actual != expected {
t.Logf("expected: %q", expected)
t.Logf(" actual: %q", actual)
t.Fatalf("expected:\n%v\n\ngot:\n%v", expected, actual)
}
})
}
}

func TestNoArgNoDefaultList(t *testing.T) {
resetTerm()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
inv := Invocation{
Expand Down Expand Up @@ -458,6 +556,7 @@ func TestIgnoreDefault(t *testing.T) {
if err := os.Setenv(mg.IgnoreDefaultEnv, "1"); err != nil {
t.Fatal(err)
}
resetTerm()

code := Invoke(inv)
if code != 0 {
Expand Down Expand Up @@ -1286,6 +1385,7 @@ func TestGoCmd(t *testing.T) {
var runtimeVer = regexp.MustCompile(`go1\.([0-9]+)`)

func TestGoModules(t *testing.T) {
resetTerm()
matches := runtimeVer.FindStringSubmatch(runtime.Version())
if len(matches) < 2 || minorVer(t, matches[1]) < 11 {
t.Skipf("Skipping Go modules test because go version %q is less than go1.11", runtime.Version())
Expand Down
132 changes: 130 additions & 2 deletions mage/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,135 @@ Options:
fs.Usage()
return
}



// color is ANSI color type
type color int

// If you add/change/remove any items in this constant,
// you will need to run "stringer -type=color" in this directory again.
// NOTE: Please keep the list in an alphabetical order.
const (
black color = iota
red
green
yellow
blue
magenta
cyan
white
brightblack
brightred
brightgreen
brightyellow
brightblue
brightmagenta
brightcyan
brightwhite
)

// AnsiColor are ANSI color codes for supported terminal colors.
var ansiColor = map[color]string{
black: "\u001b[30m",
red: "\u001b[31m",
green: "\u001b[32m",
yellow: "\u001b[33m",
blue: "\u001b[34m",
magenta: "\u001b[35m",
cyan: "\u001b[36m",
white: "\u001b[37m",
brightblack: "\u001b[30;1m",
brightred: "\u001b[31;1m",
brightgreen: "\u001b[32;1m",
brightyellow: "\u001b[33;1m",
brightblue: "\u001b[34;1m",
brightmagenta: "\u001b[35;1m",
brightcyan: "\u001b[36;1m",
brightwhite: "\u001b[37;1m",
}

const _color_name = "blackredgreenyellowbluemagentacyanwhitebrightblackbrightredbrightgreenbrightyellowbrightbluebrightmagentabrightcyanbrightwhite"

var _color_index = [...]uint8{0, 5, 8, 13, 19, 23, 30, 34, 39, 50, 59, 70, 82, 92, 105, 115, 126}

colorToLowerString := func (i color) string {
if i < 0 || i >= color(len(_color_index)-1) {
return "color(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _color_name[_color_index[i]:_color_index[i+1]]
}

// ansiColorReset is an ANSI color code to reset the terminal color.
const ansiColorReset = "\033[0m"

// defaultTargetAnsiColor is a default ANSI color for colorizing targets.
// It is set to Cyan as an arbitrary color, because it has a neutral meaning
var defaultTargetAnsiColor = ansiColor[cyan]

getAnsiColor := func(color string) (string, bool) {
colorLower := strings.ToLower(color)
for k, v := range ansiColor {
colorConstLower := colorToLowerString(k)
if colorConstLower == colorLower {
return v, true
}
}
return "", false
}

// Terminals which don't support color:
// TERM=vt100
// TERM=cygwin
// TERM=xterm-mono
var noColorTerms = map[string]bool{
"vt100": false,
"cygwin": false,
"xterm-mono": false,
}

// terminalSupportsColor checks if the current console supports color output
//
// Supported:
// linux, mac, or windows's ConEmu, Cmder, putty, git-bash.exe, pwsh.exe
// Not supported:
// windows cmd.exe, powerShell.exe
terminalSupportsColor := func() bool {
envTerm := os.Getenv("TERM")
if _, ok := noColorTerms[envTerm]; ok {
return false
}
return true
}

// enableColor reports whether the user has requested to enable a color output.
enableColor := func() bool {
b, _ := strconv.ParseBool(os.Getenv("MAGEFILE_ENABLE_COLOR"))
return b
}

// targetColor returns the ANSI color which should be used to colorize targets.
targetColor := func() string {
s, exists := os.LookupEnv("MAGEFILE_TARGET_COLOR")
if exists == true {
if c, ok := getAnsiColor(s); ok == true {
return c
}
}
return defaultTargetAnsiColor
}

// store the color terminal variables, so that the detection isn't repeated for each target
var enableColorValue = enableColor() && terminalSupportsColor()
var targetColorValue = targetColor()

printName := func(str string) string {
if enableColorValue {
return fmt.Sprintf("%s%s%s", targetColorValue, str, ansiColorReset)
} else {
return str
}
}

list := func() error {
{{with .Description}}fmt.Println(` + "`{{.}}\n`" + `)
{{- end}}
Expand All @@ -117,7 +245,7 @@ Options:
fmt.Println("Targets:")
w := tabwriter.NewWriter(os.Stdout, 0, 4, 4, ' ', 0)
for _, name := range keys {
fmt.Fprintf(w, " %v\t%v\n", name, targets[name])
fmt.Fprintf(w, " %v\t%v\n", printName(name), targets[name])
}
err := w.Flush()
{{- if .DefaultFunc.Name}}
Expand Down
80 changes: 80 additions & 0 deletions mg/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package mg

// Color is ANSI color type
type Color int

// If you add/change/remove any items in this constant,
// you will need to run "stringer -type=Color" in this directory again.
// NOTE: Please keep the list in an alphabetical order.
const (
Black Color = iota
Red
Green
Yellow
Blue
Magenta
Cyan
White
BrightBlack
BrightRed
BrightGreen
BrightYellow
BrightBlue
BrightMagenta
BrightCyan
BrightWhite
)

// AnsiColor are ANSI color codes for supported terminal colors.
var ansiColor = map[Color]string{
Black: "\u001b[30m",
Red: "\u001b[31m",
Green: "\u001b[32m",
Yellow: "\u001b[33m",
Blue: "\u001b[34m",
Magenta: "\u001b[35m",
Cyan: "\u001b[36m",
White: "\u001b[37m",
BrightBlack: "\u001b[30;1m",
BrightRed: "\u001b[31;1m",
BrightGreen: "\u001b[32;1m",
BrightYellow: "\u001b[33;1m",
BrightBlue: "\u001b[34;1m",
BrightMagenta: "\u001b[35;1m",
BrightCyan: "\u001b[36;1m",
BrightWhite: "\u001b[37;1m",
}

// AnsiColorReset is an ANSI color code to reset the terminal color.
const AnsiColorReset = "\033[0m"

// DefaultTargetAnsiColor is a default ANSI color for colorizing targets.
// It is set to Cyan as an arbitrary color, because it has a neutral meaning
var DefaultTargetAnsiColor = ansiColor[Cyan]

func toLowerCase(s string) string {
// this is a naive implementation
// borrowed from https://golang.org/src/strings/strings.go
// and only considers alphabetical characters [a-zA-Z]
// so that we don't depend on the "strings" package
buf := make([]byte, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
if 'A' <= c && c <= 'Z' {
c += 'a' - 'A'
}
buf[i] = c
}
return string(buf)
}

func getAnsiColor(color string) (string, bool) {
colorLower := toLowerCase(color)
for k, v := range ansiColor {
colorConstLower := toLowerCase(k.String())
if colorConstLower == colorLower {
return v, true
}
}
return "", false
}
Loading