colourterm/colourterm/mainwindow.py

589 lines
27 KiB
Python

# -*- coding: utf-8 -*-
import os
import threading
from string import printable
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebKit
from serial import SerialException, serial_for_url
from select import error as SelectError
from socket import error as SocketError
import time
from os.path import getsize
from colourterm import SettingsDialog, ConnectDialog, SComboBox, Highlight, from_utf8, translate, \
create_default_highlights
from colourterm.cwebview import CWebView
from colourterm.xmodem import XMODEM
class MessageType(object):
"""
An enumeration for message types
"""
Info = 1
Question = 2
Warning = 3
Critical = 4
class UiMainWindow(object):
def __init__(self):
"""
Just to satisfy PEP8/PyLint
"""
self.central_widget = None
self.central_layout = None
self.output_browser = None
self.send_layout = None
self.send_combobox = None
self.send_button = None
self.status_bar = None
self.tool_bar = None
self.open_action = None
self.close_action = None
self.capture_action = None
self.xmodem_action = None
self.follow_action = None
self.configure_action = None
self.exit_action = None
self.clear_action = None
self.find_widget = None
self.find_layout = None
self.find_combobox = None
self.find_action = None
def setup_ui(self, main_window):
"""
Set up the user interface
"""
main_window.setObjectName(from_utf8('MainWindow'))
main_window.resize(800, 600)
main_window.setWindowIcon(QtGui.QIcon(':/icons/colourterm-icon.ico'))
self.central_widget = QtWidgets.QWidget(main_window)
self.central_widget.setObjectName(from_utf8('central_widget'))
self.central_layout = QtWidgets.QVBoxLayout(self.central_widget)
self.central_layout.setSpacing(0)
self.central_layout.setContentsMargins(0, 0, 0, 0)
self.central_layout.setObjectName(from_utf8('central_layout'))
self.find_widget = QtWidgets.QWidget(main_window)
self.find_widget.setVisible(False)
self.find_widget.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
self.find_widget.setObjectName(from_utf8('find_widget'))
self.find_layout = QtWidgets.QHBoxLayout(self.find_widget)
self.find_layout.setContentsMargins(0, 2, 0, 2)
self.find_layout.setObjectName(from_utf8('find_layout'))
self.find_combobox = SComboBox(self.find_widget)
self.find_combobox.setEditable(True)
self.find_combobox.setEnabled(True)
self.find_combobox.setObjectName(from_utf8('find_combobox'))
self.find_layout.addWidget(self.find_combobox)
self.central_layout.addWidget(self.find_widget)
self.output_browser = CWebView(self.central_widget)
self.output_browser.setHtml('<html><head><style>body { color: %s; font-family: monospace; margin: 0; '
'padding: 0; }</style></head><body><pre></pre></body></html>' %
str(QtWidgets.QApplication.palette().color(QtGui.QPalette.Text).name()))
self.output_browser.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.output_browser.setObjectName(from_utf8('output_browser'))
self.central_layout.addWidget(self.output_browser)
self.send_layout = QtWidgets.QHBoxLayout()
self.send_layout.setSpacing(8)
self.send_layout.setContentsMargins(0, 4, 0, 0)
self.send_layout.setObjectName(from_utf8('sendLayout'))
self.send_combobox = SComboBox(self.central_widget)
self.send_combobox.setEditable(True)
self.send_combobox.setEnabled(False)
self.send_combobox.setObjectName(from_utf8('sendComboBox'))
self.send_layout.addWidget(self.send_combobox)
self.send_button = QtWidgets.QPushButton(self.central_widget)
size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
size_policy.setHorizontalStretch(0)
size_policy.setVerticalStretch(0)
size_policy.setHeightForWidth(self.send_button.sizePolicy().hasHeightForWidth())
self.send_button.setSizePolicy(size_policy)
self.send_button.setMaximumSize(QtCore.QSize(100, 16777215))
self.send_button.setEnabled(False)
self.send_button.setObjectName(from_utf8('sendButton'))
self.send_layout.addWidget(self.send_button)
self.central_layout.addLayout(self.send_layout)
main_window.setCentralWidget(self.central_widget)
self.status_bar = QtWidgets.QStatusBar(main_window)
self.status_bar.setObjectName(from_utf8('status_bar'))
main_window.setStatusBar(self.status_bar)
self.tool_bar = QtWidgets.QToolBar(main_window)
self.tool_bar.setMovable(False)
self.tool_bar.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
self.tool_bar.setFloatable(False)
self.tool_bar.setObjectName(from_utf8('tool_bar'))
main_window.addToolBar(QtCore.Qt.TopToolBarArea, self.tool_bar)
self.open_action = QtWidgets.QAction(main_window)
connect_icon = QtGui.QIcon()
connect_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/network-connect.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.open_action.setIcon(connect_icon)
self.open_action.setObjectName(from_utf8('open_action'))
self.open_action.setShortcut(QtGui.QKeySequence.Open)
self.close_action = QtWidgets.QAction(main_window)
disconnect_icon = QtGui.QIcon()
disconnect_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/network-disconnect.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.close_action.setIcon(disconnect_icon)
self.close_action.setShortcut(QtGui.QKeySequence.Close)
self.close_action.setObjectName(from_utf8('close_action'))
self.find_action = QtWidgets.QAction(main_window)
find_icon = QtGui.QIcon()
find_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/find.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.find_action.setIcon(find_icon)
self.find_action.setShortcut(QtGui.QKeySequence.Find)
self.find_action.setCheckable(True)
self.find_action.setChecked(False)
self.find_action.setObjectName(from_utf8('find_action'))
self.capture_action = QtWidgets.QAction(main_window)
capture_icon = QtGui.QIcon()
capture_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/capture-to-disk.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.capture_action.setIcon(capture_icon)
self.capture_action.setCheckable(True)
self.capture_action.setChecked(False)
self.capture_action.setObjectName(from_utf8('capture_action'))
self.follow_action = QtWidgets.QAction(main_window)
self.follow_action.setShortcut(QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.SHIFT + QtCore.Qt.Key_F))
follow_icon = QtGui.QIcon()
follow_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/follow-output.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.follow_action.setIcon(follow_icon)
self.follow_action.setCheckable(True)
self.follow_action.setChecked(True)
self.follow_action.setObjectName(from_utf8('follow_action'))
self.clear_action = QtWidgets.QAction(main_window)
clear_icon = QtGui.QIcon()
clear_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/clear.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.clear_action.setIcon(clear_icon)
self.clear_action.setObjectName(from_utf8('clear_action'))
self.clear_action.setShortcut(QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Backspace))
self.xmodem_action = QtWidgets.QAction(main_window)
xmodem_icon = QtGui.QIcon()
xmodem_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/move-up.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.xmodem_action.setIcon(xmodem_icon)
self.xmodem_action.setObjectName(from_utf8('xmodem_action'))
self.xmodem_action.setShortcut(QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.SHIFT + QtCore.Qt.Key_X))
self.configure_action = QtWidgets.QAction(main_window)
configure_icon = QtGui.QIcon()
configure_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/configure.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.configure_action.setIcon(configure_icon)
self.configure_action.setObjectName(from_utf8('configure_action'))
self.exit_action = QtWidgets.QAction(main_window)
exit_icon = QtGui.QIcon()
exit_icon.addPixmap(QtGui.QPixmap(from_utf8(':/toolbar/application-exit.png')),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.exit_action.setIcon(exit_icon)
self.exit_action.setObjectName(from_utf8('exit_action'))
self.tool_bar.addAction(self.open_action)
self.tool_bar.addAction(self.close_action)
self.tool_bar.addSeparator()
self.tool_bar.addAction(self.find_action)
self.tool_bar.addAction(self.capture_action)
self.tool_bar.addAction(self.follow_action)
self.tool_bar.addAction(self.clear_action)
self.tool_bar.addAction(self.xmodem_action)
self.tool_bar.addSeparator()
self.tool_bar.addAction(self.configure_action)
self.tool_bar.addAction(self.exit_action)
self.retranslate_ui(main_window)
def retranslate_ui(self, main_window):
"""
Translate the user interface
"""
main_window.setWindowTitle(translate('MainWindow', 'ColourTerm'))
self.send_button.setText(translate('MainWindow', 'Send'))
self.tool_bar.setWindowTitle(translate('MainWindow', 'Tool Bar'))
self.open_action.setText(translate('MainWindow', 'Open...'))
self.open_action.setToolTip(translate(
'MainWindow', 'Open (%s)' % QtGui.QKeySequence(QtGui.QKeySequence.Open)))
self.close_action.setText(translate('MainWindow', 'Close'))
self.close_action.setToolTip(translate(
'MainWindow', 'Close (%s)' % QtGui.QKeySequence(QtGui.QKeySequence.Close)))
self.find_action.setText(translate('MainWindow', 'Find'))
self.find_action.setToolTip(translate(
'MainWindow', 'Find (%s)' % QtGui.QKeySequence(QtGui.QKeySequence.Find)))
self.capture_action.setText(translate('MainWindow', 'Capture'))
self.capture_action.setToolTip(translate('MainWindow', 'Capture to File'))
self.follow_action.setText(translate('MainWindow', '&Follow'))
self.follow_action.setToolTip(translate('MainWindow', 'Follow (Ctrl+Shift+F)'))
self.clear_action.setText(translate('MainWindow', 'Clear'))
self.clear_action.setToolTip(translate('MainWindow', 'Clear (Ctrl+BkSpace)'))
self.xmodem_action.setText(translate('MainWindow', 'Xmodem'))
self.xmodem_action.setToolTip(translate('MainWindow', 'Send a file via Xmodem (Ctrl+Shift+X)'))
self.configure_action.setText(translate('MainWindow', 'Configure...'))
self.configure_action.setToolTip(translate('MainWindow', 'Configure...'))
self.exit_action.setText(translate('MainWindow', 'Exit'))
self.exit_action.setToolTip(translate('MainWindow', 'Exit (Alt+F4)'))
class MainWindow(QtWidgets.QMainWindow, UiMainWindow):
updateOutput = QtCore.pyqtSignal(str)
showMessage = QtCore.pyqtSignal(str, str, int)
def __init__(self):
super(MainWindow, self).__init__()
self.terminal_lines = []
self.max_lines = 5000
self.setup_ui(self)
self.device = None
self.device_closed = True
self.follow_output = True
self.capture_file = None
self.capture_filename = ''
self.highlights = self.load_highlights()
self.disable_output = False
self.xmodem_send_progress_window = None
if not self.highlights:
self.highlights = create_default_highlights()
self.settings_dialog = SettingsDialog()
self.connect_dialog = ConnectDialog(self)
self.open_action.triggered.connect(self.on_open_action_triggered)
self.close_action.triggered.connect(self.on_close_action_triggered)
self.find_action.triggered.connect(self.on_find_action_toggled)
self.capture_action.toggled.connect(self.on_capture_action_toggled)
self.follow_action.toggled.connect(self.on_follow_action_toggled)
self.clear_action.triggered.connect(self.on_clear_action_triggered)
self.xmodem_action.triggered.connect(self.on_xmodem_action_triggered)
self.configure_action.triggered.connect(self.on_configure_action_triggered)
self.exit_action.triggered.connect(self.close)
self.find_combobox.keyPressed.connect(self.on_find_combobox_key_pressed)
self.send_combobox.keyPressed.connect(self.on_send_combobox_key_pressed)
self.send_button.clicked.connect(self.on_send_button_clicked)
self.output_browser.page().mainFrame().contentsSizeChanged.connect(self.on_contents_size_changed)
self.output_browser.scrolled.connect(self.on_output_browser_scrolled)
self.updateOutput.connect(self.on_update_output)
self.showMessage.connect(self.on_show_message)
def close(self):
if not self.device_closed:
self.device_closed = True
if self.capture_file:
self.capture_file.flush()
self.capture_file.close()
return QtWidgets.QMainWindow.close(self)
def document_body(self):
return self.output_browser.page().mainFrame().documentElement().findFirst('pre')
def receive_text(self):
output = ''
while not self.device_closed:
try:
if self.disable_output:
time.sleep(0.5)
continue
output += self.device.read(1)
except SerialException as e:
self.showMessage.emit('Port Error', 'Error reading from serial port: %s' % e, MessageType.Critical)
self.on_close_action_triggered()
continue
if output.endswith('\r\n'):
self.updateOutput.emit(output.strip('\r\n'))
output = ''
elif output.endswith('\n'):
self.updateOutput.emit(output.strip('\n'))
output = ''
def on_open_action_triggered(self):
self.connect_dialog.update_port_combobox()
settings = QtCore.QSettings()
self.connect_dialog.set_port(settings.value('previous-port', ''))
if self.connect_dialog.exec_() == QtWidgets.QDialog.Accepted:
if not self.device_closed:
self.device_closed = True
self.device.close()
try:
port = self.connect_dialog.get_port()
settings.setValue('previous-port', port)
if isinstance(port, str) and port.startswith('COM'):
try:
# On Windows ports are 0-based, so strip the COM and subtract 1 from the port number
port = int(port[3:]) - 1
except (TypeError, ValueError):
QtWidgets.QMessageBox.critical(self, 'Error opening port', 'Error: Port is not valid')
return
self.device = serial_for_url(
url=port,
baudrate=self.connect_dialog.get_baud(),
bytesize=self.connect_dialog.get_data_bits(),
parity=self.connect_dialog.get_parity(),
stopbits=self.connect_dialog.get_stop_bits(),
timeout=1,
xonxoff=self.connect_dialog.get_software_handshake(),
rtscts=self.connect_dialog.get_hardware_handshake(),
dsrdtr=None,
do_not_open=False
)
self.device_closed = False
if not self.device.isOpen():
self.device.open()
output_thread = threading.Thread(target=self.receive_text)
output_thread.start()
except SerialException as e:
QtWidgets.QMessageBox.critical(self, 'Error opening port', e.args[0])
self.send_combobox.setEnabled(not self.device_closed)
self.send_button.setEnabled(not self.device_closed)
if self.send_combobox.isEnabled():
self.send_combobox.setFocus()
self.status_bar.showMessage('Connected to %s at %s baud' % (self.device.port, self.device.baudrate))
def on_close_action_triggered(self):
self.device_closed = True
if self.device and self.device.isOpen():
self.device.close()
self.send_combobox.setEnabled(not self.device_closed)
self.send_button.setEnabled(not self.device_closed)
self.status_bar.showMessage('')
def on_find_action_toggled(self, enabled):
self.find_widget.setVisible(enabled)
if enabled:
self.find_combobox.setFocus()
def on_capture_action_toggled(self, enabled):
if enabled and not self.capture_file:
if self.capture_filename:
base_dir = os.path.basename(self.capture_filename)
else:
base_dir = ''
self.capture_filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Capture To File', base_dir,
'Text files (*.txt *.log);;All files (*)')
self.capture_file = open(self.capture_filename, 'w')
self.status_bar.showMessage(self.capture_filename)
elif self.capture_file and not enabled:
self.capture_filename = ''
self.capture_file.flush()
self.capture_file.close()
self.capture_file = None
self.status_bar.clearMessage()
def on_follow_action_toggled(self, enabled):
self.follow_output = enabled
if enabled:
self.output_browser.page().mainFrame().scroll(
0, self.output_browser.page().mainFrame().contentsSize().height())
def on_clear_action_triggered(self):
elements = self.output_browser.page().mainFrame().findAllElements('div')
for element in elements:
element.removeFromDocument()
del elements
def xmodem_callback(self, total_packets, success_count, error_count):
if self.xmodem_send_progress_window:
self.xmodem_send_progress_window.setValue(success_count)
print('{} {} {}'.format(total_packets, success_count, error_count))
def on_xmodem_action_triggered(self):
file_dialog = QtWidgets.QFileDialog()
if file_dialog.exec_():
self.disable_output = True
try:
upload_file = file_dialog.selectedFiles()[0]
file_size = getsize(upload_file)
self.device.flushInput()
self.device.flushOutput()
xmodem_transfer = XMODEM(self.getc, self.putc, mode='xmodem1k', pad='\xff')
stream = open(upload_file, 'rb')
self.xmodem_send_progress_window = QtWidgets.QProgressDialog('Sending File...', '', 0, 0)
self.xmodem_send_progress_window.setCancelButton(None)
self.xmodem_send_progress_window.setMinimum(0)
self.xmodem_send_progress_window.setMaximum(file_size/1024)
self.xmodem_send_progress_window.setValue(0)
self.xmodem_send_progress_window.setModal(True)
self.xmodem_send_progress_window.show()
success = xmodem_transfer.send(stream, retry=200, callback=self.xmodem_callback)
print(success)
finally:
if self.xmodem_send_progress_window:
self.xmodem_send_progress_window.close()
self.xmodem_send_progress_window = None
self.disable_output = False
def on_configure_action_triggered(self):
self.settings_dialog.set_highlights(self.highlights)
self.settings_dialog.exec_()
self.highlights = self.settings_dialog.highlights()
self.save_highlights(self.highlights)
self.refresh_output()
def on_find_combobox_key_pressed(self, key):
if key == QtCore.Qt.Key_Return or key == QtCore.Qt.Key_Enter:
self.output_browser.findText(
self.find_combobox.currentText(),
QtWebKit.QWebPage.HighlightAllOccurrences | QtWebKit.QWebPage.FindWrapsAroundDocument
)
elif key == QtCore.Qt.Key_Escape:
if not self.find_combobox.currentText() and self.find_widget.isVisible():
self.find_action.setChecked(not self.find_action.isChecked())
else:
self.find_combobox.clearEditText()
self.output_browser.findText("")
def on_send_combobox_key_pressed(self, key):
if key == QtCore.Qt.Key_Return or key == QtCore.Qt.Key_Enter:
self.on_send_button_clicked()
def on_send_button_clicked(self):
if self.device.isOpen():
output = str(self.send_combobox.currentText())
self.send_combobox.insertItem(0, output)
self.send_combobox.setCurrentIndex(0)
self.send_combobox.clearEditText()
self.device.write(output + '\r\n')
def on_contents_size_changed(self, size):
if self.follow_output:
self.output_browser.page().mainFrame().scroll(0, size.height())
self.output_browser.update()
def on_update_output(self, output):
if self.capture_file:
self.capture_file.write(output + '\n')
self.capture_file.flush()
output = self.style_output(output)
self.document_body().appendInside(output)
def on_output_browser_scrolled(self):
scroll_value = self.output_browser.page().mainFrame().scrollBarValue(QtCore.Qt.Vertical)
scroll_max = self.output_browser.page().mainFrame().scrollBarMaximum(QtCore.Qt.Vertical)
if scroll_value < scroll_max:
self.on_follow_action_toggled(False)
self.follow_action.setChecked(False)
else:
self.on_follow_action_toggled(True)
self.follow_action.setChecked(True)
def on_show_message(self, title, message, type_=MessageType.Info):
if type_ == MessageType.Info:
QtWidgets.QMessageBox.information(self, title, message)
elif type_ == MessageType.Question:
QtWidgets.QMessageBox.question(self, title, message)
elif type_ == MessageType.Warning:
QtWidgets.QMessageBox.warning(self, title, message)
elif type_ == MessageType.Critical:
QtWidgets.QMessageBox.critical(self, title, message)
def refresh_output(self):
elements = self.output_browser.page().mainFrame().findAllElements('div')
lines = [element.toPlainText() for element in elements]
pre = self.output_browser.page().mainFrame().findFirstElement('pre')
pre.setInnerXml('')
for line in lines:
output = self.style_output(line)
self.document_body().appendInside(output)
self.output_browser.page().mainFrame().scroll(0, self.output_browser.page().mainFrame().contentsSize().height())
self.output_browser.update()
def style_output(self, output):
style = 'font-family: \'Hack\', \'Ubuntu Mono\', monospace; '
if not output:
output = '&nbsp;'
for highlight in self.highlights:
if highlight.regex.search(output):
if highlight.foreground:
style = '%scolor: %s; ' % (style, highlight.foreground)
if highlight.background:
style = '%sbackground-color: %s; ' % (style, highlight.background)
break
if style:
try:
output = '<div style="%s">%s</div>' % (style, self.filter_printable(output))
except TypeError:
output = '<div style="%s">%s</div>' % (style, output)
else:
output = '<div>%s</div>' % output
return output
def filter_printable(self, output):
printable_output = ''
for char in output:
if char not in printable:
printable_output += '\\x{:02x}'.format(ord(char))
else:
printable_output += str(char, 'utf8')
def save_highlights(self, highlights):
settings = QtCore.QSettings()
settings.setValue('highlights/count', len(highlights))
for index, highlight in enumerate(highlights):
settings.beginGroup('highlight-%s' % index)
settings.setValue('pattern', highlight.pattern)
settings.setValue('foreground', highlight.foreground)
if highlight.background:
settings.setValue('background', highlight.background)
else:
if settings.contains('background'):
settings.remove('background')
settings.endGroup()
def load_highlights(self):
settings = QtCore.QSettings()
highlight_count = settings.value('highlights/count', 0)
highlights = []
for index in range(highlight_count):
settings.beginGroup('highlight-%s' % index)
pattern = settings.value('pattern', '')
foreground = settings.value('foreground', '')
background = None
if settings.contains('background'):
background = settings.value('background', '')
settings.endGroup()
highlights.append(Highlight(pattern, foreground, background))
return highlights
def getc(self, size, timeout=1):
"""
Read a byte (usually a character) from the serial port
:param timeout:
:param size:
"""
try:
data = self.device.read(size)
except SerialException as se:
if 'interrupted system call' in str(se.args[0]).lower():
data = self.device.read(size)
else:
raise Exception(str(se))
except (SelectError, SocketError) as se:
if se.args[0] == 4:
data = self.device.read(size)
else:
raise Exception(str(se))
return data or None
def putc(self, data, timeout=1):
"""
Send a byte (usually a character) to the serial port
:param timeout:
:param data:
"""
try:
self.device.write(data)
except SerialException as se:
self.error('Got a SerialException: {}'.format(se))
if 'interrupted system call' in str(se.args[0]).lower():
self.device.write(data)
else:
raise Exception(str(se))
except (SelectError, SocketError) as se:
if se.args[0] == 4:
self.device.write(data)
else:
self.error('Got a SocketError or SelectError: {}'.format(se))
raise Exception(str(se))
return None