Skip to content

colors

The module containing all of the color-centric features of this library.

This module provides a base class, Color, and a bunch of abstractions over it.

Shoutout to: https://stackoverflow.com/a/33206814, one of the best StackOverflow answers I've ever bumped into.

Color dataclass

A terminal color.

Parameters:

Name Type Description Default
value str

The data contained within this color.

required
background bool

Whether this color will represent a color.

False

These colors are all formattable. There are currently 2 'spec' strings: - f"{my_color:tim}" -> Returns self.markup - f"{my_color:seq}" -> Returns self.sequence

They can thus be used in TIM strings

ptg.tim.parse("[{my_color:tim}]Hello") '[]Hello'

And in normal, ANSI coded strings:

>>> "{my_color:seq}Hello"
'<my_color.sequence>Hello'
Source code in pytermgui/colors.py
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
@dataclass
class Color:  # pylint: disable=too-many-public-methods
    """A terminal color.

    Args:
        value: The data contained within this color.
        background: Whether this color will represent a color.

    These colors are all formattable. There are currently 2 'spec' strings:
    - f"{my_color:tim}" -> Returns self.markup
    - f"{my_color:seq}" -> Returns self.sequence

    They can thus be used in TIM strings:

        >>> ptg.tim.parse("[{my_color:tim}]Hello")
        '[<my_color.markup>]Hello'

    And in normal, ANSI coded strings:

        >>> "{my_color:seq}Hello"
        '<my_color.sequence>Hello'
    """

    value: str
    background: bool = False

    system: ColorSystem = field(init=False)

    default_foreground: Color | None = field(default=None, repr=False)
    default_background: Color | None = field(default=None, repr=False)

    _rgb: tuple[int, int, int] | None = field(init=False, default=None, repr=False)

    def __format__(self, spec: str) -> str:
        """Formats the color by the given specification."""

        if spec == "tim":
            return self.markup

        if spec == "seq":
            return self.sequence

        return repr(self)

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> Color:
        """Creates a color from the given RGB.

        Args:
            rgb: The RGB value to base the new color off of.
        """

        return RGBColor.from_rgb(rgb)

    @classmethod
    def from_hls(cls, hsl: RGBTriplet) -> Color:
        """Creates a color from the given HLS.

        HLS stands for Hue, Lightness & Saturation. It is more commonly known as HSL,
        but the `colorsys` library uses HLS instead so that's what we use too.

        Args:
            hsl: The HLS value to base the new color off of.
        """

        rgb = cast(
            RGBTriplet,
            map(lambda n: int(256 * n), colorsys.hls_to_rgb(*hsl)),
        )

        return RGBColor.from_rgb(rgb)

    @property
    def sequence(self) -> str:
        """Returns the ANSI sequence representation of the color."""

        raise NotImplementedError

    @cached_property
    def markup(self) -> str:
        """Returns the TIM representation of this color."""

        return ("@" if self.background else "") + self.value

    @cached_property
    def rgb(self) -> RGBTriplet:
        """Returns this color as a tuple of (red, green, blue) values."""

        if self._rgb is None:
            raise NotImplementedError

        return self._rgb

    @cached_property
    def red(self) -> Number:
        """Returns the red component of this color."""

        return self.rgb[0]

    @cached_property
    def green(self) -> Number:
        """Returns the red component of this color."""

        return self.rgb[1]

    @cached_property
    def blue(self) -> Number:
        """Returns the red component of this color."""

        return self.rgb[2]

    @cached_property
    def hls(self) -> RGBTriplet:
        """Returns the HLS (Hue, Lightness, Saturation) representation of this color."""

        return colorsys.rgb_to_hls(self.red / 256, self.green / 256, self.blue / 256)

    @cached_property
    def hue(self) -> float:
        """Returns the hue component of this color."""

        return self.hls[0]

    @cached_property
    def lightness(self) -> float:
        """Returns the lightness component of this color."""

        return self.hls[1]

    @cached_property
    def saturation(self) -> float:
        """Returns the saturation component of this color."""

        return self.hls[2]

    @cached_property
    def hex(self) -> str:
        """Returns CSS-like HEX representation of this color."""

        buff = "#"
        for color in self.rgb:
            buff += f"{format(color, 'x'):0>2}"

        return buff

    @classmethod
    def get_default_foreground(cls) -> Color:
        """Gets the terminal emulator's default foreground color."""

        if cls.default_foreground is not None:
            return cls.default_foreground

        return _get_palette_color("10")

    @classmethod
    def get_default_background(cls) -> Color:
        """Gets the terminal emulator's default foreground color."""

        if cls.default_background is not None:
            return cls.default_background

        return _get_palette_color("11")

    @property
    def name(self) -> str:
        """Returns the reverse-parseable name of this color."""

        return ("@" if self.background else "") + self.value

    @cached_property
    def luminance(self) -> float:
        """Returns this color's perceived luminance (brightness).

        From https://stackoverflow.com/a/596243
        """

        def _linearize(color: float) -> float:
            """Converts sRGB color to linear value."""

            if color <= 0.04045:
                return color / 12.92

            return ((color + 0.055) / 1.055) ** 2.4

        red, green, blue = float(self.rgb[0]), float(self.rgb[1]), float(self.rgb[2])

        red /= 255
        green /= 255
        blue /= 255

        red = _linearize(red)
        blue = _linearize(blue)
        green = _linearize(green)

        return 0.2126 * red + 0.7152 * green + 0.0722 * blue

    def hue_offset(self, offset: float) -> Color:
        """Returns the color offset by the given hue."""

        hue, lightness, saturation = colorsys.rgb_to_hls(
            self.red / 256, self.green / 256, self.blue / 256
        )

        hue = (hue + offset) % 1

        return Color.parse(
            ";".join(
                map(
                    lambda n: str(int(256 * n)),
                    colorsys.hls_to_rgb(hue, lightness, saturation),
                )
            ),
            background=self.background,
            localize=False,
        )

    @cached_property
    def brightness(self) -> float:
        """Returns the perceived "brightness" of a color.

        From https://stackoverflow.com/a/56678483
        """

        if self.luminance <= (216 / 24389):
            brightness = self.luminance * (24389 / 27)

        else:
            brightness = self.luminance ** (1 / 3) * 116 - 16

        return brightness / 100

    @cached_property
    def complement(self) -> Color:
        """Returns the complement of this color."""

        if self.hue == 0.0:
            return (
                Color.parse("#FFFFFF")
                if self.lightness == 0.0
                else Color.parse("#000000")
            )

        return self.hue_offset(0.5)

    @cached_property
    def triadic(self) -> tuple[Color, Color, Color]:
        """Computes the triadic group this color is in.

        Triadic colors are 3-way complements of eachother.

        Returns:
            This color, the first triadic element and the second one.
        """

        return self, self.hue_offset(1 / 3), self.hue_offset(2 / 3)

    @cached_property
    def tetradic(self) -> tuple[Color, Color, Color, Color]:
        """Computes the tetradic group this color is in.

        Tetradic colors are 4-way complements of eachother.

        Returns:
            This color, the first tetradic element and the second one.
        """

        return self, self.hue_offset(1 / 4), self.complement, self.hue_offset(3 / 4)

    @cached_property
    def analogous(self) -> tuple[Color, Color, Color]:
        """Computes the analogous group this colors is in.

        Analogous colors are located next to eachother on the color wheel.

        Returns:
            The color to the left, this color and the color to the right.
        """

        return self.hue_offset(-1 / 12), self, self.hue_offset(1 / 12)

    @cached_property
    def contrast(self) -> Color:
        """Returns a color (black or white) that complies with the W3C contrast ratio guidelines."""

        if self.luminance > 0.179:
            return Color.parse("#000000").blend_complement(0.05)

        return Color.parse("#FFFFFF").blend_complement(0.05)

    def blend(self, other: Color, alpha: float = 0.5, localize: bool = False) -> Color:
        """Blends a color into another one.

        Args:
            other: The color to blend with.
            alpha: How much the other color should influence the outcome.
            localize: If set, the returned color will returned its localized version by running
                `get_localized` on it before returning.

        Returns:
            A `Color` that is the result of the blending.
        """

        red1, green1, blue1 = self.rgb
        red2, green2, blue2 = other.rgb

        blended: Color = RGBColor.from_rgb(
            (
                int(red1 + (red2 - red1) * alpha),
                int(green1 + (green2 - green1) * alpha),
                int(blue1 + (blue2 - blue1) * alpha),
            )
        )

        if localize:
            blended = blended.get_localized()

        return blended

    def blend_complement(self, alpha: float = 0.5) -> Color:
        """Blends this color with its complement.

        See `Color.blend`.
        """

        return self.blend(self.complement, alpha)

    def blend_contrast(self, alpha: float = 0.5) -> Color:
        """Blends this color with its contrast pair.

        See `Color.blend`.
        """

        return self.blend(self.contrast, alpha)

    def darken(self, alpha: float = 0.5) -> Color:
        """Darkens the color by blending it with black, using the alpha provided."""

        return self.blend(Color.parse("#000000"), alpha)

    def lighten(self, alpha: float = 0.5) -> Color:
        """Lightens the color by blending it with white, using the alpha provided."""

        return self.blend(Color.parse("#FFFFFF"), alpha)

    @classmethod
    def parse(
        cls,
        text: str,
        background: bool = False,  # pylint: disable=redefined-outer-name
        localize: bool = True,
        use_cache: bool = False,
    ) -> Color:
        """Uses `str_to_color` to parse some text into a `Color`."""

        return str_to_color(
            text=text,
            is_background=background,
            localize=localize,
            use_cache=use_cache,
        )

    def __call__(self, text: str, reset: bool = True) -> str:
        """Colors the given string."""

        buff = self.sequence + text
        if reset:
            buff += reset_style()

        return buff

    def get_localized(self) -> Color:
        """Creates a terminal-capability local Color instance.

        This method essentially allows for graceful degradation of colors in the
        terminal.
        """

        system = terminal.colorsystem
        if self.system <= system:
            return self

        colortype = SYSTEM_TO_TYPE[system]

        local = colortype.from_rgb(self.rgb)
        local.background = self.background

        return local

analogous: tuple[Color, Color, Color] cached property

Computes the analogous group this colors is in.

Analogous colors are located next to eachother on the color wheel.

Returns:

Type Description
tuple[Color, Color, Color]

The color to the left, this color and the color to the right.

blue: Number cached property

Returns the red component of this color.

brightness: float cached property

Returns the perceived "brightness" of a color.

From https://stackoverflow.com/a/56678483

complement: Color cached property

Returns the complement of this color.

contrast: Color cached property

Returns a color (black or white) that complies with the W3C contrast ratio guidelines.

green: Number cached property

Returns the red component of this color.

hex: str cached property

Returns CSS-like HEX representation of this color.

hls: RGBTriplet cached property

Returns the HLS (Hue, Lightness, Saturation) representation of this color.

hue: float cached property

Returns the hue component of this color.

lightness: float cached property

Returns the lightness component of this color.

luminance: float cached property

Returns this color's perceived luminance (brightness).

From https://stackoverflow.com/a/596243

markup: str cached property

Returns the TIM representation of this color.

name: str property

Returns the reverse-parseable name of this color.

red: Number cached property

Returns the red component of this color.

rgb: RGBTriplet cached property

Returns this color as a tuple of (red, green, blue) values.

saturation: float cached property

Returns the saturation component of this color.

sequence: str property

Returns the ANSI sequence representation of the color.

tetradic: tuple[Color, Color, Color, Color] cached property

Computes the tetradic group this color is in.

Tetradic colors are 4-way complements of eachother.

Returns:

Type Description
tuple[Color, Color, Color, Color]

This color, the first tetradic element and the second one.

triadic: tuple[Color, Color, Color] cached property

Computes the triadic group this color is in.

Triadic colors are 3-way complements of eachother.

Returns:

Type Description
tuple[Color, Color, Color]

This color, the first triadic element and the second one.

__call__(text, reset=True)

Colors the given string.

Source code in pytermgui/colors.py
493
494
495
496
497
498
499
500
def __call__(self, text: str, reset: bool = True) -> str:
    """Colors the given string."""

    buff = self.sequence + text
    if reset:
        buff += reset_style()

    return buff

__format__(spec)

Formats the color by the given specification.

Source code in pytermgui/colors.py
165
166
167
168
169
170
171
172
173
174
def __format__(self, spec: str) -> str:
    """Formats the color by the given specification."""

    if spec == "tim":
        return self.markup

    if spec == "seq":
        return self.sequence

    return repr(self)

blend(other, alpha=0.5, localize=False)

Blends a color into another one.

Parameters:

Name Type Description Default
other Color

The color to blend with.

required
alpha float

How much the other color should influence the outcome.

0.5
localize bool

If set, the returned color will returned its localized version by running get_localized on it before returning.

False

Returns:

Type Description
Color

A Color that is the result of the blending.

Source code in pytermgui/colors.py
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
def blend(self, other: Color, alpha: float = 0.5, localize: bool = False) -> Color:
    """Blends a color into another one.

    Args:
        other: The color to blend with.
        alpha: How much the other color should influence the outcome.
        localize: If set, the returned color will returned its localized version by running
            `get_localized` on it before returning.

    Returns:
        A `Color` that is the result of the blending.
    """

    red1, green1, blue1 = self.rgb
    red2, green2, blue2 = other.rgb

    blended: Color = RGBColor.from_rgb(
        (
            int(red1 + (red2 - red1) * alpha),
            int(green1 + (green2 - green1) * alpha),
            int(blue1 + (blue2 - blue1) * alpha),
        )
    )

    if localize:
        blended = blended.get_localized()

    return blended

blend_complement(alpha=0.5)

Blends this color with its complement.

See Color.blend.

Source code in pytermgui/colors.py
450
451
452
453
454
455
456
def blend_complement(self, alpha: float = 0.5) -> Color:
    """Blends this color with its complement.

    See `Color.blend`.
    """

    return self.blend(self.complement, alpha)

blend_contrast(alpha=0.5)

Blends this color with its contrast pair.

See Color.blend.

Source code in pytermgui/colors.py
458
459
460
461
462
463
464
def blend_contrast(self, alpha: float = 0.5) -> Color:
    """Blends this color with its contrast pair.

    See `Color.blend`.
    """

    return self.blend(self.contrast, alpha)

darken(alpha=0.5)

Darkens the color by blending it with black, using the alpha provided.

Source code in pytermgui/colors.py
466
467
468
469
def darken(self, alpha: float = 0.5) -> Color:
    """Darkens the color by blending it with black, using the alpha provided."""

    return self.blend(Color.parse("#000000"), alpha)

from_hls(hsl) classmethod

Creates a color from the given HLS.

HLS stands for Hue, Lightness & Saturation. It is more commonly known as HSL, but the colorsys library uses HLS instead so that's what we use too.

Parameters:

Name Type Description Default
hsl RGBTriplet

The HLS value to base the new color off of.

required
Source code in pytermgui/colors.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
@classmethod
def from_hls(cls, hsl: RGBTriplet) -> Color:
    """Creates a color from the given HLS.

    HLS stands for Hue, Lightness & Saturation. It is more commonly known as HSL,
    but the `colorsys` library uses HLS instead so that's what we use too.

    Args:
        hsl: The HLS value to base the new color off of.
    """

    rgb = cast(
        RGBTriplet,
        map(lambda n: int(256 * n), colorsys.hls_to_rgb(*hsl)),
    )

    return RGBColor.from_rgb(rgb)

from_rgb(rgb) classmethod

Creates a color from the given RGB.

Parameters:

Name Type Description Default
rgb RGBTriplet

The RGB value to base the new color off of.

required
Source code in pytermgui/colors.py
176
177
178
179
180
181
182
183
184
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> Color:
    """Creates a color from the given RGB.

    Args:
        rgb: The RGB value to base the new color off of.
    """

    return RGBColor.from_rgb(rgb)

get_default_background() classmethod

Gets the terminal emulator's default foreground color.

Source code in pytermgui/colors.py
286
287
288
289
290
291
292
293
@classmethod
def get_default_background(cls) -> Color:
    """Gets the terminal emulator's default foreground color."""

    if cls.default_background is not None:
        return cls.default_background

    return _get_palette_color("11")

get_default_foreground() classmethod

Gets the terminal emulator's default foreground color.

Source code in pytermgui/colors.py
277
278
279
280
281
282
283
284
@classmethod
def get_default_foreground(cls) -> Color:
    """Gets the terminal emulator's default foreground color."""

    if cls.default_foreground is not None:
        return cls.default_foreground

    return _get_palette_color("10")

get_localized()

Creates a terminal-capability local Color instance.

This method essentially allows for graceful degradation of colors in the terminal.

Source code in pytermgui/colors.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
def get_localized(self) -> Color:
    """Creates a terminal-capability local Color instance.

    This method essentially allows for graceful degradation of colors in the
    terminal.
    """

    system = terminal.colorsystem
    if self.system <= system:
        return self

    colortype = SYSTEM_TO_TYPE[system]

    local = colortype.from_rgb(self.rgb)
    local.background = self.background

    return local

hue_offset(offset)

Returns the color offset by the given hue.

Source code in pytermgui/colors.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
def hue_offset(self, offset: float) -> Color:
    """Returns the color offset by the given hue."""

    hue, lightness, saturation = colorsys.rgb_to_hls(
        self.red / 256, self.green / 256, self.blue / 256
    )

    hue = (hue + offset) % 1

    return Color.parse(
        ";".join(
            map(
                lambda n: str(int(256 * n)),
                colorsys.hls_to_rgb(hue, lightness, saturation),
            )
        ),
        background=self.background,
        localize=False,
    )

lighten(alpha=0.5)

Lightens the color by blending it with white, using the alpha provided.

Source code in pytermgui/colors.py
471
472
473
474
def lighten(self, alpha: float = 0.5) -> Color:
    """Lightens the color by blending it with white, using the alpha provided."""

    return self.blend(Color.parse("#FFFFFF"), alpha)

parse(text, background=False, localize=True, use_cache=False) classmethod

Uses str_to_color to parse some text into a Color.

Source code in pytermgui/colors.py
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
@classmethod
def parse(
    cls,
    text: str,
    background: bool = False,  # pylint: disable=redefined-outer-name
    localize: bool = True,
    use_cache: bool = False,
) -> Color:
    """Uses `str_to_color` to parse some text into a `Color`."""

    return str_to_color(
        text=text,
        is_background=background,
        localize=localize,
        use_cache=use_cache,
    )

GreyscaleRampColor

Bases: IndexedColor

The color type used for NO_COLOR greyscale ramps.

This implementation uses the color's perceived brightness as its base.

Source code in pytermgui/colors.py
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
class GreyscaleRampColor(IndexedColor):
    """The color type used for NO_COLOR greyscale ramps.

    This implementation uses the color's perceived brightness as its base.
    """

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> GreyscaleRampColor:
        """Gets a greyscale color based on the given color's luminance."""

        color = cls("0")
        setattr(color, "_rgb", rgb)

        index = int(232 + color.brightness * 23)
        color.value = str(index)

        return color

from_rgb(rgb) classmethod

Gets a greyscale color based on the given color's luminance.

Source code in pytermgui/colors.py
706
707
708
709
710
711
712
713
714
715
716
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> GreyscaleRampColor:
    """Gets a greyscale color based on the given color's luminance."""

    color = cls("0")
    setattr(color, "_rgb", rgb)

    index = int(232 + color.brightness * 23)
    color.value = str(index)

    return color

HEXColor dataclass

Bases: RGBColor

An arbitrary, CSS-like HEX color.

Source code in pytermgui/colors.py
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
@dataclass
class HEXColor(RGBColor):
    """An arbitrary, CSS-like HEX color."""

    system = ColorSystem.TRUE

    def __post_init__(self) -> None:
        """Ensures data validity."""

        data = self.value
        if data.startswith("#"):
            data = data[1:]

        indices = (0, 2), (2, 4), (4, 6)
        rgb = []
        for start, end in indices:
            value = data[start:end]
            rgb.append(int(value, base=16))

        self._rgb = rgb[0], rgb[1], rgb[2]

        assert len(self._rgb) == 3

__post_init__()

Ensures data validity.

Source code in pytermgui/colors.py
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
def __post_init__(self) -> None:
    """Ensures data validity."""

    data = self.value
    if data.startswith("#"):
        data = data[1:]

    indices = (0, 2), (2, 4), (4, 6)
    rgb = []
    for start, end in indices:
        value = data[start:end]
        rgb.append(int(value, base=16))

    self._rgb = rgb[0], rgb[1], rgb[2]

    assert len(self._rgb) == 3

IndexedColor dataclass

Bases: Color

A color representing an index into the xterm-256 color palette.

Source code in pytermgui/colors.py
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
@dataclass(repr=False)
class IndexedColor(Color):
    """A color representing an index into the xterm-256 color palette."""

    system = ColorSystem.EIGHT_BIT

    def __post_init__(self) -> None:
        """Ensures data validity."""

        if not self.value.isdigit():
            raise ValueError(
                f"IndexedColor value has to be numerical, got {self.value!r}."
            )

        if not 0 <= int(self.value) < 256:
            raise ValueError(
                f"IndexedColor value has to fit in range 0-255, got {self.value!r}."
            )

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Yields a fancy looking string."""

        yield f"<{type(self).__name__} value: {self.value}, preview: "

        yield {"text": f"{self:seq}{PREVIEW_CHAR}\x1b[0m", "highlight": False}

        yield ">"

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> IndexedColor:
        """Constructs an `IndexedColor` from the closest matching option."""

        if rgb in _COLOR_MATCH_CACHE:
            color = _COLOR_MATCH_CACHE[rgb]

            assert isinstance(color, IndexedColor)
            return color

        if terminal.colorsystem == ColorSystem.STANDARD:
            return StandardColor.from_rgb(rgb)

        # Normalize the color values
        red, green, blue = (x / 255 for x in rgb)

        # Calculate the eight-bit color index
        color_num = 16
        color_num += 36 * round(red * 5.0)
        color_num += 6 * round(green * 5.0)
        color_num += round(blue * 5.0)

        color = cls(str(color_num))
        _COLOR_MATCH_CACHE[rgb] = color

        return color

    @property
    def sequence(self) -> str:
        r"""Returns an ANSI sequence representing this color."""

        index = int(self.value)

        return "\x1b[" + ("48" if self.background else "38") + f";5;{index}m"

    @cached_property
    def rgb(self) -> RGBTriplet:
        """Returns an RGB representation of this color."""

        if self._rgb is not None:
            return self._rgb

        index = int(self.value)
        rgb = COLOR_TABLE[index]

        return (rgb[0], rgb[1], rgb[2])

rgb: RGBTriplet cached property

Returns an RGB representation of this color.

sequence: str property

Returns an ANSI sequence representing this color.

__fancy_repr__()

Yields a fancy looking string.

Source code in pytermgui/colors.py
540
541
542
543
544
545
546
547
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Yields a fancy looking string."""

    yield f"<{type(self).__name__} value: {self.value}, preview: "

    yield {"text": f"{self:seq}{PREVIEW_CHAR}\x1b[0m", "highlight": False}

    yield ">"

__post_init__()

Ensures data validity.

Source code in pytermgui/colors.py
527
528
529
530
531
532
533
534
535
536
537
538
def __post_init__(self) -> None:
    """Ensures data validity."""

    if not self.value.isdigit():
        raise ValueError(
            f"IndexedColor value has to be numerical, got {self.value!r}."
        )

    if not 0 <= int(self.value) < 256:
        raise ValueError(
            f"IndexedColor value has to fit in range 0-255, got {self.value!r}."
        )

from_rgb(rgb) classmethod

Constructs an IndexedColor from the closest matching option.

Source code in pytermgui/colors.py
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> IndexedColor:
    """Constructs an `IndexedColor` from the closest matching option."""

    if rgb in _COLOR_MATCH_CACHE:
        color = _COLOR_MATCH_CACHE[rgb]

        assert isinstance(color, IndexedColor)
        return color

    if terminal.colorsystem == ColorSystem.STANDARD:
        return StandardColor.from_rgb(rgb)

    # Normalize the color values
    red, green, blue = (x / 255 for x in rgb)

    # Calculate the eight-bit color index
    color_num = 16
    color_num += 36 * round(red * 5.0)
    color_num += 6 * round(green * 5.0)
    color_num += round(blue * 5.0)

    color = cls(str(color_num))
    _COLOR_MATCH_CACHE[rgb] = color

    return color

RGBColor dataclass

Bases: Color

An arbitrary RGB color.

Source code in pytermgui/colors.py
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
@dataclass(repr=False)
class RGBColor(Color):
    """An arbitrary RGB color."""

    system = ColorSystem.TRUE

    def __post_init__(self) -> None:
        """Ensures data validity."""

        if self.value.count(";") != 2:
            raise ValueError(
                "Invalid value passed to RGBColor."
                + f" Format has to be rrr;ggg;bbb, got {self.value!r}."
            )

        rgb = tuple(int(num) for num in self.value.split(";"))
        self._rgb = rgb[0], rgb[1], rgb[2]

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Yields a fancy looking string."""

        yield (
            f"<{type(self).__name__} red: {self.red}, green: {self.green},"
            + f" blue: {self.blue}, preview: "
        )

        yield {"text": f"{self:seq}{PREVIEW_CHAR}\x1b[0m", "highlight": False}

        yield ">"

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> RGBColor:
        """Returns an `RGBColor` from the given triplet."""

        return cls(";".join(map(str, rgb)))

    @property
    def red(self) -> float:
        """Returns the red component of this color."""

        return self.rgb[0]

    @property
    def green(self) -> float:
        """Returns the green component of this color."""

        return self.rgb[1]

    @property
    def blue(self) -> float:
        """Returns the blue component of this color."""

        return self.rgb[2]

    @property
    def sequence(self) -> str:
        """Returns the ANSI sequence representing this color."""

        return (
            "\x1b["
            + ("48" if self.background else "38")
            + ";2;"
            + ";".join(str(num) for num in self.rgb)
            + "m"
        )

blue: float property

Returns the blue component of this color.

green: float property

Returns the green component of this color.

red: float property

Returns the red component of this color.

sequence: str property

Returns the ANSI sequence representing this color.

__fancy_repr__()

Yields a fancy looking string.

Source code in pytermgui/colors.py
737
738
739
740
741
742
743
744
745
746
747
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Yields a fancy looking string."""

    yield (
        f"<{type(self).__name__} red: {self.red}, green: {self.green},"
        + f" blue: {self.blue}, preview: "
    )

    yield {"text": f"{self:seq}{PREVIEW_CHAR}\x1b[0m", "highlight": False}

    yield ">"

__post_init__()

Ensures data validity.

Source code in pytermgui/colors.py
725
726
727
728
729
730
731
732
733
734
735
def __post_init__(self) -> None:
    """Ensures data validity."""

    if self.value.count(";") != 2:
        raise ValueError(
            "Invalid value passed to RGBColor."
            + f" Format has to be rrr;ggg;bbb, got {self.value!r}."
        )

    rgb = tuple(int(num) for num in self.value.split(";"))
    self._rgb = rgb[0], rgb[1], rgb[2]

from_rgb(rgb) classmethod

Returns an RGBColor from the given triplet.

Source code in pytermgui/colors.py
749
750
751
752
753
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> RGBColor:
    """Returns an `RGBColor` from the given triplet."""

    return cls(";".join(map(str, rgb)))

StandardColor

Bases: IndexedColor

A color in the xterm-16 palette.

Source code in pytermgui/colors.py
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
class StandardColor(IndexedColor):
    """A color in the xterm-16 palette."""

    system = ColorSystem.STANDARD

    @property
    def name(self) -> str:
        """Returns the markup-compatible name for this color."""

        index = name = int(self.value)

        # Normal colors
        if 30 <= index <= 47:
            name -= 30

        elif 90 <= index <= 107:
            name -= 82

        return ("@" if self.background else "") + str(name)

    @classmethod
    def from_ansi(cls, code: str) -> StandardColor:
        """Creates a standard color from the given ANSI code.

        These codes have to be a digit ranging between 31 and 47.
        """

        if not code.isdigit():
            raise ColorSyntaxError(
                f"Standard color codes must be digits, not {code!r}."
            )

        code_int = int(code)

        if not 30 <= code_int <= 47 and not 90 <= code_int <= 107:
            raise ColorSyntaxError(
                f"Standard color codes must be in the range ]30;47[ or ]90;107[, got {code_int!r}."
            )

        is_background = 40 <= code_int <= 47 or 100 <= code_int <= 107

        if is_background:
            code_int -= 10

        return cls(str(code_int), background=is_background)

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> StandardColor:
        """Creates a color with the closest-matching xterm index, based on rgb.

        Args:
            rgb: The target color.
        """

        if rgb in _COLOR_MATCH_CACHE:
            color = _COLOR_MATCH_CACHE[rgb]

            if color.system is ColorSystem.STANDARD:
                assert isinstance(color, StandardColor)
                return color

        # Find the least-different color in the table
        index = min(range(16), key=lambda i: _get_color_difference(rgb, COLOR_TABLE[i]))

        if index > 7:
            index += 82
        else:
            index += 30

        color = cls(str(index))

        _COLOR_MATCH_CACHE[rgb] = color

        return color

    @property
    def sequence(self) -> str:
        r"""Returns an ANSI sequence representing this color."""

        index = int(self.value)

        if self.background:
            index += 10

        return f"\x1b[{index}m"

    @cached_property
    def rgb(self) -> RGBTriplet:
        """Returns an RGB representation of this color."""

        index = int(self.value)

        if 30 <= index <= 47:
            index -= 30

        elif 90 <= index <= 107:
            index -= 82

        rgb = COLOR_TABLE[index]

        return (rgb[0], rgb[1], rgb[2])

name: str property

Returns the markup-compatible name for this color.

rgb: RGBTriplet cached property

Returns an RGB representation of this color.

sequence: str property

Returns an ANSI sequence representing this color.

from_ansi(code) classmethod

Creates a standard color from the given ANSI code.

These codes have to be a digit ranging between 31 and 47.

Source code in pytermgui/colors.py
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
@classmethod
def from_ansi(cls, code: str) -> StandardColor:
    """Creates a standard color from the given ANSI code.

    These codes have to be a digit ranging between 31 and 47.
    """

    if not code.isdigit():
        raise ColorSyntaxError(
            f"Standard color codes must be digits, not {code!r}."
        )

    code_int = int(code)

    if not 30 <= code_int <= 47 and not 90 <= code_int <= 107:
        raise ColorSyntaxError(
            f"Standard color codes must be in the range ]30;47[ or ]90;107[, got {code_int!r}."
        )

    is_background = 40 <= code_int <= 47 or 100 <= code_int <= 107

    if is_background:
        code_int -= 10

    return cls(str(code_int), background=is_background)

from_rgb(rgb) classmethod

Creates a color with the closest-matching xterm index, based on rgb.

Parameters:

Name Type Description Default
rgb RGBTriplet

The target color.

required
Source code in pytermgui/colors.py
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> StandardColor:
    """Creates a color with the closest-matching xterm index, based on rgb.

    Args:
        rgb: The target color.
    """

    if rgb in _COLOR_MATCH_CACHE:
        color = _COLOR_MATCH_CACHE[rgb]

        if color.system is ColorSystem.STANDARD:
            assert isinstance(color, StandardColor)
            return color

    # Find the least-different color in the table
    index = min(range(16), key=lambda i: _get_color_difference(rgb, COLOR_TABLE[i]))

    if index > 7:
        index += 82
    else:
        index += 30

    color = cls(str(index))

    _COLOR_MATCH_CACHE[rgb] = color

    return color

background(text, color, reset=True)

Sets the background color of the given text.

Note that the given color will be forced into background = True.

Parameters:

Name Type Description Default
text str

The text to color.

required
color str | Color

The color to use. See pytermgui.colors.str_to_color for accepted str formats.

required
reset bool

Whether the return value should include a reset sequence at the end.

True

Returns:

Type Description
str

The colored text, including a reset if set.

Source code in pytermgui/colors.py
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
def background(text: str, color: str | Color, reset: bool = True) -> str:
    """Sets the background color of the given text.

    Note that the given color will be forced into `background = True`.

    Args:
        text: The text to color.
        color: The color to use. See `pytermgui.colors.str_to_color` for accepted
            str formats.
        reset: Whether the return value should include a reset sequence at the end.

    Returns:
        The colored text, including a reset if set.
    """

    if not isinstance(color, Color):
        color = str_to_color(color)

    color.background = True

    return color(text, reset=reset)

clear_color_cache()

Clears _COLOR_CACHE and _COLOR_MATCH_CACHE.

Source code in pytermgui/colors.py
88
89
90
91
92
def clear_color_cache() -> None:
    """Clears `_COLOR_CACHE` and `_COLOR_MATCH_CACHE`."""

    _COLOR_CACHE.clear()
    _COLOR_MATCH_CACHE.clear()

foreground(text, color, reset=True)

Sets the foreground color of the given text.

Note that the given color will be forced into background = True.

Parameters:

Name Type Description Default
text str

The text to color.

required
color str | Color

The color to use. See pytermgui.colors.str_to_color for accepted str formats.

required
reset bool

Whether the return value should include a reset sequence at the end.

True

Returns:

Type Description
str

The colored text, including a reset if set.

Source code in pytermgui/colors.py
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
def foreground(text: str, color: str | Color, reset: bool = True) -> str:
    """Sets the foreground color of the given text.

    Note that the given color will be forced into `background = True`.

    Args:
        text: The text to color.
        color: The color to use. See `pytermgui.colors.str_to_color` for accepted
            str formats.
        reset: Whether the return value should include a reset sequence at the end.

    Returns:
        The colored text, including a reset if set.
    """

    if not isinstance(color, Color):
        color = str_to_color(color)

    color.background = False

    return color(text, reset=reset)

str_to_color(text, is_background=False, localize=True, use_cache=True) cached

Creates a Color from the given text.

Accepted formats:

  • 0-255: IndexedColor.
  • 'rrr;ggg;bbb': RGBColor.
  • '(#)rrggbb': HEXColor. Leading hash is optional.

You can also add a leading '@' into the string to make the output represent a background color, such as @#123abc.

Parameters:

Name Type Description Default
text str

The string to format from.

required
is_background bool

Whether the output should be forced into a background color. Mostly used internally, when set will take precedence over syntax of leading '@' symbol.

False
localize bool

Whether get_localized should be called on the output color.

True
use_cache bool

Whether caching should be used.

True
Source code in pytermgui/colors.py
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
@lru_cache(maxsize=1024)
def str_to_color(
    text: str,
    is_background: bool = False,
    localize: bool = True,
    use_cache: bool = True,
) -> Color:
    """Creates a `Color` from the given text.

    Accepted formats:

    - 0-255: `IndexedColor`.
    - 'rrr;ggg;bbb': `RGBColor`.
    - '(#)rrggbb': `HEXColor`. Leading hash is optional.

    You can also add a leading '@' into the string to make the output represent a
    background color, such as `@#123abc`.

    Args:
        text: The string to format from.
        is_background: Whether the output should be forced into a background color.
            Mostly used internally, when set will take precedence over syntax of leading
            '@' symbol.
        localize: Whether `get_localized` should be called on the output color.
        use_cache: Whether caching should be used.
    """

    def _trim_code(code: str) -> str:
        """Trims the given color code."""

        if not all(char.isdigit() or char in "m;" for char in code):
            return code

        is_background = code.startswith("48;")

        if (code.startswith("38;5;") or code.startswith("48;5;")) or (
            code.startswith("38;2;") or code.startswith("48;2;")
        ):
            code = code[5:]

        if code.endswith("m"):
            code = code[:-1]

        if is_background:
            code = "@" + code

        return code

    text = _trim_code(text)

    if not use_cache:
        str_to_color.cache_clear()

    if text.startswith("@"):
        is_background = True
        text = text[1:]

    if text in NAMED_COLORS:
        return str_to_color(str(NAMED_COLORS[text]), is_background=is_background)

    color: Color

    # This code is not pretty, but having these separate branches for each type
    # should improve the performance by quite a large margin.
    match = RE_256.match(text)
    if match is not None:
        # Note: At the moment, all colors become an `IndexedColor`, due to a large
        #       amount of problems a separated `StandardColor` class caused. Not
        #       sure if there are any real drawbacks to doing it this way, bar the
        #       extra characters that 255 colors use up compared to xterm-16.
        color = IndexedColor(match[0], background=is_background)

        return color.get_localized() if localize else color

    match = RE_HEX.match(text)
    if match is not None:
        color = HEXColor(match[0], background=is_background)

        return color.get_localized() if localize else color

    match = RE_RGB.match(text)
    if match is not None:
        color = RGBColor(match[0], background=is_background)

        return color.get_localized() if localize else color

    raise ColorSyntaxError(f"Could not convert {text!r} into a `Color`.")