bec-widgets 0.110.0__py3-none-any.whl → 0.112.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 +26 -30
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +42 -0
- bec_widgets/cli/generate_cli.py +1 -0
- bec_widgets/qt_utils/palette_viewer.py +1 -1
- bec_widgets/widgets/console/console.py +366 -129
- bec_widgets/widgets/console/console.pyproject +1 -0
- bec_widgets/widgets/console/console_plugin.py +58 -0
- bec_widgets/widgets/console/register_console.py +15 -0
- bec_widgets/widgets/position_indicator/position_indicator.py +251 -33
- bec_widgets/widgets/positioner_box/positioner_box.py +6 -2
- bec_widgets/widgets/positioner_box/positioner_box.ui +14 -1
- bec_widgets/widgets/positioner_box/positioner_control_line.py +0 -2
- bec_widgets/widgets/positioner_box/positioner_control_line.ui +41 -23
- {bec_widgets-0.110.0.dist-info → bec_widgets-0.112.0.dist-info}/METADATA +1 -1
- {bec_widgets-0.110.0.dist-info → bec_widgets-0.112.0.dist-info}/RECORD +20 -17
- pyproject.toml +1 -1
- {bec_widgets-0.110.0.dist-info → bec_widgets-0.112.0.dist-info}/WHEEL +0 -0
- {bec_widgets-0.110.0.dist-info → bec_widgets-0.112.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-0.110.0.dist-info → bec_widgets-0.112.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,24 +1,24 @@
|
|
1
1
|
"""
|
2
|
-
BECConsole is a Qt widget that runs a Bash shell.
|
3
|
-
embedded like any other Qt widget.
|
2
|
+
BECConsole is a Qt widget that runs a Bash shell.
|
4
3
|
|
5
|
-
BECConsole is powered by Pyte,
|
4
|
+
BECConsole VT100 emulation is powered by Pyte,
|
6
5
|
(https://github.com/selectel/pyte).
|
7
6
|
"""
|
8
7
|
|
8
|
+
import collections
|
9
9
|
import fcntl
|
10
10
|
import html
|
11
11
|
import os
|
12
12
|
import pty
|
13
|
-
import subprocess
|
14
13
|
import sys
|
15
|
-
import threading
|
16
14
|
|
17
15
|
import pyte
|
16
|
+
from pyte.screens import History
|
18
17
|
from qtpy import QtCore, QtGui, QtWidgets
|
19
|
-
from qtpy.QtCore import
|
18
|
+
from qtpy.QtCore import Property as pyqtProperty
|
19
|
+
from qtpy.QtCore import QSize, QSocketNotifier, Qt, QTimer
|
20
20
|
from qtpy.QtCore import Signal as pyqtSignal
|
21
|
-
from qtpy.QtGui import QClipboard, QTextCursor
|
21
|
+
from qtpy.QtGui import QClipboard, QColor, QPalette, QTextCursor
|
22
22
|
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
|
23
23
|
|
24
24
|
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
@@ -137,13 +137,52 @@ def QtKeyToAscii(event):
|
|
137
137
|
|
138
138
|
|
139
139
|
class Screen(pyte.HistoryScreen):
|
140
|
-
def __init__(self, stdin_fd,
|
141
|
-
super().__init__(
|
140
|
+
def __init__(self, stdin_fd, cols, rows, historyLength):
|
141
|
+
super().__init__(cols, rows, historyLength, ratio=1 / rows)
|
142
142
|
self._fd = stdin_fd
|
143
143
|
|
144
144
|
def write_process_input(self, data):
|
145
|
-
"""Response to CPR request for example
|
146
|
-
|
145
|
+
"""Response to CPR request (for example),
|
146
|
+
this can be for other requests
|
147
|
+
"""
|
148
|
+
try:
|
149
|
+
os.write(self._fd, data.encode("utf-8"))
|
150
|
+
except (IOError, OSError):
|
151
|
+
pass
|
152
|
+
|
153
|
+
def resize(self, lines, columns):
|
154
|
+
lines = lines or self.lines
|
155
|
+
columns = columns or self.columns
|
156
|
+
|
157
|
+
if lines == self.lines and columns == self.columns:
|
158
|
+
return # No changes.
|
159
|
+
|
160
|
+
self.dirty.clear()
|
161
|
+
self.dirty.update(range(lines))
|
162
|
+
|
163
|
+
self.save_cursor()
|
164
|
+
if lines < self.lines:
|
165
|
+
if lines <= self.cursor.y:
|
166
|
+
nlines_to_move_up = self.lines - lines
|
167
|
+
for i in range(nlines_to_move_up):
|
168
|
+
line = self.buffer[i] # .pop(0)
|
169
|
+
self.history.top.append(line)
|
170
|
+
self.cursor_position(0, 0)
|
171
|
+
self.delete_lines(nlines_to_move_up)
|
172
|
+
self.restore_cursor()
|
173
|
+
self.cursor.y -= nlines_to_move_up
|
174
|
+
else:
|
175
|
+
self.restore_cursor()
|
176
|
+
|
177
|
+
self.lines, self.columns = lines, columns
|
178
|
+
self.history = History(
|
179
|
+
self.history.top,
|
180
|
+
self.history.bottom,
|
181
|
+
1 / self.lines,
|
182
|
+
self.history.size,
|
183
|
+
self.history.position,
|
184
|
+
)
|
185
|
+
self.set_margins()
|
147
186
|
|
148
187
|
|
149
188
|
class Backend(QtCore.QObject):
|
@@ -155,17 +194,17 @@ class Backend(QtCore.QObject):
|
|
155
194
|
"""
|
156
195
|
|
157
196
|
# Signals to communicate with ``_TerminalWidget``.
|
158
|
-
startWork = pyqtSignal()
|
159
197
|
dataReady = pyqtSignal(object)
|
198
|
+
processExited = pyqtSignal()
|
160
199
|
|
161
|
-
def __init__(self, fd,
|
200
|
+
def __init__(self, fd, cols, rows):
|
162
201
|
super().__init__()
|
163
202
|
|
164
203
|
# File descriptor that connects to Bash process.
|
165
204
|
self.fd = fd
|
166
205
|
|
167
206
|
# Setup Pyte (hard coded display size for now).
|
168
|
-
self.screen = Screen(self.fd,
|
207
|
+
self.screen = Screen(self.fd, cols, rows, 10000)
|
169
208
|
self.stream = pyte.ByteStream()
|
170
209
|
self.stream.attach(self.screen)
|
171
210
|
|
@@ -174,12 +213,14 @@ class Backend(QtCore.QObject):
|
|
174
213
|
|
175
214
|
def _fd_readable(self):
|
176
215
|
"""
|
177
|
-
Poll the Bash output, run it through Pyte, and notify
|
216
|
+
Poll the Bash output, run it through Pyte, and notify
|
178
217
|
"""
|
179
218
|
# Read the shell output until the file descriptor is closed.
|
180
219
|
try:
|
181
220
|
out = os.read(self.fd, 2**16)
|
182
221
|
except OSError:
|
222
|
+
self.processExited.emit()
|
223
|
+
self.notifier.setEnabled(False)
|
183
224
|
return
|
184
225
|
|
185
226
|
# Feed output into Pyte's state machine and send the new screen
|
@@ -188,93 +229,256 @@ class Backend(QtCore.QObject):
|
|
188
229
|
self.dataReady.emit(self.screen)
|
189
230
|
|
190
231
|
|
191
|
-
class BECConsole(QtWidgets.
|
232
|
+
class BECConsole(QtWidgets.QWidget):
|
192
233
|
"""Container widget for the terminal text area"""
|
193
234
|
|
194
|
-
|
195
|
-
super().__init__(parent)
|
196
|
-
|
197
|
-
self.innerWidget = QtWidgets.QWidget(self)
|
198
|
-
QHBoxLayout(self.innerWidget)
|
199
|
-
self.innerWidget.layout().setContentsMargins(0, 0, 0, 0)
|
235
|
+
ICON_NAME = "terminal"
|
200
236
|
|
201
|
-
|
202
|
-
|
203
|
-
self.innerWidget.layout().addWidget(self.term)
|
204
|
-
|
205
|
-
self.scroll_bar = QScrollBar(Qt.Vertical, self.term)
|
206
|
-
self.innerWidget.layout().addWidget(self.scroll_bar)
|
207
|
-
|
208
|
-
self.term.set_scroll(self.scroll_bar)
|
237
|
+
def __init__(self, parent=None, cols=132):
|
238
|
+
super().__init__(parent)
|
209
239
|
|
210
|
-
self.
|
240
|
+
self.term = _TerminalWidget(self, cols, rows=43)
|
241
|
+
self.scroll_bar = QScrollBar(Qt.Vertical, self)
|
242
|
+
# self.scroll_bar.hide()
|
243
|
+
layout = QHBoxLayout(self)
|
244
|
+
layout.addWidget(self.term)
|
245
|
+
layout.addWidget(self.scroll_bar)
|
246
|
+
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
247
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
248
|
+
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
249
|
+
|
250
|
+
pal = QPalette()
|
251
|
+
self.set_bgcolor(pal.window().color())
|
252
|
+
self.set_fgcolor(pal.windowText().color())
|
253
|
+
self.term.set_scroll_bar(self.scroll_bar)
|
254
|
+
self.set_cmd("bec --nogui")
|
255
|
+
|
256
|
+
self._check_designer_timer = QTimer()
|
257
|
+
self._check_designer_timer.timeout.connect(self.check_designer)
|
258
|
+
self._check_designer_timer.start(1000)
|
211
259
|
|
212
|
-
def
|
260
|
+
def minimumSizeHint(self):
|
261
|
+
size = self.term.sizeHint()
|
262
|
+
size.setWidth(size.width() + self.scroll_bar.width())
|
263
|
+
return size
|
264
|
+
|
265
|
+
def sizeHint(self):
|
266
|
+
return self.minimumSizeHint()
|
267
|
+
|
268
|
+
def check_designer(self, calls={"n": 0}):
|
269
|
+
calls["n"] += 1
|
270
|
+
if self.term.fd is not None:
|
271
|
+
# already started
|
272
|
+
self._check_designer_timer.stop()
|
273
|
+
elif self.window().windowTitle().endswith("[Preview]"):
|
274
|
+
# assuming Designer preview -> start
|
275
|
+
self._check_designer_timer.stop()
|
276
|
+
self.term.start()
|
277
|
+
elif calls["n"] >= 3:
|
278
|
+
# assuming not in Designer -> stop checking
|
279
|
+
self._check_designer_timer.stop()
|
280
|
+
|
281
|
+
def get_rows(self):
|
282
|
+
return self.term.rows
|
283
|
+
|
284
|
+
def set_rows(self, rows):
|
285
|
+
self.term.rows = rows
|
286
|
+
self.adjustSize()
|
287
|
+
self.updateGeometry()
|
288
|
+
|
289
|
+
def get_cols(self):
|
290
|
+
return self.term.cols
|
291
|
+
|
292
|
+
def set_cols(self, cols):
|
293
|
+
self.term.cols = cols
|
294
|
+
self.adjustSize()
|
295
|
+
self.updateGeometry()
|
296
|
+
|
297
|
+
def get_bgcolor(self):
|
298
|
+
return QColor.fromString(self.term.bg_color)
|
299
|
+
|
300
|
+
def set_bgcolor(self, color):
|
301
|
+
self.term.bg_color = color.name(QColor.HexRgb)
|
302
|
+
|
303
|
+
def get_fgcolor(self):
|
304
|
+
return QColor.fromString(self.term.fg_color)
|
305
|
+
|
306
|
+
def set_fgcolor(self, color):
|
307
|
+
self.term.fg_color = color.name(QColor.HexRgb)
|
308
|
+
|
309
|
+
def get_cmd(self):
|
310
|
+
return self.term._cmd
|
311
|
+
|
312
|
+
def set_cmd(self, cmd):
|
213
313
|
self.term._cmd = cmd
|
314
|
+
if self.term.fd is None:
|
315
|
+
# not started yet
|
316
|
+
self.term.clear()
|
317
|
+
self.term.appendHtml(f"<h2>BEC Console - {repr(cmd)}</h2>")
|
318
|
+
|
319
|
+
def start(self, deactivate_ctrl_d=True):
|
214
320
|
self.term.start(deactivate_ctrl_d=deactivate_ctrl_d)
|
215
321
|
|
216
322
|
def push(self, text):
|
217
323
|
"""Push some text to the terminal"""
|
218
324
|
return self.term.push(text)
|
219
325
|
|
326
|
+
cols = pyqtProperty(int, get_cols, set_cols)
|
327
|
+
rows = pyqtProperty(int, get_rows, set_rows)
|
328
|
+
bgcolor = pyqtProperty(QColor, get_bgcolor, set_bgcolor)
|
329
|
+
fgcolor = pyqtProperty(QColor, get_fgcolor, set_fgcolor)
|
330
|
+
cmd = pyqtProperty(str, get_cmd, set_cmd)
|
331
|
+
|
220
332
|
|
221
333
|
class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
222
334
|
"""
|
223
335
|
Start ``Backend`` process and render Pyte output as text.
|
224
336
|
"""
|
225
337
|
|
226
|
-
def __init__(self, parent,
|
227
|
-
super().__init__(parent)
|
228
|
-
|
338
|
+
def __init__(self, parent, cols=125, rows=50, **kwargs):
|
229
339
|
# file descriptor to communicate with the subprocess
|
230
340
|
self.fd = None
|
231
341
|
self.backend = None
|
232
|
-
self.lock = threading.Lock()
|
233
342
|
# command to execute
|
234
|
-
self._cmd =
|
343
|
+
self._cmd = ""
|
235
344
|
# should ctrl-d be deactivated ? (prevent Python exit)
|
236
345
|
self._deactivate_ctrl_d = False
|
237
346
|
|
347
|
+
# Default colors
|
348
|
+
pal = QPalette()
|
349
|
+
self._fg_color = pal.text().color().name()
|
350
|
+
self._bg_color = pal.base().color().name()
|
351
|
+
|
238
352
|
# Specify the terminal size in terms of lines and columns.
|
239
|
-
self.
|
240
|
-
self.
|
241
|
-
self.output =
|
353
|
+
self._rows = rows
|
354
|
+
self._cols = cols
|
355
|
+
self.output = collections.deque()
|
356
|
+
|
357
|
+
super().__init__(parent)
|
358
|
+
|
359
|
+
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding)
|
360
|
+
|
361
|
+
# Disable default scrollbars (we use our own, to be set via .set_scroll_bar())
|
362
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
363
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
364
|
+
self.scroll_bar = None
|
242
365
|
|
243
366
|
# Use Monospace fonts and disable line wrapping.
|
244
367
|
self.setFont(QtGui.QFont("Courier", 9))
|
245
368
|
self.setFont(QtGui.QFont("Monospace"))
|
246
369
|
self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
|
247
|
-
|
248
|
-
# Disable vertical scrollbar (we use our own, to be set via .set_scroll())
|
249
|
-
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
250
|
-
|
251
370
|
fmt = QtGui.QFontMetrics(self.font())
|
252
|
-
|
253
|
-
self.
|
254
|
-
|
255
|
-
|
371
|
+
char_width = fmt.width("w")
|
372
|
+
self.setCursorWidth(char_width)
|
373
|
+
|
374
|
+
self.adjustSize()
|
375
|
+
self.updateGeometry()
|
376
|
+
self.update_stylesheet()
|
377
|
+
|
378
|
+
@property
|
379
|
+
def bg_color(self):
|
380
|
+
return self._bg_color
|
381
|
+
|
382
|
+
@bg_color.setter
|
383
|
+
def bg_color(self, hexcolor):
|
384
|
+
self._bg_color = hexcolor
|
385
|
+
self.update_stylesheet()
|
386
|
+
|
387
|
+
@property
|
388
|
+
def fg_color(self):
|
389
|
+
return self._fg_color
|
390
|
+
|
391
|
+
@fg_color.setter
|
392
|
+
def fg_color(self, hexcolor):
|
393
|
+
self._fg_color = hexcolor
|
394
|
+
self.update_stylesheet()
|
395
|
+
|
396
|
+
def update_stylesheet(self):
|
397
|
+
self.setStyleSheet(
|
398
|
+
f"QPlainTextEdit {{ border: 0; color: {self._fg_color}; background-color: {self._bg_color}; }} "
|
399
|
+
)
|
400
|
+
|
401
|
+
@property
|
402
|
+
def rows(self):
|
403
|
+
return self._rows
|
404
|
+
|
405
|
+
@rows.setter
|
406
|
+
def rows(self, rows: int):
|
407
|
+
if self.backend is None:
|
408
|
+
# not initialized yet, ok to change
|
409
|
+
self._rows = rows
|
410
|
+
self.adjustSize()
|
411
|
+
self.updateGeometry()
|
412
|
+
else:
|
413
|
+
raise RuntimeError("Cannot change rows after console is started.")
|
414
|
+
|
415
|
+
@property
|
416
|
+
def cols(self):
|
417
|
+
return self._cols
|
418
|
+
|
419
|
+
@cols.setter
|
420
|
+
def cols(self, cols: int):
|
421
|
+
if self.fd is None:
|
422
|
+
# not initialized yet, ok to change
|
423
|
+
self._cols = cols
|
424
|
+
self.adjustSize()
|
425
|
+
self.updateGeometry()
|
426
|
+
else:
|
427
|
+
raise RuntimeError("Cannot change cols after console is started.")
|
256
428
|
|
257
|
-
def start(self, deactivate_ctrl_d=False):
|
429
|
+
def start(self, deactivate_ctrl_d: bool = False):
|
258
430
|
self._deactivate_ctrl_d = deactivate_ctrl_d
|
259
431
|
|
432
|
+
self.update_term_size()
|
433
|
+
|
260
434
|
# Start the Bash process
|
261
|
-
self.fd = self.
|
435
|
+
self.fd = self.fork_shell()
|
262
436
|
|
263
|
-
|
264
|
-
|
265
|
-
|
437
|
+
if self.fd:
|
438
|
+
# Create the ``Backend`` object
|
439
|
+
self.backend = Backend(self.fd, self.cols, self.rows)
|
440
|
+
self.backend.dataReady.connect(self.data_ready)
|
441
|
+
self.backend.processExited.connect(self.process_exited)
|
442
|
+
else:
|
443
|
+
self.process_exited()
|
266
444
|
|
267
|
-
def
|
268
|
-
|
269
|
-
|
270
|
-
|
445
|
+
def process_exited(self):
|
446
|
+
self.fd = None
|
447
|
+
self.clear()
|
448
|
+
self.appendHtml(f"<br><h2>{repr(self._cmd)} - Process exited.</h2>")
|
449
|
+
self.setReadOnly(True)
|
271
450
|
|
272
|
-
def
|
273
|
-
|
274
|
-
self.scroll.setMinimum(0)
|
275
|
-
self.scroll.valueChanged.connect(self.scroll_value_change)
|
451
|
+
def data_ready(self, screen):
|
452
|
+
"""Handle new screen: redraw, set scroll bar max and slider, move cursor to its position
|
276
453
|
|
277
|
-
|
454
|
+
This method is triggered via a signal from ``Backend``.
|
455
|
+
"""
|
456
|
+
self.redraw_screen()
|
457
|
+
self.adjust_scroll_bar()
|
458
|
+
self.move_cursor()
|
459
|
+
|
460
|
+
def minimumSizeHint(self):
|
461
|
+
"""Return minimum size for current cols and rows"""
|
462
|
+
fmt = QtGui.QFontMetrics(self.font())
|
463
|
+
char_width = fmt.width("w")
|
464
|
+
char_height = fmt.height()
|
465
|
+
width = char_width * self.cols
|
466
|
+
height = char_height * self.rows
|
467
|
+
return QSize(width, height)
|
468
|
+
|
469
|
+
def sizeHint(self):
|
470
|
+
return self.minimumSizeHint()
|
471
|
+
|
472
|
+
def set_scroll_bar(self, scroll_bar):
|
473
|
+
self.scroll_bar = scroll_bar
|
474
|
+
self.scroll_bar.setMinimum(0)
|
475
|
+
self.scroll_bar.valueChanged.connect(self.scroll_value_change)
|
476
|
+
|
477
|
+
def scroll_value_change(self, value, old={"value": -1}):
|
478
|
+
if self.backend is None:
|
479
|
+
return
|
480
|
+
if old["value"] == -1:
|
481
|
+
old["value"] = self.scroll_bar.maximum()
|
278
482
|
if value <= old["value"]:
|
279
483
|
# scroll up
|
280
484
|
# value is number of lines from the start
|
@@ -288,13 +492,35 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
|
288
492
|
for i in range(nlines):
|
289
493
|
self.backend.screen.next_page()
|
290
494
|
old["value"] = value
|
291
|
-
self.
|
495
|
+
self.redraw_screen()
|
496
|
+
|
497
|
+
def adjust_scroll_bar(self):
|
498
|
+
sb = self.scroll_bar
|
499
|
+
sb.valueChanged.disconnect(self.scroll_value_change)
|
500
|
+
tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
|
501
|
+
sb.setMaximum(tmp if tmp > 0 else 0)
|
502
|
+
sb.setSliderPosition(tmp if tmp > 0 else 0)
|
503
|
+
# if tmp > 0:
|
504
|
+
# # show scrollbar, but delayed - prevent recursion with widget size change
|
505
|
+
# QTimer.singleShot(0, scrollbar.show)
|
506
|
+
# else:
|
507
|
+
# QTimer.singleShot(0, scrollbar.hide)
|
508
|
+
sb.valueChanged.connect(self.scroll_value_change)
|
509
|
+
|
510
|
+
def write(self, data):
|
511
|
+
try:
|
512
|
+
os.write(self.fd, data)
|
513
|
+
except (IOError, OSError):
|
514
|
+
self.process_exited()
|
292
515
|
|
293
516
|
@Slot(object)
|
294
517
|
def keyPressEvent(self, event):
|
295
518
|
"""
|
296
519
|
Redirect all keystrokes to the terminal process.
|
297
520
|
"""
|
521
|
+
if self.fd is None:
|
522
|
+
# not started
|
523
|
+
return
|
298
524
|
# Convert the Qt key to the correct ASCII code.
|
299
525
|
if (
|
300
526
|
self._deactivate_ctrl_d
|
@@ -311,15 +537,17 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
|
311
537
|
# MacOS only: CMD-V handling
|
312
538
|
self._push_clipboard()
|
313
539
|
elif code is not None:
|
314
|
-
|
540
|
+
self.write(code)
|
315
541
|
|
316
542
|
def push(self, text):
|
317
543
|
"""
|
318
544
|
Write 'text' to terminal
|
319
545
|
"""
|
320
|
-
|
546
|
+
self.write(text.encode("utf-8"))
|
321
547
|
|
322
548
|
def contextMenuEvent(self, event):
|
549
|
+
if self.fd is None:
|
550
|
+
return
|
323
551
|
menu = self.createStandardContextMenu()
|
324
552
|
for action in menu.actions():
|
325
553
|
# remove all actions except copy and paste
|
@@ -341,7 +569,20 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
|
341
569
|
clipboard = QApplication.instance().clipboard()
|
342
570
|
self.push(clipboard.text())
|
343
571
|
|
572
|
+
def move_cursor(self):
|
573
|
+
textCursor = self.textCursor()
|
574
|
+
textCursor.setPosition(0)
|
575
|
+
textCursor.movePosition(
|
576
|
+
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
577
|
+
)
|
578
|
+
textCursor.movePosition(
|
579
|
+
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
580
|
+
)
|
581
|
+
self.setTextCursor(textCursor)
|
582
|
+
|
344
583
|
def mouseReleaseEvent(self, event):
|
584
|
+
if self.fd is None:
|
585
|
+
return
|
345
586
|
if event.button() == Qt.MiddleButton:
|
346
587
|
# push primary selection buffer ("mouse clipboard") to terminal
|
347
588
|
clipboard = QApplication.instance().clipboard()
|
@@ -355,34 +596,34 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
|
355
596
|
# mouse was used to select text -> nothing to do
|
356
597
|
pass
|
357
598
|
else:
|
358
|
-
# a simple 'click',
|
359
|
-
|
360
|
-
|
361
|
-
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
362
|
-
)
|
363
|
-
textCursor.movePosition(
|
364
|
-
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
365
|
-
)
|
366
|
-
self.setTextCursor(textCursor)
|
367
|
-
self.ensureCursorVisible()
|
599
|
+
# a simple 'click', move scrollbar to end
|
600
|
+
self.scroll_bar.setSliderPosition(self.scroll_bar.maximum())
|
601
|
+
self.move_cursor()
|
368
602
|
return None
|
369
603
|
return super().mouseReleaseEvent(event)
|
370
604
|
|
371
|
-
def
|
605
|
+
def redraw_screen(self):
|
372
606
|
"""
|
373
|
-
Render the
|
374
|
-
|
375
|
-
This method is triggered via a signal from ``Backend``.
|
607
|
+
Render the screen as formatted text into the widget.
|
376
608
|
"""
|
377
|
-
|
378
|
-
|
609
|
+
screen = self.backend.screen
|
610
|
+
|
611
|
+
# Clear the widget
|
612
|
+
if screen.dirty:
|
379
613
|
self.clear()
|
614
|
+
while len(self.output) < (max(screen.dirty) + 1):
|
615
|
+
self.output.append("")
|
616
|
+
while len(self.output) > (max(screen.dirty) + 1):
|
617
|
+
self.output.pop()
|
380
618
|
|
381
619
|
# Prepare the HTML output
|
382
|
-
for line_no in
|
620
|
+
for line_no in screen.dirty:
|
383
621
|
line = text = ""
|
384
622
|
style = old_style = ""
|
385
|
-
|
623
|
+
old_idx = 0
|
624
|
+
for idx, ch in screen.buffer[line_no].items():
|
625
|
+
text += " " * (idx - old_idx - 1)
|
626
|
+
old_idx = idx
|
386
627
|
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 ''}"
|
387
628
|
if style != old_style:
|
388
629
|
if old_style:
|
@@ -396,45 +637,47 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
|
396
637
|
line += f"<span style={repr(style)}>{html.escape(text, quote=True)}</span>"
|
397
638
|
else:
|
398
639
|
line += html.escape(text, quote=True)
|
640
|
+
# do a check at the cursor position:
|
641
|
+
# it is possible x pos > output line length,
|
642
|
+
# for example if last escape codes are "cursor forward" past end of text,
|
643
|
+
# like IPython does for "..." prompt (in a block, like "for" loop or "while" for example)
|
644
|
+
# In this case, cursor is at 12 but last text output is at 8 -> insert spaces
|
645
|
+
if line_no == screen.cursor.y:
|
646
|
+
llen = len(screen.buffer[line_no])
|
647
|
+
if llen < screen.cursor.x:
|
648
|
+
line += " " * (screen.cursor.x - llen)
|
399
649
|
self.output[line_no] = line
|
400
650
|
# fill the text area with HTML contents in one go
|
401
651
|
self.appendHtml(f"<pre>{chr(10).join(self.output)}</pre>")
|
402
|
-
#
|
403
|
-
|
652
|
+
# did updates, all clean
|
653
|
+
screen.dirty.clear()
|
404
654
|
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
self.scroll.valueChanged.connect(self.scroll_value_change)
|
420
|
-
|
421
|
-
# def resizeEvent(self, event):
|
422
|
-
# with self.lock:
|
423
|
-
# self.numColumns = int(self.width() / self._char_width)
|
424
|
-
# self.numLines = int(self.height() / self._char_height)
|
425
|
-
# self.output = [""] * self.numLines
|
426
|
-
# print("RESIZING TO", self.numColumns, "x", self.numLines)
|
427
|
-
# self.backend.screen.resize(self.numLines, self.numColumns)
|
655
|
+
def update_term_size(self):
|
656
|
+
fmt = QtGui.QFontMetrics(self.font())
|
657
|
+
char_width = fmt.width("w")
|
658
|
+
char_height = fmt.height()
|
659
|
+
self._cols = int(self.width() / char_width)
|
660
|
+
self._rows = int(self.height() / char_height)
|
661
|
+
|
662
|
+
def resizeEvent(self, event):
|
663
|
+
self.update_term_size()
|
664
|
+
if self.fd:
|
665
|
+
self.backend.screen.resize(self._rows, self._cols)
|
666
|
+
self.redraw_screen()
|
667
|
+
self.adjust_scroll_bar()
|
668
|
+
self.move_cursor()
|
428
669
|
|
429
670
|
def wheelEvent(self, event):
|
671
|
+
if not self.fd:
|
672
|
+
return
|
430
673
|
y = event.angleDelta().y()
|
431
674
|
if y > 0:
|
432
675
|
self.backend.screen.prev_page()
|
433
676
|
else:
|
434
677
|
self.backend.screen.next_page()
|
435
|
-
self.
|
678
|
+
self.redraw_screen()
|
436
679
|
|
437
|
-
def
|
680
|
+
def fork_shell(self):
|
438
681
|
"""
|
439
682
|
Fork the current process and execute bec in shell.
|
440
683
|
"""
|
@@ -443,32 +686,30 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
|
443
686
|
except (IOError, OSError):
|
444
687
|
return False
|
445
688
|
if pid == 0:
|
446
|
-
# Safe way to make it work under BSD and Linux
|
447
689
|
try:
|
448
690
|
ls = os.environ["LANG"].split(".")
|
449
691
|
except KeyError:
|
450
692
|
ls = []
|
451
693
|
if len(ls) < 2:
|
452
694
|
ls = ["en_US", "UTF-8"]
|
695
|
+
os.putenv("COLUMNS", str(self.cols))
|
696
|
+
os.putenv("LINES", str(self.rows))
|
697
|
+
os.putenv("TERM", "linux")
|
698
|
+
os.putenv("LANG", ls[0] + ".UTF-8")
|
699
|
+
if not self._cmd:
|
700
|
+
self._cmd = os.environ["SHELL"]
|
701
|
+
cmd = self._cmd
|
702
|
+
if isinstance(cmd, str):
|
703
|
+
cmd = cmd.split()
|
453
704
|
try:
|
454
|
-
os.
|
455
|
-
os.putenv("LINES", str(self.numLines))
|
456
|
-
os.putenv("TERM", "linux")
|
457
|
-
os.putenv("LANG", ls[0] + ".UTF-8")
|
458
|
-
if isinstance(self._cmd, str):
|
459
|
-
os.execvp(self._cmd, self._cmd)
|
460
|
-
else:
|
461
|
-
os.execvp(self._cmd[0], self._cmd)
|
462
|
-
# print "child_pid", child_pid, sts
|
705
|
+
os.execvp(cmd[0], cmd)
|
463
706
|
except (IOError, OSError):
|
464
707
|
pass
|
465
|
-
# self.proc_finish(sid)
|
466
708
|
os._exit(0)
|
467
709
|
else:
|
468
710
|
# We are in the parent process.
|
469
711
|
# Set file control
|
470
712
|
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
471
|
-
print("Spawned Bash shell (PID {})".format(pid))
|
472
713
|
return fd
|
473
714
|
|
474
715
|
|
@@ -478,17 +719,13 @@ if __name__ == "__main__":
|
|
478
719
|
|
479
720
|
from qtpy import QtGui, QtWidgets
|
480
721
|
|
481
|
-
#
|
482
|
-
numLines = 25
|
483
|
-
numColumns = 100
|
484
|
-
|
485
|
-
# Create the Qt application and QBash instance.
|
722
|
+
# Create the Qt application and console.
|
486
723
|
app = QtWidgets.QApplication([])
|
487
724
|
mainwin = QtWidgets.QMainWindow()
|
488
|
-
title = "BECConsole
|
725
|
+
title = "BECConsole"
|
489
726
|
mainwin.setWindowTitle(title)
|
490
727
|
|
491
|
-
console = BECConsole(mainwin
|
728
|
+
console = BECConsole(mainwin)
|
492
729
|
mainwin.setCentralWidget(console)
|
493
730
|
console.start()
|
494
731
|
|