├── .gitignore ├── LICENSE ├── README.md ├── example.py ├── pyproject.toml └── src └── qtpyTerminal ├── __init__.py └── qtpyTerminal.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matias Guijarro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qtpyTerminal 2 | A Vt100 terminal widget for qtpy (PyQt, Pyside) 3 | 4 | # About 5 | 6 | qtpyTerminal is a QWidget derived from QPlainTextEdit, which can execute an interactive 7 | command to run in the default shell (`SHELL` environment variable). The shell process 8 | is forked, and input/output is redirected to the Qt widget. It is then possible to 9 | interact with the program, as in a VT100 terminal thanks to the use of Pyte for 10 | escape codes interpretation (see https://github.com/selectel/pyte). 11 | 12 | https://github.com/user-attachments/assets/03e76620-f880-453b-9539-939c006c0caf 13 | 14 | # Installation 15 | 16 | qtpyTerminal only requires pyte and qtpy - it should run seamlessly with different 17 | Python Qt backends like PyQt or Pyside. 18 | 19 | ```python 20 | pip install qtpyTerminal@git+https://github.com/mguijarr/qtpyTerminal.git 21 | ``` 22 | 23 | # Example 24 | 25 | ```python 26 | import sys 27 | from qtpy import QtGui, QtWidgets 28 | from qtpyTerminal import qtpyTerminal 29 | 30 | # Create the Qt application and console. 31 | app = QtWidgets.QApplication([]) 32 | mainwin = QtWidgets.QMainWindow() 33 | mainwin.setWindowTitle("qtpyTerminal example") 34 | container = QtWidgets.QWidget(mainwin) 35 | container.setLayout(QtWidgets.QVBoxLayout()) 36 | mainwin.setCentralWidget(container) 37 | 38 | console = qtpyTerminal(mainwin) 39 | 40 | def exit(): 41 | console.stop() 42 | app.quit() 43 | 44 | start_button = QtWidgets.QPushButton("Start shell", container) 45 | start_button.clicked.connect(console.start) 46 | container.layout().addWidget(start_button) 47 | container.layout().addWidget(console) 48 | quit_button = QtWidgets.QPushButton("Quit", container) 49 | quit_button.clicked.connect(exit) 50 | container.layout().addWidget(quit_button) 51 | 52 | # Show widget and launch Qt's event loop. 53 | mainwin.show() 54 | sys.exit(app.exec_()) 55 | 56 | ``` 57 | 58 | By default the process started by the terminal is defined by the `SHELL` environment variable. 59 | It is possible to call `.set_cmd()` to launch another command - for example `.set_cmd("python")` 60 | will start an interactive session of the Python interpreter in the Terminal widget. 61 | 62 | The widget size policy is `MinimumExpanding` on both directions. Vertical scrolling is handled. 63 | Please note that horizontal scrolling is not implemented yet. So, it is better to define a fixed 64 | number of columns. By default the terminal is 132 columns. 65 | 66 | The number of columns or default number of rows can be defined with `.set_cols()` or `.set_rows()`. 67 | It is also possible to give them directly as arguments of `qtpyTerminal` constructor. 68 | 69 | By default, the widgets uses "text" color (`QPalette`) for the main foreground color, and "base" 70 | color for the background. It can be changed by calling `set_fgcolor(QColor)` or `set_bgcolor(QColor)` 71 | respectively. 72 | 73 | # Contributing 74 | 75 | Contributions are welcome. Please submit merge requests. 76 | 77 | 78 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from qtpy import QtGui, QtWidgets 3 | from qtpyTerminal import qtpyTerminal 4 | 5 | # Create the Qt application and console. 6 | app = QtWidgets.QApplication([]) 7 | mainwin = QtWidgets.QMainWindow() 8 | mainwin.setWindowTitle("qtpyTerminal example") 9 | container = QtWidgets.QWidget(mainwin) 10 | container.setLayout(QtWidgets.QVBoxLayout()) 11 | mainwin.setCentralWidget(container) 12 | 13 | console = qtpyTerminal(mainwin) 14 | 15 | def exit(): 16 | console.stop() 17 | app.quit() 18 | 19 | start_button = QtWidgets.QPushButton("Start shell", container) 20 | start_button.clicked.connect(console.start) 21 | container.layout().addWidget(start_button) 22 | container.layout().addWidget(console) 23 | quit_button = QtWidgets.QPushButton("Quit", container) 24 | quit_button.clicked.connect(exit) 25 | container.layout().addWidget(quit_button) 26 | 27 | # Show widget and launch Qt's event loop. 28 | mainwin.show() 29 | sys.exit(app.exec_()) 30 | 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "qtpyTerminal" 7 | version = "0.1" 8 | description = "qtpyTerminal - Python Qt VT100 widget" 9 | requires-python = ">=3.10" 10 | classifiers = [ 11 | "Development Status :: 3 - Alpha", 12 | "Programming Language :: Python :: 3", 13 | "Topic :: Scientific/Engineering", 14 | ] 15 | dependencies = [ 16 | "qtpy~=2.4", 17 | "pyte", 18 | ] 19 | 20 | [tool.setuptools.packages] 21 | find = { where = ["src"] } 22 | 23 | [project.optional-dependencies] 24 | pyqt6 = ["PyQt6>=6.7", "PyQt6-WebEngine>=6.7"] 25 | pyside6 = ["PySide6>=6.7"] 26 | 27 | [tool.black] 28 | line-length = 100 29 | skip-magic-trailing-comma = true 30 | 31 | [tool.isort] 32 | profile = "black" 33 | line_length = 100 34 | multi_line_output = 3 35 | include_trailing_comma = true 36 | 37 | -------------------------------------------------------------------------------- /src/qtpyTerminal/__init__.py: -------------------------------------------------------------------------------- 1 | from . qtpyTerminal import qtpyTerminal 2 | -------------------------------------------------------------------------------- /src/qtpyTerminal/qtpyTerminal.py: -------------------------------------------------------------------------------- 1 | """ 2 | qtpyTerminal is a Qt widget that runs a Bash shell. 3 | 4 | qtpyTerminal VT100 emulation is powered by Pyte, 5 | (https://github.com/selectel/pyte). 6 | """ 7 | 8 | import collections 9 | import fcntl 10 | import functools 11 | import html 12 | import os 13 | import pty 14 | import signal 15 | import sys 16 | 17 | import pyte 18 | from pyte.screens import History 19 | from qtpy import QtCore, QtGui, QtWidgets 20 | from qtpy.QtCore import Property as pyqtProperty 21 | from qtpy.QtCore import QSize, QSocketNotifier, Qt, QTimer 22 | from qtpy.QtCore import Signal as pyqtSignal, Slot as pyqtSlot 23 | from qtpy.QtGui import QClipboard, QColor, QPalette, QTextCursor 24 | from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy 25 | 26 | 27 | def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name 28 | """Function with args, acting like a decorator, to display errors instead of raising an exception 29 | """ 30 | def error_managed(method): 31 | @pyqtSlot(*slot_args, **slot_kwargs) 32 | @functools.wraps(method) 33 | def wrapper(*args, **kwargs): 34 | try: 35 | return method(*args, **kwargs) 36 | except Exception: 37 | sys.excepthook(*sys.exc_info()) 38 | 39 | return wrapper 40 | 41 | return error_managed 42 | 43 | 44 | ansi_colors = { 45 | "black": "#000000", 46 | "red": "#CD0000", 47 | "green": "#00CD00", 48 | "brown": "#996633", # Brown, replacing the yellow 49 | "blue": "#0000EE", 50 | "magenta": "#CD00CD", 51 | "cyan": "#00CDCD", 52 | "white": "#E5E5E5", 53 | "brightblack": "#7F7F7F", 54 | "brightred": "#FF0000", 55 | "brightgreen": "#00FF00", 56 | "brightyellow": "#FFFF00", 57 | "brightblue": "#5C5CFF", 58 | "brightmagenta": "#FF00FF", 59 | "brightcyan": "#00FFFF", 60 | "brightwhite": "#FFFFFF", 61 | } 62 | 63 | control_keys_mapping = { 64 | QtCore.Qt.Key_A: b"\x01", # Ctrl-A 65 | QtCore.Qt.Key_B: b"\x02", # Ctrl-B 66 | QtCore.Qt.Key_C: b"\x03", # Ctrl-C 67 | QtCore.Qt.Key_D: b"\x04", # Ctrl-D 68 | QtCore.Qt.Key_E: b"\x05", # Ctrl-E 69 | QtCore.Qt.Key_F: b"\x06", # Ctrl-F 70 | QtCore.Qt.Key_G: b"\x07", # Ctrl-G (Bell) 71 | QtCore.Qt.Key_H: b"\x08", # Ctrl-H (Backspace) 72 | QtCore.Qt.Key_I: b"\x09", # Ctrl-I (Tab) 73 | QtCore.Qt.Key_J: b"\x0A", # Ctrl-J (Line Feed) 74 | QtCore.Qt.Key_K: b"\x0B", # Ctrl-K (Vertical Tab) 75 | QtCore.Qt.Key_L: b"\x0C", # Ctrl-L (Form Feed) 76 | QtCore.Qt.Key_M: b"\x0D", # Ctrl-M (Carriage Return) 77 | QtCore.Qt.Key_N: b"\x0E", # Ctrl-N 78 | QtCore.Qt.Key_O: b"\x0F", # Ctrl-O 79 | QtCore.Qt.Key_P: b"\x10", # Ctrl-P 80 | QtCore.Qt.Key_Q: b"\x11", # Ctrl-Q 81 | QtCore.Qt.Key_R: b"\x12", # Ctrl-R 82 | QtCore.Qt.Key_S: b"\x13", # Ctrl-S 83 | QtCore.Qt.Key_T: b"\x14", # Ctrl-T 84 | QtCore.Qt.Key_U: b"\x15", # Ctrl-U 85 | QtCore.Qt.Key_V: b"\x16", # Ctrl-V 86 | QtCore.Qt.Key_W: b"\x17", # Ctrl-W 87 | QtCore.Qt.Key_X: b"\x18", # Ctrl-X 88 | QtCore.Qt.Key_Y: b"\x19", # Ctrl-Y 89 | QtCore.Qt.Key_Z: b"\x1A", # Ctrl-Z 90 | QtCore.Qt.Key_Escape: b"\x1B", # Ctrl-Escape 91 | QtCore.Qt.Key_Backslash: b"\x1C", # Ctrl-\ 92 | QtCore.Qt.Key_Underscore: b"\x1F", # Ctrl-_ 93 | } 94 | 95 | normal_keys_mapping = { 96 | QtCore.Qt.Key_Return: b"\n", 97 | QtCore.Qt.Key_Space: b" ", 98 | QtCore.Qt.Key_Enter: b"\n", 99 | QtCore.Qt.Key_Tab: b"\t", 100 | QtCore.Qt.Key_Backspace: b"\x08", 101 | QtCore.Qt.Key_Home: b"\x47", 102 | QtCore.Qt.Key_End: b"\x4f", 103 | QtCore.Qt.Key_Left: b"\x02", 104 | QtCore.Qt.Key_Up: b"\x10", 105 | QtCore.Qt.Key_Right: b"\x06", 106 | QtCore.Qt.Key_Down: b"\x0E", 107 | QtCore.Qt.Key_PageUp: b"\x49", 108 | QtCore.Qt.Key_PageDown: b"\x51", 109 | QtCore.Qt.Key_F1: b"\x1b\x31", 110 | QtCore.Qt.Key_F2: b"\x1b\x32", 111 | QtCore.Qt.Key_F3: b"\x1b\x33", 112 | QtCore.Qt.Key_F4: b"\x1b\x34", 113 | QtCore.Qt.Key_F5: b"\x1b\x35", 114 | QtCore.Qt.Key_F6: b"\x1b\x36", 115 | QtCore.Qt.Key_F7: b"\x1b\x37", 116 | QtCore.Qt.Key_F8: b"\x1b\x38", 117 | QtCore.Qt.Key_F9: b"\x1b\x39", 118 | QtCore.Qt.Key_F10: b"\x1b\x30", 119 | QtCore.Qt.Key_F11: b"\x45", 120 | QtCore.Qt.Key_F12: b"\x46", 121 | } 122 | 123 | 124 | def QtKeyToAscii(event): 125 | """ 126 | Convert the Qt key event to the corresponding ASCII sequence for 127 | the terminal. This works fine for standard alphanumerical characters, but 128 | most other characters require terminal specific control sequences. 129 | """ 130 | if sys.platform == "darwin": 131 | # special case for MacOS 132 | # /!\ Qt maps ControlModifier to CMD 133 | # CMD-C, CMD-V for copy/paste 134 | # CTRL-C and other modifiers -> key mapping 135 | if event.modifiers() == QtCore.Qt.MetaModifier: 136 | if event.key() == Qt.Key_Backspace: 137 | return control_keys_mapping.get(Qt.Key_W) 138 | return control_keys_mapping.get(event.key()) 139 | elif event.modifiers() == QtCore.Qt.ControlModifier: 140 | if event.key() == Qt.Key_C: 141 | # copy 142 | return "copy" 143 | elif event.key() == Qt.Key_V: 144 | # paste 145 | return "paste" 146 | return None 147 | else: 148 | return normal_keys_mapping.get(event.key(), event.text().encode("utf8")) 149 | if event.modifiers() == QtCore.Qt.ControlModifier: 150 | return control_keys_mapping.get(event.key()) 151 | else: 152 | return normal_keys_mapping.get(event.key(), event.text().encode("utf8")) 153 | 154 | 155 | class Screen(pyte.HistoryScreen): 156 | def __init__(self, stdin_fd, cols, rows, historyLength): 157 | super().__init__(cols, rows, historyLength, ratio=1 / rows) 158 | self._fd = stdin_fd 159 | 160 | def write_process_input(self, data): 161 | """Response to CPR request (for example), 162 | this can be for other requests 163 | """ 164 | try: 165 | os.write(self._fd, data.encode("utf-8")) 166 | except (IOError, OSError): 167 | pass 168 | 169 | def resize(self, lines, columns): 170 | lines = lines or self.lines 171 | columns = columns or self.columns 172 | 173 | if lines == self.lines and columns == self.columns: 174 | return # No changes. 175 | 176 | self.dirty.clear() 177 | self.dirty.update(range(lines)) 178 | 179 | self.save_cursor() 180 | if lines < self.lines: 181 | if lines <= self.cursor.y: 182 | nlines_to_move_up = self.lines - lines 183 | for i in range(nlines_to_move_up): 184 | line = self.buffer[i] # .pop(0) 185 | self.history.top.append(line) 186 | self.cursor_position(0, 0) 187 | self.delete_lines(nlines_to_move_up) 188 | self.restore_cursor() 189 | self.cursor.y -= nlines_to_move_up 190 | else: 191 | self.restore_cursor() 192 | 193 | self.lines, self.columns = lines, columns 194 | self.history = History( 195 | self.history.top, 196 | self.history.bottom, 197 | 1 / self.lines, 198 | self.history.size, 199 | self.history.position, 200 | ) 201 | self.set_margins() 202 | 203 | 204 | class Backend(QtCore.QObject): 205 | """ 206 | This class will run as a qsocketnotifier (started in ``_TerminalWidget``) and poll the 207 | file descriptor of the underlying executed program. 208 | """ 209 | 210 | # Signals to communicate with ``_TerminalWidget``. 211 | dataReady = pyqtSignal(object) 212 | processExited = pyqtSignal() 213 | 214 | def __init__(self, fd, cols, rows): 215 | super().__init__() 216 | 217 | # File descriptor that connects to the process. 218 | self.fd = fd 219 | 220 | self.screen = Screen(self.fd, cols, rows, 10000) 221 | self.stream = pyte.ByteStream() 222 | self.stream.attach(self.screen) 223 | 224 | self.notifier = QSocketNotifier(fd, QSocketNotifier.Read) 225 | self.notifier.activated.connect(self._fd_readable) 226 | 227 | def _fd_readable(self): 228 | """ 229 | Poll the Bash output, run it through Pyte, and notify 230 | """ 231 | # Read the shell output until the file descriptor is closed. 232 | try: 233 | out = os.read(self.fd, 2**16) 234 | except OSError: 235 | self.processExited.emit() 236 | self.notifier.setEnabled(False) 237 | return 238 | 239 | # Feed output into Pyte's state machine and send the new screen 240 | # output to the GUI 241 | self.stream.feed(out) 242 | self.dataReady.emit(self.screen) 243 | 244 | 245 | class qtpyTerminal(QtWidgets.QWidget): 246 | """Container widget for the terminal text area""" 247 | def __init__(self, parent=None, cols=132): 248 | super().__init__(parent) 249 | 250 | self.term = _TerminalWidget(self, cols, rows=25) 251 | self.scroll_bar = QScrollBar(Qt.Vertical, self) 252 | # self.scroll_bar.hide() 253 | layout = QHBoxLayout(self) 254 | layout.addWidget(self.term) 255 | layout.addWidget(self.scroll_bar) 256 | layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) 257 | layout.setContentsMargins(0, 0, 0, 0) 258 | self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) 259 | 260 | pal = QPalette() 261 | self.set_bgcolor(pal.window().color()) 262 | self.set_fgcolor(pal.windowText().color()) 263 | self.term.set_scroll_bar(self.scroll_bar) 264 | self.set_cmd("") # will execute the default shell 265 | 266 | def minimumSizeHint(self): 267 | size = self.term.sizeHint() 268 | size.setWidth(size.width() + self.scroll_bar.width()) 269 | return size 270 | 271 | def sizeHint(self): 272 | return self.minimumSizeHint() 273 | 274 | def get_rows(self): 275 | return self.term.rows 276 | 277 | def set_rows(self, rows): 278 | self.term.rows = rows 279 | self.adjustSize() 280 | self.updateGeometry() 281 | 282 | def get_cols(self): 283 | return self.term.cols 284 | 285 | def set_cols(self, cols): 286 | self.term.cols = cols 287 | self.adjustSize() 288 | self.updateGeometry() 289 | 290 | def get_bgcolor(self): 291 | return QColor.fromString(self.term.bg_color) 292 | 293 | def set_bgcolor(self, color): 294 | self.term.bg_color = color.name(QColor.HexRgb) 295 | 296 | def get_fgcolor(self): 297 | return QColor.fromString(self.term.fg_color) 298 | 299 | def set_fgcolor(self, color): 300 | self.term.fg_color = color.name(QColor.HexRgb) 301 | 302 | def get_cmd(self): 303 | return self.term._cmd 304 | 305 | def set_cmd(self, cmd): 306 | if not cmd: 307 | cmd = os.environ["SHELL"] 308 | self.term._cmd = cmd 309 | if self.term.fd is None: 310 | # not started yet 311 | self.term.clear() 312 | self.term.appendHtml(f"
{chr(10).join(self.output)}") 661 | # did updates, all clean 662 | screen.dirty.clear() 663 | 664 | def update_term_size(self): 665 | fmt = QtGui.QFontMetrics(self.font()) 666 | char_width = fmt.width("w") 667 | char_height = fmt.height() 668 | self._cols = int(self.width() / char_width) 669 | self._rows = int(self.height() / char_height) 670 | 671 | def resizeEvent(self, event): 672 | self.update_term_size() 673 | if self.fd: 674 | self.backend.screen.resize(self._rows, self._cols) 675 | self.redraw_screen() 676 | self.adjust_scroll_bar() 677 | self.move_cursor() 678 | 679 | def wheelEvent(self, event): 680 | if not self.fd: 681 | return 682 | y = event.angleDelta().y() 683 | if y > 0: 684 | self.backend.screen.prev_page() 685 | else: 686 | self.backend.screen.next_page() 687 | self.redraw_screen() 688 | 689 | def fork_shell(self): 690 | """ 691 | Fork the current process and execute in shell. 692 | """ 693 | try: 694 | pid, fd = pty.fork() 695 | except (IOError, OSError): 696 | return False 697 | if pid == 0: 698 | try: 699 | ls = os.environ["LANG"].split(".") 700 | except KeyError: 701 | ls = [] 702 | if len(ls) < 2: 703 | ls = ["en_US", "UTF-8"] 704 | os.putenv("COLUMNS", str(self.cols)) 705 | os.putenv("LINES", str(self.rows)) 706 | os.putenv("TERM", "linux") 707 | os.putenv("LANG", ls[0] + ".UTF-8") 708 | if not self._cmd: 709 | self._cmd = os.environ["SHELL"] 710 | cmd = self._cmd 711 | if isinstance(cmd, str): 712 | cmd = cmd.split() 713 | try: 714 | os.execvp(cmd[0], cmd) 715 | except (IOError, OSError): 716 | pass 717 | os._exit(0) 718 | else: 719 | # We are in the parent process. 720 | # Set file control 721 | fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) 722 | return fd, pid 723 | 724 | 725 | if __name__ == "__main__": 726 | import os 727 | import sys 728 | 729 | from qtpy import QtGui, QtWidgets 730 | 731 | # Create the Qt application and console. 732 | app = QtWidgets.QApplication([]) 733 | mainwin = QtWidgets.QMainWindow() 734 | title = "qtpyTerminal" 735 | mainwin.setWindowTitle(title) 736 | 737 | console = qtpyTerminal(mainwin) 738 | mainwin.setCentralWidget(console) 739 | console.start() 740 | 741 | # Show widget and launch Qt's event loop. 742 | mainwin.show() 743 | sys.exit(app.exec_()) 744 | --------------------------------------------------------------------------------