2from functools
import lru_cache
3from marshal
import dumps, loads
4from random
import randint
5from typing
import Any, Dict, Iterable, List, Optional, Type, Union, cast
8from .color
import Color, ColorParseError, ColorSystem, blend_rgb
9from .repr
import Result, rich_repr
10from .terminal_theme
import DEFAULT_TERMINAL_THEME, TerminalTheme
13StyleType = Union[str,
"Style"]
17 """A descriptor to get/set a style attribute bit."""
22 self.
bit = 1 << bit_no
24 def __get__(self, obj:
"Style", objtype: Type[
"Style"]) -> Optional[bool]:
34 A terminal style consists of a color (`color`), a background color (`bgcolor`), and a number of attributes, such
35 as bold, italic etc. The attributes have 3 states: they can either be on
36 (``True``), off (``False``), or not set (``None``).
39 color (Union[Color, str], optional): Color of terminal text. Defaults to None.
40 bgcolor (Union[Color, str], optional): Color of terminal background. Defaults to None.
41 bold (bool, optional): Enable bold text. Defaults to None.
42 dim (bool, optional): Enable dim text. Defaults to None.
43 italic (bool, optional): Enable italic text. Defaults to None.
44 underline (bool, optional): Enable underlined text. Defaults to None.
45 blink (bool, optional): Enabled blinking text. Defaults to None.
46 blink2 (bool, optional): Enable fast blinking text. Defaults to None.
47 reverse (bool, optional): Enabled reverse text. Defaults to None.
48 conceal (bool, optional): Enable concealed text. Defaults to None.
49 strike (bool, optional): Enable strikethrough text. Defaults to None.
50 underline2 (bool, optional): Enable doubly underlined text. Defaults to None.
51 frame (bool, optional): Enable framed text. Defaults to None.
52 encircle (bool, optional): Enable encircled text. Defaults to None.
53 overline (bool, optional): Enable overlined text. Defaults to None.
54 link (str, link): Link URL. Defaults to None.
58 _color: Optional[Color]
59 _bgcolor: Optional[Color]
64 _meta: Optional[bytes]
104 "underline":
"underline",
108 "reverse":
"reverse",
110 "conceal":
"conceal",
114 "underline2":
"underline2",
117 "encircle":
"encircle",
118 "overline":
"overline",
125 color: Optional[Union[Color, str]] =
None,
126 bgcolor: Optional[Union[Color, str]] =
None,
127 bold: Optional[bool] =
None,
128 dim: Optional[bool] =
None,
129 italic: Optional[bool] =
None,
130 underline: Optional[bool] =
None,
131 blink: Optional[bool] =
None,
132 blink2: Optional[bool] =
None,
133 reverse: Optional[bool] =
None,
134 conceal: Optional[bool] =
None,
135 strike: Optional[bool] =
None,
136 underline2: Optional[bool] =
None,
137 frame: Optional[bool] =
None,
138 encircle: Optional[bool] =
None,
139 overline: Optional[bool] =
None,
140 link: Optional[str] =
None,
141 meta: Optional[Dict[str, Any]] =
None,
143 self.
_ansi: Optional[str] =
None
146 def _make_color(color: Union[Color, str]) -> Color:
154 dim
is not None and 2,
155 italic
is not None and 4,
156 underline
is not None and 8,
157 blink
is not None and 16,
158 blink2
is not None and 32,
159 reverse
is not None and 64,
160 conceal
is not None and 128,
161 strike
is not None and 256,
162 underline2
is not None and 512,
163 frame
is not None and 1024,
164 encircle
is not None and 2048,
165 overline
is not None and 4096,
174 underline
and 8
or 0,
178 conceal
and 128
or 0,
180 underline2
and 512
or 0,
182 encircle
and 2048
or 0,
183 overline
and 4096
or 0,
193 f
"{randint(0, 999999)}{hash(self._meta)}" if (link
or meta)
else ""
200 """Create an 'null' style, equivalent to Style(), but more performant."""
205 cls, color: Optional[Color] =
None, bgcolor: Optional[Color] =
None
207 """Create a new style with colors and no attributes.
210 color (Optional[Color]): A (foreground) color, or None for no color. Defaults to None.
211 bgcolor (Optional[Color]): A (background) color, or None for no color. Defaults to None.
213 style: Style = cls.__new__(Style)
228 def from_meta(cls, meta: Optional[Dict[str, Any]]) ->
"Style":
229 """Create a new style with meta data.
232 meta (Optional[Dict[str, Any]]): A dictionary of meta data. Defaults to None.
234 style: Style = cls.__new__(Style)
249 def on(cls, meta: Optional[Dict[str, Any]] =
None, **handlers: Any) ->
"Style":
250 """Create a blank style with meta information.
253 style = Style.on(click=self.on_click)
256 meta (Optional[Dict[str, Any]], optional): An optional dict of meta information.
257 **handlers (Any): Keyword arguments are translated in to handlers.
260 Style: A Style with meta information attached.
262 meta = {}
if meta
is None else meta
282 """Get a link id, used in ansi code for links."""
286 """Re-generate style definition from attributes."""
288 attributes: List[str] = []
291 if bits & 0b0000000001111:
293 append(
"bold" if self.
bold else "not bold")
295 append(
"dim" if self.
dim else "not dim")
297 append(
"italic" if self.
italic else "not italic")
299 append(
"underline" if self.
underline else "not underline")
300 if bits & 0b0000111110000:
302 append(
"blink" if self.
blink else "not blink")
304 append(
"blink2" if self.
blink2 else "not blink2")
306 append(
"reverse" if self.
reverse else "not reverse")
308 append(
"conceal" if self.
conceal else "not conceal")
310 append(
"strike" if self.
strike else "not strike")
311 if bits & 0b1111000000000:
313 append(
"underline2" if self.
underline2 else "not underline2")
315 append(
"frame" if self.
frame else "not frame")
317 append(
"encircle" if self.
encircle else "not encircle")
319 append(
"overline" if self.
overline else "not overline")
332 """A Style is false if it has no attributes, colors, or links."""
336 """Generate ANSI codes for this style.
339 color_system (ColorSystem): Color system.
342 str: String containing codes.
345 if self.
_ansi is None:
352 append(_style_map[0])
354 append(_style_map[1])
356 append(_style_map[2])
358 append(_style_map[3])
359 if attributes & 0b0000111110000:
360 for bit
in range(4, 9):
361 if attributes & (1 << bit):
362 append(_style_map[bit])
363 if attributes & 0b1111000000000:
364 for bit
in range(9, 13):
365 if attributes & (1 << bit):
366 append(_style_map[bit])
379 @lru_cache(maxsize=1024)
381 """Normalize a style definition so that styles with the same effect have the same string
385 style (str): A style definition.
388 str: Normal form of style definition.
391 return str(cls.
parse(style))
396 def pick_first(cls, *values: Optional[StyleType]) -> StyleType:
397 """Pick first non-None style."""
399 if value
is not None:
401 raise ValueError(
"expected at least one non-None style")
406 yield "bold", self.
bold,
None,
407 yield "dim", self.
dim,
None,
408 yield "italic", self.
italic,
None
410 yield "blink", self.
blink,
None
411 yield "blink2", self.
blink2,
None
412 yield "reverse", self.
reverse,
None
413 yield "conceal", self.
conceal,
None
414 yield "strike", self.
strike,
None
416 yield "frame", self.
frame,
None
417 yield "encircle", self.
encircle,
None
420 yield "meta", self.
meta
424 return NotImplemented
429 return NotImplemented
449 """The foreground color or None if it is not set."""
454 """The background color or None if it is not set."""
458 def link(self) -> Optional[str]:
459 """Link text, if set."""
464 """Check if the style specified a transparent background."""
469 """A Style with background only."""
473 def meta(self) -> Dict[str, Any]:
474 """Get meta information (can not be changed after construction)."""
479 """Get a copy of the style with color removed."""
482 style: Style = self.__new__(Style)
497 @lru_cache(maxsize=4096)
498 def parse(cls, style_definition: str) ->
"Style":
499 """Parse a style definition.
502 style_definition (str): A string containing a style.
505 errors.StyleSyntaxError: If the style definition syntax is invalid.
508 `Style`: A Style instance.
514 color: Optional[str] =
None
515 bgcolor: Optional[str] =
None
516 attributes: Dict[str, Optional[Any]] = {}
517 link: Optional[str] =
None
520 for original_word
in words:
523 word = next(words,
"")
528 except ColorParseError
as error:
530 f
"unable to parse {word!r} as background color; {error}"
535 word = next(words,
"")
537 if attribute
is None:
539 f
"expected style attribute after 'not', found {word!r}"
541 attributes[attribute] =
False
544 word = next(words,
"")
549 elif word
in STYLE_ATTRIBUTES:
550 attributes[STYLE_ATTRIBUTES[word]] =
True
555 except ColorParseError
as error:
557 f
"unable to parse {word!r} as color; {error}"
560 style =
Style(color=color, bgcolor=bgcolor, link=link, **attributes)
563 @lru_cache(maxsize=1024)
565 """Get a CSS style rule."""
566 theme = theme
or DEFAULT_TERMINAL_THEME
573 color, bgcolor = bgcolor, color
581 if color
is not None:
583 append(f
"color: {theme_color.hex}")
584 append(f
"text-decoration-color: {theme_color.hex}")
585 if bgcolor
is not None:
587 append(f
"background-color: {theme_color.hex}")
589 append(
"font-weight: bold")
591 append(
"font-style: italic")
593 append(
"text-decoration: underline")
595 append(
"text-decoration: line-through")
597 append(
"text-decoration: overline")
598 return "; ".join(css)
601 def combine(cls, styles: Iterable[
"Style"]) ->
"Style":
602 """Combine styles and get result.
605 styles (Iterable[Style]): Styles to combine.
608 Style: A new style instance.
610 iter_styles = iter(styles)
611 return sum(iter_styles, next(iter_styles))
614 def chain(cls, *styles:
"Style") ->
"Style":
615 """Combine styles from positional argument in to a single style.
618 *styles (Iterable[Style]): Styles to combine.
621 Style: A new style instance.
623 iter_styles = iter(styles)
624 return sum(iter_styles, next(iter_styles))
626 def copy(self) -> "Style":
627 """Get a copy of this style.
630 Style: A new Style instance with identical attributes.
634 style: Style = self.__new__(Style)
648 @lru_cache(maxsize=128)
650 """Get a copy of this style with link and meta information removed.
653 Style: New style object.
657 style: Style = self.__new__(Style)
672 """Get a copy with a different value for link.
675 link (str, optional): New value for link. Defaults to None.
678 Style: A new Style instance.
680 style: Style = self.__new__(Style)
699 legacy_windows: bool =
False,
701 """Render the ANSI codes for the style.
704 text (str, optional): A string to style. Defaults to "".
705 color_system (Optional[ColorSystem], optional): Color system to render to. Defaults to ColorSystem.TRUECOLOR.
708 str: A string containing ANSI style codes.
710 if not text
or color_system
is None:
713 rendered = f
"\x1b[{attrs}m{text}\x1b[0m" if attrs
else text
714 if self.
_link and not legacy_windows:
716 f
"\x1b]8;id={self._link_id};{self._link}\x1b\\{rendered}\x1b]8;;\x1b\\"
720 def test(self, text: Optional[str] =
None) ->
None:
721 """Write text with style directly to terminal.
723 This method is for testing purposes only.
726 text (Optional[str], optional): Text to style or None for style name.
729 text = text
or str(self)
732 @lru_cache(maxsize=1024)
733 def _add(self, style: Optional[
"Style"]) ->
"Style":
738 new_style: Style = self.__new__(Style)
757 def __add__(self, style: Optional[
"Style"]) ->
"Style":
758 combined_style = self.
_add(style)
766 """A stack of styles."""
768 __slots__ = [
"_stack"]
770 def __init__(self, default_style:
"Style") ->
None:
771 self._stack: List[Style] = [default_style]
774 return f
"<stylestack {self._stack!r}>"
778 """Get the Style at the top of the stack."""
779 return self._stack[-1]
781 def push(self, style: Style) ->
None:
782 """Push a new style on to the stack.
785 style (Style): New style to combine with current style.
787 self._stack.append(self._stack[-1] + style)
790 """Pop last style and discard.
793 Style: New current style (also available as stack.current)
796 return self._stack[-1]
None push(self, Style style)
None __init__(self, "Style" default_style)
Optional[Color] bgcolor(self)
str get_html_style(self, Optional[TerminalTheme] theme=None)
"Style" from_meta(cls, Optional[Dict[str, Any]] meta)
Dict[str, Any] meta(self)
Optional[Color] color(self)
"Style" combine(cls, Iterable["Style"] styles)
"Style" _add(self, Optional["Style"] style)
"Style" background_style(self)
"Style" on(cls, Optional[Dict[str, Any]] meta=None, **Any handlers)
"Style" __add__(self, Optional["Style"] style)
"Style" clear_meta_and_links(self)
bool __ne__(self, Any other)
__init__(self, *Optional[Union[Color, str]] color=None, Optional[Union[Color, str]] bgcolor=None, Optional[bool] bold=None, Optional[bool] dim=None, Optional[bool] italic=None, Optional[bool] underline=None, Optional[bool] blink=None, Optional[bool] blink2=None, Optional[bool] reverse=None, Optional[bool] conceal=None, Optional[bool] strike=None, Optional[bool] underline2=None, Optional[bool] frame=None, Optional[bool] encircle=None, Optional[bool] overline=None, Optional[str] link=None, Optional[Dict[str, Any]] meta=None)
bool transparent_background(self)
"Style" parse(cls, str style_definition)
str _make_ansi_codes(self, ColorSystem color_system)
"Style" update_link(self, Optional[str] link=None)
None test(self, Optional[str] text=None)
Result __rich_repr__(self)
"Style" from_color(cls, Optional[Color] color=None, Optional[Color] bgcolor=None)
"Style" without_color(self)
bool __eq__(self, Any other)
StyleType pick_first(cls, *Optional[StyleType] values)
str normalize(cls, str style)
Optional[bool] __get__(self, "Style" obj, Type["Style"] objtype)
None __init__(self, int bit_no)