ansi_code_processor.py
378 lines
| 13.4 KiB
| text/x-python
|
PythonLexer
epatters
|
r2998 | """ Utilities for processing ANSI escape codes and special ASCII characters. | ||
""" | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
epatters
|
r2716 | # Standard library imports | ||
epatters
|
r2998 | from collections import namedtuple | ||
epatters
|
r2716 | import re | ||
# System library imports | ||||
Thomas Kluyver
|
r11131 | from IPython.external.qt import QtGui | ||
epatters
|
r2716 | |||
Thomas Kluyver
|
r13353 | # Local imports | ||
from IPython.utils.py3compat import string_types | ||||
epatters
|
r2998 | #----------------------------------------------------------------------------- | ||
# Constants and datatypes | ||||
#----------------------------------------------------------------------------- | ||||
epatters
|
r2716 | |||
epatters
|
r2998 | # An action for erase requests (ED and EL commands). | ||
EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to']) | ||||
epatters
|
r2783 | |||
epatters
|
r2998 | # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP, | ||
# and HVP commands). | ||||
# FIXME: Not implemented in AnsiCodeProcessor. | ||||
MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count']) | ||||
epatters
|
r2783 | |||
epatters
|
r2998 | # An action for scroll requests (SU and ST) and form feeds. | ||
ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count']) | ||||
epatters
|
r2783 | |||
Michael Droettboom
|
r5558 | # An action for the carriage return character | ||
CarriageReturnAction = namedtuple('CarriageReturnAction', ['action']) | ||||
Puneeth Chaganti
|
r6436 | # An action for the \n character | ||
NewLineAction = namedtuple('NewLineAction', ['action']) | ||||
Michael Droettboom
|
r5559 | # An action for the beep character | ||
BeepAction = namedtuple('BeepAction', ['action']) | ||||
Puneeth Chaganti
|
r6436 | # An action for backspace | ||
BackSpaceAction = namedtuple('BackSpaceAction', ['action']) | ||||
epatters
|
r3367 | # Regular expressions. | ||
CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu' | ||||
CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS | ||||
OSC_SUBPATTERN = '\](.*?)[\x07\x1b]' | ||||
Michael Droettboom
|
r5558 | ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \ | ||
(CSI_SUBPATTERN, OSC_SUBPATTERN)) | ||||
Puneeth Chaganti
|
r6436 | ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN) | ||
epatters
|
r3367 | SPECIAL_PATTERN = re.compile('([\f])') | ||
epatters
|
r2998 | #----------------------------------------------------------------------------- | ||
# Classes | ||||
#----------------------------------------------------------------------------- | ||||
epatters
|
r2783 | |||
epatters
|
r2716 | class AnsiCodeProcessor(object): | ||
epatters
|
r2998 | """ Translates special ASCII characters and ANSI escape codes into readable | ||
epatters
|
r3367 | attributes. It also supports a few non-standard, xterm-specific codes. | ||
epatters
|
r2716 | """ | ||
epatters
|
r2870 | # Whether to increase intensity or set boldness for SGR code 1. | ||
# (Different terminals handle this in different ways.) | ||||
epatters
|
r3367 | bold_text_enabled = False | ||
epatters
|
r2870 | |||
epatters
|
r3367 | # We provide an empty default color map because subclasses will likely want | ||
# to use a custom color format. | ||||
default_color_map = {} | ||||
epatters
|
r2998 | |||
#--------------------------------------------------------------------------- | ||||
# AnsiCodeProcessor interface | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2716 | |||
def __init__(self): | ||||
epatters
|
r2783 | self.actions = [] | ||
epatters
|
r3367 | self.color_map = self.default_color_map.copy() | ||
epatters
|
r2783 | self.reset_sgr() | ||
epatters
|
r2716 | |||
epatters
|
r2783 | def reset_sgr(self): | ||
""" Reset graphics attributs to their default values. | ||||
epatters
|
r2716 | """ | ||
self.intensity = 0 | ||||
self.italic = False | ||||
self.bold = False | ||||
self.underline = False | ||||
self.foreground_color = None | ||||
self.background_color = None | ||||
def split_string(self, string): | ||||
""" Yields substrings for which the same escape code applies. | ||||
""" | ||||
epatters
|
r2783 | self.actions = [] | ||
epatters
|
r2716 | start = 0 | ||
Puneeth Chaganti
|
r6436 | # strings ending with \r are assumed to be ending in \r\n since | ||
# \n is appended to output strings automatically. Accounting | ||||
# for that, here. | ||||
last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None | ||||
string = string[:-1] if last_char is not None else string | ||||
Michael Droettboom
|
r5559 | for match in ANSI_OR_SPECIAL_PATTERN.finditer(string): | ||
epatters
|
r2998 | raw = string[start:match.start()] | ||
epatters
|
r3367 | substring = SPECIAL_PATTERN.sub(self._replace_special, raw) | ||
epatters
|
r2783 | if substring or self.actions: | ||
epatters
|
r2716 | yield substring | ||
Puneeth Chaganti
|
r6436 | self.actions = [] | ||
epatters
|
r2716 | start = match.end() | ||
Thomas Kluyver
|
r13378 | groups = [g for g in match.groups() if (g is not None)] | ||
Puneeth Chaganti
|
r6436 | g0 = groups[0] | ||
if g0 == '\a': | ||||
Michael Droettboom
|
r5559 | self.actions.append(BeepAction('beep')) | ||
Puneeth Chaganti
|
r6436 | yield None | ||
self.actions = [] | ||||
elif g0 == '\r': | ||||
self.actions.append(CarriageReturnAction('carriage-return')) | ||||
yield None | ||||
self.actions = [] | ||||
elif g0 == '\b': | ||||
self.actions.append(BackSpaceAction('backspace')) | ||||
yield None | ||||
self.actions = [] | ||||
elif g0 == '\n' or g0 == '\r\n': | ||||
self.actions.append(NewLineAction('newline')) | ||||
yield g0 | ||||
self.actions = [] | ||||
Michael Droettboom
|
r5558 | else: | ||
params = [ param for param in groups[1].split(';') if param ] | ||||
Puneeth Chaganti
|
r6436 | if g0.startswith('['): | ||
Michael Droettboom
|
r5558 | # Case 1: CSI code. | ||
try: | ||||
Thomas Kluyver
|
r13378 | params = list(map(int, params)) | ||
Michael Droettboom
|
r5558 | except ValueError: | ||
# Silently discard badly formed codes. | ||||
pass | ||||
else: | ||||
self.set_csi_code(groups[2], params) | ||||
Puneeth Chaganti
|
r6436 | elif g0.startswith(']'): | ||
Michael Droettboom
|
r5558 | # Case 2: OSC code. | ||
self.set_osc_code(params) | ||||
epatters
|
r2716 | |||
epatters
|
r2998 | raw = string[start:] | ||
epatters
|
r3367 | substring = SPECIAL_PATTERN.sub(self._replace_special, raw) | ||
epatters
|
r2783 | if substring or self.actions: | ||
epatters
|
r2716 | yield substring | ||
Puneeth Chaganti
|
r6436 | if last_char is not None: | ||
self.actions.append(NewLineAction('newline')) | ||||
yield last_char | ||||
epatters
|
r2716 | def set_csi_code(self, command, params=[]): | ||
""" Set attributes based on CSI (Control Sequence Introducer) code. | ||||
Parameters | ||||
---------- | ||||
command : str | ||||
The code identifier, i.e. the final character in the sequence. | ||||
Bernardo B. Marques
|
r4872 | |||
epatters
|
r2716 | params : sequence of integers, optional | ||
The parameter codes for the command. | ||||
""" | ||||
epatters
|
r2783 | if command == 'm': # SGR - Select Graphic Rendition | ||
epatters
|
r3000 | if params: | ||
epatters
|
r3367 | self.set_sgr_code(params) | ||
epatters
|
r3000 | else: | ||
epatters
|
r3367 | self.set_sgr_code([0]) | ||
epatters
|
r2783 | |||
elif (command == 'J' or # ED - Erase Data | ||||
command == 'K'): # EL - Erase in Line | ||||
code = params[0] if params else 0 | ||||
if 0 <= code <= 2: | ||||
area = 'screen' if command == 'J' else 'line' | ||||
if code == 0: | ||||
erase_to = 'end' | ||||
elif code == 1: | ||||
erase_to = 'start' | ||||
elif code == 2: | ||||
erase_to = 'all' | ||||
epatters
|
r2998 | self.actions.append(EraseAction('erase', area, erase_to)) | ||
elif (command == 'S' or # SU - Scroll Up | ||||
command == 'T'): # SD - Scroll Down | ||||
dir = 'up' if command == 'S' else 'down' | ||||
count = params[0] if params else 1 | ||||
self.actions.append(ScrollAction('scroll', dir, 'line', count)) | ||||
epatters
|
r3367 | |||
def set_osc_code(self, params): | ||||
""" Set attributes based on OSC (Operating System Command) parameters. | ||||
Bernardo B. Marques
|
r4872 | |||
epatters
|
r3367 | Parameters | ||
---------- | ||||
params : sequence of str | ||||
The parameters for the command. | ||||
epatters
|
r2716 | """ | ||
epatters
|
r3367 | try: | ||
command = int(params.pop(0)) | ||||
except (IndexError, ValueError): | ||||
return | ||||
if command == 4: | ||||
# xterm-specific: set color number to color spec. | ||||
try: | ||||
color = int(params.pop(0)) | ||||
spec = params.pop(0) | ||||
self.color_map[color] = self._parse_xterm_color_spec(spec) | ||||
except (IndexError, ValueError): | ||||
pass | ||||
Bernardo B. Marques
|
r4872 | |||
epatters
|
r3367 | def set_sgr_code(self, params): | ||
""" Set attributes based on SGR (Select Graphic Rendition) codes. | ||||
Parameters | ||||
---------- | ||||
params : sequence of ints | ||||
A list of SGR codes for one or more SGR commands. Usually this | ||||
sequence will have one element per command, although certain | ||||
xterm-specific commands requires multiple elements. | ||||
""" | ||||
# Always consume the first parameter. | ||||
if not params: | ||||
return | ||||
code = params.pop(0) | ||||
epatters
|
r2716 | if code == 0: | ||
epatters
|
r2783 | self.reset_sgr() | ||
epatters
|
r2716 | elif code == 1: | ||
epatters
|
r2870 | if self.bold_text_enabled: | ||
self.bold = True | ||||
else: | ||||
self.intensity = 1 | ||||
epatters
|
r2716 | elif code == 2: | ||
self.intensity = 0 | ||||
elif code == 3: | ||||
self.italic = True | ||||
elif code == 4: | ||||
self.underline = True | ||||
elif code == 22: | ||||
self.intensity = 0 | ||||
self.bold = False | ||||
elif code == 23: | ||||
self.italic = False | ||||
elif code == 24: | ||||
self.underline = False | ||||
elif code >= 30 and code <= 37: | ||||
self.foreground_color = code - 30 | ||||
Bernardo B. Marques
|
r4872 | elif code == 38 and params and params.pop(0) == 5: | ||
epatters
|
r3367 | # xterm-specific: 256 color support. | ||
if params: | ||||
self.foreground_color = params.pop(0) | ||||
epatters
|
r2716 | elif code == 39: | ||
self.foreground_color = None | ||||
elif code >= 40 and code <= 47: | ||||
self.background_color = code - 40 | ||||
epatters
|
r3367 | elif code == 48 and params and params.pop(0) == 5: | ||
# xterm-specific: 256 color support. | ||||
if params: | ||||
self.background_color = params.pop(0) | ||||
epatters
|
r2716 | elif code == 49: | ||
self.background_color = None | ||||
epatters
|
r2998 | |||
epatters
|
r3367 | # Recurse with unconsumed parameters. | ||
self.set_sgr_code(params) | ||||
epatters
|
r2998 | #--------------------------------------------------------------------------- | ||
# Protected interface | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r3367 | def _parse_xterm_color_spec(self, spec): | ||
if spec.startswith('rgb:'): | ||||
return tuple(map(lambda x: int(x, 16), spec[4:].split('/'))) | ||||
elif spec.startswith('rgbi:'): | ||||
Bernardo B. Marques
|
r4872 | return tuple(map(lambda x: int(float(x) * 255), | ||
epatters
|
r3367 | spec[5:].split('/'))) | ||
elif spec == '?': | ||||
raise ValueError('Unsupported xterm color spec') | ||||
return spec | ||||
epatters
|
r2998 | def _replace_special(self, match): | ||
special = match.group(1) | ||||
if special == '\f': | ||||
self.actions.append(ScrollAction('scroll', 'down', 'page', 1)) | ||||
return '' | ||||
Bernardo B. Marques
|
r4872 | |||
epatters
|
r2716 | |||
class QtAnsiCodeProcessor(AnsiCodeProcessor): | ||||
""" Translates ANSI escape codes into QTextCharFormats. | ||||
""" | ||||
epatters
|
r3367 | # A map from ANSI color codes to SVG color names or RGB(A) tuples. | ||
darkbg_color_map = { | ||||
0 : 'black', # black | ||||
1 : 'darkred', # red | ||||
2 : 'darkgreen', # green | ||||
3 : 'brown', # yellow | ||||
4 : 'darkblue', # blue | ||||
5 : 'darkviolet', # magenta | ||||
6 : 'steelblue', # cyan | ||||
7 : 'grey', # white | ||||
8 : 'grey', # black (bright) | ||||
9 : 'red', # red (bright) | ||||
10 : 'lime', # green (bright) | ||||
11 : 'yellow', # yellow (bright) | ||||
12 : 'deepskyblue', # blue (bright) | ||||
13 : 'magenta', # magenta (bright) | ||||
14 : 'cyan', # cyan (bright) | ||||
15 : 'white' } # white (bright) | ||||
# Set the default color map for super class. | ||||
default_color_map = darkbg_color_map.copy() | ||||
def get_color(self, color, intensity=0): | ||||
""" Returns a QColor for a given color code, or None if one cannot be | ||||
constructed. | ||||
""" | ||||
if color is None: | ||||
return None | ||||
epatters
|
r2870 | |||
epatters
|
r3367 | # Adjust for intensity, if possible. | ||
if color < 8 and intensity > 0: | ||||
color += 8 | ||||
constructor = self.color_map.get(color, None) | ||||
Thomas Kluyver
|
r13353 | if isinstance(constructor, string_types): | ||
epatters
|
r3367 | # If this is an X11 color name, we just hope there is a close SVG | ||
# color name. We could use QColor's static method | ||||
# 'setAllowX11ColorNames()', but this is global and only available | ||||
# on X11. It seems cleaner to aim for uniformity of behavior. | ||||
return QtGui.QColor(constructor) | ||||
elif isinstance(constructor, (tuple, list)): | ||||
return QtGui.QColor(*constructor) | ||||
return None | ||||
Bernardo B. Marques
|
r4872 | |||
epatters
|
r2716 | def get_format(self): | ||
""" Returns a QTextCharFormat that encodes the current style attributes. | ||||
""" | ||||
format = QtGui.QTextCharFormat() | ||||
# Set foreground color | ||||
epatters
|
r3367 | qcolor = self.get_color(self.foreground_color, self.intensity) | ||
if qcolor is not None: | ||||
format.setForeground(qcolor) | ||||
epatters
|
r2716 | |||
# Set background color | ||||
epatters
|
r3367 | qcolor = self.get_color(self.background_color, self.intensity) | ||
if qcolor is not None: | ||||
format.setBackground(qcolor) | ||||
epatters
|
r2716 | |||
# Set font weight/style options | ||||
if self.bold: | ||||
format.setFontWeight(QtGui.QFont.Bold) | ||||
else: | ||||
format.setFontWeight(QtGui.QFont.Normal) | ||||
format.setFontItalic(self.italic) | ||||
format.setFontUnderline(self.underline) | ||||
return format | ||||
epatters
|
r2870 | |||
def set_background_color(self, color): | ||||
""" Given a background color (a QColor), attempt to set a color map | ||||
that will be aesthetically pleasing. | ||||
""" | ||||
epatters
|
r3367 | # Set a new default color map. | ||
self.default_color_map = self.darkbg_color_map.copy() | ||||
epatters
|
r2870 | |||
epatters
|
r3367 | if color.value() >= 127: | ||
Bernardo B. Marques
|
r4872 | # Colors appropriate for a terminal with a light background. For | ||
epatters
|
r2870 | # now, only use non-bright colors... | ||
Thomas Kluyver
|
r13356 | for i in range(8): | ||
epatters
|
r3367 | self.default_color_map[i + 8] = self.default_color_map[i] | ||
epatters
|
r2870 | |||
# ...and replace white with black. | ||||
epatters
|
r3367 | self.default_color_map[7] = self.default_color_map[15] = 'black' | ||
# Update the current color map with the new defaults. | ||||
self.color_map.update(self.default_color_map) | ||||