saneterm

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, corresponding
101      to either the 90-97 or 100-107 SGR parameters. data is always
102      a member of the BasicColor enum.
103    * ColorType.NUMBERED_256: a color of the 256 color palette
104      supported by the SGR sequence parameters 38 and 48. data
105      is always an integer in the range [0;255]
106    * ColorType.TRUECOLOR: a true RGB color as supported by SGR
107      sequence parameters 38 and 48. data should be a triple of
108      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")
113
114        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')
127
128        self.type = t
129        self.data = data
130
131    # TODO: can we prevent mutation of this object?
132    def __hash__(self):
133        return hash((self.type, self.data))
134
135    def __eq__(self, other):
136        return self.type == other.type and self.data == other.data
137
138    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 X11
142        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 colors
153                return basic_color_to_rgba(BasicColor(self.data), bright=False)
154            elif self.data < EXTENDED_COLOR_CUBE_LOWER:
155                # bright 8 colors
156                return basic_color_to_rgba(
157                    BasicColor(self.data - EXTENDED_COLOR_BRIGHT_LOWER),
158                    bright=True
159                )
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 + b
165                # * r, g, b are all >= 0 and < 6
166                # * The corresponding color channel value for the r, g, b
167                #   values can be obtained using the following expression:
168                #   x * 40 + 55 if x > 0 else 0
169                #
170                # This is not documented anywhere as far as I am aware.
171                # The information presented here has been reverse engineered
172                # from XTerm's 256colres.pl.
173                tmp = self.data - EXTENDED_COLOR_CUBE_LOWER
174                (r, tmp) = divmod(tmp, 6 * 6)
175                (g, b) = divmod(tmp, 6)
176
177                triple = tuple(map(extended_color_val, (r, g, b)))
178                return Gdk.RGBA(*triple)
179            else:
180                # grayscale in 24 steps
181                c = (self.data - EXTENDED_COLOR_GRAYSCALE_LOWER) * (1.0/24)
182                return Gdk.RGBA(c, c, c, 1.0)