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.
This commit is contained in:
Justin Findlay 2014-12-22 11:24:45 -07:00
parent be85f50178
commit 4723853128
2 changed files with 223 additions and 21 deletions

190
salt/textformat.py Normal file
View File

@ -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

View File

@ -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