├── go.mod ├── osutils.go ├── example_test.go ├── go.sum ├── colorlevels.go ├── .github └── workflows │ └── ci.yaml ├── colorlevel_string.go ├── pkg └── hasFlag │ ├── hasFlag.go │ ├── hasFlag_test.go │ └── LICENSE ├── osutils_windows.go ├── di.go ├── LICENSE ├── Makefile ├── README.md ├── supportscolor.go └── supportscolor_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jwalton/go-supportscolor 2 | 3 | go 1.15 4 | 5 | require ( 6 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 7 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d 8 | ) 9 | -------------------------------------------------------------------------------- /osutils.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package supportscolor 4 | 5 | func getWindowsVersion() (majorVersion, minorVersion, buildNumber uint32) { 6 | return 0, 0, 0 7 | } 8 | 9 | // enableColor will enable color in the terminal. Returns true if color was 10 | // enabled, false otherwise. 11 | func enableColor() bool { 12 | return true 13 | } 14 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package supportscolor_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jwalton/go-supportscolor" 7 | ) 8 | 9 | func ExampleSupportsColor() { 10 | if supportscolor.Stdout().SupportsColor { 11 | fmt.Println("\u001b[31mThis is Red!\u001b[39m") 12 | } else { 13 | fmt.Println("This is not red.") 14 | } 15 | 16 | // Output: This is not red. 17 | } 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 2 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE= 3 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 4 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= 5 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 6 | -------------------------------------------------------------------------------- /colorlevels.go: -------------------------------------------------------------------------------- 1 | package supportscolor 2 | 3 | //go:generate stringer -type=ColorLevel 4 | 5 | // ColorLevel represents the ANSI color level supported by the terminal. 6 | type ColorLevel int 7 | 8 | const ( 9 | // None represents a terminal that does not support color at all. 10 | None ColorLevel = 0 11 | // Basic represents a terminal with basic 16 color support. 12 | Basic ColorLevel = 1 13 | // Ansi256 represents a terminal with 256 color support. 14 | Ansi256 ColorLevel = 2 15 | // Ansi16m represents a terminal with full true color support. 16 | Ansi16m ColorLevel = 3 17 | ) 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: 7 | - "*" 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.16' 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v3.6.0 22 | with: 23 | version: v1.53 24 | - name: golint 25 | uses: Jerome1337/golint-action@v1.0.3 26 | - name: Test 27 | run: go test ./... 28 | -------------------------------------------------------------------------------- /colorlevel_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ColorLevel"; DO NOT EDIT. 2 | 3 | package supportscolor 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[None-0] 12 | _ = x[Basic-1] 13 | _ = x[Ansi256-2] 14 | _ = x[Ansi16m-3] 15 | } 16 | 17 | const _ColorLevel_name = "NoneBasicAnsi256Ansi16m" 18 | 19 | var _ColorLevel_index = [...]uint8{0, 4, 9, 16, 23} 20 | 21 | func (i ColorLevel) String() string { 22 | if i < 0 || i >= ColorLevel(len(_ColorLevel_index)-1) { 23 | return "ColorLevel(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _ColorLevel_name[_ColorLevel_index[i]:_ColorLevel_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /pkg/hasFlag/hasFlag.go: -------------------------------------------------------------------------------- 1 | // Package hasflag checks if `os.Args` has a specific flag. Correctly stops looking 2 | // after an -- argument terminator. 3 | // 4 | // Ported from https://github.com/sindresorhus/has-flag 5 | // 6 | package hasflag 7 | 8 | import ( 9 | "os" 10 | "strings" 11 | ) 12 | 13 | func argIndexOf(argv []string, str string) int { 14 | result := -1 15 | for index, arg := range argv { 16 | if arg == str { 17 | result = index 18 | break 19 | } 20 | } 21 | 22 | return result 23 | } 24 | 25 | // HasFlag checks to see if the given flag was supplied on the command line. 26 | // 27 | func HasFlag(flag string) bool { 28 | return hasFlag(flag, os.Args[1:]) 29 | } 30 | 31 | func hasFlag(flag string, argv []string) bool { 32 | var prefix string 33 | 34 | if strings.HasPrefix(flag, "-") { 35 | prefix = "" 36 | } else if len(flag) == 1 { 37 | prefix = "-" 38 | } else { 39 | prefix = "--" 40 | } 41 | 42 | position := argIndexOf(argv, prefix+flag) 43 | terminatorPosition := argIndexOf(argv, "--") 44 | return position != -1 && (terminatorPosition == -1 || position < terminatorPosition) 45 | } 46 | -------------------------------------------------------------------------------- /osutils_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package supportscolor 4 | 5 | import ( 6 | "golang.org/x/sys/windows" 7 | ) 8 | 9 | func getWindowsVersion() (majorVersion, minorVersion, buildNumber uint32) { 10 | return windows.RtlGetNtVersionNumbers() 11 | } 12 | 13 | func enableColor() bool { 14 | handle, err := windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) 15 | if err != nil { 16 | return false 17 | } 18 | 19 | // Get the existing console mode. 20 | var mode uint32 21 | err = windows.GetConsoleMode(handle, &mode) 22 | if err != nil { 23 | return false 24 | } 25 | 26 | // If ENABLE_VIRTUAL_TERMINAL_PROCESSING is not set, then set it. This will 27 | // enable native ANSI color support from Windows. 28 | if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING { 29 | // Enable color. 30 | // See https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences. 31 | mode = mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING 32 | err = windows.SetConsoleMode(handle, mode) 33 | if err != nil { 34 | return false 35 | } 36 | } 37 | 38 | return true 39 | } 40 | -------------------------------------------------------------------------------- /pkg/hasFlag/hasFlag_test.go: -------------------------------------------------------------------------------- 1 | package hasflag 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func checkHasFlag(t *testing.T, expected bool, flag string, argv []string) { 8 | if hasFlag(flag, argv) != expected { 9 | t.Errorf("Expected %v, got %v for flag %v in %v", expected, !expected, flag, argv) 10 | } 11 | } 12 | 13 | func TestHasFlag(t *testing.T) { 14 | checkHasFlag(t, true, "unicorn", []string{"--foo", "--unicorn", "--bar"}) 15 | // Optional prefix. 16 | checkHasFlag(t, true, "--unicorn", []string{"--foo", "--unicorn", "--bar"}) 17 | checkHasFlag(t, true, "unicorn=rainbow", []string{"--foo", "--unicorn=rainbow", "--bar"}) 18 | checkHasFlag(t, true, "unicorn", []string{"--unicorn", "--", "--foo"}) 19 | // Don't match flags after terminator. 20 | checkHasFlag(t, false, "unicorn", []string{"--foo", "--", "--unicorn"}) 21 | checkHasFlag(t, false, "unicorn", []string{"--foo"}) 22 | checkHasFlag(t, true, "-u", []string{"-f", "-u", "-b"}) 23 | checkHasFlag(t, true, "-u", []string{"-u", "--", "-f"}) 24 | checkHasFlag(t, true, "u", []string{"-f", "-u", "-b"}) 25 | checkHasFlag(t, true, "u", []string{"-u", "--", "-f"}) 26 | checkHasFlag(t, false, "f", []string{"-u", "--", "-f"}) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/hasFlag/LICENSE: -------------------------------------------------------------------------------- 1 | The files in this folder were ported from https://github.com/sindresorhus/has-flag, which had the following copyright notice: 2 | 3 | MIT License 4 | 5 | Copyright (c) Sindre Sorhus (sindresorhus.com) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /di.go: -------------------------------------------------------------------------------- 1 | package supportscolor 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | hasflag "github.com/jwalton/go-supportscolor/pkg/hasFlag" 8 | "golang.org/x/term" 9 | ) 10 | 11 | type environment interface { 12 | LookupEnv(name string) (string, bool) 13 | Getenv(name string) string 14 | HasFlag(name string) bool 15 | IsTerminal(fd int) bool 16 | getWindowsVersion() (majorVersion, minorVersion, buildNumber uint32) 17 | osEnableColor() bool 18 | getGOOS() string 19 | } 20 | 21 | type defaultEnvironmentType struct{} 22 | 23 | func (*defaultEnvironmentType) LookupEnv(name string) (string, bool) { 24 | return os.LookupEnv(name) 25 | } 26 | 27 | func (*defaultEnvironmentType) Getenv(name string) string { 28 | return os.Getenv(name) 29 | } 30 | 31 | func (*defaultEnvironmentType) HasFlag(flag string) bool { 32 | return hasflag.HasFlag(flag) 33 | } 34 | 35 | func (*defaultEnvironmentType) IsTerminal(fd int) bool { 36 | // TODO: Replace with github.com/mattn/go-isatty? 37 | return term.IsTerminal(int(fd)) 38 | } 39 | 40 | func (*defaultEnvironmentType) getWindowsVersion() (majorVersion, minorVersion, buildNumber uint32) { 41 | return getWindowsVersion() 42 | } 43 | 44 | func (*defaultEnvironmentType) osEnableColor() bool { 45 | return enableColor() 46 | } 47 | 48 | func (*defaultEnvironmentType) getGOOS() string { 49 | return runtime.GOOS 50 | } 51 | 52 | var defaultEnvironment = defaultEnvironmentType{} 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Jason Walton (https://www.thedreaming.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | 12 | 13 | This software was ported from https://github.com/chalk/supports-color/, which has the following license: 14 | 15 | MIT License 16 | 17 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Adapted from https://github.com/vincentbernat/hellogopher 2 | 3 | MODULE = $(shell env GO111MODULE=on $(GO) list -m) 4 | DATE ?= $(shell date +%FT%T%z) 5 | VERSION ?= $(shell git describe --tags --always --dirty --match=v* | cut -c2- 2> /dev/null || \ 6 | cat $(CURDIR)/.version 2> /dev/null || echo v0) 7 | COMMIT ?= $(shell git rev-parse HEAD) 8 | PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) 9 | TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \ 10 | '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ 11 | $(PKGS)) 12 | GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/) 13 | 14 | GO = go 15 | GOLINT = golint 16 | GOLANGCILINT = golangci-lint 17 | GOCOV = gocov 18 | GOCOVXML = gocov-xml 19 | 20 | TIMEOUT = 15 21 | V = 0 22 | Q = $(if $(filter 1,$V),,@) 23 | # Prompt shown before each item 24 | M = $(shell printf "\033[34;1m▶\033[0m") 25 | 26 | export GO111MODULE=on 27 | 28 | .PHONY: all 29 | all: fmt lint test ## Build, format, and test 30 | 31 | # Tests 32 | 33 | TEST_TARGETS := test-default test-bench test-short test-verbose test-race 34 | .PHONY: $(TEST_TARGETS) check test tests 35 | test-bench: ARGS=-run=__absolutelynothing__ -bench=. ## Run benchmarks 36 | test-short: ARGS=-short ## Run only short tests 37 | test-verbose: ARGS=-v ## Run tests in verbose mode with coverage reporting 38 | test-race: ARGS=-race ## Run tests with race detector 39 | $(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%) 40 | $(TEST_TARGETS): test 41 | test: lint ; $(info $(M) running $(NAME:%=% )tests…) @ ## Run tests 42 | $Q $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS) 43 | 44 | COVERAGE_MODE = atomic 45 | COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out 46 | COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml 47 | COVERAGE_HTML = $(COVERAGE_DIR)/index.html 48 | .PHONY: test-coverage test-coverage-tools 49 | test-coverage-tools: | $(GOCOV) $(GOCOVXML) 50 | test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 51 | test-coverage: lint test-coverage-tools ; $(info $(M) running coverage tests…) @ ## Run coverage tests 52 | $Q mkdir -p $(COVERAGE_DIR) 53 | $Q $(GO) test \ 54 | -coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $(TESTPKGS) | \ 55 | grep '^$(MODULE)/' | \ 56 | tr '\n' ',' | sed 's/,$$//') \ 57 | -covermode=$(COVERAGE_MODE) \ 58 | -coverprofile="$(COVERAGE_PROFILE)" $(TESTPKGS) 59 | $Q $(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) 60 | $Q $(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML) 61 | 62 | .PHONY: lint 63 | lint: golint golangci-lint ## Run linters 64 | 65 | golint: | ; $(info $(M) running golint…) @ ## Run golint 66 | $Q $(GOLINT) -set_exit_status ./... 67 | 68 | .PHONY: golangci-lint 69 | golangci-lint: | ; $(info $(M) running golangci-lint…) @ ## Run golangci-lint 70 | $Q $(GOLANGCILINT) run 71 | 72 | .PHONY: fmt 73 | fmt: ; $(info $(M) running gofmt…) @ ## Run gofmt on all source files 74 | $Q $(GO) fmt $(PKGS) 75 | 76 | # Misc 77 | 78 | .PHONY: help 79 | help: 80 | @grep -hE '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 81 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-17s\033[0m %s\n", $$1, $$2}' 82 | 83 | .PHONY: version 84 | version: 85 | @echo $(VERSION) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # supports-color 2 | 3 | Go library to detect whether a terminal supports color, and enables ANSI color support in recent Windows 10 builds. 4 | 5 | This is a port of the Node.js package [supports-color](https://github.com/chalk/supports-color) v8.1.1 by [Sindre Sorhus](https://github.com/sindresorhus) and [Josh Junon](https://github.com/qix-). 6 | 7 | ## Install 8 | 9 | ```sh 10 | $ go get github.com/jwalton/go-supportscolor 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```go 16 | import ( 17 | "fmt" 18 | "github.com/jwalton/go-supportscolor" 19 | ) 20 | 21 | if supportscolor.Stdout().SupportsColor { 22 | fmt.Println("Terminal stdout supports color") 23 | } 24 | 25 | if supportscolor.Stdout().Has256 { 26 | fmt.Println("Terminal stdout supports 256 colors") 27 | } 28 | 29 | if supportscolor.Stderr().Has16m { 30 | fmt.Println("Terminal stderr supports 16 million colors (true color)") 31 | } 32 | ``` 33 | 34 | ## Windows 10 Support 35 | 36 | `supportscolor` is cross-platform, and will work on Linux and MacOS systems, but will also work on Windows 10. 37 | 38 | Many ANSI color libraries for Go do a poor job of handling colors in Windows. This is because historically, Windows has not supported ANSI color codes, so hacks like [ansicon](https://github.com/adoxa/ansicon) or [go-colorable](https://github.com/mattn/go-colorable) were required. However, Windows 10 has supported ANSI escape codes since 2017 (build 10586 for 256 color support, and build 14931 for 16.7 million true color support). In [Windows Terminal](https://github.com/Microsoft/Terminal) this is enabled by default, but in `CMD.EXE` or PowerShell, ANSI support must be enabled via [`ENABLE_VIRTUAL_TERMINAL_PROCESSING`](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences). 39 | 40 | This library takes care of all of this for you, though - if you call `supportscolor.Stdout()` on a modern build of Windows 10, it will set the `ENABLE_VIRTUAL_TERMINAL_PROCESSING` console mode automatically if required, and return the correct color level, and then you can just write ANSI escape codes to stdout and not worry about it. If someone uses your app on an old version of Windows, this will return `SupportsColor == false`, and you can write black and white to stdout. 41 | 42 | ## API 43 | 44 | Returns a `supportscolor.Support` with a `Stdout()` and `Stderr()` function for testing either stream. (There's one for stdout and one for stderr, because if you run `mycmd > foo.txt` then stdout would be redirected to a file, and since it would not be a TTY would not have color support, while stderr would still be going to the console and would have color support.) 45 | 46 | The `Stdout()`/`Stderr()` objects specify a level of support for color through a `.Level` property and a corresponding flag: 47 | 48 | - `.Level = None` and `.SupportsColor = false`: No color support 49 | - `.Level = Basic` and `.SupportsColor = true`: Basic color support (16 colors) 50 | - `.Level = Ansi256` and `.Has256 = true`: 256 color support 51 | - `.Level = Ansi16m` and `.Has16m = true`: True color support (16 million colors) 52 | 53 | ### `supportscolor.SupportsColor(fd, ...options)` 54 | 55 | Additionally, `supportscolor` exposes the `.SupportsColor()` function that takes an arbitrary file descriptor (e.g. `os.Stdout.Fd()`) and options and will (re-)evaluate color support for an arbitrary stream. 56 | 57 | For example, `supportscolor.Stdout()` is the equivalent of `supportscolor.SupportsColor(os.Stdout.Fd())`. 58 | 59 | Available options are: 60 | 61 | - `supportscolor.IsTTYOption(isTTY bool)` - Force whether the given file should be considered a TTY or not. If this not specified, TTY status will be detected automatically via `term.IsTerminal()`. 62 | - `supportscolor.SniffFlagsOption(sniffFlags bool)` - By default it is `true`, which instructs `SupportsColor()` to sniff `os.Args` for the multitude of `--color` flags (see _Info_ below). If `false`, then `os.Args` is not considered when determining color support. 63 | 64 | ## Info 65 | 66 | By default, supportscolor checks `os.Args` for the `--color` and `--no-color` CLI flags. 67 | 68 | For situations where using `--color` is not possible, use the environment variable `FORCE_COLOR=1` (level 1 - 16 colors), `FORCE_COLOR=2` (level 2 - 256 colors), or `FORCE_COLOR=3` (level 3 - true color) to forcefully enable color, or `FORCE_COLOR=0` to forcefully disable. The use of `FORCE_COLOR` overrides all other color support checks. 69 | 70 | If `NO_COLOR` is specified and `FORCE_COLOR` is not, then colors will be disabled. 71 | 72 | Explicit 256/True color mode can be enabled using the `--color=256` and `--color=16m` flags, respectively. 73 | -------------------------------------------------------------------------------- /supportscolor.go: -------------------------------------------------------------------------------- 1 | // Package supportscolor detects whether a terminal supports color, and enables ANSI color support in recent Windows 10 builds. 2 | // 3 | // This is a port of the Node.js package supports-color (https://github.com/chalk/supports-color) by 4 | // Sindre Sorhus and Josh Junon. 5 | // 6 | // Returns a `supportscolor.Support` with a `Stdout()` and `Stderr()` function for 7 | // testing either stream. Note that on recent Windows 10 machines, these 8 | // functions will also set the `ENABLE_VIRTUAL_TERMINAL_PROCESSING` console mode 9 | // if required, which will enable support for normal ANSI escape codes on stdout 10 | // and stderr. 11 | // 12 | // The `Stdout()`/`Stderr()` objects specify a level of support for color through 13 | // a `.Level` property and a corresponding flag: 14 | // 15 | // - `.Level = None` and `.SupportsColor = false`: No color support 16 | // - `.Level = Basic` and `.SupportsColor = true`: Basic color support (16 colors) 17 | // - `.Level = Ansi256` and `.Has256 = true`: 256 color support 18 | // - `.Level = Ansi16m` and `.Has16m = true`: True color support (16 million colors) 19 | // 20 | // Additionally, `supportscolor` exposes the `.SupportsColor()` function that 21 | // takes an arbitrary file descriptor (e.g. `os.Stdout.Fd()`) and options, and will 22 | // (re-)evaluate color support for an arbitrary stream. 23 | // 24 | // For example, `supportscolor.Stdout()` is the equivalent of `supportscolor.SupportsColor(os.Stdout.Fd())`. 25 | // 26 | // Available options are: 27 | // 28 | // `supportscolor.IsTTYOption(isTTY bool)` - Force whether the given file 29 | // should be considered a TTY or not. If this not specified, TTY status will 30 | // be detected automatically via `term.IsTerminal()`. 31 | // 32 | // `supportscolor.SniffFlagsOption(sniffFlags bool)` - By default it is `true`, 33 | // which instructs `SupportsColor()` to sniff `os.Args` for the multitude of 34 | // `--color` flags (see Info section in README.md). If `false`, then `os.Args` 35 | // is not considered when determining color support. 36 | // 37 | package supportscolor 38 | 39 | import ( 40 | "os" 41 | "regexp" 42 | "strconv" 43 | "strings" 44 | ) 45 | 46 | // Support represents the color support available. 47 | // 48 | // Level will be the supported ColorLevel. SupportsColor will be true if the 49 | // terminal supports basic 16 color ANSI color escape codes. Has256 will be 50 | // true if the terminal supports ANSI 256 color, and Has16m will be true if the 51 | // terminal supports true color. 52 | // 53 | type Support struct { 54 | Level ColorLevel 55 | SupportsColor bool 56 | Has256 bool 57 | Has16m bool 58 | } 59 | 60 | func checkForceColorFlags(env environment) *ColorLevel { 61 | var flagForceColor ColorLevel = None 62 | var flagForceColorPreset bool = false 63 | // TODO: It would be very nice if `HasFlag` supported `--color false`. 64 | if env.HasFlag("no-color") || 65 | env.HasFlag("no-colors") || 66 | env.HasFlag("color=false") || 67 | env.HasFlag("color=never") { 68 | flagForceColor = None 69 | flagForceColorPreset = true 70 | } else if env.HasFlag("color") || 71 | env.HasFlag("colors") || 72 | env.HasFlag("color=true") || 73 | env.HasFlag("color=always") { 74 | flagForceColor = Basic 75 | flagForceColorPreset = true 76 | } 77 | 78 | if flagForceColorPreset { 79 | return &flagForceColor 80 | } 81 | return nil 82 | } 83 | 84 | func checkForceColorEnv(env environment) *ColorLevel { 85 | forceColor, present := env.LookupEnv("FORCE_COLOR") 86 | if present { 87 | if forceColor == "true" || forceColor == "" { 88 | result := Basic 89 | return &result 90 | } 91 | 92 | if forceColor == "false" { 93 | result := None 94 | return &result 95 | } 96 | 97 | forceColorInt, err := strconv.ParseInt(forceColor, 10, 8) 98 | if err == nil { 99 | var result ColorLevel 100 | if forceColorInt <= 0 { 101 | result = None 102 | } else if forceColorInt >= 3 { 103 | result = Ansi16m 104 | } else { 105 | result = ColorLevel(forceColorInt) 106 | } 107 | return &result 108 | } 109 | } 110 | 111 | if _, isNoColor := env.LookupEnv("NO_COLOR"); isNoColor { 112 | result := None 113 | return &result 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func translateLevel(level ColorLevel) Support { 120 | return Support{ 121 | Level: level, 122 | SupportsColor: level >= 1, 123 | Has256: level >= 2, 124 | Has16m: level >= 3, 125 | } 126 | } 127 | 128 | func supportsColor(config *configuration) ColorLevel { 129 | env := config.env 130 | 131 | // TODO: We don't have to call `checkForceColorFlags` multiple times, 132 | // as it's not common practice to modify `os.Args`. We can call it once 133 | // and cache the result, in say `init()`. 134 | flagForceColor := checkForceColorFlags(env) 135 | noFlagForceColor := checkForceColorEnv(env) 136 | 137 | // Env preferences should override flags 138 | if noFlagForceColor != nil { 139 | flagForceColor = noFlagForceColor 140 | } 141 | 142 | var forceColor *ColorLevel 143 | if config.sniffFlags { 144 | forceColor = flagForceColor 145 | } else { 146 | forceColor = noFlagForceColor 147 | } 148 | 149 | if forceColor != nil && *forceColor == None { 150 | return None 151 | } 152 | 153 | if config.sniffFlags { 154 | if env.HasFlag("color=16m") || 155 | env.HasFlag("color=full") || 156 | env.HasFlag("color=truecolor") { 157 | env.osEnableColor() 158 | return Ansi16m 159 | } 160 | 161 | if env.HasFlag("color=256") { 162 | env.osEnableColor() 163 | return Ansi256 164 | } 165 | } 166 | 167 | if !config.isTTY && forceColor == nil { 168 | return None 169 | } 170 | 171 | min := None 172 | if forceColor != nil { 173 | min = *forceColor 174 | } 175 | 176 | term := env.Getenv("TERM") 177 | if term == "dumb" { 178 | return min 179 | } 180 | 181 | osColorEnabled := env.osEnableColor() 182 | if (!osColorEnabled) && forceColor == nil { 183 | return None 184 | } 185 | 186 | if env.getGOOS() == "windows" { 187 | // If we couldn't get windows to enable color, return basic. 188 | if !osColorEnabled { 189 | return None 190 | } 191 | 192 | // Windows 10 build 10586 is the first Windows release that supports 256 colors. 193 | // Windows 10 build 14931 is the first release that supports 16m/True color. 194 | major, minor, build := env.getWindowsVersion() 195 | if (major == 10 && minor >= 1) || major > 10 { 196 | // Optimistically hope that future versions of windows won't backslide. 197 | return Ansi16m 198 | } else if major >= 10 && build >= 14931 { 199 | return Ansi16m 200 | } else if major >= 10 && build >= 10586 { 201 | return Ansi256 202 | } 203 | 204 | // We should be able to return Basic here - if the terminal doesn't support 205 | // basic ANSI escape codes, we should have gotten false from `osEnableColor()` 206 | // because we should have gotten an error when we tried to set 207 | // ENABLE_VIRTUAL_TERMINAL_PROCESSING. 208 | // TODO: Make sure this is really true on an old version of Windows. 209 | return Basic 210 | } 211 | 212 | if _, ci := env.LookupEnv("CI"); ci { 213 | var trueColorEnvNames = []string{"GITHUB_ACTIONS", "GITEA_ACTIONS"} 214 | for _, trueColorEnvName := range trueColorEnvNames { 215 | _, exists := env.LookupEnv(trueColorEnvName) 216 | if exists { 217 | return Ansi16m 218 | } 219 | } 220 | 221 | var ciEnvNames = []string{"TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"} 222 | for _, ciEnvName := range ciEnvNames { 223 | _, exists := env.LookupEnv(ciEnvName) 224 | if exists { 225 | return Basic 226 | } 227 | } 228 | 229 | if env.Getenv("CI_NAME") == "codeship" { 230 | return Basic 231 | } 232 | 233 | return min 234 | } 235 | 236 | if teamCityVersion, isTeamCity := env.LookupEnv("TEAMCITY_VERSION"); isTeamCity { 237 | versionRegex := regexp.MustCompile(`^(9\.(0*[1-9]\d*)\.|\d{2,}\.)`) 238 | if versionRegex.MatchString(teamCityVersion) { 239 | return Basic 240 | } 241 | return None 242 | } 243 | 244 | if env.Getenv("COLORTERM") == "truecolor" { 245 | return Ansi16m 246 | } 247 | 248 | termProgram, termProgramPreset := env.LookupEnv("TERM_PROGRAM") 249 | if termProgramPreset { 250 | switch termProgram { 251 | case "iTerm.app": 252 | termProgramVersion := strings.Split(env.Getenv("TERM_PROGRAM_VERSION"), ".") 253 | version, err := strconv.ParseInt(termProgramVersion[0], 10, 64) 254 | if err == nil && version >= 3 { 255 | return Ansi16m 256 | } 257 | return Ansi256 258 | case "Apple_Terminal": 259 | return Ansi256 260 | 261 | default: 262 | // No default 263 | } 264 | } 265 | 266 | var term256Regex = regexp.MustCompile("(?i)-256(color)?$") 267 | if term256Regex.MatchString(term) { 268 | return Ansi256 269 | } 270 | 271 | var termBasicRegex = regexp.MustCompile("(?i)^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux") 272 | 273 | if termBasicRegex.MatchString(term) { 274 | return Basic 275 | } 276 | 277 | if _, colorTerm := env.LookupEnv("COLORTERM"); colorTerm { 278 | return Basic 279 | } 280 | 281 | return min 282 | } 283 | 284 | type configuration struct { 285 | isTTY bool 286 | forceIsTTY bool 287 | sniffFlags bool 288 | env environment 289 | } 290 | 291 | // Option is the type for an option which can be passed to SupportsColor(). 292 | type Option func(*configuration) 293 | 294 | // IsTTYOption is an option which can be passed to `SupportsColor` to force 295 | // whether the given file should be considered a TTY or not. If this not 296 | // specified, TTY status will be detected automatically via `term.IsTerminal()`. 297 | func IsTTYOption(isTTY bool) Option { 298 | return func(config *configuration) { 299 | config.forceIsTTY = true 300 | config.isTTY = isTTY 301 | } 302 | } 303 | 304 | func setEnvironment(env environment) Option { 305 | return func(config *configuration) { 306 | config.env = env 307 | } 308 | } 309 | 310 | // SniffFlagsOption can be passed to SupportsColor to enable or disable checking 311 | // command line flags to force supporting color. If set true (the default), then 312 | // the following flags will disable color support: 313 | // 314 | // --no-color 315 | // --no-colors 316 | // --color=false 317 | // --color=never 318 | // 319 | // And the following will force color support 320 | // 321 | // --colors 322 | // --color=true 323 | // --color=always 324 | // --color=256 // Ansi 256 color mode 325 | // --color=16m // 16.7 million color support 326 | // --color=full // 16.7 million color support 327 | // --color=truecolor // 16.7 million color support 328 | // 329 | func SniffFlagsOption(sniffFlags bool) Option { 330 | return func(config *configuration) { 331 | config.sniffFlags = sniffFlags 332 | } 333 | } 334 | 335 | // SupportsColor returns color support information for the given file handle. 336 | func SupportsColor(fd uintptr, options ...Option) Support { 337 | config := configuration{sniffFlags: true, env: &defaultEnvironment} 338 | for _, opt := range options { 339 | opt(&config) 340 | } 341 | 342 | if !config.forceIsTTY { 343 | config.isTTY = config.env.IsTerminal(int(fd)) 344 | } 345 | 346 | level := supportsColor(&config) 347 | return translateLevel(level) 348 | } 349 | 350 | var stdout *Support 351 | 352 | // Stdout returns color support information for os.Stdout. 353 | func Stdout() Support { 354 | if stdout == nil { 355 | result := SupportsColor(os.Stdout.Fd()) 356 | stdout = &result 357 | } 358 | return *stdout 359 | } 360 | 361 | var stderr *Support 362 | 363 | // Stderr returns color support information for os.Stderr. 364 | func Stderr() Support { 365 | if stderr == nil { 366 | result := SupportsColor(os.Stderr.Fd()) 367 | stderr = &result 368 | } 369 | return *stderr 370 | } 371 | -------------------------------------------------------------------------------- /supportscolor_test.go: -------------------------------------------------------------------------------- 1 | package supportscolor 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | type testEnvironment struct { 9 | flags []string 10 | env map[string]string 11 | isNotTerminal bool 12 | winMajorVersion uint32 13 | winMinorVersion uint32 14 | winBuildNumber uint32 15 | colorCantBeEnabled bool 16 | colorWasEnabled bool 17 | goos string 18 | } 19 | 20 | func (test *testEnvironment) LookupEnv(name string) (string, bool) { 21 | val, present := test.env[name] 22 | return val, present 23 | } 24 | 25 | func (test *testEnvironment) Getenv(name string) string { 26 | return test.env[name] 27 | } 28 | 29 | func (test *testEnvironment) HasFlag(flag string) bool { 30 | for _, f := range test.flags { 31 | if f == flag { 32 | return true 33 | } 34 | } 35 | return false 36 | } 37 | 38 | func (test *testEnvironment) IsTerminal(fd int) bool { 39 | return !test.isNotTerminal 40 | } 41 | 42 | func (test *testEnvironment) getWindowsVersion() (majorVersion, minorVersion, buildNumber uint32) { 43 | return test.winMajorVersion, test.winMinorVersion, test.winBuildNumber 44 | } 45 | 46 | func (test *testEnvironment) osEnableColor() bool { 47 | test.colorWasEnabled = true 48 | return !test.colorCantBeEnabled 49 | } 50 | 51 | func (test *testEnvironment) getGOOS() string { 52 | if test.goos != "" { 53 | return test.goos 54 | } 55 | return runtime.GOOS 56 | } 57 | 58 | func TestReturnBasicIfForceColorAndNotTty(t *testing.T) { 59 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 60 | env: map[string]string{"FORCE_COLOR": "true"}, 61 | isNotTerminal: true, 62 | })) 63 | 64 | if result.Level != Basic { 65 | t.Errorf("Expected %v, got %v", Basic, result.Level) 66 | } 67 | } 68 | 69 | func TestForceColorAnd256Flag(t *testing.T) { 70 | // return true if `FORCE_COLOR` is in env, but honor 256 flag 71 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 72 | env: map[string]string{"FORCE_COLOR": "true"}, 73 | flags: []string{"color=256"}, 74 | })) 75 | 76 | if result.Level != Ansi256 { 77 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 78 | } 79 | 80 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 81 | env: map[string]string{"FORCE_COLOR": "1"}, 82 | flags: []string{"color=256"}, 83 | })) 84 | 85 | if result.Level != Ansi256 { 86 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 87 | } 88 | } 89 | 90 | func TestForceColorIs0(t *testing.T) { 91 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 92 | env: map[string]string{"FORCE_COLOR": "0"}, 93 | })) 94 | 95 | if result.Level != None { 96 | t.Errorf("Expected %v, got %v", None, result.Level) 97 | } 98 | } 99 | 100 | func TestDoNotCacheForceColor(t *testing.T) { 101 | env := map[string]string{"FORCE_COLOR": "0"} 102 | 103 | result := SupportsColor(0, setEnvironment(&testEnvironment{env: env})) 104 | if result.Level != None { 105 | t.Errorf("Expected %v, got %v", None, result.Level) 106 | } 107 | 108 | env["FORCE_COLOR"] = "1" 109 | result = SupportsColor(0, setEnvironment(&testEnvironment{env: env})) 110 | if result.Level != Basic { 111 | t.Errorf("Expected %v, got %v", Basic, result.Level) 112 | } 113 | } 114 | 115 | func TestNoColor(t *testing.T) { 116 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 117 | env: map[string]string{"NO_COLOR": ""}, 118 | isNotTerminal: true, 119 | })) 120 | 121 | if result.Level != None { 122 | t.Errorf("Expected %v, got %v", None, result.Level) 123 | } 124 | } 125 | 126 | func TestReturnNoneIfNotTty(t *testing.T) { 127 | result := SupportsColor(0, setEnvironment(&testEnvironment{isNotTerminal: true})) 128 | if result.Level != None { 129 | t.Errorf("Expected %v, got %v", None, result.Level) 130 | } 131 | } 132 | 133 | func TestReturnNoneIfNoColorFlagIsUsed(t *testing.T) { 134 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 135 | env: map[string]string{"TERM": "xterm-256color"}, 136 | flags: []string{"no-color"}, 137 | })) 138 | if result.Level != None { 139 | t.Errorf("Expected %v, got %v", None, result.Level) 140 | } 141 | } 142 | 143 | func TestReturnNoneIfNoColorsFlagIsUsed(t *testing.T) { 144 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 145 | env: map[string]string{"TERM": "xterm-256color"}, 146 | flags: []string{"no-colors"}, 147 | })) 148 | if result.Level != None { 149 | t.Errorf("Expected %v, got %v", None, result.Level) 150 | } 151 | } 152 | 153 | func TestReturnBasicIfColorFlagIsUsed(t *testing.T) { 154 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 155 | env: map[string]string{}, 156 | flags: []string{"color"}, 157 | })) 158 | if result.Level != Basic { 159 | t.Errorf("Expected %v, got %v", Basic, result.Level) 160 | } 161 | } 162 | 163 | func TestReturnBasicIfColorsFlagIsUsed(t *testing.T) { 164 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 165 | env: map[string]string{}, 166 | flags: []string{"colors"}, 167 | })) 168 | if result.Level != Basic { 169 | t.Errorf("Expected %v, got %v", Basic, result.Level) 170 | } 171 | } 172 | 173 | func TestReturnBasicIfColorTermInEnv(t *testing.T) { 174 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 175 | env: map[string]string{"COLORTERM": "true"}, 176 | flags: []string{}, 177 | })) 178 | if result.Level != Basic { 179 | t.Errorf("Expected %v, got %v", Basic, result.Level) 180 | } 181 | } 182 | 183 | func TestSupportColorTrueFlag(t *testing.T) { 184 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 185 | env: map[string]string{}, 186 | flags: []string{"color=true"}, 187 | })) 188 | if result.Level != Basic { 189 | t.Errorf("Expected %v, got %v", Basic, result.Level) 190 | } 191 | } 192 | 193 | func TestSupportColorAlwaysFlag(t *testing.T) { 194 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 195 | env: map[string]string{}, 196 | flags: []string{"color=always"}, 197 | })) 198 | if result.Level != Basic { 199 | t.Errorf("Expected %v, got %v", Basic, result.Level) 200 | } 201 | } 202 | 203 | func TestSupportColorFalseFlag(t *testing.T) { 204 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 205 | env: map[string]string{}, 206 | flags: []string{"color=false"}, 207 | })) 208 | if result.Level != None { 209 | t.Errorf("Expected %v, got %v", None, result.Level) 210 | } 211 | } 212 | 213 | func TestSupportColor256Flag(t *testing.T) { 214 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 215 | env: map[string]string{}, 216 | flags: []string{"color=256"}, 217 | })) 218 | if result.Level != Ansi256 { 219 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 220 | } 221 | if result.Has256 == false { 222 | t.Errorf("Expected Has256 to be true") 223 | } 224 | } 225 | 226 | func TestSupportColor16mFlag(t *testing.T) { 227 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 228 | env: map[string]string{}, 229 | flags: []string{"color=16m"}, 230 | })) 231 | if result.Level != Ansi16m { 232 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 233 | } 234 | if result.Has256 == false { 235 | t.Errorf("Expected Has256 to be true") 236 | } 237 | if result.Has16m == false { 238 | t.Errorf("Expected Has16m to be true") 239 | } 240 | } 241 | 242 | func TestSupportColorFullFlag(t *testing.T) { 243 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 244 | env: map[string]string{}, 245 | flags: []string{"color=full"}, 246 | })) 247 | if result.Level != Ansi16m { 248 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 249 | } 250 | if result.Has256 == false { 251 | t.Errorf("Expected Has256 to be true") 252 | } 253 | if result.Has16m == false { 254 | t.Errorf("Expected Has16m to be true") 255 | } 256 | } 257 | 258 | func TestSupportColorTruecolorFlag(t *testing.T) { 259 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 260 | env: map[string]string{}, 261 | flags: []string{"color=truecolor"}, 262 | })) 263 | if result.Level != Ansi16m { 264 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 265 | } 266 | if result.Has256 == false { 267 | t.Errorf("Expected Has256 to be true") 268 | } 269 | if result.Has16m == false { 270 | t.Errorf("Expected Has16m to be true") 271 | } 272 | } 273 | 274 | func TestReturnNoneIfCIIsInEnv(t *testing.T) { 275 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 276 | env: map[string]string{"CI": ""}, 277 | })) 278 | 279 | if result.Level != None { 280 | t.Errorf("Expected %v, got %v", None, result.Level) 281 | } 282 | } 283 | 284 | func TestReturnBasicIfTravis(t *testing.T) { 285 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 286 | env: map[string]string{"CI": "Travis", "TRAVIS": "1"}, 287 | })) 288 | 289 | if result.Level != Basic { 290 | t.Errorf("Expected %v, got %v", Basic, result.Level) 291 | } 292 | } 293 | 294 | func TestReturnTrueColorIfActions(t *testing.T) { 295 | for _, ci := range []string{"GITHUB_ACTIONS", "GITEA_ACTIONS"} { 296 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 297 | env: map[string]string{"CI": "true", ci: "true"}, 298 | })) 299 | 300 | if result.Level != Ansi16m { 301 | t.Errorf("%v: Expected %v, got %v", ci, Ansi16m, result.Level) 302 | } 303 | } 304 | } 305 | 306 | func TestReturnBasicIfCI(t *testing.T) { 307 | for _, ci := range []string{"CIRCLECI", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"} { 308 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 309 | env: map[string]string{"CI": "true", ci: "true"}, 310 | })) 311 | 312 | if result.Level != Basic { 313 | t.Errorf("%v: Expected %v, got %v", ci, Basic, result.Level) 314 | } 315 | } 316 | } 317 | 318 | func TestReturnBasicIfCodeship(t *testing.T) { 319 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 320 | env: map[string]string{"CI": "true", "CI_NAME": "codeship"}, 321 | })) 322 | 323 | if result.Level != Basic { 324 | t.Errorf("Expected %v, got %v", Basic, result.Level) 325 | } 326 | } 327 | 328 | func TestTeamcity(t *testing.T) { 329 | // < 9.1 330 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 331 | env: map[string]string{"TEAMCITY_VERSION": "9.0.5 (build 32523)"}, 332 | })) 333 | if result.Level != None { 334 | t.Errorf("Expected %v, got %v", None, result.Level) 335 | } 336 | 337 | // >= 9.1 338 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 339 | env: map[string]string{"TEAMCITY_VERSION": "9.1.0 (build 32523)"}, 340 | })) 341 | if result.Level != Basic { 342 | t.Errorf("Expected %v, got %v", Basic, result.Level) 343 | } 344 | } 345 | 346 | func TestRxvt(t *testing.T) { 347 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 348 | env: map[string]string{"TERM": "rxvt"}, 349 | })) 350 | if result.Level != Basic { 351 | t.Errorf("Expected %v, got %v", Basic, result.Level) 352 | } 353 | } 354 | 355 | func TestPreferLevel2XtermOverColorTerm(t *testing.T) { 356 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 357 | env: map[string]string{"TERM": "xterm-256color", "COLORTERM": "1"}, 358 | })) 359 | 360 | if result.Level != Ansi256 { 361 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 362 | } 363 | } 364 | 365 | func TestScreen256Color(t *testing.T) { 366 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 367 | env: map[string]string{"TERM": "screen-256color"}, 368 | })) 369 | 370 | if result.Level != Ansi256 { 371 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 372 | } 373 | } 374 | 375 | func TestPutty256Color(t *testing.T) { 376 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 377 | env: map[string]string{"TERM": "putty-256color"}, 378 | })) 379 | 380 | if result.Level != Ansi256 { 381 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 382 | } 383 | } 384 | 385 | func TestITerm(t *testing.T) { 386 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 387 | env: map[string]string{"TERM_PROGRAM": "iTerm.app", "TERM_PROGRAM_VERSION": "3.0.10"}, 388 | })) 389 | if result.Level != Ansi16m { 390 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 391 | } 392 | 393 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 394 | env: map[string]string{"TERM_PROGRAM": "iTerm.app", "TERM_PROGRAM_VERSION": "2.9.3"}, 395 | })) 396 | if result.Level != Ansi256 { 397 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 398 | } 399 | } 400 | 401 | func TestWindows(t *testing.T) { 402 | // return level 1 if on Windows earlier than 10 build 10586 403 | env := &testEnvironment{ 404 | goos: "windows", 405 | colorCantBeEnabled: true, 406 | winMajorVersion: 10, 407 | winMinorVersion: 0, 408 | winBuildNumber: 10240, 409 | } 410 | result := SupportsColor(0, setEnvironment(env)) 411 | if result.Level != None { 412 | t.Errorf("Expected %v, got %v", None, result.Level) 413 | } 414 | 415 | // return level 2 if on Windows 10 build 10586 or later 416 | env = &testEnvironment{ 417 | goos: "windows", 418 | winMajorVersion: 10, 419 | winMinorVersion: 0, 420 | winBuildNumber: 10586, 421 | } 422 | result = SupportsColor(0, setEnvironment(env)) 423 | if result.Level != Ansi256 { 424 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 425 | } 426 | if !env.colorWasEnabled { 427 | t.Errorf("Expected color to be enabled") 428 | } 429 | 430 | // return level 3 if on Windows 10 build 14931 or later 431 | env = &testEnvironment{ 432 | goos: "windows", 433 | winMajorVersion: 10, 434 | winMinorVersion: 0, 435 | winBuildNumber: 14931, 436 | } 437 | result = SupportsColor(0, setEnvironment(env)) 438 | if result.Level != Ansi16m { 439 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 440 | } 441 | if !env.colorWasEnabled { 442 | t.Errorf("Expected color to be enabled") 443 | } 444 | 445 | // return level 2 if on Windows and force color flag 446 | env = &testEnvironment{ 447 | flags: []string{"color=256"}, 448 | goos: "windows", 449 | winMajorVersion: 10, 450 | winMinorVersion: 0, 451 | winBuildNumber: 10586, 452 | } 453 | result = SupportsColor(0, setEnvironment(env)) 454 | if result.Level != Ansi256 { 455 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 456 | } 457 | if !env.colorWasEnabled { 458 | t.Errorf("Expected color to be enabled") 459 | } 460 | 461 | // return level 3 if on Windows and force color flag 462 | env = &testEnvironment{ 463 | flags: []string{"color=16m"}, 464 | goos: "windows", 465 | winMajorVersion: 10, 466 | winMinorVersion: 0, 467 | winBuildNumber: 10586, 468 | } 469 | result = SupportsColor(0, setEnvironment(env)) 470 | if result.Level != Ansi16m { 471 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 472 | } 473 | if !env.colorWasEnabled { 474 | t.Errorf("Expected color to be enabled") 475 | } 476 | } 477 | 478 | func TestReturnAnsi256WhenForceColorIsSetWhenNotTTYInXterm256(t *testing.T) { 479 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 480 | isNotTerminal: true, 481 | env: map[string]string{ 482 | "FORCE_COLOR": "true", 483 | "TERM": "xterm-256color", 484 | }, 485 | })) 486 | 487 | if result.Level != Ansi256 { 488 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 489 | } 490 | } 491 | 492 | func TestSupportsSettingAColorLevelUsingForceColor(t *testing.T) { 493 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 494 | env: map[string]string{"FORCE_COLOR": "0"}, 495 | })) 496 | if result.Level != None { 497 | t.Errorf("Expected %v, got %v", None, result.Level) 498 | } 499 | 500 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 501 | env: map[string]string{"FORCE_COLOR": "1"}, 502 | })) 503 | if result.Level != Basic { 504 | t.Errorf("Expected %v, got %v", Basic, result.Level) 505 | } 506 | 507 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 508 | env: map[string]string{"FORCE_COLOR": "2"}, 509 | })) 510 | if result.Level != Ansi256 { 511 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 512 | } 513 | 514 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 515 | env: map[string]string{"FORCE_COLOR": "3"}, 516 | })) 517 | if result.Level != Ansi16m { 518 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 519 | } 520 | 521 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 522 | env: map[string]string{"FORCE_COLOR": "4"}, 523 | })) 524 | if result.Level != Ansi16m { 525 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 526 | } 527 | } 528 | 529 | func TestForceColorWorksWhenSetViaCommandLine(t *testing.T) { 530 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 531 | env: map[string]string{"FORCE_COLOR": "true"}, 532 | })) 533 | if result.Level != Basic { 534 | t.Errorf("Expected %v, got %v", Basic, result.Level) 535 | } 536 | 537 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 538 | env: map[string]string{"FORCE_COLOR": "true", "TERM": "xterm-256color"}, 539 | })) 540 | if result.Level != Ansi256 { 541 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 542 | } 543 | 544 | result = SupportsColor(0, setEnvironment(&testEnvironment{ 545 | env: map[string]string{"FORCE_COLOR": "false"}, 546 | })) 547 | if result.Level != None { 548 | t.Errorf("Expected %v, got %v", None, result.Level) 549 | } 550 | } 551 | func TestTermIsDumb(t *testing.T) { 552 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 553 | env: map[string]string{"TERM": "dumb"}, 554 | })) 555 | 556 | if result.Level != None { 557 | t.Errorf("Expected %v, got %v", None, result.Level) 558 | } 559 | } 560 | 561 | func TestTermIsDumbAndTermProgramIsSet(t *testing.T) { 562 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 563 | env: map[string]string{"TERM": "dumb", "TERM_PROGRAM": "Apple_Terminal"}, 564 | })) 565 | 566 | if result.Level != None { 567 | t.Errorf("Expected %v, got %v", None, result.Level) 568 | } 569 | } 570 | 571 | func TestTermIsDumbAndWindows(t *testing.T) { 572 | env := &testEnvironment{ 573 | env: map[string]string{"TERM": "dumb", "TERM_PROGRAM": "Apple_Terminal"}, 574 | goos: "windows", 575 | winMajorVersion: 10, 576 | winMinorVersion: 0, 577 | winBuildNumber: 14931, 578 | } 579 | result := SupportsColor(0, setEnvironment(env)) 580 | 581 | if result.Level != None { 582 | t.Errorf("Expected %v, got %v", None, result.Level) 583 | } 584 | if env.colorWasEnabled { 585 | t.Errorf("Expected color to not be enabled") 586 | } 587 | } 588 | 589 | func TestTermIsDumbAndForceColorIsSet(t *testing.T) { 590 | result := SupportsColor(0, setEnvironment(&testEnvironment{ 591 | env: map[string]string{"TERM": "dumb", "FORCE_COLOR": "1"}, 592 | })) 593 | 594 | if result.Level != Basic { 595 | t.Errorf("Expected %v, got %v", Basic, result.Level) 596 | } 597 | } 598 | 599 | func TestIgnoreFlagsWhenSniffFlagsIsFalse(t *testing.T) { 600 | env := &testEnvironment{ 601 | env: map[string]string{"TERM": "dumb"}, 602 | flags: []string{"color=256"}, 603 | } 604 | 605 | result := SupportsColor(0, setEnvironment(env)) 606 | if result.Level != Ansi256 { 607 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 608 | } 609 | 610 | result = SupportsColor(0, SniffFlagsOption(true), setEnvironment(env)) 611 | if result.Level != Ansi256 { 612 | t.Errorf("Expected %v, got %v", Ansi256, result.Level) 613 | } 614 | 615 | result = SupportsColor(0, SniffFlagsOption(false), setEnvironment(env)) 616 | if result.Level != None { 617 | t.Errorf("Expected %v, got %v", None, result.Level) 618 | } 619 | } 620 | 621 | func TestForceTTY(t *testing.T) { 622 | // Should be able to force that we are a terminal even when we detect we are not. 623 | result := SupportsColor(0, IsTTYOption(true), setEnvironment(&testEnvironment{ 624 | env: map[string]string{"TERM_PROGRAM": "iTerm.app", "TERM_PROGRAM_VERSION": "3.0.10"}, 625 | isNotTerminal: true, 626 | })) 627 | if result.Level != Ansi16m { 628 | t.Errorf("Expected %v, got %v", Ansi16m, result.Level) 629 | } 630 | 631 | // Should be able to force that we are not a terminal even when we detect we are. 632 | result = SupportsColor(0, IsTTYOption(false), setEnvironment(&testEnvironment{ 633 | env: map[string]string{"TERM_PROGRAM": "iTerm.app", "TERM_PROGRAM_VERSION": "3.0.10"}, 634 | isNotTerminal: false, 635 | })) 636 | if result.Level != None { 637 | t.Errorf("Expected %v, got %v", None, result.Level) 638 | } 639 | } 640 | --------------------------------------------------------------------------------