Last active
March 4, 2022 15:55
-
-
Save vivekjoshy/a986a2241e15e95be7489f443e79fb7f to your computer and use it in GitHub Desktop.
Thread Based PyQt5 Python Console
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import time | |
from code import InteractiveConsole | |
from PyQt5 import uic | |
from PyQt5.QtCore import pyqtSlot, QThread, QObject, pyqtSignal, QTimer | |
from PyQt5.QtGui import QTextOption, QTextCursor | |
from PyQt5.QtWidgets import QApplication | |
__author__ = "daegontaven" | |
__copyright__ = "daegontaven" | |
__license__ = "gpl3" | |
class ConsoleBuffer(QObject): | |
excrete = pyqtSignal(str) | |
def __init__(self, parent=None, minimum=0.050): | |
super(ConsoleBuffer, self).__init__(parent) | |
self.minimum = minimum | |
self.last_time = time.monotonic() - minimum | |
self.buffer = [] | |
self.timer = QTimer() | |
self.timer.setSingleShot(True) | |
self.timer.timeout.connect(self._excrete) | |
@pyqtSlot(str) | |
def consume(self, s): | |
self.buffer.append(s) | |
delta = time.monotonic() - self.last_time | |
remaining = self.minimum - delta | |
if remaining <= 0: | |
self._excrete() | |
elif not self.timer.isActive(): | |
self.timer.start(int(1000 * remaining)) | |
def _excrete(self): | |
self.timer.stop() | |
s = ''.join(self.buffer) | |
if len(s): | |
self.last_time = time.monotonic() | |
self.excrete.emit(s) | |
self.buffer = [] | |
class ConsoleStream(QObject): | |
""" | |
Custom StreamIO class that handles when send data | |
to console_log | |
""" | |
written = pyqtSignal(str) | |
def __init__(self, parent=None): | |
super(ConsoleStream, self).__init__(parent) | |
def write(self, string): | |
self.written.emit(string) | |
class PythonInterpreter(QObject, InteractiveConsole): | |
""" | |
A reimplementation of the builtin InteractiveConsole to | |
work with threads. | |
""" | |
push_command = pyqtSignal(str) | |
multi_line = pyqtSignal(bool) | |
output = pyqtSignal(str) | |
error = pyqtSignal(str) | |
def __init__(self, parent=None): | |
super(PythonInterpreter, self).__init__(parent) | |
self.locals = {} | |
InteractiveConsole.__init__(self, self.locals) | |
self.stream = ConsoleStream(self) | |
self.push_command.connect(self.command) | |
def write(self, string): | |
self.error.emit(string) | |
def runcode(self, code): | |
""" | |
Overrides and captures stdout and stdin from | |
InteractiveConsole. | |
""" | |
sys.stdout = self.stream | |
sys.stderr = self.stream | |
sys.excepthook = sys.__excepthook__ | |
result = InteractiveConsole.runcode(self, code) | |
sys.stdout = sys.__stdout__ | |
sys.stderr = sys.__stderr__ | |
return result | |
@pyqtSlot(str) | |
def command(self, command): | |
""" | |
:param command: line retrieved from console_input on | |
returnPressed | |
""" | |
result = self.push(command) | |
self.multi_line.emit(result) | |
class MainWindow: | |
""" | |
The main GUI window. Opens maximized. | |
""" | |
def __init__(self): | |
self.ui = uic.loadUi("main.ui") | |
self.ui.showMaximized() | |
# Console Properties | |
self.ui.console_log.document().setMaximumBlockCount(500) | |
self.ui.console_log.setWordWrapMode(QTextOption.WrapAnywhere) | |
self.ps1 = '>>>' | |
self.ps2 = '...' | |
self.ui.console_prompt.setText(self.ps1) | |
# Controls | |
self.cursor = self.ui.console_log.textCursor() | |
self.scrollbar = self.ui.console_log.verticalScrollBar() | |
# Spawn Interpreter | |
self.thread = QThread() | |
self.thread.start() | |
self.buffer = ConsoleBuffer() | |
self.interpreter = PythonInterpreter() | |
self.interpreter.moveToThread(self.thread) | |
# Interpreter Signals | |
self.ui.console_input.returnPressed.connect(self.send_console_input) | |
self.interpreter.stream.written.connect(self.buffer.consume) | |
self.buffer.excrete.connect(self.send_console_log) | |
self.interpreter.error.connect(self.send_console_log) | |
self.interpreter.multi_line.connect(self.prompt) | |
def prompt(self, multi_line): | |
""" | |
Sets what prompt to use. | |
""" | |
if multi_line: | |
self.ui.console_prompt.setText(self.ps2) | |
else: | |
self.ui.console_prompt.setText(self.ps1) | |
def send_console_input(self): | |
""" | |
Send input grabbed from the QLineEdit prompt to the console. | |
""" | |
command = self.ui.console_input.text() | |
self.ui.console_input.clear() | |
self.interpreter.push_command.emit(str(command)) | |
def send_console_log(self, output): | |
""" | |
Set the output from InteractiveConsole in the QTextEdit. | |
Auto scroll scrollbar. | |
""" | |
# Move cursor | |
self.cursor.movePosition(QTextCursor.End) | |
self.ui.console_log.setTextCursor(self.cursor) | |
# Insert text | |
self.ui.console_log.insertPlainText(output) | |
# Move scrollbar | |
self.scrollbar.setValue(self.scrollbar.maximum()) | |
def main(): | |
app = QApplication(sys.argv) | |
window = MainWindow() | |
sys.exit(app.exec_()) | |
if __name__ == "__main__": | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<ui version="4.0"> | |
<class>main_window</class> | |
<widget class="QMainWindow" name="main_window"> | |
<property name="geometry"> | |
<rect> | |
<x>0</x> | |
<y>0</y> | |
<width>800</width> | |
<height>600</height> | |
</rect> | |
</property> | |
<property name="sizePolicy"> | |
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> | |
<horstretch>0</horstretch> | |
<verstretch>0</verstretch> | |
</sizepolicy> | |
</property> | |
<property name="windowTitle"> | |
<string>MainWindow</string> | |
</property> | |
<property name="tabShape"> | |
<enum>QTabWidget::Rounded</enum> | |
</property> | |
<widget class="QWidget" name="central_widget"> | |
<layout class="QHBoxLayout" name="horizontalLayout"> | |
<item> | |
<layout class="QVBoxLayout" name="console_layout"> | |
<item> | |
<widget class="QPlainTextEdit" name="console_log"> | |
<property name="readOnly"> | |
<bool>true</bool> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<layout class="QHBoxLayout" name="horizontalLayout_4"> | |
<item> | |
<widget class="QLabel" name="console_prompt"> | |
<property name="text"> | |
<string/> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QLineEdit" name="console_input"> | |
<property name="frame"> | |
<bool>true</bool> | |
</property> | |
</widget> | |
</item> | |
</layout> | |
</item> | |
</layout> | |
</item> | |
</layout> | |
</widget> | |
<widget class="QMenuBar" name="menu_bar"> | |
<property name="geometry"> | |
<rect> | |
<x>0</x> | |
<y>0</y> | |
<width>800</width> | |
<height>26</height> | |
</rect> | |
</property> | |
</widget> | |
<widget class="QStatusBar" name="status_bar"/> | |
</widget> | |
<resources/> | |
<connections/> | |
</ui> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment