about summary refs log tree commit diff
path: root/saneterm/color.py
blob: 06e1ade510acae8e3947744ef1435b0e4691b870 (plain) (blame)
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
from enum import Enum, auto, unique

from gi.repository import Gdk

@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 < 8:
                # normal 8 colors
                return basic_color_to_rgba(BasicColor(self.data), bright=False)
            elif self.data >= 8 and self.data < 16:
                # bright 8 colors
                return basic_color_to_rgba(BasicColor(self.data - 8), bright=True)
            elif self.data >= 16 and self.data < 232:
                # 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 - 16
                (r, tmp) = divmod(tmp, 36)
                (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 - 232) * (1.0/24)
                return Gdk.RGBA(c, c, c, 1.0)