Modern line-oriented terminal emulator without support for TUIs
git clone https://git.8pit.net/saneterm.git
1from enum import Enum, auto, unique 2 3from gi.repository import Gdk 4 5# lower bounds for the extended (256) color sections 6# except for the regular 8 colors: 7# 8# 0 - 7 regular 8 colors 9# 8 - 15 bright 8 colors 10# 16 - 231 6 * 6 * 6 color cube 11# 232 - 255 24 step grayscale 12# 13# For a description of the sections and their meaning 14# as well as color values see the comment in Color.to_gdk() 15EXTENDED_COLOR_BRIGHT_LOWER = 8 16EXTENDED_COLOR_CUBE_LOWER = 16 17EXTENDED_COLOR_GRAYSCALE_LOWER = 232 18 19@unique 20class BasicColor(Enum): 21 BLACK = 0 22 RED = 1 23 GREEN = 2 24 YELLOW = 3 25 BLUE = 4 26 MAGENTA = 5 27 CYAN = 6 28 WHITE = 7 29 30# colors are (almost) the same as XTerm's default ones, 31# see https://en.wikipedia.org/wiki/X11_color_names for values 32BASIC_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} 42 43BASIC_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} 53 54class ColorType(Enum): 55 NUMBERED_8 = auto() 56 NUMBERED_8_BRIGHT = auto() 57 NUMBERED_256 = auto() 58 TRUECOLOR = auto() 59 60def extended_color_val(x): 61 """ 62 Convert a 256 color cube axis index into 63 its corresponding color channel value. 64 """ 65 val = x * 40 + 55 if x > 0 else 0 66 return val / 255 67 68def int_triple_to_rgba(c): 69 """ 70 Convert a triple of the form (r, g, b) into 71 a valid Gdk.RGBA where r, g and b are integers 72 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) 76 77def basic_color_to_rgba(n, bright=False): 78 """ 79 Convert a BasicColor into a Gdk.RGBA object using 80 the BASIC_COLOR_NAMES_* lookup tables. Raises an 81 AssertionFailure if the conversion fails. 82 """ 83 color = Gdk.RGBA() 84 85 if bright: 86 assert color.parse(BASIC_COLOR_NAMES_BRIGHT[n]) 87 else: 88 assert color.parse(BASIC_COLOR_NAMES_REGULAR[n]) 89 90 return color 91 92class Color(object): 93 """ 94 Color represents all possible types of colors 95 used in SGR escape sequences: 96 97 * ColorType.NUMBERED_8: regular BasicColor, corresponding to 98 either the 30-37 or 40-47 SGR parameters. data is always 99 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)