ansi_code_processor.py
233 lines
| 8.3 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 | ||||
from PyQt4 import QtCore, QtGui | ||||
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 | |||
epatters
|
r2998 | #----------------------------------------------------------------------------- | ||
# Classes | ||||
#----------------------------------------------------------------------------- | ||||
epatters
|
r2783 | |||
epatters
|
r2716 | class AnsiCodeProcessor(object): | ||
epatters
|
r2998 | """ Translates special ASCII characters and ANSI escape codes into readable | ||
attributes. | ||||
epatters
|
r2716 | """ | ||
epatters
|
r2870 | # Whether to increase intensity or set boldness for SGR code 1. | ||
# (Different terminals handle this in different ways.) | ||||
bold_text_enabled = False | ||||
epatters
|
r2716 | # Protected class variables. | ||
_ansi_commands = 'ABCDEFGHJKSTfmnsu' | ||||
_ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands) | ||||
epatters
|
r2998 | _special_pattern = re.compile('([\f])') | ||
#--------------------------------------------------------------------------- | ||||
# AnsiCodeProcessor interface | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2716 | |||
def __init__(self): | ||||
epatters
|
r2783 | self.actions = [] | ||
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 | ||
for match in self._ansi_pattern.finditer(string): | ||||
epatters
|
r2998 | raw = string[start:match.start()] | ||
substring = self._special_pattern.sub(self._replace_special, raw) | ||||
epatters
|
r2783 | if substring or self.actions: | ||
epatters
|
r2716 | yield substring | ||
start = match.end() | ||||
epatters
|
r2783 | self.actions = [] | ||
try: | ||||
params = [] | ||||
for param in match.group(1).split(';'): | ||||
if param: | ||||
params.append(int(param)) | ||||
except ValueError: | ||||
# Silently discard badly formed escape codes. | ||||
pass | ||||
else: | ||||
self.set_csi_code(match.group(2), params) | ||||
epatters
|
r2716 | |||
epatters
|
r2998 | raw = string[start:] | ||
substring = self._special_pattern.sub(self._replace_special, raw) | ||||
epatters
|
r2783 | if substring or self.actions: | ||
epatters
|
r2716 | yield substring | ||
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. | ||||
params : sequence of integers, optional | ||||
The parameter codes for the command. | ||||
""" | ||||
epatters
|
r2783 | if command == 'm': # SGR - Select Graphic Rendition | ||
epatters
|
r3000 | if params: | ||
for code in params: | ||||
self.set_sgr_code(code) | ||||
else: | ||||
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
|
r2716 | |||
def set_sgr_code(self, code): | ||||
""" Set attributes based on SGR (Select Graphic Rendition) code. | ||||
""" | ||||
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 | ||||
elif code == 39: | ||||
self.foreground_color = None | ||||
elif code >= 40 and code <= 47: | ||||
self.background_color = code - 40 | ||||
elif code == 49: | ||||
self.background_color = None | ||||
epatters
|
r2998 | |||
#--------------------------------------------------------------------------- | ||||
# Protected interface | ||||
#--------------------------------------------------------------------------- | ||||
def _replace_special(self, match): | ||||
special = match.group(1) | ||||
if special == '\f': | ||||
self.actions.append(ScrollAction('scroll', 'down', 'page', 1)) | ||||
return '' | ||||
epatters
|
r2716 | |||
class QtAnsiCodeProcessor(AnsiCodeProcessor): | ||||
""" Translates ANSI escape codes into QTextCharFormats. | ||||
""" | ||||
# A map from color codes to RGB colors. | ||||
epatters
|
r2870 | default_map = (# Normal, Bright/Light ANSI color code | ||
epatters
|
r2784 | ('black', 'grey'), # 0: black | ||
('darkred', 'red'), # 1: red | ||||
epatters
|
r2870 | ('darkgreen', 'lime'), # 2: green | ||
('brown', 'yellow'), # 3: yellow | ||||
('darkblue', 'deepskyblue'), # 4: blue | ||||
epatters
|
r2784 | ('darkviolet', 'magenta'), # 5: magenta | ||
('steelblue', 'cyan'), # 6: cyan | ||||
('grey', 'white')) # 7: white | ||||
epatters
|
r2870 | |||
def __init__(self): | ||||
super(QtAnsiCodeProcessor, self).__init__() | ||||
self.color_map = self.default_map | ||||
epatters
|
r2716 | |||
def get_format(self): | ||||
""" Returns a QTextCharFormat that encodes the current style attributes. | ||||
""" | ||||
format = QtGui.QTextCharFormat() | ||||
# Set foreground color | ||||
if self.foreground_color is not None: | ||||
epatters
|
r2870 | color = self.color_map[self.foreground_color][self.intensity] | ||
epatters
|
r2716 | format.setForeground(QtGui.QColor(color)) | ||
# Set background color | ||||
if self.background_color is not None: | ||||
epatters
|
r2870 | color = self.color_map[self.background_color][self.intensity] | ||
epatters
|
r2716 | format.setBackground(QtGui.QColor(color)) | ||
# 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. | ||||
""" | ||||
if color.value() < 127: | ||||
# Colors appropriate for a terminal with a dark background. | ||||
self.color_map = self.default_map | ||||
else: | ||||
# Colors appropriate for a terminal with a light background. For | ||||
# now, only use non-bright colors... | ||||
self.color_map = [ (pair[0], pair[0]) for pair in self.default_map ] | ||||
# ...and replace white with black. | ||||
self.color_map[7] = ('black', 'black') | ||||