"""Tools for color handling in xonsh.
This includes Convert values between RGB hex codes and xterm-256
color codes. Parts of this file were originally forked from Micah Elliott
http://MicahElliott.com Copyright (C) 2011 Micah Elliott. All rights reserved.
WTFPL http://sam.zoy.org/wtfpl/
"""
import math
import re
from xonsh.lib.lazyasd import LazyObject, lazyobject
from xonsh.tools import print_warning
_NO_COLOR_WARNING_SHOWN = False
RE_BACKGROUND = LazyObject(
lambda: re.compile("(BG#|BGHEX|BACKGROUND)"), globals(), "RE_BACKGROUND"
)
[docs]
class COLORS:
"""constants"""
RESET = "{RESET}"
RED = "{RED}"
GREEN = "{GREEN}"
BOLD_RED = "{BOLD_RED}"
BOLD_GREEN = "{BOLD_GREEN}"
@lazyobject
def KNOWN_XONSH_COLORS():
"""These are the minimum number of colors that need to be implemented by
any style.
"""
return frozenset(
[
"DEFAULT",
"BLACK",
"RED",
"GREEN",
"YELLOW",
"BLUE",
"PURPLE",
"CYAN",
"WHITE",
"INTENSE_BLACK",
"INTENSE_RED",
"INTENSE_GREEN",
"INTENSE_YELLOW",
"INTENSE_BLUE",
"INTENSE_PURPLE",
"INTENSE_CYAN",
"INTENSE_WHITE",
]
)
@lazyobject
def BASE_XONSH_COLORS():
return {
"BLACK": (0, 0, 0),
"RED": (170, 0, 0),
"GREEN": (0, 170, 0),
"YELLOW": (170, 85, 0),
"BLUE": (0, 0, 170),
"PURPLE": (170, 0, 170),
"CYAN": (0, 170, 170),
"WHITE": (170, 170, 170),
"INTENSE_BLACK": (85, 85, 85),
"INTENSE_RED": (255, 85, 85),
"INTENSE_GREEN": (85, 255, 85),
"INTENSE_YELLOW": (255, 255, 85),
"INTENSE_BLUE": (85, 85, 255),
"INTENSE_PURPLE": (255, 85, 255),
"INTENSE_CYAN": (85, 255, 255),
"INTENSE_WHITE": (255, 255, 255),
}
@lazyobject
def RE_XONSH_COLOR():
hex = "[0-9a-fA-F]"
s = (
# background
r"((?P<background>BACKGROUND_)|(?P<modifiers>("
# modifiers, only apply to foreground
r"BOLD_|FAINT_|ITALIC_|UNDERLINE_|SLOWBLINK_|FASTBLINK_|INVERT_|CONCEAL_|"
r"STRIKETHROUGH_|BOLDOFF_|FAINTOFF_|ITALICOFF_|UNDERLINEOFF_|BLINKOFF_|"
r"INVERTOFF_|REVEALOFF_|STRIKETHROUGHOFF_)+))?"
# colors
r"(?P<color>BLACK|RED|GREEN|YELLOW|BLUE|PURPLE|CYAN|WHITE|INTENSE_BLACK|"
r"INTENSE_RED|INTENSE_GREEN|INTENSE_YELLOW|INTENSE_BLUE|INTENSE_PURPLE|"
r"INTENSE_CYAN|INTENSE_WHITE|#" + hex + "{3}|#" + hex + "{6}|DEFAULT)"
)
bghex = (
"bg#" + hex + "{3}|"
"bg#" + hex + "{6}|"
"BG#" + hex + "{3}|"
"BG#" + hex + "{6}"
)
s = "^((?P<reset>RESET|NO_COLOR)|(?P<bghex>" + bghex + ")|" + s + ")$"
return re.compile(s)
[docs]
def iscolor(s):
"""Tests if a string is a valid color"""
return RE_XONSH_COLOR.match(s) is not None
@lazyobject
def CLUT():
"""color look-up table"""
return [
# 8-bit, RGB hex
# Primary 3-bit (8 colors). Unique representation!
("0", "000000"),
("1", "800000"),
("2", "008000"),
("3", "808000"),
("4", "000080"),
("5", "800080"),
("6", "008080"),
("7", "c0c0c0"),
# Equivalent "bright" versions of original 8 colors.
("8", "808080"),
("9", "ff0000"),
("10", "00ff00"),
("11", "ffff00"),
("12", "0000ff"),
("13", "ff00ff"),
("14", "00ffff"),
("15", "ffffff"),
# Strictly ascending.
("16", "000000"),
("17", "00005f"),
("18", "000087"),
("19", "0000af"),
("20", "0000d7"),
("21", "0000ff"),
("22", "005f00"),
("23", "005f5f"),
("24", "005f87"),
("25", "005faf"),
("26", "005fd7"),
("27", "005fff"),
("28", "008700"),
("29", "00875f"),
("30", "008787"),
("31", "0087af"),
("32", "0087d7"),
("33", "0087ff"),
("34", "00af00"),
("35", "00af5f"),
("36", "00af87"),
("37", "00afaf"),
("38", "00afd7"),
("39", "00afff"),
("40", "00d700"),
("41", "00d75f"),
("42", "00d787"),
("43", "00d7af"),
("44", "00d7d7"),
("45", "00d7ff"),
("46", "00ff00"),
("47", "00ff5f"),
("48", "00ff87"),
("49", "00ffaf"),
("50", "00ffd7"),
("51", "00ffff"),
("52", "5f0000"),
("53", "5f005f"),
("54", "5f0087"),
("55", "5f00af"),
("56", "5f00d7"),
("57", "5f00ff"),
("58", "5f5f00"),
("59", "5f5f5f"),
("60", "5f5f87"),
("61", "5f5faf"),
("62", "5f5fd7"),
("63", "5f5fff"),
("64", "5f8700"),
("65", "5f875f"),
("66", "5f8787"),
("67", "5f87af"),
("68", "5f87d7"),
("69", "5f87ff"),
("70", "5faf00"),
("71", "5faf5f"),
("72", "5faf87"),
("73", "5fafaf"),
("74", "5fafd7"),
("75", "5fafff"),
("76", "5fd700"),
("77", "5fd75f"),
("78", "5fd787"),
("79", "5fd7af"),
("80", "5fd7d7"),
("81", "5fd7ff"),
("82", "5fff00"),
("83", "5fff5f"),
("84", "5fff87"),
("85", "5fffaf"),
("86", "5fffd7"),
("87", "5fffff"),
("88", "870000"),
("89", "87005f"),
("90", "870087"),
("91", "8700af"),
("92", "8700d7"),
("93", "8700ff"),
("94", "875f00"),
("95", "875f5f"),
("96", "875f87"),
("97", "875faf"),
("98", "875fd7"),
("99", "875fff"),
("100", "878700"),
("101", "87875f"),
("102", "878787"),
("103", "8787af"),
("104", "8787d7"),
("105", "8787ff"),
("106", "87af00"),
("107", "87af5f"),
("108", "87af87"),
("109", "87afaf"),
("110", "87afd7"),
("111", "87afff"),
("112", "87d700"),
("113", "87d75f"),
("114", "87d787"),
("115", "87d7af"),
("116", "87d7d7"),
("117", "87d7ff"),
("118", "87ff00"),
("119", "87ff5f"),
("120", "87ff87"),
("121", "87ffaf"),
("122", "87ffd7"),
("123", "87ffff"),
("124", "af0000"),
("125", "af005f"),
("126", "af0087"),
("127", "af00af"),
("128", "af00d7"),
("129", "af00ff"),
("130", "af5f00"),
("131", "af5f5f"),
("132", "af5f87"),
("133", "af5faf"),
("134", "af5fd7"),
("135", "af5fff"),
("136", "af8700"),
("137", "af875f"),
("138", "af8787"),
("139", "af87af"),
("140", "af87d7"),
("141", "af87ff"),
("142", "afaf00"),
("143", "afaf5f"),
("144", "afaf87"),
("145", "afafaf"),
("146", "afafd7"),
("147", "afafff"),
("148", "afd700"),
("149", "afd75f"),
("150", "afd787"),
("151", "afd7af"),
("152", "afd7d7"),
("153", "afd7ff"),
("154", "afff00"),
("155", "afff5f"),
("156", "afff87"),
("157", "afffaf"),
("158", "afffd7"),
("159", "afffff"),
("160", "d70000"),
("161", "d7005f"),
("162", "d70087"),
("163", "d700af"),
("164", "d700d7"),
("165", "d700ff"),
("166", "d75f00"),
("167", "d75f5f"),
("168", "d75f87"),
("169", "d75faf"),
("170", "d75fd7"),
("171", "d75fff"),
("172", "d78700"),
("173", "d7875f"),
("174", "d78787"),
("175", "d787af"),
("176", "d787d7"),
("177", "d787ff"),
("178", "d7af00"),
("179", "d7af5f"),
("180", "d7af87"),
("181", "d7afaf"),
("182", "d7afd7"),
("183", "d7afff"),
("184", "d7d700"),
("185", "d7d75f"),
("186", "d7d787"),
("187", "d7d7af"),
("188", "d7d7d7"),
("189", "d7d7ff"),
("190", "d7ff00"),
("191", "d7ff5f"),
("192", "d7ff87"),
("193", "d7ffaf"),
("194", "d7ffd7"),
("195", "d7ffff"),
("196", "ff0000"),
("197", "ff005f"),
("198", "ff0087"),
("199", "ff00af"),
("200", "ff00d7"),
("201", "ff00ff"),
("202", "ff5f00"),
("203", "ff5f5f"),
("204", "ff5f87"),
("205", "ff5faf"),
("206", "ff5fd7"),
("207", "ff5fff"),
("208", "ff8700"),
("209", "ff875f"),
("210", "ff8787"),
("211", "ff87af"),
("212", "ff87d7"),
("213", "ff87ff"),
("214", "ffaf00"),
("215", "ffaf5f"),
("216", "ffaf87"),
("217", "ffafaf"),
("218", "ffafd7"),
("219", "ffafff"),
("220", "ffd700"),
("221", "ffd75f"),
("222", "ffd787"),
("223", "ffd7af"),
("224", "ffd7d7"),
("225", "ffd7ff"),
("226", "ffff00"),
("227", "ffff5f"),
("228", "ffff87"),
("229", "ffffaf"),
("230", "ffffd7"),
("231", "ffffff"),
# Gray-scale range.
("232", "080808"),
("233", "121212"),
("234", "1c1c1c"),
("235", "262626"),
("236", "303030"),
("237", "3a3a3a"),
("238", "444444"),
("239", "4e4e4e"),
("240", "585858"),
("241", "626262"),
("242", "6c6c6c"),
("243", "767676"),
("244", "808080"),
("245", "8a8a8a"),
("246", "949494"),
("247", "9e9e9e"),
("248", "a8a8a8"),
("249", "b2b2b2"),
("250", "bcbcbc"),
("251", "c6c6c6"),
("252", "d0d0d0"),
("253", "dadada"),
("254", "e4e4e4"),
("255", "eeeeee"),
]
def _str2hex(hexstr):
return int(hexstr, 16)
def _strip_hash(rgb):
# Strip leading `#` if exists.
if rgb.startswith("#"):
rgb = rgb.lstrip("#")
return rgb
@lazyobject
def SHORT_TO_RGB():
return dict(CLUT)
@lazyobject
def RGB_TO_SHORT():
return {v: k for k, v in SHORT_TO_RGB.items()}
[docs]
def short2rgb(short):
short = short.lstrip("0")
if short == "":
short = "0"
return SHORT_TO_RGB[short]
[docs]
def rgb_to_256(rgb):
"""Find the closest ANSI 256 approximation to the given RGB value.
>>> rgb2short('123456')
('23', '005f5f')
>>> rgb2short('ffffff')
('231', 'ffffff')
>>> rgb2short('0DADD6') # vimeo logo
('38', '00afd7')
Parameters
----------
rgb : Hex code representing an RGB value, eg, 'abcdef'
Returns
-------
Tuple of String between 0 and 255 (compatible with xterm) and
hex code (length-6).
"""
rgb = rgb.lstrip("#")
if len(rgb) == 0:
return "0", "000000"
incs = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF)
# Break 6-char RGB code into 3 integer vals.
parts = rgb_to_ints(rgb)
res = []
for part in parts:
i = 0
while i < len(incs) - 1:
s, b = incs[i], incs[i + 1] # smaller, bigger
if s <= part <= b:
s1 = abs(s - part)
b1 = abs(b - part)
if s1 < b1:
closest = s
else:
closest = b
res.append(closest)
break
i += 1
res = "".join([f"{i:02x}" for i in res])
equiv = RGB_TO_SHORT[res]
return equiv, res
rgb2short = rgb_to_256
@lazyobject
def RE_RGB3():
return re.compile(r"(.)(.)(.)")
@lazyobject
def RE_RGB6():
return re.compile(r"(..)(..)(..)")
[docs]
def rgb_to_ints(rgb):
if len(rgb) == 6:
return tuple(int(h, 16) for h in RE_RGB6.split(rgb)[1:4])
else:
return tuple(int(h * 2, 16) for h in RE_RGB3.split(rgb)[1:4])
[docs]
def short_to_ints(short):
"""Coverts a short (256) color to a 3-tuple of ints."""
return rgb_to_ints(short2rgb(short))
[docs]
def color_dist(x, y) -> float:
return math.sqrt((x[0] - y[0]) ** 2 + (x[1] - y[1]) ** 2 + (x[2] - y[2]) ** 2)
[docs]
def find_closest_color(x, palette):
return min(
sorted(palette.keys(), reverse=True), key=lambda k: color_dist(x, palette[k])
)
[docs]
def make_palette(strings):
"""Makes a color palette from a collection of strings."""
palette = {}
for s in strings:
while "#" in s:
_, t = s.split("#", 1)
t, _, s = t.partition(" ")
palette[t] = rgb_to_ints(t)
return palette
[docs]
def warn_deprecated_no_color():
"""Show a warning once if NO_COLOR was used instead of RESET."""
global _NO_COLOR_WARNING_SHOWN
if not _NO_COLOR_WARNING_SHOWN:
print_warning("NO_COLOR is deprecated and should be replaced with RESET.")
_NO_COLOR_WARNING_SHOWN = True