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.
- rich_gradient/__init__.py +18 -0
- rich_gradient/_simple_gradient.py +315 -0
- rich_gradient/color.py +875 -0
- rich_gradient/default_styles.py +778 -0
- rich_gradient/examples/__init__.py +0 -0
- rich_gradient/examples/example_gradient.py +180 -0
- rich_gradient/examples/example_rule.py +62 -0
- rich_gradient/examples/py.typed +0 -0
- rich_gradient/gradient.py +593 -0
- rich_gradient/py.typed +0 -0
- rich_gradient/rule.py +374 -0
- rich_gradient/spectrum.py +162 -0
- rich_gradient/theme.py +102 -0
- rich_gradient-0.1.0.dist-info/METADATA +138 -0
- rich_gradient-0.1.0.dist-info/RECORD +16 -0
- rich_gradient-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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)
|