Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
style.py797 linesDownload Raw Back to rich
1import sys2from functools import lru_cache3from marshal import dumps, loads4from random import randint5from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast6 7from . import errors8from .color import Color, ColorParseError, ColorSystem, blend_rgb9from .repr import Result, rich_repr10from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme11 12# Style instances and style definitions are often interchangeable13StyleType = Union[str, "Style"]14 15 16class _Bit:17    """A descriptor to get/set a style attribute bit."""18 19    __slots__ = ["bit"]20 21    def __init__(self, bit_no: int) -> None:22        self.bit = 1 << bit_no23 24    def __get__(self, obj: "Style", objtype: Type["Style"]) -> Optional[bool]:25        if obj._set_attributes & self.bit:26            return obj._attributes & self.bit != 027        return None28 29 30@rich_repr31class Style:32    """A terminal style.33 34    A terminal style consists of a color (`color`), a background color (`bgcolor`), and a number of attributes, such35    as bold, italic etc. The attributes have 3 states: they can either be on36    (``True``), off (``False``), or not set (``None``).37 38    Args: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.55 56    """57 58    _color: Optional[Color]59    _bgcolor: Optional[Color]60    _attributes: int61    _set_attributes: int62    _hash: Optional[int]63    _null: bool64    _meta: Optional[bytes]65 66    __slots__ = [67        "_color",68        "_bgcolor",69        "_attributes",70        "_set_attributes",71        "_link",72        "_link_id",73        "_ansi",74        "_style_definition",75        "_hash",76        "_null",77        "_meta",78    ]79 80    # maps bits on to SGR parameter81    _style_map = {82        0: "1",83        1: "2",84        2: "3",85        3: "4",86        4: "5",87        5: "6",88        6: "7",89        7: "8",90        8: "9",91        9: "21",92        10: "51",93        11: "52",94        12: "53",95    }96 97    STYLE_ATTRIBUTES = {98        "dim": "dim",99        "d": "dim",100        "bold": "bold",101        "b": "bold",102        "italic": "italic",103        "i": "italic",104        "underline": "underline",105        "u": "underline",106        "blink": "blink",107        "blink2": "blink2",108        "reverse": "reverse",109        "r": "reverse",110        "conceal": "conceal",111        "c": "conceal",112        "strike": "strike",113        "s": "strike",114        "underline2": "underline2",115        "uu": "underline2",116        "frame": "frame",117        "encircle": "encircle",118        "overline": "overline",119        "o": "overline",120    }121 122    def __init__(123        self,124        *,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,142    ):143        self._ansi: Optional[str] = None144        self._style_definition: Optional[str] = None145 146        def _make_color(color: Union[Color, str]) -> Color:147            return color if isinstance(color, Color) else Color.parse(color)148 149        self._color = None if color is None else _make_color(color)150        self._bgcolor = None if bgcolor is None else _make_color(bgcolor)151        self._set_attributes = sum(152            (153                bold is not None,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,166            )167        )168        self._attributes = (169            sum(170                (171                    bold and 1 or 0,172                    dim and 2 or 0,173                    italic and 4 or 0,174                    underline and 8 or 0,175                    blink and 16 or 0,176                    blink2 and 32 or 0,177                    reverse and 64 or 0,178                    conceal and 128 or 0,179                    strike and 256 or 0,180                    underline2 and 512 or 0,181                    frame and 1024 or 0,182                    encircle and 2048 or 0,183                    overline and 4096 or 0,184                )185            )186            if self._set_attributes187            else 0188        )189 190        self._link = link191        self._meta = None if meta is None else dumps(meta)192        self._link_id = (193            f"{randint(0, 999999)}{hash(self._meta)}" if (link or meta) else ""194        )195        self._hash: Optional[int] = None196        self._null = not (self._set_attributes or color or bgcolor or link or meta)197 198    @classmethod199    def null(cls) -> "Style":200        """Create an 'null' style, equivalent to Style(), but more performant."""201        return NULL_STYLE202 203    @classmethod204    def from_color(205        cls, color: Optional[Color] = None, bgcolor: Optional[Color] = None206    ) -> "Style":207        """Create a new style with colors and no attributes.208 209        Returns: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.212        """213        style: Style = cls.__new__(Style)214        style._ansi = None215        style._style_definition = None216        style._color = color217        style._bgcolor = bgcolor218        style._set_attributes = 0219        style._attributes = 0220        style._link = None221        style._link_id = ""222        style._meta = None223        style._null = not (color or bgcolor)224        style._hash = None225        return style226 227    @classmethod228    def from_meta(cls, meta: Optional[Dict[str, Any]]) -> "Style":229        """Create a new style with meta data.230 231        Returns:232            meta (Optional[Dict[str, Any]]): A dictionary of meta data. Defaults to None.233        """234        style: Style = cls.__new__(Style)235        style._ansi = None236        style._style_definition = None237        style._color = None238        style._bgcolor = None239        style._set_attributes = 0240        style._attributes = 0241        style._link = None242        style._meta = dumps(meta)243        style._link_id = f"{randint(0, 999999)}{hash(style._meta)}"244        style._hash = None245        style._null = not (meta)246        return style247 248    @classmethod249    def on(cls, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Style":250        """Create a blank style with meta information.251 252        Example:253            style = Style.on(click=self.on_click)254 255        Args:256            meta (Optional[Dict[str, Any]], optional): An optional dict of meta information.257            **handlers (Any): Keyword arguments are translated in to handlers.258 259        Returns:260            Style: A Style with meta information attached.261        """262        meta = {} if meta is None else meta263        meta.update({f"@{key}": value for key, value in handlers.items()})264        return cls.from_meta(meta)265 266    bold = _Bit(0)267    dim = _Bit(1)268    italic = _Bit(2)269    underline = _Bit(3)270    blink = _Bit(4)271    blink2 = _Bit(5)272    reverse = _Bit(6)273    conceal = _Bit(7)274    strike = _Bit(8)275    underline2 = _Bit(9)276    frame = _Bit(10)277    encircle = _Bit(11)278    overline = _Bit(12)279 280    @property281    def link_id(self) -> str:282        """Get a link id, used in ansi code for links."""283        return self._link_id284 285    def __str__(self) -> str:286        """Re-generate style definition from attributes."""287        if self._style_definition is None:288            attributes: List[str] = []289            append = attributes.append290            bits = self._set_attributes291            if bits & 0b0000000001111:292                if bits & 1:293                    append("bold" if self.bold else "not bold")294                if bits & (1 << 1):295                    append("dim" if self.dim else "not dim")296                if bits & (1 << 2):297                    append("italic" if self.italic else "not italic")298                if bits & (1 << 3):299                    append("underline" if self.underline else "not underline")300            if bits & 0b0000111110000:301                if bits & (1 << 4):302                    append("blink" if self.blink else "not blink")303                if bits & (1 << 5):304                    append("blink2" if self.blink2 else "not blink2")305                if bits & (1 << 6):306                    append("reverse" if self.reverse else "not reverse")307                if bits & (1 << 7):308                    append("conceal" if self.conceal else "not conceal")309                if bits & (1 << 8):310                    append("strike" if self.strike else "not strike")311            if bits & 0b1111000000000:312                if bits & (1 << 9):313                    append("underline2" if self.underline2 else "not underline2")314                if bits & (1 << 10):315                    append("frame" if self.frame else "not frame")316                if bits & (1 << 11):317                    append("encircle" if self.encircle else "not encircle")318                if bits & (1 << 12):319                    append("overline" if self.overline else "not overline")320            if self._color is not None:321                append(self._color.name)322            if self._bgcolor is not None:323                append("on")324                append(self._bgcolor.name)325            if self._link:326                append("link")327                append(self._link)328            self._style_definition = " ".join(attributes) or "none"329        return self._style_definition330 331    def __bool__(self) -> bool:332        """A Style is false if it has no attributes, colors, or links."""333        return not self._null334 335    def _make_ansi_codes(self, color_system: ColorSystem) -> str:336        """Generate ANSI codes for this style.337 338        Args:339            color_system (ColorSystem): Color system.340 341        Returns:342            str: String containing codes.343        """344 345        if self._ansi is None:346            sgr: List[str] = []347            append = sgr.append348            _style_map = self._style_map349            attributes = self._attributes & self._set_attributes350            if attributes:351                if attributes & 1:352                    append(_style_map[0])353                if attributes & 2:354                    append(_style_map[1])355                if attributes & 4:356                    append(_style_map[2])357                if attributes & 8: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])367            if self._color is not None:368                sgr.extend(self._color.downgrade(color_system).get_ansi_codes())369            if self._bgcolor is not None:370                sgr.extend(371                    self._bgcolor.downgrade(color_system).get_ansi_codes(372                        foreground=False373                    )374                )375            self._ansi = ";".join(sgr)376        return self._ansi377 378    @classmethod379    @lru_cache(maxsize=1024)380    def normalize(cls, style: str) -> str:381        """Normalize a style definition so that styles with the same effect have the same string382        representation.383 384        Args:385            style (str): A style definition.386 387        Returns:388            str: Normal form of style definition.389        """390        try:391            return str(cls.parse(style))392        except errors.StyleSyntaxError:393            return style.strip().lower()394 395    @classmethod396    def pick_first(cls, *values: Optional[StyleType]) -> StyleType:397        """Pick first non-None style."""398        for value in values:399            if value is not None:400                return value401        raise ValueError("expected at least one non-None style")402 403    def __rich_repr__(self) -> Result:404        yield "color", self.color, None405        yield "bgcolor", self.bgcolor, None406        yield "bold", self.bold, None,407        yield "dim", self.dim, None,408        yield "italic", self.italic, None409        yield "underline", self.underline, None,410        yield "blink", self.blink, None411        yield "blink2", self.blink2, None412        yield "reverse", self.reverse, None413        yield "conceal", self.conceal, None414        yield "strike", self.strike, None415        yield "underline2", self.underline2, None416        yield "frame", self.frame, None417        yield "encircle", self.encircle, None418        yield "link", self.link, None419        if self._meta:420            yield "meta", self.meta421 422    def __eq__(self, other: Any) -> bool:423        if not isinstance(other, Style):424            return NotImplemented425        return self.__hash__() == other.__hash__()426 427    def __ne__(self, other: Any) -> bool:428        if not isinstance(other, Style):429            return NotImplemented430        return self.__hash__() != other.__hash__()431 432    def __hash__(self) -> int:433        if self._hash is not None:434            return self._hash435        self._hash = hash(436            (437                self._color,438                self._bgcolor,439                self._attributes,440                self._set_attributes,441                self._link,442                self._meta,443            )444        )445        return self._hash446 447    @property448    def color(self) -> Optional[Color]:449        """The foreground color or None if it is not set."""450        return self._color451 452    @property453    def bgcolor(self) -> Optional[Color]:454        """The background color or None if it is not set."""455        return self._bgcolor456 457    @property458    def link(self) -> Optional[str]:459        """Link text, if set."""460        return self._link461 462    @property463    def transparent_background(self) -> bool:464        """Check if the style specified a transparent background."""465        return self.bgcolor is None or self.bgcolor.is_default466 467    @property468    def background_style(self) -> "Style":469        """A Style with background only."""470        return Style(bgcolor=self.bgcolor)471 472    @property473    def meta(self) -> Dict[str, Any]:474        """Get meta information (can not be changed after construction)."""475        return {} if self._meta is None else cast(Dict[str, Any], loads(self._meta))476 477    @property478    def without_color(self) -> "Style":479        """Get a copy of the style with color removed."""480        if self._null:481            return NULL_STYLE482        style: Style = self.__new__(Style)483        style._ansi = None484        style._style_definition = None485        style._color = None486        style._bgcolor = None487        style._attributes = self._attributes488        style._set_attributes = self._set_attributes489        style._link = self._link490        style._link_id = f"{randint(0, 999999)}" if self._link else ""491        style._null = False492        style._meta = None493        style._hash = None494        return style495 496    @classmethod497    @lru_cache(maxsize=4096)498    def parse(cls, style_definition: str) -> "Style":499        """Parse a style definition.500 501        Args:502            style_definition (str): A string containing a style.503 504        Raises:505            errors.StyleSyntaxError: If the style definition syntax is invalid.506 507        Returns:508            `Style`: A Style instance.509        """510        if style_definition.strip() == "none" or not style_definition:511            return cls.null()512 513        STYLE_ATTRIBUTES = cls.STYLE_ATTRIBUTES514        color: Optional[str] = None515        bgcolor: Optional[str] = None516        attributes: Dict[str, Optional[Any]] = {}517        link: Optional[str] = None518 519        words = iter(style_definition.split())520        for original_word in words:521            word = original_word.lower()522            if word == "on":523                word = next(words, "")524                if not word:525                    raise errors.StyleSyntaxError("color expected after 'on'")526                try:527                    Color.parse(word) is None528                except ColorParseError as error:529                    raise errors.StyleSyntaxError(530                        f"unable to parse {word!r} as background color; {error}"531                    ) from None532                bgcolor = word533 534            elif word == "not":535                word = next(words, "")536                attribute = STYLE_ATTRIBUTES.get(word)537                if attribute is None:538                    raise errors.StyleSyntaxError(539                        f"expected style attribute after 'not', found {word!r}"540                    )541                attributes[attribute] = False542 543            elif word == "link":544                word = next(words, "")545                if not word:546                    raise errors.StyleSyntaxError("URL expected after 'link'")547                link = word548 549            elif word in STYLE_ATTRIBUTES:550                attributes[STYLE_ATTRIBUTES[word]] = True551 552            else:553                try:554                    Color.parse(word)555                except ColorParseError as error:556                    raise errors.StyleSyntaxError(557                        f"unable to parse {word!r} as color; {error}"558                    ) from None559                color = word560        style = Style(color=color, bgcolor=bgcolor, link=link, **attributes)561        return style562 563    @lru_cache(maxsize=1024)564    def get_html_style(self, theme: Optional[TerminalTheme] = None) -> str:565        """Get a CSS style rule."""566        theme = theme or DEFAULT_TERMINAL_THEME567        css: List[str] = []568        append = css.append569 570        color = self.color571        bgcolor = self.bgcolor572        if self.reverse:573            color, bgcolor = bgcolor, color574        if self.dim:575            foreground_color = (576                theme.foreground_color if color is None else color.get_truecolor(theme)577            )578            color = Color.from_triplet(579                blend_rgb(foreground_color, theme.background_color, 0.5)580            )581        if color is not None:582            theme_color = color.get_truecolor(theme)583            append(f"color: {theme_color.hex}")584            append(f"text-decoration-color: {theme_color.hex}")585        if bgcolor is not None:586            theme_color = bgcolor.get_truecolor(theme, foreground=False)587            append(f"background-color: {theme_color.hex}")588        if self.bold:589            append("font-weight: bold")590        if self.italic:591            append("font-style: italic")592        if self.underline:593            append("text-decoration: underline")594        if self.strike:595            append("text-decoration: line-through")596        if self.overline:597            append("text-decoration: overline")598        return "; ".join(css)599 600    @classmethod601    def combine(cls, styles: Iterable["Style"]) -> "Style":602        """Combine styles and get result.603 604        Args:605            styles (Iterable[Style]): Styles to combine.606 607        Returns:608            Style: A new style instance.609        """610        iter_styles = iter(styles)611        return sum(iter_styles, next(iter_styles))612 613    @classmethod614    def chain(cls, *styles: "Style") -> "Style":615        """Combine styles from positional argument in to a single style.616 617        Args:618            *styles (Iterable[Style]): Styles to combine.619 620        Returns:621            Style: A new style instance.622        """623        iter_styles = iter(styles)624        return sum(iter_styles, next(iter_styles))625 626    def copy(self) -> "Style":627        """Get a copy of this style.628 629        Returns:630            Style: A new Style instance with identical attributes.631        """632        if self._null:633            return NULL_STYLE634        style: Style = self.__new__(Style)635        style._ansi = self._ansi636        style._style_definition = self._style_definition637        style._color = self._color638        style._bgcolor = self._bgcolor639        style._attributes = self._attributes640        style._set_attributes = self._set_attributes641        style._link = self._link642        style._link_id = f"{randint(0, 999999)}" if self._link else ""643        style._hash = self._hash644        style._null = False645        style._meta = self._meta646        return style647 648    @lru_cache(maxsize=128)649    def clear_meta_and_links(self) -> "Style":650        """Get a copy of this style with link and meta information removed.651 652        Returns:653            Style: New style object.654        """655        if self._null:656            return NULL_STYLE657        style: Style = self.__new__(Style)658        style._ansi = self._ansi659        style._style_definition = self._style_definition660        style._color = self._color661        style._bgcolor = self._bgcolor662        style._attributes = self._attributes663        style._set_attributes = self._set_attributes664        style._link = None665        style._link_id = ""666        style._hash = self._hash667        style._null = False668        style._meta = None669        return style670 671    def update_link(self, link: Optional[str] = None) -> "Style":672        """Get a copy with a different value for link.673 674        Args:675            link (str, optional): New value for link. Defaults to None.676 677        Returns:678            Style: A new Style instance.679        """680        style: Style = self.__new__(Style)681        style._ansi = self._ansi682        style._style_definition = self._style_definition683        style._color = self._color684        style._bgcolor = self._bgcolor685        style._attributes = self._attributes686        style._set_attributes = self._set_attributes687        style._link = link688        style._link_id = f"{randint(0, 999999)}" if link else ""689        style._hash = None690        style._null = False691        style._meta = self._meta692        return style693 694    def render(695        self,696        text: str = "",697        *,698        color_system: Optional[ColorSystem] = ColorSystem.TRUECOLOR,699        legacy_windows: bool = False,700    ) -> str:701        """Render the ANSI codes for the style.702 703        Args: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.706 707        Returns:708            str: A string containing ANSI style codes.709        """710        if not text or color_system is None:711            return text712        attrs = self._ansi or self._make_ansi_codes(color_system)713        rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text714        if self._link and not legacy_windows:715            rendered = (716                f"\x1b]8;id={self._link_id};{self._link}\x1b\\{rendered}\x1b]8;;\x1b\\"717            )718        return rendered719 720    def test(self, text: Optional[str] = None) -> None:721        """Write text with style directly to terminal.722 723        This method is for testing purposes only.724 725        Args:726            text (Optional[str], optional): Text to style or None for style name.727 728        """729        text = text or str(self)730        sys.stdout.write(f"{self.render(text)}\n")731 732    @lru_cache(maxsize=1024)733    def _add(self, style: Optional["Style"]) -> "Style":734        if style is None or style._null:735            return self736        if self._null:737            return style738        new_style: Style = self.__new__(Style)739        new_style._ansi = None740        new_style._style_definition = None741        new_style._color = style._color or self._color742        new_style._bgcolor = style._bgcolor or self._bgcolor743        new_style._attributes = (self._attributes & ~style._set_attributes) | (744            style._attributes & style._set_attributes745        )746        new_style._set_attributes = self._set_attributes | style._set_attributes747        new_style._link = style._link or self._link748        new_style._link_id = style._link_id or self._link_id749        new_style._null = style._null750        if self._meta and style._meta:751            new_style._meta = dumps({**self.meta, **style.meta})752        else:753            new_style._meta = self._meta or style._meta754        new_style._hash = None755        return new_style756 757    def __add__(self, style: Optional["Style"]) -> "Style":758        combined_style = self._add(style)759        return combined_style.copy() if combined_style.link else combined_style760 761 762NULL_STYLE = Style()763 764 765class StyleStack:766    """A stack of styles."""767 768    __slots__ = ["_stack"]769 770    def __init__(self, default_style: "Style") -> None:771        self._stack: List[Style] = [default_style]772 773    def __repr__(self) -> str:774        return f"<stylestack {self._stack!r}>"775 776    @property777    def current(self) -> Style:778        """Get the Style at the top of the stack."""779        return self._stack[-1]780 781    def push(self, style: Style) -> None:782        """Push a new style on to the stack.783 784        Args:785            style (Style): New style to combine with current style.786        """787        self._stack.append(self._stack[-1] + style)788 789    def pop(self) -> Style:790        """Pop last style and discard.791 792        Returns:793            Style: New current style (also available as stack.current)794        """795        self._stack.pop()796        return self._stack[-1]797 
codekingpro/portable-devtools · Team Ai