# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ This module implements a QT4 Python interpreter widget. It is inspired by PyCute : http://gerard.vermeulen.free.fr """ import sys from .qt import QtCore, QtGui, QtWidgets class MultipleRedirection(QtCore.QObject): writed = QtCore.Signal(str) def __init__(self, console, stdout): super().__init__() self.console = console self.stdout = stdout def write(self, str): self.writed.emit(str) if self.stdout.encoding is None: str = str.encode("ascii", "ignore").decode("ascii", "ignore") elif self.stdout.encoding != "UTF-8": str = str.encode(self.stdout.encoding, "ignore").decode(self.stdout.encoding, "ignore") self.stdout.write(str) def isatty(self): return self.console.isatty() or self.stdout.isatty() def flush(self): self.console.flush() try: self.stdout.flush() # On OSX when frozen flush raise a BrokenPipeError except BrokenPipeError: pass class PyCutExt(QtWidgets.QTextEdit): """ PyCute is a Python shell for PyQt. Creating, displaying and controlling PyQt widgets from the Python command line interpreter is very hard, if not, impossible. PyCute solves this problem by interfacing the Python interpreter to a PyQt widget. """ def __init__(self, interpreter, message="", log="", parent=None): super().__init__(parent) self.interpreter = interpreter self._default_text_color = QtGui.QColor(0, 0, 0) # black self.colorizer = SyntaxColor() # session log self.log = log or '' # to exit the main interpreter by a Ctrl-D if PyCute has no parent if parent is None: self.eofKey = QtCore.Qt.Key.Key_D else: self.eofKey = None # capture all interactive input/output self._old_stdout = sys.stdout sys.stdout = MultipleRedirection(self, sys.stdout) sys.stdout.writed.connect(self.write) # sys.stderr = MultipleRedirection((sys.stderr, self)) self._old_stdin = sys.stdin sys.stdin = self # last line + last incomplete lines self.line = "" self.lines = [] # the cursor position in the last line self.point = 0 # flag: the interpreter needs more input to run the last lines. self.more = 0 # flag: readline() is being used for e.g. raw_input() and input() self.reading = 0 # history self.history = [] self.pointer = 0 self.cursor_pos = 0 self.setLineWrapMode(QtWidgets.QTextEdit.LineWrapMode.NoWrap) try: sys.ps1 except AttributeError: sys.ps1 = ">>> " try: sys.ps2 except AttributeError: sys.ps2 = "... " self.write(message + '\n\n') self.write(sys.ps1) def closeIO(self): """ Call when parent is closing we need to stop writing in the windows and go back to the standard output """ sys.stdout = self._old_stdout sys.stdin = self._old_stdin def get_interpreter(self): """ Return the interpreter object """ return self.interpreter def moveCursor(self, operation, mode=QtGui.QTextCursor.MoveMode.MoveAnchor): """ Convenience function to move the cursor This function will be present in PyQT4.2 """ cursor = self.textCursor() cursor.movePosition(operation, mode) self.setTextCursor(cursor) def flush(self): """ Simulate stdin, stdout, and stderr. """ pass def isatty(self): """ Simulate stdin, stdout, and stderr. """ return 1 def readline(self): """ Simulate stdin, stdout, and stderr. """ self.reading = 1 self._clearLine() self.moveCursor(QtGui.QTextCursor.MoveOperation.End) while self.reading: QtWidgets.QApplication.processEvents(QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 1000) if len(self.line) == 0: return '\n' else: return self.line def setDefaultTextColor(self, color): """ Set the default text color and reset the colorizer with the new color. """ self._default_text_color = color self.colorizer.set_default_text_color(color) self.setTextColor(color) # reset everything self.clear() self.write(self.intro + '\n\n') self.write(sys.ps1) def write(self, text, error=False, warning=False): """ Simulates stdin, stdout, and stderr. """ cursor = self.textCursor() cursor.movePosition(QtGui.QTextCursor.MoveOperation.End) pos1 = cursor.position() cursor.insertText(text) self.cursor_pos = cursor.position() self.setTextCursor(cursor) self.ensureCursorVisible() # Set the format cursor.setPosition(pos1, QtGui.QTextCursor.MoveMode.KeepAnchor) char_format = cursor.charFormat() if error: color = QtGui.QColor(255, 0, 0) # red elif warning: color = QtGui.QColor(255, 128, 0) # orange else: color = self._default_text_color char_format.setForeground(QtGui.QBrush(color)) cursor.setCharFormat(char_format) def writelines(self, text): """ Simulate stdin, stdout, and stderr. """ map(self.write, text) def _run(self): """ Append the last line to the history list, let the interpreter execute the last line(s), and clean up accounting for the interpreter results: (1) the interpreter succeeds (2) the interpreter fails, finds no errors and wants more line(s) (3) the interpreter fails, finds errors and writes them to sys.stderr """ self.pointer = 0 self.history.append(self.line) try: self.lines.append(self.line) except Exception as e: print(e) source = '\n'.join(self.lines) self.more = self.interpreter.runsource(source) if self.more: self.write(sys.ps2) else: self.write(sys.ps1) self.lines = [] self._clearLine() def _clearLine(self): """ Clear input line buffer """ self.line = "" self.point = 0 def _insertText(self, text): """ Inserts text at the current cursor position. """ self.line = self.line[:self.point] + text + self.line[self.point:] self.point += len(text) cursor = self.textCursor() cursor.insertText(text) self.color_line() def keyPressEvent(self, e): """ Handle user input a key at a time. """ text = e.text() key = e.key() if e.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier: return super().keyPressEvent(e) # Keep the cursor after the last prompt. self.moveCursor(QtGui.QTextCursor.MoveOperation.End) if key == QtCore.Qt.Key.Key_Backspace: if self.point: cursor = self.textCursor() cursor.movePosition(QtGui.QTextCursor.MoveOperation.PreviousCharacter, QtGui.QTextCursor.MoveMode.KeepAnchor) cursor.removeSelectedText() self.color_line() self.point -= 1 self.line = self.line[:-1] elif key == QtCore.Qt.Key.Key_Delete: cursor = self.textCursor() cursor.movePosition(QtGui.QTextCursor.MoveOperation.NextCharacter, QtGui.QTextCursor.MoveMode.KeepAnchor) cursor.removeSelectedText() self.color_line() self.line = self.line[:-1] elif key == QtCore.Qt.Key.Key_Return or key == QtCore.Qt.Key.Key_Enter: self.write("\n") if self.reading: self.reading = 0 else: self._run() elif key == QtCore.Qt.Key.Key_Tab: self.onKeyPress_Tab() elif key == QtCore.Qt.Key.Key_Left: if self.point: self.moveCursor(QtGui.QTextCursor.MoveOperation.Left) self.point -= 1 elif key == QtCore.Qt.Key.Key_Right: if self.point < len(self.line): self.moveCursor(QtGui.QTextCursor.MoveOperation.Right) self.point += 1 elif key == QtCore.Qt.Key.Key_Home: cursor = self.textCursor() cursor.setPosition(self.cursor_pos) self.setTextCursor(cursor) self.point = 0 elif key == QtCore.Qt.Key.Key_End: self.moveCursor(QtGui.QTextCursor.MoveOperation.EndOfLine) self.point = len(self.line) elif key == QtCore.Qt.Key.Key_Up: if len(self.history): if self.pointer == 0: self.pointer = len(self.history) self.pointer -= 1 self._recall() elif key == QtCore.Qt.Key.Key_Down: if len(self.history): self.pointer += 1 if self.pointer == len(self.history): self.pointer = 0 self._recall() elif len(text): self._insertText(text) return def _recall(self): """ Display the current item from the command history. """ cursor = self.textCursor() cursor.select(QtGui.QTextCursor.SelectionType.LineUnderCursor) cursor.removeSelectedText() if self.more: self.write(sys.ps2) else: self.write(sys.ps1) self._clearLine() self._insertText(self.history[self.pointer]) def contentsContextMenuEvent(self, ev): """ Suppress the right button context menu. """ return def color_line(self): """ Color the current line. """ cursor = self.textCursor() cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfLine) newpos = cursor.position() pos = -1 while (newpos != pos): cursor.movePosition(QtGui.QTextCursor.MoveOperation.NextWord) pos = newpos newpos = cursor.position() cursor.select(QtGui.QTextCursor.SelectionType.WordUnderCursor) word = cursor.selectedText() if not word: continue color = self.colorizer.get_color(word) char_format = cursor.charFormat() char_format.setForeground(QtGui.QBrush(color)) cursor.setCharFormat(char_format) class SyntaxColor: """ Allows to color Python keywords. """ keywords = set(["and", "del", "from", "not", "while", "as", "elif", "global", "or", "with", "assert", "else", "if", "pass", "yield", "break", "except", "import", "print", "class", "exec", "in", "raise", "continue", "finally", "is", "return", "def", "for", "lambda", "try"]) def __init__(self): self._default_text_color = QtGui.QColor(0, 0, 0) # black def set_default_text_color(self, default_text_color): self._default_text_color = default_text_color def get_color(self, word): """ Return a color based on the string word. """ stripped = word.strip() if stripped in self.keywords: return QtGui.QColor(165, 42, 42) # brown else: return self._default_text_color