pytermgui.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.

View Source
  0"""The module containing all of the color-centric features of this library.
  1
  2This module provides a base class, `Color`, and a bunch of abstractions over it.
  3
  4Shoutout to: https://stackoverflow.com/a/33206814, one of the best StackOverflow
  5answers I've ever bumped into.
  6"""
  7
  8# pylint: disable=too-many-instance-attributes
  9
 10
 11from __future__ import annotations
 12
 13import re
 14import sys
 15from math import sqrt  # pylint: disable=no-name-in-module
 16from dataclasses import dataclass, field
 17from typing import TYPE_CHECKING, Type, Literal
 18from functools import lru_cache, cached_property
 19
 20from .input import getch
 21from .exceptions import ColorSyntaxError
 22from .terminal import terminal, ColorSystem
 23from .ansi_interface import reset as reset_style
 24
 25if TYPE_CHECKING:
 26    # This cyclic won't be relevant while type checking.
 27    from .parser import StyledText  # pylint: disable=cyclic-import
 28
 29__all__ = [
 30    "COLOR_TABLE",
 31    "XTERM_NAMED_COLORS",
 32    "NAMED_COLORS",
 33    "clear_color_cache",
 34    "foreground",
 35    "background",
 36    "str_to_color",
 37    "Color",
 38    "IndexedColor",
 39    "RGBColor",
 40    "HEXColor",
 41]
 42
 43
 44RE_256 = re.compile(r"^([\d]{1,3})$")
 45RE_HEX = re.compile(r"(?:#)?([0-9a-fA-F]{6})")
 46RE_RGB = re.compile(r"(\d{1,3};\d{1,3};\d{1,3})")
 47
 48RE_PALETTE_REPLY = re.compile(
 49    r"\x1b]((?:10)|(?:11));rgb:([0-9a-f]{4})\/([0-9a-f]{4})\/([0-9a-f]{4})\x1b\\"
 50)
 51
 52# Adapted from https://gist.github.com/MicahElliott/719710
 53# TODO: Maybe this could be generated dynamically?
 54#       See https://superuser.com/a/905280
 55COLOR_TABLE = [
 56    (0, 0, 0),
 57    (170, 0, 0),
 58    (0, 170, 0),
 59    (170, 85, 0),
 60    (0, 0, 170),
 61    (170, 0, 170),
 62    (0, 170, 170),
 63    (170, 170, 170),
 64    (85, 85, 85),
 65    (255, 85, 85),
 66    (85, 255, 85),
 67    (255, 255, 85),
 68    (85, 85, 255),
 69    (255, 85, 255),
 70    (85, 255, 255),
 71    (255, 255, 255),
 72    (0, 0, 0),
 73    (0, 0, 95),
 74    (0, 0, 135),
 75    (0, 0, 175),
 76    (0, 0, 215),
 77    (0, 0, 255),
 78    (0, 95, 0),
 79    (0, 95, 95),
 80    (0, 95, 135),
 81    (0, 95, 175),
 82    (0, 95, 215),
 83    (0, 95, 255),
 84    (0, 135, 0),
 85    (0, 135, 95),
 86    (0, 135, 135),
 87    (0, 135, 175),
 88    (0, 135, 215),
 89    (0, 135, 255),
 90    (0, 175, 0),
 91    (0, 175, 95),
 92    (0, 175, 135),
 93    (0, 175, 175),
 94    (0, 175, 215),
 95    (0, 175, 255),
 96    (0, 215, 0),
 97    (0, 215, 95),
 98    (0, 215, 135),
 99    (0, 215, 175),
100    (0, 215, 215),
101    (0, 215, 255),
102    (0, 255, 0),
103    (0, 255, 95),
104    (0, 255, 135),
105    (0, 255, 175),
106    (0, 255, 215),
107    (0, 255, 255),
108    (95, 0, 0),
109    (95, 0, 95),
110    (95, 0, 135),
111    (95, 0, 175),
112    (95, 0, 215),
113    (95, 0, 255),
114    (95, 95, 0),
115    (95, 95, 95),
116    (95, 95, 135),
117    (95, 95, 175),
118    (95, 95, 215),
119    (95, 95, 255),
120    (95, 135, 0),
121    (95, 135, 95),
122    (95, 135, 135),
123    (95, 135, 175),
124    (95, 135, 215),
125    (95, 135, 255),
126    (95, 175, 0),
127    (95, 175, 95),
128    (95, 175, 135),
129    (95, 175, 175),
130    (95, 175, 215),
131    (95, 175, 255),
132    (95, 215, 0),
133    (95, 215, 95),
134    (95, 215, 135),
135    (95, 215, 175),
136    (95, 215, 215),
137    (95, 215, 255),
138    (95, 255, 0),
139    (95, 255, 95),
140    (95, 255, 135),
141    (95, 255, 175),
142    (95, 255, 215),
143    (95, 255, 255),
144    (135, 0, 0),
145    (135, 0, 95),
146    (135, 0, 135),
147    (135, 0, 175),
148    (135, 0, 215),
149    (135, 0, 255),
150    (135, 95, 0),
151    (135, 95, 95),
152    (135, 95, 135),
153    (135, 95, 175),
154    (135, 95, 215),
155    (135, 95, 255),
156    (135, 135, 0),
157    (135, 135, 95),
158    (135, 135, 135),
159    (135, 135, 175),
160    (135, 135, 215),
161    (135, 135, 255),
162    (135, 175, 0),
163    (135, 175, 95),
164    (135, 175, 135),
165    (135, 175, 175),
166    (135, 175, 215),
167    (135, 175, 255),
168    (135, 215, 0),
169    (135, 215, 95),
170    (135, 215, 135),
171    (135, 215, 175),
172    (135, 215, 215),
173    (135, 215, 255),
174    (135, 255, 0),
175    (135, 255, 95),
176    (135, 255, 135),
177    (135, 255, 175),
178    (135, 255, 215),
179    (135, 255, 255),
180    (175, 0, 0),
181    (175, 0, 95),
182    (175, 0, 135),
183    (175, 0, 175),
184    (175, 0, 215),
185    (175, 0, 255),
186    (175, 95, 0),
187    (175, 95, 95),
188    (175, 95, 135),
189    (175, 95, 175),
190    (175, 95, 215),
191    (175, 95, 255),
192    (175, 135, 0),
193    (175, 135, 95),
194    (175, 135, 135),
195    (175, 135, 175),
196    (175, 135, 215),
197    (175, 135, 255),
198    (175, 175, 0),
199    (175, 175, 95),
200    (175, 175, 135),
201    (175, 175, 175),
202    (175, 175, 215),
203    (175, 175, 255),
204    (175, 215, 0),
205    (175, 215, 95),
206    (175, 215, 135),
207    (175, 215, 175),
208    (175, 215, 215),
209    (175, 215, 255),
210    (175, 255, 0),
211    (175, 255, 95),
212    (175, 255, 135),
213    (175, 255, 175),
214    (175, 255, 215),
215    (175, 255, 255),
216    (215, 0, 0),
217    (215, 0, 95),
218    (215, 0, 135),
219    (215, 0, 175),
220    (215, 0, 215),
221    (215, 0, 255),
222    (215, 95, 0),
223    (215, 95, 95),
224    (215, 95, 135),
225    (215, 95, 175),
226    (215, 95, 215),
227    (215, 95, 255),
228    (215, 135, 0),
229    (215, 135, 95),
230    (215, 135, 135),
231    (215, 135, 175),
232    (215, 135, 215),
233    (215, 135, 255),
234    (215, 175, 0),
235    (215, 175, 95),
236    (215, 175, 135),
237    (215, 175, 175),
238    (215, 175, 215),
239    (215, 175, 255),
240    (215, 215, 0),
241    (215, 215, 95),
242    (215, 215, 135),
243    (215, 215, 175),
244    (215, 215, 215),
245    (215, 215, 255),
246    (215, 255, 0),
247    (215, 255, 95),
248    (215, 255, 135),
249    (215, 255, 175),
250    (215, 255, 215),
251    (215, 255, 255),
252    (255, 0, 0),
253    (255, 0, 95),
254    (255, 0, 135),
255    (255, 0, 175),
256    (255, 0, 215),
257    (255, 0, 255),
258    (255, 95, 0),
259    (255, 95, 95),
260    (255, 95, 135),
261    (255, 95, 175),
262    (255, 95, 215),
263    (255, 95, 255),
264    (255, 135, 0),
265    (255, 135, 95),
266    (255, 135, 135),
267    (255, 135, 175),
268    (255, 135, 215),
269    (255, 135, 255),
270    (255, 175, 0),
271    (255, 175, 95),
272    (255, 175, 135),
273    (255, 175, 175),
274    (255, 175, 215),
275    (255, 175, 255),
276    (255, 215, 0),
277    (255, 215, 95),
278    (255, 215, 135),
279    (255, 215, 175),
280    (255, 215, 215),
281    (255, 215, 255),
282    (255, 255, 0),
283    (255, 255, 95),
284    (255, 255, 135),
285    (255, 255, 175),
286    (255, 255, 215),
287    (255, 255, 255),
288    (8, 8, 8),
289    (18, 18, 18),
290    (28, 28, 28),
291    (38, 38, 38),
292    (48, 48, 48),
293    (58, 58, 58),
294    (68, 68, 68),
295    (78, 78, 78),
296    (88, 88, 88),
297    (98, 98, 98),
298    (108, 108, 108),
299    (118, 118, 118),
300    (128, 128, 128),
301    (138, 138, 138),
302    (148, 148, 148),
303    (158, 158, 158),
304    (168, 168, 168),
305    (178, 178, 178),
306    (188, 188, 188),
307    (198, 198, 198),
308    (208, 208, 208),
309    (218, 218, 218),
310    (228, 228, 228),
311    (238, 238, 238),
312]
313
314XTERM_NAMED_COLORS = {
315    0: "black",
316    1: "red",
317    2: "green",
318    3: "yellow",
319    4: "blue",
320    5: "magenta",
321    6: "cyan",
322    7: "white",
323    8: "bright-black",
324    9: "bright-red",
325    10: "bright-green",
326    11: "bright-yellow",
327    12: "bright-blue",
328    14: "bright-magenta",
329    15: "bright-cyan",
330    16: "bright-white",
331}
332
333NAMED_COLORS = {**{color: str(index) for index, color in XTERM_NAMED_COLORS.items()}}
334
335_COLOR_CACHE: dict[str, Color] = {}
336_COLOR_MATCH_CACHE: dict[tuple[float, float, float], Color] = {}
337
338
339def clear_color_cache() -> None:
340    """Clears `_COLOR_CACHE` and `_COLOR_MATCH_CACHE`."""
341
342    _COLOR_CACHE.clear()
343    _COLOR_MATCH_CACHE.clear()
344
345
346def _get_palette_color(color: Literal["10", "11"]) -> Color:
347    """Gets either the foreground or background color of the current emulator.
348
349    Args:
350        color: The value used for `Ps` in the query. See https://unix.stackexchange.com/a/172674.
351    """
352
353    defaults = {
354        "10": RGBColor.from_rgb((222, 222, 222)),
355        "11": RGBColor.from_rgb((20, 20, 20)),
356    }
357
358    if not terminal.isatty():
359        return defaults[color]
360
361    sys.stdout.write(f"\x1b]{color};?\007")
362    sys.stdout.flush()
363
364    reply = getch()
365
366    match = RE_PALETTE_REPLY.match(reply)
367    if match is None:
368        return defaults[color]
369
370    _, red, green, blue = match.groups()
371
372    rgb: list[int] = []
373    for part in (red, green, blue):
374        rgb.append(int(part[:2], base=16))
375
376    palette_color = RGBColor.from_rgb(tuple(rgb))  # type: ignore
377    palette_color.background = color == "11"
378
379    return palette_color
380
381
382@dataclass
383class Color:
384    """A terminal color.
385
386    Args:
387        value: The data contained within this color.
388        background: Whether this color will represent a color.
389    """
390
391    value: str
392    background: bool = False
393
394    system: ColorSystem = field(init=False)
395
396    default_foreground: Color | None = field(default=None, repr=False)
397    default_background: Color | None = field(default=None, repr=False)
398
399    _luminance: float | None = field(init=False, default=None, repr=False)
400    _brightness: float | None = field(init=False, default=None, repr=False)
401    _rgb: tuple[int, int, int] | None = field(init=False, default=None, repr=False)
402
403    @classmethod
404    def from_rgb(cls, rgb: tuple[int, int, int]) -> Color:
405        """Creates a color from the given RGB, within terminal's colorsystem.
406
407        Args:
408            rgb: The RGB value to base the new color off of.
409        """
410
411        raise NotImplementedError
412
413    @property
414    def sequence(self) -> str:
415        """Returns the ANSI sequence representation of the color."""
416
417        raise NotImplementedError
418
419    @cached_property
420    def rgb(self) -> tuple[int, int, int]:
421        """Returns this color as a tuple of (red, green, blue) values."""
422
423        if self._rgb is None:
424            raise NotImplementedError
425
426        return self._rgb
427
428    @cached_property
429    def hex(self) -> str:
430        """Returns CSS-like HEX representation of this color."""
431
432        buff = "#"
433        for color in self.rgb:
434            buff += f"{format(color, 'x'):0>2}"
435
436        return buff
437
438    @classmethod
439    def get_default_foreground(cls) -> Color:
440        """Gets the terminal emulator's default foreground color."""
441
442        if cls.default_foreground is not None:
443            return cls.default_foreground
444
445        return _get_palette_color("10")
446
447    @classmethod
448    def get_default_background(cls) -> Color:
449        """Gets the terminal emulator's default foreground color."""
450
451        if cls.default_background is not None:
452            return cls.default_background
453
454        return _get_palette_color("11")
455
456    @property
457    def name(self) -> str:
458        """Returns the reverse-parseable name of this color."""
459
460        return ("@" if self.background else "") + self.value
461
462    @property
463    def luminance(self) -> float:
464        """Returns this color's perceived luminance (brightness).
465
466        From https://stackoverflow.com/a/596243
467        """
468
469        # Don't do expensive calculations over and over
470        if self._luminance is not None:
471            return self._luminance
472
473        def _linearize(color: float) -> float:
474            """Converts sRGB color to linear value."""
475
476            if color <= 0.04045:
477                return color / 12.92
478
479            return ((color + 0.055) / 1.055) ** 2.4
480
481        red, green, blue = float(self.rgb[0]), float(self.rgb[1]), float(self.rgb[2])
482
483        red /= 255
484        green /= 255
485        blue /= 255
486
487        red = _linearize(red)
488        blue = _linearize(blue)
489        green = _linearize(green)
490
491        self._luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue
492
493        return self._luminance
494
495    @property
496    def brightness(self) -> float:
497        """Returns the perceived "brightness" of a color.
498
499        From https://stackoverflow.com/a/56678483
500        """
501
502        # Don't do expensive calculations over and over
503        if self._brightness is not None:
504            return self._brightness
505
506        if self.luminance <= (216 / 24389):
507            brightness = self.luminance * (24389 / 27)
508
509        else:
510            brightness = self.luminance ** (1 / 3) * 116 - 16
511
512        self._brightness = brightness / 100
513        return self._brightness
514
515    def __call__(self, text: str, reset: bool = True) -> StyledText:
516        """Colors the given string, returning a `pytermgui.parser.StyledText`."""
517
518        # We import this here as toplevel would cause circular imports, and it won't
519        # be used until this method is called anyways.
520        from .parser import StyledText  # pylint: disable=import-outside-toplevel
521
522        buff = self.sequence + text
523        if reset:
524            buff += reset_style()
525
526        return StyledText(buff)
527
528    def get_localized(self) -> Color:
529        """Creates a terminal-capability local Color instance.
530
531        This method essentially allows for graceful degradation of colors in the
532        terminal.
533        """
534
535        system = terminal.colorsystem
536        if self.system <= system:
537            return self
538
539        colortype = SYSTEM_TO_TYPE[system]
540
541        local = colortype.from_rgb(self.rgb)
542        local.background = self.background
543
544        return local
545
546
547@dataclass(repr=False)
548class IndexedColor(Color):
549    """A color representing an index into the xterm-256 color palette."""
550
551    system = ColorSystem.EIGHT_BIT
552
553    def __post_init__(self) -> None:
554        """Ensures data validity."""
555
556        if not self.value.isdigit():
557            raise ValueError(
558                f"IndexedColor value has to be numerical, got {self.value!r}."
559            )
560
561        if not 0 <= int(self.value) < 256:
562            raise ValueError(
563                f"IndexedColor value has to fit in range 0-255, got {self.value!r}."
564            )
565
566    @classmethod
567    def from_rgb(cls, rgb: tuple[int, int, int]) -> IndexedColor:
568        """Constructs an `IndexedColor` from the closest matching option."""
569
570        if rgb in _COLOR_MATCH_CACHE:
571            color = _COLOR_MATCH_CACHE[rgb]
572
573            assert isinstance(color, IndexedColor)
574            return color
575
576        if terminal.colorsystem == ColorSystem.STANDARD:
577            return StandardColor.from_rgb(rgb)
578
579        # Normalize the color values
580        red, green, blue = (x / 255 for x in rgb)
581
582        # Calculate the eight-bit color index
583        color_num = 16
584        color_num += 36 * round(red * 5.0)
585        color_num += 6 * round(green * 5.0)
586        color_num += round(blue * 5.0)
587
588        color = cls(str(color_num))
589        _COLOR_MATCH_CACHE[rgb] = color
590
591        return color
592
593    @property
594    def sequence(self) -> str:
595        r"""Returns an ANSI sequence representing this color."""
596
597        index = int(self.value)
598
599        return "\x1b[" + ("48" if self.background else "38") + f";5;{index}m"
600
601    @cached_property
602    def rgb(self) -> tuple[int, int, int]:
603        """Returns an RGB representation of this color."""
604
605        if self._rgb is not None:
606            return self._rgb
607
608        index = int(self.value)
609        rgb = COLOR_TABLE[index]
610
611        return (rgb[0], rgb[1], rgb[2])
612
613
614class StandardColor(IndexedColor):
615    """A color in the xterm-16 palette."""
616
617    system = ColorSystem.STANDARD
618
619    @classmethod
620    def from_rgb(cls, rgb: tuple[int, int, int]) -> StandardColor:
621        """Creates a color with the closest-matching xterm index, based on rgb.
622
623        Args:
624            rgb: The target color.
625        """
626
627        if rgb in _COLOR_MATCH_CACHE:
628            color = _COLOR_MATCH_CACHE[rgb]
629
630            if color.system is ColorSystem.STANDARD:
631                assert isinstance(color, StandardColor)
632                return color
633
634        # Find the least-different color in the table
635        index = min(range(16), key=lambda i: _get_color_difference(rgb, COLOR_TABLE[i]))
636        color = cls(str(index))
637
638        _COLOR_MATCH_CACHE[rgb] = color
639
640        return color
641
642    @property
643    def sequence(self) -> str:
644        r"""Returns an ANSI sequence representing this color."""
645
646        index = int(self.value)
647
648        if index <= 7:
649            index += 30
650
651        else:
652            index = index + 82
653
654        if self.background:
655            index += 10
656
657        return f"\x1b[{index}m"
658
659
660class GreyscaleRampColor(IndexedColor):
661    """The color type used for NO_COLOR greyscale ramps.
662
663    This implementation uses the color's perceived brightness as its base.
664    """
665
666    @classmethod
667    def from_rgb(cls, rgb: tuple[int, int, int]) -> GreyscaleRampColor:
668        """Gets a greyscale color based on the given color's luminance."""
669
670        color = cls("0")
671        setattr(color, "_rgb", rgb)
672
673        index = int(232 + color.brightness * 23)
674        color.value = str(index)
675
676        return color
677
678
679@dataclass(repr=False)
680class RGBColor(Color):
681    """An arbitrary RGB color."""
682
683    system = ColorSystem.TRUE
684
685    def __post_init__(self) -> None:
686        """Ensures data validity."""
687
688        if self.value.count(";") != 2:
689            raise ValueError(
690                "Invalid value passed to RGBColor."
691                + f" Format has to be rrr;ggg;bbb, got {self.value!r}."
692            )
693
694        rgb = tuple(int(num) for num in self.value.split(";"))
695        self._rgb = rgb[0], rgb[1], rgb[2]
696
697    @classmethod
698    def from_rgb(cls, rgb: tuple[int, int, int]) -> RGBColor:
699        """Returns an `RGBColor` from the given triplet."""
700
701        return cls(";".join(map(str, rgb)))
702
703    @property
704    def sequence(self) -> str:
705        """Returns the ANSI sequence representing this color."""
706
707        return (
708            "\x1b["
709            + ("48" if self.background else "38")
710            + ";2;"
711            + ";".join(str(num) for num in self.rgb)
712            + "m"
713        )
714
715
716@dataclass
717class HEXColor(RGBColor):
718    """An arbitrary, CSS-like HEX color."""
719
720    system = ColorSystem.TRUE
721
722    def __post_init__(self) -> None:
723        """Ensures data validity."""
724
725        data = self.value
726        if data.startswith("#"):
727            data = data[1:]
728
729        indices = (0, 2), (2, 4), (4, 6)
730        rgb = []
731        for start, end in indices:
732            value = data[start:end]
733            rgb.append(int(value, base=16))
734
735        self._rgb = rgb[0], rgb[1], rgb[2]
736
737        assert len(self._rgb) == 3
738
739
740SYSTEM_TO_TYPE: dict[ColorSystem, Type[Color]] = {
741    ColorSystem.NO_COLOR: GreyscaleRampColor,
742    ColorSystem.STANDARD: StandardColor,
743    ColorSystem.EIGHT_BIT: IndexedColor,
744    ColorSystem.TRUE: RGBColor,
745}
746
747
748def _get_color_difference(
749    rgb1: tuple[int, int, int], rgb2: tuple[int, int, int]
750) -> float:
751    """Gets the geometric difference of 2 RGB colors (0-255).
752
753    See https://en.wikipedia.org/wiki/Color_difference's Euclidian section.
754    """
755
756    red1, green1, blue1 = rgb1
757    red2, green2, blue2 = rgb2
758
759    redmean = (red1 + red2) // 2
760
761    delta_red = red1 - red2
762    delta_green = green1 - green2
763    delta_blue = blue1 - blue2
764
765    return sqrt(
766        (2 + (redmean / 256)) * (delta_red**2)
767        + 4 * (delta_green**2)
768        + (2 + (255 - redmean) / 256) * (delta_blue**2)
769    )
770
771
772@lru_cache(maxsize=None)
773def str_to_color(
774    text: str,
775    is_background: bool = False,
776    localize: bool = True,
777    use_cache: bool = False,
778) -> Color:
779    """Creates a `Color` from the given text.
780
781    Accepted formats:
782    - 0-255: `IndexedColor`.
783    - 'rrr;ggg;bbb': `RGBColor`.
784    - '(#)rrggbb': `HEXColor`. Leading hash is optional.
785
786    You can also add a leading '@' into the string to make the output represent a
787    background color, such as `@#123abc`.
788
789    Args:
790        text: The string to format from.
791        is_background: Whether the output should be forced into a background color.
792            Mostly used internally, when set will take precedence over syntax of leading
793            '@' symbol.
794        localize: Whether `get_localized` should be called on the output color.
795        use_cache: Whether caching should be used.
796    """
797
798    def _trim_code(code: str) -> str:
799        """Trims the given color code."""
800
801        is_background = code.startswith("48;")
802
803        if (code.startswith("38;5;") or code.startswith("48;5;")) or (
804            code.startswith("38;2;") or code.startswith("48;2;")
805        ):
806            code = code[5:]
807
808        if code.endswith("m"):
809            code = code[:-1]
810
811        if is_background:
812            code = "@" + code
813
814        return code
815
816    text = _trim_code(text)
817
818    if not use_cache:
819        str_to_color.cache_clear()
820
821    if text.startswith("@"):
822        is_background = True
823        text = text[1:]
824
825    if text in NAMED_COLORS:
826        return str_to_color(str(NAMED_COLORS[text]), is_background=is_background)
827
828    color: Color
829
830    # This code is not pretty, but having these separate branches for each type
831    # should improve the performance by quite a large margin.
832    match = RE_256.match(text)
833    if match is not None:
834        # Note: At the moment, all colors become an `IndexedColor`, due to a large
835        #       amount of problems a separated `StandardColor` class caused. Not
836        #       sure if there are any real drawbacks to doing it this way, bar the
837        #       extra characters that 255 colors use up compared to xterm-16.
838        color = IndexedColor(match[0], background=is_background)
839
840        return color.get_localized() if localize else color
841
842    match = RE_HEX.match(text)
843    if match is not None:
844        color = HEXColor(match[0], background=is_background).get_localized()
845
846        return color.get_localized() if localize else color
847
848    match = RE_RGB.match(text)
849    if match is not None:
850        color = RGBColor(match[0], background=is_background).get_localized()
851
852        return color.get_localized() if localize else color
853
854    raise ColorSyntaxError(f"Could not convert {text!r} into a `Color`.")
855
856
857def foreground(text: str, color: str | Color, reset: bool = True) -> str:
858    """Sets the foreground color of the given text.
859
860    Note that the given color will be forced into `background = True`.
861
862    Args:
863        text: The text to color.
864        color: The color to use. See `pytermgui.colors.str_to_color` for accepted
865            str formats.
866        reset: Whether the return value should include a reset sequence at the end.
867
868    Returns:
869        The colored text, including a reset if set.
870    """
871
872    if not isinstance(color, Color):
873        color = str_to_color(color)
874
875    color.background = False
876
877    return color(text, reset=reset)
878
879
880def background(text: str, color: str | Color, reset: bool = True) -> str:
881    """Sets the background color of the given text.
882
883    Note that the given color will be forced into `background = True`.
884
885    Args:
886        text: The text to color.
887        color: The color to use. See `pytermgui.colors.str_to_color` for accepted
888            str formats.
889        reset: Whether the return value should include a reset sequence at the end.
890
891    Returns:
892        The colored text, including a reset if set.
893    """
894
895    if not isinstance(color, Color):
896        color = str_to_color(color)
897
898    color.background = True
899
900    return color(text, reset=reset)
#   COLOR_TABLE = [(0, 0, 0), (170, 0, 0), (0, 170, 0), (170, 85, 0), (0, 0, 170), (170, 0, 170), (0, 170, 170), (170, 170, 170), (85, 85, 85), (255, 85, 85), (85, 255, 85), (255, 255, 85), (85, 85, 255), (255, 85, 255), (85, 255, 255), (255, 255, 255), (0, 0, 0), (0, 0, 95), (0, 0, 135), (0, 0, 175), (0, 0, 215), (0, 0, 255), (0, 95, 0), (0, 95, 95), (0, 95, 135), (0, 95, 175), (0, 95, 215), (0, 95, 255), (0, 135, 0), (0, 135, 95), (0, 135, 135), (0, 135, 175), (0, 135, 215), (0, 135, 255), (0, 175, 0), (0, 175, 95), (0, 175, 135), (0, 175, 175), (0, 175, 215), (0, 175, 255), (0, 215, 0), (0, 215, 95), (0, 215, 135), (0, 215, 175), (0, 215, 215), (0, 215, 255), (0, 255, 0), (0, 255, 95), (0, 255, 135), (0, 255, 175), (0, 255, 215), (0, 255, 255), (95, 0, 0), (95, 0, 95), (95, 0, 135), (95, 0, 175), (95, 0, 215), (95, 0, 255), (95, 95, 0), (95, 95, 95), (95, 95, 135), (95, 95, 175), (95, 95, 215), (95, 95, 255), (95, 135, 0), (95, 135, 95), (95, 135, 135), (95, 135, 175), (95, 135, 215), (95, 135, 255), (95, 175, 0), (95, 175, 95), (95, 175, 135), (95, 175, 175), (95, 175, 215), (95, 175, 255), (95, 215, 0), (95, 215, 95), (95, 215, 135), (95, 215, 175), (95, 215, 215), (95, 215, 255), (95, 255, 0), (95, 255, 95), (95, 255, 135), (95, 255, 175), (95, 255, 215), (95, 255, 255), (135, 0, 0), (135, 0, 95), (135, 0, 135), (135, 0, 175), (135, 0, 215), (135, 0, 255), (135, 95, 0), (135, 95, 95), (135, 95, 135), (135, 95, 175), (135, 95, 215), (135, 95, 255), (135, 135, 0), (135, 135, 95), (135, 135, 135), (135, 135, 175), (135, 135, 215), (135, 135, 255), (135, 175, 0), (135, 175, 95), (135, 175, 135), (135, 175, 175), (135, 175, 215), (135, 175, 255), (135, 215, 0), (135, 215, 95), (135, 215, 135), (135, 215, 175), (135, 215, 215), (135, 215, 255), (135, 255, 0), (135, 255, 95), (135, 255, 135), (135, 255, 175), (135, 255, 215), (135, 255, 255), (175, 0, 0), (175, 0, 95), (175, 0, 135), (175, 0, 175), (175, 0, 215), (175, 0, 255), (175, 95, 0), (175, 95, 95), (175, 95, 135), (175, 95, 175), (175, 95, 215), (175, 95, 255), (175, 135, 0), (175, 135, 95), (175, 135, 135), (175, 135, 175), (175, 135, 215), (175, 135, 255), (175, 175, 0), (175, 175, 95), (175, 175, 135), (175, 175, 175), (175, 175, 215), (175, 175, 255), (175, 215, 0), (175, 215, 95), (175, 215, 135), (175, 215, 175), (175, 215, 215), (175, 215, 255), (175, 255, 0), (175, 255, 95), (175, 255, 135), (175, 255, 175), (175, 255, 215), (175, 255, 255), (215, 0, 0), (215, 0, 95), (215, 0, 135), (215, 0, 175), (215, 0, 215), (215, 0, 255), (215, 95, 0), (215, 95, 95), (215, 95, 135), (215, 95, 175), (215, 95, 215), (215, 95, 255), (215, 135, 0), (215, 135, 95), (215, 135, 135), (215, 135, 175), (215, 135, 215), (215, 135, 255), (215, 175, 0), (215, 175, 95), (215, 175, 135), (215, 175, 175), (215, 175, 215), (215, 175, 255), (215, 215, 0), (215, 215, 95), (215, 215, 135), (215, 215, 175), (215, 215, 215), (215, 215, 255), (215, 255, 0), (215, 255, 95), (215, 255, 135), (215, 255, 175), (215, 255, 215), (215, 255, 255), (255, 0, 0), (255, 0, 95), (255, 0, 135), (255, 0, 175), (255, 0, 215), (255, 0, 255), (255, 95, 0), (255, 95, 95), (255, 95, 135), (255, 95, 175), (255, 95, 215), (255, 95, 255), (255, 135, 0), (255, 135, 95), (255, 135, 135), (255, 135, 175), (255, 135, 215), (255, 135, 255), (255, 175, 0), (255, 175, 95), (255, 175, 135), (255, 175, 175), (255, 175, 215), (255, 175, 255), (255, 215, 0), (255, 215, 95), (255, 215, 135), (255, 215, 175), (255, 215, 215), (255, 215, 255), (255, 255, 0), (255, 255, 95), (255, 255, 135), (255, 255, 175), (255, 255, 215), (255, 255, 255), (8, 8, 8), (18, 18, 18), (28, 28, 28), (38, 38, 38), (48, 48, 48), (58, 58, 58), (68, 68, 68), (78, 78, 78), (88, 88, 88), (98, 98, 98), (108, 108, 108), (118, 118, 118), (128, 128, 128), (138, 138, 138), (148, 148, 148), (158, 158, 158), (168, 168, 168), (178, 178, 178), (188, 188, 188), (198, 198, 198), (208, 208, 208), (218, 218, 218), (228, 228, 228), (238, 238, 238)]
#   XTERM_NAMED_COLORS = {0: 'black', 1: 'red', 2: 'green', 3: 'yellow', 4: 'blue', 5: 'magenta', 6: 'cyan', 7: 'white', 8: 'bright-black', 9: 'bright-red', 10: 'bright-green', 11: 'bright-yellow', 12: 'bright-blue', 14: 'bright-magenta', 15: 'bright-cyan', 16: 'bright-white'}
#   NAMED_COLORS = {'black': '0', 'red': '1', 'green': '2', 'yellow': '3', 'blue': '4', 'magenta': '5', 'cyan': '6', 'white': '7', 'bright-black': '8', 'bright-red': '9', 'bright-green': '10', 'bright-yellow': '11', 'bright-blue': '12', 'bright-magenta': '14', 'bright-cyan': '15', 'bright-white': '16'}
#   def clear_color_cache() -> None:
View Source
340def clear_color_cache() -> None:
341    """Clears `_COLOR_CACHE` and `_COLOR_MATCH_CACHE`."""
342
343    _COLOR_CACHE.clear()
344    _COLOR_MATCH_CACHE.clear()

Clears _COLOR_CACHE and _COLOR_MATCH_CACHE.

#   def foreground( text: str, color: str | pytermgui.colors.Color, reset: bool = True ) -> str:
View Source
858def foreground(text: str, color: str | Color, reset: bool = True) -> str:
859    """Sets the foreground color of the given text.
860
861    Note that the given color will be forced into `background = True`.
862
863    Args:
864        text: The text to color.
865        color: The color to use. See `pytermgui.colors.str_to_color` for accepted
866            str formats.
867        reset: Whether the return value should include a reset sequence at the end.
868
869    Returns:
870        The colored text, including a reset if set.
871    """
872
873    if not isinstance(color, Color):
874        color = str_to_color(color)
875
876    color.background = False
877
878    return color(text, reset=reset)

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.

#   def background( text: str, color: str | pytermgui.colors.Color, reset: bool = True ) -> str:
View Source
881def background(text: str, color: str | Color, reset: bool = True) -> str:
882    """Sets the background color of the given text.
883
884    Note that the given color will be forced into `background = True`.
885
886    Args:
887        text: The text to color.
888        color: The color to use. See `pytermgui.colors.str_to_color` for accepted
889            str formats.
890        reset: Whether the return value should include a reset sequence at the end.
891
892    Returns:
893        The colored text, including a reset if set.
894    """
895
896    if not isinstance(color, Color):
897        color = str_to_color(color)
898
899    color.background = True
900
901    return color(text, reset=reset)

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.

#  
@lru_cache(maxsize=None)
def str_to_color( text: str, is_background: bool = False, localize: bool = True, use_cache: bool = False ) -> pytermgui.colors.Color:
View Source
773@lru_cache(maxsize=None)
774def str_to_color(
775    text: str,
776    is_background: bool = False,
777    localize: bool = True,
778    use_cache: bool = False,
779) -> Color:
780    """Creates a `Color` from the given text.
781
782    Accepted formats:
783    - 0-255: `IndexedColor`.
784    - 'rrr;ggg;bbb': `RGBColor`.
785    - '(#)rrggbb': `HEXColor`. Leading hash is optional.
786
787    You can also add a leading '@' into the string to make the output represent a
788    background color, such as `@#123abc`.
789
790    Args:
791        text: The string to format from.
792        is_background: Whether the output should be forced into a background color.
793            Mostly used internally, when set will take precedence over syntax of leading
794            '@' symbol.
795        localize: Whether `get_localized` should be called on the output color.
796        use_cache: Whether caching should be used.
797    """
798
799    def _trim_code(code: str) -> str:
800        """Trims the given color code."""
801
802        is_background = code.startswith("48;")
803
804        if (code.startswith("38;5;") or code.startswith("48;5;")) or (
805            code.startswith("38;2;") or code.startswith("48;2;")
806        ):
807            code = code[5:]
808
809        if code.endswith("m"):
810            code = code[:-1]
811
812        if is_background:
813            code = "@" + code
814
815        return code
816
817    text = _trim_code(text)
818
819    if not use_cache:
820        str_to_color.cache_clear()
821
822    if text.startswith("@"):
823        is_background = True
824        text = text[1:]
825
826    if text in NAMED_COLORS:
827        return str_to_color(str(NAMED_COLORS[text]), is_background=is_background)
828
829    color: Color
830
831    # This code is not pretty, but having these separate branches for each type
832    # should improve the performance by quite a large margin.
833    match = RE_256.match(text)
834    if match is not None:
835        # Note: At the moment, all colors become an `IndexedColor`, due to a large
836        #       amount of problems a separated `StandardColor` class caused. Not
837        #       sure if there are any real drawbacks to doing it this way, bar the
838        #       extra characters that 255 colors use up compared to xterm-16.
839        color = IndexedColor(match[0], background=is_background)
840
841        return color.get_localized() if localize else color
842
843    match = RE_HEX.match(text)
844    if match is not None:
845        color = HEXColor(match[0], background=is_background).get_localized()
846
847        return color.get_localized() if localize else color
848
849    match = RE_RGB.match(text)
850    if match is not None:
851        color = RGBColor(match[0], background=is_background).get_localized()
852
853        return color.get_localized() if localize else color
854
855    raise ColorSyntaxError(f"Could not convert {text!r} into a `Color`.")

Creates a Color from the given text.

Accepted formats:

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.
#  
@dataclass
class Color:
View Source
383@dataclass
384class Color:
385    """A terminal color.
386
387    Args:
388        value: The data contained within this color.
389        background: Whether this color will represent a color.
390    """
391
392    value: str
393    background: bool = False
394
395    system: ColorSystem = field(init=False)
396
397    default_foreground: Color | None = field(default=None, repr=False)
398    default_background: Color | None = field(default=None, repr=False)
399
400    _luminance: float | None = field(init=False, default=None, repr=False)
401    _brightness: float | None = field(init=False, default=None, repr=False)
402    _rgb: tuple[int, int, int] | None = field(init=False, default=None, repr=False)
403
404    @classmethod
405    def from_rgb(cls, rgb: tuple[int, int, int]) -> Color:
406        """Creates a color from the given RGB, within terminal's colorsystem.
407
408        Args:
409            rgb: The RGB value to base the new color off of.
410        """
411
412        raise NotImplementedError
413
414    @property
415    def sequence(self) -> str:
416        """Returns the ANSI sequence representation of the color."""
417
418        raise NotImplementedError
419
420    @cached_property
421    def rgb(self) -> tuple[int, int, int]:
422        """Returns this color as a tuple of (red, green, blue) values."""
423
424        if self._rgb is None:
425            raise NotImplementedError
426
427        return self._rgb
428
429    @cached_property
430    def hex(self) -> str:
431        """Returns CSS-like HEX representation of this color."""
432
433        buff = "#"
434        for color in self.rgb:
435            buff += f"{format(color, 'x'):0>2}"
436
437        return buff
438
439    @classmethod
440    def get_default_foreground(cls) -> Color:
441        """Gets the terminal emulator's default foreground color."""
442
443        if cls.default_foreground is not None:
444            return cls.default_foreground
445
446        return _get_palette_color("10")
447
448    @classmethod
449    def get_default_background(cls) -> Color:
450        """Gets the terminal emulator's default foreground color."""
451
452        if cls.default_background is not None:
453            return cls.default_background
454
455        return _get_palette_color("11")
456
457    @property
458    def name(self) -> str:
459        """Returns the reverse-parseable name of this color."""
460
461        return ("@" if self.background else "") + self.value
462
463    @property
464    def luminance(self) -> float:
465        """Returns this color's perceived luminance (brightness).
466
467        From https://stackoverflow.com/a/596243
468        """
469
470        # Don't do expensive calculations over and over
471        if self._luminance is not None:
472            return self._luminance
473
474        def _linearize(color: float) -> float:
475            """Converts sRGB color to linear value."""
476
477            if color <= 0.04045:
478                return color / 12.92
479
480            return ((color + 0.055) / 1.055) ** 2.4
481
482        red, green, blue = float(self.rgb[0]), float(self.rgb[1]), float(self.rgb[2])
483
484        red /= 255
485        green /= 255
486        blue /= 255
487
488        red = _linearize(red)
489        blue = _linearize(blue)
490        green = _linearize(green)
491
492        self._luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue
493
494        return self._luminance
495
496    @property
497    def brightness(self) -> float:
498        """Returns the perceived "brightness" of a color.
499
500        From https://stackoverflow.com/a/56678483
501        """
502
503        # Don't do expensive calculations over and over
504        if self._brightness is not None:
505            return self._brightness
506
507        if self.luminance <= (216 / 24389):
508            brightness = self.luminance * (24389 / 27)
509
510        else:
511            brightness = self.luminance ** (1 / 3) * 116 - 16
512
513        self._brightness = brightness / 100
514        return self._brightness
515
516    def __call__(self, text: str, reset: bool = True) -> StyledText:
517        """Colors the given string, returning a `pytermgui.parser.StyledText`."""
518
519        # We import this here as toplevel would cause circular imports, and it won't
520        # be used until this method is called anyways.
521        from .parser import StyledText  # pylint: disable=import-outside-toplevel
522
523        buff = self.sequence + text
524        if reset:
525            buff += reset_style()
526
527        return StyledText(buff)
528
529    def get_localized(self) -> Color:
530        """Creates a terminal-capability local Color instance.
531
532        This method essentially allows for graceful degradation of colors in the
533        terminal.
534        """
535
536        system = terminal.colorsystem
537        if self.system <= system:
538            return self
539
540        colortype = SYSTEM_TO_TYPE[system]
541
542        local = colortype.from_rgb(self.rgb)
543        local.background = self.background
544
545        return local

A terminal color.

Args
  • value: The data contained within this color.
  • background: Whether this color will represent a color.
#   Color( value: str, background: bool = False, default_foreground: pytermgui.colors.Color | None = None, default_background: pytermgui.colors.Color | None = None )
#   background: bool = False
#   default_foreground: pytermgui.colors.Color | None = None
#   default_background: pytermgui.colors.Color | None = None
#  
@classmethod
def from_rgb(cls, rgb: tuple[int, int, int]) -> pytermgui.colors.Color:
View Source
404    @classmethod
405    def from_rgb(cls, rgb: tuple[int, int, int]) -> Color:
406        """Creates a color from the given RGB, within terminal's colorsystem.
407
408        Args:
409            rgb: The RGB value to base the new color off of.
410        """
411
412        raise NotImplementedError

Creates a color from the given RGB, within terminal's colorsystem.

Args
  • rgb: The RGB value to base the new color off of.
#   sequence: str

Returns the ANSI sequence representation of the color.

#   rgb: tuple[int, int, int]

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

#   hex: str

Returns CSS-like HEX representation of this color.

#  
@classmethod
def get_default_foreground(cls) -> pytermgui.colors.Color:
View Source
439    @classmethod
440    def get_default_foreground(cls) -> Color:
441        """Gets the terminal emulator's default foreground color."""
442
443        if cls.default_foreground is not None:
444            return cls.default_foreground
445
446        return _get_palette_color("10")

Gets the terminal emulator's default foreground color.

#  
@classmethod
def get_default_background(cls) -> pytermgui.colors.Color:
View Source
448    @classmethod
449    def get_default_background(cls) -> Color:
450        """Gets the terminal emulator's default foreground color."""
451
452        if cls.default_background is not None:
453            return cls.default_background
454
455        return _get_palette_color("11")

Gets the terminal emulator's default foreground color.

#   name: str

Returns the reverse-parseable name of this color.

#   luminance: float

Returns this color's perceived luminance (brightness).

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

#   brightness: float

Returns the perceived "brightness" of a color.

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

#   def get_localized(self) -> pytermgui.colors.Color:
View Source
529    def get_localized(self) -> Color:
530        """Creates a terminal-capability local Color instance.
531
532        This method essentially allows for graceful degradation of colors in the
533        terminal.
534        """
535
536        system = terminal.colorsystem
537        if self.system <= system:
538            return self
539
540        colortype = SYSTEM_TO_TYPE[system]
541
542        local = colortype.from_rgb(self.rgb)
543        local.background = self.background
544
545        return local

Creates a terminal-capability local Color instance.

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

#  
@dataclass(repr=False)
class IndexedColor(Color):
View Source
548@dataclass(repr=False)
549class IndexedColor(Color):
550    """A color representing an index into the xterm-256 color palette."""
551
552    system = ColorSystem.EIGHT_BIT
553
554    def __post_init__(self) -> None:
555        """Ensures data validity."""
556
557        if not self.value.isdigit():
558            raise ValueError(
559                f"IndexedColor value has to be numerical, got {self.value!r}."
560            )
561
562        if not 0 <= int(self.value) < 256:
563            raise ValueError(
564                f"IndexedColor value has to fit in range 0-255, got {self.value!r}."
565            )
566
567    @classmethod
568    def from_rgb(cls, rgb: tuple[int, int, int]) -> IndexedColor:
569        """Constructs an `IndexedColor` from the closest matching option."""
570
571        if rgb in _COLOR_MATCH_CACHE:
572            color = _COLOR_MATCH_CACHE[rgb]
573
574            assert isinstance(color, IndexedColor)
575            return color
576
577        if terminal.colorsystem == ColorSystem.STANDARD:
578            return StandardColor.from_rgb(rgb)
579
580        # Normalize the color values
581        red, green, blue = (x / 255 for x in rgb)
582
583        # Calculate the eight-bit color index
584        color_num = 16
585        color_num += 36 * round(red * 5.0)
586        color_num += 6 * round(green * 5.0)
587        color_num += round(blue * 5.0)
588
589        color = cls(str(color_num))
590        _COLOR_MATCH_CACHE[rgb] = color
591
592        return color
593
594    @property
595    def sequence(self) -> str:
596        r"""Returns an ANSI sequence representing this color."""
597
598        index = int(self.value)
599
600        return "\x1b[" + ("48" if self.background else "38") + f";5;{index}m"
601
602    @cached_property
603    def rgb(self) -> tuple[int, int, int]:
604        """Returns an RGB representation of this color."""
605
606        if self._rgb is not None:
607            return self._rgb
608
609        index = int(self.value)
610        rgb = COLOR_TABLE[index]
611
612        return (rgb[0], rgb[1], rgb[2])

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

#   IndexedColor( value: str, background: bool = False, default_foreground: pytermgui.colors.Color | None = None, default_background: pytermgui.colors.Color | None = None )
#   system: pytermgui.terminal.ColorSystem = <ColorSystem.EIGHT_BIT: 1>
#  
@classmethod
def from_rgb(cls, rgb: tuple[int, int, int]) -> pytermgui.colors.IndexedColor:
View Source
567    @classmethod
568    def from_rgb(cls, rgb: tuple[int, int, int]) -> IndexedColor:
569        """Constructs an `IndexedColor` from the closest matching option."""
570
571        if rgb in _COLOR_MATCH_CACHE:
572            color = _COLOR_MATCH_CACHE[rgb]
573
574            assert isinstance(color, IndexedColor)
575            return color
576
577        if terminal.colorsystem == ColorSystem.STANDARD:
578            return StandardColor.from_rgb(rgb)
579
580        # Normalize the color values
581        red, green, blue = (x / 255 for x in rgb)
582
583        # Calculate the eight-bit color index
584        color_num = 16
585        color_num += 36 * round(red * 5.0)
586        color_num += 6 * round(green * 5.0)
587        color_num += round(blue * 5.0)
588
589        color = cls(str(color_num))
590        _COLOR_MATCH_CACHE[rgb] = color
591
592        return color

Constructs an IndexedColor from the closest matching option.

#   sequence: str

Returns an ANSI sequence representing this color.

#   rgb: tuple[int, int, int]

Returns an RGB representation of this color.

#  
@dataclass(repr=False)
class RGBColor(Color):
View Source
680@dataclass(repr=False)
681class RGBColor(Color):
682    """An arbitrary RGB color."""
683
684    system = ColorSystem.TRUE
685
686    def __post_init__(self) -> None:
687        """Ensures data validity."""
688
689        if self.value.count(";") != 2:
690            raise ValueError(
691                "Invalid value passed to RGBColor."
692                + f" Format has to be rrr;ggg;bbb, got {self.value!r}."
693            )
694
695        rgb = tuple(int(num) for num in self.value.split(";"))
696        self._rgb = rgb[0], rgb[1], rgb[2]
697
698    @classmethod
699    def from_rgb(cls, rgb: tuple[int, int, int]) -> RGBColor:
700        """Returns an `RGBColor` from the given triplet."""
701
702        return cls(";".join(map(str, rgb)))
703
704    @property
705    def sequence(self) -> str:
706        """Returns the ANSI sequence representing this color."""
707
708        return (
709            "\x1b["
710            + ("48" if self.background else "38")
711            + ";2;"
712            + ";".join(str(num) for num in self.rgb)
713            + "m"
714        )

An arbitrary RGB color.

#   RGBColor( value: str, background: bool = False, default_foreground: pytermgui.colors.Color | None = None, default_background: pytermgui.colors.Color | None = None )
#   system: pytermgui.terminal.ColorSystem = <ColorSystem.TRUE: 2>
#  
@classmethod
def from_rgb(cls, rgb: tuple[int, int, int]) -> pytermgui.colors.RGBColor:
View Source
698    @classmethod
699    def from_rgb(cls, rgb: tuple[int, int, int]) -> RGBColor:
700        """Returns an `RGBColor` from the given triplet."""
701
702        return cls(";".join(map(str, rgb)))

Returns an RGBColor from the given triplet.

#   sequence: str

Returns the ANSI sequence representing this color.

#  
@dataclass
class HEXColor(RGBColor):
View Source
717@dataclass
718class HEXColor(RGBColor):
719    """An arbitrary, CSS-like HEX color."""
720
721    system = ColorSystem.TRUE
722
723    def __post_init__(self) -> None:
724        """Ensures data validity."""
725
726        data = self.value
727        if data.startswith("#"):
728            data = data[1:]
729
730        indices = (0, 2), (2, 4), (4, 6)
731        rgb = []
732        for start, end in indices:
733            value = data[start:end]
734            rgb.append(int(value, base=16))
735
736        self._rgb = rgb[0], rgb[1], rgb[2]
737
738        assert len(self._rgb) == 3

An arbitrary, CSS-like HEX color.

#   HEXColor( value: str, background: bool = False, default_foreground: pytermgui.colors.Color | None = None, default_background: pytermgui.colors.Color | None = None )
#   system: pytermgui.terminal.ColorSystem = <ColorSystem.TRUE: 2>