Module termcolors
[hide private]
[frames] | no frames]

Source Code for Module termcolors

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Filename: termcolors.py
# Author: Mazhugin Aleksey
# Created: 2019/07/25
# ID: $Id: termcolors.py 63 2019-09-09 15:31:40Z Aleksey $
# URL: $URL: file:///D:/svn_/pybag/trunc/src/termcolors.py $
# Copyright: Copyright (c) 2019, Mazhugin Aleksey.
# License: BSD
#--------------------------------------------------------------------

"""
Simple colors for terminal output.

Main class is `TermColors`.

::

    ANSY 4 bit color for unix.
    ==========================
     \033[FG;BGm      \x1bFG;BGm      \e[FG;BGm
    
     30-37 (30 + n) == text         90-97==bright text
     40-47 (40 + n) == background   100-107==bright background
    
     n     color
     0   black
     1   red
     2   green
     3   yellow
     4   blue
     5   magenta
     6   cyan
     7   white
    
    
    \033[31m == set red text
    \033[31;42m == set red text and green background
    
    
    ESC==\\033
    
    ESC [ 0 m       # reset all (colors and brightness)
    ESC [ 1 m       # bright
    ESC [ 2 m       # dim (looks same as normal brightness)
    ESC [ 22 m      # normal brightness
    
    # FOREGROUND:
    ESC [ 30 m      # black
    ESC [ 31 m      # red
    ESC [ 32 m      # green
    ESC [ 33 m      # yellow
    ESC [ 34 m      # blue
    ESC [ 35 m      # magenta
    ESC [ 36 m      # cyan
    ESC [ 37 m      # white
    ESC [ 39 m      # reset
    
    # BACKGROUND
    ESC [ 40 m      # black
    ESC [ 41 m      # red
    ESC [ 42 m      # green
    ESC [ 43 m      # yellow
    ESC [ 44 m      # blue
    ESC [ 45 m      # magenta
    ESC [ 46 m      # cyan
    ESC [ 47 m      # white
    ESC [ 49 m      # reset
    
    # cursor positioning
    ESC [ y;x H     # position cursor at x across, y down
    ESC [ y;x f     # position cursor at x across, y down
    ESC [ n A       # move cursor n lines up
    ESC [ n B       # move cursor n lines down
    ESC [ n C       # move cursor n characters forward
    ESC [ n D       # move cursor n characters backward
    
    # clear the screen
    ESC [ mode J    # clear the screen
    
    # clear the line
    ESC [ mode K    # clear the line
    
    
    
    
    
    
    Windows colors.
    ===============
    
      0-7 dark
      8-15 bright
    
     text         background
     0  black       0
     1  blue        16
     2  green       32
     3  cyan        48      (blue+green)
     4  red         64
     5  magenta     80      (blue+red)
     6  yellow      96      (green+red)
     7  white       112     (blue+green+red)
    
    FOREGROUND_BLUE = 0x01,
    FOREGROUND_GREEN = 0x02,
    FOREGROUND_RED = 0x04,
    FOREGROUND_INTENSITY = 0x08,
    BACKGROUND_BLUE = 0x10,
    BACKGROUND_GREEN = 0x20,
    BACKGROUND_RED = 0x40
    BACKGROUND_INTENSITY = 0x80

"""

#----------- Implementation --------------

__docformat__ = "restructuredtext" # "epytext"


import ctypes
import platform
import sys
import re
import atexit
import io


_OSWIN = platform.system().lower().startswith('windows')
"""Is os is Windows."""


class TermColors(object):
    """
    Base class for terminal colorization.
    
    Use `getDefault` for get default terminal colorization class for you OS.
    """
    
    C_BLACK = 0
    C_BLUE = 1
    C_GREEN = 2
    C_CYAN = 3
    C_RED = 4
    C_MAGENTA = 5
    C_YELLOW = 6
    C_WHITE = 7
    
    C_RE_PATT = "<([rgbkcmyw_RGBKCMYW]{1,2})>((.|\n)*?)"
    """Pattern for search tags.[1]-color string, [2]-string for colorize."""
    
    @staticmethod
    def getDefault(stream=None):
        """
        Return TermColors instance with default color scheme (initialized
        for current terminal).
        """
        r = None
        if _OSWIN:
            r = TermColorsWindows(stream)
        else:
            r = TermColorsUnix(stream)
        return r
    
    
    def __init__(self, stream=None):
        """
        ``stream`` - Stream for out colors. In windows not used.
        
        Default is ``None`` = use ``sys.stdout``.
        """
        
        self._stream = stream
        """stream for out colors."""
        
        self._color_table = {}
        """Color table. Index is a color constants from class, C_xxxx.
        """
        
        self._isbg = False
        """If true then now set bg colors."""
        
        self._txtbright = None
        """Use text bright. If None - use last or default."""
        
        self._bgbright = None
        """Use text bright. If None - use last or default."""
        
        self._ctxt = None
        """Text color. If None - use last or default."""
        
        self._cbg = None
        """Background color. If None - use last or default."""
        
        self._last_ctxt = None
        """Previous text color."""
        
        self._last_cbg = None
        """Previous bg color."""
        
        self._last_bgbright = None
        """Previous bg brightness."""
        
        self._last_txtbright = None
        """Previous text brightness."""
        
        self._reset_value = None
        """Color color value or None for use default."""
        
        self._recomp = re.compile(self.C_RE_PATT)
        """Compiled pattern for RE."""
        
        self._tag_table = {}
        """
        Table for match colr strings to color index.
        Store 2tuple: (color,bright).
        """
        
        self._tag_table["r"] = (self.C_RED, False)
        self._tag_table["g"] = (self.C_GREEN, False)
        self._tag_table["b"] = (self.C_BLUE, False)
        self._tag_table["k"] = (self.C_BLACK, False)
        self._tag_table["c"] = (self.C_CYAN, False)
        self._tag_table["m"] = (self.C_MAGENTA, False)
        self._tag_table["y"] = (self.C_YELLOW, False)
        self._tag_table["w"] = (self.C_WHITE, False)
        self._tag_table["_"] = (None, None)
        self._tag_table["R"] = (self.C_RED, True)
        self._tag_table["G"] = (self.C_GREEN, True)
        self._tag_table["B"] = (self.C_BLUE, True)
        self._tag_table["K"] = (self.C_BLACK, True)
        self._tag_table["C"] = (self.C_CYAN, True)
        self._tag_table["M"] = (self.C_MAGENTA, True)
        self._tag_table["Y"] = (self.C_YELLOW, True)
        self._tag_table["W"] = (self.C_WHITE, True)
        
    
    
    def _calcColor(self, text_color, text_bright, bg_color, bg_bright):
        """
        Calculate color value.
        
        ``text_color`` - Text color, use class color constant C_xxxxx.
            If color argument is ``None`` use last value or not use this component if no last.
            or use default value. See implementation detail.
            
        ``bg_color`` - Background color, use class color constant C_xxxxx.
            If color argument is ``None`` use last value or not use this component if no last.
            or use default value. See implementation detail.
        
        ``text_bright`` - Text brightness,``True`` for bright, ``False`` for dark color.
            If color argument is ``None`` use last value or not use this component if no last.
            or use default value. See implementation detail.
            
        ``bg_bright`` - Background brightness,``True`` for bright, ``False`` for dark color.
            If color argument is ``None`` use last value or not use this component if no last.
            or use default value. See implementation detail.
        
        Return value for set color. Returned value must be right for use without check.
        """
    
    
    def _setColor(self, value):
        """
        Set specified color from value.
        
        Value must be right. (Not check more).
        """
        
    
    def _calcResetColor(self):
        """
        Calculate value for reset colors to default.
        """
        
    
    def set_reset_color(self):
        """
        Save specified color to use for reset.
        """
        v = self._decode_color()
        self._reset_value = v
        self._clear()
        
        
    def black(self):
        """
        Set black color. Return self instance.
        """
        if self._isbg:
            self._cbg = self.C_BLACK
        else:
            self._ctxt = self.C_BLACK
        return self
    
    def blue(self):
        """
        Set blue color. Return self instance.
        """
        if self._isbg:
            self._cbg = self.C_BLUE
        else:
            self._ctxt = self.C_BLUE
        return self
    
    def green(self):
        """
        Set green color. Return self instance.
        """
        if self._isbg:
            self._cbg = self.C_GREEN
        else:
            self._ctxt = self.C_GREEN
        return self
        
    def cyan(self):
        """
        Set cyan color. Return self instance.
        """
        if self._isbg:
            self._cbg = self.C_CYAN
        else:
            self._ctxt = self.C_CYAN
        return self
        
    def red(self):
        """
        Set red color. Return self instance.
        """
        if self._isbg:
            self._cbg = self.C_RED
        else:
            self._ctxt = self.C_RED
        return self
        
    def magenta(self):
        """
        Set magenta color. Return self instance.
        """
        if self._isbg:
            self._cbg = self.C_MAGENTA
        else:
            self._ctxt = self.C_MAGENTA
        return self
        
    def yellow(self):
        """
        Set yellow color. Return self instance.
        """
        if self._isbg:
            self._cbg = self.C_YELLOW
        else:
            self._ctxt = self.C_YELLOW
        return self
        
    def white(self):
        """
        Set white color. Return self instance.
        """
        if self._isbg:
            self._cbg = self.C_WHITE
        else:
            self._ctxt = self.C_WHITE
        return self
        
    def bright(self):
        """
        Set bright color. Return self instance.
        """
        if self._isbg:
            self._bgbright = True
        else:
            self._txtbright = True
        return self
    
    def dark(self):
        """
        Set dark color. Return self instance.
        """
        if self._isbg:
            self._bgbright = False
        else:
            self._txtbright = False
        return self
    
    def bg(self):
        """
        Go to enter background color. Return self instance.
        """
        self._isbg = True
        return self
        
    def txt(self):
        """
        Go to enter text color(this is default). Return self instance.
        """
        self._isbg = False
        return self
    
    def set(self):
        """
        Set (apply) all specified colors.
        """
        v = self._decode_color()
        self._setColor(v)
        self._clear()
    
    def _decode_color(self):
        """
        Decode color to value from specified colors.
        """
        t = self._ctxt is None and self._last_ctxt or self._ctxt
        bg = self._cbg is None and self._last_cbg or self._cbg
        tbr = self._txtbright is None and self._last_txtbright or self._txtbright
        bgbr = self._bgbright is None and self._last_bgbright or self._bgbright
        v = self._calcColor(t, tbr, bg, bgbr)
        return v
    
    
    def _clear(self, save_last=True):
        """
        Clear specified colors to ``None`` and save to last (if not ``None``).
        
        ``save_last`` - if true? then save to last value.
        """
        if save_last:
            if self._ctxt is not None:
                self._last_ctxt = self._ctxt
            if self._cbg is not None:
                self._last_cbg = self._cbg
            if self._txtbright is not None:
                self._last_txtbright = self._txtbright
            if self._bgbright is not None:
                self._last_bgbright = self._bgbright
        
        self._ctxt=None
        self._cbg=None
        self._isbg=False
        self._txtbright = None
        self._bgbright=None
    
    def reset(self):
        """
        Try reset colors to default.
        """
        if self._reset_value is None:
            self._reset_value = self._calcResetColor()
        
        self._setColor(self._reset_value)
        self._clear(False) # do not save reset to last
    
    
    def write(self, s, skip=False, stream=None):
        """
        Colorized ouput string to ``stream``.
        
        If **stream** ``None`` then use initialized `self._stream`,
        otherwise use ``sys.stdout``.
        
        **skip** - If True then remove color tags from string and out it
        without colorization.
        
        Sintax is next (html-like): ``some text``, where first C is
        a text color, second C is a background color.
        
        ``color`` is next code:
            
            r - red,
            g - green,
            b - blue,
            k - black,
            c - cyan,
            m - magenta,
            y - yellow,
            w - white,
            _ - not use (use previous or default).
            
        Lowcase letter mean dark color, Uppercase mean bright color.
        
        For example:
        
            ``Bright red on default background.``
            
            ``Dark white on black``
            
        """
        if stream is None:
            stream = self._stream
        if stream is None:
            stream = sys.stdout
        pos = 0
        m = self._recomp.search(s, pos)
        while m is not None:
            start = m.start()
            if pos!= start:
                stream.write(s[pos:start])
            pos = m.end()
            cs = m.group(1) # Color code string
            t = m.group(2) # string for colorize
            if not skip:
                ct = cs[0] # text color
                cb = len(cs)>1 and cs[1] or "_" # BG color
                #print "[ct="+str(ct)+"]\n"
                #print "[cb="+str(cb)+"]\n"
                self._ctxt, self._txtbright = self._tag_table[ct]
                self._cbg, self._bgbright = self._tag_table[cb]
                v = self._decode_color()
                #print "[v="+str(v)+"]\n"
                self._setColor(v)
                stream.write(t)
                v = self._calcResetColor()
                self._setColor(v)
            else:
                stream.write(t)
            
            m = self._recomp.search(s, pos)
        #while end
        if pos<=len(s):
            stream.write(s[pos:])
        
    
    
    def writeln(self, s="", skip=False, stream=None):
        """
        As `write` but add new line at end.
        """
        
        self.write(s+"\n", skip, stream)
    
    @classmethod
    def skipTags(cls, s):
        """
        Remove colorize tags from string.
        ``s`` must be unicode.
        Return string.
        """
        
        stream = io.StringIO()
        _recomp = re.compile(cls.C_RE_PATT)
        s = unicode(s)
        pos = 0
        m = _recomp.search(s, pos)
        while m is not None:
            start = m.start()
            if pos!= start:
                stream.write(s[pos:start])
            pos = m.end()
            t = m.group(2) # string for colorize
            stream.write(t)
            
            m = _recomp.search(s, pos)
        #while end
        if pos<=len(s):
            stream.write(s[pos:])
        o = stream.getvalue()
        stream.close()
        return o
    

class TermColorsWindows(TermColors):
    """
    Windows color scheme.
    """
    
    
    def __init__(self, stream=None):
        
        super(TermColorsWindows, self).__init__(stream)
        
        self._color_table[self.C_BLACK]=0;
        self._color_table[self.C_BLUE]=1;
        self._color_table[self.C_GREEN]=2;
        self._color_table[self.C_CYAN]=3;
        self._color_table[self.C_RED]=4;
        self._color_table[self.C_MAGENTA]=5;
        self._color_table[self.C_YELLOW]=6;
        self._color_table[self.C_WHITE]=7;
        
        #self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes
        #self._reset_value = ctypes.windll.Kernel32.GetConsoleScreenBufferInfo(STDOUT).wAttributes
    
    
    def _calcColor(self, text_color, text_bright, bg_color, bg_bright):
        
        c = (type(text_color) is int and self._color_table[abs(text_color%8)] or 0)
        c = c + (text_bright and 8 or 0)
        c = c + (type(bg_color) is int and self._color_table[abs(bg_color%8)] or 0)*16
        c = c + (bg_bright and 128 or 0)
        return c
    
    def _setColor(self, value):
        """
        """
        
        ctypes.windll.Kernel32.GetStdHandle.restype = ctypes.c_ulong
        h = ctypes.windll.Kernel32.GetStdHandle(ctypes.c_ulong(0xfffffff5))
        ctypes.windll.Kernel32.SetConsoleTextAttribute(h, value)
        
    
    def _calcResetColor(self):
        """
        Calculate value for reset colors to default.
        
        In windows set dark white on black bacground.
        """
        # x-XXX Reset defaultt colors. In Win this is hard-white on black.
        return self._calcColor(self.C_WHITE, 0, self.C_BLACK, 0)
    



class TermColorsUnix(TermColors):
    """
    Unix color scheme.
    """
    
    
    def __init__(self, stream=None):
        
        stream = stream and stream or sys.stdout
        
        super(TermColorsUnix, self).__init__(stream)
        
        self._color_table[self.C_BLACK]=0;
        self._color_table[self.C_BLUE]=4;
        self._color_table[self.C_GREEN]=2;
        self._color_table[self.C_CYAN]=6;
        self._color_table[self.C_RED]=1;
        self._color_table[self.C_MAGENTA]=5;
        self._color_table[self.C_YELLOW]=3;
        self._color_table[self.C_WHITE]=7;
    
    
    def _calcColor(self, text_color, text_bright, bg_color, bg_bright):
        
        
        ct = 30 + (type(text_color) is int and self._color_table[abs(text_color%8)] or 0)
        ct = ct + (text_bright and 60 or 0)
        ct = text_color is None and "" or str(ct)
        cb = 40 + (type(bg_color) is int and self._color_table[abs(bg_color%8)] or 0)
        cb = cb + (bg_bright and 60 or 0)
        cb = bg_color is None and "" or str(cb)
        c = ct + (len(ct) and ";" or "") + cb
        c = len(c) and "\033[" + c + "m"
        return c
    
    def _setColor(self, value):
        """
        """
        
        self._stream.write(value)
        
    
    def _calcResetColor(self):
        """
        Calculate value for reset colors to default.
        
        """
        return "\033[0m"




class StreamProxy(object):
    """
    Wraps a object (such as stdout), acting as a transparent proxy for all
    attribute access.
    """
    
    
    
    def wrap_stdout(self):
        """
        Wrap sys.stdout to this class.
        
        Register atexit() for restore sys.stdout.
        Assign stdout to stream.
        
        If already wrapped, then raise RuntimeError.
        """
        if self._stdout_orig:
            raise RuntimeError("stdout already wrapped by this instance.")
        self._stdout_orig = sys.stdout
        self._stream = sys.stdout
        sys.stdout = self
        atexit.register(self.restore_stdout)
    
    def restore_stdout(self):
        """
        Restore sys.stdout.
        
        Also set stream to None.
        """
        if self._stdout_orig:
            sys.stdout = self._stdout_orig
            self._stream = None
            self._stdout_orig = None
    
    def __init__(self, stream=None, skip=False, term_colors=None):
        """
        :Parameters:
            stream
                stream for wrapping.
                If ``stream`` is None then use sys.stdout.
            skip
                Skip tags (and do not colorize) if ``True``.
            term_colors
                TermColors` instance.
            
        """
        
        self._stdout_orig = None
        """Original sys.stdout"""
        
        stream = stream is None and sys.stdout or stream
        
        if not hasattr(stream, "__enter__") or not hasattr(stream, "__enter__"):
            #raise TypeError("wrapped must have context methods (__enter__, __exit__).")
            pass
        
        self._skip = skip
        self._stream = stream
        self.term_colors = (term_colors is not None) and term_colors or TermColors.getDefault()

    def __getattr__(self, name):
        return getattr(self._stream, name)

        
    def __enter__(self, *args, **kwargs):
        # special method lookup bypasses __getattr__/__getattribute__, see
        # https://stackoverflow.com/questions/12632894/why-doesnt-getattr-work-with-exit
        # thus, contextlib magic methods are not proxied via __getattr__
        return self._stream.__enter__(*args, **kwargs)

    def __exit__(self, *args, **kwargs):
        return self._stream.__exit__(*args, **kwargs)
    
    @property
    def closed(self):
        stream = self._stream
        try:
            return stream.closed
        except AttributeError:
            return True
    
    def write(self, s):
        self.term_colors.write(s, self._skip, self._stream)
        
    def setSkipTags(self, skip=False):
        """
        Set skip tag. Skip tags at out and do not colorize.
        """
        self._skip = skip
    
    





def _debug():
    if _OSWIN:
        print "Windows"
        w = TermColorsWindows()
        w.reset()
        w.red().set()
        print "dark red on black"
        w.bg().bright().set()
        print "dark red on dark white"
        w.reset()
        print "reset colors"
        w.writeln("test TAGS Gw  test", False)
        w.writeln("test <_>TAGS _  test", False)
        w.writeln("test TAGS rW  test", False)
        w.writeln("test TAGS skip rW  test", True)
        print "end"
        print
    
    print "Unix"
    w = TermColorsUnix()
    w.reset()
    w.red().set()
    if _OSWIN: print
    print "dark red on black"
    w.bg().bright().set()
    if _OSWIN: print
    print "dark red on dark white"
    w.reset()
    if _OSWIN: print
    print "reset colors"
    w.writeln("test Gw TAGS Gw  test", False)
    w.writeln("test _ <_>TAGS _  test", False)
    w.writeln("test rW TAGS rW  test", False)
    w.writeln("test rW TAGS skip rW  test", True)
    print "end"
    print
    
    
    print "WRAP STDOUT"
    w = StreamProxy()
    w.wrap_stdout()
    w.term_colors.reset()
    print " SYS.STDOUT IS WRAPPED"
    w.setSkipTags(True)
    print " SKIP TAGS"
    w.restore_stdout()
    print " STDOUT IS RESTORED"
    
    s = TermColors.skipTags(u" TRY CLASSMETHOD TAGS")
    print s
    assert s==" TRY CLASSMETHOD TAGS"
# _debug end



if __name__== "__main__":
    _debug()