1456 lines
56 KiB
Python
1456 lines
56 KiB
Python
"""
|
|
This module contains the output window widget that can be used to visually run a child process in a PyQt application.
|
|
|
|
The widget supports most ANSI Escape Sequences (colors, cursor positioning,...) and can be used to create a rudimentary
|
|
terminal emulator (such a widget is available in the terminal module).
|
|
"""
|
|
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
import string
|
|
|
|
from collections import namedtuple
|
|
|
|
from pyqode.core.api import CodeEdit, SyntaxHighlighter, TextHelper
|
|
from pyqode.core.api.client import PROCESS_ERROR_STRING
|
|
from pyqode.core.backend import server
|
|
from pyqode.qt import QtWidgets, QtGui, QtCore
|
|
from pyqode.qt.QtGui import QColor
|
|
from pyqode.qt.QtWidgets import qApp
|
|
|
|
from . import pty_wrapper
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
# Widget
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
class OutputWindow(CodeEdit):
|
|
"""
|
|
Widget that runs a child process and print its output in a text area.
|
|
|
|
This widget can be used to show the output of a process in a Qt application (i.e. the run output of an IDE) or
|
|
it could be used to implement a terminal emulator. While the parser supports most common Ansi Escape Codes, it
|
|
does not aim to support all VT100 features.
|
|
|
|
User inputs are handled by an InputHandler, there are two types of input handlers:
|
|
- ImmediateInputHandler: forward ansi code of each key strokes directly to the child process's stdin. Use this
|
|
for an interactive process such as bash or python.
|
|
- BufferedInputHandler: bufferize user inputs until user pressed enter. Use this for any other kind of process.
|
|
|
|
Usage:
|
|
|
|
- create an OutputWindow instance (you can specify a color scheme and an input handler).
|
|
- call start_process to actually start your child process.
|
|
"""
|
|
#
|
|
# Public API
|
|
#
|
|
#: Define the color_scheme of the output window.
|
|
ColorScheme = namedtuple('ColorScheme', 'background foreground error custom red green yellow blue magenta cyan')
|
|
|
|
#: Default theme using QPalette colors, must be initialised by a call to ``_init_default_scheme``
|
|
DefaultColorScheme = None
|
|
#: Linux color scheme (black background with ANSI colors)
|
|
LinuxColorScheme = None
|
|
#: Tango theme (black background with pastel colors).
|
|
TangoColorScheme = None
|
|
#: The famous Solarized dark theme.
|
|
SolarizedColorScheme = None
|
|
|
|
#: Signal emitted when the user pressed on a file link.
|
|
#: Client code should open the requested file in the editor.
|
|
#: Parameters:
|
|
#: - path (str)
|
|
#: - line number (int, 0 based)
|
|
open_file_requested = QtCore.Signal(str, int)
|
|
|
|
process_finished = QtCore.Signal()
|
|
|
|
@property
|
|
def process(self):
|
|
"""
|
|
Returns a reference to the child process being run (QProcess)
|
|
"""
|
|
return self._process
|
|
|
|
@property
|
|
def is_running(self):
|
|
"""
|
|
Checks if the child process is running (or is starting).
|
|
"""
|
|
return self._process.state() in [self._process.Running, self._process.Starting]
|
|
|
|
@property
|
|
def color_scheme(self):
|
|
"""
|
|
Gets/Sets the color scheme
|
|
"""
|
|
return self._formatter.theme
|
|
|
|
@color_scheme.setter
|
|
def color_scheme(self, scheme):
|
|
self._formatter.color_scheme = scheme
|
|
self.background = scheme.background
|
|
self.foreground = scheme.foreground
|
|
self._reset_stylesheet()
|
|
|
|
@property
|
|
def input_handler(self):
|
|
"""
|
|
Gets/Sets an input handler (see :class:`InputHandler).
|
|
"""
|
|
return self._input_handler
|
|
|
|
@input_handler.setter
|
|
def input_handler(self, handler):
|
|
self._input_handler = handler
|
|
self._input_handler.edit = self
|
|
self._input_handler.process = self._process
|
|
|
|
def __init__(self, parent=None, color_scheme=None, formatter=None, input_handler=None, backend=server.__file__,
|
|
link_regex=re.compile(r'("|\')(?P<url>(/|[a-zA-Z]:\\)[\w/\s\\\.\-]*)("|\')(, line (?P<line>\d*))?')):
|
|
"""
|
|
:param parent: parent widget, if any
|
|
:param color_scheme: color scheme to use
|
|
:param formatter: The formatter to use to draw the process output. Use our builtin formatter by default.
|
|
:param input_handler: the user input handler, buffered by default.
|
|
:param backend: backend script used for searching in the process output.
|
|
:param link_regex: Regex used to match file links, default regex was made to match file path of python
|
|
tracebacks. You can specify another regex if needed, the only requirements is that the regex must have
|
|
two named capture groups: 'url' and 'line'
|
|
"""
|
|
super(OutputWindow, self).__init__(parent)
|
|
if formatter is None:
|
|
formatter = OutputFormatter(self, color_scheme=color_scheme)
|
|
if input_handler is None:
|
|
input_handler = BufferedInputHandler()
|
|
self.link_regex = link_regex
|
|
self._link_match = None
|
|
self._formatter = formatter
|
|
self._init_code_edit(backend)
|
|
self._process = QtCore.QProcess()
|
|
self._process.readyReadStandardOutput.connect(self._read_stdout)
|
|
self._process.readyReadStandardError.connect(self._read_stderr)
|
|
self._process.error.connect(self._on_process_error)
|
|
self._process.finished.connect(self._on_process_finished)
|
|
self._input_handler = input_handler
|
|
self._input_handler.edit = self
|
|
self._input_handler.process = self._process
|
|
self._last_hovered_block = None
|
|
self.flg_use_pty = False
|
|
|
|
def start_process(self, program, arguments=None, working_dir=None, print_command=True,
|
|
use_pseudo_terminal=True, env=None):
|
|
"""
|
|
Starts the child process.
|
|
|
|
:param program: program to start
|
|
:param arguments: list of program arguments
|
|
:param working_dir: working directory of the child process
|
|
:param print_command: True to print the full command (pgm + arguments) as the first line
|
|
of the output window
|
|
:param use_pseudo_terminal: True to use a pseudo terminal on Unix (pty), False to avoid
|
|
using a pty wrapper. When using a pty wrapper, both stdout and stderr are merged together.
|
|
:param environ: environment variables to set on the child process. If None, os.environ will be used.
|
|
"""
|
|
# clear previous output
|
|
self.clear()
|
|
self.setReadOnly(False)
|
|
if arguments is None:
|
|
arguments = []
|
|
if sys.platform != 'win32' and use_pseudo_terminal:
|
|
pgm = sys.executable
|
|
args = [pty_wrapper.__file__, program] + arguments
|
|
self.flg_use_pty = use_pseudo_terminal
|
|
else:
|
|
pgm = program
|
|
args = arguments
|
|
self.flg_use_pty = False # pty not available on windows
|
|
|
|
self._process.setProcessEnvironment(self._setup_process_environment(env))
|
|
|
|
if working_dir:
|
|
self._process.setWorkingDirectory(working_dir)
|
|
|
|
if print_command:
|
|
self._formatter.append_message('\x1b[0m%s %s\n' % (program, ' '.join(arguments)),
|
|
output_format=OutputFormat.CustomFormat)
|
|
self._process.start(pgm, args)
|
|
|
|
def stop_process(self):
|
|
"""
|
|
Stops the child process.
|
|
"""
|
|
self._process.terminate()
|
|
if not self._process.waitForFinished(100):
|
|
self._process.kill()
|
|
|
|
def paste(self):
|
|
"""
|
|
Paste the content of the clipboard to the child process'stdtin.
|
|
"""
|
|
self.input_handler.paste(QtWidgets.qApp.clipboard().text())
|
|
|
|
@staticmethod
|
|
def create_color_scheme(background=None, foreground=None, error=None, custom=None, red=None,
|
|
green=None, yellow=None, blue=None, magenta=None, cyan=None):
|
|
"""
|
|
Utility function that creates a color scheme instance, with default values.
|
|
|
|
The default colors are chosen based on the current palette.
|
|
|
|
:param background: background color
|
|
:param foreground: foreground color
|
|
:param error: color of error messages (stderr)
|
|
:param custom: color of custom messages (e.g. to print the full command or the process exit code)
|
|
:param red: value of the red ANSI color
|
|
:param green: value of the green ANSI color
|
|
:param yellow: value of the yellow ANSI color
|
|
:param blue: value of the blue ANSI color
|
|
:param magenta: value of the magenta ANSI color
|
|
:param cyan: value of the cyan ANSI color
|
|
:return: A ColorScheme instance.
|
|
"""
|
|
if background is None:
|
|
background = qApp.palette().base().color()
|
|
if foreground is None:
|
|
foreground = qApp.palette().text().color()
|
|
is_light = background.lightness() >= 128
|
|
if error is None:
|
|
if is_light:
|
|
error = QColor('dark red')
|
|
else:
|
|
error = QColor('#FF5555')
|
|
if red is None:
|
|
red = QColor(error)
|
|
if green is None:
|
|
if is_light:
|
|
green = QColor('dark green')
|
|
else:
|
|
green = QColor('#55FF55')
|
|
if yellow is None:
|
|
if is_light:
|
|
yellow = QColor('#aaaa00')
|
|
else:
|
|
yellow = QColor('#FFFF55')
|
|
if blue is None:
|
|
if is_light:
|
|
blue = QColor('dark blue')
|
|
else:
|
|
blue = QColor('#5555FF')
|
|
if magenta is None:
|
|
if is_light:
|
|
magenta = QColor('dark magenta')
|
|
else:
|
|
magenta = QColor('#FF55FF')
|
|
if cyan is None:
|
|
if is_light:
|
|
cyan = QColor('dark cyan')
|
|
else:
|
|
cyan = QColor('#55FFFF')
|
|
if custom is None:
|
|
custom = QColor('orange')
|
|
return OutputWindow.ColorScheme(background, foreground, error, custom,
|
|
red, green, yellow, blue, magenta, cyan)
|
|
|
|
#
|
|
# Overriden Qt Methods
|
|
#
|
|
def setReadOnly(self, value):
|
|
QtWidgets.QPlainTextEdit.setReadOnly(self, value)
|
|
|
|
def closeEvent(self, event):
|
|
"""
|
|
Terminates the child process on close.
|
|
"""
|
|
self.stop_process()
|
|
self.backend.stop()
|
|
try:
|
|
self.modes.remove('_LinkHighlighter')
|
|
except KeyError:
|
|
pass # already removed
|
|
super(OutputWindow, self).closeEvent(event)
|
|
|
|
def keyPressEvent(self, event):
|
|
"""
|
|
Handle key press event using the defined input handler.
|
|
"""
|
|
if self._process.state() != self._process.Running:
|
|
return
|
|
tc = self.textCursor()
|
|
sel_start = tc.selectionStart()
|
|
sel_end = tc.selectionEnd()
|
|
tc.setPosition(self._formatter._last_cursor_pos)
|
|
self.setTextCursor(tc)
|
|
if self.input_handler.key_press_event(event):
|
|
tc.setPosition(sel_start)
|
|
tc.setPosition(sel_end, tc.KeepAnchor)
|
|
self.setTextCursor(tc)
|
|
super(OutputWindow, self).keyPressEvent(event)
|
|
self._formatter._last_cursor_pos = self.textCursor().position()
|
|
|
|
def mouseMoveEvent(self, event):
|
|
"""
|
|
Handle mouse over file link.
|
|
"""
|
|
c = self.cursorForPosition(event.pos())
|
|
block = c.block()
|
|
self._link_match = None
|
|
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
|
|
for match in self.link_regex.finditer(block.text()):
|
|
if not match:
|
|
continue
|
|
start, end = match.span()
|
|
if start <= c.positionInBlock() <= end:
|
|
self._link_match = match
|
|
self.viewport().setCursor(QtCore.Qt.PointingHandCursor)
|
|
break
|
|
|
|
self._last_hovered_block = block
|
|
super(OutputWindow, self).mouseMoveEvent(event)
|
|
|
|
def mousePressEvent(self, event):
|
|
"""
|
|
Handle file link clicks.
|
|
"""
|
|
super(OutputWindow, self).mousePressEvent(event)
|
|
if self._link_match:
|
|
path = self._link_match.group('url')
|
|
line = self._link_match.group('line')
|
|
if line is not None:
|
|
line = int(line) - 1
|
|
else:
|
|
line = 0
|
|
self.open_file_requested.emit(path, line)
|
|
|
|
def eventFilter(self, *args):
|
|
return False
|
|
|
|
#
|
|
# Utility methods
|
|
#
|
|
def _init_code_edit(self, backend):
|
|
"""
|
|
Initializes the code editor (setup modes, panels and colors).
|
|
"""
|
|
from pyqode.core import panels, modes
|
|
self.modes.append(_LinkHighlighter(self.document()))
|
|
self.background = self._formatter.color_scheme.background
|
|
self.foreground = self._formatter.color_scheme.foreground
|
|
self._reset_stylesheet()
|
|
self.setCenterOnScroll(False)
|
|
self.setMouseTracking(True)
|
|
self.setUndoRedoEnabled(False)
|
|
search_panel = panels.SearchAndReplacePanel()
|
|
self.panels.append(search_panel, search_panel.Position.TOP)
|
|
self.action_copy.setShortcut('Ctrl+Shift+C')
|
|
self.action_paste.setShortcut('Ctrl+Shift+V')
|
|
self.remove_action(self.action_undo, sub_menu=None)
|
|
self.remove_action(self.action_redo, sub_menu=None)
|
|
self.remove_action(self.action_cut, sub_menu=None)
|
|
self.remove_action(self.action_duplicate_line, sub_menu=None)
|
|
self.remove_action(self.action_indent)
|
|
self.remove_action(self.action_un_indent)
|
|
self.remove_action(self.action_goto_line)
|
|
self.remove_action(search_panel.menu.menuAction())
|
|
self.remove_menu(self._sub_menus['Advanced'])
|
|
self.add_action(search_panel.actionSearch, sub_menu=None)
|
|
self.modes.append(modes.ZoomMode())
|
|
self.backend.start(backend)
|
|
|
|
def _setup_process_environment(self, env):
|
|
"""
|
|
Sets up the process environment.
|
|
"""
|
|
environ = self._process.processEnvironment()
|
|
if env is None:
|
|
env = {}
|
|
for k, v in os.environ.items():
|
|
environ.insert(k, v)
|
|
for k, v in env.items():
|
|
environ.insert(k, v)
|
|
if sys.platform != 'win32':
|
|
environ.insert('TERM', 'xterm')
|
|
environ.insert('LINES', '24')
|
|
environ.insert('COLUMNS', '450')
|
|
environ.insert('PYTHONUNBUFFERED', '1')
|
|
environ.insert('QT_LOGGING_TO_CONSOLE', '1')
|
|
return environ
|
|
|
|
def _on_process_error(self, error):
|
|
"""
|
|
Display child process error in the text edit.
|
|
"""
|
|
if self is None:
|
|
return
|
|
err = PROCESS_ERROR_STRING[error]
|
|
self._formatter.append_message(err + '\r\n', output_format=OutputFormat.ErrorMessageFormat)
|
|
|
|
def _on_process_finished(self):
|
|
"""
|
|
Write the process finished message and emit the `finished` signal.
|
|
"""
|
|
exit_code = self._process.exitCode()
|
|
if self._process.exitStatus() != self._process.NormalExit:
|
|
exit_code = 139
|
|
self._formatter.append_message('\x1b[0m\nProcess finished with exit code %d' % exit_code,
|
|
output_format=OutputFormat.CustomFormat)
|
|
self.setReadOnly(True)
|
|
self.process_finished.emit()
|
|
|
|
def _decode(self, data):
|
|
for encoding in ['utf-8', 'cp850', 'cp1252', 'ascii']:
|
|
try:
|
|
string = data.decode(encoding)
|
|
except UnicodeDecodeError:
|
|
_logger().debug('failed to decode output with encoding=%r', encoding)
|
|
continue
|
|
else:
|
|
_logger().debug('decoding output with encoding=%r succeeded', encoding)
|
|
return string
|
|
return str(data).replace("b'", '')[:-1].replace('\\r', '\r').replace('\\n', '\n').replace('\\\\', '\\')
|
|
|
|
def _read_stdout(self):
|
|
"""
|
|
Reads the child process' stdout and process it.
|
|
"""
|
|
output = self._decode(self._process.readAllStandardOutput().data())
|
|
if self._formatter:
|
|
self._formatter.append_message(output, output_format=OutputFormat.NormalMessageFormat)
|
|
else:
|
|
self.insertPlainText(output)
|
|
|
|
def _read_stderr(self):
|
|
"""
|
|
Reads the child process' stderr and process it.
|
|
"""
|
|
output = self._decode(self._process.readAllStandardError().data())
|
|
if self._formatter:
|
|
self._formatter.append_message(output, output_format=OutputFormat.ErrorMessageFormat)
|
|
else:
|
|
self.insertPlainText(output)
|
|
|
|
|
|
class _LinkHighlighter(SyntaxHighlighter):
|
|
"""
|
|
Highlights links using OutputWindow.link_regex.
|
|
"""
|
|
def highlight_block(self, text, block):
|
|
for match in self.editor.link_regex.finditer(text):
|
|
if match:
|
|
start, end = match.span('url')
|
|
fmt = QtGui.QTextCharFormat()
|
|
fmt.setForeground(QtWidgets.qApp.palette().highlight().color())
|
|
fmt.setUnderlineStyle(fmt.SingleUnderline)
|
|
self.setFormat(start, end - start, fmt)
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
# Parser
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
#: Represents a formatted text: a string + a QtGui.QTextCharFormat
|
|
FormattedText = namedtuple('FormattedText', 'txt fmt')
|
|
#: Generic structure for representing a terminal operation (draw, move cursor,...).
|
|
Operation = namedtuple('Operation', 'command data')
|
|
|
|
|
|
class AnsiEscapeCodeParser(object):
|
|
"""
|
|
The AnsiEscapeCodeParser class parses text and extracts ANSI escape codes from it.
|
|
|
|
In order to preserve color information across text segments, an instance of this class
|
|
must be stored for the lifetime of a stream.
|
|
Also, one instance of this class should not handle multiple streams (at least not
|
|
at the same time).
|
|
|
|
Its main function is parse_text(), which accepts text and default QTextCharFormat.
|
|
|
|
This function is designed to parse text and split colored text to smaller strings,
|
|
with their appropriate formatting information set inside QTextCharFormat.
|
|
|
|
Compared to the QtCreator implementation, we added limited support for terminal
|
|
emulation (changing cursor position, erasing display/line,...).
|
|
|
|
Usage:
|
|
- Create new instance of AnsiEscapeCodeParser for a stream.
|
|
- To add new text, call parse_text() with the text and a default QTextCharFormat.
|
|
The result of this function is a list of Operation with their associated data.
|
|
"""
|
|
|
|
_ResetFormat = 0
|
|
_BoldText = 1
|
|
_ItalicText = 3
|
|
_UnderlinedText = 4
|
|
_NotBold = 21
|
|
_NotItalicNotFraktur = 23
|
|
_NotUnderlined = 24
|
|
_Negative = 7
|
|
_Positive = 27
|
|
_TextColorStart = 30
|
|
_TextColorEnd = 37
|
|
_RgbTextColor = 38
|
|
_DefaultTextColor = 39
|
|
_BackgroundColorStart = 40
|
|
_BackgroundColorEnd = 47
|
|
_RgbBackgroundColor = 48
|
|
_DefaultBackgroundColor = 49
|
|
_Dim = 2
|
|
|
|
_escape = "\x1b["
|
|
_escape_alts = ["\x1b(", "\x1b)", '\x1b=', '\x1b]', '\x08', '\x07']
|
|
_escape_len = len(_escape)
|
|
_semicolon = ';'
|
|
_color_terminator = 'm'
|
|
|
|
_supported_commands = re.compile(r'^(?P<n>\d*;?\d*)(?P<cmd>[ABCDEFGHJKfP]{1})')
|
|
_unsupported_command = re.compile(r'^(\?\d+h)|^(\??\d+l)|^(\d+d)|^(\d+X)|^(\([AB01])|'
|
|
r'^(\)[AB01])|^(\d*;?\d*r)|^(=)|^(>)|^(\d;.*\x07)')
|
|
|
|
_commands = {
|
|
'A': 'cursor_up',
|
|
'B': 'cursor_down',
|
|
'C': 'cursor_forward',
|
|
'D': 'cursor_back',
|
|
'E': 'cursor_next_line',
|
|
'F': 'cursor_previous_lined',
|
|
'G': 'cursor_horizontal_absolute',
|
|
'H': 'cursor_position',
|
|
'J': 'erase_display',
|
|
'K': 'erase_in_line',
|
|
'f': 'cursor_position', # same as H
|
|
'P': 'delete_chars'
|
|
}
|
|
|
|
DIM_FACTOR = 120
|
|
|
|
def __init__(self):
|
|
fmt = QtGui.QTextCharFormat()
|
|
fmt.setForeground(QtWidgets.qApp.palette().text().color())
|
|
fmt.setBackground(QtWidgets.qApp.palette().base().color())
|
|
FormattedText.__new__.__defaults__ = '', fmt
|
|
self._prev_fmt_closed = True
|
|
self._prev_fmt = fmt
|
|
self._pending_text = ''
|
|
self.color_scheme = None
|
|
|
|
def parse_text(self, formatted_text):
|
|
"""
|
|
Retursn a list of operations (draw, cup, ed,...).
|
|
|
|
Each operation consist of a command and its associated data.
|
|
|
|
:param formatted_text: text to parse with the default char format to apply.
|
|
:return: list of Operation
|
|
"""
|
|
assert isinstance(formatted_text, FormattedText)
|
|
ret_val = []
|
|
fmt = formatted_text.fmt if self._prev_fmt_closed else self._prev_fmt
|
|
fmt = QtGui.QTextCharFormat(fmt)
|
|
if not self._pending_text:
|
|
stripped_text = formatted_text.txt
|
|
else:
|
|
stripped_text = self._pending_text + formatted_text.txt
|
|
self._pending_text = ''
|
|
while stripped_text:
|
|
try:
|
|
escape_pos = stripped_text.index(self._escape[0])
|
|
except ValueError:
|
|
ret_val.append(Operation('draw', FormattedText(stripped_text, fmt)))
|
|
break
|
|
else:
|
|
if escape_pos != 0:
|
|
ret_val.append(Operation('draw', FormattedText(stripped_text[:escape_pos], fmt)))
|
|
stripped_text = stripped_text[escape_pos:]
|
|
fmt = QtGui.QTextCharFormat(fmt)
|
|
assert stripped_text[0] == self._escape[0]
|
|
while stripped_text and stripped_text[0] == self._escape[0]:
|
|
if self._escape.startswith(stripped_text):
|
|
# control sequence not complete
|
|
self._pending_text += stripped_text
|
|
stripped_text = ''
|
|
break
|
|
if not stripped_text.startswith(self._escape):
|
|
# check vt100 escape sequences
|
|
ctrl_seq = False
|
|
for alt_seq in self._escape_alts:
|
|
if stripped_text.startswith(alt_seq):
|
|
ctrl_seq = True
|
|
break
|
|
if not ctrl_seq:
|
|
# not a control sequence
|
|
self._pending_text = ''
|
|
ret_val.append(Operation('draw', FormattedText(stripped_text[:1], fmt)))
|
|
fmt = QtGui.QTextCharFormat(fmt)
|
|
stripped_text = stripped_text[1:]
|
|
continue
|
|
self._pending_text += _mid(stripped_text, 0, self._escape_len)
|
|
stripped_text = stripped_text[self._escape_len:]
|
|
|
|
# Non draw related command (cursor/erase)
|
|
if self._pending_text in [self._escape] + self._escape_alts:
|
|
m = self._supported_commands.match(stripped_text)
|
|
if m and self._pending_text == self._escape:
|
|
_, e = m.span()
|
|
n = m.group('n')
|
|
cmd = m.group('cmd')
|
|
if not n:
|
|
n = 0
|
|
ret_val.append(Operation(self._commands[cmd], n))
|
|
self._pending_text = ''
|
|
stripped_text = stripped_text[e:]
|
|
continue
|
|
else:
|
|
m = self._unsupported_command.match(stripped_text)
|
|
if m:
|
|
self._pending_text = ''
|
|
stripped_text = stripped_text[m.span()[1]:]
|
|
continue
|
|
elif self._pending_text in ['\x1b=', '\x1b>']:
|
|
self._pending_text = ''
|
|
continue
|
|
|
|
# Handle Select Graphic Rendition commands
|
|
# get the number
|
|
str_nbr = ''
|
|
numbers = []
|
|
while stripped_text:
|
|
if stripped_text[0].isdigit():
|
|
str_nbr += stripped_text[0]
|
|
else:
|
|
if str_nbr:
|
|
numbers.append(str_nbr)
|
|
if not str_nbr or stripped_text[0] != self._semicolon:
|
|
break
|
|
str_nbr = ''
|
|
self._pending_text += _mid(stripped_text, 0, 1)
|
|
stripped_text = stripped_text[1:]
|
|
|
|
if not stripped_text:
|
|
break
|
|
|
|
# remove terminating char
|
|
if not stripped_text.startswith(self._color_terminator):
|
|
# _logger().warn('removing %s', repr(self._pending_text + stripped_text[0]))
|
|
self._pending_text = ''
|
|
stripped_text = stripped_text[1:]
|
|
break
|
|
|
|
# got consistent control sequence, ok to clear pending text
|
|
self._pending_text = ''
|
|
stripped_text = stripped_text[1:]
|
|
|
|
if not numbers:
|
|
fmt = QtGui.QTextCharFormat(formatted_text.fmt)
|
|
self.end_format_scope()
|
|
|
|
i_offset = 0
|
|
n = len(numbers)
|
|
for i in range(n):
|
|
i += i_offset
|
|
code = int(numbers[i])
|
|
|
|
if self._TextColorStart <= code <= self._TextColorEnd:
|
|
fmt.setForeground(_ansi_color(code - self._TextColorStart, self.color_scheme))
|
|
self._set_format_scope(fmt)
|
|
elif self._BackgroundColorStart <= code <= self._BackgroundColorEnd:
|
|
fmt.setBackground(_ansi_color(code - self._BackgroundColorStart, self.color_scheme))
|
|
self._set_format_scope(fmt)
|
|
else:
|
|
if code == self._ResetFormat:
|
|
fmt = QtGui.QTextCharFormat(formatted_text.fmt)
|
|
self.end_format_scope()
|
|
elif code == self._BoldText:
|
|
fmt.setFontWeight(QtGui.QFont.Bold)
|
|
self._set_format_scope(fmt)
|
|
elif code == self._NotBold:
|
|
fmt.setFontWeight(QtGui.QFont.Normal)
|
|
self._set_format_scope(fmt)
|
|
elif code == self._ItalicText:
|
|
fmt.setFontItalic(True)
|
|
self._set_format_scope(fmt)
|
|
elif code == self._NotItalicNotFraktur:
|
|
fmt.setFontItalic(False)
|
|
self._set_format_scope(fmt)
|
|
elif code == self._UnderlinedText:
|
|
fmt.setUnderlineStyle(fmt.SingleUnderline)
|
|
fmt.setUnderlineColor(fmt.foreground().color())
|
|
self._set_format_scope(fmt)
|
|
elif code == self._NotUnderlined:
|
|
fmt.setUnderlineStyle(fmt.NoUnderline)
|
|
self._set_format_scope(fmt)
|
|
elif code == self._DefaultTextColor:
|
|
fmt.setForeground(formatted_text.fmt.foreground())
|
|
self._set_format_scope(fmt)
|
|
elif code == self._DefaultBackgroundColor:
|
|
fmt.setBackground(formatted_text.fmt.background())
|
|
self._set_format_scope(fmt)
|
|
elif code == self._Dim:
|
|
fmt = QtGui.QTextCharFormat(fmt)
|
|
fmt.setForeground(fmt.foreground().color().darker(self.DIM_FACTOR))
|
|
elif code == self._Negative:
|
|
normal_fmt = fmt
|
|
fmt = QtGui.QTextCharFormat(fmt)
|
|
fmt.setForeground(normal_fmt.background())
|
|
fmt.setBackground(normal_fmt.foreground())
|
|
elif code == self._Positive:
|
|
fmt = QtGui.QTextCharFormat(formatted_text.fmt)
|
|
elif code in [self._RgbBackgroundColor, self._RgbTextColor]:
|
|
# See http://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
|
i += 1
|
|
if i == n:
|
|
break
|
|
next_code = int(numbers[i])
|
|
if next_code == 2:
|
|
# RGB set with format: 38;2;<r>;<g>;<b>
|
|
if i + 3 < n:
|
|
method = fmt.setForeground if code == self._RgbTextColor else fmt.setBackground
|
|
method(QtGui.QColor(int(numbers[i + 1]), int(numbers[i + 2]), int(numbers[i + 3])))
|
|
self._set_format_scope(fmt)
|
|
i_offset = 3
|
|
elif next_code == 5:
|
|
# 256 color mode with format: 38;5;<i>
|
|
index = int(numbers[i + 1])
|
|
if index < 8:
|
|
# The first 8 colors are standard low-intensity ANSI colors.
|
|
color = _ansi_color(index, self.color_scheme)
|
|
elif index < 16:
|
|
# The next 8 colors are standard high-intensity ANSI colors.
|
|
color = _ansi_color(index - 8, self.color_scheme).lighter(150)
|
|
elif index < 232:
|
|
# The next 216 colors are a 6x6x6 RGB cube.
|
|
o = index - 16
|
|
color = QtGui.QColor((o / 36) * 51, ((o / 6) % 6) * 51, (o % 6) * 51)
|
|
else:
|
|
# The last 24 colors are a greyscale gradient.
|
|
grey = (index - 232) * 11
|
|
color = QtGui.QColor(grey, grey, grey)
|
|
|
|
if code == self._RgbTextColor:
|
|
fmt.setForeground(color)
|
|
else:
|
|
fmt.setBackground(color)
|
|
|
|
self._set_format_scope(fmt)
|
|
else:
|
|
_logger().warn('unsupported SGR code: %r', code)
|
|
return ret_val
|
|
|
|
def end_format_scope(self):
|
|
"""
|
|
Close the format scope
|
|
"""
|
|
self._prev_fmt_closed = True
|
|
|
|
def _set_format_scope(self, fmt):
|
|
"""
|
|
Opens the format scope.
|
|
"""
|
|
self._prev_fmt = QtGui.QTextCharFormat(fmt)
|
|
self._prev_fmt_closed = False
|
|
|
|
|
|
def _mid(string, start, end=None):
|
|
"""
|
|
Returns a substring delimited by start and end position.
|
|
"""
|
|
if end is None:
|
|
end = len(string)
|
|
return string[start:start + end]
|
|
|
|
|
|
def _ansi_color(code, theme):
|
|
"""
|
|
Converts an ansi code to a QColor, taking the color scheme (theme) into account.
|
|
"""
|
|
red = 170 if code & 1 else 0
|
|
green = 170 if code & 2 else 0
|
|
blue = 170 if code & 4 else 0
|
|
color = QtGui.QColor(red, green, blue)
|
|
if theme is not None:
|
|
mappings = {
|
|
'#aa0000': theme.red,
|
|
'#00aa00': theme.green,
|
|
'#aaaa00': theme.yellow,
|
|
'#0000aa': theme.blue,
|
|
'#aa00aa': theme.magenta,
|
|
'#00aaaa': theme.cyan,
|
|
'#000000': theme.background,
|
|
"#ffffff": theme.foreground
|
|
}
|
|
try:
|
|
return mappings[color.name()]
|
|
except KeyError:
|
|
pass
|
|
return color
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
# Input handlers
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
class InputHandler(object):
|
|
"""
|
|
Base class for handling user inputs
|
|
"""
|
|
def __init__(self):
|
|
# references set by the outout window instance that owns the handler.
|
|
self.edit = None
|
|
self.process = None
|
|
|
|
|
|
class ImmediateInputHandler(InputHandler):
|
|
"""
|
|
Write ascii key code immediately to the process' stdin.
|
|
"""
|
|
def key_press_event(self, event):
|
|
"""
|
|
Directly writes the ascii code of the key to the process' stdin.
|
|
|
|
:retuns: False to prevent the event from being propagated to the parent widget.
|
|
"""
|
|
if event.key() == QtCore.Qt.Key_Return:
|
|
cursor = self.edit.textCursor()
|
|
cursor.movePosition(cursor.EndOfBlock)
|
|
self.edit.setTextCursor(cursor)
|
|
code = _qkey_to_ascii(event)
|
|
if code:
|
|
self.process.writeData(code)
|
|
return False
|
|
return True
|
|
|
|
def paste(self, text):
|
|
self.process.write(text.encode())
|
|
|
|
|
|
def _qkey_to_ascii(event):
|
|
"""
|
|
(Try to) convert the Qt key event to the corresponding ASCII sequence for
|
|
the terminal. This works fine for standard alphanumerical characters, but
|
|
most other characters require terminal specific control_modifier sequences.
|
|
The conversion below works for TERM="linux' terminals.
|
|
"""
|
|
if sys.platform == 'darwin':
|
|
control_modifier = QtCore.Qt.MetaModifier
|
|
else:
|
|
control_modifier = QtCore.Qt.ControlModifier
|
|
ctrl = int(event.modifiers() & control_modifier) != 0
|
|
if ctrl:
|
|
if event.key() == QtCore.Qt.Key_P:
|
|
return b'\x10'
|
|
elif event.key() == QtCore.Qt.Key_N:
|
|
return b'\x0E'
|
|
elif event.key() == QtCore.Qt.Key_C:
|
|
return b'\x03'
|
|
elif event.key() == QtCore.Qt.Key_L:
|
|
return b'\x0C'
|
|
elif event.key() == QtCore.Qt.Key_B:
|
|
return b'\x02'
|
|
elif event.key() == QtCore.Qt.Key_F:
|
|
return b'\x06'
|
|
elif event.key() == QtCore.Qt.Key_D:
|
|
return b'\x04'
|
|
elif event.key() == QtCore.Qt.Key_O:
|
|
return b'\x0F'
|
|
elif event.key() == QtCore.Qt.Key_V:
|
|
return QtWidgets.qApp.clipboard().text().encode('utf-8')
|
|
else:
|
|
return None
|
|
else:
|
|
if event.key() == QtCore.Qt.Key_Return:
|
|
return '\n'.encode('utf-8')
|
|
elif event.key() == QtCore.Qt.Key_Enter:
|
|
return '\n'.encode('utf-8')
|
|
elif event.key() == QtCore.Qt.Key_Tab:
|
|
return '\t'.encode('utf-8')
|
|
elif event.key() == QtCore.Qt.Key_Backspace:
|
|
return b'\x08'
|
|
elif event.key() == QtCore.Qt.Key_Delete:
|
|
return b'\x06\x08'
|
|
elif event.key() == QtCore.Qt.Key_Enter:
|
|
return '\n'.encode('utf-8')
|
|
elif event.key() == QtCore.Qt.Key_Home:
|
|
return b'\x1b[H'
|
|
elif event.key() == QtCore.Qt.Key_End:
|
|
return b'\x1b[F'
|
|
elif event.key() == QtCore.Qt.Key_Left:
|
|
return b'\x02'
|
|
elif event.key() == QtCore.Qt.Key_Up:
|
|
return b'\x10'
|
|
elif event.key() == QtCore.Qt.Key_Right:
|
|
return b'\x06'
|
|
elif event.key() == QtCore.Qt.Key_Down:
|
|
return b'\x0E'
|
|
elif event.key() == QtCore.Qt.Key_PageUp:
|
|
return b'\x49'
|
|
elif event.key() == QtCore.Qt.Key_PageDown:
|
|
return b'\x51'
|
|
elif event.key() == QtCore.Qt.Key_F1:
|
|
return b'\x1b\x31'
|
|
elif event.key() == QtCore.Qt.Key_F2:
|
|
return b'\x1b\x32'
|
|
elif event.key() == QtCore.Qt.Key_F3:
|
|
return b'\x00\x3b'
|
|
elif event.key() == QtCore.Qt.Key_F4:
|
|
return b'\x1b\x34'
|
|
elif event.key() == QtCore.Qt.Key_F5:
|
|
return b'\x1b\x35'
|
|
elif event.key() == QtCore.Qt.Key_F6:
|
|
return b'\x1b\x36'
|
|
elif event.key() == QtCore.Qt.Key_F7:
|
|
return b'\x1b\x37'
|
|
elif event.key() == QtCore.Qt.Key_F8:
|
|
return b'\x1b\x38'
|
|
elif event.key() == QtCore.Qt.Key_F9:
|
|
return b'\x1b\x39'
|
|
elif event.key() == QtCore.Qt.Key_F10:
|
|
return b'\x1b\x30'
|
|
elif event.key() == QtCore.Qt.Key_F11:
|
|
return b'\x45'
|
|
elif event.key() == QtCore.Qt.Key_F12:
|
|
return b'\x46'
|
|
elif event.text() in ('abcdefghijklmnopqrstuvwxyz'
|
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
'[],=-.;/`&^~*@|#(){}$><%+?"_!'
|
|
"'\\ :"):
|
|
return event.text().encode('utf8')
|
|
else:
|
|
return None
|
|
|
|
|
|
class CommandHistory(object):
|
|
"""
|
|
A very basic history of commands.
|
|
|
|
Use add_command when user press RETURN, use scroll_up/scroll_down to scroll the history.
|
|
"""
|
|
def __init__(self):
|
|
self._history = []
|
|
self._index = -1
|
|
|
|
def add_command(self, command):
|
|
"""
|
|
Adds a command to the history and reset history index.
|
|
"""
|
|
try:
|
|
self._history.remove(command)
|
|
except ValueError:
|
|
pass
|
|
self._history.insert(0, command)
|
|
self._index = -1
|
|
|
|
def scroll_up(self):
|
|
"""
|
|
Returns the previous command, if any.
|
|
"""
|
|
self._index += 1
|
|
nb_commands = len(self._history)
|
|
if self._index >= nb_commands:
|
|
self._index = nb_commands - 1
|
|
try:
|
|
return self._history[self._index]
|
|
except IndexError:
|
|
return ''
|
|
|
|
def scroll_down(self):
|
|
"""
|
|
Returns the next command if any.
|
|
"""
|
|
self._index -= 1
|
|
if self._index < 0:
|
|
self._index = -1
|
|
return ''
|
|
try:
|
|
return self._history[self._index]
|
|
except IndexError:
|
|
return ''
|
|
|
|
|
|
class BufferedInputHandler(InputHandler):
|
|
"""
|
|
Bufferise user inputs until user press RETURN.
|
|
|
|
Use :class:`CommandHistory` to manage the history of commands/inputs.
|
|
"""
|
|
def __init__(self):
|
|
super(BufferedInputHandler, self).__init__()
|
|
self._history = CommandHistory()
|
|
|
|
def _insert_command(self, command):
|
|
"""
|
|
Insert command by replacing the current input buffer and display it on the text edit.
|
|
"""
|
|
self._clear_user_buffer()
|
|
tc = self.edit.textCursor()
|
|
tc.insertText(command)
|
|
self.edit.setTextCursor(tc)
|
|
|
|
def _clear_user_buffer(self):
|
|
tc = self.edit.textCursor()
|
|
for _ in self._get_input_buffer():
|
|
tc.deletePreviousChar()
|
|
self.edit.setTextCursor(tc)
|
|
|
|
def is_code_completion_popup_visible(self):
|
|
try:
|
|
mode = self.edit.modes.get('CodeCompletionMode')
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
return mode._completer.popup().isVisible()
|
|
|
|
def key_press_event(self, event):
|
|
"""
|
|
Manages our own buffer and send it to the subprocess when user pressed RETURN.
|
|
"""
|
|
input_buffer = self._get_input_buffer()
|
|
ctrl = int(event.modifiers() & QtCore.Qt.ControlModifier) != 0
|
|
shift = int(event.modifiers() & QtCore.Qt.ShiftModifier) != 0
|
|
delete = event.key() in [QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete]
|
|
ignore = False
|
|
if delete and not input_buffer and not shift:
|
|
return False
|
|
if ctrl:
|
|
if shift and event.key() == QtCore.Qt.Key_V:
|
|
self.edit.insertPlainText(QtWidgets.qApp.clipboard().text())
|
|
return False
|
|
elif event.key() == QtCore.Qt.Key_L:
|
|
self.edit.clear()
|
|
if sys.platform == 'win32':
|
|
self.process.write(b'\r')
|
|
self.process.write(b'\n')
|
|
return False
|
|
if (shift or ctrl) and event.key() == QtCore.Qt.Key_Backspace:
|
|
if input_buffer.strip() != '':
|
|
return True
|
|
self._clear_user_buffer()
|
|
return False
|
|
if event.key() == QtCore.Qt.Key_Up:
|
|
if self.is_code_completion_popup_visible():
|
|
return True
|
|
self._insert_command(self._history.scroll_up())
|
|
return False
|
|
if event.key() == QtCore.Qt.Key_Left:
|
|
return bool(input_buffer)
|
|
if event.key() == QtCore.Qt.Key_Down:
|
|
if self.is_code_completion_popup_visible():
|
|
return True
|
|
self._insert_command(self._history.scroll_down())
|
|
return False
|
|
if event.key() == QtCore.Qt.Key_Home:
|
|
tc = self.edit.textCursor()
|
|
tc.movePosition(tc.StartOfBlock)
|
|
tc.movePosition(tc.Right, tc.MoveAnchor, self.edit._formatter._prefix_len)
|
|
self.edit.setTextCursor(tc)
|
|
return False
|
|
if event.key() == QtCore.Qt.Key_End:
|
|
tc = self.edit.textCursor()
|
|
tc.movePosition(tc.EndOfBlock)
|
|
self.edit.setTextCursor(tc)
|
|
self._cursor_pos = len(self._get_input_buffer())
|
|
return False
|
|
if event.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
|
|
if self.is_code_completion_popup_visible():
|
|
return True
|
|
tc = self.edit.textCursor()
|
|
tc.movePosition(tc.EndOfBlock)
|
|
self.edit.setTextCursor(tc)
|
|
# send the user input to the child process
|
|
if self.edit.flg_use_pty or 'cmd.exe' in self.process.program():
|
|
# remove user buffer from text edit, the content of the buffer will be
|
|
# drawn as soon as we write it to the process stdin
|
|
tc = self.edit.textCursor()
|
|
for _ in input_buffer:
|
|
tc.deletePreviousChar()
|
|
self.edit.setTextCursor(tc)
|
|
self._history.add_command(input_buffer)
|
|
if sys.platform == 'win32':
|
|
input_buffer += "\r"
|
|
input_buffer += "\n"
|
|
self.process.write(input_buffer.encode())
|
|
if self.edit.flg_use_pty or 'cmd.exe' in self.process.program():
|
|
ignore = True
|
|
return not ignore
|
|
|
|
def _get_input_buffer(self):
|
|
current_line = TextHelper(self.edit).current_line_text()
|
|
return current_line[self.edit._formatter._prefix_len:]
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
# Formatter
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
class OutputFormat:
|
|
"""
|
|
Enumerates the possible output formats.
|
|
"""
|
|
#: format used to display normal messages
|
|
NormalMessageFormat = 0
|
|
#: format used to display normal messages
|
|
ErrorMessageFormat = 1
|
|
#: format used to display custom messages
|
|
CustomFormat = 2
|
|
|
|
|
|
class OutputFormatter(object):
|
|
"""
|
|
Perform formatting (draw text, move cursor,...).
|
|
"""
|
|
@property
|
|
def color_scheme(self):
|
|
"""
|
|
Gets/Sets the formatter color scheme
|
|
"""
|
|
return self._color_scheme
|
|
|
|
@color_scheme.setter
|
|
def color_scheme(self, new_theme):
|
|
self._color_scheme = new_theme
|
|
self._init_formats()
|
|
self._parser.color_scheme = new_theme
|
|
|
|
def __init__(self, text_edit, color_scheme=None):
|
|
if text_edit is None:
|
|
raise ValueError('text_edit parameter cannot be None')
|
|
self._last_cursor_pos = 0
|
|
self._text_edit = text_edit
|
|
self._parser = AnsiEscapeCodeParser()
|
|
self._cursor = text_edit.textCursor()
|
|
self._formats = {}
|
|
self._overwrite_output = False
|
|
_init_default_scheme()
|
|
self._color_scheme = color_scheme
|
|
if self._color_scheme is None:
|
|
self._color_scheme = OutputWindow.DefaultColorScheme
|
|
self._init_formats()
|
|
self._parser.color_scheme = self._color_scheme
|
|
self.flg_bash = False
|
|
|
|
def append_message(self, text, output_format=OutputFormat.NormalMessageFormat):
|
|
"""
|
|
Parses and append message to the text edit.
|
|
"""
|
|
self._append_message(text, self._formats[output_format])
|
|
|
|
def flush(self):
|
|
"""
|
|
Flush intermediary resutls: close the format scope.
|
|
"""
|
|
self._parser.end_format_scope()
|
|
|
|
# Utility methods
|
|
def _append_message(self, text, char_format):
|
|
"""
|
|
Parses text and executes parsed operations.
|
|
"""
|
|
self._cursor = self._text_edit.textCursor()
|
|
operations = self._parser.parse_text(FormattedText(text, char_format))
|
|
for i, operation in enumerate(operations):
|
|
try:
|
|
func = getattr(self, '_%s' % operation.command)
|
|
except AttributeError:
|
|
print('command not implemented: %r - %r' % (
|
|
operation.command, operation.data))
|
|
else:
|
|
try:
|
|
func(operation.data)
|
|
except Exception:
|
|
_logger().exception('exception while running %r', operation)
|
|
# uncomment next line for debugging commands
|
|
self._text_edit.repaint()
|
|
|
|
def _init_formats(self):
|
|
"""
|
|
Initialise default formats.
|
|
"""
|
|
theme = self._color_scheme
|
|
# normal message format
|
|
fmt = QtGui.QTextCharFormat()
|
|
fmt.setForeground(theme.foreground)
|
|
fmt.setBackground(theme.background)
|
|
self._formats[OutputFormat.NormalMessageFormat] = fmt
|
|
|
|
# error message
|
|
fmt = QtGui.QTextCharFormat()
|
|
fmt.setForeground(theme.error)
|
|
fmt.setBackground(theme.background)
|
|
self._formats[OutputFormat.ErrorMessageFormat] = fmt
|
|
|
|
# debug message
|
|
fmt = QtGui.QTextCharFormat()
|
|
fmt.setForeground(theme.custom)
|
|
fmt.setBackground(theme.background)
|
|
self._formats[OutputFormat.CustomFormat] = fmt
|
|
|
|
# Commands implementation
|
|
def _draw(self, data):
|
|
"""
|
|
Draw text
|
|
"""
|
|
self._cursor.clearSelection()
|
|
self._cursor.setPosition(self._last_cursor_pos)
|
|
|
|
if '\x07' in data.txt:
|
|
print('\a')
|
|
txt = data.txt.replace('\x07', '')
|
|
|
|
if '\x08' in txt:
|
|
parts = txt.split('\x08')
|
|
else:
|
|
parts = [txt]
|
|
|
|
for i, part in enumerate(parts):
|
|
if part:
|
|
part = part.replace('\r\r', '\r')
|
|
if len(part) >= 80 * 24 * 8:
|
|
# big output, process it in one step (\r and \n will not be handled)
|
|
self._draw_chars(data, part)
|
|
continue
|
|
to_draw = ''
|
|
for n, char in enumerate(part):
|
|
if char == '\n':
|
|
self._draw_chars(data, to_draw)
|
|
to_draw = ''
|
|
self._linefeed()
|
|
elif char == '\r':
|
|
self._draw_chars(data, to_draw)
|
|
to_draw = ''
|
|
self._erase_in_line(0)
|
|
try:
|
|
nchar = part[n + 1]
|
|
except IndexError:
|
|
nchar = None
|
|
if self._cursor.positionInBlock() > 80 and self.flg_bash and nchar != '\n':
|
|
self._linefeed()
|
|
self._cursor.movePosition(self._cursor.StartOfBlock)
|
|
self._text_edit.setTextCursor(self._cursor)
|
|
else:
|
|
to_draw += char
|
|
if to_draw:
|
|
self._draw_chars(data, to_draw)
|
|
if i != len(parts) - 1:
|
|
self._cursor_back(1)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
self._prefix_len = self._cursor.positionInBlock()
|
|
self._text_edit.setTextCursor(self._cursor)
|
|
|
|
def _draw_chars(self, data, to_draw):
|
|
"""
|
|
Draw the specified charachters using the specified format.
|
|
"""
|
|
i = 0
|
|
while not self._cursor.atBlockEnd() and i < len(to_draw) and len(to_draw) > 1:
|
|
self._cursor.deleteChar()
|
|
i += 1
|
|
self._cursor.insertText(to_draw, data.fmt)
|
|
|
|
def _linefeed(self):
|
|
"""
|
|
Performs a line feed.
|
|
"""
|
|
last_line = self._cursor.blockNumber() == self._text_edit.blockCount() - 1
|
|
if self._cursor.atEnd() or last_line:
|
|
if last_line:
|
|
self._cursor.movePosition(self._cursor.EndOfBlock)
|
|
self._cursor.insertText('\n')
|
|
else:
|
|
self._cursor.movePosition(self._cursor.Down)
|
|
self._cursor.movePosition(self._cursor.StartOfBlock)
|
|
self._text_edit.setTextCursor(self._cursor)
|
|
|
|
def _cursor_down(self, value):
|
|
"""
|
|
Moves the cursor down by ``value``.
|
|
"""
|
|
self._cursor.clearSelection()
|
|
if self._cursor.atEnd():
|
|
self._cursor.insertText('\n')
|
|
else:
|
|
self._cursor.movePosition(self._cursor.Down, self._cursor.MoveAnchor, value)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _cursor_up(self, value):
|
|
"""
|
|
Moves the cursor up by ``value``.
|
|
"""
|
|
value = int(value)
|
|
if value == 0:
|
|
value = 1
|
|
self._cursor.clearSelection()
|
|
self._cursor.movePosition(self._cursor.Up, self._cursor.MoveAnchor, value)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _cursor_position(self, data):
|
|
"""
|
|
Moves the cursor position.
|
|
"""
|
|
column, line = self._get_line_and_col(data)
|
|
self._move_cursor_to_line(line)
|
|
self._move_cursor_to_column(column)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _move_cursor_to_column(self, column):
|
|
"""
|
|
Moves the cursor to the specified column, if possible.
|
|
"""
|
|
last_col = len(self._cursor.block().text())
|
|
self._cursor.movePosition(self._cursor.EndOfBlock)
|
|
to_insert = ''
|
|
for i in range(column - last_col):
|
|
to_insert += ' '
|
|
if to_insert:
|
|
self._cursor.insertText(to_insert)
|
|
self._cursor.movePosition(self._cursor.StartOfBlock)
|
|
self._cursor.movePosition(self._cursor.Right, self._cursor.MoveAnchor, column)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _move_cursor_to_line(self, line):
|
|
"""
|
|
Moves the cursor to the specified line, if possible.
|
|
"""
|
|
last_line = self._text_edit.document().blockCount() - 1
|
|
self._cursor.clearSelection()
|
|
self._cursor.movePosition(self._cursor.End)
|
|
to_insert = ''
|
|
for i in range(line - last_line):
|
|
to_insert += '\n'
|
|
if to_insert:
|
|
self._cursor.insertText(to_insert)
|
|
self._cursor.movePosition(self._cursor.Start)
|
|
self._cursor.movePosition(self._cursor.Down, self._cursor.MoveAnchor, line)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _cursor_horizontal_absolute(self, column):
|
|
"""
|
|
Moves the cursor to the specified column, if possible.
|
|
"""
|
|
self._move_cursor_to_column(int(column) - 1)
|
|
|
|
@staticmethod
|
|
def _get_line_and_col(data):
|
|
"""
|
|
Gets line and column from a string like the following: "1;5" or "1;" or ";5"
|
|
|
|
and convers the column/line numbers to 0 base.
|
|
"""
|
|
try:
|
|
line, column = data.split(';')
|
|
except AttributeError:
|
|
line = int(data)
|
|
column = 1
|
|
# handle empty values and convert them to 0 based indices
|
|
if not line:
|
|
line = 0
|
|
else:
|
|
line = int(line) - 1
|
|
if line < 0:
|
|
line = 0
|
|
if not column:
|
|
column = 0
|
|
else:
|
|
column = int(column) - 1
|
|
if column < 0:
|
|
column = 0
|
|
return column, line
|
|
|
|
def _erase_in_line(self, value):
|
|
"""
|
|
Erases charachters in line.
|
|
"""
|
|
initial_pos = self._cursor.position()
|
|
if value == 0:
|
|
# delete end of line
|
|
self._cursor.movePosition(self._cursor.EndOfBlock, self._cursor.KeepAnchor)
|
|
elif value == 1:
|
|
# delete start of line
|
|
self._cursor.movePosition(self._cursor.StartOfBlock, self._cursor.KeepAnchor)
|
|
else:
|
|
# delete whole line
|
|
self._cursor.movePosition(self._cursor.StartOfBlock)
|
|
self._cursor.movePosition(self._cursor.EndOfBlock, self._cursor.KeepAnchor)
|
|
self._cursor.insertText(' ' * len(self._cursor.selectedText()))
|
|
self._cursor.setPosition(initial_pos)
|
|
self._text_edit.setTextCursor(self._cursor)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _erase_display(self, value):
|
|
"""
|
|
Erases display.
|
|
"""
|
|
if value == 0:
|
|
# delete end of line
|
|
self._cursor.movePosition(self._cursor.End, self._cursor.KeepAnchor)
|
|
elif value == 1:
|
|
# delete start of line
|
|
self._cursor.movePosition(self._cursor.Start, self._cursor.KeepAnchor)
|
|
else:
|
|
# delete whole line
|
|
self._cursor.movePosition(self._cursor.Start)
|
|
self._cursor.movePosition(self._cursor.End, self._cursor.KeepAnchor)
|
|
self._cursor.removeSelectedText()
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _cursor_back(self, value):
|
|
"""
|
|
Moves the cursor back.
|
|
"""
|
|
if value <= 0:
|
|
value = 1
|
|
self._cursor.movePosition(self._cursor.Left, self._cursor.MoveAnchor, value)
|
|
self._text_edit.setTextCursor(self._cursor)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _cursor_forward(self, value):
|
|
"""
|
|
Moves the cursor forward.
|
|
"""
|
|
if value <= 0:
|
|
value = 1
|
|
self._cursor.movePosition(self._cursor.Right, self._cursor.MoveAnchor, value)
|
|
self._text_edit.setTextCursor(self._cursor)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
def _delete_chars(self, value):
|
|
"""
|
|
Deletes the specified number of charachters.
|
|
"""
|
|
value = int(value)
|
|
if value <= 0:
|
|
value = 1
|
|
for i in range(value):
|
|
self._cursor.deleteChar()
|
|
self._text_edit.setTextCursor(self._cursor)
|
|
self._last_cursor_pos = self._cursor.position()
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
# Color schemes definition
|
|
# ----------------------------------------------------------------------------------------------------------------------
|
|
def _init_default_scheme():
|
|
"""
|
|
Initialises the default color scheme with colors based on QPalette.
|
|
|
|
Call this function once after QApplication has been created (otherwise QPalette cannot be used).
|
|
"""
|
|
if OutputWindow.DefaultColorScheme is None:
|
|
OutputWindow.DefaultColorScheme = OutputWindow.create_color_scheme()
|
|
|
|
# Initialize non-palette dependant themes.
|
|
OutputWindow.LinuxColorScheme = OutputWindow.create_color_scheme(
|
|
background=QColor('black'), foreground=QColor('white'), red=QColor('#FF5555'), green=QColor('#55FF55'),
|
|
yellow=QColor('#FFFF55'), blue=QColor('#5555FF'), magenta=QColor('#FF55FF'), cyan=QColor('#55FFFF'))
|
|
#: Tango theme (black background with pastel colors).
|
|
OutputWindow.TangoColorScheme = OutputWindow.create_color_scheme(
|
|
background=QColor('black'), foreground=QColor('white'), red=QColor('#CC0000'), green=QColor('#4E9A06'),
|
|
yellow=QColor('#C4A000'), blue=QColor('#3465A4'), magenta=QColor('#75507B'), cyan=QColor('#06989A'))
|
|
#: The famous Solarized dark theme.
|
|
OutputWindow.SolarizedColorScheme = OutputWindow.create_color_scheme(
|
|
background=QColor('#073642'), foreground=QColor('#EEE8D5'), red=QColor('#DC322F'), green=QColor('#859900'),
|
|
yellow=QColor('#B58900'), blue=QColor('#268BD2'), magenta=QColor('#D33682'), cyan=QColor('#2AA198'))
|
|
|
|
|
|
def _logger():
|
|
"""
|
|
Returns a logger instance for this module.
|
|
"""
|
|
return logging.getLogger(__name__)
|