Source code for pyqterm.frontend
# -*- coding: utf-8 -*-
import sys
import time
from PyQt5.QtCore import QRect, Qt, pyqtSignal
from PyQt5 import QtWidgets, QtGui
from .backend import Session
DEBUG = False
[docs]class TerminalWidget(QtWidgets.QWidget):
foreground_color_map = {
0: "#000",
1: "#b00",
2: "#0b0",
3: "#bb0",
4: "#5454FF", #### path color
5: "#b0b",
6: "#0bb",
7: "#bbb",
8: "#666",
9: "#f00",
10: "#0f0",
11: "#ff0",
12: "#00f", # concelaed
13: "#f0f",
14: "#000", # negative
15: "#fff", # default
}
background_color_map = {
0: "#000",
1: "#b00",
2: "#0b0",
3: "#bb0",
4: "#00b",
5: "#b0b",
6: "#0bb",
7: "#bbb",
12: "#aaa", # cursor
14: "#000", # default
15: "#fff", # negative
}
keymap = {
Qt.Key_Backspace: chr(127),
Qt.Key_Escape: chr(27),
Qt.Key_AsciiTilde: "~~",
Qt.Key_Up: "~A",
Qt.Key_Down: "~B",
Qt.Key_Left: "~D",
Qt.Key_Right: "~C",
Qt.Key_PageUp: "~1",
Qt.Key_PageDown: "~2",
Qt.Key_Home: "~H",
Qt.Key_End: "~F",
Qt.Key_Insert: "~3",
Qt.Key_Delete: "~4",
Qt.Key_F1: "~a",
Qt.Key_F2: "~b",
Qt.Key_F3: "~c",
Qt.Key_F4: "~d",
Qt.Key_F5: "~e",
Qt.Key_F6: "~f",
Qt.Key_F7: "~g",
Qt.Key_F8: "~h",
Qt.Key_F9: "~i",
Qt.Key_F10: "~j",
Qt.Key_F11: "~k",
Qt.Key_F12: "~l",
}
session_closed = pyqtSignal()
def __init__(self, parent=None, command="/bin/bash",
font_name="monospace", font_size=12):
super(TerminalWidget, self).__init__(parent)
self.parent().setTabOrder(self, self)
self.setFocusPolicy(Qt.WheelFocus)
self.setAutoFillBackground(False)
self.setAttribute(Qt.WA_OpaquePaintEvent, True)
self.setCursor(Qt.IBeamCursor)
font = QtGui.QFont(font_name)
font.setPixelSize(font_size)
self.setFont(font)
self._session = None
self._last_update = None
self._screen = []
self._text = []
self._cursor_rect = None
self._cursor_col = 0
self._cursor_row = 0
self._dirty = False
self._blink = False
self._press_pos = None
self._selection = None
self._clipboard = QtWidgets.QApplication.clipboard()
QtWidgets.QApplication.instance().lastWindowClosed.connect(Session.close_all)
if command:
self.execute()
[docs] def execute(self, command="/bin/bash"):
self._session = Session()
self._session.start(command)
self._timer_id = None
# start timer either with high or low priority
if self.hasFocus():
self.focusInEvent(None)
else:
self.focusOutEvent(None)
[docs] def focusNextPrevChild(self, next):
if not self._session.is_alive():
return True
return False
[docs] def focusInEvent(self, event):
if not self._session.is_alive():
return
if self._timer_id is not None:
self.killTimer(self._timer_id)
self._timer_id = self.startTimer(250)
self.update_screen()
[docs] def focusOutEvent(self, event):
if not self._session.is_alive():
return
# reduced update interval
# -> slower screen updates
# -> but less load on main app which results in better responsiveness
if self._timer_id is not None:
self.killTimer(self._timer_id)
self._timer_id = self.startTimer(750)
[docs] def resizeEvent(self, event):
if not self._session.is_alive():
return
self._columns, self._rows = self._pixel2pos(
self.width(), self.height())
self._session.resize(self._columns, self._rows)
[docs] def timerEvent(self, event):
if not self._session.is_alive():
if self._timer_id is not None:
self.killTimer(self._timer_id)
self._timer_id = None
if DEBUG:
print("Session closed")
self.session_closed.emit()
return
last_change = self._session.last_change()
if not last_change:
return
if not self._last_update or last_change > self._last_update:
self._last_update = last_change
old_screen = self._screen
(self._cursor_col, self._cursor_row), self._screen = self._session.dump()
self._update_cursor_rect()
if old_screen != self._screen:
self._dirty = True
if self.hasFocus():
self._blink = not self._blink
self.update()
[docs] def _update_metrics(self):
fm = self.fontMetrics()
self._char_height = fm.height()
self._char_width = fm.width("W")
[docs] def _update_cursor_rect(self):
cx, cy = self._pos2pixel(self._cursor_col, self._cursor_row)
self._cursor_rect = QRect(cx, cy, self._char_width, self._char_height)
[docs] def _reset(self):
self._update_metrics()
self._update_cursor_rect()
self.resizeEvent(None)
self.update_screen()
[docs] def paintEvent(self, event):
painter = QtGui.QPainter(self)
if self._dirty:
self._dirty = False
self._paint_screen(painter)
else:
if self._cursor_rect and not self._selection:
self._paint_cursor(painter)
if self._selection:
self._paint_selection(painter)
self._dirty = True
[docs] def _pixel2pos(self, x, y):
col = int(round(x / self._char_width))
row = int(round(y / self._char_height))
return col, row
[docs] def _pos2pixel(self, col, row):
x = col * self._char_width
y = row * self._char_height
return x, y
[docs] def _paint_cursor(self, painter):
if self._blink:
color = "#aaa"
else:
color = "#fff"
painter.setPen(QtGui.QPen(QtGui.QColor(color)))
painter.drawRect(self._cursor_rect)
[docs] def _paint_screen(self, painter):
# Speed hacks: local name lookups are faster
vars().update(QColor=QtGui.QColor, QBrush=QtGui.QBrush, QPen=QtGui.QPen, QRect=QRect)
background_color_map = self.background_color_map
foreground_color_map = self.foreground_color_map
char_width = self._char_width
char_height = self._char_height
painter_drawText = painter.drawText
painter_fillRect = painter.fillRect
painter_setPen = painter.setPen
align = Qt.AlignTop | Qt.AlignLeft
# set defaults
background_color = background_color_map[14]
foreground_color = foreground_color_map[15]
brush = QtGui.QBrush(QtGui.QColor(background_color))
painter_fillRect(self.rect(), brush)
pen = QtGui.QPen(QtGui.QColor(foreground_color))
painter_setPen(pen)
y = 0
text = []
text_append = text.append
for row, line in enumerate(self._screen):
col = 0
text_line = ""
for item in line:
if isinstance(item, str):
x = col * char_width
length = len(item)
rect = QRect(
x, y, x + char_width * length, y + char_height)
painter_fillRect(rect, brush)
painter_drawText(rect, align, item)
col += length
text_line += item
else:
foreground_color_idx, background_color_idx, underline_flag = item
foreground_color = foreground_color_map[
foreground_color_idx]
background_color = background_color_map[
background_color_idx]
pen = QtGui.QPen(QtGui.QColor(foreground_color))
brush = QtGui.QBrush(QtGui.QColor(background_color))
painter_setPen(pen)
# painter.setBrush(brush)
y += char_height
text_append(text_line)
self._text = text
[docs] def _paint_selection(self, painter):
pcol = QtGui.QColor(200, 200, 200, 50)
pen = QtGui.QPen(pcol)
bcol = QtGui.QColor(230, 230, 230, 50)
brush = QtGui.QBrush(bcol)
painter.setPen(pen)
painter.setBrush(brush)
for (start_col, start_row, end_col, end_row) in self._selection:
x, y = self._pos2pixel(start_col, start_row)
width, height = self._pos2pixel(
end_col - start_col, end_row - start_row)
rect = QRect(x, y, width, height)
# painter.drawRect(rect)
painter.fillRect(rect, brush)
[docs] def zoom_in(self):
font = self.font()
font.setPixelSize(font.pixelSize() + 2)
self.setFont(font)
self._reset()
[docs] def zoom_out(self):
font = self.font()
font.setPixelSize(font.pixelSize() - 2)
self.setFont(font)
self._reset()
return_pressed = pyqtSignal()
[docs] def keyPressEvent(self, event):
text = str(event.text())
key = event.key()
modifiers = event.modifiers()
ctrl = modifiers == Qt.ControlModifier
if ctrl and key == Qt.Key_Plus:
self.zoom_in()
elif ctrl and key == Qt.Key_Minus:
self.zoom_out()
else:
if text and key != Qt.Key_Backspace:
self.send(text.encode("utf-8"))
else:
s = self.keymap.get(key)
if s:
self.send(s.encode("utf-8"))
elif DEBUG:
print("Unkonwn key combination")
print("Modifiers:", modifiers)
print("Key:", key)
for name in dir(Qt):
if not name.startswith("Key_"):
continue
value = getattr(Qt, name)
if value == key:
print("Symbol: Qt.%s" % name)
print("Text: %r" % text)
event.accept()
if key in (Qt.Key_Enter, Qt.Key_Return):
self.return_pressed.emit()
[docs] def mousePressEvent(self, event):
button = event.button()
if button == Qt.RightButton:
ctx_event = QtGui.QContextMenuEvent(QtGui.QContextMenuEvent.Mouse, event.pos())
self.contextMenuEvent(ctx_event)
self._press_pos = None
elif button == Qt.LeftButton:
self._press_pos = event.pos()
self._selection = None
self.update_screen()
elif button == Qt.MiddleButton:
self._press_pos = None
self._selection = None
text = str(self._clipboard.text(QtGui.QClipboard.Selection))
self.send(text.encode("utf-8"))
# self.update_screen()
[docs] def _selection_rects(self, start_pos, end_pos):
sx, sy = start_pos.x(), start_pos.y()
start_col, start_row = self._pixel2pos(sx, sy)
ex, ey = end_pos.x(), end_pos.y()
end_col, end_row = self._pixel2pos(ex, ey)
if start_row == end_row:
if ey > sy or end_row == 0:
end_row += 1
else:
end_row -= 1
if start_col == end_col:
if ex > sx or end_col == 0:
end_col += 1
else:
end_col -= 1
if start_row > end_row:
start_row, end_row = end_row, start_row
if start_col > end_col:
start_col, end_col = end_col, start_col
if end_row - start_row == 1:
return [(start_col, start_row, end_col, end_row)]
else:
return [
(start_col, start_row, self._columns, start_row + 1),
(0, start_row + 1, self._columns, end_row - 1),
(0, end_row - 1, end_col, end_row)
]
[docs] def text(self, rect=None):
if rect is None:
return "\n".join(self._text)
else:
text = []
(start_col, start_row, end_col, end_row) = rect
for row in range(start_row, end_row):
text.append(self._text[row][start_col:end_col])
return text
[docs] def text_selection(self):
text = []
for (start_col, start_row, end_col, end_row) in self._selection:
for row in range(start_row, end_row):
text.append(self._text[row][start_col:end_col])
return "\n".join(text)
[docs] def mouseMoveEvent(self, event):
if self._press_pos:
move_pos = event.pos()
self._selection = self._selection_rects(self._press_pos, move_pos)
sel = self.text_selection()
if DEBUG:
print("%r copied to xselection" % sel)
self._clipboard.setText(sel, QtGui.QClipboard.Selection)
self.update_screen()
[docs] def mouseDoubleClickEvent(self, event):
self._press_pos = None
# double clicks create a selection for the word under the cursor
pos = event.pos()
x, y = pos.x(), pos.y()
col, row = self._pixel2pos(x, y)
line = self._text[row]
# find start of word
start_col = col
found_left = 0
while start_col > 0:
char = line[start_col]
if not char.isalnum() and char not in ("_",):
found_left = 1
break
start_col -= 1
# find end of word
end_col = col
found_right = 0
while end_col < self._columns:
char = line[end_col]
if not char.isalnum() and char not in ("_",):
found_right = 1
break
end_col += 1
self._selection = [
(start_col + found_left, row, end_col - found_right + 1, row + 1)]
sel = self.text_selection()
if DEBUG:
print("%r copied to xselection" % sel)
self._clipboard.setText(sel, QtGui.QClipboard.Selection)
self.update_screen()