pax_global_header 0000666 0000000 0000000 00000000064 15047201503 0014507 g ustar 00root root 0000000 0000000 52 comment=561b8ac1cff6f8c286c7dd86e95cab3875c7ac01 colorprofile-0.3.2/ 0000775 0000000 0000000 00000000000 15047201503 0014210 5 ustar 00root root 0000000 0000000 colorprofile-0.3.2/.github/ 0000775 0000000 0000000 00000000000 15047201503 0015550 5 ustar 00root root 0000000 0000000 colorprofile-0.3.2/.github/CODEOWNERS 0000664 0000000 0000000 00000000021 15047201503 0017134 0 ustar 00root root 0000000 0000000 * @aymanbagabas colorprofile-0.3.2/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15047201503 0017733 5 ustar 00root root 0000000 0000000 colorprofile-0.3.2/.github/ISSUE_TEMPLATE/bug.yml 0000664 0000000 0000000 00000003251 15047201503 0021234 0 ustar 00root root 0000000 0000000 name: 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.md 0000664 0000000 0000000 00000001474 15047201503 0022433 0 ustar 00root root 0000000 0000000 --- 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.yml 0000664 0000000 0000000 00000000170 15047201503 0021721 0 ustar 00root root 0000000 0000000 blank_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.md 0000664 0000000 0000000 00000001134 15047201503 0023457 0 ustar 00root root 0000000 0000000 --- 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.yml 0000664 0000000 0000000 00000002534 15047201503 0020404 0 ustar 00root root 0000000 0000000 version: 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/ 0000775 0000000 0000000 00000000000 15047201503 0017605 5 ustar 00root root 0000000 0000000 colorprofile-0.3.2/.github/workflows/build.yml 0000664 0000000 0000000 00000000401 15047201503 0021422 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000235 15047201503 0022123 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000643 15047201503 0023412 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000417 15047201503 0022252 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000163 15047201503 0021276 0 ustar 00root root 0000000 0000000 name: lint on: push: pull_request: jobs: lint: uses: charmbracelet/meta/.github/workflows/lint.yml@main colorprofile-0.3.2/.github/workflows/release.yml 0000664 0000000 0000000 00000002122 15047201503 0021745 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001267 15047201503 0016602 0 ustar 00root root 0000000 0000000 version: "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.yml 0000664 0000000 0000000 00000000237 15047201503 0017143 0 ustar 00root root 0000000 0000000 includes: - from_url: url: charmbracelet/meta/main/goreleaser-lib.yaml # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json colorprofile-0.3.2/LICENSE 0000664 0000000 0000000 00000002070 15047201503 0015214 0 ustar 00root root 0000000 0000000 MIT 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.md 0000664 0000000 0000000 00000006427 15047201503 0015500 0 ustar 00root root 0000000 0000000 # Colorprofile
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).
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
colorprofile-0.3.2/doc.go 0000664 0000000 0000000 00000000305 15047201503 0015302 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000016765 15047201503 0015346 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000217 15047201503 0016530 0 ustar 00root root 0000000 0000000 //go:build !windows
// +build !windows
package colorprofile
func windowsColorProfile(map[string]string) (Profile, bool) {
return 0, false
}
colorprofile-0.3.2/env_test.go 0000664 0000000 0000000 00000007327 15047201503 0016377 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001546 15047201503 0017107 0 ustar 00root root 0000000 0000000 //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/ 0000775 0000000 0000000 00000000000 15047201503 0016026 5 ustar 00root root 0000000 0000000 colorprofile-0.3.2/examples/go.mod 0000664 0000000 0000000 00000001032 15047201503 0017130 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000003023 15047201503 0017157 0 ustar 00root root 0000000 0000000 github.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/ 0000775 0000000 0000000 00000000000 15047201503 0017466 5 ustar 00root root 0000000 0000000 colorprofile-0.3.2/examples/profile/main.go 0000664 0000000 0000000 00000004407 15047201503 0020746 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 15047201503 0017342 5 ustar 00root root 0000000 0000000 colorprofile-0.3.2/examples/writer/writer.go 0000664 0000000 0000000 00000000647 15047201503 0021214 0 ustar 00root root 0000000 0000000 // 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.mod 0000664 0000000 0000000 00000000601 15047201503 0015313 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000003025 15047201503 0015343 0 ustar 00root root 0000000 0000000 github.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.go 0000664 0000000 0000000 00000003453 15047201503 0016204 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000012055 15047201503 0017241 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000011051 15047201503 0016051 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000013711 15047201503 0017115 0 ustar 00root root 0000000 0000000 package 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)
}
}