blob: b0bc7382c36a7e71197d3087aeb1eef515f82815 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
LIGHT = 010
ansi_CSI = '\033['
ansi_seq = re.compile(re.escape(ansi_CSI) + r'(?P<params>[\x20-\x3f]*)(?P<final>[\x40-\x7e])')
ansi_cmd_SGR = 'm' # set graphics rendition
color_defs = (
(000, 'k', 'black'),
(001, 'r', 'dark red'),
(002, 'g', 'dark green'),
(003, 'w', 'brown', 'dark yellow'),
(004, 'b', 'dark blue'),
(005, 'm', 'dark magenta', 'dark purple'),
(006, 'c', 'dark cyan'),
(007, 'n', 'light grey', 'light gray', 'neutral', 'dark white'),
(010, 'B', 'dark grey', 'dark gray', 'light black'),
(011, 'R', 'red', 'light red'),
(012, 'G', 'green', 'light green'),
(013, 'Y', 'yellow', 'light yellow'),
(014, 'B', 'blue', 'light blue'),
(015, 'M', 'magenta', 'purple', 'light magenta', 'light purple'),
(016, 'C', 'cyan', 'light cyan'),
(017, 'W', 'white', 'light white'),
)
colors_by_num = {}
colors_by_letter = {}
colors_by_name = {}
letters_by_num = {}
for colordef in color_defs:
colorcode = colordef[0]
colorletter = colordef[1]
colors_by_num[colorcode] = nameset = set(colordef[2:])
colors_by_letter[colorletter] = colorcode
letters_by_num[colorcode] = colorletter
for c in list(nameset):
# equivalent names without spaces
nameset.add(c.replace(' ', ''))
for c in list(nameset):
# with "bright" being an alias for "light"
nameset.add(c.replace('light', 'bright'))
for c in nameset:
colors_by_name[c] = colorcode
class ColoredChar:
def __init__(self, c, colorcode):
self.c = c
self._colorcode = colorcode
def colorcode(self):
return self._colorcode
def plain(self):
return self.c
def __getattr__(self, name):
return getattr(self.c, name)
def ansi_color(self):
clr = str(30 + (07 & self._colorcode))
if self._colorcode & 010:
clr = '1;' + clr
return clr
def __str__(self):
return "<%s '%r'>" % (self.__class__.__name__, self.colored_repr())
__repr__ = __str__
def colored_version(self):
return '%s0;%sm%s%s0m' % (ansi_CSI, self.ansi_color(), self.c, ansi_CSI)
def colored_repr(self):
if self.c == "'":
crepr = r"\'"
elif self.c == '"':
crepr = self.c
else:
crepr = repr(self.c)[1:-1]
return '%s0;%sm%s%s0m' % (ansi_CSI, self.ansi_color(), crepr, ansi_CSI)
def colortag(self):
return lookup_letter_from_code(self._colorcode)
class ColoredText:
def __init__(self, source=''):
if isinstance(source, basestring):
plain, colors = self.parse_ansi_colors(source)
self.chars = map(ColoredChar, plain, colors)
else:
# expected that source is an iterable of ColoredChars (or duck-typed as such)
self.chars = tuple(source)
def splitlines(self):
lines = [[]]
for c in self.chars:
if c.plain() == '\n':
lines.append([])
else:
lines[-1].append(c)
return [self.__class__(line) for line in lines]
def plain(self):
return ''.join([c.plain() for c in self.chars])
def __getitem__(self, index):
return self.chars[index]
@classmethod
def parse_ansi_colors(cls, source):
# note: strips all control sequences, even if not SGRs.
colors = []
plain = ''
last = 0
curclr = 0
for match in ansi_seq.finditer(source):
prevsegment = source[last:match.start()]
plain += prevsegment
colors.extend([curclr] * len(prevsegment))
if match.group('final') == ansi_cmd_SGR:
try:
curclr = cls.parse_sgr_param(curclr, match.group('params'))
except ValueError:
pass
last = match.end()
prevsegment = source[last:]
plain += prevsegment
colors.extend([curclr] * len(prevsegment))
return ''.join(plain), colors
@staticmethod
def parse_sgr_param(curclr, paramstr):
oldclr = curclr
args = map(int, paramstr.split(';'))
for a in args:
if a == 0:
curclr = lookup_colorcode('neutral')
elif a == 1:
curclr |= LIGHT
elif 30 <= a <= 37:
curclr = (curclr & LIGHT) | (a - 30)
else:
# not supported renditions here; ignore for now
pass
return curclr
def __repr__(self):
return "<%s '%s'>" % (self.__class__.__name__, ''.join([c.colored_repr() for c in self.chars]))
__str__ = __repr__
def __iter__(self):
return iter(self.chars)
def colored_version(self):
return ''.join([c.colored_version() for c in self.chars])
def colortags(self):
return ''.join([c.colortag() for c in self.chars])
def lookup_colorcode(name):
return colors_by_name[name]
def lookup_colorname(code):
return colors_by_num.get(code, 'Unknown-color-0%o' % code)
def lookup_colorletter(letter):
return colors_by_letter[letter]
def lookup_letter_from_code(code):
letr = letters_by_num.get(code, ' ')
if letr == 'n':
letr = ' '
return letr