bec-widgets 0.76.1__py3-none-any.whl → 0.77.0__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 +36 -38
- PKG-INFO +2 -1
- bec_widgets/cli/client.py +73 -196
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +25 -4
- bec_widgets/utils/bec_connector.py +66 -8
- bec_widgets/utils/colors.py +38 -0
- bec_widgets/utils/yaml_dialog.py +27 -3
- bec_widgets/widgets/console/console.py +496 -0
- bec_widgets/widgets/dock/dock.py +2 -2
- bec_widgets/widgets/dock/dock_area.py +2 -2
- bec_widgets/widgets/figure/figure.py +149 -195
- bec_widgets/widgets/figure/plots/image/image.py +62 -49
- bec_widgets/widgets/figure/plots/image/image_item.py +4 -3
- bec_widgets/widgets/figure/plots/motor_map/motor_map.py +98 -29
- bec_widgets/widgets/figure/plots/plot_base.py +1 -1
- bec_widgets/widgets/figure/plots/waveform/waveform.py +7 -8
- bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +2 -2
- bec_widgets/widgets/ring_progress_bar/ring.py +3 -3
- bec_widgets/widgets/ring_progress_bar/ring_progress_bar.py +3 -3
- {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/METADATA +2 -1
- {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/RECORD +38 -37
- pyproject.toml +2 -1
- tests/end-2-end/test_bec_dock_rpc_e2e.py +16 -16
- tests/end-2-end/test_bec_figure_rpc_e2e.py +7 -7
- tests/end-2-end/test_rpc_register_e2e.py +8 -8
- tests/unit_tests/client_mocks.py +1 -0
- tests/unit_tests/test_bec_figure.py +49 -26
- tests/unit_tests/test_bec_motor_map.py +179 -41
- tests/unit_tests/test_color_validation.py +15 -0
- tests/unit_tests/test_device_input_base.py +1 -1
- tests/unit_tests/test_device_input_widgets.py +2 -0
- tests/unit_tests/test_motor_control.py +5 -4
- tests/unit_tests/test_plot_base.py +3 -3
- tests/unit_tests/test_waveform1d.py +18 -17
- tests/unit_tests/test_yaml_dialog.py +7 -7
- {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/WHEEL +0 -0
- {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,496 @@
|
|
1
|
+
"""
|
2
|
+
BECConsole is a Qt widget that runs a Bash shell. The widget can be used and
|
3
|
+
embedded like any other Qt widget.
|
4
|
+
|
5
|
+
BECConsole is powered by Pyte, a Python based terminal emulator
|
6
|
+
(https://github.com/selectel/pyte).
|
7
|
+
"""
|
8
|
+
|
9
|
+
import fcntl
|
10
|
+
import html
|
11
|
+
import os
|
12
|
+
import pty
|
13
|
+
import subprocess
|
14
|
+
import sys
|
15
|
+
import threading
|
16
|
+
|
17
|
+
import pyte
|
18
|
+
from qtpy import QtCore, QtGui, QtWidgets
|
19
|
+
from qtpy.QtCore import QSize, QSocketNotifier, Qt
|
20
|
+
from qtpy.QtCore import Signal as pyqtSignal
|
21
|
+
from qtpy.QtCore import Slot as pyqtSlot
|
22
|
+
from qtpy.QtGui import QClipboard, QTextCursor
|
23
|
+
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
|
24
|
+
|
25
|
+
ansi_colors = {
|
26
|
+
"black": "#000000",
|
27
|
+
"red": "#CD0000",
|
28
|
+
"green": "#00CD00",
|
29
|
+
"brown": "#996633", # Brown, replacing the yellow
|
30
|
+
"blue": "#0000EE",
|
31
|
+
"magenta": "#CD00CD",
|
32
|
+
"cyan": "#00CDCD",
|
33
|
+
"white": "#E5E5E5",
|
34
|
+
"brightblack": "#7F7F7F",
|
35
|
+
"brightred": "#FF0000",
|
36
|
+
"brightgreen": "#00FF00",
|
37
|
+
"brightyellow": "#FFFF00",
|
38
|
+
"brightblue": "#5C5CFF",
|
39
|
+
"brightmagenta": "#FF00FF",
|
40
|
+
"brightcyan": "#00FFFF",
|
41
|
+
"brightwhite": "#FFFFFF",
|
42
|
+
}
|
43
|
+
|
44
|
+
control_keys_mapping = {
|
45
|
+
QtCore.Qt.Key_A: b"\x01", # Ctrl-A
|
46
|
+
QtCore.Qt.Key_B: b"\x02", # Ctrl-B
|
47
|
+
QtCore.Qt.Key_C: b"\x03", # Ctrl-C
|
48
|
+
QtCore.Qt.Key_D: b"\x04", # Ctrl-D
|
49
|
+
QtCore.Qt.Key_E: b"\x05", # Ctrl-E
|
50
|
+
QtCore.Qt.Key_F: b"\x06", # Ctrl-F
|
51
|
+
QtCore.Qt.Key_G: b"\x07", # Ctrl-G (Bell)
|
52
|
+
QtCore.Qt.Key_H: b"\x08", # Ctrl-H (Backspace)
|
53
|
+
QtCore.Qt.Key_I: b"\x09", # Ctrl-I (Tab)
|
54
|
+
QtCore.Qt.Key_J: b"\x0A", # Ctrl-J (Line Feed)
|
55
|
+
QtCore.Qt.Key_K: b"\x0B", # Ctrl-K (Vertical Tab)
|
56
|
+
QtCore.Qt.Key_L: b"\x0C", # Ctrl-L (Form Feed)
|
57
|
+
QtCore.Qt.Key_M: b"\x0D", # Ctrl-M (Carriage Return)
|
58
|
+
QtCore.Qt.Key_N: b"\x0E", # Ctrl-N
|
59
|
+
QtCore.Qt.Key_O: b"\x0F", # Ctrl-O
|
60
|
+
QtCore.Qt.Key_P: b"\x10", # Ctrl-P
|
61
|
+
QtCore.Qt.Key_Q: b"\x11", # Ctrl-Q
|
62
|
+
QtCore.Qt.Key_R: b"\x12", # Ctrl-R
|
63
|
+
QtCore.Qt.Key_S: b"\x13", # Ctrl-S
|
64
|
+
QtCore.Qt.Key_T: b"\x14", # Ctrl-T
|
65
|
+
QtCore.Qt.Key_U: b"\x15", # Ctrl-U
|
66
|
+
QtCore.Qt.Key_V: b"\x16", # Ctrl-V
|
67
|
+
QtCore.Qt.Key_W: b"\x17", # Ctrl-W
|
68
|
+
QtCore.Qt.Key_X: b"\x18", # Ctrl-X
|
69
|
+
QtCore.Qt.Key_Y: b"\x19", # Ctrl-Y
|
70
|
+
QtCore.Qt.Key_Z: b"\x1A", # Ctrl-Z
|
71
|
+
QtCore.Qt.Key_Escape: b"\x1B", # Ctrl-Escape
|
72
|
+
QtCore.Qt.Key_Backslash: b"\x1C", # Ctrl-\
|
73
|
+
QtCore.Qt.Key_Underscore: b"\x1F", # Ctrl-_
|
74
|
+
}
|
75
|
+
|
76
|
+
normal_keys_mapping = {
|
77
|
+
QtCore.Qt.Key_Return: b"\n",
|
78
|
+
QtCore.Qt.Key_Space: b" ",
|
79
|
+
QtCore.Qt.Key_Enter: b"\n",
|
80
|
+
QtCore.Qt.Key_Tab: b"\t",
|
81
|
+
QtCore.Qt.Key_Backspace: b"\x08",
|
82
|
+
QtCore.Qt.Key_Home: b"\x47",
|
83
|
+
QtCore.Qt.Key_End: b"\x4f",
|
84
|
+
QtCore.Qt.Key_Left: b"\x02",
|
85
|
+
QtCore.Qt.Key_Up: b"\x10",
|
86
|
+
QtCore.Qt.Key_Right: b"\x06",
|
87
|
+
QtCore.Qt.Key_Down: b"\x0E",
|
88
|
+
QtCore.Qt.Key_PageUp: b"\x49",
|
89
|
+
QtCore.Qt.Key_PageDown: b"\x51",
|
90
|
+
QtCore.Qt.Key_F1: b"\x1b\x31",
|
91
|
+
QtCore.Qt.Key_F2: b"\x1b\x32",
|
92
|
+
QtCore.Qt.Key_F3: b"\x1b\x33",
|
93
|
+
QtCore.Qt.Key_F4: b"\x1b\x34",
|
94
|
+
QtCore.Qt.Key_F5: b"\x1b\x35",
|
95
|
+
QtCore.Qt.Key_F6: b"\x1b\x36",
|
96
|
+
QtCore.Qt.Key_F7: b"\x1b\x37",
|
97
|
+
QtCore.Qt.Key_F8: b"\x1b\x38",
|
98
|
+
QtCore.Qt.Key_F9: b"\x1b\x39",
|
99
|
+
QtCore.Qt.Key_F10: b"\x1b\x30",
|
100
|
+
QtCore.Qt.Key_F11: b"\x45",
|
101
|
+
QtCore.Qt.Key_F12: b"\x46",
|
102
|
+
}
|
103
|
+
|
104
|
+
|
105
|
+
def QtKeyToAscii(event):
|
106
|
+
"""
|
107
|
+
Convert the Qt key event to the corresponding ASCII sequence for
|
108
|
+
the terminal. This works fine for standard alphanumerical characters, but
|
109
|
+
most other characters require terminal specific control sequences.
|
110
|
+
|
111
|
+
The conversion below works for TERM="linux" terminals.
|
112
|
+
"""
|
113
|
+
if sys.platform == "darwin":
|
114
|
+
# special case for MacOS
|
115
|
+
# /!\ Qt maps ControlModifier to CMD
|
116
|
+
# CMD-C, CMD-V for copy/paste
|
117
|
+
# CTRL-C and other modifiers -> key mapping
|
118
|
+
if event.modifiers() == QtCore.Qt.MetaModifier:
|
119
|
+
if event.key() == Qt.Key_Backspace:
|
120
|
+
return control_keys_mapping.get(Qt.Key_W)
|
121
|
+
return control_keys_mapping.get(event.key())
|
122
|
+
elif event.modifiers() == QtCore.Qt.ControlModifier:
|
123
|
+
if event.key() == Qt.Key_C:
|
124
|
+
# copy
|
125
|
+
return "copy"
|
126
|
+
elif event.key() == Qt.Key_V:
|
127
|
+
# paste
|
128
|
+
return "paste"
|
129
|
+
return None
|
130
|
+
else:
|
131
|
+
return normal_keys_mapping.get(event.key(), event.text().encode("utf8"))
|
132
|
+
if event.modifiers() == QtCore.Qt.ControlModifier:
|
133
|
+
return control_keys_mapping.get(event.key())
|
134
|
+
else:
|
135
|
+
return normal_keys_mapping.get(event.key(), event.text().encode("utf8"))
|
136
|
+
|
137
|
+
|
138
|
+
class Screen(pyte.HistoryScreen):
|
139
|
+
def __init__(self, stdin_fd, numColumns, numLines, historyLength):
|
140
|
+
super().__init__(numColumns, numLines, historyLength, ratio=1 / numLines)
|
141
|
+
self._fd = stdin_fd
|
142
|
+
|
143
|
+
def write_process_input(self, data):
|
144
|
+
"""Response to CPR request for example"""
|
145
|
+
os.write(self._fd, data.encode("utf-8"))
|
146
|
+
|
147
|
+
|
148
|
+
class Backend(QtCore.QObject):
|
149
|
+
"""
|
150
|
+
Poll Bash.
|
151
|
+
|
152
|
+
This class will run as a qsocketnotifier (started in ``_TerminalWidget``) and poll the
|
153
|
+
file descriptor of the Bash terminal.
|
154
|
+
"""
|
155
|
+
|
156
|
+
# Signals to communicate with ``_TerminalWidget``.
|
157
|
+
startWork = pyqtSignal()
|
158
|
+
dataReady = pyqtSignal(object)
|
159
|
+
|
160
|
+
def __init__(self, fd, numColumns, numLines):
|
161
|
+
super().__init__()
|
162
|
+
|
163
|
+
# File descriptor that connects to Bash process.
|
164
|
+
self.fd = fd
|
165
|
+
|
166
|
+
# Setup Pyte (hard coded display size for now).
|
167
|
+
self.screen = Screen(self.fd, numColumns, numLines, 10000)
|
168
|
+
self.stream = pyte.ByteStream()
|
169
|
+
self.stream.attach(self.screen)
|
170
|
+
|
171
|
+
self.notifier = QSocketNotifier(fd, QSocketNotifier.Read)
|
172
|
+
self.notifier.activated.connect(self._fd_readable)
|
173
|
+
|
174
|
+
def _fd_readable(self):
|
175
|
+
"""
|
176
|
+
Poll the Bash output, run it through Pyte, and notify the main applet.
|
177
|
+
"""
|
178
|
+
# Read the shell output until the file descriptor is closed.
|
179
|
+
try:
|
180
|
+
out = os.read(self.fd, 2**16)
|
181
|
+
except OSError:
|
182
|
+
return
|
183
|
+
|
184
|
+
# Feed output into Pyte's state machine and send the new screen
|
185
|
+
# output to the GUI
|
186
|
+
self.stream.feed(out)
|
187
|
+
self.dataReady.emit(self.screen)
|
188
|
+
|
189
|
+
|
190
|
+
class BECConsole(QtWidgets.QScrollArea):
|
191
|
+
"""Container widget for the terminal text area"""
|
192
|
+
|
193
|
+
def __init__(self, parent=None, numLines=50, numColumns=125):
|
194
|
+
super().__init__(parent)
|
195
|
+
|
196
|
+
self.innerWidget = QtWidgets.QWidget(self)
|
197
|
+
QHBoxLayout(self.innerWidget)
|
198
|
+
self.innerWidget.layout().setContentsMargins(0, 0, 0, 0)
|
199
|
+
|
200
|
+
self.term = _TerminalWidget(self.innerWidget, numLines, numColumns)
|
201
|
+
self.term.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
202
|
+
self.innerWidget.layout().addWidget(self.term)
|
203
|
+
|
204
|
+
self.scroll_bar = QScrollBar(Qt.Vertical, self.term)
|
205
|
+
self.innerWidget.layout().addWidget(self.scroll_bar)
|
206
|
+
|
207
|
+
self.term.set_scroll(self.scroll_bar)
|
208
|
+
|
209
|
+
self.setWidget(self.innerWidget)
|
210
|
+
|
211
|
+
def start(self, cmd=["bec", "--nogui"], deactivate_ctrl_d=True):
|
212
|
+
self.term._cmd = cmd
|
213
|
+
self.term.start(deactivate_ctrl_d=deactivate_ctrl_d)
|
214
|
+
|
215
|
+
def push(self, text):
|
216
|
+
"""Push some text to the terminal"""
|
217
|
+
return self.term.push(text)
|
218
|
+
|
219
|
+
|
220
|
+
class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
221
|
+
"""
|
222
|
+
Start ``Backend`` process and render Pyte output as text.
|
223
|
+
"""
|
224
|
+
|
225
|
+
def __init__(self, parent, numColumns, numLines, **kwargs):
|
226
|
+
super().__init__(parent)
|
227
|
+
|
228
|
+
# file descriptor to communicate with the subprocess
|
229
|
+
self.fd = None
|
230
|
+
self.backend = None
|
231
|
+
self.lock = threading.Lock()
|
232
|
+
# command to execute
|
233
|
+
self._cmd = None
|
234
|
+
# should ctrl-d be deactivated ? (prevent Python exit)
|
235
|
+
self._deactivate_ctrl_d = False
|
236
|
+
|
237
|
+
# Specify the terminal size in terms of lines and columns.
|
238
|
+
self.numLines = numLines
|
239
|
+
self.numColumns = numColumns
|
240
|
+
self.output = [""] * numLines
|
241
|
+
|
242
|
+
# Use Monospace fonts and disable line wrapping.
|
243
|
+
self.setFont(QtGui.QFont("Courier", 9))
|
244
|
+
self.setFont(QtGui.QFont("Monospace"))
|
245
|
+
self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
|
246
|
+
|
247
|
+
# Disable vertical scrollbar (we use our own, to be set via .set_scroll())
|
248
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
249
|
+
|
250
|
+
fmt = QtGui.QFontMetrics(self.font())
|
251
|
+
self._char_width = fmt.width("w")
|
252
|
+
self._char_height = fmt.height()
|
253
|
+
self.setCursorWidth(self._char_width)
|
254
|
+
# self.setStyleSheet("QPlainTextEdit { color: #ffff00; background-color: #303030; } ");
|
255
|
+
|
256
|
+
def start(self, deactivate_ctrl_d=False):
|
257
|
+
self._deactivate_ctrl_d = deactivate_ctrl_d
|
258
|
+
|
259
|
+
# Start the Bash process
|
260
|
+
self.fd = self.forkShell()
|
261
|
+
|
262
|
+
# Create the ``Backend`` object
|
263
|
+
self.backend = Backend(self.fd, self.numColumns, self.numLines)
|
264
|
+
self.backend.dataReady.connect(self.dataReady)
|
265
|
+
|
266
|
+
def minimumSizeHint(self):
|
267
|
+
width = self._char_width * self.numColumns
|
268
|
+
height = self._char_height * self.numLines
|
269
|
+
return QSize(width, height + 20)
|
270
|
+
|
271
|
+
def set_scroll(self, scroll):
|
272
|
+
self.scroll = scroll
|
273
|
+
self.scroll.setMinimum(0)
|
274
|
+
self.scroll.valueChanged.connect(self.scroll_value_change)
|
275
|
+
|
276
|
+
def scroll_value_change(self, value, old={"value": 0}):
|
277
|
+
if value <= old["value"]:
|
278
|
+
# scroll up
|
279
|
+
# value is number of lines from the start
|
280
|
+
nlines = old["value"] - value
|
281
|
+
# history ratio gives prev_page == 1 line
|
282
|
+
for i in range(nlines):
|
283
|
+
self.backend.screen.prev_page()
|
284
|
+
else:
|
285
|
+
# scroll down
|
286
|
+
nlines = value - old["value"]
|
287
|
+
for i in range(nlines):
|
288
|
+
self.backend.screen.next_page()
|
289
|
+
old["value"] = value
|
290
|
+
self.dataReady(self.backend.screen, reset_scroll=False)
|
291
|
+
|
292
|
+
@pyqtSlot(object)
|
293
|
+
def keyPressEvent(self, event):
|
294
|
+
"""
|
295
|
+
Redirect all keystrokes to the terminal process.
|
296
|
+
"""
|
297
|
+
# Convert the Qt key to the correct ASCII code.
|
298
|
+
if (
|
299
|
+
self._deactivate_ctrl_d
|
300
|
+
and event.modifiers() == QtCore.Qt.ControlModifier
|
301
|
+
and event.key() == QtCore.Qt.Key_D
|
302
|
+
):
|
303
|
+
return None
|
304
|
+
|
305
|
+
code = QtKeyToAscii(event)
|
306
|
+
if code == "copy":
|
307
|
+
# MacOS only: CMD-C handling
|
308
|
+
self.copy()
|
309
|
+
elif code == "paste":
|
310
|
+
# MacOS only: CMD-V handling
|
311
|
+
self._push_clipboard()
|
312
|
+
elif code is not None:
|
313
|
+
os.write(self.fd, code)
|
314
|
+
|
315
|
+
def push(self, text):
|
316
|
+
"""
|
317
|
+
Write 'text' to terminal
|
318
|
+
"""
|
319
|
+
os.write(self.fd, text.encode("utf-8"))
|
320
|
+
|
321
|
+
def contextMenuEvent(self, event):
|
322
|
+
menu = self.createStandardContextMenu()
|
323
|
+
for action in menu.actions():
|
324
|
+
# remove all actions except copy and paste
|
325
|
+
if "opy" in action.text():
|
326
|
+
# redefine text without shortcut
|
327
|
+
# since it probably clashes with control codes (like CTRL-C etc)
|
328
|
+
action.setText("Copy")
|
329
|
+
continue
|
330
|
+
if "aste" in action.text():
|
331
|
+
# redefine text without shortcut
|
332
|
+
action.setText("Paste")
|
333
|
+
# paste -> have to insert with self.push
|
334
|
+
action.triggered.connect(self._push_clipboard)
|
335
|
+
continue
|
336
|
+
menu.removeAction(action)
|
337
|
+
menu.exec_(event.globalPos())
|
338
|
+
|
339
|
+
def _push_clipboard(self):
|
340
|
+
clipboard = QApplication.instance().clipboard()
|
341
|
+
self.push(clipboard.text())
|
342
|
+
|
343
|
+
def mouseReleaseEvent(self, event):
|
344
|
+
if event.button() == Qt.MiddleButton:
|
345
|
+
# push primary selection buffer ("mouse clipboard") to terminal
|
346
|
+
clipboard = QApplication.instance().clipboard()
|
347
|
+
if clipboard.supportsSelection():
|
348
|
+
self.push(clipboard.text(QClipboard.Selection))
|
349
|
+
return None
|
350
|
+
elif event.button() == Qt.LeftButton:
|
351
|
+
# left button click
|
352
|
+
textCursor = self.textCursor()
|
353
|
+
if textCursor.selectedText():
|
354
|
+
# mouse was used to select text -> nothing to do
|
355
|
+
pass
|
356
|
+
else:
|
357
|
+
# a simple 'click', make cursor going to end
|
358
|
+
textCursor.setPosition(0)
|
359
|
+
textCursor.movePosition(
|
360
|
+
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
361
|
+
)
|
362
|
+
textCursor.movePosition(
|
363
|
+
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
364
|
+
)
|
365
|
+
self.setTextCursor(textCursor)
|
366
|
+
self.ensureCursorVisible()
|
367
|
+
return None
|
368
|
+
return super().mouseReleaseEvent(event)
|
369
|
+
|
370
|
+
def dataReady(self, screenData, reset_scroll=True):
|
371
|
+
"""
|
372
|
+
Render the new screen as text into the widget.
|
373
|
+
|
374
|
+
This method is triggered via a signal from ``Backend``.
|
375
|
+
"""
|
376
|
+
with self.lock:
|
377
|
+
# Clear the widget
|
378
|
+
self.clear()
|
379
|
+
|
380
|
+
# Prepare the HTML output
|
381
|
+
for line_no in screenData.dirty:
|
382
|
+
line = text = ""
|
383
|
+
style = old_style = ""
|
384
|
+
for ch in screenData.buffer[line_no].values():
|
385
|
+
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 ''}"
|
386
|
+
if style != old_style:
|
387
|
+
if old_style:
|
388
|
+
line += f"<span style={repr(old_style)}>{html.escape(text, quote=True)}</span>"
|
389
|
+
else:
|
390
|
+
line += html.escape(text, quote=True)
|
391
|
+
text = ""
|
392
|
+
old_style = style
|
393
|
+
text += ch.data
|
394
|
+
if style:
|
395
|
+
line += f"<span style={repr(style)}>{html.escape(text, quote=True)}</span>"
|
396
|
+
else:
|
397
|
+
line += html.escape(text, quote=True)
|
398
|
+
self.output[line_no] = line
|
399
|
+
# fill the text area with HTML contents in one go
|
400
|
+
self.appendHtml(f"<pre>{chr(10).join(self.output)}</pre>")
|
401
|
+
# done updates, all clean
|
402
|
+
screenData.dirty.clear()
|
403
|
+
|
404
|
+
# Activate cursor
|
405
|
+
textCursor = self.textCursor()
|
406
|
+
textCursor.setPosition(0)
|
407
|
+
textCursor.movePosition(QTextCursor.Down, QTextCursor.MoveAnchor, screenData.cursor.y)
|
408
|
+
textCursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, screenData.cursor.x)
|
409
|
+
self.setTextCursor(textCursor)
|
410
|
+
self.ensureCursorVisible()
|
411
|
+
|
412
|
+
# manage scroll
|
413
|
+
if reset_scroll:
|
414
|
+
self.scroll.valueChanged.disconnect(self.scroll_value_change)
|
415
|
+
tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
|
416
|
+
self.scroll.setMaximum(tmp if tmp > 0 else 0)
|
417
|
+
self.scroll.setSliderPosition(len(self.backend.screen.history.top))
|
418
|
+
self.scroll.valueChanged.connect(self.scroll_value_change)
|
419
|
+
|
420
|
+
# def resizeEvent(self, event):
|
421
|
+
# with self.lock:
|
422
|
+
# self.numColumns = int(self.width() / self._char_width)
|
423
|
+
# self.numLines = int(self.height() / self._char_height)
|
424
|
+
# self.output = [""] * self.numLines
|
425
|
+
# print("RESIZING TO", self.numColumns, "x", self.numLines)
|
426
|
+
# self.backend.screen.resize(self.numLines, self.numColumns)
|
427
|
+
|
428
|
+
def wheelEvent(self, event):
|
429
|
+
y = event.angleDelta().y()
|
430
|
+
if y > 0:
|
431
|
+
self.backend.screen.prev_page()
|
432
|
+
else:
|
433
|
+
self.backend.screen.next_page()
|
434
|
+
self.dataReady(self.backend.screen, reset_scroll=False)
|
435
|
+
|
436
|
+
def forkShell(self):
|
437
|
+
"""
|
438
|
+
Fork the current process and execute bec in shell.
|
439
|
+
"""
|
440
|
+
try:
|
441
|
+
pid, fd = pty.fork()
|
442
|
+
except (IOError, OSError):
|
443
|
+
return False
|
444
|
+
if pid == 0:
|
445
|
+
# Safe way to make it work under BSD and Linux
|
446
|
+
try:
|
447
|
+
ls = os.environ["LANG"].split(".")
|
448
|
+
except KeyError:
|
449
|
+
ls = []
|
450
|
+
if len(ls) < 2:
|
451
|
+
ls = ["en_US", "UTF-8"]
|
452
|
+
try:
|
453
|
+
os.putenv("COLUMNS", str(self.numColumns))
|
454
|
+
os.putenv("LINES", str(self.numLines))
|
455
|
+
os.putenv("TERM", "linux")
|
456
|
+
os.putenv("LANG", ls[0] + ".UTF-8")
|
457
|
+
if isinstance(self._cmd, str):
|
458
|
+
os.execvp(self._cmd, self._cmd)
|
459
|
+
else:
|
460
|
+
os.execvp(self._cmd[0], self._cmd)
|
461
|
+
# print "child_pid", child_pid, sts
|
462
|
+
except (IOError, OSError):
|
463
|
+
pass
|
464
|
+
# self.proc_finish(sid)
|
465
|
+
os._exit(0)
|
466
|
+
else:
|
467
|
+
# We are in the parent process.
|
468
|
+
# Set file control
|
469
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
470
|
+
print("Spawned Bash shell (PID {})".format(pid))
|
471
|
+
return fd
|
472
|
+
|
473
|
+
|
474
|
+
if __name__ == "__main__":
|
475
|
+
import os
|
476
|
+
import sys
|
477
|
+
|
478
|
+
from qtpy import QtGui, QtWidgets
|
479
|
+
|
480
|
+
# Terminal size in characters.
|
481
|
+
numLines = 25
|
482
|
+
numColumns = 100
|
483
|
+
|
484
|
+
# Create the Qt application and QBash instance.
|
485
|
+
app = QtWidgets.QApplication([])
|
486
|
+
mainwin = QtWidgets.QMainWindow()
|
487
|
+
title = "BECConsole ({}x{})".format(numColumns, numLines)
|
488
|
+
mainwin.setWindowTitle(title)
|
489
|
+
|
490
|
+
console = BECConsole(mainwin, numColumns, numLines)
|
491
|
+
mainwin.setCentralWidget(console)
|
492
|
+
console.start()
|
493
|
+
|
494
|
+
# Show widget and launch Qt's event loop.
|
495
|
+
mainwin.show()
|
496
|
+
sys.exit(app.exec_())
|
bec_widgets/widgets/dock/dock.py
CHANGED
@@ -23,7 +23,7 @@ class DockAreaConfig(ConnectionConfig):
|
|
23
23
|
|
24
24
|
class BECDockArea(BECConnector, DockArea):
|
25
25
|
USER_ACCESS = [
|
26
|
-
"
|
26
|
+
"_config_dict",
|
27
27
|
"panels",
|
28
28
|
"save_state",
|
29
29
|
"remove_dock",
|
@@ -32,7 +32,7 @@ class BECDockArea(BECConnector, DockArea):
|
|
32
32
|
"clear_all",
|
33
33
|
"detach_dock",
|
34
34
|
"attach_all",
|
35
|
-
"
|
35
|
+
"_get_all_rpc",
|
36
36
|
"temp_areas",
|
37
37
|
]
|
38
38
|
|