1from enum import Enum, auto, unique23from gi.repository import Gdk45# lower bounds for the extended (256) color sections6# except for the regular 8 colors:7#8# 0 - 7 regular 8 colors9# 8 - 15 bright 8 colors10# 16 - 231 6 * 6 * 6 color cube11# 232 - 255 24 step grayscale12#13# For a description of the sections and their meaning14# as well as color values see the comment in Color.to_gdk()15EXTENDED_COLOR_BRIGHT_LOWER = 816EXTENDED_COLOR_CUBE_LOWER = 1617EXTENDED_COLOR_GRAYSCALE_LOWER = 2321819@unique20class BasicColor(Enum):21 BLACK = 022 RED = 123 GREEN = 224 YELLOW = 325 BLUE = 426 MAGENTA = 527 CYAN = 628 WHITE = 72930# colors are (almost) the same as XTerm's default ones,31# see https://en.wikipedia.org/wiki/X11_color_names for values32BASIC_COLOR_NAMES_REGULAR = {33 BasicColor.BLACK : "black",34 BasicColor.RED : "red3",35 BasicColor.GREEN : "green3",36 BasicColor.YELLOW : "yellow3",37 BasicColor.BLUE : "blue2",38 BasicColor.MAGENTA : "magenta3",39 BasicColor.CYAN : "cyan3",40 BasicColor.WHITE : "gray90",41}4243BASIC_COLOR_NAMES_BRIGHT = {44 BasicColor.BLACK : "gray50",45 BasicColor.RED : "red",46 BasicColor.GREEN : "green",47 BasicColor.YELLOW : "yellow",48 BasicColor.BLUE : "CornflowerBlue",49 BasicColor.MAGENTA : "magenta",50 BasicColor.CYAN : "cyan",51 BasicColor.WHITE : "white",52}5354class ColorType(Enum):55 NUMBERED_8 = auto()56 NUMBERED_8_BRIGHT = auto()57 NUMBERED_256 = auto()58 TRUECOLOR = auto()5960def extended_color_val(x):61 """62 Convert a 256 color cube axis index into63 its corresponding color channel value.64 """65 val = x * 40 + 55 if x > 0 else 066 return val / 2556768def int_triple_to_rgba(c):69 """70 Convert a triple of the form (r, g, b) into71 a valid Gdk.RGBA where r, g and b are integers72 in the range [0;255].73 """74 (r, g, b) = tuple(map(lambda x: x / 255, c))75 return Gdk.RGBA(r, g, b, 1)7677def basic_color_to_rgba(n, bright=False):78 """79 Convert a BasicColor into a Gdk.RGBA object using80 the BASIC_COLOR_NAMES_* lookup tables. Raises an81 AssertionFailure if the conversion fails.82 """83 color = Gdk.RGBA()8485 if bright:86 assert color.parse(BASIC_COLOR_NAMES_BRIGHT[n])87 else:88 assert color.parse(BASIC_COLOR_NAMES_REGULAR[n])8990 return color9192class Color(object):93 """94 Color represents all possible types of colors95 used in SGR escape sequences:9697 * ColorType.NUMBERED_8: regular BasicColor, corresponding to98 either the 30-37 or 40-47 SGR parameters. data is always99 a member of the BasicColor enum.100 * ColorType.NUMBERED_8_BRIGHT: bright BasicColor, corresponding101 to either the 90-97 or 100-107 SGR parameters. data is always102 a member of the BasicColor enum.103 * ColorType.NUMBERED_256: a color of the 256 color palette104 supported by the SGR sequence parameters 38 and 48. data105 is always an integer in the range [0;255]106 * ColorType.TRUECOLOR: a true RGB color as supported by SGR107 sequence parameters 38 and 48. data should be a triple of108 integers in the range [0;255].109 """110 def __init__(self, t, data):111 if not isinstance(t, ColorType):112 raise TypeError("type must be ColorType")113114 if t is ColorType.TRUECOLOR:115 if not type(data) is tuple:116 raise TypeError("data must be tuple for TRUECOLOR")117 if not len(data) == 3:118 raise TypeError("tuple must have 3 elements for TRUECOLOR")119 elif t is ColorType.NUMBERED_8 or t is ColorType.NUMBERED_8_BRIGHT:120 if not isinstance(data, BasicColor):121 raise TypeError(f'data must be BasicColor for {t}')122 elif t is ColorType.NUMBERED_256:123 if not type(data) is int:124 raise TypeError('data must be integer for NUMBERED_256')125 if not (data >= 0 and data < 256):126 raise TypeError('data must be in range [0;255] for NUMBERED_256')127128 self.type = t129 self.data = data130131 # TODO: can we prevent mutation of this object?132 def __hash__(self):133 return hash((self.type, self.data))134135 def __eq__(self, other):136 return self.type == other.type and self.data == other.data137138 def to_gdk(self):139 """140 Convert a Color into a Gdk.RGBA which TextTag accepts.141 The color scheme for the 16 color part uses default X11142 colors and is currently not configurable.143 """144 if self.type is ColorType.NUMBERED_8:145 return basic_color_to_rgba(self.data, bright=False)146 elif self.type is ColorType.NUMBERED_8_BRIGHT:147 return basic_color_to_rgba(self.data, bright=True)148 elif self.type is ColorType.TRUECOLOR:149 return int_triple_to_rgba(self.data)150 elif self.type is ColorType.NUMBERED_256:151 if self.data < EXTENDED_COLOR_BRIGHT_LOWER:152 # normal 8 colors153 return basic_color_to_rgba(BasicColor(self.data), bright=False)154 elif self.data < EXTENDED_COLOR_CUBE_LOWER:155 # bright 8 colors156 return basic_color_to_rgba(157 BasicColor(self.data - EXTENDED_COLOR_BRIGHT_LOWER),158 bright=True159 )160 elif self.data < EXTENDED_COLOR_GRAYSCALE_LOWER:161 # color cube which is constructed in the following manner:162 #163 # * The color number is described by the following formula:164 # n = 16 + 36r + 6g + b165 # * r, g, b are all >= 0 and < 6166 # * The corresponding color channel value for the r, g, b167 # values can be obtained using the following expression:168 # x * 40 + 55 if x > 0 else 0169 #170 # This is not documented anywhere as far as I am aware.171 # The information presented here has been reverse engineered172 # from XTerm's 256colres.pl.173 tmp = self.data - EXTENDED_COLOR_CUBE_LOWER174 (r, tmp) = divmod(tmp, 6 * 6)175 (g, b) = divmod(tmp, 6)176177 triple = tuple(map(extended_color_val, (r, g, b)))178 return Gdk.RGBA(*triple)179 else:180 # grayscale in 24 steps181 c = (self.data - EXTENDED_COLOR_GRAYSCALE_LOWER) * (1.0/24)182 return Gdk.RGBA(c, c, c, 1.0)