| 1 | # Copyright (c) Twisted Matrix Laboratories. |
|---|
| 2 | # See LICENSE for details. |
|---|
| 3 | |
|---|
| 4 | # |
|---|
| 5 | """Module to parse ANSI escape sequences |
|---|
| 6 | |
|---|
| 7 | Maintainer: Jean-Paul Calderone |
|---|
| 8 | """ |
|---|
| 9 | |
|---|
| 10 | import string |
|---|
| 11 | |
|---|
| 12 | # Twisted imports |
|---|
| 13 | from twisted.python import log |
|---|
| 14 | |
|---|
| 15 | class ColorText: |
|---|
| 16 | """ |
|---|
| 17 | Represents an element of text along with the texts colors and |
|---|
| 18 | additional attributes. |
|---|
| 19 | """ |
|---|
| 20 | |
|---|
| 21 | # The colors to use |
|---|
| 22 | COLORS = ('b', 'r', 'g', 'y', 'l', 'm', 'c', 'w') |
|---|
| 23 | BOLD_COLORS = tuple([x.upper() for x in COLORS]) |
|---|
| 24 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(len(COLORS)) |
|---|
| 25 | |
|---|
| 26 | # Color names |
|---|
| 27 | COLOR_NAMES = ( |
|---|
| 28 | 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' |
|---|
| 29 | ) |
|---|
| 30 | |
|---|
| 31 | def __init__(self, text, fg, bg, display, bold, underline, flash, reverse): |
|---|
| 32 | self.text, self.fg, self.bg = text, fg, bg |
|---|
| 33 | self.display = display |
|---|
| 34 | self.bold = bold |
|---|
| 35 | self.underline = underline |
|---|
| 36 | self.flash = flash |
|---|
| 37 | self.reverse = reverse |
|---|
| 38 | if self.reverse: |
|---|
| 39 | self.fg, self.bg = self.bg, self.fg |
|---|
| 40 | |
|---|
| 41 | |
|---|
| 42 | class AnsiParser: |
|---|
| 43 | """ |
|---|
| 44 | Parser class for ANSI codes. |
|---|
| 45 | """ |
|---|
| 46 | |
|---|
| 47 | # Terminators for cursor movement ansi controls - unsupported |
|---|
| 48 | CURSOR_SET = ('H', 'f', 'A', 'B', 'C', 'D', 'R', 's', 'u', 'd','G') |
|---|
| 49 | |
|---|
| 50 | # Terminators for erasure ansi controls - unsupported |
|---|
| 51 | ERASE_SET = ('J', 'K', 'P') |
|---|
| 52 | |
|---|
| 53 | # Terminators for mode change ansi controls - unsupported |
|---|
| 54 | MODE_SET = ('h', 'l') |
|---|
| 55 | |
|---|
| 56 | # Terminators for keyboard assignment ansi controls - unsupported |
|---|
| 57 | ASSIGN_SET = ('p',) |
|---|
| 58 | |
|---|
| 59 | # Terminators for color change ansi controls - supported |
|---|
| 60 | COLOR_SET = ('m',) |
|---|
| 61 | |
|---|
| 62 | SETS = (CURSOR_SET, ERASE_SET, MODE_SET, ASSIGN_SET, COLOR_SET) |
|---|
| 63 | |
|---|
| 64 | def __init__(self, defaultFG, defaultBG): |
|---|
| 65 | self.defaultFG, self.defaultBG = defaultFG, defaultBG |
|---|
| 66 | self.currentFG, self.currentBG = self.defaultFG, self.defaultBG |
|---|
| 67 | self.bold, self.flash, self.underline, self.reverse = 0, 0, 0, 0 |
|---|
| 68 | self.display = 1 |
|---|
| 69 | self.prepend = '' |
|---|
| 70 | |
|---|
| 71 | |
|---|
| 72 | def stripEscapes(self, string): |
|---|
| 73 | """ |
|---|
| 74 | Remove all ANSI color escapes from the given string. |
|---|
| 75 | """ |
|---|
| 76 | result = '' |
|---|
| 77 | show = 1 |
|---|
| 78 | i = 0 |
|---|
| 79 | L = len(string) |
|---|
| 80 | while i < L: |
|---|
| 81 | if show == 0 and string[i] in _sets: |
|---|
| 82 | show = 1 |
|---|
| 83 | elif show: |
|---|
| 84 | n = string.find('\x1B', i) |
|---|
| 85 | if n == -1: |
|---|
| 86 | return result + string[i:] |
|---|
| 87 | else: |
|---|
| 88 | result = result + string[i:n] |
|---|
| 89 | i = n |
|---|
| 90 | show = 0 |
|---|
| 91 | i = i + 1 |
|---|
| 92 | return result |
|---|
| 93 | |
|---|
| 94 | def writeString(self, colorstr): |
|---|
| 95 | pass |
|---|
| 96 | |
|---|
| 97 | def parseString(self, str): |
|---|
| 98 | """ |
|---|
| 99 | Turn a string input into a list of L{ColorText} elements. |
|---|
| 100 | """ |
|---|
| 101 | |
|---|
| 102 | if self.prepend: |
|---|
| 103 | str = self.prepend + str |
|---|
| 104 | self.prepend = '' |
|---|
| 105 | parts = str.split('\x1B') |
|---|
| 106 | |
|---|
| 107 | if len(parts) == 1: |
|---|
| 108 | self.writeString(self.formatText(parts[0])) |
|---|
| 109 | else: |
|---|
| 110 | self.writeString(self.formatText(parts[0])) |
|---|
| 111 | for s in parts[1:]: |
|---|
| 112 | L = len(s) |
|---|
| 113 | i = 0 |
|---|
| 114 | type = None |
|---|
| 115 | while i < L: |
|---|
| 116 | if s[i] not in string.digits+'[;?': |
|---|
| 117 | break |
|---|
| 118 | i+=1 |
|---|
| 119 | if not s: |
|---|
| 120 | self.prepend = '\x1b' |
|---|
| 121 | return |
|---|
| 122 | if s[0]!='[': |
|---|
| 123 | self.writeString(self.formatText(s[i+1:])) |
|---|
| 124 | continue |
|---|
| 125 | else: |
|---|
| 126 | s=s[1:] |
|---|
| 127 | i-=1 |
|---|
| 128 | if i==L-1: |
|---|
| 129 | self.prepend = '\x1b[' |
|---|
| 130 | return |
|---|
| 131 | type = _setmap.get(s[i], None) |
|---|
| 132 | if type is None: |
|---|
| 133 | continue |
|---|
| 134 | |
|---|
| 135 | if type == AnsiParser.COLOR_SET: |
|---|
| 136 | self.parseColor(s[:i + 1]) |
|---|
| 137 | s = s[i + 1:] |
|---|
| 138 | self.writeString(self.formatText(s)) |
|---|
| 139 | elif type == AnsiParser.CURSOR_SET: |
|---|
| 140 | cursor, s = s[:i+1], s[i+1:] |
|---|
| 141 | self.parseCursor(cursor) |
|---|
| 142 | self.writeString(self.formatText(s)) |
|---|
| 143 | elif type == AnsiParser.ERASE_SET: |
|---|
| 144 | erase, s = s[:i+1], s[i+1:] |
|---|
| 145 | self.parseErase(erase) |
|---|
| 146 | self.writeString(self.formatText(s)) |
|---|
| 147 | elif type == AnsiParser.MODE_SET: |
|---|
| 148 | mode, s = s[:i+1], s[i+1:] |
|---|
| 149 | #self.parseErase('2J') |
|---|
| 150 | self.writeString(self.formatText(s)) |
|---|
| 151 | elif i == L: |
|---|
| 152 | self.prepend = '\x1B[' + s |
|---|
| 153 | else: |
|---|
| 154 | log.msg('Unhandled ANSI control type: %c' % (s[i],)) |
|---|
| 155 | s = s[i + 1:] |
|---|
| 156 | self.writeString(self.formatText(s)) |
|---|
| 157 | |
|---|
| 158 | def parseColor(self, str): |
|---|
| 159 | """ |
|---|
| 160 | Handle a single ANSI color sequence |
|---|
| 161 | """ |
|---|
| 162 | # Drop the trailing 'm' |
|---|
| 163 | str = str[:-1] |
|---|
| 164 | |
|---|
| 165 | if not str: |
|---|
| 166 | str = '0' |
|---|
| 167 | |
|---|
| 168 | try: |
|---|
| 169 | parts = map(int, str.split(';')) |
|---|
| 170 | except ValueError: |
|---|
| 171 | log.msg('Invalid ANSI color sequence (%d): %s' % (len(str), str)) |
|---|
| 172 | self.currentFG, self.currentBG = self.defaultFG, self.defaultBG |
|---|
| 173 | return |
|---|
| 174 | |
|---|
| 175 | for x in parts: |
|---|
| 176 | if x == 0: |
|---|
| 177 | self.currentFG, self.currentBG = self.defaultFG, self.defaultBG |
|---|
| 178 | self.bold, self.flash, self.underline, self.reverse = 0, 0, 0, 0 |
|---|
| 179 | self.display = 1 |
|---|
| 180 | elif x == 1: |
|---|
| 181 | self.bold = 1 |
|---|
| 182 | elif 30 <= x <= 37: |
|---|
| 183 | self.currentFG = x - 30 |
|---|
| 184 | elif 40 <= x <= 47: |
|---|
| 185 | self.currentBG = x - 40 |
|---|
| 186 | elif x == 39: |
|---|
| 187 | self.currentFG = self.defaultFG |
|---|
| 188 | elif x == 49: |
|---|
| 189 | self.currentBG = self.defaultBG |
|---|
| 190 | elif x == 4: |
|---|
| 191 | self.underline = 1 |
|---|
| 192 | elif x == 5: |
|---|
| 193 | self.flash = 1 |
|---|
| 194 | elif x == 7: |
|---|
| 195 | self.reverse = 1 |
|---|
| 196 | elif x == 8: |
|---|
| 197 | self.display = 0 |
|---|
| 198 | elif x == 22: |
|---|
| 199 | self.bold = 0 |
|---|
| 200 | elif x == 24: |
|---|
| 201 | self.underline = 0 |
|---|
| 202 | elif x == 25: |
|---|
| 203 | self.blink = 0 |
|---|
| 204 | elif x == 27: |
|---|
| 205 | self.reverse = 0 |
|---|
| 206 | elif x == 28: |
|---|
| 207 | self.display = 1 |
|---|
| 208 | else: |
|---|
| 209 | log.msg('Unrecognised ANSI color command: %d' % (x,)) |
|---|
| 210 | |
|---|
| 211 | def parseCursor(self, cursor): |
|---|
| 212 | pass |
|---|
| 213 | |
|---|
| 214 | def parseErase(self, erase): |
|---|
| 215 | pass |
|---|
| 216 | |
|---|
| 217 | |
|---|
| 218 | def pickColor(self, value, mode, BOLD = ColorText.BOLD_COLORS): |
|---|
| 219 | if mode: |
|---|
| 220 | return ColorText.COLORS[value] |
|---|
| 221 | else: |
|---|
| 222 | return self.bold and BOLD[value] or ColorText.COLORS[value] |
|---|
| 223 | |
|---|
| 224 | |
|---|
| 225 | def formatText(self, text): |
|---|
| 226 | return ColorText( |
|---|
| 227 | text, |
|---|
| 228 | self.pickColor(self.currentFG, 0), |
|---|
| 229 | self.pickColor(self.currentBG, 1), |
|---|
| 230 | self.display, self.bold, self.underline, self.flash, self.reverse |
|---|
| 231 | ) |
|---|
| 232 | |
|---|
| 233 | |
|---|
| 234 | _sets = ''.join(map(''.join, AnsiParser.SETS)) |
|---|
| 235 | |
|---|
| 236 | _setmap = {} |
|---|
| 237 | for s in AnsiParser.SETS: |
|---|
| 238 | for r in s: |
|---|
| 239 | _setmap[r] = s |
|---|
| 240 | del s |
|---|