bec-widgets 2.10.0__py3-none-any.whl → 2.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +13 -0
- PKG-INFO +1 -2
- {bec_widgets-2.10.0.dist-info → bec_widgets-2.10.1.dist-info}/METADATA +1 -2
- {bec_widgets-2.10.0.dist-info → bec_widgets-2.10.1.dist-info}/RECORD +8 -13
- pyproject.toml +1 -2
- bec_widgets/widgets/editors/console/__init__.py +0 -0
- bec_widgets/widgets/editors/console/console.py +0 -870
- bec_widgets/widgets/editors/console/console.pyproject +0 -1
- bec_widgets/widgets/editors/console/console_plugin.py +0 -58
- bec_widgets/widgets/editors/console/register_console.py +0 -15
- {bec_widgets-2.10.0.dist-info → bec_widgets-2.10.1.dist-info}/WHEEL +0 -0
- {bec_widgets-2.10.0.dist-info → bec_widgets-2.10.1.dist-info}/entry_points.txt +0 -0
- {bec_widgets-2.10.0.dist-info → bec_widgets-2.10.1.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,6 +1,19 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
3
|
|
4
|
+
## v2.10.1 (2025-06-02)
|
5
|
+
|
6
|
+
### Bug Fixes
|
7
|
+
|
8
|
+
- **console**: Qt console widget deleted
|
9
|
+
([`cd4e90a`](https://github.com/bec-project/bec_widgets/commit/cd4e90a79fcdbc96f4ec23db22375d05a48731db))
|
10
|
+
|
11
|
+
### Build System
|
12
|
+
|
13
|
+
- Pyte removed from dependencies
|
14
|
+
([`a64cf0d`](https://github.com/bec-project/bec_widgets/commit/a64cf0dd871c1419e02d3803c74cc45966baac19))
|
15
|
+
|
16
|
+
|
4
17
|
## v2.10.0 (2025-06-02)
|
5
18
|
|
6
19
|
### Bug Fixes
|
PKG-INFO
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bec_widgets
|
3
|
-
Version: 2.10.
|
3
|
+
Version: 2.10.1
|
4
4
|
Summary: BEC Widgets
|
5
5
|
Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
|
6
6
|
Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
|
@@ -17,7 +17,6 @@ Requires-Dist: isort>=5.13.2,~=5.13
|
|
17
17
|
Requires-Dist: pydantic~=2.0
|
18
18
|
Requires-Dist: pyqtgraph~=0.13
|
19
19
|
Requires-Dist: pyside6~=6.8.2
|
20
|
-
Requires-Dist: pyte
|
21
20
|
Requires-Dist: qtconsole>=5.5.1,~=5.5
|
22
21
|
Requires-Dist: qtpy~=2.4
|
23
22
|
Provides-Extra: dev
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bec_widgets
|
3
|
-
Version: 2.10.
|
3
|
+
Version: 2.10.1
|
4
4
|
Summary: BEC Widgets
|
5
5
|
Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
|
6
6
|
Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
|
@@ -17,7 +17,6 @@ Requires-Dist: isort>=5.13.2,~=5.13
|
|
17
17
|
Requires-Dist: pydantic~=2.0
|
18
18
|
Requires-Dist: pyqtgraph~=0.13
|
19
19
|
Requires-Dist: pyside6~=6.8.2
|
20
|
-
Requires-Dist: pyte
|
21
20
|
Requires-Dist: qtconsole>=5.5.1,~=5.5
|
22
21
|
Requires-Dist: qtpy~=2.4
|
23
22
|
Provides-Extra: dev
|
@@ -2,11 +2,11 @@
|
|
2
2
|
.gitlab-ci.yml,sha256=1nMYldzVk0tFkBWYTcUjumOrdSADASheWOAc0kOFDYs,9509
|
3
3
|
.pylintrc,sha256=eeY8YwSI74oFfq6IYIbCqnx3Vk8ZncKaatv96n_Y8Rs,18544
|
4
4
|
.readthedocs.yaml,sha256=ivqg3HTaOxNbEW3bzWh9MXAkrekuGoNdj0Mj3SdRYuw,639
|
5
|
-
CHANGELOG.md,sha256=
|
5
|
+
CHANGELOG.md,sha256=8TfG8PPlQX7cHUlQKo1f5h5jRxV-JZPgWg7Pjn4Ns2k,292512
|
6
6
|
LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
|
7
|
-
PKG-INFO,sha256=
|
7
|
+
PKG-INFO,sha256=cqpZ5fRKb9iyumyY2VgHNy0qtW_B-5gbcH-uPvrWkPA,1254
|
8
8
|
README.md,sha256=oY5Jc1uXehRASuwUJ0umin2vfkFh7tHF-LLruHTaQx0,3560
|
9
|
-
pyproject.toml,sha256=
|
9
|
+
pyproject.toml,sha256=T1p9cRdEf-x8mv73uVd77Cq2gmvp3vu7tihGr2VnxOk,2835
|
10
10
|
.git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
|
11
11
|
.github/pull_request_template.md,sha256=F_cJXzooWMFgMGtLK-7KeGcQt0B4AYFse5oN0zQ9p6g,801
|
12
12
|
.github/ISSUE_TEMPLATE/bug_report.yml,sha256=WdRnt7HGxvsIBLzhkaOWNfg8IJQYa_oV9_F08Ym6znQ,1081
|
@@ -219,11 +219,6 @@ bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui,sha256=BVm0Xydqbgm
|
|
219
219
|
bec_widgets/widgets/dap/lmfit_dialog/register_lm_fit_dialog.py,sha256=7tB1gsvv310_kVuKf2u4EdSR4F1posm7QCrWH5Kih-Q,480
|
220
220
|
bec_widgets/widgets/editors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
221
221
|
bec_widgets/widgets/editors/dict_backed_table.py,sha256=u8n5diNaKY-UBb5vkpDSRtSI7w7g1096g37co8nxA8A,6422
|
222
|
-
bec_widgets/widgets/editors/console/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
223
|
-
bec_widgets/widgets/editors/console/console.py,sha256=OB5vvqpYNhUGWzn5Dx5rR1n0rQulef5Yv-by5RGjr2w,29323
|
224
|
-
bec_widgets/widgets/editors/console/console.pyproject,sha256=JcoDuZG03g1Bxkd3Aipo7jjLexujfbibIZqXHIgLSWc,26
|
225
|
-
bec_widgets/widgets/editors/console/console_plugin.py,sha256=EvFTruYDVHiS4pHIwZnuEvJhS9eQoktuB_k5mcPuEts,1357
|
226
|
-
bec_widgets/widgets/editors/console/register_console.py,sha256=zoF-i3R9sRGzb85sdoxVunebYOfOD53fkCELTPtrFRc,471
|
227
222
|
bec_widgets/widgets/editors/jupyter_console/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
228
223
|
bec_widgets/widgets/editors/jupyter_console/jupyter_console.py,sha256=-e7HQOECeH5eDrJYh4BFIzRL78LDkooU4otabyN0aX4,2343
|
229
224
|
bec_widgets/widgets/editors/scan_metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -408,8 +403,8 @@ bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py,sha256=O
|
|
408
403
|
bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.pyproject,sha256=Lbi9zb6HNlIq14k6hlzR-oz6PIFShBuF7QxE6d87d64,34
|
409
404
|
bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button_plugin.py,sha256=CzChz2SSETYsR8-36meqWnsXCT-FIy_J_xeU5coWDY8,1350
|
410
405
|
bec_widgets/widgets/utility/visual/dark_mode_button/register_dark_mode_button.py,sha256=rMpZ1CaoucwobgPj1FuKTnt07W82bV1GaSYdoqcdMb8,521
|
411
|
-
bec_widgets-2.10.
|
412
|
-
bec_widgets-2.10.
|
413
|
-
bec_widgets-2.10.
|
414
|
-
bec_widgets-2.10.
|
415
|
-
bec_widgets-2.10.
|
406
|
+
bec_widgets-2.10.1.dist-info/METADATA,sha256=cqpZ5fRKb9iyumyY2VgHNy0qtW_B-5gbcH-uPvrWkPA,1254
|
407
|
+
bec_widgets-2.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
408
|
+
bec_widgets-2.10.1.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
|
409
|
+
bec_widgets-2.10.1.dist-info/licenses/LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
|
410
|
+
bec_widgets-2.10.1.dist-info/RECORD,,
|
pyproject.toml
CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "bec_widgets"
|
7
|
-
version = "2.10.
|
7
|
+
version = "2.10.1"
|
8
8
|
description = "BEC Widgets"
|
9
9
|
requires-python = ">=3.10"
|
10
10
|
classifiers = [
|
@@ -21,7 +21,6 @@ dependencies = [
|
|
21
21
|
"pydantic~=2.0",
|
22
22
|
"pyqtgraph~=0.13",
|
23
23
|
"PySide6~=6.8.2",
|
24
|
-
"pyte", # needed for vt100 console
|
25
24
|
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
26
25
|
"qtpy~=2.4",
|
27
26
|
]
|
File without changes
|
@@ -1,870 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
BECConsole is a Qt widget that runs a Bash shell.
|
3
|
-
|
4
|
-
BECConsole VT100 emulation is powered by Pyte,
|
5
|
-
(https://github.com/selectel/pyte).
|
6
|
-
"""
|
7
|
-
|
8
|
-
import collections
|
9
|
-
import fcntl
|
10
|
-
import html
|
11
|
-
import os
|
12
|
-
import pty
|
13
|
-
import re
|
14
|
-
import signal
|
15
|
-
import sys
|
16
|
-
import time
|
17
|
-
|
18
|
-
import pyte
|
19
|
-
from pygments.token import Token
|
20
|
-
from pyte.screens import History
|
21
|
-
from qtpy import QtCore, QtGui, QtWidgets
|
22
|
-
from qtpy.QtCore import Property as pyqtProperty
|
23
|
-
from qtpy.QtCore import QSize, QSocketNotifier, Qt, QTimer
|
24
|
-
from qtpy.QtCore import Signal as pyqtSignal
|
25
|
-
from qtpy.QtGui import QClipboard, QColor, QPalette, QTextCursor
|
26
|
-
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
|
27
|
-
|
28
|
-
from bec_widgets.utils.error_popups import SafeSlot as Slot
|
29
|
-
|
30
|
-
ansi_colors = {
|
31
|
-
"black": "#000000",
|
32
|
-
"red": "#CD0000",
|
33
|
-
"green": "#00CD00",
|
34
|
-
"brown": "#996633", # Brown, replacing the yellow
|
35
|
-
"blue": "#0000EE",
|
36
|
-
"magenta": "#CD00CD",
|
37
|
-
"cyan": "#00CDCD",
|
38
|
-
"white": "#E5E5E5",
|
39
|
-
"brightblack": "#7F7F7F",
|
40
|
-
"brightred": "#FF0000",
|
41
|
-
"brightgreen": "#00FF00",
|
42
|
-
"brightyellow": "#FFFF00",
|
43
|
-
"brightblue": "#5C5CFF",
|
44
|
-
"brightmagenta": "#FF00FF",
|
45
|
-
"brightcyan": "#00FFFF",
|
46
|
-
"brightwhite": "#FFFFFF",
|
47
|
-
}
|
48
|
-
|
49
|
-
control_keys_mapping = {
|
50
|
-
QtCore.Qt.Key_A: b"\x01", # Ctrl-A
|
51
|
-
QtCore.Qt.Key_B: b"\x02", # Ctrl-B
|
52
|
-
QtCore.Qt.Key_C: b"\x03", # Ctrl-C
|
53
|
-
QtCore.Qt.Key_D: b"\x04", # Ctrl-D
|
54
|
-
QtCore.Qt.Key_E: b"\x05", # Ctrl-E
|
55
|
-
QtCore.Qt.Key_F: b"\x06", # Ctrl-F
|
56
|
-
QtCore.Qt.Key_G: b"\x07", # Ctrl-G (Bell)
|
57
|
-
QtCore.Qt.Key_H: b"\x08", # Ctrl-H (Backspace)
|
58
|
-
QtCore.Qt.Key_I: b"\x09", # Ctrl-I (Tab)
|
59
|
-
QtCore.Qt.Key_J: b"\x0a", # Ctrl-J (Line Feed)
|
60
|
-
QtCore.Qt.Key_K: b"\x0b", # Ctrl-K (Vertical Tab)
|
61
|
-
QtCore.Qt.Key_L: b"\x0c", # Ctrl-L (Form Feed)
|
62
|
-
QtCore.Qt.Key_M: b"\x0d", # Ctrl-M (Carriage Return)
|
63
|
-
QtCore.Qt.Key_N: b"\x0e", # Ctrl-N
|
64
|
-
QtCore.Qt.Key_O: b"\x0f", # Ctrl-O
|
65
|
-
QtCore.Qt.Key_P: b"\x10", # Ctrl-P
|
66
|
-
QtCore.Qt.Key_Q: b"\x11", # Ctrl-Q
|
67
|
-
QtCore.Qt.Key_R: b"\x12", # Ctrl-R
|
68
|
-
QtCore.Qt.Key_S: b"\x13", # Ctrl-S
|
69
|
-
QtCore.Qt.Key_T: b"\x14", # Ctrl-T
|
70
|
-
QtCore.Qt.Key_U: b"\x15", # Ctrl-U
|
71
|
-
QtCore.Qt.Key_V: b"\x16", # Ctrl-V
|
72
|
-
QtCore.Qt.Key_W: b"\x17", # Ctrl-W
|
73
|
-
QtCore.Qt.Key_X: b"\x18", # Ctrl-X
|
74
|
-
QtCore.Qt.Key_Y: b"\x19", # Ctrl-Y
|
75
|
-
QtCore.Qt.Key_Z: b"\x1a", # Ctrl-Z
|
76
|
-
QtCore.Qt.Key_Escape: b"\x1b", # Ctrl-Escape
|
77
|
-
QtCore.Qt.Key_Backslash: b"\x1c", # Ctrl-\
|
78
|
-
QtCore.Qt.Key_Underscore: b"\x1f", # Ctrl-_
|
79
|
-
}
|
80
|
-
|
81
|
-
normal_keys_mapping = {
|
82
|
-
QtCore.Qt.Key_Return: b"\n",
|
83
|
-
QtCore.Qt.Key_Space: b" ",
|
84
|
-
QtCore.Qt.Key_Enter: b"\n",
|
85
|
-
QtCore.Qt.Key_Tab: b"\t",
|
86
|
-
QtCore.Qt.Key_Backspace: b"\x08",
|
87
|
-
QtCore.Qt.Key_Home: b"\x47",
|
88
|
-
QtCore.Qt.Key_End: b"\x4f",
|
89
|
-
QtCore.Qt.Key_Left: b"\x02",
|
90
|
-
QtCore.Qt.Key_Up: b"\x10",
|
91
|
-
QtCore.Qt.Key_Right: b"\x06",
|
92
|
-
QtCore.Qt.Key_Down: b"\x0e",
|
93
|
-
QtCore.Qt.Key_PageUp: b"\x49",
|
94
|
-
QtCore.Qt.Key_PageDown: b"\x51",
|
95
|
-
QtCore.Qt.Key_F1: b"\x1b\x31",
|
96
|
-
QtCore.Qt.Key_F2: b"\x1b\x32",
|
97
|
-
QtCore.Qt.Key_F3: b"\x1b\x33",
|
98
|
-
QtCore.Qt.Key_F4: b"\x1b\x34",
|
99
|
-
QtCore.Qt.Key_F5: b"\x1b\x35",
|
100
|
-
QtCore.Qt.Key_F6: b"\x1b\x36",
|
101
|
-
QtCore.Qt.Key_F7: b"\x1b\x37",
|
102
|
-
QtCore.Qt.Key_F8: b"\x1b\x38",
|
103
|
-
QtCore.Qt.Key_F9: b"\x1b\x39",
|
104
|
-
QtCore.Qt.Key_F10: b"\x1b\x30",
|
105
|
-
QtCore.Qt.Key_F11: b"\x45",
|
106
|
-
QtCore.Qt.Key_F12: b"\x46",
|
107
|
-
}
|
108
|
-
|
109
|
-
|
110
|
-
def QtKeyToAscii(event):
|
111
|
-
"""
|
112
|
-
Convert the Qt key event to the corresponding ASCII sequence for
|
113
|
-
the terminal. This works fine for standard alphanumerical characters, but
|
114
|
-
most other characters require terminal specific control sequences.
|
115
|
-
|
116
|
-
The conversion below works for TERM="linux" terminals.
|
117
|
-
"""
|
118
|
-
if sys.platform == "darwin":
|
119
|
-
# special case for MacOS
|
120
|
-
# /!\ Qt maps ControlModifier to CMD
|
121
|
-
# CMD-C, CMD-V for copy/paste
|
122
|
-
# CTRL-C and other modifiers -> key mapping
|
123
|
-
if event.modifiers() == QtCore.Qt.MetaModifier:
|
124
|
-
if event.key() == Qt.Key_Backspace:
|
125
|
-
return control_keys_mapping.get(Qt.Key_W)
|
126
|
-
return control_keys_mapping.get(event.key())
|
127
|
-
elif event.modifiers() == QtCore.Qt.ControlModifier:
|
128
|
-
if event.key() == Qt.Key_C:
|
129
|
-
# copy
|
130
|
-
return "copy"
|
131
|
-
elif event.key() == Qt.Key_V:
|
132
|
-
# paste
|
133
|
-
return "paste"
|
134
|
-
return None
|
135
|
-
else:
|
136
|
-
return normal_keys_mapping.get(event.key(), event.text().encode("utf8"))
|
137
|
-
if event.modifiers() == QtCore.Qt.ControlModifier:
|
138
|
-
return control_keys_mapping.get(event.key())
|
139
|
-
else:
|
140
|
-
return normal_keys_mapping.get(event.key(), event.text().encode("utf8"))
|
141
|
-
|
142
|
-
|
143
|
-
class Screen(pyte.HistoryScreen):
|
144
|
-
def __init__(self, stdin_fd, cols, rows, historyLength):
|
145
|
-
super().__init__(cols, rows, historyLength, ratio=1 / rows)
|
146
|
-
self._fd = stdin_fd
|
147
|
-
|
148
|
-
def write_process_input(self, data):
|
149
|
-
"""Response to CPR request (for example),
|
150
|
-
this can be for other requests
|
151
|
-
"""
|
152
|
-
try:
|
153
|
-
os.write(self._fd, data.encode("utf-8"))
|
154
|
-
except (IOError, OSError):
|
155
|
-
pass
|
156
|
-
|
157
|
-
def resize(self, lines, columns):
|
158
|
-
lines = lines or self.lines
|
159
|
-
columns = columns or self.columns
|
160
|
-
|
161
|
-
if lines == self.lines and columns == self.columns:
|
162
|
-
return # No changes.
|
163
|
-
|
164
|
-
self.dirty.clear()
|
165
|
-
self.dirty.update(range(lines))
|
166
|
-
|
167
|
-
self.save_cursor()
|
168
|
-
if lines < self.lines:
|
169
|
-
if lines <= self.cursor.y:
|
170
|
-
nlines_to_move_up = self.lines - lines
|
171
|
-
for i in range(nlines_to_move_up):
|
172
|
-
line = self.buffer[i] # .pop(0)
|
173
|
-
self.history.top.append(line)
|
174
|
-
self.cursor_position(0, 0)
|
175
|
-
self.delete_lines(nlines_to_move_up)
|
176
|
-
self.restore_cursor()
|
177
|
-
self.cursor.y -= nlines_to_move_up
|
178
|
-
else:
|
179
|
-
self.restore_cursor()
|
180
|
-
|
181
|
-
self.lines, self.columns = lines, columns
|
182
|
-
self.history = History(
|
183
|
-
self.history.top,
|
184
|
-
self.history.bottom,
|
185
|
-
1 / self.lines,
|
186
|
-
self.history.size,
|
187
|
-
self.history.position,
|
188
|
-
)
|
189
|
-
self.set_margins()
|
190
|
-
|
191
|
-
|
192
|
-
class Backend(QtCore.QObject):
|
193
|
-
"""
|
194
|
-
Poll Bash.
|
195
|
-
|
196
|
-
This class will run as a qsocketnotifier (started in ``_TerminalWidget``) and poll the
|
197
|
-
file descriptor of the Bash terminal.
|
198
|
-
"""
|
199
|
-
|
200
|
-
# Signals to communicate with ``_TerminalWidget``.
|
201
|
-
dataReady = pyqtSignal(object)
|
202
|
-
processExited = pyqtSignal()
|
203
|
-
|
204
|
-
def __init__(self, fd, cols, rows):
|
205
|
-
super().__init__()
|
206
|
-
|
207
|
-
# File descriptor that connects to Bash process.
|
208
|
-
self.fd = fd
|
209
|
-
|
210
|
-
# Setup Pyte (hard coded display size for now).
|
211
|
-
self.screen = Screen(self.fd, cols, rows, 10000)
|
212
|
-
self.stream = pyte.ByteStream()
|
213
|
-
self.stream.attach(self.screen)
|
214
|
-
|
215
|
-
self.notifier = QSocketNotifier(fd, QSocketNotifier.Read)
|
216
|
-
self.notifier.activated.connect(self._fd_readable)
|
217
|
-
|
218
|
-
def _fd_readable(self):
|
219
|
-
"""
|
220
|
-
Poll the Bash output, run it through Pyte, and notify
|
221
|
-
"""
|
222
|
-
# Read the shell output until the file descriptor is closed.
|
223
|
-
try:
|
224
|
-
out = os.read(self.fd, 2**16)
|
225
|
-
except OSError:
|
226
|
-
self.processExited.emit()
|
227
|
-
self.notifier.setEnabled(False)
|
228
|
-
return
|
229
|
-
|
230
|
-
# Feed output into Pyte's state machine and send the new screen
|
231
|
-
# output to the GUI
|
232
|
-
self.stream.feed(out)
|
233
|
-
self.dataReady.emit(self.screen)
|
234
|
-
|
235
|
-
|
236
|
-
class BECConsole(QtWidgets.QWidget):
|
237
|
-
"""Container widget for the terminal text area"""
|
238
|
-
|
239
|
-
PLUGIN = True
|
240
|
-
ICON_NAME = "terminal"
|
241
|
-
|
242
|
-
prompt = pyqtSignal(bool)
|
243
|
-
|
244
|
-
def __init__(self, parent=None, cols=132):
|
245
|
-
super().__init__(parent)
|
246
|
-
|
247
|
-
self.term = _TerminalWidget(self, cols, rows=43)
|
248
|
-
self.term.prompt.connect(self.prompt) # forward signal from term to this widget
|
249
|
-
|
250
|
-
self.scroll_bar = QScrollBar(Qt.Vertical, self)
|
251
|
-
# self.scroll_bar.hide()
|
252
|
-
layout = QHBoxLayout(self)
|
253
|
-
layout.addWidget(self.term)
|
254
|
-
layout.addWidget(self.scroll_bar)
|
255
|
-
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
256
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
257
|
-
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
258
|
-
|
259
|
-
pal = QPalette()
|
260
|
-
self.set_bgcolor(pal.window().color())
|
261
|
-
self.set_fgcolor(pal.windowText().color())
|
262
|
-
self.term.set_scroll_bar(self.scroll_bar)
|
263
|
-
self.set_cmd("bec --nogui")
|
264
|
-
|
265
|
-
self._check_designer_timer = QTimer()
|
266
|
-
self._check_designer_timer.timeout.connect(self.check_designer)
|
267
|
-
self._check_designer_timer.start(1000)
|
268
|
-
|
269
|
-
def minimumSizeHint(self):
|
270
|
-
size = self.term.sizeHint()
|
271
|
-
size.setWidth(size.width() + self.scroll_bar.width())
|
272
|
-
return size
|
273
|
-
|
274
|
-
def sizeHint(self):
|
275
|
-
return self.minimumSizeHint()
|
276
|
-
|
277
|
-
def check_designer(self, calls={"n": 0}):
|
278
|
-
calls["n"] += 1
|
279
|
-
if self.term.fd is not None:
|
280
|
-
# already started
|
281
|
-
self._check_designer_timer.stop()
|
282
|
-
elif self.window().windowTitle().endswith("[Preview]"):
|
283
|
-
# assuming Designer preview -> start
|
284
|
-
self._check_designer_timer.stop()
|
285
|
-
self.term.start()
|
286
|
-
elif calls["n"] >= 3:
|
287
|
-
# assuming not in Designer -> stop checking
|
288
|
-
self._check_designer_timer.stop()
|
289
|
-
|
290
|
-
def get_rows(self):
|
291
|
-
return self.term.rows
|
292
|
-
|
293
|
-
def set_rows(self, rows):
|
294
|
-
self.term.rows = rows
|
295
|
-
self.adjustSize()
|
296
|
-
self.updateGeometry()
|
297
|
-
|
298
|
-
def get_cols(self):
|
299
|
-
return self.term.cols
|
300
|
-
|
301
|
-
def set_cols(self, cols):
|
302
|
-
self.term.cols = cols
|
303
|
-
self.adjustSize()
|
304
|
-
self.updateGeometry()
|
305
|
-
|
306
|
-
def get_bgcolor(self):
|
307
|
-
return QColor.fromString(self.term.bg_color)
|
308
|
-
|
309
|
-
def set_bgcolor(self, color):
|
310
|
-
self.term.bg_color = color.name(QColor.HexRgb)
|
311
|
-
|
312
|
-
def get_fgcolor(self):
|
313
|
-
return QColor.fromString(self.term.fg_color)
|
314
|
-
|
315
|
-
def set_fgcolor(self, color):
|
316
|
-
self.term.fg_color = color.name(QColor.HexRgb)
|
317
|
-
|
318
|
-
def get_cmd(self):
|
319
|
-
return self.term._cmd
|
320
|
-
|
321
|
-
def set_cmd(self, cmd):
|
322
|
-
self.term._cmd = cmd
|
323
|
-
if self.term.fd is None:
|
324
|
-
# not started yet
|
325
|
-
self.term.clear()
|
326
|
-
self.term.appendHtml(f"<h2>BEC Console - {repr(cmd)}</h2>")
|
327
|
-
|
328
|
-
def start(self, deactivate_ctrl_d=True):
|
329
|
-
self.term.start(deactivate_ctrl_d=deactivate_ctrl_d)
|
330
|
-
|
331
|
-
def push(self, text, hit_return=False):
|
332
|
-
"""Push some text to the terminal"""
|
333
|
-
return self.term.push(text, hit_return=hit_return)
|
334
|
-
|
335
|
-
def execute_command(self, command):
|
336
|
-
self.push(command, hit_return=True)
|
337
|
-
|
338
|
-
def set_prompt_tokens(self, *tokens):
|
339
|
-
"""Prepare regexp to identify prompt, based on tokens
|
340
|
-
|
341
|
-
Tokens are returned from get_ipython().prompts.in_prompt_tokens()
|
342
|
-
"""
|
343
|
-
regex_parts = []
|
344
|
-
for token_type, token_value in tokens:
|
345
|
-
if token_type == Token.PromptNum: # Handle dynamic prompt number
|
346
|
-
regex_parts.append(r"[\d\?]+") # Match one or more digits or '?'
|
347
|
-
else:
|
348
|
-
# Escape other prompt parts (e.g., "In [", "]: ")
|
349
|
-
if not token_value:
|
350
|
-
regex_parts.append(".+?") # arbitrary string
|
351
|
-
else:
|
352
|
-
regex_parts.append(re.escape(token_value))
|
353
|
-
|
354
|
-
# Combine into a single regex
|
355
|
-
prompt_pattern = "".join(regex_parts)
|
356
|
-
self.term._prompt_re = re.compile(prompt_pattern + r"\s*$")
|
357
|
-
|
358
|
-
def terminate(self, timeout=10):
|
359
|
-
self.term.stop(timeout=timeout)
|
360
|
-
|
361
|
-
def send_ctrl_c(self, timeout=None):
|
362
|
-
self.term.send_ctrl_c(timeout)
|
363
|
-
|
364
|
-
cols = pyqtProperty(int, get_cols, set_cols)
|
365
|
-
rows = pyqtProperty(int, get_rows, set_rows)
|
366
|
-
bgcolor = pyqtProperty(QColor, get_bgcolor, set_bgcolor)
|
367
|
-
fgcolor = pyqtProperty(QColor, get_fgcolor, set_fgcolor)
|
368
|
-
cmd = pyqtProperty(str, get_cmd, set_cmd)
|
369
|
-
|
370
|
-
|
371
|
-
class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
372
|
-
"""
|
373
|
-
Start ``Backend`` process and render Pyte output as text.
|
374
|
-
"""
|
375
|
-
|
376
|
-
prompt = pyqtSignal(bool)
|
377
|
-
|
378
|
-
def __init__(self, parent, cols=125, rows=50, **kwargs):
|
379
|
-
# regexp to match prompt
|
380
|
-
self._prompt_re = None
|
381
|
-
# last prompt
|
382
|
-
self._prompt_str = None
|
383
|
-
# process pid
|
384
|
-
self.pid = None
|
385
|
-
# file descriptor to communicate with the subprocess
|
386
|
-
self.fd = None
|
387
|
-
self.backend = None
|
388
|
-
# command to execute
|
389
|
-
self._cmd = ""
|
390
|
-
# should ctrl-d be deactivated ? (prevent Python exit)
|
391
|
-
self._deactivate_ctrl_d = False
|
392
|
-
|
393
|
-
# Default colors
|
394
|
-
pal = QPalette()
|
395
|
-
self._fg_color = pal.text().color().name()
|
396
|
-
self._bg_color = pal.base().color().name()
|
397
|
-
|
398
|
-
# Specify the terminal size in terms of lines and columns.
|
399
|
-
self._rows = rows
|
400
|
-
self._cols = cols
|
401
|
-
self.output = collections.deque()
|
402
|
-
|
403
|
-
super().__init__(parent)
|
404
|
-
|
405
|
-
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding)
|
406
|
-
|
407
|
-
# Disable default scrollbars (we use our own, to be set via .set_scroll_bar())
|
408
|
-
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
409
|
-
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
410
|
-
self.scroll_bar = None
|
411
|
-
|
412
|
-
# Use Monospace fonts and disable line wrapping.
|
413
|
-
self.setFont(QtGui.QFont("Courier", 9))
|
414
|
-
self.setFont(QtGui.QFont("Monospace"))
|
415
|
-
self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
|
416
|
-
fmt = QtGui.QFontMetrics(self.font())
|
417
|
-
char_width = fmt.width("w")
|
418
|
-
self.setCursorWidth(char_width)
|
419
|
-
|
420
|
-
self.adjustSize()
|
421
|
-
self.updateGeometry()
|
422
|
-
self.update_stylesheet()
|
423
|
-
|
424
|
-
@property
|
425
|
-
def bg_color(self):
|
426
|
-
return self._bg_color
|
427
|
-
|
428
|
-
@bg_color.setter
|
429
|
-
def bg_color(self, hexcolor):
|
430
|
-
self._bg_color = hexcolor
|
431
|
-
self.update_stylesheet()
|
432
|
-
|
433
|
-
@property
|
434
|
-
def fg_color(self):
|
435
|
-
return self._fg_color
|
436
|
-
|
437
|
-
@fg_color.setter
|
438
|
-
def fg_color(self, hexcolor):
|
439
|
-
self._fg_color = hexcolor
|
440
|
-
self.update_stylesheet()
|
441
|
-
|
442
|
-
def update_stylesheet(self):
|
443
|
-
self.setStyleSheet(
|
444
|
-
f"QPlainTextEdit {{ border: 0; color: {self._fg_color}; background-color: {self._bg_color}; }} "
|
445
|
-
)
|
446
|
-
|
447
|
-
@property
|
448
|
-
def rows(self):
|
449
|
-
return self._rows
|
450
|
-
|
451
|
-
@rows.setter
|
452
|
-
def rows(self, rows: int):
|
453
|
-
if self.backend is None:
|
454
|
-
# not initialized yet, ok to change
|
455
|
-
self._rows = rows
|
456
|
-
self.adjustSize()
|
457
|
-
self.updateGeometry()
|
458
|
-
else:
|
459
|
-
raise RuntimeError("Cannot change rows after console is started.")
|
460
|
-
|
461
|
-
@property
|
462
|
-
def cols(self):
|
463
|
-
return self._cols
|
464
|
-
|
465
|
-
@cols.setter
|
466
|
-
def cols(self, cols: int):
|
467
|
-
if self.fd is None:
|
468
|
-
# not initialized yet, ok to change
|
469
|
-
self._cols = cols
|
470
|
-
self.adjustSize()
|
471
|
-
self.updateGeometry()
|
472
|
-
else:
|
473
|
-
raise RuntimeError("Cannot change cols after console is started.")
|
474
|
-
|
475
|
-
def start(self, deactivate_ctrl_d: bool = False):
|
476
|
-
self._deactivate_ctrl_d = deactivate_ctrl_d
|
477
|
-
|
478
|
-
self.update_term_size()
|
479
|
-
|
480
|
-
# Start the Bash process
|
481
|
-
self.pid, self.fd = self.fork_shell()
|
482
|
-
|
483
|
-
if self.fd:
|
484
|
-
# Create the ``Backend`` object
|
485
|
-
self.backend = Backend(self.fd, self.cols, self.rows)
|
486
|
-
self.backend.dataReady.connect(self.data_ready)
|
487
|
-
self.backend.processExited.connect(self.process_exited)
|
488
|
-
else:
|
489
|
-
self.process_exited()
|
490
|
-
|
491
|
-
def process_exited(self):
|
492
|
-
self.fd = None
|
493
|
-
self.clear()
|
494
|
-
self.appendHtml(f"<br><h2>{repr(self._cmd)} - Process exited.</h2>")
|
495
|
-
self.setReadOnly(True)
|
496
|
-
|
497
|
-
def send_ctrl_c(self, wait_prompt=True, timeout=None):
|
498
|
-
"""Send CTRL-C to the process
|
499
|
-
|
500
|
-
If wait_prompt=True (default), wait for a new prompt after CTRL-C
|
501
|
-
If no prompt is displayed after 'timeout' seconds, TimeoutError is raised
|
502
|
-
"""
|
503
|
-
os.kill(self.pid, signal.SIGINT)
|
504
|
-
if wait_prompt:
|
505
|
-
timeout_error = False
|
506
|
-
if timeout:
|
507
|
-
|
508
|
-
def set_timeout_error():
|
509
|
-
nonlocal timeout_error
|
510
|
-
timeout_error = True
|
511
|
-
|
512
|
-
timeout_timer = QTimer()
|
513
|
-
timeout_timer.singleShot(timeout * 1000, set_timeout_error)
|
514
|
-
while self._prompt_str is None:
|
515
|
-
QApplication.instance().process_events()
|
516
|
-
if timeout_error:
|
517
|
-
raise TimeoutError(
|
518
|
-
f"CTRL-C: could not get back to prompt after {timeout} seconds."
|
519
|
-
)
|
520
|
-
|
521
|
-
def _is_running(self):
|
522
|
-
if os.waitpid(self.pid, os.WNOHANG) == (0, 0):
|
523
|
-
return True
|
524
|
-
return False
|
525
|
-
|
526
|
-
def stop(self, kill=True, timeout=None):
|
527
|
-
"""Stop the running process
|
528
|
-
|
529
|
-
SIGTERM is the default signal for terminating processes.
|
530
|
-
|
531
|
-
If kill=True (default), SIGKILL will be sent if the process does not exit after timeout
|
532
|
-
"""
|
533
|
-
# try to exit gracefully
|
534
|
-
os.kill(self.pid, signal.SIGTERM)
|
535
|
-
|
536
|
-
# wait until process is truly dead
|
537
|
-
t0 = time.perf_counter()
|
538
|
-
while self._is_running():
|
539
|
-
time.sleep(1)
|
540
|
-
if timeout is not None and time.perf_counter() - t0 > timeout:
|
541
|
-
# still alive after 'timeout' seconds
|
542
|
-
if kill:
|
543
|
-
# send SIGKILL and make a last check in loop
|
544
|
-
os.kill(self.pid, signal.SIGKILL)
|
545
|
-
kill = False
|
546
|
-
else:
|
547
|
-
# still running after timeout...
|
548
|
-
raise TimeoutError(
|
549
|
-
f"Could not terminate process with pid: {self.pid} within timeout"
|
550
|
-
)
|
551
|
-
self.process_exited()
|
552
|
-
|
553
|
-
def data_ready(self, screen):
|
554
|
-
"""Handle new screen: redraw, set scroll bar max and slider, move cursor to its position
|
555
|
-
|
556
|
-
This method is triggered via a signal from ``Backend``.
|
557
|
-
"""
|
558
|
-
self.redraw_screen()
|
559
|
-
self.adjust_scroll_bar()
|
560
|
-
self.move_cursor()
|
561
|
-
|
562
|
-
def minimumSizeHint(self):
|
563
|
-
"""Return minimum size for current cols and rows"""
|
564
|
-
fmt = QtGui.QFontMetrics(self.font())
|
565
|
-
char_width = fmt.width("w")
|
566
|
-
char_height = fmt.height()
|
567
|
-
width = char_width * self.cols
|
568
|
-
height = char_height * self.rows
|
569
|
-
return QSize(width, height)
|
570
|
-
|
571
|
-
def sizeHint(self):
|
572
|
-
return self.minimumSizeHint()
|
573
|
-
|
574
|
-
def set_scroll_bar(self, scroll_bar):
|
575
|
-
self.scroll_bar = scroll_bar
|
576
|
-
self.scroll_bar.setMinimum(0)
|
577
|
-
self.scroll_bar.valueChanged.connect(self.scroll_value_change)
|
578
|
-
|
579
|
-
def scroll_value_change(self, value, old={"value": -1}):
|
580
|
-
if self.backend is None:
|
581
|
-
return
|
582
|
-
if old["value"] == -1:
|
583
|
-
old["value"] = self.scroll_bar.maximum()
|
584
|
-
if value <= old["value"]:
|
585
|
-
# scroll up
|
586
|
-
# value is number of lines from the start
|
587
|
-
nlines = old["value"] - value
|
588
|
-
# history ratio gives prev_page == 1 line
|
589
|
-
for i in range(nlines):
|
590
|
-
self.backend.screen.prev_page()
|
591
|
-
else:
|
592
|
-
# scroll down
|
593
|
-
nlines = value - old["value"]
|
594
|
-
for i in range(nlines):
|
595
|
-
self.backend.screen.next_page()
|
596
|
-
old["value"] = value
|
597
|
-
self.redraw_screen()
|
598
|
-
|
599
|
-
def adjust_scroll_bar(self):
|
600
|
-
sb = self.scroll_bar
|
601
|
-
sb.valueChanged.disconnect(self.scroll_value_change)
|
602
|
-
tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
|
603
|
-
sb.setMaximum(tmp if tmp > 0 else 0)
|
604
|
-
sb.setSliderPosition(tmp if tmp > 0 else 0)
|
605
|
-
# if tmp > 0:
|
606
|
-
# # show scrollbar, but delayed - prevent recursion with widget size change
|
607
|
-
# QTimer.singleShot(0, scrollbar.show)
|
608
|
-
# else:
|
609
|
-
# QTimer.singleShot(0, scrollbar.hide)
|
610
|
-
sb.valueChanged.connect(self.scroll_value_change)
|
611
|
-
|
612
|
-
def write(self, data):
|
613
|
-
try:
|
614
|
-
os.write(self.fd, data)
|
615
|
-
except (IOError, OSError):
|
616
|
-
self.process_exited()
|
617
|
-
|
618
|
-
@Slot(object)
|
619
|
-
def keyPressEvent(self, event):
|
620
|
-
"""
|
621
|
-
Redirect all keystrokes to the terminal process.
|
622
|
-
"""
|
623
|
-
if self.fd is None:
|
624
|
-
# not started
|
625
|
-
return
|
626
|
-
# Convert the Qt key to the correct ASCII code.
|
627
|
-
if (
|
628
|
-
self._deactivate_ctrl_d
|
629
|
-
and event.modifiers() == QtCore.Qt.ControlModifier
|
630
|
-
and event.key() == QtCore.Qt.Key_D
|
631
|
-
):
|
632
|
-
return None
|
633
|
-
|
634
|
-
code = QtKeyToAscii(event)
|
635
|
-
if code == "copy":
|
636
|
-
# MacOS only: CMD-C handling
|
637
|
-
self.copy()
|
638
|
-
elif code == "paste":
|
639
|
-
# MacOS only: CMD-V handling
|
640
|
-
self._push_clipboard()
|
641
|
-
elif code is not None:
|
642
|
-
self.write(code)
|
643
|
-
|
644
|
-
def push(self, text, hit_return=False):
|
645
|
-
"""
|
646
|
-
Write 'text' to terminal
|
647
|
-
"""
|
648
|
-
self.write(text.encode("utf-8"))
|
649
|
-
if hit_return:
|
650
|
-
self.write(b"\n")
|
651
|
-
|
652
|
-
def contextMenuEvent(self, event):
|
653
|
-
if self.fd is None:
|
654
|
-
return
|
655
|
-
menu = self.createStandardContextMenu()
|
656
|
-
for action in menu.actions():
|
657
|
-
# remove all actions except copy and paste
|
658
|
-
if "opy" in action.text():
|
659
|
-
# redefine text without shortcut
|
660
|
-
# since it probably clashes with control codes (like CTRL-C etc)
|
661
|
-
action.setText("Copy")
|
662
|
-
continue
|
663
|
-
if "aste" in action.text():
|
664
|
-
# redefine text without shortcut
|
665
|
-
action.setText("Paste")
|
666
|
-
# paste -> have to insert with self.push
|
667
|
-
action.triggered.connect(self._push_clipboard)
|
668
|
-
continue
|
669
|
-
menu.removeAction(action)
|
670
|
-
menu.exec_(event.globalPos())
|
671
|
-
|
672
|
-
def _push_clipboard(self):
|
673
|
-
clipboard = QApplication.instance().clipboard()
|
674
|
-
self.push(clipboard.text())
|
675
|
-
|
676
|
-
def move_cursor(self):
|
677
|
-
textCursor = self.textCursor()
|
678
|
-
textCursor.setPosition(0)
|
679
|
-
textCursor.movePosition(
|
680
|
-
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
681
|
-
)
|
682
|
-
textCursor.movePosition(
|
683
|
-
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
684
|
-
)
|
685
|
-
self.setTextCursor(textCursor)
|
686
|
-
|
687
|
-
def mouseReleaseEvent(self, event):
|
688
|
-
if self.fd is None:
|
689
|
-
return
|
690
|
-
if event.button() == Qt.MiddleButton:
|
691
|
-
# push primary selection buffer ("mouse clipboard") to terminal
|
692
|
-
clipboard = QApplication.instance().clipboard()
|
693
|
-
if clipboard.supportsSelection():
|
694
|
-
self.push(clipboard.text(QClipboard.Selection))
|
695
|
-
return None
|
696
|
-
elif event.button() == Qt.LeftButton:
|
697
|
-
# left button click
|
698
|
-
textCursor = self.textCursor()
|
699
|
-
if textCursor.selectedText():
|
700
|
-
# mouse was used to select text -> nothing to do
|
701
|
-
pass
|
702
|
-
else:
|
703
|
-
# a simple 'click', move scrollbar to end
|
704
|
-
self.scroll_bar.setSliderPosition(self.scroll_bar.maximum())
|
705
|
-
self.move_cursor()
|
706
|
-
return None
|
707
|
-
return super().mouseReleaseEvent(event)
|
708
|
-
|
709
|
-
def redraw_screen(self):
|
710
|
-
"""
|
711
|
-
Render the screen as formatted text into the widget.
|
712
|
-
"""
|
713
|
-
screen = self.backend.screen
|
714
|
-
|
715
|
-
# Clear the widget
|
716
|
-
if screen.dirty:
|
717
|
-
self.clear()
|
718
|
-
while len(self.output) < (max(screen.dirty) + 1):
|
719
|
-
self.output.append("")
|
720
|
-
while len(self.output) > (max(screen.dirty) + 1):
|
721
|
-
self.output.pop()
|
722
|
-
|
723
|
-
# Prepare the HTML output
|
724
|
-
for line_no in screen.dirty:
|
725
|
-
line = text = ""
|
726
|
-
style = old_style = ""
|
727
|
-
old_idx = 0
|
728
|
-
for idx, ch in screen.buffer[line_no].items():
|
729
|
-
text += " " * (idx - old_idx - 1)
|
730
|
-
old_idx = idx
|
731
|
-
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 ''}"
|
732
|
-
if style != old_style:
|
733
|
-
if old_style:
|
734
|
-
line += f"<span style={repr(old_style)}>{html.escape(text, quote=True)}</span>"
|
735
|
-
else:
|
736
|
-
line += html.escape(text, quote=True)
|
737
|
-
text = ""
|
738
|
-
old_style = style
|
739
|
-
text += ch.data
|
740
|
-
if style:
|
741
|
-
line += f"<span style={repr(style)}>{html.escape(text, quote=True)}</span>"
|
742
|
-
else:
|
743
|
-
line += html.escape(text, quote=True)
|
744
|
-
# do a check at the cursor position:
|
745
|
-
# it is possible x pos > output line length,
|
746
|
-
# for example if last escape codes are "cursor forward" past end of text,
|
747
|
-
# like IPython does for "..." prompt (in a block, like "for" loop or "while" for example)
|
748
|
-
# In this case, cursor is at 12 but last text output is at 8 -> insert spaces
|
749
|
-
if line_no == screen.cursor.y:
|
750
|
-
llen = len(screen.buffer[line_no])
|
751
|
-
if llen < screen.cursor.x:
|
752
|
-
line += " " * (screen.cursor.x - llen)
|
753
|
-
self.output[line_no] = line
|
754
|
-
# fill the text area with HTML contents in one go
|
755
|
-
self.appendHtml(f"<pre>{chr(10).join(self.output)}</pre>")
|
756
|
-
|
757
|
-
if self._prompt_re is not None:
|
758
|
-
text_buf = self.toPlainText()
|
759
|
-
prompt = self._prompt_re.search(text_buf)
|
760
|
-
if prompt is None:
|
761
|
-
if self._prompt_str:
|
762
|
-
self.prompt.emit(False)
|
763
|
-
self._prompt_str = None
|
764
|
-
else:
|
765
|
-
prompt_str = prompt.string.rstrip()
|
766
|
-
if prompt_str != self._prompt_str:
|
767
|
-
self._prompt_str = prompt_str
|
768
|
-
self.prompt.emit(True)
|
769
|
-
|
770
|
-
# did updates, all clean
|
771
|
-
screen.dirty.clear()
|
772
|
-
|
773
|
-
def update_term_size(self):
|
774
|
-
fmt = QtGui.QFontMetrics(self.font())
|
775
|
-
char_width = fmt.width("w")
|
776
|
-
char_height = fmt.height()
|
777
|
-
self._cols = int(self.width() / char_width)
|
778
|
-
self._rows = int(self.height() / char_height)
|
779
|
-
|
780
|
-
def resizeEvent(self, event):
|
781
|
-
self.update_term_size()
|
782
|
-
if self.fd:
|
783
|
-
self.backend.screen.resize(self._rows, self._cols)
|
784
|
-
self.redraw_screen()
|
785
|
-
self.adjust_scroll_bar()
|
786
|
-
self.move_cursor()
|
787
|
-
|
788
|
-
def wheelEvent(self, event):
|
789
|
-
if not self.fd:
|
790
|
-
return
|
791
|
-
y = event.angleDelta().y()
|
792
|
-
if y > 0:
|
793
|
-
self.backend.screen.prev_page()
|
794
|
-
else:
|
795
|
-
self.backend.screen.next_page()
|
796
|
-
self.redraw_screen()
|
797
|
-
|
798
|
-
def fork_shell(self):
|
799
|
-
"""
|
800
|
-
Fork the current process and execute bec in shell.
|
801
|
-
"""
|
802
|
-
try:
|
803
|
-
pid, fd = pty.fork()
|
804
|
-
except (IOError, OSError):
|
805
|
-
return False
|
806
|
-
if pid == 0:
|
807
|
-
try:
|
808
|
-
ls = os.environ["LANG"].split(".")
|
809
|
-
except KeyError:
|
810
|
-
ls = []
|
811
|
-
if len(ls) < 2:
|
812
|
-
ls = ["en_US", "UTF-8"]
|
813
|
-
os.putenv("COLUMNS", str(self.cols))
|
814
|
-
os.putenv("LINES", str(self.rows))
|
815
|
-
os.putenv("TERM", "linux")
|
816
|
-
os.putenv("LANG", ls[0] + ".UTF-8")
|
817
|
-
if not self._cmd:
|
818
|
-
self._cmd = os.environ["SHELL"]
|
819
|
-
cmd = self._cmd
|
820
|
-
if isinstance(cmd, str):
|
821
|
-
cmd = cmd.split()
|
822
|
-
try:
|
823
|
-
os.execvp(cmd[0], cmd)
|
824
|
-
except (IOError, OSError):
|
825
|
-
pass
|
826
|
-
os._exit(0)
|
827
|
-
else:
|
828
|
-
# We are in the parent process.
|
829
|
-
# Set file control
|
830
|
-
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
831
|
-
return pid, fd
|
832
|
-
|
833
|
-
|
834
|
-
if __name__ == "__main__":
|
835
|
-
import os
|
836
|
-
import sys
|
837
|
-
|
838
|
-
from qtpy import QtGui, QtWidgets
|
839
|
-
|
840
|
-
# Create the Qt application and console.
|
841
|
-
app = QtWidgets.QApplication([])
|
842
|
-
mainwin = QtWidgets.QMainWindow()
|
843
|
-
title = "BECConsole"
|
844
|
-
mainwin.setWindowTitle(title)
|
845
|
-
|
846
|
-
console = BECConsole(mainwin)
|
847
|
-
mainwin.setCentralWidget(console)
|
848
|
-
|
849
|
-
def check_prompt(at_prompt):
|
850
|
-
if at_prompt:
|
851
|
-
print("NEW PROMPT")
|
852
|
-
else:
|
853
|
-
print("EXECUTING SOMETHING...")
|
854
|
-
|
855
|
-
console.set_prompt_tokens(
|
856
|
-
(Token.OutPromptNum, "•"),
|
857
|
-
(Token.Prompt, ""), # will match arbitrary string,
|
858
|
-
(Token.Prompt, " ["),
|
859
|
-
(Token.PromptNum, "3"),
|
860
|
-
(Token.Prompt, "/"),
|
861
|
-
(Token.PromptNum, "1"),
|
862
|
-
(Token.Prompt, "] "),
|
863
|
-
(Token.Prompt, "❯❯"),
|
864
|
-
)
|
865
|
-
console.prompt.connect(check_prompt)
|
866
|
-
console.start()
|
867
|
-
|
868
|
-
# Show widget and launch Qt's event loop.
|
869
|
-
mainwin.show()
|
870
|
-
sys.exit(app.exec_())
|
@@ -1 +0,0 @@
|
|
1
|
-
{'files': ['console.py']}
|
@@ -1,58 +0,0 @@
|
|
1
|
-
# Copyright (C) 2022 The Qt Company Ltd.
|
2
|
-
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
3
|
-
import os
|
4
|
-
|
5
|
-
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
6
|
-
|
7
|
-
import bec_widgets
|
8
|
-
from bec_widgets.utils.bec_designer import designer_material_icon
|
9
|
-
from bec_widgets.widgets.editors.console.console import BECConsole
|
10
|
-
|
11
|
-
DOM_XML = """
|
12
|
-
<ui language='c++'>
|
13
|
-
<widget class='BECConsole' name='bec_console'>
|
14
|
-
</widget>
|
15
|
-
</ui>
|
16
|
-
"""
|
17
|
-
|
18
|
-
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
19
|
-
|
20
|
-
|
21
|
-
class BECConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
22
|
-
def __init__(self):
|
23
|
-
super().__init__()
|
24
|
-
self._form_editor = None
|
25
|
-
|
26
|
-
def createWidget(self, parent):
|
27
|
-
t = BECConsole(parent)
|
28
|
-
return t
|
29
|
-
|
30
|
-
def domXml(self):
|
31
|
-
return DOM_XML
|
32
|
-
|
33
|
-
def group(self):
|
34
|
-
return "BEC Console"
|
35
|
-
|
36
|
-
def icon(self):
|
37
|
-
return designer_material_icon(BECConsole.ICON_NAME)
|
38
|
-
|
39
|
-
def includeFile(self):
|
40
|
-
return "bec_console"
|
41
|
-
|
42
|
-
def initialize(self, form_editor):
|
43
|
-
self._form_editor = form_editor
|
44
|
-
|
45
|
-
def isContainer(self):
|
46
|
-
return False
|
47
|
-
|
48
|
-
def isInitialized(self):
|
49
|
-
return self._form_editor is not None
|
50
|
-
|
51
|
-
def name(self):
|
52
|
-
return "BECConsole"
|
53
|
-
|
54
|
-
def toolTip(self):
|
55
|
-
return "A terminal-like vt100 widget."
|
56
|
-
|
57
|
-
def whatsThis(self):
|
58
|
-
return self.toolTip()
|
@@ -1,15 +0,0 @@
|
|
1
|
-
def main(): # pragma: no cover
|
2
|
-
from qtpy import PYSIDE6
|
3
|
-
|
4
|
-
if not PYSIDE6:
|
5
|
-
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
6
|
-
return
|
7
|
-
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
8
|
-
|
9
|
-
from bec_widgets.widgets.editors.console.console_plugin import BECConsolePlugin
|
10
|
-
|
11
|
-
QPyDesignerCustomWidgetCollection.addCustomWidget(BECConsolePlugin())
|
12
|
-
|
13
|
-
|
14
|
-
if __name__ == "__main__": # pragma: no cover
|
15
|
-
main()
|
File without changes
|
File without changes
|
File without changes
|