# -*- coding: utf-8 -*- import os import threading from string import printable from PyQt4 import QtCore, QtGui, 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 XMODEM1k, 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 = QtGui.QWidget(main_window) self.central_widget.setObjectName(from_utf8('central_widget')) self.central_layout = QtGui.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 = QtGui.QWidget(main_window) self.find_widget.setVisible(False) self.find_widget.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) self.find_widget.setObjectName(from_utf8('find_widget')) self.find_layout = QtGui.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('
' % str(QtGui.QApplication.palette().color(QtGui.QPalette.Text).name())) self.output_browser.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.output_browser.setObjectName(from_utf8('output_browser')) self.central_layout.addWidget(self.output_browser) self.send_layout = QtGui.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 = QtGui.QPushButton(self.central_widget) size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.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 = QtGui.QStatusBar(main_window) self.status_bar.setObjectName(from_utf8('status_bar')) main_window.setStatusBar(self.status_bar) self.tool_bar = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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).toString())) self.close_action.setText(translate('MainWindow', 'Close')) self.close_action.setToolTip(translate('MainWindow', 'Close (%s)' % QtGui.QKeySequence(QtGui.QKeySequence.Close).toString())) self.find_action.setText(translate('MainWindow', 'Find')) self.find_action.setToolTip(translate('MainWindow', 'Find (%s)' % QtGui.QKeySequence(QtGui.QKeySequence.Find).toString())) 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(QtGui.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 = u'' 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 QtGui.QMainWindow.close(self) def document_body(self): return self.output_browser.page().mainFrame().documentElement().findFirst(u'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(u'Port Error', u'Error reading from serial port: %s' % e, MessageType.Critical) self.on_close_action_triggered() continue if output.endswith('\r\n'): #self.terminal_lines.append(output.strip('\r\n')) #if len(self.terminal_lines) > self.max_lines: # self.terminal_lines = self.terminal_lines[-self.max_lines:] # self.refreshOutput() #else: 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(unicode(settings.value(u'previous-port', u'').toString())) if self.connect_dialog.exec_() == QtGui.QDialog.Accepted: if not self.device_closed: self.device_closed = True self.device.close() try: port = self.connect_dialog.get_port() settings.setValue(u'previous-port', port) if isinstance(port, basestring) 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): QtGui.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: QtGui.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 = u'' self.capture_filename = QtGui.QFileDialog.getSaveFileName(self, u'Capture To File', base_dir, u'Text files (*.txt *.log);;All files (*)') self.capture_file = open(self.capture_filename, u'w') self.status_bar.showMessage(self.capture_filename) elif self.capture_file and not enabled: self.capture_filename = u'' 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 total_packets, success_count, error_count def on_xmodem_action_triggered(self): file_dialog = QtGui.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 = QtGui.QProgressDialog(u'Sending File...', u'', 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): #self.terminal_lines.append(output) if self.capture_file: self.capture_file.write(output + '\n') self.capture_file.flush() #if len(self.terminal_lines) > 5000: # self.terminal_lines = self.terminal_lines[-5000:] # self.refreshOutput() #else: 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: QtGui.QMessageBox.information(self, title, message) elif type_ == MessageType.Question: QtGui.QMessageBox.question(self, title, message) elif type_ == MessageType.Warning: QtGui.QMessageBox.warning(self, title, message) elif type_ == MessageType.Critical: QtGui.QMessageBox.critical(self, title, message) def refresh_output(self): elements = self.output_browser.page().mainFrame().findAllElements('div') lines = [unicode(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 = u'font-family: \'Ubuntu Mono\', monospace; ' if not output: output = u' ' for highlight in self.highlights: if highlight.regex.search(output): if highlight.foreground: style = u'%scolor: %s; ' % (style, highlight.foreground) if highlight.background: style = u'%sbackground-color: %s; ' % (style, highlight.background) break if style: try: output = u'