Source code for sty.primitive

"""
The Register class: Sty's heart.
"""
from collections import namedtuple
from copy import deepcopy
from typing import Callable, Dict, Iterable, List, NamedTuple, Tuple, Type, Union

from sty.rendertype import RenderType

Renderfuncs = Dict[Type[RenderType], Callable]

StylingRule = Union["Style", RenderType]


class Style(str):
    """
    This type stores the different styling rules for the registers and the resulting
    ANSI-sequence as a string.

    For example:

        fg.orange = Style(RgbFg(1,5,10), Sgr(1))

        isinstance(fg.orange, Style) # True

        isinstance(fg.orange, str) # True

        str(fg.orange) # '\x1b[38;2;1;5;10m\x1b[1m' (The ASNI sequence for orange and bold)
    """

    rules: Iterable[StylingRule]

    def __new__(cls, *rules: StylingRule, value: str = "") -> "Style":
        new_cls = str.__new__(cls, value)  # type: ignore
        setattr(new_cls, "rules", rules)
        return new_cls


def _render_rules(
    renderfuncs: Renderfuncs,
    rules: Iterable[StylingRule],
) -> Tuple[str, Iterable[StylingRule]]:
    rendered: str = ""
    flattened_rules: List[StylingRule] = []

    for rule in rules:
        if isinstance(rule, RenderType):
            f1: Callable = renderfuncs[type(rule)]
            rendered += f1(*rule.args)
            flattened_rules.append(rule)

        elif isinstance(rule, Style):
            r1, r2 = _render_rules(renderfuncs, rule.rules)
            rendered += r1
            flattened_rules.extend(r2)

        else:
            raise ValueError("Parameter 'rules' must be of type Iterable[Rule].")

    return rendered, flattened_rules


class Register:
    """
    This is the base Register class. All default registers (fg, bg, ef, rs) are
    created from this class. You can use it to create your own custom registers.
    """

    def __init__(self):
        self.renderfuncs: Renderfuncs = {}
        self.is_muted = False
        self.eightbit_call = lambda x: x
        self.rgb_call = lambda r, g, b: (r, g, b)

    def __setattr__(self, name: str, value: Style):
        if isinstance(value, Style):
            if self.is_muted:
                rendered_style = Style(*value.rules, value="")
            else:
                rendered, rules = _render_rules(self.renderfuncs, value.rules)
                rendered_style = Style(*rules, value=rendered)

            return super().__setattr__(name, rendered_style)
        else:
            # TODO: Why do we need this??? What should be set here?
            return super().__setattr__(name, value)

    def __call__(self, *args: Union[int, str], **kwargs) -> str:
        """
        This function is to handle calls such as `fg(42)`, `bg(102, 49, 42)`, `fg('red')`.
        """

        # Return empty str if object is muted.
        if self.is_muted:
            return ""

        len_args = len(args)

        if len_args == 1:
            # If input is an 8bit color code, run 8bit render function.
            if isinstance(args[0], int):
                return self.eightbit_call(*args, **kwargs)

            # If input is a string, return attribute with the name that matches
            # input.
            else:
                return getattr(self, args[0])

        # If input is an 24bit color code, run 24bit render function.
        elif len_args == 3:
            return self.rgb_call(*args, **kwargs)

        else:
            return ""

[docs] def set_eightbit_call(self, rendertype: Type[RenderType]) -> None: """ You can call a register-object directly. A call like this ``fg(144)`` is a Eightbit-call. With this method you can define the render-type for such calls. :param rendertype: The new rendertype that is used for Eightbit-calls. """ func: Callable = self.renderfuncs[rendertype] self.eightbit_call = func
[docs] def set_rgb_call(self, rendertype: Type[RenderType]) -> None: """ You can call a register-object directly. A call like this ``fg(10, 42, 255)`` is a RGB-call. With this method you can define the render-type for such calls. :param rendertype: The new rendertype that is used for RGB-calls. """ func: Callable = self.renderfuncs[rendertype] self.rgb_call = func
[docs] def set_renderfunc(self, rendertype: Type[RenderType], func: Callable) -> None: """ With this method you can add or replace render-functions for a given register-object: :param rendertype: The render type for which the new renderfunc is used. :param func: The new render function. """ # Save new render-func in register self.renderfuncs.update({rendertype: func}) # Update style atributes and styles with the new renderfunc. for attr_name in dir(self): val = getattr(self, attr_name) if isinstance(val, Style): setattr(self, attr_name, val)
[docs] def mute(self) -> None: """ Sometimes it is useful to disable the formatting for a register-object. You can do so by invoking this method. """ self.is_muted = True for attr_name in dir(self): val = getattr(self, attr_name) if isinstance(val, Style): setattr(self, attr_name, val)
[docs] def unmute(self) -> None: """ Use this method to unmute a previously muted register object. """ self.is_muted = False for attr_name in dir(self): val = getattr(self, attr_name) if isinstance(val, Style): setattr(self, attr_name, val)
[docs] def as_dict(self) -> Dict[str, str]: """ Export color register as dict. """ items: Dict[str, str] = {} for name in dir(self): if not name.startswith("_") and isinstance(getattr(self, name), str): items.update({name: str(getattr(self, name))}) return items
[docs] def as_namedtuple(self): """ Export color register as namedtuple. """ d = self.as_dict() return namedtuple("StyleRegister", d.keys())(*d.values())
[docs] def copy(self) -> "Register": """ Make a deepcopy of a register-object. """ return deepcopy(self)