thehelp.gradient

  1import string
  2from dataclasses import dataclass
  3
  4from rich.color import Color
  5from typing_extensions import Self
  6
  7from .colormap import Tag
  8
  9
 10@dataclass
 11class RGB:
 12    """
 13    Dataclass representing a 3 channel RGB color that is converted to a `rich` tag when casted to a string.
 14
 15    >>> color = RGB(100, 100, 100)
 16    >>> str(color)
 17    >>> "[rgb(100,100,100)]"
 18    >>> from rich.console import Console
 19    >>> console = Console()
 20    >>> console.print(f"{color}Yeehaw")
 21
 22    Can also be initialized using a color name from https://rich.readthedocs.io/en/stable/appendix/colors.html
 23
 24    >>> color = RGB(name="magenta3")
 25    >>> print(color)
 26    >>> "[rgb(215,0,215)]"
 27
 28    Supports addition and subtraction of `RGB` objects as well as scalar multiplication and division.
 29
 30    >>> color1 = RGB(100, 100, 100)
 31    >>> color2 = RGB(25, 50, 75)
 32    >>> print(color1 + color2)
 33    >>> "[rgb(125,150,175)]"
 34    >>> print(color2 * 2)
 35    >>> "[rgb(50,100,150)]"
 36    """
 37
 38    # Typing these as floats so `Gradient` can fractionally increment them
 39    # When casted to a string, the values will be rounded to integers
 40    r: float = 0
 41    g: float = 0
 42    b: float = 0
 43    name: str = ""
 44
 45    def __post_init__(self):
 46        if self.name:
 47            self.r, self.g, self.b = Color.parse(self.name).get_truecolor()
 48
 49    def __str__(self) -> str:
 50        return f"[rgb({round(self.r)},{round(self.g)},{round(self.b)})]"
 51
 52    def __sub__(self, other: Self) -> Self:
 53        return self.__class__(self.r - other.r, self.g - other.g, self.b - other.b)
 54
 55    def __add__(self, other: Self) -> Self:
 56        return self.__class__(self.r + other.r, self.g + other.g, self.b + other.b)
 57
 58    def __truediv__(self, val: float) -> Self:
 59        return self.__class__(self.r / val, self.g / val, self.b / val)
 60
 61    def __mul__(self, val: float) -> Self:
 62        return self.__class__(self.r * val, self.g * val, self.b * val)
 63
 64    def __eq__(self, other: Self) -> bool:
 65        return all(getattr(self, c) == getattr(other, c) for c in "rgb")
 66
 67
 68class Gradient:
 69    """
 70    Apply a color gradient to strings when using `rich`.
 71
 72    When applied to a string, each character will increment in color from a start to a stop color.
 73
 74    Start and stop colors can be specified by either
 75    a 3 tuple representing RGB values,
 76    a `shortrich.Tag` object,
 77    or a color name from https://rich.readthedocs.io/en/stable/appendix/colors.html.
 78
 79    Tuple:
 80    >>> gradient = Gradient((255, 0, 0), (0, 255, 0))
 81
 82    `shortrich.Tag`:
 83    >>> colors = shortrich.ColorMap()
 84    >>> gradient = Gradient(colors.red, colors.green)
 85
 86    Name:
 87    >>> gradient = Gradient("red", "green")
 88
 89    Usage:
 90    >>> from shortrich import Gradient
 91    >>> from rich.console import Console
 92    >>> console = Console()
 93    >>> gradient = Gradient("red", "green")
 94    >>> text = "Yeehaw"
 95    >>> gradient_text = gradient.apply(text)
 96    >>> # This produces:
 97    >>> print(gradient_text)
 98    >>> "[rgb(128,0,0)]Y[/][rgb(102,25,0)]e[/][rgb(76,51,0)]e[/][rgb(51,76,0)]h[/][rgb(25,102,0)]a[/][rgb(0,128,0)]w[/]"
 99    >>> # When used with `console.print`, each character will be a different color
100    >>> console.print(gradient_text)
101
102    """
103
104    def __init__(
105        self,
106        start: tuple[int, int, int] | str | Tag = "pink1",
107        stop: tuple[int, int, int] | str | Tag = "turquoise2",
108    ):
109        self._start = self._parse(start)
110        self._stop = self._parse(stop)
111
112    @property
113    def start(self) -> RGB:
114        """The starting color for the gradient."""
115        return self._start
116
117    @start.setter
118    def start(self, color: str | Tag | tuple[int, int, int]):
119        self._start = self._parse(color)
120
121    @property
122    def stop(self) -> RGB:
123        """The ending color for the gradient."""
124        return self._stop
125
126    @stop.setter
127    def stop(self, color: str | Tag | tuple[int, int, int]):
128        self._stop = self._parse(color)
129
130    @property
131    def valid_characters(self) -> str:
132        """Characters a color step can be applied to."""
133        return string.ascii_letters + string.digits + string.punctuation
134
135    def _parse(self, color: str | Tag | tuple[int, int, int]) -> RGB:
136        if isinstance(color, str):
137            return RGB(name=color)
138        elif isinstance(color, Tag):
139            return RGB(name=color.name)
140        elif isinstance(color, tuple):
141            return RGB(*color)
142        raise ValueError(f"{color!r} is an invalid type.")
143
144    def _get_step_sizes(self, total_steps: int) -> RGB:
145        """Returns a `RGB` object representing the step size for each color channel."""
146        return (self.stop - self.start) / total_steps
147
148    def _get_gradient_color(self, step: int, step_sizes: RGB) -> RGB:
149        """Returns a `RGB` object representing the color at `step`."""
150        return self.start + (step_sizes * step)
151
152    def _get_num_steps(self, text: str) -> int:
153        """Returns the number of steps the gradient should be divided into."""
154        return len([ch for ch in text if ch in self.valid_characters]) - 1
155
156    def apply(self, text: str) -> str:
157        """Apply the gradient to ascii letters, digits, and punctuation in `text`."""
158        steps = self._get_num_steps(text)
159        step_sizes = self._get_step_sizes(steps)
160        gradient_text = ""
161        step = 0
162        for ch in text:
163            if ch in self.valid_characters:
164                gradient_text += f"{self._get_gradient_color(step, step_sizes)}{ch}[/]"
165                step += 1
166            else:
167                gradient_text += ch
168        return gradient_text
@dataclass
class RGB:
11@dataclass
12class RGB:
13    """
14    Dataclass representing a 3 channel RGB color that is converted to a `rich` tag when casted to a string.
15
16    >>> color = RGB(100, 100, 100)
17    >>> str(color)
18    >>> "[rgb(100,100,100)]"
19    >>> from rich.console import Console
20    >>> console = Console()
21    >>> console.print(f"{color}Yeehaw")
22
23    Can also be initialized using a color name from https://rich.readthedocs.io/en/stable/appendix/colors.html
24
25    >>> color = RGB(name="magenta3")
26    >>> print(color)
27    >>> "[rgb(215,0,215)]"
28
29    Supports addition and subtraction of `RGB` objects as well as scalar multiplication and division.
30
31    >>> color1 = RGB(100, 100, 100)
32    >>> color2 = RGB(25, 50, 75)
33    >>> print(color1 + color2)
34    >>> "[rgb(125,150,175)]"
35    >>> print(color2 * 2)
36    >>> "[rgb(50,100,150)]"
37    """
38
39    # Typing these as floats so `Gradient` can fractionally increment them
40    # When casted to a string, the values will be rounded to integers
41    r: float = 0
42    g: float = 0
43    b: float = 0
44    name: str = ""
45
46    def __post_init__(self):
47        if self.name:
48            self.r, self.g, self.b = Color.parse(self.name).get_truecolor()
49
50    def __str__(self) -> str:
51        return f"[rgb({round(self.r)},{round(self.g)},{round(self.b)})]"
52
53    def __sub__(self, other: Self) -> Self:
54        return self.__class__(self.r - other.r, self.g - other.g, self.b - other.b)
55
56    def __add__(self, other: Self) -> Self:
57        return self.__class__(self.r + other.r, self.g + other.g, self.b + other.b)
58
59    def __truediv__(self, val: float) -> Self:
60        return self.__class__(self.r / val, self.g / val, self.b / val)
61
62    def __mul__(self, val: float) -> Self:
63        return self.__class__(self.r * val, self.g * val, self.b * val)
64
65    def __eq__(self, other: Self) -> bool:
66        return all(getattr(self, c) == getattr(other, c) for c in "rgb")

Dataclass representing a 3 channel RGB color that is converted to a rich tag when casted to a string.

>>> color = RGB(100, 100, 100)
>>> str(color)
>>> "[rgb(100,100,100)]"
>>> from rich.console import Console
>>> console = Console()
>>> console.print(f"{color}Yeehaw")

Can also be initialized using a color name from https://rich.readthedocs.io/en/stable/appendix/colors.html

>>> color = RGB(name="magenta3")
>>> print(color)
>>> "[rgb(215,0,215)]"

Supports addition and subtraction of RGB objects as well as scalar multiplication and division.

>>> color1 = RGB(100, 100, 100)
>>> color2 = RGB(25, 50, 75)
>>> print(color1 + color2)
>>> "[rgb(125,150,175)]"
>>> print(color2 * 2)
>>> "[rgb(50,100,150)]"
RGB(r: float = 0, g: float = 0, b: float = 0, name: str = '')
class Gradient:
 69class Gradient:
 70    """
 71    Apply a color gradient to strings when using `rich`.
 72
 73    When applied to a string, each character will increment in color from a start to a stop color.
 74
 75    Start and stop colors can be specified by either
 76    a 3 tuple representing RGB values,
 77    a `shortrich.Tag` object,
 78    or a color name from https://rich.readthedocs.io/en/stable/appendix/colors.html.
 79
 80    Tuple:
 81    >>> gradient = Gradient((255, 0, 0), (0, 255, 0))
 82
 83    `shortrich.Tag`:
 84    >>> colors = shortrich.ColorMap()
 85    >>> gradient = Gradient(colors.red, colors.green)
 86
 87    Name:
 88    >>> gradient = Gradient("red", "green")
 89
 90    Usage:
 91    >>> from shortrich import Gradient
 92    >>> from rich.console import Console
 93    >>> console = Console()
 94    >>> gradient = Gradient("red", "green")
 95    >>> text = "Yeehaw"
 96    >>> gradient_text = gradient.apply(text)
 97    >>> # This produces:
 98    >>> print(gradient_text)
 99    >>> "[rgb(128,0,0)]Y[/][rgb(102,25,0)]e[/][rgb(76,51,0)]e[/][rgb(51,76,0)]h[/][rgb(25,102,0)]a[/][rgb(0,128,0)]w[/]"
100    >>> # When used with `console.print`, each character will be a different color
101    >>> console.print(gradient_text)
102
103    """
104
105    def __init__(
106        self,
107        start: tuple[int, int, int] | str | Tag = "pink1",
108        stop: tuple[int, int, int] | str | Tag = "turquoise2",
109    ):
110        self._start = self._parse(start)
111        self._stop = self._parse(stop)
112
113    @property
114    def start(self) -> RGB:
115        """The starting color for the gradient."""
116        return self._start
117
118    @start.setter
119    def start(self, color: str | Tag | tuple[int, int, int]):
120        self._start = self._parse(color)
121
122    @property
123    def stop(self) -> RGB:
124        """The ending color for the gradient."""
125        return self._stop
126
127    @stop.setter
128    def stop(self, color: str | Tag | tuple[int, int, int]):
129        self._stop = self._parse(color)
130
131    @property
132    def valid_characters(self) -> str:
133        """Characters a color step can be applied to."""
134        return string.ascii_letters + string.digits + string.punctuation
135
136    def _parse(self, color: str | Tag | tuple[int, int, int]) -> RGB:
137        if isinstance(color, str):
138            return RGB(name=color)
139        elif isinstance(color, Tag):
140            return RGB(name=color.name)
141        elif isinstance(color, tuple):
142            return RGB(*color)
143        raise ValueError(f"{color!r} is an invalid type.")
144
145    def _get_step_sizes(self, total_steps: int) -> RGB:
146        """Returns a `RGB` object representing the step size for each color channel."""
147        return (self.stop - self.start) / total_steps
148
149    def _get_gradient_color(self, step: int, step_sizes: RGB) -> RGB:
150        """Returns a `RGB` object representing the color at `step`."""
151        return self.start + (step_sizes * step)
152
153    def _get_num_steps(self, text: str) -> int:
154        """Returns the number of steps the gradient should be divided into."""
155        return len([ch for ch in text if ch in self.valid_characters]) - 1
156
157    def apply(self, text: str) -> str:
158        """Apply the gradient to ascii letters, digits, and punctuation in `text`."""
159        steps = self._get_num_steps(text)
160        step_sizes = self._get_step_sizes(steps)
161        gradient_text = ""
162        step = 0
163        for ch in text:
164            if ch in self.valid_characters:
165                gradient_text += f"{self._get_gradient_color(step, step_sizes)}{ch}[/]"
166                step += 1
167            else:
168                gradient_text += ch
169        return gradient_text

Apply a color gradient to strings when using rich.

When applied to a string, each character will increment in color from a start to a stop color.

Start and stop colors can be specified by either a 3 tuple representing RGB values, a shortrich.Tag object, or a color name from https://rich.readthedocs.io/en/stable/appendix/colors.html.

Tuple:

>>> gradient = Gradient((255, 0, 0), (0, 255, 0))

shortrich.Tag:

>>> colors = shortrich.ColorMap()
>>> gradient = Gradient(colors.red, colors.green)

Name:

>>> gradient = Gradient("red", "green")

Usage:

>>> from shortrich import Gradient
>>> from rich.console import Console
>>> console = Console()
>>> gradient = Gradient("red", "green")
>>> text = "Yeehaw"
>>> gradient_text = gradient.apply(text)
>>> # This produces:
>>> print(gradient_text)
>>> "[rgb(128,0,0)]Y[/][rgb(102,25,0)]e[/][rgb(76,51,0)]e[/][rgb(51,76,0)]h[/][rgb(25,102,0)]a[/][rgb(0,128,0)]w[/]"
>>> # When used with `console.print`, each character will be a different color
>>> console.print(gradient_text)
Gradient( start: tuple[int, int, int] | str | thehelp.colormap.Tag = 'pink1', stop: tuple[int, int, int] | str | thehelp.colormap.Tag = 'turquoise2')
105    def __init__(
106        self,
107        start: tuple[int, int, int] | str | Tag = "pink1",
108        stop: tuple[int, int, int] | str | Tag = "turquoise2",
109    ):
110        self._start = self._parse(start)
111        self._stop = self._parse(stop)

The starting color for the gradient.

The ending color for the gradient.

valid_characters: str

Characters a color step can be applied to.

def apply(self, text: str) -> str:
157    def apply(self, text: str) -> str:
158        """Apply the gradient to ascii letters, digits, and punctuation in `text`."""
159        steps = self._get_num_steps(text)
160        step_sizes = self._get_step_sizes(steps)
161        gradient_text = ""
162        step = 0
163        for ch in text:
164            if ch in self.valid_characters:
165                gradient_text += f"{self._get_gradient_color(step, step_sizes)}{ch}[/]"
166                step += 1
167            else:
168                gradient_text += ch
169        return gradient_text

Apply the gradient to ascii letters, digits, and punctuation in text.