pax_global_header00006660000000000000000000000064150472015030014507gustar00rootroot0000000000000052 comment=561b8ac1cff6f8c286c7dd86e95cab3875c7ac01 colorprofile-0.3.2/000077500000000000000000000000001504720150300142105ustar00rootroot00000000000000colorprofile-0.3.2/.github/000077500000000000000000000000001504720150300155505ustar00rootroot00000000000000colorprofile-0.3.2/.github/CODEOWNERS000066400000000000000000000000211504720150300171340ustar00rootroot00000000000000* @aymanbagabas colorprofile-0.3.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001504720150300177335ustar00rootroot00000000000000colorprofile-0.3.2/.github/ISSUE_TEMPLATE/bug.yml000066400000000000000000000032511504720150300212340ustar00rootroot00000000000000name: Bug Report description: File a bug report labels: [bug] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! Please fill the form below. - type: textarea id: what-happened attributes: label: What happened? description: Also tell us, what did you expect to happen? validations: required: true - type: textarea id: reproducible attributes: label: How can we reproduce this? description: | Please share a code snippet, gist, or public repository that reproduces the issue. Make sure to make the reproducible as concise as possible, with only the minimum required code to reproduce the issue. validations: required: true - type: textarea id: version attributes: label: Which version of bubbletea are you using? description: '' render: bash validations: required: true - type: textarea id: terminaal attributes: label: Which terminals did you reproduce this with? description: | Other helpful information: was it over SSH? On tmux? Which version of said terminal? validations: required: true - type: checkboxes id: search attributes: label: Search options: - label: | I searched for other open and closed issues and pull requests before opening this, and didn't find anything that seems related. required: true - type: textarea id: ctx attributes: label: Additional context description: Anything else you would like to add validations: required: false colorprofile-0.3.2/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000014741504720150300224330ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Setup** Please complete the following information along with version numbers, if applicable. - OS [e.g. Ubuntu, macOS] - Shell [e.g. zsh, fish] - Terminal Emulator [e.g. kitty, iterm] - Terminal Multiplexer [e.g. tmux] **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Source Code** Please include source code if needed to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** Add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. colorprofile-0.3.2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000001701504720150300217210ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: Discord url: https://charm.sh/discord about: Chat on our Discord. colorprofile-0.3.2/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011341504720150300234570ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. colorprofile-0.3.2/.github/dependabot.yml000066400000000000000000000025341504720150300204040ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" - package-ecosystem: "gomod" directory: "/examples" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" - package-ecosystem: "gomod" directory: "/tutorials" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" colorprofile-0.3.2/.github/workflows/000077500000000000000000000000001504720150300176055ustar00rootroot00000000000000colorprofile-0.3.2/.github/workflows/build.yml000066400000000000000000000004011504720150300214220ustar00rootroot00000000000000name: build on: [push, pull_request] jobs: build: uses: charmbracelet/meta/.github/workflows/build.yml@main build-go-mod: uses: charmbracelet/meta/.github/workflows/build.yml@main with: go-version: "" go-version-file: ./go.mod colorprofile-0.3.2/.github/workflows/coverage.yml000066400000000000000000000002351504720150300221230ustar00rootroot00000000000000name: coverage on: push: branches: - "main" pull_request: jobs: coverage: uses: charmbracelet/meta/.github/workflows/coverage.yml@main colorprofile-0.3.2/.github/workflows/dependabot-sync.yml000066400000000000000000000006431504720150300234120ustar00rootroot00000000000000name: dependabot-sync on: schedule: - cron: "0 0 * * 0" # every Sunday at midnight workflow_dispatch: # allows manual triggering permissions: contents: write pull-requests: write jobs: dependabot-sync: uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main with: repo_name: ${{ github.event.repository.name }} secrets: gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} colorprofile-0.3.2/.github/workflows/lint-sync.yml000066400000000000000000000004171504720150300222520ustar00rootroot00000000000000name: lint-sync on: schedule: # every Sunday at midnight - cron: "0 0 * * 0" workflow_dispatch: # allows manual triggering permissions: contents: write pull-requests: write jobs: lint: uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main colorprofile-0.3.2/.github/workflows/lint.yml000066400000000000000000000001631504720150300212760ustar00rootroot00000000000000name: lint on: push: pull_request: jobs: lint: uses: charmbracelet/meta/.github/workflows/lint.yml@main colorprofile-0.3.2/.github/workflows/release.yml000066400000000000000000000021221504720150300217450ustar00rootroot00000000000000name: goreleaser on: push: tags: - v*.*.* concurrency: group: goreleaser cancel-in-progress: true jobs: goreleaser: uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main secrets: docker_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_token: ${{ secrets.DOCKERHUB_TOKEN }} gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }} goreleaser_key: ${{ secrets.GORELEASER_KEY }} twitter_consumer_key: ${{ secrets.TWITTER_CONSUMER_KEY }} twitter_consumer_secret: ${{ secrets.TWITTER_CONSUMER_SECRET }} twitter_access_token: ${{ secrets.TWITTER_ACCESS_TOKEN }} twitter_access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} mastodon_client_id: ${{ secrets.MASTODON_CLIENT_ID }} mastodon_client_secret: ${{ secrets.MASTODON_CLIENT_SECRET }} mastodon_access_token: ${{ secrets.MASTODON_ACCESS_TOKEN }} discord_webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }} discord_webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json colorprofile-0.3.2/.golangci.yml000066400000000000000000000012671504720150300166020ustar00rootroot00000000000000version: "2" run: tests: false linters: enable: - bodyclose - exhaustive - goconst - godot - gomoddirectives - goprintffuncname - gosec - misspell - nakedret - nestif - nilerr - noctx - nolintlint - prealloc - revive - rowserrcheck - sqlclosecheck - tparallel - unconvert - unparam - whitespace - wrapcheck exclusions: rules: - text: '(slog|log)\.\w+' linters: - noctx generated: lax presets: - common-false-positives issues: max-issues-per-linter: 0 max-same-issues: 0 formatters: enable: - gofumpt - goimports exclusions: generated: lax colorprofile-0.3.2/.goreleaser.yml000066400000000000000000000002371504720150300171430ustar00rootroot00000000000000includes: - from_url: url: charmbracelet/meta/main/goreleaser-lib.yaml # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json colorprofile-0.3.2/LICENSE000066400000000000000000000020701504720150300152140ustar00rootroot00000000000000MIT License Copyright (c) 2020-2024 Charmbracelet, Inc 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. colorprofile-0.3.2/README.md000066400000000000000000000064271504720150300155000ustar00rootroot00000000000000# Colorprofile

Latest Release GoDoc Build Status

A simple, powerful—and at times magical—package for detecting terminal color profiles and performing color (and CSI) degradation. ## Detecting the terminal’s color profile Detecting the terminal’s color profile is easy. ```go import "github.com/charmbracelet/colorprofile" // Detect the color profile. If you’re planning on writing to stderr you'd want // to use os.Stderr instead. p := colorprofile.Detect(os.Stdout, os.Environ()) // Comment on the profile. fmt.Printf("You know, your colors are quite %s.", func() string { switch p { case colorprofile.TrueColor: return "fancy" case colorprofile.ANSI256: return "1990s fancy" case colorprofile.ANSI: return "normcore" case colorprofile.Ascii: return "ancient" case colorprofile.NoTTY: return "naughty!" } return "...IDK" // this should never happen }()) ``` ## Downsampling colors When necessary, colors can be downsampled to a given profile, or manually downsampled to a specific profile. ```go p := colorprofile.Detect(os.Stdout, os.Environ()) c := color.RGBA{0x6b, 0x50, 0xff, 0xff} // #6b50ff // Downsample to the detected profile, when necessary. convertedColor := p.Convert(c) // Or manually convert to a given profile. ansi256Color := colorprofile.ANSI256.Convert(c) ansiColor := colorprofile.ANSI.Convert(c) noColor := colorprofile.Ascii.Convert(c) noANSI := colorprofile.NoTTY.Convert(c) ``` ## Automatic downsampling with a Writer You can also magically downsample colors in ANSI output, when necessary. If output is not a TTY ANSI will be dropped entirely. ```go myFancyANSI := "\x1b[38;2;107;80;255mCute \x1b[1;3mpuppy!!\x1b[m" // Automatically downsample for the terminal at stdout. w := colorprofile.NewWriter(os.Stdout, os.Environ()) fmt.Fprintf(w, myFancyANSI) // Downsample to 4-bit ANSI. w.Profile = colorprofile.ANSI fmt.Fprintf(w, myFancyANSI) // Ascii-fy, no colors. w.Profile = colorprofile.Ascii fmt.Fprintf(w, myFancyANSI) // Strip ANSI altogether. w.Profile = colorprofile.NoTTY fmt.Fprintf(w, myFancyANSI) // not as fancy ``` ## Contributing See [contributing][contribute]. [contribute]: https://github.com/charmbracelet/colorprofile/contribute ## Feedback We’d love to hear your thoughts on this project. Feel free to drop us a note! - [Twitter](https://twitter.com/charmcli) - [The Fediverse](https://mastodon.social/@charmcli) - [Discord](https://charm.sh/chat) ## License [MIT](https://github.com/charmbracelet/bubbletea/raw/master/LICENSE) --- Part of [Charm](https://charm.sh). The Charm logo Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة colorprofile-0.3.2/doc.go000066400000000000000000000003051504720150300153020ustar00rootroot00000000000000// Package colorprofile provides a way to downsample ANSI escape sequence // colors and styles automatically based on output, environment variables, and // Terminfo databases. package colorprofile colorprofile-0.3.2/env.go000066400000000000000000000167651504720150300153460ustar00rootroot00000000000000package colorprofile import ( "bytes" "io" "os/exec" "runtime" "strconv" "strings" "github.com/charmbracelet/x/term" "github.com/xo/terminfo" ) const dumbTerm = "dumb" // Detect returns the color profile based on the terminal output, and // environment variables. This respects NO_COLOR, CLICOLOR, and CLICOLOR_FORCE // environment variables. // // The rules as follows: // - TERM=dumb is always treated as NoTTY unless CLICOLOR_FORCE=1 is set. // - If COLORTERM=truecolor, and the profile is not NoTTY, it gest upgraded to TrueColor. // - Using any 256 color terminal (e.g. TERM=xterm-256color) will set the profile to ANSI256. // - Using any color terminal (e.g. TERM=xterm-color) will set the profile to ANSI. // - Using CLICOLOR=1 without TERM defined should be treated as ANSI if the // output is a terminal. // - NO_COLOR takes precedence over CLICOLOR/CLICOLOR_FORCE, and will disable // colors but not text decoration, i.e. bold, italic, faint, etc. // // See https://no-color.org/ and https://bixense.com/clicolors/ for more information. func Detect(output io.Writer, env []string) Profile { out, ok := output.(term.File) environ := newEnviron(env) isatty := isTTYForced(environ) || (ok && term.IsTerminal(out.Fd())) term, ok := environ.lookup("TERM") isDumb := !ok || term == dumbTerm envp := colorProfile(isatty, environ) if envp == TrueColor || envNoColor(environ) { // We already know we have TrueColor, or NO_COLOR is set. return envp } if isatty && !isDumb { tip := Terminfo(term) tmuxp := tmux(environ) // Color profile is the maximum of env, terminfo, and tmux. return max(envp, max(tip, tmuxp)) } return envp } // Env returns the color profile based on the terminal environment variables. // This respects NO_COLOR, CLICOLOR, and CLICOLOR_FORCE environment variables. // // The rules as follows: // - TERM=dumb is always treated as NoTTY unless CLICOLOR_FORCE=1 is set. // - If COLORTERM=truecolor, and the profile is not NoTTY, it gest upgraded to TrueColor. // - Using any 256 color terminal (e.g. TERM=xterm-256color) will set the profile to ANSI256. // - Using any color terminal (e.g. TERM=xterm-color) will set the profile to ANSI. // - Using CLICOLOR=1 without TERM defined should be treated as ANSI if the // output is a terminal. // - NO_COLOR takes precedence over CLICOLOR/CLICOLOR_FORCE, and will disable // colors but not text decoration, i.e. bold, italic, faint, etc. // // See https://no-color.org/ and https://bixense.com/clicolors/ for more information. func Env(env []string) (p Profile) { return colorProfile(true, newEnviron(env)) } func colorProfile(isatty bool, env environ) (p Profile) { term, ok := env.lookup("TERM") isDumb := (!ok && runtime.GOOS != "windows") || term == dumbTerm envp := envColorProfile(env) if !isatty || isDumb { // Check if the output is a terminal. // Treat dumb terminals as NoTTY p = NoTTY } else { p = envp } if envNoColor(env) && isatty { if p > Ascii { p = Ascii } return //nolint:nakedret } if cliColorForced(env) { if p < ANSI { p = ANSI } if envp > p { p = envp } return //nolint:nakedret } if cliColor(env) { if isatty && !isDumb && p < ANSI { p = ANSI } } return p } // envNoColor returns true if the environment variables explicitly disable color output // by setting NO_COLOR (https://no-color.org/). func envNoColor(env environ) bool { noColor, _ := strconv.ParseBool(env.get("NO_COLOR")) return noColor } func cliColor(env environ) bool { cliColor, _ := strconv.ParseBool(env.get("CLICOLOR")) return cliColor } func cliColorForced(env environ) bool { cliColorForce, _ := strconv.ParseBool(env.get("CLICOLOR_FORCE")) return cliColorForce } func isTTYForced(env environ) bool { skip, _ := strconv.ParseBool(env.get("TTY_FORCE")) return skip } func colorTerm(env environ) bool { colorTerm := strings.ToLower(env.get("COLORTERM")) return colorTerm == "truecolor" || colorTerm == "24bit" || colorTerm == "yes" || colorTerm == "true" } // envColorProfile returns infers the color profile from the environment. func envColorProfile(env environ) (p Profile) { term, ok := env.lookup("TERM") if !ok || len(term) == 0 || term == dumbTerm { p = NoTTY if runtime.GOOS == "windows" { // Use Windows API to detect color profile. Windows Terminal and // cmd.exe don't define $TERM. if wcp, ok := windowsColorProfile(env); ok { p = wcp } } } else { p = ANSI } parts := strings.Split(term, "-") switch parts[0] { case "alacritty", "contour", "foot", "ghostty", "kitty", "rio", "st", "wezterm": return TrueColor case "xterm": if len(parts) > 1 { switch parts[1] { case "ghostty", "kitty": // These terminals can be defined as xterm-TERMNAME return TrueColor } } case "tmux", "screen": if p < ANSI256 { p = ANSI256 } } if isCloudShell, _ := strconv.ParseBool(env.get("GOOGLE_CLOUD_SHELL")); isCloudShell { return TrueColor } // GNU Screen doesn't support TrueColor // Tmux doesn't support $COLORTERM if colorTerm(env) && !strings.HasPrefix(term, "screen") && !strings.HasPrefix(term, "tmux") { return TrueColor } if strings.HasSuffix(term, "256color") && p < ANSI256 { p = ANSI256 } // Direct color terminals support true colors. if strings.HasSuffix(term, "direct") { return TrueColor } return //nolint:nakedret } // Terminfo returns the color profile based on the terminal's terminfo // database. This relies on the Tc and RGB capabilities to determine if the // terminal supports TrueColor. // If term is empty or "dumb", it returns NoTTY. func Terminfo(term string) (p Profile) { if len(term) == 0 || term == "dumb" { return NoTTY } p = ANSI ti, err := terminfo.Load(term) if err != nil { return } extbools := ti.ExtBoolCapsShort() if _, ok := extbools["Tc"]; ok { return TrueColor } if _, ok := extbools["RGB"]; ok { return TrueColor } return } // Tmux returns the color profile based on `tmux info` output. Tmux supports // overriding the terminal's color capabilities, so this function will return // the color profile based on the tmux configuration. func Tmux(env []string) Profile { return tmux(newEnviron(env)) } // tmux returns the color profile based on the tmux environment variables. func tmux(env environ) (p Profile) { if tmux, ok := env.lookup("TMUX"); !ok || len(tmux) == 0 { // Not in tmux return NoTTY } // Check if tmux has either Tc or RGB capabilities. Otherwise, return // ANSI256. p = ANSI256 cmd := exec.Command("tmux", "info") out, err := cmd.Output() if err != nil { return } for _, line := range bytes.Split(out, []byte("\n")) { if (bytes.Contains(line, []byte("Tc")) || bytes.Contains(line, []byte("RGB"))) && bytes.Contains(line, []byte("true")) { return TrueColor } } return } // environ is a map of environment variables. type environ map[string]string // newEnviron returns a new environment map from a slice of environment // variables. func newEnviron(environ []string) environ { m := make(map[string]string, len(environ)) for _, e := range environ { parts := strings.SplitN(e, "=", 2) var value string if len(parts) == 2 { value = parts[1] } m[parts[0]] = value } return m } // lookup returns the value of an environment variable and a boolean indicating // if it exists. func (e environ) lookup(key string) (string, bool) { v, ok := e[key] return v, ok } // get returns the value of an environment variable and empty string if it // doesn't exist. func (e environ) get(key string) string { v, _ := e.lookup(key) return v } colorprofile-0.3.2/env_other.go000066400000000000000000000002171504720150300165300ustar00rootroot00000000000000//go:build !windows // +build !windows package colorprofile func windowsColorProfile(map[string]string) (Profile, bool) { return 0, false } colorprofile-0.3.2/env_test.go000066400000000000000000000073271504720150300163770ustar00rootroot00000000000000package colorprofile import ( "runtime" "testing" ) var cases = []struct { name string environ []string expected Profile }{ { name: "empty", environ: []string{}, expected: func() Profile { if runtime.GOOS == "windows" { p, _ := windowsColorProfile(map[string]string{}) return p } else { return NoTTY } }(), }, { name: "no tty", environ: []string{"TERM=dumb"}, expected: NoTTY, }, { name: "dumb term, truecolor, not forced", environ: []string{ "TERM=dumb", "COLORTERM=truecolor", }, expected: NoTTY, }, { name: "dumb term, truecolor, forced", environ: []string{ "TERM=dumb", "COLORTERM=truecolor", "CLICOLOR_FORCE=1", }, expected: TrueColor, }, { name: "dumb term, CLICOLOR_FORCE=1", environ: []string{ "TERM=dumb", "CLICOLOR_FORCE=1", }, expected: func() Profile { if runtime.GOOS == "windows" { // Windows Terminal supports TrueColor return TrueColor } else { return ANSI } }(), }, { name: "dumb term, CLICOLOR=1", environ: []string{ "TERM=dumb", "CLICOLOR=1", }, expected: NoTTY, }, { name: "xterm-256color", environ: []string{ "TERM=xterm-256color", }, expected: ANSI256, }, { name: "xterm-256color, CLICOLOR=1", environ: []string{ "TERM=xterm-256color", "CLICOLOR=1", }, expected: ANSI256, }, { name: "xterm-256color, COLORTERM=yes", environ: []string{ "TERM=xterm-256color", "COLORTERM=yes", }, expected: TrueColor, }, { name: "xterm-256color, NO_COLOR=1", environ: []string{ "TERM=xterm-256color", "NO_COLOR=1", }, expected: Ascii, }, { name: "xterm", environ: []string{ "TERM=xterm", }, expected: ANSI, }, { name: "xterm, NO_COLOR=1", environ: []string{ "TERM=xterm", "NO_COLOR=1", }, expected: Ascii, }, { name: "xterm, CLICOLOR=1", environ: []string{ "TERM=xterm", "CLICOLOR=1", }, expected: ANSI, }, { name: "xterm, CLICOLOR_FORCE=1", environ: []string{ "TERM=xterm", "CLICOLOR_FORCE=1", }, expected: ANSI, }, { name: "xterm-16color", environ: []string{ "TERM=xterm-16color", }, expected: ANSI, }, { name: "xterm-color", environ: []string{ "TERM=xterm-color", }, expected: ANSI, }, { name: "xterm-256color, NO_COLOR=1, CLICOLOR_FORCE=1", environ: []string{ "TERM=xterm-256color", "NO_COLOR=1", "CLICOLOR_FORCE=1", }, expected: Ascii, }, { name: "Windows Terminal", environ: []string{ "WT_SESSION=1", }, expected: func() Profile { if runtime.GOOS == "windows" { // Windows Terminal supports TrueColor return TrueColor } else { return NoTTY } }(), }, { name: "screen default", environ: []string{ "TERM=screen", }, expected: ANSI256, }, { name: "screen colorterm", environ: []string{ "TERM=screen", "COLORTERM=truecolor", }, expected: ANSI256, }, { name: "tmux colorterm", environ: []string{ "TERM=tmux", "COLORTERM=truecolor", }, expected: ANSI256, }, { name: "tmux 256color", environ: []string{ "TERM=tmux-256color", }, expected: ANSI256, }, { name: "ignore COLORTERM when no TERM is defined", environ: []string{ "COLORTERM=truecolor", }, expected: func() Profile { if runtime.GOOS == "windows" { p, _ := windowsColorProfile(map[string]string{}) return p } else { return NoTTY } }(), }, { name: "direct color xterm terminal", environ: []string{ "TERM=xterm-direct", }, expected: TrueColor, }, } func TestEnvColorProfile(t *testing.T) { for i, tc := range cases { t.Run(tc.name, func(t *testing.T) { p := Env(tc.environ) if p != tc.expected { t.Errorf("case %d: expected %v, got %v", i, tc.expected, p) } }) } } colorprofile-0.3.2/env_windows.go000066400000000000000000000015461504720150300171070ustar00rootroot00000000000000//go:build windows // +build windows package colorprofile import ( "strconv" "golang.org/x/sys/windows" ) func windowsColorProfile(env map[string]string) (Profile, bool) { if env["ConEmuANSI"] == "ON" { return TrueColor, true } if len(env["WT_SESSION"]) > 0 { // Windows Terminal supports TrueColor return TrueColor, true } major, _, build := windows.RtlGetNtVersionNumbers() if build < 10586 || major < 10 { // No ANSI support before WindowsNT 10 build 10586 if len(env["ANSICON"]) > 0 { ansiconVer := env["ANSICON_VER"] cv, err := strconv.Atoi(ansiconVer) if err != nil || cv < 181 { // No 8 bit color support before ANSICON 1.81 return ANSI, true } return ANSI256, true } return NoTTY, true } if build < 14931 { // No true color support before build 14931 return ANSI256, true } return TrueColor, true } colorprofile-0.3.2/examples/000077500000000000000000000000001504720150300160265ustar00rootroot00000000000000colorprofile-0.3.2/examples/go.mod000066400000000000000000000010321504720150300171300ustar00rootroot00000000000000module examples go 1.23.1 replace github.com/charmbracelet/colorprofile => ../ require github.com/charmbracelet/colorprofile v0.0.0-20240913192632-4a4ff4a5f48a require ( github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.29.0 // indirect ) colorprofile-0.3.2/examples/go.sum000066400000000000000000000030231504720150300171570ustar00rootroot00000000000000github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= colorprofile-0.3.2/examples/profile/000077500000000000000000000000001504720150300174665ustar00rootroot00000000000000colorprofile-0.3.2/examples/profile/main.go000066400000000000000000000044071504720150300207460ustar00rootroot00000000000000package main import ( "fmt" "image/color" "os" "github.com/charmbracelet/colorprofile" ) func main() { // Detect the color profile for stdout. p := colorprofile.Detect(os.Stdout, os.Environ()) fmt.Printf("Your color profile is what we call '%s'.\n\n", p) // Let's talk about the profile. fmt.Printf("You know, your colors are quite %s.\n\n", func() string { switch p { case colorprofile.TrueColor: return "fancy" case colorprofile.ANSI256: return "1990s fancy" case colorprofile.ANSI: return "normcore" case colorprofile.Ascii: return "ancient" case colorprofile.NoTTY: return "naughty!" } // This should never happen. return "...IDK" }()) // Here's a nice color. myCuteColor := color.RGBA{0x6b, 0x50, 0xff, 0xff} // #6b50ff fmt.Printf("A cute color we like is: %s.\n\n", colorToHex(myCuteColor)) // Let's convert it to the detected color profile. theColorWeNeed := p.Convert(myCuteColor) fmt.Printf("This terminal needs that color to be a %T, at best.\n", theColorWeNeed) fmt.Printf("In this case that color is %s.\n\n", colorToHex(theColorWeNeed)) // Now let's convert it to a color profile that only supports up to 256 // colors. ansi256Color := colorprofile.ANSI256.Convert(myCuteColor) fmt.Printf("Apple Terminal would want this color to be: %d (an %T).\n\n", ansi256Color, ansi256Color) // But really, who has time to convert? Not you? Well, kiddo, here's // a magical writer that will just auto-convert whatever ANSI you throw at // it to the appropriate color profile. myFancyANSI := "\x1b[38;2;107;80;255mCute \x1b[1;3mpuppy!!\x1b[m" w := colorprofile.NewWriter(os.Stdout, os.Environ()) fmt.Fprintln(w, "This terminal:", myFancyANSI) // But we're old school. Make the writer only use 4-bit ANSI, 1980s style. w.Profile = colorprofile.ANSI fmt.Fprintln(w, "4-bit ANSI:", myFancyANSI) // Too colorful. Use black and white only. w.Profile = colorprofile.Ascii fmt.Fprintln(w, "Old school cool:", myFancyANSI) // no colors // That's way too modern. Let's go back to MIT in the 1970s. w.Profile = colorprofile.NoTTY fmt.Fprintln(w, "No TTY :(", myFancyANSI) // less fancy } func colorToHex(c color.Color) string { if c == nil { return "" } r, g, b, _ := c.RGBA() return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8) } colorprofile-0.3.2/examples/writer/000077500000000000000000000000001504720150300173425ustar00rootroot00000000000000colorprofile-0.3.2/examples/writer/writer.go000066400000000000000000000006471504720150300212140ustar00rootroot00000000000000// This package provides a writer that can be piped into to degrade the colors // based on the terminal capabilities and profile. package main import ( "io" "log" "os" "github.com/charmbracelet/colorprofile" ) func main() { w := colorprofile.NewWriter(os.Stdout, os.Environ()) // Read from stdin and write to stdout bts, err := io.ReadAll(os.Stdin) if err != nil { log.Fatal(err) } _, err = w.Write(bts) } colorprofile-0.3.2/go.mod000066400000000000000000000006011504720150300153130ustar00rootroot00000000000000module github.com/charmbracelet/colorprofile go 1.23.0 require ( github.com/charmbracelet/x/ansi v0.10.1 github.com/charmbracelet/x/term v0.2.1 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e golang.org/x/sys v0.35.0 ) require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect ) colorprofile-0.3.2/go.sum000066400000000000000000000030251504720150300153430ustar00rootroot00000000000000github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= colorprofile-0.3.2/profile.go000066400000000000000000000034531504720150300162040ustar00rootroot00000000000000package colorprofile import ( "image/color" "sync" "github.com/charmbracelet/x/ansi" ) // Profile is a color profile: NoTTY, Ascii, ANSI, ANSI256, or TrueColor. type Profile byte const ( // NoTTY is a profile with no terminal support. NoTTY Profile = iota // Ascii is a profile with no color support. Ascii //nolint:revive // ANSI is a profile with 16 colors (4-bit). ANSI // ANSI256 is a profile with 256 colors (8-bit). ANSI256 // TrueColor is a profile with 16 million colors (24-bit). TrueColor ) // String returns the string representation of a Profile. func (p Profile) String() string { switch p { case TrueColor: return "TrueColor" case ANSI256: return "ANSI256" case ANSI: return "ANSI" case Ascii: return "Ascii" case NoTTY: return "NoTTY" } return "Unknown" } var ( cache = map[Profile]map[color.Color]color.Color{ ANSI256: {}, ANSI: {}, } mu sync.RWMutex ) // Convert transforms a given Color to a Color supported within the Profile. func (p Profile) Convert(c color.Color) (cc color.Color) { if p <= Ascii { return nil } if p == TrueColor { // TrueColor is a passthrough. return c } // Do we have a cached color for this profile and color? mu.RLock() if c != nil && cache[p] != nil { if cc, ok := cache[p][c]; ok { mu.RUnlock() return cc } } mu.RUnlock() // If we don't have a cached color, we need to convert it and cache it. defer func() { mu.Lock() if cc != nil && cache[p] != nil { if _, ok := cache[p][c]; !ok { cache[p][c] = cc } } mu.Unlock() }() switch c.(type) { case ansi.BasicColor: return c case ansi.IndexedColor: if p == ANSI { return ansi.Convert16(c) } return c default: if p == ANSI256 { return ansi.Convert256(c) } else if p == ANSI { return ansi.Convert16(c) } return c } } colorprofile-0.3.2/profile_test.go000066400000000000000000000120551504720150300172410ustar00rootroot00000000000000package colorprofile import ( "image/color" "log" "testing" "github.com/charmbracelet/x/ansi" "github.com/lucasb-eyer/go-colorful" ) func TestHexTo256(t *testing.T) { testCases := map[string]struct { input colorful.Color expectedHex string expectedOutput ansi.IndexedColor }{ "white": { input: colorful.Color{R: 1, G: 1, B: 1}, expectedHex: "#ffffff", expectedOutput: 231, }, "offwhite": { input: colorful.Color{R: 0.9333, G: 0.9333, B: 0.933}, expectedHex: "#eeeeee", expectedOutput: 255, }, "slightly brighter than offwhite": { input: colorful.Color{R: 0.95, G: 0.95, B: 0.95}, expectedHex: "#f2f2f2", expectedOutput: 255, }, "red": { input: colorful.Color{R: 1, G: 0, B: 0}, expectedHex: "#ff0000", expectedOutput: 196, }, "silver foil": { input: colorful.Color{R: 0.6863, G: 0.6863, B: 0.6863}, expectedHex: "#afafaf", expectedOutput: 145, }, "silver chalice": { input: colorful.Color{R: 0.698, G: 0.698, B: 0.698}, expectedHex: "#b2b2b2", expectedOutput: 249, }, "slightly closer to silver foil": { input: colorful.Color{R: 0.692, G: 0.692, B: 0.692}, expectedHex: "#b0b0b0", expectedOutput: 145, }, "slightly closer to silver chalice": { input: colorful.Color{R: 0.694, G: 0.694, B: 0.694}, expectedHex: "#b1b1b1", expectedOutput: 249, }, "gray": { input: colorful.Color{R: 0.5, G: 0.5, B: 0.5}, expectedHex: "#808080", expectedOutput: 244, }, } for testName, testCase := range testCases { t.Run(testName, func(t *testing.T) { // hex := fmt.Sprintf("#%02x%02x%02x", uint8(testCase.input.R*255), uint8(testCase.input.G*255), uint8(testCase.input.B*255)) col := ANSI256.Convert(testCase.input) if testCase.input.Hex() != testCase.expectedHex { t.Errorf("Expected %+v to map to %s, but instead received %s", testCase.input, testCase.expectedHex, testCase.input.Hex()) } output, ok := col.(ansi.IndexedColor) if !ok { t.Errorf("Expected %+v to be an ansi.IndexedColor, but instead received %T", testCase.input, col) } if output != testCase.expectedOutput { t.Errorf("Expected truecolor %+v to map to 256 color %d, but instead received %d", testCase.input, testCase.expectedOutput, output) } }) } } func TestDetectionByEnvironment(t *testing.T) { testCases := map[string]struct { environ []string expected Profile }{ "TERM is set to dumb": { environ: []string{"TERM=dumb"}, expected: NoTTY, }, "TERM set to xterm": { environ: []string{"TERM=xterm"}, expected: ANSI, }, "TERM is set to rio": { environ: []string{"TERM=rio"}, expected: TrueColor, }, "TERM set to xterm-256color": { environ: []string{"TERM=xterm-256color"}, expected: ANSI256, }, } for testName, testCase := range testCases { t.Run(testName, func(t *testing.T) { profile := Env(testCase.environ) if profile != testCase.expected { t.Errorf("Expected profile to be %s, but instead received %s", testCase.expected, profile) } }) } } func TestCache(t *testing.T) { mu.Lock() // Clear the cache before running the test cache = map[Profile]map[color.Color]color.Color{ TrueColor: {}, ANSI256: {}, ANSI: {}, } mu.Unlock() hex := func(s string) color.Color { c, err := colorful.Hex(s) if err != nil { log.Fatalf("Failed to parse hex color %s: %v", s, err) } return c } testCases := map[string]struct { input color.Color profile Profile expected color.Color }{ "red": { input: colorful.Color{R: 1, G: 0, B: 0}, profile: ANSI256, expected: ansi.IndexedColor(196), }, "grey": { input: colorful.Color{R: 0.5, G: 0.5, B: 0.5}, profile: ANSI256, expected: ansi.IndexedColor(244), }, "white": { input: colorful.Color{R: 1, G: 1, B: 1}, profile: ANSI, expected: ansi.BrightWhite, }, "light burgundy": { input: hex("#7b2c2c"), profile: ANSI256, expected: ansi.IndexedColor(88), }, "truecolor": { input: hex("#8ab7ed"), profile: TrueColor, expected: hex("#8ab7ed"), }, "offwhite": { input: hex("#eeeeee"), profile: ANSI256, expected: ansi.IndexedColor(255), }, } for testName, testCase := range testCases { t.Run(testName, func(t *testing.T) { col := testCase.profile.Convert(testCase.input) if col != testCase.expected { t.Errorf("Expected %+v to map to %s, but instead received %s", testCase.input, testCase.expected, col) } if testCase.profile == TrueColor { // TrueColor is a passthrough, so we don't cache it. return } // Check if the color is cached mu.RLock() cachedColor, ok := cache[testCase.profile][testCase.input] mu.RUnlock() if !ok { t.Errorf("Expected color %+v to be cached for profile %s, but it was not", testCase.input, testCase.profile) } if cachedColor != testCase.expected { t.Errorf("Expected cached color for %+v to be %s, but instead received %s", testCase.input, testCase.expected, cachedColor) } }) } } colorprofile-0.3.2/writer.go000066400000000000000000000110511504720150300160510ustar00rootroot00000000000000package colorprofile import ( "bytes" "fmt" "image/color" "io" "strconv" "github.com/charmbracelet/x/ansi" ) // NewWriter creates a new color profile writer that downgrades color sequences // based on the detected color profile. // // If environ is nil, it will use os.Environ() to get the environment variables. // // It queries the given writer to determine if it supports ANSI escape codes. // If it does, along with the given environment variables, it will determine // the appropriate color profile to use for color formatting. // // This respects the NO_COLOR, CLICOLOR, and CLICOLOR_FORCE environment variables. func NewWriter(w io.Writer, environ []string) *Writer { return &Writer{ Forward: w, Profile: Detect(w, environ), } } // Writer represents a color profile writer that writes ANSI sequences to the // underlying writer. type Writer struct { Forward io.Writer Profile Profile } // Write writes the given text to the underlying writer. func (w *Writer) Write(p []byte) (int, error) { switch w.Profile { case TrueColor: return w.Forward.Write(p) //nolint:wrapcheck case NoTTY: return io.WriteString(w.Forward, ansi.Strip(string(p))) //nolint:wrapcheck case Ascii, ANSI, ANSI256: return w.downsample(p) default: return 0, fmt.Errorf("invalid profile: %v", w.Profile) } } // downsample downgrades the given text to the appropriate color profile. func (w *Writer) downsample(p []byte) (int, error) { var buf bytes.Buffer var state byte parser := ansi.GetParser() defer ansi.PutParser(parser) for len(p) > 0 { parser.Reset() seq, _, read, newState := ansi.DecodeSequence(p, state, parser) switch { case ansi.HasCsiPrefix(seq) && parser.Command() == 'm': handleSgr(w, parser, &buf) default: // If we're not a style SGR sequence, just write the bytes. if n, err := buf.Write(seq); err != nil { return n, err //nolint:wrapcheck } } p = p[read:] state = newState } return w.Forward.Write(buf.Bytes()) //nolint:wrapcheck } // WriteString writes the given text to the underlying writer. func (w *Writer) WriteString(s string) (n int, err error) { return w.Write([]byte(s)) } func handleSgr(w *Writer, p *ansi.Parser, buf *bytes.Buffer) { var style ansi.Style params := p.Params() for i := 0; i < len(params); i++ { param := params[i] switch param := param.Param(0); param { case 0: // SGR default parameter is 0. We use an empty string to reduce the // number of bytes written to the buffer. style = append(style, "") case 30, 31, 32, 33, 34, 35, 36, 37: // 8-bit foreground color if w.Profile < ANSI { continue } style = style.ForegroundColor( w.Profile.Convert(ansi.BasicColor(param - 30))) //nolint:gosec case 38: // 16 or 24-bit foreground color var c color.Color if n := ansi.ReadStyleColor(params[i:], &c); n > 0 { i += n - 1 } if w.Profile < ANSI { continue } style = style.ForegroundColor(w.Profile.Convert(c)) case 39: // default foreground color if w.Profile < ANSI { continue } style = style.DefaultForegroundColor() case 40, 41, 42, 43, 44, 45, 46, 47: // 8-bit background color if w.Profile < ANSI { continue } style = style.BackgroundColor( w.Profile.Convert(ansi.BasicColor(param - 40))) //nolint:gosec case 48: // 16 or 24-bit background color var c color.Color if n := ansi.ReadStyleColor(params[i:], &c); n > 0 { i += n - 1 } if w.Profile < ANSI { continue } style = style.BackgroundColor(w.Profile.Convert(c)) case 49: // default background color if w.Profile < ANSI { continue } style = style.DefaultBackgroundColor() case 58: // 16 or 24-bit underline color var c color.Color if n := ansi.ReadStyleColor(params[i:], &c); n > 0 { i += n - 1 } if w.Profile < ANSI { continue } style = style.UnderlineColor(w.Profile.Convert(c)) case 59: // default underline color if w.Profile < ANSI { continue } style = style.DefaultUnderlineColor() case 90, 91, 92, 93, 94, 95, 96, 97: // 8-bit bright foreground color if w.Profile < ANSI { continue } style = style.ForegroundColor( w.Profile.Convert(ansi.BasicColor(param - 90 + 8))) //nolint:gosec case 100, 101, 102, 103, 104, 105, 106, 107: // 8-bit bright background color if w.Profile < ANSI { continue } style = style.BackgroundColor( w.Profile.Convert(ansi.BasicColor(param - 100 + 8))) //nolint:gosec default: // If this is not a color attribute, just append it to the style. style = append(style, strconv.Itoa(param)) } } _, _ = buf.WriteString(style.String()) } colorprofile-0.3.2/writer_test.go000066400000000000000000000137111504720150300171150ustar00rootroot00000000000000package colorprofile import ( "bytes" "io" "os" "testing" "github.com/charmbracelet/x/ansi" ) var writers = map[Profile]func(io.Writer) *Writer{ TrueColor: func(w io.Writer) *Writer { return &Writer{w, TrueColor} }, ANSI256: func(w io.Writer) *Writer { return &Writer{w, ANSI256} }, ANSI: func(w io.Writer) *Writer { return &Writer{w, ANSI} }, Ascii: func(w io.Writer) *Writer { return &Writer{w, Ascii} }, NoTTY: func(w io.Writer) *Writer { return &Writer{w, NoTTY} }, } var writer_cases = []struct { name string input string expectedTrueColor string expectedANSI256 string expectedANSI string expectedAscii string }{ { name: "empty", }, { name: "no styles", input: "hello world", expectedTrueColor: "hello world", expectedANSI256: "hello world", expectedANSI: "hello world", expectedAscii: "hello world", }, { name: "simple style attributes", input: "hello \x1b[1mworld\x1b[m", expectedTrueColor: "hello \x1b[1mworld\x1b[m", expectedANSI256: "hello \x1b[1mworld\x1b[m", expectedANSI: "hello \x1b[1mworld\x1b[m", expectedAscii: "hello \x1b[1mworld\x1b[m", }, { name: "simple ansi color fg", input: "hello \x1b[31mworld\x1b[m", expectedTrueColor: "hello \x1b[31mworld\x1b[m", expectedANSI256: "hello \x1b[31mworld\x1b[m", expectedANSI: "hello \x1b[31mworld\x1b[m", expectedAscii: "hello \x1b[mworld\x1b[m", }, { name: "default fg color after ansi color", input: "\x1b[31mhello \x1b[39mworld\x1b[m", expectedTrueColor: "\x1b[31mhello \x1b[39mworld\x1b[m", expectedANSI256: "\x1b[31mhello \x1b[39mworld\x1b[m", expectedANSI: "\x1b[31mhello \x1b[39mworld\x1b[m", expectedAscii: "\x1b[mhello \x1b[mworld\x1b[m", }, { name: "ansi color fg and bg", input: "\x1b[31;42mhello world\x1b[m", expectedTrueColor: "\x1b[31;42mhello world\x1b[m", expectedANSI256: "\x1b[31;42mhello world\x1b[m", expectedANSI: "\x1b[31;42mhello world\x1b[m", expectedAscii: "\x1b[mhello world\x1b[m", }, { name: "bright ansi color fg and bg", input: "\x1b[91;102mhello world\x1b[m", expectedTrueColor: "\x1b[91;102mhello world\x1b[m", expectedANSI256: "\x1b[91;102mhello world\x1b[m", expectedANSI: "\x1b[91;102mhello world\x1b[m", expectedAscii: "\x1b[mhello world\x1b[m", }, { name: "simple 256 color fg", input: "hello \x1b[38;5;196mworld\x1b[m", expectedTrueColor: "hello \x1b[38;5;196mworld\x1b[m", expectedANSI256: "hello \x1b[38;5;196mworld\x1b[m", expectedANSI: "hello \x1b[91mworld\x1b[m", expectedAscii: "hello \x1b[mworld\x1b[m", }, { name: "256 color bg", input: "\x1b[48;5;196mhello world\x1b[m", expectedTrueColor: "\x1b[48;5;196mhello world\x1b[m", expectedANSI256: "\x1b[48;5;196mhello world\x1b[m", expectedANSI: "\x1b[101mhello world\x1b[m", expectedAscii: "\x1b[mhello world\x1b[m", }, { name: "simple true color bg", input: "hello \x1b[38;2;255;133;55mworld\x1b[m", // #ff8537 expectedTrueColor: "hello \x1b[38;2;255;133;55mworld\x1b[m", expectedANSI256: "hello \x1b[38;5;209mworld\x1b[m", expectedANSI: "hello \x1b[91mworld\x1b[m", expectedAscii: "hello \x1b[mworld\x1b[m", }, { name: "itu true color bg", input: "hello \x1b[38:2::255:133:55mworld\x1b[m", // #ff8537 expectedTrueColor: "hello \x1b[38:2::255:133:55mworld\x1b[m", expectedANSI256: "hello \x1b[38;5;209mworld\x1b[m", expectedANSI: "hello \x1b[91mworld\x1b[m", expectedAscii: "hello \x1b[mworld\x1b[m", }, { name: "simple ansi 256 color bg", input: "hello \x1b[48:5:196mworld\x1b[m", expectedTrueColor: "hello \x1b[48:5:196mworld\x1b[m", expectedANSI256: "hello \x1b[48;5;196mworld\x1b[m", expectedANSI: "hello \x1b[101mworld\x1b[m", expectedAscii: "hello \x1b[mworld\x1b[m", }, { name: "simple missing param", input: "\x1b[31mhello \x1b[;1mworld", expectedTrueColor: "\x1b[31mhello \x1b[;1mworld", expectedANSI256: "\x1b[31mhello \x1b[;1mworld", expectedANSI: "\x1b[31mhello \x1b[;1mworld", expectedAscii: "\x1b[mhello \x1b[;1mworld", }, { name: "color with other attributes", input: "\x1b[1;38;5;204mhello \x1b[38;5;204mworld\x1b[m", expectedTrueColor: "\x1b[1;38;5;204mhello \x1b[38;5;204mworld\x1b[m", expectedANSI256: "\x1b[1;38;5;204mhello \x1b[38;5;204mworld\x1b[m", expectedANSI: "\x1b[1;91mhello \x1b[91mworld\x1b[m", expectedAscii: "\x1b[1mhello \x1b[mworld\x1b[m", }, } func TestWriter(t *testing.T) { for i, c := range writer_cases { for profile, writer := range writers { t.Run(c.name+"-"+profile.String(), func(t *testing.T) { var buf bytes.Buffer w := writer(&buf) _, err := w.Write([]byte(c.input)) if err != nil { t.Errorf("unexpected error: %v", err) } var expected string switch profile { case TrueColor: expected = c.expectedTrueColor case ANSI256: expected = c.expectedANSI256 case ANSI: expected = c.expectedANSI case Ascii: expected = c.expectedAscii case NoTTY: expected = ansi.Strip(c.input) } if got := buf.String(); got != expected { t.Errorf("case: %d, got: %q, expected: %q", i+1, got, expected) } }) } } } func TestNewWriterPanic(t *testing.T) { _ = NewWriter(io.Discard, []string{"TERM=dumb"}) } func TestNewWriterOsEnviron(t *testing.T) { w := NewWriter(io.Discard, os.Environ()) if w.Profile != NoTTY { t.Errorf("expected NoTTY, got %v", w.Profile) } } func BenchmarkWriter(b *testing.B) { w := &Writer{&bytes.Buffer{}, ANSI} input := []byte("\x1b[1;3;59mhello\x1b[m \x1b[38;2;255;133;55mworld\x1b[m") for i := 0; i < b.N; i++ { _, _ = w.Write(input) } }