From 47238531288fed80deca6f5c42a9ee095a21514d Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Mon, 22 Dec 2014 11:24:45 -0700 Subject: [PATCH] add full support for ANSI SGR escape sequences This is for adding color to the text output salt emits in various contexts including support for extended colors (256 and 256^3) and a few other common text modifiers like bold, italic, underline, etc. The code is put in salt/textformat.py (rather than salt/utils/textformat.py) in order to allow for logging to import TextFormat without loading all of salt/utils/__init__.py and causing logging failures. --- salt/textformat.py | 190 +++++++++++++++++++++++++++++++++++++++++ salt/utils/__init__.py | 54 +++++++----- 2 files changed, 223 insertions(+), 21 deletions(-) create mode 100644 salt/textformat.py diff --git a/salt/textformat.py b/salt/textformat.py new file mode 100644 index 0000000000..af1e0acb02 --- /dev/null +++ b/salt/textformat.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +''' +ANSI escape code utilities, see +http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf +''' + +graph_prefix = '\x1b[' +graph_suffix = 'm' +codes = { + 'reset': '0', + + 'bold': '1', + 'faint': '2', + 'italic': '3', + 'underline': '4', + 'blink': '5', + 'slow_blink': '5', + 'fast_blink': '6', + 'inverse': '7', + 'conceal': '8', + 'strike': '9', + + 'primary_font': '10', + 'reset_font': '10', + 'font_0': '10', + 'font_1': '11', + 'font_2': '12', + 'font_3': '13', + 'font_4': '14', + 'font_5': '15', + 'font_6': '16', + 'font_7': '17', + 'font_8': '18', + 'font_9': '19', + 'fraktur': '20', + + 'double_underline': '21', + 'end_bold': '21', + 'normal_intensity': '22', + 'end_italic': '23', + 'end_fraktur': '23', + 'end_underline': '24', # single or double + 'end_blink': '25', + 'end_inverse': '27', + 'end_conceal': '28', + 'end_strike': '29', + + 'black': '30', + 'red': '31', + 'green': '32', + 'yellow': '33', + 'blue': '34', + 'magenta': '35', + 'cyan': '36', + 'white': '37', + 'extended': '38', + 'default': '39', + + 'fg_black': '30', + 'fg_red': '31', + 'fg_green': '32', + 'fg_yellow': '33', + 'fg_blue': '34', + 'fg_magenta': '35', + 'fg_cyan': '36', + 'fg_white': '37', + 'fg_extended': '38', + 'fg_default': '39', + + 'bg_black': '40', + 'bg_red': '41', + 'bg_green': '42', + 'bg_yellow': '44', + 'bg_blue': '44', + 'bg_magenta': '45', + 'bg_cyan': '46', + 'bg_white': '47', + 'bg_extended': '48', + 'bg_default': '49', + + 'frame': '51', + 'encircle': '52', + 'overline': '53', + 'end_frame': '54', + 'end_encircle': '54', + 'end_overline': '55', + + 'ideogram_underline': '60', + 'right_line': '60', + 'ideogram_double_underline': '61', + 'right_double_line': '61', + 'ideogram_overline': '62', + 'left_line': '62', + 'ideogram_double_overline': '63', + 'left_double_line': '63', + 'ideogram_stress': '64', + 'reset_ideogram': '65' +} + + +class TextFormat(object): + ''' + ANSI Select Graphic Rendition (SGR) code escape sequence. + ''' + + def __init__(self, *attrs, **kwargs): + ''' + :param attrs: are the attribute names of any format codes in `codes` + + :param kwargs: may contain + + `x`, an integer in the range [0-255] that selects the corresponding + color from the extended ANSI 256 color space for foreground text + + `rgb`, an iterable of 3 integers in the range [0-255] that select the + corresponding colors from the extended ANSI 256^3 color space for + foreground text + + `bg_x`, an integer in the range [0-255] that selects the corresponding + color from the extended ANSI 256 color space for background text + + `bg_rgb`, an iterable of 3 integers in the range [0-255] that select + the corresponding colors from the extended ANSI 256^3 color space for + background text + + `reset`, prepend reset SGR code to sequence (default `True`) + + Examples: + + .. code-block:: python + + red_underlined = TextFormat('red', 'underline') + + nuanced_text = TextFormat(x=29, bg_x=71) + + magenta_on_green = TextFormat('magenta', 'bg_green') + print( + '{0}Can you read this?{1}' + ).format(magenta_on_green, TextFormat('reset')) + ''' + self.codes = [codes[attr.lower()] for attr in attrs if isinstance(attr, str)] + + if kwargs.get('reset', True): + self.codes[:0] = [codes['reset']] + + def qualify_int(i): + if isinstance(i, int): + return i%256 # set i to unit element of its equivalence class + def qualify_triple_int(t): + if isinstance(t, (list, tuple)) and len(t) == 3: + return qualify_int(i) + + if kwargs.get('x', None) is not None: + self.codes.extend((codes['extended'], '5', qualify_int(kwargs['x']))) + elif kwargs.get('rgb', None) is not None: + self.codes.extend((codes['extended'], '2')) + self.codes.extend(*qualify_triple_int(kwargs['rgb'])) + + if kwargs.get('bg_x', None) is not None: + self.codes.extend((codes['extended'], '5', qualify_int(kwargs['bg_x']))) + elif kwargs.get('bg_rgb', None) is not None: + self.codes.extend((codes['extended'], '2')) + self.codes.extend(*qualify_triple_int(kwargs['bg_rgb'])) + + self.sequence = '{p}{c}{s}'.format( + p=graph_prefix, + c=';'.join(self.codes), + s=graph_suffix) + + + def __call__(self, text, reset=True): + ''' + Format :param text: by prefixing `self.sequence` and suffixing the + reset sequence if :param reset: is `True`. + + Examples: + + .. code-block:: python + + green_blink_text = TextFormat('blink', 'green') + 'The answer is: {0}'.format(green_blink_text(42)) + ''' + end = TextFormat('reset') if reset else '' + return '{s}{t}{e}'.format(s=self.sequence, t=text, e=end) + + def __str__(self): + return self.sequence + + def __repr__(self): + return self.sequence diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 2c9b23ab91..d9b48e985e 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -98,6 +98,7 @@ import salt.defaults.exitcodes import salt.log import salt.version from salt.utils.decorators import memoize as real_memoize +from salt.textformat import TextFormat from salt.exceptions import ( CommandExecutionError, SaltClientError, CommandNotFoundError, SaltSystemExit, @@ -127,6 +128,7 @@ DEFAULT_COLOR = '\033[00m' RED_BOLD = '\033[01;31m' ENDC = '\033[0m' + log = logging.getLogger(__name__) _empty = object() @@ -154,29 +156,36 @@ def is_empty(filename): def get_colors(use=True): ''' - Return the colors as an easy to use dict, pass False to return the colors - as empty strings so that they will not be applied + Return the colors as an easy to use dict. Pass `False` to deactivate all + colors by setting them to empty strings. Pass a string containing only the + name of a single color to be used in place of all colors. Examples: + + .. code-block:: python + + colors = get_colors() # enable all colors + no_colors = get_colors(False) # disable all colors + red_colors = get_colors('RED') # set all colors to red ''' colors = { - 'BLACK': '\033[0;30m', - 'DARK_GRAY': '\033[1;30m', - 'LIGHT_GRAY': '\033[0;37m', - 'BLUE': '\033[0;34m', - 'LIGHT_BLUE': '\033[1;34m', - 'GREEN': '\033[0;32m', - 'LIGHT_GREEN': '\033[1;32m', - 'CYAN': '\033[0;36m', - 'LIGHT_CYAN': '\033[1;36m', - 'RED': '\033[0;31m', - 'LIGHT_RED': '\033[1;31m', - 'PURPLE': '\033[0;35m', - 'LIGHT_PURPLE': '\033[1;35m', - 'BROWN': '\033[0;33m', - 'YELLOW': '\033[1;33m', - 'WHITE': '\033[1;37m', - 'DEFAULT_COLOR': '\033[00m', - 'RED_BOLD': '\033[01;31m', - 'ENDC': '\033[0m', + 'BLACK': TextFormat('black'), + 'DARK_GRAY': TextFormat('bold', 'black'), + 'LIGHT_GRAY': TextFormat('white'), + 'BLUE': TextFormat('blue'), + 'LIGHT_BLUE': TextFormat('bold', 'blue'), + 'GREEN': TextFormat('green'), + 'LIGHT_GREEN': TextFormat('bold', 'green'), + 'CYAN': TextFormat('cyan'), + 'LIGHT_CYAN': TextFormat('bold', 'cyan'), + 'RED': TextFormat('red'), + 'LIGHT_RED': TextFormat('bold', 'red'), + 'RED_BOLD': TextFormat('bold', 'red'), + 'PURPLE': TextFormat('magenta'), + 'LIGHT_PURPLE': TextFormat('bold', 'magenta'), + 'BROWN': TextFormat('yellow'), + 'YELLOW': TextFormat('bold', 'yellow'), + 'WHITE': TextFormat('bold', 'white'), + 'DEFAULT_COLOR': TextFormat('default'), + 'ENDC': TextFormat('reset'), } if not use: @@ -186,6 +195,9 @@ def get_colors(use=True): # Try to set all of the colors to the passed color if use in colors: for color in colors: + # except for color reset + if color == 'ENDC': + continue colors[color] = colors[use] return colors