From 896739353a613f23c007d9acaa2809010a522a37 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 16 Sep 2022 11:10:14 +0200 Subject: Adding upstream version 2.2.0. Signed-off-by: Daniel Baumann --- colorclass/__init__.py | 38 +++++ colorclass/__main__.py | 33 +++++ colorclass/codes.py | 229 +++++++++++++++++++++++++++++ colorclass/color.py | 220 ++++++++++++++++++++++++++++ colorclass/core.py | 342 +++++++++++++++++++++++++++++++++++++++++++ colorclass/parse.py | 96 ++++++++++++ colorclass/search.py | 49 +++++++ colorclass/toggles.py | 42 ++++++ colorclass/windows.py | 388 +++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1437 insertions(+) create mode 100644 colorclass/__init__.py create mode 100644 colorclass/__main__.py create mode 100644 colorclass/codes.py create mode 100644 colorclass/color.py create mode 100644 colorclass/core.py create mode 100644 colorclass/parse.py create mode 100644 colorclass/search.py create mode 100644 colorclass/toggles.py create mode 100644 colorclass/windows.py (limited to 'colorclass') diff --git a/colorclass/__init__.py b/colorclass/__init__.py new file mode 100644 index 0000000..cae4016 --- /dev/null +++ b/colorclass/__init__.py @@ -0,0 +1,38 @@ +"""Colorful worry-free console applications for Linux, Mac OS X, and Windows. + +Supported natively on Linux and Mac OSX (Just Works), and on Windows it works the same if Windows.enable() is called. + +Gives you expected and sane results from methods like len() and .capitalize(). + +https://github.com/Robpol86/colorclass +https://pypi.python.org/pypi/colorclass +""" + +from colorclass.codes import list_tags # noqa +from colorclass.color import Color # noqa +from colorclass.toggles import disable_all_colors # noqa +from colorclass.toggles import disable_if_no_tty # noqa +from colorclass.toggles import enable_all_colors # noqa +from colorclass.toggles import is_enabled # noqa +from colorclass.toggles import is_light # noqa +from colorclass.toggles import set_dark_background # noqa +from colorclass.toggles import set_light_background # noqa +from colorclass.windows import Windows # noqa + + +__all__ = ( + 'Color', + 'disable_all_colors', + 'enable_all_colors', + 'is_enabled', + 'is_light', + 'list_tags', + 'set_dark_background', + 'set_light_background', + 'Windows', +) + + +__author__ = '@Robpol86' +__license__ = 'MIT' +__version__ = '2.2.0' diff --git a/colorclass/__main__.py b/colorclass/__main__.py new file mode 100644 index 0000000..d8f3f00 --- /dev/null +++ b/colorclass/__main__.py @@ -0,0 +1,33 @@ +"""Called by "python -m". Allows package to be used as a script. + +Example usage: +echo "{red}Red{/red}" |python -m colorclass +""" + +from __future__ import print_function + +import fileinput +import os + +from colorclass.color import Color +from colorclass.toggles import disable_all_colors +from colorclass.toggles import enable_all_colors +from colorclass.toggles import set_dark_background +from colorclass.toggles import set_light_background +from colorclass.windows import Windows + +TRUTHY = ('true', '1', 'yes', 'on') + + +if __name__ == '__main__': + if os.environ.get('COLOR_ENABLE', '').lower() in TRUTHY: + enable_all_colors() + elif os.environ.get('COLOR_DISABLE', '').lower() in TRUTHY: + disable_all_colors() + if os.environ.get('COLOR_LIGHT', '').lower() in TRUTHY: + set_light_background() + elif os.environ.get('COLOR_DARK', '').lower() in TRUTHY: + set_dark_background() + Windows.enable() + for LINE in fileinput.input(): + print(Color(LINE)) diff --git a/colorclass/codes.py b/colorclass/codes.py new file mode 100644 index 0000000..b0ecb03 --- /dev/null +++ b/colorclass/codes.py @@ -0,0 +1,229 @@ +"""Handles mapping between color names and ANSI codes and determining auto color codes.""" + +import sys +from collections import Mapping + +BASE_CODES = { + '/all': 0, 'b': 1, 'f': 2, 'i': 3, 'u': 4, 'flash': 5, 'outline': 6, 'negative': 7, 'invis': 8, 'strike': 9, + '/b': 22, '/f': 22, '/i': 23, '/u': 24, '/flash': 25, '/outline': 26, '/negative': 27, '/invis': 28, + '/strike': 29, '/fg': 39, '/bg': 49, + + 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, + + 'bgblack': 40, 'bgred': 41, 'bggreen': 42, 'bgyellow': 43, 'bgblue': 44, 'bgmagenta': 45, 'bgcyan': 46, + 'bgwhite': 47, + + 'hiblack': 90, 'hired': 91, 'higreen': 92, 'hiyellow': 93, 'hiblue': 94, 'himagenta': 95, 'hicyan': 96, + 'hiwhite': 97, + + 'hibgblack': 100, 'hibgred': 101, 'hibggreen': 102, 'hibgyellow': 103, 'hibgblue': 104, 'hibgmagenta': 105, + 'hibgcyan': 106, 'hibgwhite': 107, + + 'autored': None, 'autoblack': None, 'automagenta': None, 'autowhite': None, 'autoblue': None, 'autoyellow': None, + 'autogreen': None, 'autocyan': None, + + 'autobgred': None, 'autobgblack': None, 'autobgmagenta': None, 'autobgwhite': None, 'autobgblue': None, + 'autobgyellow': None, 'autobggreen': None, 'autobgcyan': None, + + '/black': 39, '/red': 39, '/green': 39, '/yellow': 39, '/blue': 39, '/magenta': 39, '/cyan': 39, '/white': 39, + '/hiblack': 39, '/hired': 39, '/higreen': 39, '/hiyellow': 39, '/hiblue': 39, '/himagenta': 39, '/hicyan': 39, + '/hiwhite': 39, + + '/bgblack': 49, '/bgred': 49, '/bggreen': 49, '/bgyellow': 49, '/bgblue': 49, '/bgmagenta': 49, '/bgcyan': 49, + '/bgwhite': 49, '/hibgblack': 49, '/hibgred': 49, '/hibggreen': 49, '/hibgyellow': 49, '/hibgblue': 49, + '/hibgmagenta': 49, '/hibgcyan': 49, '/hibgwhite': 49, + + '/autored': 39, '/autoblack': 39, '/automagenta': 39, '/autowhite': 39, '/autoblue': 39, '/autoyellow': 39, + '/autogreen': 39, '/autocyan': 39, + + '/autobgred': 49, '/autobgblack': 49, '/autobgmagenta': 49, '/autobgwhite': 49, '/autobgblue': 49, + '/autobgyellow': 49, '/autobggreen': 49, '/autobgcyan': 49, +} + + +class ANSICodeMapping(Mapping): + """Read-only dictionary, resolves closing tags and automatic colors. Iterates only used color tags. + + :cvar bool DISABLE_COLORS: Disable colors (strip color codes). + :cvar bool LIGHT_BACKGROUND: Use low intensity color codes. + """ + + DISABLE_COLORS = False + LIGHT_BACKGROUND = False + + def __init__(self, value_markup): + """Constructor. + + :param str value_markup: String with {color} tags. + """ + self.whitelist = [k for k in BASE_CODES if '{' + k + '}' in value_markup] + + def __getitem__(self, item): + """Return value for key or None if colors are disabled. + + :param str item: Key. + + :return: Color code integer. + :rtype: int + """ + if item not in self.whitelist: + raise KeyError(item) + if self.DISABLE_COLORS: + return None + return getattr(self, item, BASE_CODES[item]) + + def __iter__(self): + """Iterate dictionary.""" + return iter(self.whitelist) + + def __len__(self): + """Dictionary length.""" + return len(self.whitelist) + + @classmethod + def disable_all_colors(cls): + """Disable all colors. Strips any color tags or codes.""" + cls.DISABLE_COLORS = True + + @classmethod + def enable_all_colors(cls): + """Enable all colors. Strips any color tags or codes.""" + cls.DISABLE_COLORS = False + + @classmethod + def disable_if_no_tty(cls): + """Disable all colors only if there is no TTY available. + + :return: True if colors are disabled, False if stderr or stdout is a TTY. + :rtype: bool + """ + if sys.stdout.isatty() or sys.stderr.isatty(): + return False + cls.disable_all_colors() + return True + + @classmethod + def set_dark_background(cls): + """Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds.""" + cls.LIGHT_BACKGROUND = False + + @classmethod + def set_light_background(cls): + """Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds.""" + cls.LIGHT_BACKGROUND = True + + @property + def autoblack(self): + """Return automatic black foreground color depending on background color.""" + return BASE_CODES['black' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiblack'] + + @property + def autored(self): + """Return automatic red foreground color depending on background color.""" + return BASE_CODES['red' if ANSICodeMapping.LIGHT_BACKGROUND else 'hired'] + + @property + def autogreen(self): + """Return automatic green foreground color depending on background color.""" + return BASE_CODES['green' if ANSICodeMapping.LIGHT_BACKGROUND else 'higreen'] + + @property + def autoyellow(self): + """Return automatic yellow foreground color depending on background color.""" + return BASE_CODES['yellow' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiyellow'] + + @property + def autoblue(self): + """Return automatic blue foreground color depending on background color.""" + return BASE_CODES['blue' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiblue'] + + @property + def automagenta(self): + """Return automatic magenta foreground color depending on background color.""" + return BASE_CODES['magenta' if ANSICodeMapping.LIGHT_BACKGROUND else 'himagenta'] + + @property + def autocyan(self): + """Return automatic cyan foreground color depending on background color.""" + return BASE_CODES['cyan' if ANSICodeMapping.LIGHT_BACKGROUND else 'hicyan'] + + @property + def autowhite(self): + """Return automatic white foreground color depending on background color.""" + return BASE_CODES['white' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiwhite'] + + @property + def autobgblack(self): + """Return automatic black background color depending on background color.""" + return BASE_CODES['bgblack' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgblack'] + + @property + def autobgred(self): + """Return automatic red background color depending on background color.""" + return BASE_CODES['bgred' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgred'] + + @property + def autobggreen(self): + """Return automatic green background color depending on background color.""" + return BASE_CODES['bggreen' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibggreen'] + + @property + def autobgyellow(self): + """Return automatic yellow background color depending on background color.""" + return BASE_CODES['bgyellow' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgyellow'] + + @property + def autobgblue(self): + """Return automatic blue background color depending on background color.""" + return BASE_CODES['bgblue' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgblue'] + + @property + def autobgmagenta(self): + """Return automatic magenta background color depending on background color.""" + return BASE_CODES['bgmagenta' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgmagenta'] + + @property + def autobgcyan(self): + """Return automatic cyan background color depending on background color.""" + return BASE_CODES['bgcyan' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgcyan'] + + @property + def autobgwhite(self): + """Return automatic white background color depending on background color.""" + return BASE_CODES['bgwhite' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgwhite'] + + +def list_tags(): + """List the available tags. + + :return: List of 4-item tuples: opening tag, closing tag, main ansi value, closing ansi value. + :rtype: list + """ + # Build reverse dictionary. Keys are closing tags, values are [closing ansi, opening tag, opening ansi]. + reverse_dict = dict() + for tag, ansi in sorted(BASE_CODES.items()): + if tag.startswith('/'): + reverse_dict[tag] = [ansi, None, None] + else: + reverse_dict['/' + tag][1:] = [tag, ansi] + + # Collapse + four_item_tuples = [(v[1], k, v[2], v[0]) for k, v in reverse_dict.items()] + + # Sort. + def sorter(four_item): + """Sort /all /fg /bg first, then b i u flash, then auto colors, then dark colors, finally light colors. + + :param iter four_item: [opening tag, closing tag, main ansi value, closing ansi value] + + :return Sorting weight. + :rtype: int + """ + if not four_item[2]: # /all /fg /bg + return four_item[3] - 200 + if four_item[2] < 10 or four_item[0].startswith('auto'): # b f i u or auto colors + return four_item[2] - 100 + return four_item[2] + four_item_tuples.sort(key=sorter) + + return four_item_tuples diff --git a/colorclass/color.py b/colorclass/color.py new file mode 100644 index 0000000..2849d06 --- /dev/null +++ b/colorclass/color.py @@ -0,0 +1,220 @@ +"""Color class used by library users.""" + +from colorclass.core import ColorStr + + +class Color(ColorStr): + """Unicode (str in Python3) subclass with ANSI terminal text color support. + + Example syntax: Color('{red}Sample Text{/red}') + + Example without parsing logic: Color('{red}Sample Text{/red}', keep_tags=True) + + For a list of codes, call: colorclass.list_tags() + """ + + @classmethod + def colorize(cls, color, string, auto=False): + """Color-code entire string using specified color. + + :param str color: Color of string. + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + tag = '{0}{1}'.format('auto' if auto else '', color) + return cls('{%s}%s{/%s}' % (tag, string, tag)) + + @classmethod + def black(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('black', string, auto=auto) + + @classmethod + def bgblack(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('bgblack', string, auto=auto) + + @classmethod + def red(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('red', string, auto=auto) + + @classmethod + def bgred(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('bgred', string, auto=auto) + + @classmethod + def green(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('green', string, auto=auto) + + @classmethod + def bggreen(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('bggreen', string, auto=auto) + + @classmethod + def yellow(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('yellow', string, auto=auto) + + @classmethod + def bgyellow(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('bgyellow', string, auto=auto) + + @classmethod + def blue(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('blue', string, auto=auto) + + @classmethod + def bgblue(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('bgblue', string, auto=auto) + + @classmethod + def magenta(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('magenta', string, auto=auto) + + @classmethod + def bgmagenta(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('bgmagenta', string, auto=auto) + + @classmethod + def cyan(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('cyan', string, auto=auto) + + @classmethod + def bgcyan(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('bgcyan', string, auto=auto) + + @classmethod + def white(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('white', string, auto=auto) + + @classmethod + def bgwhite(cls, string, auto=False): + """Color-code entire string. + + :param str string: String to colorize. + :param bool auto: Enable auto-color (dark/light terminal). + + :return: Class instance for colorized string. + :rtype: Color + """ + return cls.colorize('bgwhite', string, auto=auto) diff --git a/colorclass/core.py b/colorclass/core.py new file mode 100644 index 0000000..481bb40 --- /dev/null +++ b/colorclass/core.py @@ -0,0 +1,342 @@ +"""String subclass that handles ANSI color codes.""" + +from colorclass.codes import ANSICodeMapping +from colorclass.parse import parse_input, RE_SPLIT +from colorclass.search import build_color_index, find_char_color + +PARENT_CLASS = type(u'') + + +def apply_text(incoming, func): + """Call `func` on text portions of incoming color string. + + :param iter incoming: Incoming string/ColorStr/string-like object to iterate. + :param func: Function to call with string portion as first and only parameter. + + :return: Modified string, same class type as incoming string. + """ + split = RE_SPLIT.split(incoming) + for i, item in enumerate(split): + if not item or RE_SPLIT.match(item): + continue + split[i] = func(item) + return incoming.__class__().join(split) + + +class ColorBytes(bytes): + """Str (bytes in Python3) subclass, .decode() overridden to return unicode (str in Python3) subclass instance.""" + + def __new__(cls, *args, **kwargs): + """Save original class so decode() returns an instance of it.""" + original_class = kwargs.pop('original_class') + combined_args = [cls] + list(args) + instance = bytes.__new__(*combined_args, **kwargs) + instance.original_class = original_class + return instance + + def decode(self, encoding='utf-8', errors='strict'): + """Decode using the codec registered for encoding. Default encoding is 'utf-8'. + + errors may be given to set a different error handling scheme. Default is 'strict' meaning that encoding errors + raise a UnicodeDecodeError. Other possible values are 'ignore' and 'replace' as well as any other name + registered with codecs.register_error that is able to handle UnicodeDecodeErrors. + + :param str encoding: Codec. + :param str errors: Error handling scheme. + """ + original_class = getattr(self, 'original_class') + return original_class(super(ColorBytes, self).decode(encoding, errors)) + + +class ColorStr(PARENT_CLASS): + """Core color class.""" + + def __new__(cls, *args, **kwargs): + """Parse color markup and instantiate.""" + keep_tags = kwargs.pop('keep_tags', False) + + # Parse string. + value_markup = args[0] if args else PARENT_CLASS() # e.g. '{red}test{/red}' + value_colors, value_no_colors = parse_input(value_markup, ANSICodeMapping.DISABLE_COLORS, keep_tags) + color_index = build_color_index(value_colors) + + # Instantiate. + color_args = [cls, value_colors] + list(args[1:]) + instance = PARENT_CLASS.__new__(*color_args, **kwargs) + + # Add additional attributes and return. + instance.value_colors = value_colors + instance.value_no_colors = value_no_colors + instance.has_colors = value_colors != value_no_colors + instance.color_index = color_index + return instance + + def __add__(self, other): + """Concatenate.""" + return self.__class__(self.value_colors + other, keep_tags=True) + + def __getitem__(self, item): + """Retrieve character.""" + try: + color_pos = self.color_index[int(item)] + except TypeError: # slice + return super(ColorStr, self).__getitem__(item) + return self.__class__(find_char_color(self.value_colors, color_pos), keep_tags=True) + + def __iter__(self): + """Yield one color-coded character at a time.""" + for color_pos in self.color_index: + yield self.__class__(find_char_color(self.value_colors, color_pos)) + + def __len__(self): + """Length of string without color codes (what users expect).""" + return self.value_no_colors.__len__() + + def __mod__(self, other): + """String substitution (like printf).""" + return self.__class__(self.value_colors % other, keep_tags=True) + + def __mul__(self, other): + """Multiply string.""" + return self.__class__(self.value_colors * other, keep_tags=True) + + def __repr__(self): + """Representation of a class instance (like datetime.datetime.now()).""" + return '{name}({value})'.format(name=self.__class__.__name__, value=repr(self.value_colors)) + + def capitalize(self): + """Return a copy of the string with only its first character capitalized.""" + return apply_text(self, lambda s: s.capitalize()) + + def center(self, width, fillchar=None): + """Return centered in a string of length width. Padding is done using the specified fill character or space. + + :param int width: Length of output string. + :param str fillchar: Use this character instead of spaces. + """ + if fillchar is not None: + result = self.value_no_colors.center(width, fillchar) + else: + result = self.value_no_colors.center(width) + return self.__class__(result.replace(self.value_no_colors, self.value_colors), keep_tags=True) + + def count(self, sub, start=0, end=-1): + """Return the number of non-overlapping occurrences of substring sub in string[start:end]. + + Optional arguments start and end are interpreted as in slice notation. + + :param str sub: Substring to search. + :param int start: Beginning position. + :param int end: Stop comparison at this position. + """ + return self.value_no_colors.count(sub, start, end) + + def endswith(self, suffix, start=0, end=None): + """Return True if ends with the specified suffix, False otherwise. + + With optional start, test beginning at that position. With optional end, stop comparing at that position. + suffix can also be a tuple of strings to try. + + :param str suffix: Suffix to search. + :param int start: Beginning position. + :param int end: Stop comparison at this position. + """ + args = [suffix, start] + ([] if end is None else [end]) + return self.value_no_colors.endswith(*args) + + def encode(self, encoding=None, errors='strict'): + """Encode using the codec registered for encoding. encoding defaults to the default encoding. + + errors may be given to set a different error handling scheme. Default is 'strict' meaning that encoding errors + raise a UnicodeEncodeError. Other possible values are 'ignore', 'replace' and 'xmlcharrefreplace' as well as any + other name registered with codecs.register_error that is able to handle UnicodeEncodeErrors. + + :param str encoding: Codec. + :param str errors: Error handling scheme. + """ + return ColorBytes(super(ColorStr, self).encode(encoding, errors), original_class=self.__class__) + + def decode(self, encoding=None, errors='strict'): + """Decode using the codec registered for encoding. encoding defaults to the default encoding. + + errors may be given to set a different error handling scheme. Default is 'strict' meaning that encoding errors + raise a UnicodeDecodeError. Other possible values are 'ignore' and 'replace' as well as any other name + registered with codecs.register_error that is able to handle UnicodeDecodeErrors. + + :param str encoding: Codec. + :param str errors: Error handling scheme. + """ + return self.__class__(super(ColorStr, self).decode(encoding, errors), keep_tags=True) + + def find(self, sub, start=None, end=None): + """Return the lowest index where substring sub is found, such that sub is contained within string[start:end]. + + Optional arguments start and end are interpreted as in slice notation. + + :param str sub: Substring to search. + :param int start: Beginning position. + :param int end: Stop comparison at this position. + """ + return self.value_no_colors.find(sub, start, end) + + def format(self, *args, **kwargs): + """Return a formatted version, using substitutions from args and kwargs. + + The substitutions are identified by braces ('{' and '}'). + """ + return self.__class__(super(ColorStr, self).format(*args, **kwargs), keep_tags=True) + + def index(self, sub, start=None, end=None): + """Like S.find() but raise ValueError when the substring is not found. + + :param str sub: Substring to search. + :param int start: Beginning position. + :param int end: Stop comparison at this position. + """ + return self.value_no_colors.index(sub, start, end) + + def isalnum(self): + """Return True if all characters in string are alphanumeric and there is at least one character in it.""" + return self.value_no_colors.isalnum() + + def isalpha(self): + """Return True if all characters in string are alphabetic and there is at least one character in it.""" + return self.value_no_colors.isalpha() + + def isdecimal(self): + """Return True if there are only decimal characters in string, False otherwise.""" + return self.value_no_colors.isdecimal() + + def isdigit(self): + """Return True if all characters in string are digits and there is at least one character in it.""" + return self.value_no_colors.isdigit() + + def isnumeric(self): + """Return True if there are only numeric characters in string, False otherwise.""" + return self.value_no_colors.isnumeric() + + def isspace(self): + """Return True if all characters in string are whitespace and there is at least one character in it.""" + return self.value_no_colors.isspace() + + def istitle(self): + """Return True if string is a titlecased string and there is at least one character in it. + + That is uppercase characters may only follow uncased characters and lowercase characters only cased ones. Return + False otherwise. + """ + return self.value_no_colors.istitle() + + def isupper(self): + """Return True if all cased characters are uppercase and there is at least one cased character in it.""" + return self.value_no_colors.isupper() + + def join(self, iterable): + """Return a string which is the concatenation of the strings in the iterable. + + :param iterable: Join items in this iterable. + """ + return self.__class__(super(ColorStr, self).join(iterable), keep_tags=True) + + def ljust(self, width, fillchar=None): + """Return left-justified string of length width. Padding is done using the specified fill character or space. + + :param int width: Length of output string. + :param str fillchar: Use this character instead of spaces. + """ + if fillchar is not None: + result = self.value_no_colors.ljust(width, fillchar) + else: + result = self.value_no_colors.ljust(width) + return self.__class__(result.replace(self.value_no_colors, self.value_colors), keep_tags=True) + + def rfind(self, sub, start=None, end=None): + """Return the highest index where substring sub is found, such that sub is contained within string[start:end]. + + Optional arguments start and end are interpreted as in slice notation. + + :param str sub: Substring to search. + :param int start: Beginning position. + :param int end: Stop comparison at this position. + """ + return self.value_no_colors.rfind(sub, start, end) + + def rindex(self, sub, start=None, end=None): + """Like .rfind() but raise ValueError when the substring is not found. + + :param str sub: Substring to search. + :param int start: Beginning position. + :param int end: Stop comparison at this position. + """ + return self.value_no_colors.rindex(sub, start, end) + + def rjust(self, width, fillchar=None): + """Return right-justified string of length width. Padding is done using the specified fill character or space. + + :param int width: Length of output string. + :param str fillchar: Use this character instead of spaces. + """ + if fillchar is not None: + result = self.value_no_colors.rjust(width, fillchar) + else: + result = self.value_no_colors.rjust(width) + return self.__class__(result.replace(self.value_no_colors, self.value_colors), keep_tags=True) + + def splitlines(self, keepends=False): + """Return a list of the lines in the string, breaking at line boundaries. + + Line breaks are not included in the resulting list unless keepends is given and True. + + :param bool keepends: Include linebreaks. + """ + return [self.__class__(l) for l in self.value_colors.splitlines(keepends)] + + def startswith(self, prefix, start=0, end=-1): + """Return True if string starts with the specified prefix, False otherwise. + + With optional start, test beginning at that position. With optional end, stop comparing at that position. prefix + can also be a tuple of strings to try. + + :param str prefix: Prefix to search. + :param int start: Beginning position. + :param int end: Stop comparison at this position. + """ + return self.value_no_colors.startswith(prefix, start, end) + + def swapcase(self): + """Return a copy of the string with uppercase characters converted to lowercase and vice versa.""" + return apply_text(self, lambda s: s.swapcase()) + + def title(self): + """Return a titlecased version of the string. + + That is words start with uppercase characters, all remaining cased characters have lowercase. + """ + return apply_text(self, lambda s: s.title()) + + def translate(self, table): + """Return a copy of the string, where all characters have been mapped through the given translation table. + + Table must be a mapping of Unicode ordinals to Unicode ordinals, strings, or None. Unmapped characters are left + untouched. Characters mapped to None are deleted. + + :param table: Translation table. + """ + return apply_text(self, lambda s: s.translate(table)) + + def upper(self): + """Return a copy of the string converted to uppercase.""" + return apply_text(self, lambda s: s.upper()) + + def zfill(self, width): + """Pad a numeric string with zeros on the left, to fill a field of the specified width. + + The string is never truncated. + + :param int width: Length of output string. + """ + if not self.value_no_colors: + result = self.value_no_colors.zfill(width) + else: + result = self.value_colors.replace(self.value_no_colors, self.value_no_colors.zfill(width)) + return self.__class__(result, keep_tags=True) diff --git a/colorclass/parse.py b/colorclass/parse.py new file mode 100644 index 0000000..46dc28e --- /dev/null +++ b/colorclass/parse.py @@ -0,0 +1,96 @@ +"""Parse color markup tags into ANSI escape sequences.""" + +import re + +from colorclass.codes import ANSICodeMapping, BASE_CODES + +CODE_GROUPS = ( + tuple(set(str(i) for i in BASE_CODES.values() if i and (40 <= i <= 49 or 100 <= i <= 109))), # bg colors + tuple(set(str(i) for i in BASE_CODES.values() if i and (30 <= i <= 39 or 90 <= i <= 99))), # fg colors + ('1', '22'), ('2', '22'), ('3', '23'), ('4', '24'), ('5', '25'), ('6', '26'), ('7', '27'), ('8', '28'), ('9', '29'), +) +RE_ANSI = re.compile(r'(\033\[([\d;]+)m)') +RE_COMBINE = re.compile(r'\033\[([\d;]+)m\033\[([\d;]+)m') +RE_SPLIT = re.compile(r'(\033\[[\d;]+m)') + + +def prune_overridden(ansi_string): + """Remove color codes that are rendered ineffective by subsequent codes in one escape sequence then sort codes. + + :param str ansi_string: Incoming ansi_string with ANSI color codes. + + :return: Color string with pruned color sequences. + :rtype: str + """ + multi_seqs = set(p for p in RE_ANSI.findall(ansi_string) if ';' in p[1]) # Sequences with multiple color codes. + + for escape, codes in multi_seqs: + r_codes = list(reversed(codes.split(';'))) + + # Nuke everything before {/all}. + try: + r_codes = r_codes[:r_codes.index('0') + 1] + except ValueError: + pass + + # Thin out groups. + for group in CODE_GROUPS: + for pos in reversed([i for i, n in enumerate(r_codes) if n in group][1:]): + r_codes.pop(pos) + + # Done. + reduced_codes = ';'.join(sorted(r_codes, key=int)) + if codes != reduced_codes: + ansi_string = ansi_string.replace(escape, '\033[' + reduced_codes + 'm') + + return ansi_string + + +def parse_input(tagged_string, disable_colors, keep_tags): + """Perform the actual conversion of tags to ANSI escaped codes. + + Provides a version of the input without any colors for len() and other methods. + + :param str tagged_string: The input unicode value. + :param bool disable_colors: Strip all colors in both outputs. + :param bool keep_tags: Skip parsing curly bracket tags into ANSI escape sequences. + + :return: 2-item tuple. First item is the parsed output. Second item is a version of the input without any colors. + :rtype: tuple + """ + codes = ANSICodeMapping(tagged_string) + output_colors = getattr(tagged_string, 'value_colors', tagged_string) + + # Convert: '{b}{red}' -> '\033[1m\033[31m' + if not keep_tags: + for tag, replacement in (('{' + k + '}', '' if v is None else '\033[%dm' % v) for k, v in codes.items()): + output_colors = output_colors.replace(tag, replacement) + + # Strip colors. + output_no_colors = RE_ANSI.sub('', output_colors) + if disable_colors: + return output_no_colors, output_no_colors + + # Combine: '\033[1m\033[31m' -> '\033[1;31m' + while True: + simplified = RE_COMBINE.sub(r'\033[\1;\2m', output_colors) + if simplified == output_colors: + break + output_colors = simplified + + # Prune: '\033[31;32;33;34;35m' -> '\033[35m' + output_colors = prune_overridden(output_colors) + + # Deduplicate: '\033[1;mT\033[1;mE\033[1;mS\033[1;mT' -> '\033[1;mTEST' + previous_escape = None + segments = list() + for item in (i for i in RE_SPLIT.split(output_colors) if i): + if RE_SPLIT.match(item): + if item != previous_escape: + segments.append(item) + previous_escape = item + else: + segments.append(item) + output_colors = ''.join(segments) + + return output_colors, output_no_colors diff --git a/colorclass/search.py b/colorclass/search.py new file mode 100644 index 0000000..555402d --- /dev/null +++ b/colorclass/search.py @@ -0,0 +1,49 @@ +"""Determine color of characters that may or may not be adjacent to ANSI escape sequences.""" + +from colorclass.parse import RE_SPLIT + + +def build_color_index(ansi_string): + """Build an index between visible characters and a string with invisible color codes. + + :param str ansi_string: String with color codes (ANSI escape sequences). + + :return: Position of visible characters in color string (indexes match non-color string). + :rtype: tuple + """ + mapping = list() + color_offset = 0 + for item in (i for i in RE_SPLIT.split(ansi_string) if i): + if RE_SPLIT.match(item): + color_offset += len(item) + else: + for _ in range(len(item)): + mapping.append(color_offset) + color_offset += 1 + return tuple(mapping) + + +def find_char_color(ansi_string, pos): + """Determine what color a character is in the string. + + :param str ansi_string: String with color codes (ANSI escape sequences). + :param int pos: Position of the character in the ansi_string. + + :return: Character along with all surrounding color codes. + :rtype: str + """ + result = list() + position = 0 # Set to None when character is found. + for item in (i for i in RE_SPLIT.split(ansi_string) if i): + if RE_SPLIT.match(item): + result.append(item) + if position is not None: + position += len(item) + elif position is not None: + for char in item: + if position == pos: + result.append(char) + position = None + break + position += 1 + return ''.join(result) diff --git a/colorclass/toggles.py b/colorclass/toggles.py new file mode 100644 index 0000000..1ba6bce --- /dev/null +++ b/colorclass/toggles.py @@ -0,0 +1,42 @@ +"""Convenience functions to enable/disable features.""" + +from colorclass.codes import ANSICodeMapping + + +def disable_all_colors(): + """Disable all colors. Strip any color tags or codes.""" + ANSICodeMapping.disable_all_colors() + + +def enable_all_colors(): + """Enable colors.""" + ANSICodeMapping.enable_all_colors() + + +def disable_if_no_tty(): + """Disable all colors if there is no TTY available. + + :return: True if colors are disabled, False if stderr or stdout is a TTY. + :rtype: bool + """ + return ANSICodeMapping.disable_if_no_tty() + + +def is_enabled(): + """Are colors enabled.""" + return not ANSICodeMapping.DISABLE_COLORS + + +def set_light_background(): + """Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds.""" + ANSICodeMapping.set_light_background() + + +def set_dark_background(): + """Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds.""" + ANSICodeMapping.set_dark_background() + + +def is_light(): + """Are background colors for light backgrounds.""" + return ANSICodeMapping.LIGHT_BACKGROUND diff --git a/colorclass/windows.py b/colorclass/windows.py new file mode 100644 index 0000000..8f69478 --- /dev/null +++ b/colorclass/windows.py @@ -0,0 +1,388 @@ +"""Windows console screen buffer handlers.""" + +from __future__ import print_function + +import atexit +import ctypes +import re +import sys + +from colorclass.codes import ANSICodeMapping, BASE_CODES +from colorclass.core import RE_SPLIT + +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 +INVALID_HANDLE_VALUE = -1 +IS_WINDOWS = sys.platform == 'win32' +RE_NUMBER_SEARCH = re.compile(r'\033\[([\d;]+)m') +STD_ERROR_HANDLE = -12 +STD_OUTPUT_HANDLE = -11 +WINDOWS_CODES = { + '/all': -33, '/fg': -39, '/bg': -49, + + 'black': 0, 'red': 4, 'green': 2, 'yellow': 6, 'blue': 1, 'magenta': 5, 'cyan': 3, 'white': 7, + + 'bgblack': -8, 'bgred': 64, 'bggreen': 32, 'bgyellow': 96, 'bgblue': 16, 'bgmagenta': 80, 'bgcyan': 48, + 'bgwhite': 112, + + 'hiblack': 8, 'hired': 12, 'higreen': 10, 'hiyellow': 14, 'hiblue': 9, 'himagenta': 13, 'hicyan': 11, 'hiwhite': 15, + + 'hibgblack': 128, 'hibgred': 192, 'hibggreen': 160, 'hibgyellow': 224, 'hibgblue': 144, 'hibgmagenta': 208, + 'hibgcyan': 176, 'hibgwhite': 240, + + '/black': -39, '/red': -39, '/green': -39, '/yellow': -39, '/blue': -39, '/magenta': -39, '/cyan': -39, + '/white': -39, '/hiblack': -39, '/hired': -39, '/higreen': -39, '/hiyellow': -39, '/hiblue': -39, '/himagenta': -39, + '/hicyan': -39, '/hiwhite': -39, + + '/bgblack': -49, '/bgred': -49, '/bggreen': -49, '/bgyellow': -49, '/bgblue': -49, '/bgmagenta': -49, + '/bgcyan': -49, '/bgwhite': -49, '/hibgblack': -49, '/hibgred': -49, '/hibggreen': -49, '/hibgyellow': -49, + '/hibgblue': -49, '/hibgmagenta': -49, '/hibgcyan': -49, '/hibgwhite': -49, +} + + +class COORD(ctypes.Structure): + """COORD structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119.""" + + _fields_ = [ + ('X', ctypes.c_short), + ('Y', ctypes.c_short), + ] + + +class SmallRECT(ctypes.Structure): + """SMALL_RECT structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311.""" + + _fields_ = [ + ('Left', ctypes.c_short), + ('Top', ctypes.c_short), + ('Right', ctypes.c_short), + ('Bottom', ctypes.c_short), + ] + + +class ConsoleScreenBufferInfo(ctypes.Structure): + """CONSOLE_SCREEN_BUFFER_INFO structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093.""" + + _fields_ = [ + ('dwSize', COORD), + ('dwCursorPosition', COORD), + ('wAttributes', ctypes.c_ushort), + ('srWindow', SmallRECT), + ('dwMaximumWindowSize', COORD) + ] + + +def init_kernel32(kernel32=None): + """Load a unique instance of WinDLL into memory, set arg/return types, and get stdout/err handles. + + 1. Since we are setting DLL function argument types and return types, we need to maintain our own instance of + kernel32 to prevent overriding (or being overwritten by) user's own changes to ctypes.windll.kernel32. + 2. While we're doing all this we might as well get the handles to STDOUT and STDERR streams. + 3. If either stream has already been replaced set return value to INVALID_HANDLE_VALUE to indicate it shouldn't be + replaced. + + :raise AttributeError: When called on a non-Windows platform. + + :param kernel32: Optional mock kernel32 object. For testing. + + :return: Loaded kernel32 instance, stderr handle (int), stdout handle (int). + :rtype: tuple + """ + if not kernel32: + kernel32 = ctypes.LibraryLoader(ctypes.WinDLL).kernel32 # Load our own instance. Unique memory address. + kernel32.GetStdHandle.argtypes = [ctypes.c_ulong] + kernel32.GetStdHandle.restype = ctypes.c_void_p + kernel32.GetConsoleScreenBufferInfo.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ConsoleScreenBufferInfo), + ] + kernel32.GetConsoleScreenBufferInfo.restype = ctypes.c_long + + # Get handles. + if hasattr(sys.stderr, '_original_stream'): + stderr = INVALID_HANDLE_VALUE + else: + stderr = kernel32.GetStdHandle(STD_ERROR_HANDLE) + if hasattr(sys.stdout, '_original_stream'): + stdout = INVALID_HANDLE_VALUE + else: + stdout = kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + + return kernel32, stderr, stdout + + +def get_console_info(kernel32, handle): + """Get information about this current console window. + + http://msdn.microsoft.com/en-us/library/windows/desktop/ms683231 + https://code.google.com/p/colorama/issues/detail?id=47 + https://bitbucket.org/pytest-dev/py/src/4617fe46/py/_io/terminalwriter.py + + Windows 10 Insider since around February 2016 finally introduced support for ANSI colors. No need to replace stdout + and stderr streams to intercept colors and issue multiple SetConsoleTextAttribute() calls for these consoles. + + :raise OSError: When GetConsoleScreenBufferInfo or GetConsoleMode API calls fail. + + :param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance. + :param int handle: stderr or stdout handle. + + :return: Foreground and background colors (integers) as well as native ANSI support (bool). + :rtype: tuple + """ + # Query Win32 API. + csbi = ConsoleScreenBufferInfo() # Populated by GetConsoleScreenBufferInfo. + lpcsbi = ctypes.byref(csbi) + dword = ctypes.c_ulong() # Populated by GetConsoleMode. + lpdword = ctypes.byref(dword) + if not kernel32.GetConsoleScreenBufferInfo(handle, lpcsbi) or not kernel32.GetConsoleMode(handle, lpdword): + raise ctypes.WinError() + + # Parse data. + # buffer_width = int(csbi.dwSize.X - 1) + # buffer_height = int(csbi.dwSize.Y) + # terminal_width = int(csbi.srWindow.Right - csbi.srWindow.Left) + # terminal_height = int(csbi.srWindow.Bottom - csbi.srWindow.Top) + fg_color = csbi.wAttributes % 16 + bg_color = csbi.wAttributes & 240 + native_ansi = bool(dword.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) + + return fg_color, bg_color, native_ansi + + +def bg_color_native_ansi(kernel32, stderr, stdout): + """Get background color and if console supports ANSI colors natively for both streams. + + :param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance. + :param int stderr: stderr handle. + :param int stdout: stdout handle. + + :return: Background color (int) and native ANSI support (bool). + :rtype: tuple + """ + try: + if stderr == INVALID_HANDLE_VALUE: + raise OSError + bg_color, native_ansi = get_console_info(kernel32, stderr)[1:] + except OSError: + try: + if stdout == INVALID_HANDLE_VALUE: + raise OSError + bg_color, native_ansi = get_console_info(kernel32, stdout)[1:] + except OSError: + bg_color, native_ansi = WINDOWS_CODES['black'], False + return bg_color, native_ansi + + +class WindowsStream(object): + """Replacement stream which overrides sys.stdout or sys.stderr. When writing or printing, ANSI codes are converted. + + ANSI (Linux/Unix) color codes are converted into win32 system calls, changing the next character's color before + printing it. Resources referenced: + https://github.com/tartley/colorama + http://www.cplusplus.com/articles/2ywTURfi/ + http://thomasfischer.biz/python-and-windows-terminal-colors/ + http://stackoverflow.com/questions/17125440/c-win32-console-color + http://www.tysos.org/svn/trunk/mono/corlib/System/WindowsConsoleDriver.cs + http://stackoverflow.com/questions/287871/print-in-terminal-with-colors-using-python + http://msdn.microsoft.com/en-us/library/windows/desktop/ms682088#_win32_character_attributes + + :cvar list ALL_BG_CODES: List of bg Windows codes. Used to determine if requested color is foreground or background. + :cvar dict COMPILED_CODES: Translation dict. Keys are ANSI codes (values of BASE_CODES), values are Windows codes. + :ivar int default_fg: Foreground Windows color code at the time of instantiation. + :ivar int default_bg: Background Windows color code at the time of instantiation. + """ + + ALL_BG_CODES = [v for k, v in WINDOWS_CODES.items() if k.startswith('bg') or k.startswith('hibg')] + COMPILED_CODES = dict((v, WINDOWS_CODES[k]) for k, v in BASE_CODES.items() if k in WINDOWS_CODES) + + def __init__(self, kernel32, stream_handle, original_stream): + """Constructor. + + :param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance. + :param int stream_handle: stderr or stdout handle. + :param original_stream: sys.stderr or sys.stdout before being overridden by this class' instance. + """ + self._kernel32 = kernel32 + self._stream_handle = stream_handle + self._original_stream = original_stream + self.default_fg, self.default_bg = self.colors + + def __getattr__(self, item): + """If an attribute/function/etc is not defined in this function, retrieve the one from the original stream. + + Fixes ipython arrow key presses. + """ + return getattr(self._original_stream, item) + + @property + def colors(self): + """Return the current foreground and background colors.""" + try: + return get_console_info(self._kernel32, self._stream_handle)[:2] + except OSError: + return WINDOWS_CODES['white'], WINDOWS_CODES['black'] + + @colors.setter + def colors(self, color_code): + """Change the foreground and background colors for subsequently printed characters. + + None resets colors to their original values (when class was instantiated). + + Since setting a color requires including both foreground and background codes (merged), setting just the + foreground color resets the background color to black, and vice versa. + + This function first gets the current background and foreground colors, merges in the requested color code, and + sets the result. + + However if we need to remove just the foreground color but leave the background color the same (or vice versa) + such as when {/red} is used, we must merge the default foreground color with the current background color. This + is the reason for those negative values. + + :param int color_code: Color code from WINDOWS_CODES. + """ + if color_code is None: + color_code = WINDOWS_CODES['/all'] + + # Get current color code. + current_fg, current_bg = self.colors + + # Handle special negative codes. Also determine the final color code. + if color_code == WINDOWS_CODES['/fg']: + final_color_code = self.default_fg | current_bg # Reset the foreground only. + elif color_code == WINDOWS_CODES['/bg']: + final_color_code = current_fg | self.default_bg # Reset the background only. + elif color_code == WINDOWS_CODES['/all']: + final_color_code = self.default_fg | self.default_bg # Reset both. + elif color_code == WINDOWS_CODES['bgblack']: + final_color_code = current_fg # Black background. + else: + new_is_bg = color_code in self.ALL_BG_CODES + final_color_code = color_code | (current_fg if new_is_bg else current_bg) + + # Set new code. + self._kernel32.SetConsoleTextAttribute(self._stream_handle, final_color_code) + + def write(self, p_str): + """Write to stream. + + :param str p_str: string to print. + """ + for segment in RE_SPLIT.split(p_str): + if not segment: + # Empty string. p_str probably starts with colors so the first item is always ''. + continue + if not RE_SPLIT.match(segment): + # No color codes, print regular text. + print(segment, file=self._original_stream, end='') + self._original_stream.flush() + continue + for color_code in (int(c) for c in RE_NUMBER_SEARCH.findall(segment)[0].split(';')): + if color_code in self.COMPILED_CODES: + self.colors = self.COMPILED_CODES[color_code] + + +class Windows(object): + """Enable and disable Windows support for ANSI color character codes. + + Call static method Windows.enable() to enable color support for the remainder of the process' lifetime. + + This class is also a context manager. You can do this: + with Windows(): + print(Color('{autored}Test{/autored}')) + + Or this: + with Windows(auto_colors=True): + print(Color('{autored}Test{/autored}')) + """ + + @classmethod + def disable(cls): + """Restore sys.stderr and sys.stdout to their original objects. Resets colors to their original values. + + :return: If streams restored successfully. + :rtype: bool + """ + # Skip if not on Windows. + if not IS_WINDOWS: + return False + + # Restore default colors. + if hasattr(sys.stderr, '_original_stream'): + getattr(sys, 'stderr').color = None + if hasattr(sys.stdout, '_original_stream'): + getattr(sys, 'stdout').color = None + + # Restore original streams. + changed = False + if hasattr(sys.stderr, '_original_stream'): + changed = True + sys.stderr = getattr(sys.stderr, '_original_stream') + if hasattr(sys.stdout, '_original_stream'): + changed = True + sys.stdout = getattr(sys.stdout, '_original_stream') + + return changed + + @staticmethod + def is_enabled(): + """Return True if either stderr or stdout has colors enabled.""" + return hasattr(sys.stderr, '_original_stream') or hasattr(sys.stdout, '_original_stream') + + @classmethod + def enable(cls, auto_colors=False, reset_atexit=False): + """Enable color text with print() or sys.stdout.write() (stderr too). + + :param bool auto_colors: Automatically selects dark or light colors based on current terminal's background + color. Only works with {autored} and related tags. + :param bool reset_atexit: Resets original colors upon Python exit (in case you forget to reset it yourself with + a closing tag). Does nothing on native ANSI consoles. + + :return: If streams replaced successfully. + :rtype: bool + """ + if not IS_WINDOWS: + return False # Windows only. + + # Get values from init_kernel32(). + kernel32, stderr, stdout = init_kernel32() + if stderr == INVALID_HANDLE_VALUE and stdout == INVALID_HANDLE_VALUE: + return False # No valid handles, nothing to do. + + # Get console info. + bg_color, native_ansi = bg_color_native_ansi(kernel32, stderr, stdout) + + # Set auto colors: + if auto_colors: + if bg_color in (112, 96, 240, 176, 224, 208, 160): + ANSICodeMapping.set_light_background() + else: + ANSICodeMapping.set_dark_background() + + # Don't replace streams if ANSI codes are natively supported. + if native_ansi: + return False + + # Reset on exit if requested. + if reset_atexit: + atexit.register(cls.disable) + + # Overwrite stream references. + if stderr != INVALID_HANDLE_VALUE: + sys.stderr.flush() + sys.stderr = WindowsStream(kernel32, stderr, sys.stderr) + if stdout != INVALID_HANDLE_VALUE: + sys.stdout.flush() + sys.stdout = WindowsStream(kernel32, stdout, sys.stdout) + + return True + + def __init__(self, auto_colors=False): + """Constructor.""" + self.auto_colors = auto_colors + + def __enter__(self): + """Context manager, enables colors on Windows.""" + self.enable(auto_colors=self.auto_colors) + + def __exit__(self, *_): + """Context manager, disabled colors on Windows.""" + self.disable() -- cgit v1.2.3