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 itertools import count4from operator import attrgetter5from pickle import dumps, loads6from random import getrandbits7from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast8 9from . import errors10from .color import Color, ColorParseError, ColorSystem, blend_rgb11from .repr import Result, rich_repr12from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme13 14_hash_getter = attrgetter(15    "_color", "_bgcolor", "_attributes", "_set_attributes", "_link", "_meta"16)17 18# Style instances and style definitions are often interchangeable19StyleType = Union[str, "Style"]20 21 22_id_generator = count(getrandbits(24))23 24 25class _Bit:26    """A descriptor to get/set a style attribute bit."""27 28    __slots__ = ["bit"]29 30    def __init__(self, bit_no: int) -> None:31        self.bit = 1 << bit_no32 33    def __get__(self, obj: "Style", objtype: Type["Style"]) -> Optional[bool]:34        if obj._set_attributes & self.bit:35            return obj._attributes & self.bit != 036        return None37 38 39@rich_repr40class Style:41    """A terminal style.42 43    A terminal style consists of a color (`color`), a background color (`bgcolor`), and a number of attributes, such44    as bold, italic etc. The attributes have 3 states: they can either be on45    (``True``), off (``False``), or not set (``None``).46 47    Args:48        color (Union[Color, str], optional): Color of terminal text. Defaults to None.49        bgcolor (Union[Color, str], optional): Color of terminal background. Defaults to None.50        bold (bool, optional): Enable bold text. Defaults to None.51        dim (bool, optional): Enable dim text. Defaults to None.52        italic (bool, optional): Enable italic text. Defaults to None.53        underline (bool, optional): Enable underlined text. Defaults to None.54        blink (bool, optional): Enabled blinking text. Defaults to None.55        blink2 (bool, optional): Enable fast blinking text. Defaults to None.56        reverse (bool, optional): Enabled reverse text. Defaults to None.57        conceal (bool, optional): Enable concealed text. Defaults to None.58        strike (bool, optional): Enable strikethrough text. Defaults to None.59        underline2 (bool, optional): Enable doubly underlined text. Defaults to None.60        frame (bool, optional): Enable framed text. Defaults to None.61        encircle (bool, optional): Enable encircled text. Defaults to None.62        overline (bool, optional): Enable overlined text. Defaults to None.63        link (str, link): Link URL. Defaults to None.64 65    """66 67    _color: Optional[Color]68    _bgcolor: Optional[Color]69    _attributes: int70    _set_attributes: int71    _hash: Optional[int]72    _null: bool73    _meta: Optional[bytes]74 75    __slots__ = [76        "_color",77        "_bgcolor",78        "_attributes",79        "_set_attributes",80        "_link",81        "_link_id",82        "_ansi",83        "_style_definition",84        "_hash",85        "_null",86        "_meta",87    ]88 89    # maps bits on to SGR parameter90    _style_map = {91        0: "1",92        1: "2",93        2: "3",94        3: "4",95        4: "5",96        5: "6",97        6: "7",98        7: "8",99        8: "9",100        9: "21",101        10: "51",102        11: "52",103        12: "53",104    }105 106    STYLE_ATTRIBUTES = {107        "dim": "dim",108        "d": "dim",109        "bold": "bold",110        "b": "bold",111        "italic": "italic",112        "i": "italic",113        "underline": "underline",114        "u": "underline",115        "blink": "blink",116        "blink2": "blink2",117        "reverse": "reverse",118        "r": "reverse",119        "conceal": "conceal",120        "c": "conceal",121        "strike": "strike",122        "s": "strike",123        "underline2": "underline2",124        "uu": "underline2",125        "frame": "frame",126        "encircle": "encircle",127        "overline": "overline",128        "o": "overline",129    }130 131    def __init__(132        self,133        *,134        color: Optional[Union[Color, str]] = None,135        bgcolor: Optional[Union[Color, str]] = None,136        bold: Optional[bool] = None,137        dim: Optional[bool] = None,138        italic: Optional[bool] = None,139        underline: Optional[bool] = None,140        blink: Optional[bool] = None,141        blink2: Optional[bool] = None,142        reverse: Optional[bool] = None,143        conceal: Optional[bool] = None,144        strike: Optional[bool] = None,145        underline2: Optional[bool] = None,146        frame: Optional[bool] = None,147        encircle: Optional[bool] = None,148        overline: Optional[bool] = None,149        link: Optional[str] = None,150        meta: Optional[Dict[str, Any]] = None,151    ):152        self._ansi: Optional[str] = None153        self._style_definition: Optional[str] = None154 155        def _make_color(color: Union[Color, str]) -> Color:156            return color if isinstance(color, Color) else Color.parse(color)157 158        self._color = None if color is None else _make_color(color)159        self._bgcolor = None if bgcolor is None else _make_color(bgcolor)160        self._set_attributes = sum(161            (162                bold is not None,163                dim is not None and 2,164                italic is not None and 4,165                underline is not None and 8,166                blink is not None and 16,167                blink2 is not None and 32,168                reverse is not None and 64,169                conceal is not None and 128,170                strike is not None and 256,171                underline2 is not None and 512,172                frame is not None and 1024,173                encircle is not None and 2048,174                overline is not None and 4096,175            )176        )177        self._attributes = (178            sum(179                (180                    bold and 1 or 0,181                    dim and 2 or 0,182                    italic and 4 or 0,183                    underline and 8 or 0,184                    blink and 16 or 0,185                    blink2 and 32 or 0,186                    reverse and 64 or 0,187                    conceal and 128 or 0,188                    strike and 256 or 0,189                    underline2 and 512 or 0,190                    frame and 1024 or 0,191                    encircle and 2048 or 0,192                    overline and 4096 or 0,193                )194            )195            if self._set_attributes196            else 0197        )198 199        self._link = link200        self._meta = None if meta is None else dumps(meta)201        self._link_id = (202            f"{next(_id_generator)}{hash(self._meta)}" if (link or meta) else ""203        )204        self._hash: Optional[int] = None205        self._null = not (self._set_attributes or color or bgcolor or link or meta)206 207    @classmethod208    def null(cls) -> "Style":209        """Create an 'null' style, equivalent to Style(), but more performant."""210        return NULL_STYLE211 212    @classmethod213    def from_color(214        cls, color: Optional[Color] = None, bgcolor: Optional[Color] = None215    ) -> "Style":216        """Create a new style with colors and no attributes.217 218        Returns:219            color (Optional[Color]): A (foreground) color, or None for no color. Defaults to None.220            bgcolor (Optional[Color]): A (background) color, or None for no color. Defaults to None.221        """222        style: Style = cls.__new__(Style)223        style._ansi = None224        style._style_definition = None225        style._color = color226        style._bgcolor = bgcolor227        style._set_attributes = 0228        style._attributes = 0229        style._link = None230        style._link_id = ""231        style._meta = None232        style._null = not (color or bgcolor)233        style._hash = None234        return style235 236    @classmethod237    def from_meta(cls, meta: Optional[Dict[str, Any]]) -> "Style":238        """Create a new style with meta data.239 240        Returns:241            meta (Optional[Dict[str, Any]]): A dictionary of meta data. Defaults to None.242        """243        style: Style = cls.__new__(Style)244        style._ansi = None245        style._style_definition = None246        style._color = None247        style._bgcolor = None248        style._set_attributes = 0249        style._attributes = 0250        style._link = None251        style._meta = dumps(meta)252        style._link_id = f"{next(_id_generator)}{hash(style._meta)}"253        style._hash = None254        style._null = not (meta)255        return style256 257    @classmethod258    def on(cls, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Style":259        """Create a blank style with meta information.260 261        Example:262            style = Style.on(click=self.on_click)263 264        Args:265            meta (Optional[Dict[str, Any]], optional): An optional dict of meta information.266            **handlers (Any): Keyword arguments are translated in to handlers.267 268        Returns:269            Style: A Style with meta information attached.270        """271        meta = {} if meta is None else meta272        meta.update({f"@{key}": value for key, value in handlers.items()})273        return cls.from_meta(meta)274 275    bold = _Bit(0)276    dim = _Bit(1)277    italic = _Bit(2)278    underline = _Bit(3)279    blink = _Bit(4)280    blink2 = _Bit(5)281    reverse = _Bit(6)282    conceal = _Bit(7)283    strike = _Bit(8)284    underline2 = _Bit(9)285    frame = _Bit(10)286    encircle = _Bit(11)287    overline = _Bit(12)288 289    @property290    def link_id(self) -> str:291        """Get a link id, used in ansi code for links."""292        return self._link_id293 294    def __str__(self) -> str:295        """Re-generate style definition from attributes."""296        if self._style_definition is None:297            attributes: List[str] = []298            append = attributes.append299            bits = self._set_attributes300            if bits & 0b0000000001111:301                if bits & 1:302                    append("bold" if self.bold else "not bold")303                if bits & (1 << 1):304                    append("dim" if self.dim else "not dim")305                if bits & (1 << 2):306                    append("italic" if self.italic else "not italic")307                if bits & (1 << 3):308                    append("underline" if self.underline else "not underline")309            if bits & 0b0000111110000:310                if bits & (1 << 4):311                    append("blink" if self.blink else "not blink")312                if bits & (1 << 5):313                    append("blink2" if self.blink2 else "not blink2")314                if bits & (1 << 6):315                    append("reverse" if self.reverse else "not reverse")316                if bits & (1 << 7):317                    append("conceal" if self.conceal else "not conceal")318                if bits & (1 << 8):319                    append("strike" if self.strike else "not strike")320            if bits & 0b1111000000000:321                if bits & (1 << 9):322                    append("underline2" if self.underline2 else "not underline2")323                if bits & (1 << 10):324                    append("frame" if self.frame else "not frame")325                if bits & (1 << 11):326                    append("encircle" if self.encircle else "not encircle")327                if bits & (1 << 12):328                    append("overline" if self.overline else "not overline")329            if self._color is not None:330                append(self._color.name)331            if self._bgcolor is not None:332                append("on")333                append(self._bgcolor.name)334            if self._link:335                append("link")336                append(self._link)337            self._style_definition = " ".join(attributes) or "none"338        return self._style_definition339 340    def __bool__(self) -> bool:341        """A Style is false if it has no attributes, colors, or links."""342        return not self._null343 344    def _make_ansi_codes(self, color_system: ColorSystem) -> str:345        """Generate ANSI codes for this style.346 347        Args:348            color_system (ColorSystem): Color system.349 350        Returns:351            str: String containing codes.352        """353 354        if self._ansi is None:355            sgr: List[str] = []356            append = sgr.append357            _style_map = self._style_map358            attributes = self._attributes & self._set_attributes359            if attributes:360                if attributes & 1:361                    append(_style_map[0])362                if attributes & 2:363                    append(_style_map[1])364                if attributes & 4:365                    append(_style_map[2])366                if attributes & 8:367                    append(_style_map[3])368                if attributes & 0b0000111110000:369                    for bit in range(4, 9):370                        if attributes & (1 << bit):371                            append(_style_map[bit])372                if attributes & 0b1111000000000:373                    for bit in range(9, 13):374                        if attributes & (1 << bit):375                            append(_style_map[bit])376            if self._color is not None:377                sgr.extend(self._color.downgrade(color_system).get_ansi_codes())378            if self._bgcolor is not None:379                sgr.extend(380                    self._bgcolor.downgrade(color_system).get_ansi_codes(381                        foreground=False382                    )383                )384            self._ansi = ";".join(sgr)385        return self._ansi386 387    @classmethod388    @lru_cache(maxsize=1024)389    def normalize(cls, style: str) -> str:390        """Normalize a style definition so that styles with the same effect have the same string391        representation.392 393        Args:394            style (str): A style definition.395 396        Returns:397            str: Normal form of style definition.398        """399        try:400            return str(cls.parse(style))401        except errors.StyleSyntaxError:402            return style.strip().lower()403 404    @classmethod405    def pick_first(cls, *values: Optional[StyleType]) -> StyleType:406        """Pick first non-None style."""407        for value in values:408            if value is not None:409                return value410        raise ValueError("expected at least one non-None style")411 412    def __rich_repr__(self) -> Result:413        yield "color", self.color, None414        yield "bgcolor", self.bgcolor, None415        yield "bold", self.bold, None,416        yield "dim", self.dim, None,417        yield "italic", self.italic, None418        yield "underline", self.underline, None,419        yield "blink", self.blink, None420        yield "blink2", self.blink2, None421        yield "reverse", self.reverse, None422        yield "conceal", self.conceal, None423        yield "strike", self.strike, None424        yield "underline2", self.underline2, None425        yield "frame", self.frame, None426        yield "encircle", self.encircle, None427        yield "link", self.link, None428        if self._meta:429            yield "meta", self.meta430 431    def __eq__(self, other: Any) -> bool:432        if not isinstance(other, Style):433            return NotImplemented434        return self.__hash__() == other.__hash__()435 436    def __ne__(self, other: Any) -> bool:437        if not isinstance(other, Style):438            return NotImplemented439        return self.__hash__() != other.__hash__()440 441    def __hash__(self) -> int:442        if self._hash is not None:443            return self._hash444        self._hash = hash(_hash_getter(self))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"{next(_id_generator)}" 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)528                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"{next(_id_generator)}" 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 = None667        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"{next(_id_generator)}" 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