summaryrefslogtreecommitdiffstats
path: root/colorclass
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2022-09-16 09:10:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2022-09-16 09:10:14 +0000
commit896739353a613f23c007d9acaa2809010a522a37 (patch)
treecadd194400c11d0a5caaeda7d9d771602eb1ba40 /colorclass
parentInitial commit. (diff)
downloadcolorclass-896739353a613f23c007d9acaa2809010a522a37.tar.xz
colorclass-896739353a613f23c007d9acaa2809010a522a37.zip
Adding upstream version 2.2.0.upstream/2.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'colorclass')
-rw-r--r--colorclass/__init__.py38
-rw-r--r--colorclass/__main__.py33
-rw-r--r--colorclass/codes.py229
-rw-r--r--colorclass/color.py220
-rw-r--r--colorclass/core.py342
-rw-r--r--colorclass/parse.py96
-rw-r--r--colorclass/search.py49
-rw-r--r--colorclass/toggles.py42
-rw-r--r--colorclass/windows.py388
9 files changed, 1437 insertions, 0 deletions
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()