rich-gradient 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ # ruff: noqa: F401
2
+ from pydantic_extra_types.color import ColorType
3
+
4
+ from rich_gradient.color import Color
5
+ from rich_gradient.default_styles import DEFAULT_STYLES
6
+ from rich_gradient.gradient import Gradient
7
+ from rich_gradient.spectrum import Spectrum
8
+ from rich_gradient.theme import GRADIENT_TERMINAL_THEME, GradientTheme
9
+
10
+ __all__ = [
11
+ "Color",
12
+ "ColorType",
13
+ "DEFAULT_STYLES",
14
+ "Gradient",
15
+ "GRADIENT_TERMINAL_THEME",
16
+ "GradientTheme",
17
+ "Spectrum",
18
+ ]
@@ -0,0 +1,315 @@
1
+ """Generate a simple gradient"""
2
+
3
+ # ruff: noqa: F401
4
+ import re
5
+ from functools import partial
6
+ from operator import itemgetter
7
+ from typing import Any, Dict, Generator, Iterable, List, Literal, Optional, Tuple
8
+
9
+ import rich.style
10
+ from rich._pick import pick_bool
11
+ from rich.cells import cell_len
12
+ from rich.console import Console, ConsoleOptions, JustifyMethod, OverflowMethod
13
+ from rich.control import strip_control_codes
14
+ from rich.measure import Measurement
15
+ from rich.segment import Segment
16
+ from rich.style import Style, StyleType
17
+ from rich.text import Span, Text
18
+ from pydantic_extra_types.color import ColorType
19
+
20
+ from rich_gradient import Color
21
+
22
+ GradientMethod = Literal["default", "list", "mono", "rainbow"]
23
+ DEFAULT_JUSTIFY: JustifyMethod = "default"
24
+ DEFAULT_OVERFLOW: OverflowMethod = "fold"
25
+ WHITESPACE_REGEX = re.compile(r"^\s+$")
26
+
27
+
28
+ VERBOSE: bool = False
29
+
30
+
31
+ class SimpleGradient(Text):
32
+ """
33
+ Text with gradient with two colors.
34
+
35
+ Args:
36
+ text (Text|str): The text to print. Defaults to `""`.
37
+ color1 (ColorType): The first color.
38
+ color2 (ColorType): The second color.
39
+ justify (JustifyMethod, optional): Justify method. Defaults to "default".
40
+ overflow (OverflowMethod, optional): Overflow method. Defaults to "crop".
41
+ no_wrap (bool, optional): Disable wrapping. Defaults to False.
42
+ style (StyleType, optional): The style of the gradient text. Defaults to None.
43
+ end (str, optional): The end character. Defaults to " ".
44
+ """
45
+
46
+ __slots__ = (
47
+ "color1",
48
+ "color2",
49
+ "_text",
50
+ "_length",
51
+ "_style",
52
+ "_spans",
53
+ "end",
54
+ "verbose",
55
+ )
56
+
57
+ def __init__(
58
+ self,
59
+ text: str | Text = "",
60
+ *,
61
+ color1: ColorType,
62
+ color2: ColorType,
63
+ justify: JustifyMethod = "default",
64
+ overflow: OverflowMethod = "fold",
65
+ no_wrap: bool = False,
66
+ style: StyleType = Style.null(),
67
+ end: str = "",
68
+ spans: Optional[List[Span]] = None,
69
+ verbose: bool = False,
70
+ ) -> None:
71
+ self.verbose = verbose
72
+ self.text = text # type: ignore
73
+ _style = Style.parse(style) if isinstance(style, str) else style
74
+
75
+ super().__init__(
76
+ text=self.text,
77
+ style=_style,
78
+ justify=justify,
79
+ overflow=overflow,
80
+ no_wrap=no_wrap,
81
+ end=end,
82
+ spans=spans,
83
+ )
84
+
85
+ if not isinstance(color1, Color):
86
+ color1 = Color(color1) # type: ignore
87
+ if not isinstance(color2, Color):
88
+ color2 = Color(color2) # type: ignore
89
+
90
+ self.color1 = Color(color1)
91
+ self.color2 = Color(color2)
92
+ self._spans = list(self.generate_spans())
93
+
94
+ def __repr__(self) -> str:
95
+ return f"SimpleGradient({self.text!r}, \
96
+ {self.color1.as_named()!r}, \
97
+ {self.color2.as_named()!r}"
98
+
99
+ def __add__(self, other: Any) -> "Text":
100
+ if isinstance(other, (str, Text)):
101
+ result = self.copy()
102
+ result.append(other)
103
+ return result
104
+ return NotImplemented
105
+
106
+ def __eq__(self, other: object) -> bool:
107
+ if not isinstance(other, Text):
108
+ return NotImplemented
109
+ return self.plain == other.plain and self._spans == other._spans
110
+
111
+ @property
112
+ def text(self) -> str:
113
+ """
114
+ Returns the concatenated string representation of the `_text` attribute.
115
+
116
+ Returns:
117
+ str: The concatenated string representation of the `_text` attribute.
118
+ """
119
+ return "".join(self._text)
120
+
121
+ @text.setter
122
+ def text(self, value: str | Text) -> None:
123
+ """
124
+ Setter for the text attribute.
125
+
126
+ Args:
127
+ value (str|Text): The value to set for the text attribute.
128
+
129
+ Returns:
130
+ None
131
+ """
132
+ if isinstance(value, list):
133
+ value = "".join(value)
134
+ if isinstance(value, Text):
135
+ sanitized_text = strip_control_codes(value.plain)
136
+ self._length = value._length
137
+ self._text = value._text
138
+ self._spans = value.spans
139
+ elif isinstance(value, str):
140
+ if value == "":
141
+ raise ValueError("Text cannot be empty.")
142
+ sanitized_text = strip_control_codes(value)
143
+ self._length = len(sanitized_text)
144
+ self._text = sanitized_text # type: ignore
145
+ elif value is None:
146
+ raise ValueError("Text cannot be None.")
147
+ else:
148
+ raise TypeError(f"Text must be a string or Text, not {type(value)}")
149
+
150
+ @property # type: ignore
151
+ def style(self) -> Style:
152
+ """The style of the gradient."""
153
+ return self._style
154
+
155
+ @style.setter
156
+ def style(self, style: StyleType) -> None:
157
+ """
158
+ Setter for the style attribute.
159
+
160
+ Args:
161
+ style (StyleType): The value to set for the style attribute.
162
+ """
163
+ if style is None:
164
+ self._style: Style = Style.null()
165
+ elif isinstance(style, rich.style.Style):
166
+ self._style = style
167
+ else:
168
+ self._style = Style.parse(style)
169
+
170
+ def generate_spans(self) -> Generator[Span, None, None]:
171
+ """
172
+ Generate the gradient's spans.
173
+
174
+ Args:
175
+ None
176
+
177
+ Returns:
178
+ List[Span]: The gradient's spans
179
+ """
180
+ if self.verbose:
181
+ console.log("Entered generate_gradient")
182
+ triplet1 = self.color1.triplet
183
+ r1, g1, b1 = triplet1
184
+ triplet2 = self.color2.triplet
185
+ r2, g2, b2 = triplet2
186
+ dr: int = r2 - r1
187
+ dg: int = g2 - g1
188
+ db: int = b2 - b1
189
+
190
+ for index in range(self._length):
191
+ blend: float = index / self._length
192
+ red: int = int(r1 + (dr * blend))
193
+ green: int = int(g1 + (dg * blend))
194
+ blue: int = int(b1 + (db * blend))
195
+ hex_str: str = f"#{red:02X}{green:02X}{blue:02X}"
196
+ color = Color(hex_str)
197
+ style = color.style + self._style
198
+ yield Span(index, index + 1, style=style)
199
+
200
+ def __rich_console__(
201
+ self, console: "Console", options: "ConsoleOptions"
202
+ ) -> Iterable[Segment]:
203
+ tab_size: int = console.tab_size or self.tab_size or 8
204
+ justify = self.justify or options.justify or DEFAULT_JUSTIFY
205
+
206
+ overflow = self.overflow or options.overflow or DEFAULT_OVERFLOW
207
+
208
+ lines = self.wrap(
209
+ console,
210
+ options.max_width,
211
+ justify=justify,
212
+ overflow=overflow,
213
+ tab_size=tab_size or 8,
214
+ no_wrap=pick_bool(self.no_wrap, options.no_wrap, False), # type: ignore
215
+ )
216
+ all_lines = Text("\n").join(lines)
217
+ yield from all_lines.render(console, end=self.end)
218
+
219
+ def __rich_measure__(
220
+ self, console: "Console", options: "ConsoleOptions"
221
+ ) -> Measurement:
222
+ text: str = self.plain
223
+ lines: List[str] = text.splitlines()
224
+ max_text_width = max(cell_len(line) for line in lines) if lines else 0
225
+ words = text.split()
226
+ min_text_width = (
227
+ max(cell_len(word) for word in words) if words else max_text_width
228
+ )
229
+ return Measurement(min_text_width, max_text_width)
230
+
231
+ def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:
232
+ """Render the text as Segments.
233
+
234
+ Args:
235
+ console (Console): Console instance.
236
+ end (Optional[str], optional): Optional end character.
237
+
238
+ Returns:
239
+ Iterable[Segment]: Result of render that may be written to the console.
240
+ """
241
+ _Segment = Segment
242
+ text = str.strip(self.plain)
243
+ if not self._spans:
244
+ yield Segment(text)
245
+ if end:
246
+ yield _Segment(end)
247
+ return
248
+ get_style = partial(console.get_style, default=Style.null())
249
+
250
+ enumerated_spans = list(enumerate(self._spans, 1))
251
+ style_map = {index: get_style(span.style) for index, span in enumerated_spans}
252
+ style_map[0] = get_style(self.style)
253
+
254
+ spans = [
255
+ (0, False, 0),
256
+ *((span.start, False, index) for index, span in enumerated_spans),
257
+ *((span.end, True, index) for index, span in enumerated_spans),
258
+ (len(text), True, 0),
259
+ ]
260
+ spans.sort(key=itemgetter(0, 1))
261
+
262
+ stack: List[int] = []
263
+ stack_append = stack.append
264
+ stack_pop = stack.remove
265
+
266
+ style_cache: Dict[Tuple[Style, ...], Style] = {}
267
+ style_cache_get = style_cache.get
268
+ combine = Style.combine
269
+
270
+ def get_current_style() -> Style:
271
+ """Construct current style from stack."""
272
+ styles = tuple(style_map[_style_id] for _style_id in sorted(stack))
273
+ cached_style = style_cache_get(styles)
274
+ if cached_style is not None:
275
+ return cached_style
276
+ current_style = combine(styles)
277
+ style_cache[styles] = current_style
278
+ return current_style
279
+
280
+ for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):
281
+ if leaving:
282
+ stack_pop(style_id)
283
+ else:
284
+ stack_append(style_id)
285
+ if next_offset > offset:
286
+ yield _Segment(text[offset:next_offset], get_current_style())
287
+ if end:
288
+ yield _Segment(end)
289
+
290
+ def as_text(self, style: StyleType, end: str = "") -> Text:
291
+ return Text(self.plain, spans=self._spans, style=style, end=end)
292
+
293
+
294
+ if __name__ == "__main__": # pragma: no cover
295
+ from rich.panel import Panel
296
+ from rich_gradient.theme import GRADIENT_TERMINAL_THEME
297
+
298
+ console = Console(width=64, record=True)
299
+ sample_text = "SimpleGradient is a class that prints a `string` \nor `rich.text.Text` object as a gradient from \ncolor1 to color2 with an optional style."
300
+ gradient = SimpleGradient(sample_text, color1="green", color2="cyan", style="bold", justify="center") # type: ignore
301
+ gradient.highlight_regex(r"(`.+`)", "#af00ff")
302
+ console.line()
303
+ console.print(
304
+ Panel(
305
+ gradient,
306
+ padding=(1,4),
307
+ title=SimpleGradient("SimpleGradient", color1="#ff9900", color2="#ffff00", style="bold")
308
+ ),
309
+ justify="center"
310
+ )
311
+ console.line()
312
+ console.save_svg(
313
+ "docs/img/simple_gradient.svg",
314
+ title="Rich Gradient",
315
+ theme=GRADIENT_TERMINAL_THEME)