summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/spack/spack/color.py162
1 files changed, 162 insertions, 0 deletions
diff --git a/lib/spack/spack/color.py b/lib/spack/spack/color.py
new file mode 100644
index 0000000000..f84ba626c3
--- /dev/null
+++ b/lib/spack/spack/color.py
@@ -0,0 +1,162 @@
+"""
+This file implements an expression syntax, similar to printf, for adding
+ANSI colors to text.
+
+See colorize(), cwrite(), and cprint() for routines that can generate
+colored output.
+
+colorize will take a string and replace all color expressions with
+ANSI control codes. If the isatty keyword arg is set to False, then
+the color expressions will be converted to null strings, and the
+returned string will have no color.
+
+cwrite and cprint are equivalent to write() and print() calls in
+python, but they colorize their output. If the stream argument is
+not supplied, they write to sys.stdout.
+
+Here are some example color expressions:
+
+ @r Turn on red coloring
+ @R Turn on bright red coloring
+ @*b Turn on bold, blue text
+ @_B Turn on bright blue text with an underline
+ @. Revert to plain formatting
+ @*g{green} Print out 'green' in bold, green text, then reset to plain.
+ @*ggreen@. Print out 'green' in bold, green text, then reset to plain.
+
+The syntax consists of:
+
+ color-expr = '@' [style] color-code '{' text '}' | '@.' | '@@'
+ style = '*' | '_'
+ color-code = [krgybmcwKRGYBMCW]
+ text = .*
+
+'@' indicates the start of a color expression. It can be followed
+by an optional * or _ that indicates whether the font should be bold or
+underlined. If * or _ is not provided, the text will be plain. Then
+an optional color code is supplied. This can be [krgybmcw] or [KRGYBMCW],
+where the letters map to black(k), red(r), green(g), yellow(y), blue(b),
+magenta(m), cyan(c), and white(w). Lowercase letters denote normal ANSI
+colors and capital letters denote bright ANSI colors.
+
+Finally, the color expression can be followed by text enclosed in {}. If
+braces are present, only the text in braces is colored. If the braces are
+NOT present, then just the control codes to enable the color will be output.
+The console can be reset later to plain text with '@.'.
+
+To output an @, use '@@'. To output a } inside braces, use '}}'.
+"""
+import re
+import sys
+import spack.error
+
+class ColorParseError(spack.error.SpackError):
+ """Raised when a color format fails to parse."""
+ def __init__(self, message):
+ super(ColorParseError, self).__init__(message)
+
+# Text styles for ansi codes
+styles = {'*' : '1;%s', # bold
+ '_' : '4:%s', # underline
+ None : '0;%s' } # plain
+
+# Dim and bright ansi colors
+colors = {'k' : 30, 'K' : 90, # black
+ 'r' : 31, 'R' : 91, # red
+ 'g' : 32, 'G' : 92, # green
+ 'y' : 33, 'Y' : 93, # yellow
+ 'b' : 34, 'B' : 94, # blue
+ 'm' : 35, 'M' : 95, # magenta
+ 'c' : 36, 'C' : 96, # cyan
+ 'w' : 37, 'W' : 97 } # white
+
+# Regex to be used for color formatting
+color_re = r'@(?:@|\.|([*_])?([a-zA-Z])?(?:{((?:[^}]|}})*)})?)'
+
+
+class match_to_ansi(object):
+ def __init__(self, color=True):
+ self.color = color
+
+ def escape(self, s):
+ """Returns a TTY escape sequence for a color"""
+ if self.color:
+ return "\033[%sm" % s
+ else:
+ return ''
+
+ def __call__(self, match):
+ """Convert a match object generated by color_re into an ansi color code
+ This can be used as a handler in re.sub.
+ """
+ style, color, text = match.groups()
+ m = match.group(0)
+
+ if m == '@@':
+ return '@'
+ elif m == '@.':
+ return self.escape(0)
+ elif m == '@' or (style and not color):
+ raise ColorParseError("Incomplete color format: '%s'" % m)
+ elif color not in colors:
+ raise ColorParseError("invalid color specifier: '%s'" % color)
+
+ colored_text = ''
+ if text:
+ colored_text = text + self.escape(0)
+
+ if style == '*':
+ color_code = self.escape(styles[style] % colors[color])
+ elif style == '_':
+ color_code = self.escape(styles[style] % colors[color])
+ else:
+ color_code = self.escape(styles[style] % colors[color])
+
+ return color_code + colored_text
+
+
+def colorize(string, **kwargs):
+ """Take a string and replace all color expressions with ANSI control
+ codes. Return the resulting string.
+ If color=False is supplied, output will be plain text without
+ control codes, for output to non-console devices.
+ """
+ color = kwargs.get('color', True)
+ return re.sub(color_re, match_to_ansi(color), string)
+
+
+def cwrite(string, stream=sys.stdout, color=None):
+ """Replace all color expressions in string with ANSI control
+ codes and write the result to the stream. If color is
+ False, this will write plain text with o color. If True,
+ then it will always write colored output. If not supplied,
+ then it will be set based on stream.isatty().
+ """
+ if color == None:
+ color = stream.isatty()
+ stream.write(colorize(string, color=color))
+
+
+def cprint(string, stream=sys.stdout, color=None):
+ """Same as cwrite, but writes a trailing newline to the stream."""
+ cwrite(string + "\n", stream, color)
+
+
+class ColorStream(object):
+ def __init__(self, stream, color=None):
+ self.__class__ = type(stream.__class__.__name__,
+ (self.__class__, stream.__class__), {})
+ self.__dict__ = stream.__dict__
+ self.color = color
+ self.stream = stream
+
+ def write(self, string, **kwargs):
+ if kwargs.get('raw', False):
+ super(ColorStream, self).write(string)
+ else:
+ cwrite(string, self.stream, self.color)
+
+ def writelines(self, sequence, **kwargs):
+ raw = kwargs.get('raw', False)
+ for string in sequence:
+ self.write(string, self.color, raw=raw)