# 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 = 0o10 ansi_CSI = '\x1b[' ansi_seq = re.compile(re.escape(ansi_CSI) + r'(?P[\x20-\x3f]*)(?P[\x40-\x7e])') ansi_cmd_SGR = 'm' # set graphics rendition color_defs = ( (000, 'k', 'black'), (0o01, 'r', 'dark red'), (0o02, 'g', 'dark green'), (0o03, 'w', 'brown', 'dark yellow'), (0o04, 'b', 'dark blue'), (0o05, 'm', 'dark magenta', 'dark purple'), (0o06, 'c', 'dark cyan'), (0o07, 'n', 'light grey', 'light gray', 'neutral', 'dark white'), (0o10, 'B', 'dark grey', 'dark gray', 'light black'), (0o11, 'R', 'red', 'light red'), (0o12, 'G', 'green', 'light green'), (0o13, 'Y', 'yellow', 'light yellow'), (0o14, 'B', 'blue', 'light blue'), (0o15, 'M', 'magenta', 'purple', 'light magenta', 'light purple'), (0o16, 'C', 'cyan', 'light cyan'), (0o17, '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(object): 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 + (0o7 & self._colorcode)) if self._colorcode & 0o10: 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(object): def __init__(self, source=''): if isinstance(source, str): plain, colors = self.parse_ansi_colors(source) self.chars = list(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): args = list(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