diff --git a/main.go b/main.go index f026abd..16d9103 100644 --- a/main.go +++ b/main.go @@ -136,7 +136,7 @@ func main() { case "auto", "": // do nothing default: - log.Fatal("unsupported color mode: use always, never, auto") + log.Fatalf("unsupported color mode: %s. Expected always, auto, or never", colorMode) } if showVersion { diff --git a/runner/config.go b/runner/config.go index 583c018..9ebe24e 100644 --- a/runner/config.go +++ b/runner/config.go @@ -15,6 +15,7 @@ import ( "time" "dario.cat/mergo" + "github.com/fatih/color" "github.com/pelletier/go-toml" ) @@ -163,6 +164,7 @@ type cfgColor struct { Watcher string `toml:"watcher" usage:"Customize watcher part's color"` Build string `toml:"build" usage:"Customize build part's color"` Runner string `toml:"runner" usage:"Customize runner part's color"` + Mode string `toml:"mode" usage:"Colorized output mode, one of always, auto, or never. Defaults to auto"` App string `toml:"app"` } @@ -625,6 +627,19 @@ func (c *Config) preprocess(args map[string]TomlInfo) error { } c.Build.ExcludeDir = ed + + // Set colorful output, see https://github.com/fatih/color#disableenable-color + switch c.Color.Mode { + case "always": + color.NoColor = false + case "never": + color.NoColor = true + case "auto", "": + break + default: + return fmt.Errorf("unsupported color mode: %s. Expected always, auto, or never", c.Color.Mode) + } + if len(c.Build.FullBin) > 0 { c.Build.Bin = c.Build.FullBin return err diff --git a/runner/config_test.go b/runner/config_test.go index 2d5d5fe..267e592 100644 --- a/runner/config_test.go +++ b/runner/config_test.go @@ -10,6 +10,8 @@ import ( "strings" "testing" "time" + + "github.com/fatih/color" ) const ( @@ -1119,3 +1121,63 @@ cmd = "go build -o ./tmp/main ." } }) } + +func TestColorMode(t *testing.T) { + // Save and restore the global NoColor state so tests don't bleed into each other. + original := color.NoColor + t.Cleanup(func() { color.NoColor = original }) + + cases := []struct { + name string + mode string + wantNoColor bool + wantErr bool + }{ + {name: "always enables color", mode: "always", wantNoColor: false}, + {name: "never disables color", mode: "never", wantNoColor: true}, + {name: "auto leaves default", mode: "auto", wantNoColor: original}, + {name: "empty leaves default", mode: "", wantNoColor: original}, + {name: "invalid returns error", mode: "rainbow", wantErr: true}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + color.NoColor = original + cfg := defaultConfig() + cfg.Color.Mode = tc.mode + err := cfg.preprocess(nil) + if tc.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "unsupported color mode") { + t.Fatalf("unexpected error message: %v", err) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if color.NoColor != tc.wantNoColor { + t.Fatalf("color.NoColor = %v, want %v", color.NoColor, tc.wantNoColor) + } + }) + } +} + +func TestColorModeWithFullBin(t *testing.T) { + // Regression: color mode must be applied even when build.full_bin is set, + // because preprocess returns early after FullBin is processed. + original := color.NoColor + t.Cleanup(func() { color.NoColor = original }) + + cfg := defaultConfig() + cfg.Build.FullBin = "./tmp/main" + cfg.Color.Mode = "never" + if err := cfg.preprocess(nil); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !color.NoColor { + t.Fatal("color.NoColor should be true when mode=never and full_bin is set") + } +}