ansi_code_processor.py
177 lines
| 5.6 KiB
| text/x-python
|
PythonLexer
epatters
|
r2716 | # Standard library imports | ||
import re | ||||
# System library imports | ||||
from PyQt4 import QtCore, QtGui | ||||
epatters
|
r2783 | class AnsiAction(object): | ||
""" Represents an action requested by an ANSI escape sequence. | ||||
""" | ||||
def __init__(self, kind): | ||||
self.kind = kind | ||||
class MoveAction(AnsiAction): | ||||
""" An AnsiAction for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, | ||||
CHA, and CUP commands). | ||||
""" | ||||
def __init__(self): | ||||
raise NotImplementedError | ||||
class EraseAction(AnsiAction): | ||||
""" An AnsiAction for erase requests (ED and EL commands). | ||||
""" | ||||
def __init__(self, area, erase_to): | ||||
super(EraseAction, self).__init__('erase') | ||||
self.area = area | ||||
self.erase_to = erase_to | ||||
epatters
|
r2716 | class AnsiCodeProcessor(object): | ||
""" Translates ANSI escape codes into readable attributes. | ||||
""" | ||||
# Protected class variables. | ||||
_ansi_commands = 'ABCDEFGHJKSTfmnsu' | ||||
_ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands) | ||||
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): | ||||
substring = string[start:match.start()] | ||||
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 | |||
substring = string[start:] | ||||
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
|
r2716 | for code in params: | ||
self.set_sgr_code(code) | ||||
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' | ||||
self.actions.append(EraseAction(area, erase_to)) | ||||
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: | ||
self.intensity = 1 | ||||
self.bold = True | ||||
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 | ||||
class QtAnsiCodeProcessor(AnsiCodeProcessor): | ||||
""" Translates ANSI escape codes into QTextCharFormats. | ||||
""" | ||||
# A map from color codes to RGB colors. | ||||
epatters
|
r2784 | ansi_colors = (# Normal, Bright/Light ANSI color code | ||
('black', 'grey'), # 0: black | ||||
('darkred', 'red'), # 1: red | ||||
('darkgreen', 'green'), # 2: green | ||||
('gold', 'yellow'), # 3: yellow | ||||
('darkblue', 'blue'), # 4: blue | ||||
('darkviolet', 'magenta'), # 5: magenta | ||||
('steelblue', 'cyan'), # 6: cyan | ||||
('grey', 'white')) # 7: white | ||||
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: | ||||
color = self.ansi_colors[self.foreground_color][self.intensity] | ||||
format.setForeground(QtGui.QColor(color)) | ||||
# Set background color | ||||
if self.background_color is not None: | ||||
color = self.ansi_colors[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 | ||||