1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
from enum import Enum, auto, unique
from gi.repository import Gdk
# lower bounds for the extended (256) color sections
# except for the regular 8 colors:
#
# 0 - 7 regular 8 colors
# 8 - 15 bright 8 colors
# 16 - 231 6 * 6 * 6 color cube
# 232 - 255 24 step grayscale
#
# For a description of the sections and their meaning
# as well as color values see the comment in Color.to_gdk()
EXTENDED_COLOR_BRIGHT_LOWER = 8
EXTENDED_COLOR_CUBE_LOWER = 16
EXTENDED_COLOR_GRAYSCALE_LOWER = 232
@unique
class BasicColor(Enum):
BLACK = 0
RED = 1
GREEN = 2
YELLOW = 3
BLUE = 4
MAGENTA = 5
CYAN = 6
WHITE = 7
# colors are (almost) the same as XTerm's default ones,
# see https://en.wikipedia.org/wiki/X11_color_names for values
BASIC_COLOR_NAMES_REGULAR = {
BasicColor.BLACK : "black",
BasicColor.RED : "red3",
BasicColor.GREEN : "green3",
BasicColor.YELLOW : "yellow3",
BasicColor.BLUE : "blue2",
BasicColor.MAGENTA : "magenta3",
BasicColor.CYAN : "cyan3",
BasicColor.WHITE : "gray90",
}
BASIC_COLOR_NAMES_BRIGHT = {
BasicColor.BLACK : "gray50",
BasicColor.RED : "red",
BasicColor.GREEN : "green",
BasicColor.YELLOW : "yellow",
BasicColor.BLUE : "CornflowerBlue",
BasicColor.MAGENTA : "magenta",
BasicColor.CYAN : "cyan",
BasicColor.WHITE : "white",
}
class ColorType(Enum):
NUMBERED_8 = auto()
NUMBERED_8_BRIGHT = auto()
NUMBERED_256 = auto()
TRUECOLOR = auto()
def extended_color_val(x):
"""
Convert a 256 color cube axis index into
its corresponding color channel value.
"""
val = x * 40 + 55 if x > 0 else 0
return val / 255
def int_triple_to_rgba(c):
"""
Convert a triple of the form (r, g, b) into
a valid Gdk.RGBA where r, g and b are integers
in the range [0;255].
"""
(r, g, b) = tuple(map(lambda x: x / 255, c))
return Gdk.RGBA(r, g, b, 1)
def basic_color_to_rgba(n, bright=False):
"""
Convert a BasicColor into a Gdk.RGBA object using
the BASIC_COLOR_NAMES_* lookup tables. Raises an
AssertionFailure if the conversion fails.
"""
color = Gdk.RGBA()
if bright:
assert color.parse(BASIC_COLOR_NAMES_BRIGHT[n])
else:
assert color.parse(BASIC_COLOR_NAMES_REGULAR[n])
return color
class Color(object):
"""
Color represents all possible types of colors
used in SGR escape sequences:
* ColorType.NUMBERED_8: regular BasicColor, corresponding to
either the 30-37 or 40-47 SGR parameters. data is always
a member of the BasicColor enum.
* ColorType.NUMBERED_8_BRIGHT: bright BasicColor, corresponding
to either the 90-97 or 100-107 SGR parameters. data is always
a member of the BasicColor enum.
* ColorType.NUMBERED_256: a color of the 256 color palette
supported by the SGR sequence parameters 38 and 48. data
is always an integer in the range [0;255]
* ColorType.TRUECOLOR: a true RGB color as supported by SGR
sequence parameters 38 and 48. data should be a triple of
integers in the range [0;255].
"""
def __init__(self, t, data):
if not isinstance(t, ColorType):
raise TypeError("type must be ColorType")
if t is ColorType.TRUECOLOR:
if not type(data) is tuple:
raise TypeError("data must be tuple for TRUECOLOR")
if not len(data) == 3:
raise TypeError("tuple must have 3 elements for TRUECOLOR")
elif t is ColorType.NUMBERED_8 or t is ColorType.NUMBERED_8_BRIGHT:
if not isinstance(data, BasicColor):
raise TypeError(f'data must be BasicColor for {t}')
elif t is ColorType.NUMBERED_256:
if not type(data) is int:
raise TypeError('data must be integer for NUMBERED_256')
if not (data >= 0 and data < 256):
raise TypeError('data must be in range [0;255] for NUMBERED_256')
self.type = t
self.data = data
# TODO: can we prevent mutation of this object?
def __hash__(self):
return hash((self.type, self.data))
def __eq__(self, other):
return self.type == other.type and self.data == other.data
def to_gdk(self):
"""
Convert a Color into a Gdk.RGBA which TextTag accepts.
The color scheme for the 16 color part uses default X11
colors and is currently not configurable.
"""
if self.type is ColorType.NUMBERED_8:
return basic_color_to_rgba(self.data, bright=False)
elif self.type is ColorType.NUMBERED_8_BRIGHT:
return basic_color_to_rgba(self.data, bright=True)
elif self.type is ColorType.TRUECOLOR:
return int_triple_to_rgba(self.data)
elif self.type is ColorType.NUMBERED_256:
if self.data < EXTENDED_COLOR_BRIGHT_LOWER:
# normal 8 colors
return basic_color_to_rgba(BasicColor(self.data), bright=False)
elif self.data < EXTENDED_COLOR_CUBE_LOWER:
# bright 8 colors
return basic_color_to_rgba(
BasicColor(self.data - EXTENDED_COLOR_BRIGHT_LOWER),
bright=True
)
elif self.data < EXTENDED_COLOR_GRAYSCALE_LOWER:
# color cube which is constructed in the following manner:
#
# * The color number is described by the following formula:
# n = 16 + 36r + 6g + b
# * r, g, b are all >= 0 and < 6
# * The corresponding color channel value for the r, g, b
# values can be obtained using the following expression:
# x * 40 + 55 if x > 0 else 0
#
# This is not documented anywhere as far as I am aware.
# The information presented here has been reverse engineered
# from XTerm's 256colres.pl.
tmp = self.data - EXTENDED_COLOR_CUBE_LOWER
(r, tmp) = divmod(tmp, 6 * 6)
(g, b) = divmod(tmp, 6)
triple = tuple(map(extended_color_val, (r, g, b)))
return Gdk.RGBA(*triple)
else:
# grayscale in 24 steps
c = (self.data - EXTENDED_COLOR_GRAYSCALE_LOWER) * (1.0/24)
return Gdk.RGBA(c, c, c, 1.0)
|