Source code for cli2.theme

"""
Color themes.

The theme is available in the `cli2.t` namespace.

.. envar:: CLI2_THEME

    The default is "standard" but "monokai" and "flashy" are also available.
    Standard theme uses basic colors, and lets you override them with
    environment variables: CLI2_RED, CLI2_GREEN, and so on.

Example:

.. code-block:: python

    import cli2

    print(f'{cli2.theme.green}OK{cli2.theme.reset}')

    # with a mode, such as bold, dim, italic, underline and strike:
    print(f'{cli2.theme.green.bold}OK{cli2.theme.reset}')

    # all is also callable appending the reset automatically
    print(cli2.theme.green('OK'))
    print(cli2.theme.green.bold('OK'))

We also have shortcuts, ``cli2.theme`` is ``cli2.t``, each color can be
referred to by first letter in lowercase, except for black and gray which are
refered to by their first letter in uppercase. Modes can be referred to by
first letter in lowercase too, and reset is rs:

.. code-block:: python

    # shortcuts
    print(f'{cli2.t.g.b}OK{cli2.t.rs}')

    # shorter with callable
    print(cli2.t.g.b('OK'))

Run ``cli2-theme`` for the list of colors by theme.
"""
import re
import os

from .cli import Command

themes = dict(
    standard=dict(
        black=int(os.getenv('CLI2_BLACK', 0)),
        red=int(os.getenv('CLI2_RED', 1)),
        green=int(os.getenv('CLI2_GREEN', 2)),
        yellow=int(os.getenv('CLI2_YELLOW', 3)),
        orange=int(os.getenv('CLI2_ORANGE', 208)),
        blue=int(os.getenv('CLI2_BLUE', 4)),
        mauve=int(os.getenv('CLI2_MAUVE', 5)),
        pink=int(os.getenv('CLI2_PINK', 164)),
        cyan=int(os.getenv('CLI2_CYAN', 6)),
        gray=int(os.getenv('CLI2_GRAY', 7)),
    ),
    flashy=dict(
        black=0,
        red=196,
        green=46,
        yellow=227,
        blue=27,
        mauve=129,
        pink=201,
        orange=202,
        cyan=51,
        gray=253,
    ),
    monokai=dict(
        black=0,
        red=124,
        green=150,
        yellow=179,
        blue=67,
        mauve=140,
        pink=132,
        orange=202,
        cyan=80,
        gray=246,
    ),
)


class Renderer:
    def __call__(self, *content):
        return f'{self}{" ".join([str(c) for c in content])}{t.rs}'


class Mode(Renderer):
    def __init__(self, name, code):
        self.name = name
        self.code = code
        self.alias = name[0]

    def __str__(self):
        return f'\u001b[{self.code}m'


class ColorMode(Renderer):
    def __init__(self, color, mode):
        self.color = color
        self.mode = mode

    def __str__(self):
        return f'\u001b[{self.mode.code};38;5;{self.color.code}m'


modes = {
    name: Mode(name, code)
    for name, code in dict(
        bold=1,
        dim=2,
        italic=3,
        underline=4,
        strike=9,
    ).items()
}


class Color(Renderer):
    def __init__(self, code, name=None, alias=None):
        self.name = name
        self.alias = alias
        self.code = code

        for mode, code in modes.items():
            color_mode = ColorMode(self, code)
            setattr(self, mode, color_mode)
            setattr(self, mode[0], color_mode)

    def __str__(self):
        return f'\u001b[38;5;{self.code}m'


class Theme:
    def __init__(self, colors):
        self.colors = colors

        for name, mode in modes.items():
            setattr(self, mode.name, mode)

        for name, value in colors.items():
            if name in ('black', 'gray'):
                alias = name[0].upper()
            else:
                alias = name[0]

            color = Color(value, name, alias)
            setattr(self, name, color)
            setattr(self, alias, color)

        for name in ('reset', 'rs'):
            setattr(self, name, '\u001b[0m')

    @staticmethod
    def len(string):
        """
        Counts the number of alphabetic characters in a string,
        ignoring ANSI escape codes.

        :param string: Input string
        :return: Integer count of actual printable chars
        """
        # Regular expression to match ANSI escape codes
        ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')

        # Remove ANSI escape codes from the string
        cleaned_text = ansi_escape.sub('', string)

        # Count the alphabetic characters in the cleaned string
        letter_count = 0
        for char in cleaned_text:
            if 'a' <= char <= 'z' or 'A' <= char <= 'Z':
                letter_count += 1

        return letter_count


t = theme = Theme(themes[os.getenv('CLI2_THEME', 'standard')])


[docs] def demo(): """ Print all colors and modes from the theme. """ for name, theme in themes.items(): theme = Theme(themes[name]) print(f'\n\nTheme: {theme.bold(name)}') _demo(theme) print() print(theme.bold('MODES:')) for name in modes: mode = getattr(theme, name) print(f'{mode}t.{name}{t.rs}')
def _demo(theme): def color_data(alias, color): data = [ (color, alias), ] for mode in modes: data.append( (getattr(color, mode[0]), f'{alias}.{mode[0]}'), ) return data from .table import Table table = Table() for name in t.colors: color = getattr(theme, name) table.append(color_data(color.alias, color)) table.print() cli = Command(demo)