Sun, 16 Mar 2025 12:53:12 +0100
Added the Adafruit Feather nRF52840 to the list of known NRF52 boards and changed the list of known CircuitPython boards to be more explicit with respect to Adafruit boards (i.e. VID 0x239A).
# -*- coding: utf-8 -*- # Copyright (c) 2023 - 2025 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the MicroPython REPL widget. """ import re from PyQt6.QtCore import QPoint, Qt, pyqtSignal, pyqtSlot from PyQt6.QtGui import ( QBrush, QClipboard, QColor, QGuiApplication, QKeySequence, QTextCursor, ) from PyQt6.QtWidgets import ( QHBoxLayout, QLabel, QMenu, QSizePolicy, QTextEdit, QVBoxLayout, QWidget, ) from eric7 import Preferences from eric7.EricGui import EricPixmapCache from eric7.EricWidgets.EricZoomWidget import EricZoomWidget from eric7.SystemUtilities import OSUtilities AnsiColorSchemes = { "Windows 7": { 0: QBrush(QColor(0, 0, 0)), 1: QBrush(QColor(128, 0, 0)), 2: QBrush(QColor(0, 128, 0)), 3: QBrush(QColor(128, 128, 0)), 4: QBrush(QColor(0, 0, 128)), 5: QBrush(QColor(128, 0, 128)), 6: QBrush(QColor(0, 128, 128)), 7: QBrush(QColor(192, 192, 192)), 10: QBrush(QColor(128, 128, 128)), 11: QBrush(QColor(255, 0, 0)), 12: QBrush(QColor(0, 255, 0)), 13: QBrush(QColor(255, 255, 0)), 14: QBrush(QColor(0, 0, 255)), 15: QBrush(QColor(255, 0, 255)), 16: QBrush(QColor(0, 255, 255)), 17: QBrush(QColor(255, 255, 255)), }, "Windows 10": { 0: QBrush(QColor(12, 12, 12)), 1: QBrush(QColor(197, 15, 31)), 2: QBrush(QColor(19, 161, 14)), 3: QBrush(QColor(193, 156, 0)), 4: QBrush(QColor(0, 55, 218)), 5: QBrush(QColor(136, 23, 152)), 6: QBrush(QColor(58, 150, 221)), 7: QBrush(QColor(204, 204, 204)), 10: QBrush(QColor(118, 118, 118)), 11: QBrush(QColor(231, 72, 86)), 12: QBrush(QColor(22, 198, 12)), 13: QBrush(QColor(249, 241, 165)), 14: QBrush(QColor(59, 12, 255)), 15: QBrush(QColor(180, 0, 158)), 16: QBrush(QColor(97, 214, 214)), 17: QBrush(QColor(242, 242, 242)), }, "PuTTY": { 0: QBrush(QColor(0, 0, 0)), 1: QBrush(QColor(187, 0, 0)), 2: QBrush(QColor(0, 187, 0)), 3: QBrush(QColor(187, 187, 0)), 4: QBrush(QColor(0, 0, 187)), 5: QBrush(QColor(187, 0, 187)), 6: QBrush(QColor(0, 187, 187)), 7: QBrush(QColor(187, 187, 187)), 10: QBrush(QColor(85, 85, 85)), 11: QBrush(QColor(255, 85, 85)), 12: QBrush(QColor(85, 255, 85)), 13: QBrush(QColor(255, 255, 85)), 14: QBrush(QColor(85, 85, 255)), 15: QBrush(QColor(255, 85, 255)), 16: QBrush(QColor(85, 255, 255)), 17: QBrush(QColor(255, 255, 255)), }, "xterm": { 0: QBrush(QColor(0, 0, 0)), 1: QBrush(QColor(205, 0, 0)), 2: QBrush(QColor(0, 205, 0)), 3: QBrush(QColor(205, 205, 0)), 4: QBrush(QColor(0, 0, 238)), 5: QBrush(QColor(205, 0, 205)), 6: QBrush(QColor(0, 205, 205)), 7: QBrush(QColor(229, 229, 229)), 10: QBrush(QColor(127, 127, 127)), 11: QBrush(QColor(255, 0, 0)), 12: QBrush(QColor(0, 255, 0)), 13: QBrush(QColor(255, 255, 0)), 14: QBrush(QColor(0, 0, 255)), 15: QBrush(QColor(255, 0, 255)), 16: QBrush(QColor(0, 255, 255)), 17: QBrush(QColor(255, 255, 255)), }, "Ubuntu": { 0: QBrush(QColor(1, 1, 1)), 1: QBrush(QColor(222, 56, 43)), 2: QBrush(QColor(57, 181, 74)), 3: QBrush(QColor(255, 199, 6)), 4: QBrush(QColor(0, 11, 184)), 5: QBrush(QColor(118, 38, 113)), 6: QBrush(QColor(44, 181, 233)), 7: QBrush(QColor(204, 204, 204)), 10: QBrush(QColor(128, 128, 128)), 11: QBrush(QColor(255, 0, 0)), 12: QBrush(QColor(0, 255, 0)), 13: QBrush(QColor(255, 255, 0)), 14: QBrush(QColor(0, 0, 255)), 15: QBrush(QColor(255, 0, 255)), 16: QBrush(QColor(0, 255, 255)), 17: QBrush(QColor(255, 255, 255)), }, "Ubuntu (dark)": { 0: QBrush(QColor(96, 96, 96)), 1: QBrush(QColor(235, 58, 45)), 2: QBrush(QColor(57, 181, 74)), 3: QBrush(QColor(255, 199, 29)), 4: QBrush(QColor(25, 56, 230)), 5: QBrush(QColor(200, 64, 193)), 6: QBrush(QColor(48, 200, 255)), 7: QBrush(QColor(204, 204, 204)), 10: QBrush(QColor(128, 128, 128)), 11: QBrush(QColor(255, 0, 0)), 12: QBrush(QColor(0, 255, 0)), 13: QBrush(QColor(255, 255, 0)), 14: QBrush(QColor(0, 0, 255)), 15: QBrush(QColor(255, 0, 255)), 16: QBrush(QColor(0, 255, 255)), 17: QBrush(QColor(255, 255, 255)), }, "Breeze (dark)": { 0: QBrush(QColor(35, 38, 39)), 1: QBrush(QColor(237, 21, 21)), 2: QBrush(QColor(17, 209, 22)), 3: QBrush(QColor(246, 116, 0)), 4: QBrush(QColor(29, 153, 243)), 5: QBrush(QColor(155, 89, 182)), 6: QBrush(QColor(26, 188, 156)), 7: QBrush(QColor(252, 252, 252)), 10: QBrush(QColor(127, 140, 141)), 11: QBrush(QColor(192, 57, 43)), 12: QBrush(QColor(28, 220, 154)), 13: QBrush(QColor(253, 188, 75)), 14: QBrush(QColor(61, 174, 233)), 15: QBrush(QColor(142, 68, 173)), 16: QBrush(QColor(22, 160, 133)), 17: QBrush(QColor(255, 255, 255)), }, } class MicroPythonReplWidget(QWidget): """ Class implementing the MicroPython REPL widget. """ ZoomMin = -10 ZoomMax = 20 def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget (defaults to None) @type QWidget (optional) """ super().__init__(parent=parent) self.__layout = QVBoxLayout(self) self.__layout.setContentsMargins(0, 0, 0, 0) self.__zoomLayout = QHBoxLayout() self.__osdLabel = QLabel() self.__osdLabel.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) self.__zoomLayout.addWidget(self.__osdLabel) self.__zoomWidget = EricZoomWidget( EricPixmapCache.getPixmap("zoomOut"), EricPixmapCache.getPixmap("zoomIn"), EricPixmapCache.getPixmap("zoomReset"), self, ) self.__zoomWidget.setMinimum(self.ZoomMin) self.__zoomWidget.setMaximum(self.ZoomMax) self.__zoomLayout.addWidget(self.__zoomWidget) self.__layout.addLayout(self.__zoomLayout) self.__replEdit = MicroPythonReplEdit(self) self.__layout.addWidget(self.__replEdit) self.setLayout(self.__layout) self.__zoomWidget.valueChanged.connect(self.__replEdit.doZoom) self.__replEdit.osdInfo.connect(self.setOSDInfo) @pyqtSlot(str) def setOSDInfo(self, infoStr): """ Public slot to set the OSD information. @param infoStr string to be shown @type str """ self.__osdLabel.setText(infoStr) @pyqtSlot() def clearOSD(self): """ Public slot to clear the OSD info. """ self.__osdLabel.clear() def replEdit(self): """ Public method to get a reference to the REPL edit. @return reference to the REPL edit @rtype MicroPythonReplEdit """ return self.__replEdit class MicroPythonReplEdit(QTextEdit): """ Class implementing the REPL edit pane. @signal osdInfo(str) emitted when some OSD data was received from the device """ osdInfo = pyqtSignal(str) def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget (defaults to None) @type QWidget (optional) """ super().__init__(parent=parent) self.setAcceptRichText(False) self.setUndoRedoEnabled(False) self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.__currentZoom = 0 self.__replBuffer = b"" self.__vt100Re = re.compile( r"(?P<count>\d*)(?P<color>(?:;?\d*)*)(?P<action>[ABCDKm])" ) self.customContextMenuRequested.connect(self.__showContextMenu) charFormat = self.currentCharFormat() self.DefaultForeground = charFormat.foreground() self.DefaultBackground = charFormat.background() self.__interface = None def setInterface(self, deviceInterface): """ Public method to set the reference to the device interface object. @param deviceInterface reference to the device interface object @type MicroPythonDeviceInterface """ self.__interface = deviceInterface @pyqtSlot(int) def doZoom(self, value): """ Public slot to zoom in or out. @param value zoom value @type int """ if value < self.__currentZoom: self.zoomOut(self.__currentZoom - value) elif value > self.__currentZoom: self.zoomIn(value - self.__currentZoom) self.__currentZoom = value @pyqtSlot(QPoint) def __showContextMenu(self, pos): """ Private slot to show the REPL context menu. @param pos position to show the menu at @type QPoint """ connected = bool(self.__interface) and self.__interface.isConnected() if OSUtilities.isMacPlatform(): copyKeys = QKeySequence("Ctrl+C") pasteKeys = QKeySequence("Ctrl+V") selectAllKeys = QKeySequence("Ctrl+A") else: copyKeys = QKeySequence("Ctrl+Shift+C") pasteKeys = QKeySequence("Ctrl+Shift+V") selectAllKeys = QKeySequence("Ctrl+Shift+A") menu = QMenu(self) menu.addAction( EricPixmapCache.getIcon("editDelete"), self.tr("Clear"), self.__clear ).setEnabled(bool(self.toPlainText())) menu.addSeparator() menu.addAction( EricPixmapCache.getIcon("editCopy"), self.tr("Copy"), copyKeys, self.copy, ).setEnabled(self.textCursor().hasSelection()) menu.addAction( EricPixmapCache.getIcon("editPaste"), self.tr("Paste"), pasteKeys, self.__paste, ).setEnabled(self.canPaste() and connected) menu.addSeparator() menu.addAction( EricPixmapCache.getIcon("editSelectAll"), self.tr("Select All"), selectAllKeys, self.selectAll, ).setEnabled(bool(self.toPlainText())) menu.exec(self.mapToGlobal(pos)) @pyqtSlot() def handlePreferencesChanged(self): """ Public slot to handle a change in preferences. """ self.__colorScheme = Preferences.getMicroPython("ColorScheme") self.__font = Preferences.getEditorOtherFonts("MonospacedFont") self.setFontFamily(self.__font.family()) self.setFontPointSize(self.__font.pointSize()) if Preferences.getMicroPython("ReplLineWrap"): self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) else: self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) @pyqtSlot() def __clear(self): """ Private slot to clear the REPL pane. """ self.clear() if bool(self.__interface) and self.__interface.isConnected(): self.__interface.write(b"\r") @pyqtSlot() def __paste(self, mode=QClipboard.Mode.Clipboard): """ Private slot to perform a paste operation. @param mode paste mode (defaults to QClipboard.Mode.Clipboard) @type QClipboard.Mode (optional) """ # add support for paste by mouse middle button clipboard = QGuiApplication.clipboard() if clipboard: pasteText = clipboard.text(mode=mode) if pasteText: pasteText = pasteText.replace("\n\r", "\r") pasteText = pasteText.replace("\n", "\r") if bool(self.__interface) and self.__interface.isConnected(): self.__interface.write(b"\x05") self.__interface.write(pasteText.encode("utf-8")) self.__interface.write(b"\x04") def keyPressEvent(self, evt): """ Protected method to handle key press events. @param evt reference to the key press event @type QKeyEvent """ key = evt.key() msg = bytes(evt.text(), "utf8") if key == Qt.Key.Key_Backspace: msg = b"\b" elif key == Qt.Key.Key_Delete: msg = b"\x1b[\x33\x7e" elif key == Qt.Key.Key_Up: msg = b"\x1b[A" elif key == Qt.Key.Key_Down: msg = b"\x1b[B" elif key == Qt.Key.Key_Right: msg = b"\x1b[C" elif key == Qt.Key.Key_Left: msg = b"\x1b[D" elif key == Qt.Key.Key_Home: msg = b"\x1b[H" elif key == Qt.Key.Key_End: msg = b"\x1b[F" elif ( OSUtilities.isMacPlatform() and evt.modifiers() == Qt.KeyboardModifier.MetaModifier ) or ( not OSUtilities.isMacPlatform() and evt.modifiers() == Qt.KeyboardModifier.ControlModifier ): if Qt.Key.Key_A <= key <= Qt.Key.Key_Z: # devices treat an input of \x01 as Ctrl+A, etc. msg = bytes([1 + key - Qt.Key.Key_A]) elif evt.modifiers() == ( Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier ) or ( OSUtilities.isMacPlatform() and evt.modifiers() == Qt.KeyboardModifier.ControlModifier ): if key == Qt.Key.Key_C: self.copy() msg = b"" elif key == Qt.Key.Key_V: self.__paste() msg = b"" elif key == Qt.Key.Key_A: self.selectAll() msg = b"" elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): tc = self.textCursor() tc.movePosition(QTextCursor.MoveOperation.EndOfLine) self.setTextCursor(tc) if bool(self.__interface) and self.__interface.isConnected(): self.__interface.write(msg) evt.accept() def mouseReleaseEvent(self, evt): """ Protected method to handle mouse release events. @param evt reference to the event object @type QMouseEvent """ if evt.button() == Qt.MouseButton.MiddleButton: self.__paste(mode=QClipboard.Mode.Selection) msg = b"" if bool(self.__interface) and self.__interface.isConnected(): self.__interface.write(msg) evt.accept() else: super().mouseReleaseEvent(evt) @pyqtSlot(bytes) def processData(self, data): """ Public slot to process the data received from the device. @param data data received from the device @type bytes """ tc = self.textCursor() # the text cursor must be on the last line while tc.movePosition(QTextCursor.MoveOperation.Down): pass # reset the font self.__setCharFormat(None, tc) # add received data to the buffered one data = self.__replBuffer + data index = 0 while index < len(data): if data[index] == 8: # \b tc.movePosition(QTextCursor.MoveOperation.Left) self.setTextCursor(tc) elif data[index] in (4, 13): # EOT, \r pass elif len(data) > index + 1 and data[index] == 27 and data[index + 1] == 91: # VT100 cursor command detected: <Esc>[ index += 2 # move index to after the [ match = self.__vt100Re.search( data[index:].decode("utf-8", errors="replace") ) if match: # move to last position in control sequence # ++ will be done at end of loop index += match.end() - 1 action = match.group("action") if action in "ABCD": if match.group("count") == "": count = 1 else: count = int(match.group("count")) if action == "A": # up tc.movePosition(QTextCursor.MoveOperation.Up, n=count) self.setTextCursor(tc) elif action == "B": # down tc.movePosition(QTextCursor.MoveOperation.Down, n=count) self.setTextCursor(tc) elif action == "C": # right tc.movePosition(QTextCursor.MoveOperation.Right, n=count) self.setTextCursor(tc) elif action == "D": # left tc.movePosition(QTextCursor.MoveOperation.Left, n=count) self.setTextCursor(tc) elif action == "K": # delete things if match.group("count") in ("", "0"): # delete to end of line tc.movePosition( QTextCursor.MoveOperation.EndOfLine, mode=QTextCursor.MoveMode.KeepAnchor, ) tc.removeSelectedText() self.setTextCursor(tc) elif match.group("count") == "1": # delete to beginning of line tc.movePosition( QTextCursor.MoveOperation.StartOfLine, mode=QTextCursor.MoveMode.KeepAnchor, ) tc.removeSelectedText() self.setTextCursor(tc) elif match.group("count") == "2": # delete whole line tc.movePosition(QTextCursor.MoveOperation.EndOfLine) tc.movePosition( QTextCursor.MoveOperation.StartOfLine, mode=QTextCursor.MoveMode.KeepAnchor, ) tc.removeSelectedText() self.setTextCursor(tc) elif action == "m": self.__setCharFormat(match.group(0)[:-1].split(";"), tc) elif ( len(data) > index + 1 and data[index] == 27 and data[index + 1 : index + 4] == b"]0;" ): if b"\x1b\\" in data[index + 4 :]: # 'set window title' command detected: <Esc>]0;...<Esc>\ # __IGNORE_WARNING_M-891__ titleData = data[index + 4 :].split(b"\x1b\\")[0] title = titleData.decode("utf-8") index += len(titleData) + 5 # one more is done at the end self.osdInfo.emit(title) else: # data is incomplete; buffer and stop processing self.__replBuffer = data[index:] return else: tc.deleteChar() self.setTextCursor(tc) # unicode handling if data[index] & 0b11110000 == 0b11110000: length = 4 elif data[index] & 0b11100000 == 0b11100000: length = 3 elif data[index] & 0b11000000 == 0b11000000: length = 2 else: length = 1 try: txt = data[index : index + length].decode("utf8") except UnicodeDecodeError: txt = data[index : index + length].decode("iso8859-1") index += length - 1 # one more is done at the end self.insertPlainText(txt) index += 1 self.ensureCursorVisible() self.__replBuffer = b"" def __setCharFormat(self, formatCodes, textCursor): """ Private method setting the current text format of the REPL pane based on the passed ANSI codes. Following codes are used: <ul> <li>0: Reset</li> <li>1: Bold font (weight 75)</li> <li>2: Light font (weight 25)</li> <li>3: Italic font</li> <li>4: Underlined font</li> <li>9: Strikeout font</li> <li>21: Bold off (weight 50)</li> <li>22: Light off (weight 50)</li> <li>23: Italic off</li> <li>24: Underline off</li> <li>29: Strikeout off</li> <li>30: foreground Black</li> <li>31: foreground Dark Red</li> <li>32: foreground Dark Green</li> <li>33: foreground Dark Yellow</li> <li>34: foreground Dark Blue</li> <li>35: foreground Dark Magenta</li> <li>36: foreground Dark Cyan</li> <li>37: foreground Light Gray</li> <li>39: reset foreground to default</li> <li>40: background Black</li> <li>41: background Dark Red</li> <li>42: background Dark Green</li> <li>43: background Dark Yellow</li> <li>44: background Dark Blue</li> <li>45: background Dark Magenta</li> <li>46: background Dark Cyan</li> <li>47: background Light Gray</li> <li>49: reset background to default</li> <li>53: Overlined font</li> <li>55: Overline off</li> <li>90: bright foreground Dark Gray</li> <li>91: bright foreground Red</li> <li>92: bright foreground Green</li> <li>93: bright foreground Yellow</li> <li>94: bright foreground Blue</li> <li>95: bright foreground Magenta</li> <li>96: bright foreground Cyan</li> <li>97: bright foreground White</li> <li>100: bright background Dark Gray</li> <li>101: bright background Red</li> <li>102: bright background Green</li> <li>103: bright background Yellow</li> <li>104: bright background Blue</li> <li>105: bright background Magenta</li> <li>106: bright background Cyan</li> <li>107: bright background White</li> </ul> @param formatCodes list of format codes @type list of str @param textCursor reference to the text cursor @type QTextCursor """ if not formatCodes: # empty format codes list is treated as a reset formatCodes = ["0"] charFormat = textCursor.charFormat() charFormat.setFontFamilies([self.__font.family()]) charFormat.setFontPointSize(self.__font.pointSize()) for formatCode in formatCodes: try: formatCode = int(formatCode) except ValueError: # ignore non digit values continue if formatCode == 0: charFormat.setFontWeight(50) charFormat.setFontItalic(False) charFormat.setFontUnderline(False) charFormat.setFontStrikeOut(False) charFormat.setFontOverline(False) charFormat.setForeground(self.DefaultForeground) charFormat.setBackground(self.DefaultBackground) elif formatCode == 1: charFormat.setFontWeight(75) elif formatCode == 2: charFormat.setFontWeight(25) elif formatCode == 3: charFormat.setFontItalic(True) elif formatCode == 4: charFormat.setFontUnderline(True) elif formatCode == 9: charFormat.setFontStrikeOut(True) elif formatCode in (21, 22): charFormat.setFontWeight(50) elif formatCode == 23: charFormat.setFontItalic(False) elif formatCode == 24: charFormat.setFontUnderline(False) elif formatCode == 29: charFormat.setFontStrikeOut(False) elif formatCode == 53: charFormat.setFontOverline(True) elif formatCode == 55: charFormat.setFontOverline(False) elif formatCode in (30, 31, 32, 33, 34, 35, 36, 37): charFormat.setForeground( AnsiColorSchemes[self.__colorScheme][formatCode - 30] ) elif formatCode in (40, 41, 42, 43, 44, 45, 46, 47): charFormat.setBackground( AnsiColorSchemes[self.__colorScheme][formatCode - 40] ) elif formatCode in (90, 91, 92, 93, 94, 95, 96, 97): charFormat.setForeground( AnsiColorSchemes[self.__colorScheme][formatCode - 80] ) elif formatCode in (100, 101, 102, 103, 104, 105, 106, 107): charFormat.setBackground( AnsiColorSchemes[self.__colorScheme][formatCode - 90] ) elif formatCode == 39: charFormat.setForeground(self.DefaultForeground) elif formatCode == 49: charFormat.setBackground(self.DefaultBackground) textCursor.setCharFormat(charFormat)