|
|
""" Utilities for processing ANSI escape codes and special ASCII characters.
|
|
|
"""
|
|
|
#-----------------------------------------------------------------------------
|
|
|
# Imports
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
|
|
# Standard library imports
|
|
|
from collections import namedtuple
|
|
|
import re
|
|
|
|
|
|
# System library imports
|
|
|
from PyQt4 import QtCore, QtGui
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
# Constants and datatypes
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
|
|
# An action for erase requests (ED and EL commands).
|
|
|
EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
|
|
|
|
|
|
# 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'])
|
|
|
|
|
|
# An action for scroll requests (SU and ST) and form feeds.
|
|
|
ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
# Classes
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
|
|
class AnsiCodeProcessor(object):
|
|
|
""" Translates special ASCII characters and ANSI escape codes into readable
|
|
|
attributes.
|
|
|
"""
|
|
|
|
|
|
# Whether to increase intensity or set boldness for SGR code 1.
|
|
|
# (Different terminals handle this in different ways.)
|
|
|
bold_text_enabled = False
|
|
|
|
|
|
# Protected class variables.
|
|
|
_ansi_commands = 'ABCDEFGHJKSTfmnsu'
|
|
|
_ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
|
|
|
_special_pattern = re.compile('([\f])')
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
# AnsiCodeProcessor interface
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
|
|
def __init__(self):
|
|
|
self.actions = []
|
|
|
self.reset_sgr()
|
|
|
|
|
|
def reset_sgr(self):
|
|
|
""" Reset graphics attributs to their default values.
|
|
|
"""
|
|
|
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.
|
|
|
"""
|
|
|
self.actions = []
|
|
|
start = 0
|
|
|
|
|
|
for match in self._ansi_pattern.finditer(string):
|
|
|
raw = string[start:match.start()]
|
|
|
substring = self._special_pattern.sub(self._replace_special, raw)
|
|
|
if substring or self.actions:
|
|
|
yield substring
|
|
|
start = match.end()
|
|
|
|
|
|
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)
|
|
|
|
|
|
raw = string[start:]
|
|
|
substring = self._special_pattern.sub(self._replace_special, raw)
|
|
|
if substring or self.actions:
|
|
|
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.
|
|
|
"""
|
|
|
if command == 'm': # SGR - Select Graphic Rendition
|
|
|
if params:
|
|
|
for code in params:
|
|
|
self.set_sgr_code(code)
|
|
|
else:
|
|
|
self.set_sgr_code(0)
|
|
|
|
|
|
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'
|
|
|
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))
|
|
|
|
|
|
def set_sgr_code(self, code):
|
|
|
""" Set attributes based on SGR (Select Graphic Rendition) code.
|
|
|
"""
|
|
|
if code == 0:
|
|
|
self.reset_sgr()
|
|
|
elif code == 1:
|
|
|
if self.bold_text_enabled:
|
|
|
self.bold = True
|
|
|
else:
|
|
|
self.intensity = 1
|
|
|
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
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
# Protected interface
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
|
|
def _replace_special(self, match):
|
|
|
special = match.group(1)
|
|
|
if special == '\f':
|
|
|
self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
|
|
|
return ''
|
|
|
|
|
|
|
|
|
class QtAnsiCodeProcessor(AnsiCodeProcessor):
|
|
|
""" Translates ANSI escape codes into QTextCharFormats.
|
|
|
"""
|
|
|
|
|
|
# A map from color codes to RGB colors.
|
|
|
default_map = (# Normal, Bright/Light ANSI color code
|
|
|
('black', 'grey'), # 0: black
|
|
|
('darkred', 'red'), # 1: red
|
|
|
('darkgreen', 'lime'), # 2: green
|
|
|
('brown', 'yellow'), # 3: yellow
|
|
|
('darkblue', 'deepskyblue'), # 4: blue
|
|
|
('darkviolet', 'magenta'), # 5: magenta
|
|
|
('steelblue', 'cyan'), # 6: cyan
|
|
|
('grey', 'white')) # 7: white
|
|
|
|
|
|
def __init__(self):
|
|
|
super(QtAnsiCodeProcessor, self).__init__()
|
|
|
self.color_map = self.default_map
|
|
|
|
|
|
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:
|
|
|
color = self.color_map[self.foreground_color][self.intensity]
|
|
|
format.setForeground(QtGui.QColor(color))
|
|
|
|
|
|
# Set background color
|
|
|
if self.background_color is not None:
|
|
|
color = self.color_map[self.background_color][self.intensity]
|
|
|
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
|
|
|
|
|
|
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')
|
|
|
|