diff --git a/bec_widgets/widgets/console/console.py b/bec_widgets/widgets/console/console.py
index c2b2247b..a72d98dc 100644
--- a/bec_widgets/widgets/console/console.py
+++ b/bec_widgets/widgets/console/console.py
@@ -1,24 +1,24 @@
"""
-BECConsole is a Qt widget that runs a Bash shell. The widget can be used and
-embedded like any other Qt widget.
+BECConsole is a Qt widget that runs a Bash shell.
-BECConsole is powered by Pyte, a Python based terminal emulator
+BECConsole VT100 emulation is powered by Pyte,
(https://github.com/selectel/pyte).
"""
+import collections
import fcntl
import html
import os
import pty
-import subprocess
import sys
-import threading
import pyte
+from pyte.screens import History
from qtpy import QtCore, QtGui, QtWidgets
-from qtpy.QtCore import QSize, QSocketNotifier, Qt
+from qtpy.QtCore import Property as pyqtProperty
+from qtpy.QtCore import QSize, QSocketNotifier, Qt, QTimer
from qtpy.QtCore import Signal as pyqtSignal
-from qtpy.QtGui import QClipboard, QTextCursor
+from qtpy.QtGui import QClipboard, QColor, QPalette, QTextCursor
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
@@ -137,13 +137,52 @@ def QtKeyToAscii(event):
class Screen(pyte.HistoryScreen):
- def __init__(self, stdin_fd, numColumns, numLines, historyLength):
- super().__init__(numColumns, numLines, historyLength, ratio=1 / numLines)
+ def __init__(self, stdin_fd, cols, rows, historyLength):
+ super().__init__(cols, rows, historyLength, ratio=1 / rows)
self._fd = stdin_fd
def write_process_input(self, data):
- """Response to CPR request for example"""
- os.write(self._fd, data.encode("utf-8"))
+ """Response to CPR request (for example),
+ this can be for other requests
+ """
+ try:
+ os.write(self._fd, data.encode("utf-8"))
+ except (IOError, OSError):
+ pass
+
+ def resize(self, lines, columns):
+ lines = lines or self.lines
+ columns = columns or self.columns
+
+ if lines == self.lines and columns == self.columns:
+ return # No changes.
+
+ self.dirty.clear()
+ self.dirty.update(range(lines))
+
+ self.save_cursor()
+ if lines < self.lines:
+ if lines <= self.cursor.y:
+ nlines_to_move_up = self.lines - lines
+ for i in range(nlines_to_move_up):
+ line = self.buffer[i] # .pop(0)
+ self.history.top.append(line)
+ self.cursor_position(0, 0)
+ self.delete_lines(nlines_to_move_up)
+ self.restore_cursor()
+ self.cursor.y -= nlines_to_move_up
+ else:
+ self.restore_cursor()
+
+ self.lines, self.columns = lines, columns
+ self.history = History(
+ self.history.top,
+ self.history.bottom,
+ 1 / self.lines,
+ self.history.size,
+ self.history.position,
+ )
+ self.set_margins()
class Backend(QtCore.QObject):
@@ -155,17 +194,17 @@ class Backend(QtCore.QObject):
"""
# Signals to communicate with ``_TerminalWidget``.
- startWork = pyqtSignal()
dataReady = pyqtSignal(object)
+ processExited = pyqtSignal()
- def __init__(self, fd, numColumns, numLines):
+ def __init__(self, fd, cols, rows):
super().__init__()
# File descriptor that connects to Bash process.
self.fd = fd
# Setup Pyte (hard coded display size for now).
- self.screen = Screen(self.fd, numColumns, numLines, 10000)
+ self.screen = Screen(self.fd, cols, rows, 10000)
self.stream = pyte.ByteStream()
self.stream.attach(self.screen)
@@ -174,12 +213,14 @@ class Backend(QtCore.QObject):
def _fd_readable(self):
"""
- Poll the Bash output, run it through Pyte, and notify the main applet.
+ Poll the Bash output, run it through Pyte, and notify
"""
# Read the shell output until the file descriptor is closed.
try:
out = os.read(self.fd, 2**16)
except OSError:
+ self.processExited.emit()
+ self.notifier.setEnabled(False)
return
# Feed output into Pyte's state machine and send the new screen
@@ -188,93 +229,256 @@ class Backend(QtCore.QObject):
self.dataReady.emit(self.screen)
-class BECConsole(QtWidgets.QScrollArea):
+class BECConsole(QtWidgets.QWidget):
"""Container widget for the terminal text area"""
- def __init__(self, parent=None, numLines=50, numColumns=125):
+ ICON_NAME = "terminal"
+
+ def __init__(self, parent=None, cols=132):
super().__init__(parent)
- self.innerWidget = QtWidgets.QWidget(self)
- QHBoxLayout(self.innerWidget)
- self.innerWidget.layout().setContentsMargins(0, 0, 0, 0)
+ self.term = _TerminalWidget(self, cols, rows=43)
+ self.scroll_bar = QScrollBar(Qt.Vertical, self)
+ # self.scroll_bar.hide()
+ layout = QHBoxLayout(self)
+ layout.addWidget(self.term)
+ layout.addWidget(self.scroll_bar)
+ layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
+ layout.setContentsMargins(0, 0, 0, 0)
+ self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
- self.term = _TerminalWidget(self.innerWidget, numLines, numColumns)
- self.term.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
- self.innerWidget.layout().addWidget(self.term)
+ pal = QPalette()
+ self.set_bgcolor(pal.window().color())
+ self.set_fgcolor(pal.windowText().color())
+ self.term.set_scroll_bar(self.scroll_bar)
+ self.set_cmd("bec --nogui")
- self.scroll_bar = QScrollBar(Qt.Vertical, self.term)
- self.innerWidget.layout().addWidget(self.scroll_bar)
+ self._check_designer_timer = QTimer()
+ self._check_designer_timer.timeout.connect(self.check_designer)
+ self._check_designer_timer.start(1000)
- self.term.set_scroll(self.scroll_bar)
+ def minimumSizeHint(self):
+ size = self.term.sizeHint()
+ size.setWidth(size.width() + self.scroll_bar.width())
+ return size
- self.setWidget(self.innerWidget)
+ def sizeHint(self):
+ return self.minimumSizeHint()
- def start(self, cmd=["bec", "--nogui"], deactivate_ctrl_d=True):
+ def check_designer(self, calls={"n": 0}):
+ calls["n"] += 1
+ if self.term.fd is not None:
+ # already started
+ self._check_designer_timer.stop()
+ elif self.window().windowTitle().endswith("[Preview]"):
+ # assuming Designer preview -> start
+ self._check_designer_timer.stop()
+ self.term.start()
+ elif calls["n"] >= 3:
+ # assuming not in Designer -> stop checking
+ self._check_designer_timer.stop()
+
+ def get_rows(self):
+ return self.term.rows
+
+ def set_rows(self, rows):
+ self.term.rows = rows
+ self.adjustSize()
+ self.updateGeometry()
+
+ def get_cols(self):
+ return self.term.cols
+
+ def set_cols(self, cols):
+ self.term.cols = cols
+ self.adjustSize()
+ self.updateGeometry()
+
+ def get_bgcolor(self):
+ return QColor.fromString(self.term.bg_color)
+
+ def set_bgcolor(self, color):
+ self.term.bg_color = color.name(QColor.HexRgb)
+
+ def get_fgcolor(self):
+ return QColor.fromString(self.term.fg_color)
+
+ def set_fgcolor(self, color):
+ self.term.fg_color = color.name(QColor.HexRgb)
+
+ def get_cmd(self):
+ return self.term._cmd
+
+ def set_cmd(self, cmd):
self.term._cmd = cmd
+ if self.term.fd is None:
+ # not started yet
+ self.term.clear()
+ self.term.appendHtml(f"
BEC Console - {repr(cmd)}
")
+
+ def start(self, deactivate_ctrl_d=True):
self.term.start(deactivate_ctrl_d=deactivate_ctrl_d)
def push(self, text):
"""Push some text to the terminal"""
return self.term.push(text)
+ cols = pyqtProperty(int, get_cols, set_cols)
+ rows = pyqtProperty(int, get_rows, set_rows)
+ bgcolor = pyqtProperty(QColor, get_bgcolor, set_bgcolor)
+ fgcolor = pyqtProperty(QColor, get_fgcolor, set_fgcolor)
+ cmd = pyqtProperty(str, get_cmd, set_cmd)
+
class _TerminalWidget(QtWidgets.QPlainTextEdit):
"""
Start ``Backend`` process and render Pyte output as text.
"""
- def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
- super().__init__(parent)
-
+ def __init__(self, parent, cols=125, rows=50, **kwargs):
# file descriptor to communicate with the subprocess
self.fd = None
self.backend = None
- self.lock = threading.Lock()
# command to execute
- self._cmd = None
+ self._cmd = ""
# should ctrl-d be deactivated ? (prevent Python exit)
self._deactivate_ctrl_d = False
+ # Default colors
+ pal = QPalette()
+ self._fg_color = pal.text().color().name()
+ self._bg_color = pal.base().color().name()
+
# Specify the terminal size in terms of lines and columns.
- self.numLines = numLines
- self.numColumns = numColumns
- self.output = [""] * numLines
+ self._rows = rows
+ self._cols = cols
+ self.output = collections.deque()
+
+ super().__init__(parent)
+
+ self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding)
+
+ # Disable default scrollbars (we use our own, to be set via .set_scroll_bar())
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ self.scroll_bar = None
# Use Monospace fonts and disable line wrapping.
self.setFont(QtGui.QFont("Courier", 9))
self.setFont(QtGui.QFont("Monospace"))
self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
-
- # Disable vertical scrollbar (we use our own, to be set via .set_scroll())
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-
fmt = QtGui.QFontMetrics(self.font())
- self._char_width = fmt.width("w")
- self._char_height = fmt.height()
- self.setCursorWidth(self._char_width)
- # self.setStyleSheet("QPlainTextEdit { color: #ffff00; background-color: #303030; } ");
+ char_width = fmt.width("w")
+ self.setCursorWidth(char_width)
- def start(self, deactivate_ctrl_d=False):
+ self.adjustSize()
+ self.updateGeometry()
+ self.update_stylesheet()
+
+ @property
+ def bg_color(self):
+ return self._bg_color
+
+ @bg_color.setter
+ def bg_color(self, hexcolor):
+ self._bg_color = hexcolor
+ self.update_stylesheet()
+
+ @property
+ def fg_color(self):
+ return self._fg_color
+
+ @fg_color.setter
+ def fg_color(self, hexcolor):
+ self._fg_color = hexcolor
+ self.update_stylesheet()
+
+ def update_stylesheet(self):
+ self.setStyleSheet(
+ f"QPlainTextEdit {{ border: 0; color: {self._fg_color}; background-color: {self._bg_color}; }} "
+ )
+
+ @property
+ def rows(self):
+ return self._rows
+
+ @rows.setter
+ def rows(self, rows: int):
+ if self.backend is None:
+ # not initialized yet, ok to change
+ self._rows = rows
+ self.adjustSize()
+ self.updateGeometry()
+ else:
+ raise RuntimeError("Cannot change rows after console is started.")
+
+ @property
+ def cols(self):
+ return self._cols
+
+ @cols.setter
+ def cols(self, cols: int):
+ if self.fd is None:
+ # not initialized yet, ok to change
+ self._cols = cols
+ self.adjustSize()
+ self.updateGeometry()
+ else:
+ raise RuntimeError("Cannot change cols after console is started.")
+
+ def start(self, deactivate_ctrl_d: bool = False):
self._deactivate_ctrl_d = deactivate_ctrl_d
- # Start the Bash process
- self.fd = self.forkShell()
+ self.update_term_size()
- # Create the ``Backend`` object
- self.backend = Backend(self.fd, self.numColumns, self.numLines)
- self.backend.dataReady.connect(self.dataReady)
+ # Start the Bash process
+ self.fd = self.fork_shell()
+
+ if self.fd:
+ # Create the ``Backend`` object
+ self.backend = Backend(self.fd, self.cols, self.rows)
+ self.backend.dataReady.connect(self.data_ready)
+ self.backend.processExited.connect(self.process_exited)
+ else:
+ self.process_exited()
+
+ def process_exited(self):
+ self.fd = None
+ self.clear()
+ self.appendHtml(f"
{repr(self._cmd)} - Process exited.
")
+ self.setReadOnly(True)
+
+ def data_ready(self, screen):
+ """Handle new screen: redraw, set scroll bar max and slider, move cursor to its position
+
+ This method is triggered via a signal from ``Backend``.
+ """
+ self.redraw_screen()
+ self.adjust_scroll_bar()
+ self.move_cursor()
def minimumSizeHint(self):
- width = self._char_width * self.numColumns
- height = self._char_height * self.numLines
- return QSize(width, height + 20)
+ """Return minimum size for current cols and rows"""
+ fmt = QtGui.QFontMetrics(self.font())
+ char_width = fmt.width("w")
+ char_height = fmt.height()
+ width = char_width * self.cols
+ height = char_height * self.rows
+ return QSize(width, height)
- def set_scroll(self, scroll):
- self.scroll = scroll
- self.scroll.setMinimum(0)
- self.scroll.valueChanged.connect(self.scroll_value_change)
+ def sizeHint(self):
+ return self.minimumSizeHint()
- def scroll_value_change(self, value, old={"value": 0}):
+ def set_scroll_bar(self, scroll_bar):
+ self.scroll_bar = scroll_bar
+ self.scroll_bar.setMinimum(0)
+ self.scroll_bar.valueChanged.connect(self.scroll_value_change)
+
+ def scroll_value_change(self, value, old={"value": -1}):
+ if self.backend is None:
+ return
+ if old["value"] == -1:
+ old["value"] = self.scroll_bar.maximum()
if value <= old["value"]:
# scroll up
# value is number of lines from the start
@@ -288,13 +492,35 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
for i in range(nlines):
self.backend.screen.next_page()
old["value"] = value
- self.dataReady(self.backend.screen, reset_scroll=False)
+ self.redraw_screen()
+
+ def adjust_scroll_bar(self):
+ sb = self.scroll_bar
+ sb.valueChanged.disconnect(self.scroll_value_change)
+ tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
+ sb.setMaximum(tmp if tmp > 0 else 0)
+ sb.setSliderPosition(tmp if tmp > 0 else 0)
+ # if tmp > 0:
+ # # show scrollbar, but delayed - prevent recursion with widget size change
+ # QTimer.singleShot(0, scrollbar.show)
+ # else:
+ # QTimer.singleShot(0, scrollbar.hide)
+ sb.valueChanged.connect(self.scroll_value_change)
+
+ def write(self, data):
+ try:
+ os.write(self.fd, data)
+ except (IOError, OSError):
+ self.process_exited()
@Slot(object)
def keyPressEvent(self, event):
"""
Redirect all keystrokes to the terminal process.
"""
+ if self.fd is None:
+ # not started
+ return
# Convert the Qt key to the correct ASCII code.
if (
self._deactivate_ctrl_d
@@ -311,15 +537,17 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
# MacOS only: CMD-V handling
self._push_clipboard()
elif code is not None:
- os.write(self.fd, code)
+ self.write(code)
def push(self, text):
"""
Write 'text' to terminal
"""
- os.write(self.fd, text.encode("utf-8"))
+ self.write(text.encode("utf-8"))
def contextMenuEvent(self, event):
+ if self.fd is None:
+ return
menu = self.createStandardContextMenu()
for action in menu.actions():
# remove all actions except copy and paste
@@ -341,7 +569,20 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
clipboard = QApplication.instance().clipboard()
self.push(clipboard.text())
+ def move_cursor(self):
+ textCursor = self.textCursor()
+ textCursor.setPosition(0)
+ textCursor.movePosition(
+ QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
+ )
+ textCursor.movePosition(
+ QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
+ )
+ self.setTextCursor(textCursor)
+
def mouseReleaseEvent(self, event):
+ if self.fd is None:
+ return
if event.button() == Qt.MiddleButton:
# push primary selection buffer ("mouse clipboard") to terminal
clipboard = QApplication.instance().clipboard()
@@ -355,34 +596,34 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
# mouse was used to select text -> nothing to do
pass
else:
- # a simple 'click', make cursor going to end
- textCursor.setPosition(0)
- textCursor.movePosition(
- QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
- )
- textCursor.movePosition(
- QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
- )
- self.setTextCursor(textCursor)
- self.ensureCursorVisible()
+ # a simple 'click', move scrollbar to end
+ self.scroll_bar.setSliderPosition(self.scroll_bar.maximum())
+ self.move_cursor()
return None
return super().mouseReleaseEvent(event)
- def dataReady(self, screenData, reset_scroll=True):
+ def redraw_screen(self):
"""
- Render the new screen as text into the widget.
+ Render the screen as formatted text into the widget.
+ """
+ screen = self.backend.screen
- This method is triggered via a signal from ``Backend``.
- """
- with self.lock:
- # Clear the widget
+ # Clear the widget
+ if screen.dirty:
self.clear()
+ while len(self.output) < (max(screen.dirty) + 1):
+ self.output.append("")
+ while len(self.output) > (max(screen.dirty) + 1):
+ self.output.pop()
# Prepare the HTML output
- for line_no in screenData.dirty:
+ for line_no in screen.dirty:
line = text = ""
style = old_style = ""
- for ch in screenData.buffer[line_no].values():
+ old_idx = 0
+ for idx, ch in screen.buffer[line_no].items():
+ text += " " * (idx - old_idx - 1)
+ old_idx = idx
style = f"{'background-color:%s;' % ansi_colors.get(ch.bg, ansi_colors['black']) if ch.bg!='default' else ''}{'color:%s;' % ansi_colors.get(ch.fg, ansi_colors['white']) if ch.fg!='default' else ''}{'font-weight:bold;' if ch.bold else ''}{'font-style:italic;' if ch.italics else ''}"
if style != old_style:
if old_style:
@@ -396,45 +637,47 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
line += f"{html.escape(text, quote=True)}"
else:
line += html.escape(text, quote=True)
+ # do a check at the cursor position:
+ # it is possible x pos > output line length,
+ # for example if last escape codes are "cursor forward" past end of text,
+ # like IPython does for "..." prompt (in a block, like "for" loop or "while" for example)
+ # In this case, cursor is at 12 but last text output is at 8 -> insert spaces
+ if line_no == screen.cursor.y:
+ llen = len(screen.buffer[line_no])
+ if llen < screen.cursor.x:
+ line += " " * (screen.cursor.x - llen)
self.output[line_no] = line
# fill the text area with HTML contents in one go
self.appendHtml(f"{chr(10).join(self.output)}
")
- # done updates, all clean
- screenData.dirty.clear()
+ # did updates, all clean
+ screen.dirty.clear()
- # Activate cursor
- textCursor = self.textCursor()
- textCursor.setPosition(0)
- textCursor.movePosition(QTextCursor.Down, QTextCursor.MoveAnchor, screenData.cursor.y)
- textCursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, screenData.cursor.x)
- self.setTextCursor(textCursor)
- self.ensureCursorVisible()
+ def update_term_size(self):
+ fmt = QtGui.QFontMetrics(self.font())
+ char_width = fmt.width("w")
+ char_height = fmt.height()
+ self._cols = int(self.width() / char_width)
+ self._rows = int(self.height() / char_height)
- # manage scroll
- if reset_scroll:
- self.scroll.valueChanged.disconnect(self.scroll_value_change)
- tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
- self.scroll.setMaximum(tmp if tmp > 0 else 0)
- self.scroll.setSliderPosition(len(self.backend.screen.history.top))
- self.scroll.valueChanged.connect(self.scroll_value_change)
-
- # def resizeEvent(self, event):
- # with self.lock:
- # self.numColumns = int(self.width() / self._char_width)
- # self.numLines = int(self.height() / self._char_height)
- # self.output = [""] * self.numLines
- # print("RESIZING TO", self.numColumns, "x", self.numLines)
- # self.backend.screen.resize(self.numLines, self.numColumns)
+ def resizeEvent(self, event):
+ self.update_term_size()
+ if self.fd:
+ self.backend.screen.resize(self._rows, self._cols)
+ self.redraw_screen()
+ self.adjust_scroll_bar()
+ self.move_cursor()
def wheelEvent(self, event):
+ if not self.fd:
+ return
y = event.angleDelta().y()
if y > 0:
self.backend.screen.prev_page()
else:
self.backend.screen.next_page()
- self.dataReady(self.backend.screen, reset_scroll=False)
+ self.redraw_screen()
- def forkShell(self):
+ def fork_shell(self):
"""
Fork the current process and execute bec in shell.
"""
@@ -443,32 +686,30 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
except (IOError, OSError):
return False
if pid == 0:
- # Safe way to make it work under BSD and Linux
try:
ls = os.environ["LANG"].split(".")
except KeyError:
ls = []
if len(ls) < 2:
ls = ["en_US", "UTF-8"]
+ os.putenv("COLUMNS", str(self.cols))
+ os.putenv("LINES", str(self.rows))
+ os.putenv("TERM", "linux")
+ os.putenv("LANG", ls[0] + ".UTF-8")
+ if not self._cmd:
+ self._cmd = os.environ["SHELL"]
+ cmd = self._cmd
+ if isinstance(cmd, str):
+ cmd = cmd.split()
try:
- os.putenv("COLUMNS", str(self.numColumns))
- os.putenv("LINES", str(self.numLines))
- os.putenv("TERM", "linux")
- os.putenv("LANG", ls[0] + ".UTF-8")
- if isinstance(self._cmd, str):
- os.execvp(self._cmd, self._cmd)
- else:
- os.execvp(self._cmd[0], self._cmd)
- # print "child_pid", child_pid, sts
+ os.execvp(cmd[0], cmd)
except (IOError, OSError):
pass
- # self.proc_finish(sid)
os._exit(0)
else:
# We are in the parent process.
# Set file control
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
- print("Spawned Bash shell (PID {})".format(pid))
return fd
@@ -478,17 +719,13 @@ if __name__ == "__main__":
from qtpy import QtGui, QtWidgets
- # Terminal size in characters.
- numLines = 25
- numColumns = 100
-
- # Create the Qt application and QBash instance.
+ # Create the Qt application and console.
app = QtWidgets.QApplication([])
mainwin = QtWidgets.QMainWindow()
- title = "BECConsole ({}x{})".format(numColumns, numLines)
+ title = "BECConsole"
mainwin.setWindowTitle(title)
- console = BECConsole(mainwin, numColumns, numLines)
+ console = BECConsole(mainwin)
mainwin.setCentralWidget(console)
console.start()
diff --git a/bec_widgets/widgets/console/console.pyproject b/bec_widgets/widgets/console/console.pyproject
new file mode 100644
index 00000000..628b4e99
--- /dev/null
+++ b/bec_widgets/widgets/console/console.pyproject
@@ -0,0 +1 @@
+{'files': ['console.py']}
diff --git a/bec_widgets/widgets/console/console_plugin.py b/bec_widgets/widgets/console/console_plugin.py
new file mode 100644
index 00000000..93af471e
--- /dev/null
+++ b/bec_widgets/widgets/console/console_plugin.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+import os
+
+from qtpy.QtDesigner import QDesignerCustomWidgetInterface
+
+import bec_widgets
+from bec_widgets.utils.bec_designer import designer_material_icon
+from bec_widgets.widgets.console.console import BECConsole
+
+DOM_XML = """
+
+
+
+
+"""
+
+MODULE_PATH = os.path.dirname(bec_widgets.__file__)
+
+
+class BECConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
+ def __init__(self):
+ super().__init__()
+ self._form_editor = None
+
+ def createWidget(self, parent):
+ t = BECConsole(parent)
+ return t
+
+ def domXml(self):
+ return DOM_XML
+
+ def group(self):
+ return "BEC Console"
+
+ def icon(self):
+ return designer_material_icon(BECConsole.ICON_NAME)
+
+ def includeFile(self):
+ return "bec_console"
+
+ def initialize(self, form_editor):
+ self._form_editor = form_editor
+
+ def isContainer(self):
+ return False
+
+ def isInitialized(self):
+ return self._form_editor is not None
+
+ def name(self):
+ return "BECConsole"
+
+ def toolTip(self):
+ return "A terminal-like vt100 widget."
+
+ def whatsThis(self):
+ return self.toolTip()
diff --git a/bec_widgets/widgets/console/register_console.py b/bec_widgets/widgets/console/register_console.py
new file mode 100644
index 00000000..ae18e420
--- /dev/null
+++ b/bec_widgets/widgets/console/register_console.py
@@ -0,0 +1,15 @@
+def main(): # pragma: no cover
+ from qtpy import PYSIDE6
+
+ if not PYSIDE6:
+ print("PYSIDE6 is not available in the environment. Cannot patch designer.")
+ return
+ from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
+
+ from bec_widgets.widgets.console.console_plugin import BECConsolePlugin
+
+ QPyDesignerCustomWidgetCollection.addCustomWidget(BECConsolePlugin())
+
+
+if __name__ == "__main__": # pragma: no cover
+ main()