update
							
								
								
									
										0
									
								
								src/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										19
									
								
								src/qweechat/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,19 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
							
								
								
									
										122
									
								
								src/qweechat/__main__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,122 @@ | |||
| import os | ||||
| import sys | ||||
| import traceback | ||||
| import logging | ||||
| 
 | ||||
| from  qtpy import QtWidgets, QtGui, QtCore | ||||
| from  qtpy.QtWidgets import QApplication | ||||
| 
 | ||||
| global LOG | ||||
| LOG = logging.getLogger('qweechat') | ||||
| 
 | ||||
| def iMain(lArgs=None): | ||||
|     try: | ||||
|         from qweechat import qweechat | ||||
|         from qweechat.config import write | ||||
|         LOG.info("Loading WeechatConsole") | ||||
|     except ImportError as e: | ||||
|         LOG.error(f"ImportError Loading import qweechat {e} {sys.path}") | ||||
|         LOG.debug(traceback.print_exc()) | ||||
|         return 1 | ||||
| 
 | ||||
|     from qtpy.QtWidgets import QApplication | ||||
|     _app = QApplication([]) | ||||
| 
 | ||||
|     # is this still needed? | ||||
|     if sys.platform == 'Linux' and \ | ||||
|           hasattr(QtCore.Qt, 'AA_X11InitThreads'): | ||||
|             QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) | ||||
| 
 | ||||
|     try: | ||||
|         # WeeChat backported from PySide6 to PyQt5 | ||||
|         LOG.info("Adding WeechatConsole") | ||||
|         class WeechatConsole(qweechat.MainWindow): | ||||
|             def __init__(self, *args): | ||||
|                 qweechat.MainWindow.__init__(self, *args) | ||||
| 
 | ||||
|             def closeEvent(self, event): | ||||
|                 """Called when QWeeChat window is closed.""" | ||||
|                 self.network.disconnect_weechat() | ||||
|                 if self.network.debug_dialog: | ||||
|                     self.network.debug_dialog.close() | ||||
|                 write(self.config) | ||||
|     except Exception as e: | ||||
|         LOG.exception(f"ERROR WeechatConsole {e}") | ||||
|         MainWindow = None | ||||
|         return 2 | ||||
|     size = 12 | ||||
|     font_name = "Courier New" | ||||
|     font_name = "DejaVu Sans Mono" | ||||
| 
 | ||||
|     try: | ||||
|         LOG.info("Creating WeechatConsole") | ||||
|         _we = WeechatConsole() | ||||
|         _we.show() | ||||
|         _we.setWindowTitle('File/Connect to 127.0.0.1:9000') | ||||
|         # Fix the pyconsole geometry | ||||
|         try: | ||||
|             font = _we.buffers[0].widget.chat.defaultFont() | ||||
|             font.setFamily(font_name) | ||||
|             font.setBold(True) | ||||
|             if font_width is None: | ||||
|                 font_width = QFontMetrics(font).width('M') | ||||
|             _we.setFont(font) | ||||
|         except Exception as e: | ||||
| #                LOG.debug(e) | ||||
|             font_width = size | ||||
|         geometry = _we.geometry() | ||||
|         # make this configable? | ||||
|         geometry.setWidth(int(font_width*70)) | ||||
|         geometry.setHeight(int(font_width*(2+24)*11/8)) | ||||
|         _we.setGeometry(geometry) | ||||
|         #? QtCore.QSize() | ||||
|         _we.resize(int(font_width*80+20), int(font_width*(2+24)*11/8)) | ||||
| 
 | ||||
|         _we.list_buffers.setSizePolicy(QtWidgets.QSizePolicy.Preferred, | ||||
|                                             QtWidgets.QSizePolicy.Preferred) | ||||
|         _we.stacked_buffers.setSizePolicy(QtWidgets.QSizePolicy.Expanding, | ||||
|                                                QtWidgets.QSizePolicy.Expanding) | ||||
| 
 | ||||
|         LOG.info("Showing WeechatConsole") | ||||
|         _we.show() | ||||
|         # or _we.eval_in_thread() | ||||
| #?        return 0 | ||||
|     except Exception as e: | ||||
|         LOG.exception(f"Error creating WeechatConsole {e}") | ||||
|         return 4 | ||||
|   | ||||
|     LOG.info("_execute_app") | ||||
| 
 | ||||
|     # self._app.lastWindowClosed.connect(self._app.quit) | ||||
|     while True: | ||||
|         try: | ||||
|             _app.exec_() | ||||
|         except Exception as ex: | ||||
|             LOG.error('Unhandled exception: ' + str(ex)) | ||||
|             return 5 | ||||
|         else: | ||||
|             pass # break | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     iRet = 0 | ||||
|     try: | ||||
|         iRet = iMain(sys.argv[1:]) | ||||
|     except KeyboardInterrupt: | ||||
|         iRet = 0 | ||||
|     except SystemExit as e: | ||||
|         iRet = e | ||||
|     except Exception as e: | ||||
|         import traceback | ||||
|         sys.stderr.write(f"Exception from main  {e}" \ | ||||
|                          +'\n' + traceback.format_exc() +'\n' ) | ||||
|         iRet = 1 | ||||
| 
 | ||||
|     # Exception ignored in: <module 'threading' from '/usr/lib/python3.9/threading.py'> | ||||
|     # File "/usr/lib/python3.9/threading.py", line 1428, in _shutdown | ||||
|     # lock.acquire() | ||||
|     # gevent.exceptions.LoopExit as e: | ||||
|     # This operation would block forever | ||||
| #    sys.stderr.write('Calling sys.exit' +'\n') | ||||
| #    sys.exit(iRet) | ||||
|      | ||||
							
								
								
									
										61
									
								
								src/qweechat/about.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,61 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # about.py - about dialog box | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """About dialog box.""" | ||||
| 
 | ||||
| from  qtpy import QtCore, QtWidgets as QtGui | ||||
| 
 | ||||
| from qweechat.version import qweechat_version | ||||
| 
 | ||||
| 
 | ||||
| class AboutDialog(QtGui.QDialog): | ||||
|     """About dialog.""" | ||||
| 
 | ||||
|     def __init__(self, app_name, author, weechat_site, *args): | ||||
|         QtGui.QDialog.__init__(*(self,) + args) | ||||
|         self.setModal(True) | ||||
|         self.setWindowTitle('About') | ||||
| 
 | ||||
|         close_button = QtGui.QPushButton('Close') | ||||
|         close_button.pressed.connect(self.close) | ||||
| 
 | ||||
|         hbox = QtGui.QHBoxLayout() | ||||
|         hbox.addStretch(1) | ||||
|         hbox.addWidget(close_button) | ||||
|         hbox.addStretch(1) | ||||
| 
 | ||||
|         vbox = QtGui.QVBoxLayout() | ||||
|         messages = [ | ||||
|             f'<b>{app_name}</b> {qweechat_version()}', | ||||
|             f'© 2011-2022 {author}', | ||||
|             '', | ||||
|             f'<a href="{weechat_site}">{weechat_site}</a>', | ||||
|             '', | ||||
|         ] | ||||
|         for msg in messages: | ||||
|             label = QtGui.QLabel(msg) | ||||
|             label.setAlignment(QtCore.Qt.AlignHCenter) | ||||
|             vbox.addWidget(label) | ||||
|         vbox.addLayout(hbox) | ||||
| 
 | ||||
|         self.setLayout(vbox) | ||||
|         self.show() | ||||
							
								
								
									
										248
									
								
								src/qweechat/buffer.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,248 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # buffer.py - management of WeeChat buffers/nicklist | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Management of WeeChat buffers/nicklist.""" | ||||
| 
 | ||||
| from pkg_resources import resource_filename | ||||
| 
 | ||||
| from  qtpy import QtCore, QtGui, QtWidgets | ||||
| from  qtpy.QtCore import Signal | ||||
| 
 | ||||
| from qweechat.chat import ChatTextEdit | ||||
| from qweechat.input import InputLineEdit | ||||
| from qweechat.weechat import color | ||||
| 
 | ||||
| class GenericListWidget(QtWidgets.QListWidget): | ||||
|     """Generic QListWidget with dynamic size.""" | ||||
| 
 | ||||
|     def __init__(self, *args): | ||||
|         super().__init__(*args) | ||||
|         self.setMaximumWidth(100) | ||||
|         self.setTextElideMode(QtCore.Qt.ElideNone) | ||||
|         self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) | ||||
|         self.setFocusPolicy(QtCore.Qt.NoFocus) | ||||
|         pal = self.palette() | ||||
|         pal.setColor(QtGui.QPalette.Highlight, QtGui.QColor('#ddddff')) | ||||
|         pal.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor('black')) | ||||
|         self.setPalette(pal) | ||||
| 
 | ||||
|     def auto_resize(self): | ||||
|         size = self.sizeHintForColumn(0) | ||||
|         if size > 0: | ||||
|             size += 4 | ||||
|         self.setMaximumWidth(size) | ||||
| 
 | ||||
|     def clear(self, *args): | ||||
|         """Re-implement clear to set dynamic size after clear.""" | ||||
|         QtWidgets.QListWidget.clear(*(self,) + args) | ||||
|         self.auto_resize() | ||||
| 
 | ||||
|     def addItem(self, *args): | ||||
|         """Re-implement addItem to set dynamic size after add.""" | ||||
|         QtWidgets.QListWidget.addItem(*(self,) + args) | ||||
|         self.auto_resize() | ||||
| 
 | ||||
|     def insertItem(self, *args): | ||||
|         """Re-implement insertItem to set dynamic size after insert.""" | ||||
|         QtWidgets.QListWidget.insertItem(*(self,) + args) | ||||
|         self.auto_resize() | ||||
| 
 | ||||
| 
 | ||||
| class BufferListWidget(GenericListWidget): | ||||
|     """Widget with list of buffers.""" | ||||
| 
 | ||||
|     def switch_prev_buffer(self): | ||||
|         if self.currentRow() > 0: | ||||
|             self.setCurrentRow(self.currentRow() - 1) | ||||
|         else: | ||||
|             self.setCurrentRow(self.count() - 1) | ||||
| 
 | ||||
|     def switch_next_buffer(self): | ||||
|         if self.currentRow() < self.count() - 1: | ||||
|             self.setCurrentRow(self.currentRow() + 1) | ||||
|         else: | ||||
|             self.setCurrentRow(0) | ||||
| 
 | ||||
| 
 | ||||
| class BufferWidget(QtWidgets.QWidget): | ||||
|     """ | ||||
|     Widget with (from top to bottom): | ||||
|     title, chat + nicklist (optional) + prompt/input. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, display_nicklist=False): | ||||
|         super().__init__() | ||||
| 
 | ||||
|         # title | ||||
|         self.title = QtWidgets.QLineEdit() | ||||
|         self.title.setFocusPolicy(QtCore.Qt.NoFocus) | ||||
| 
 | ||||
|         # splitter with chat + nicklist | ||||
|         self.chat_nicklist = QtWidgets.QSplitter() | ||||
|         self.chat_nicklist.setSizePolicy(QtWidgets.QSizePolicy.Expanding, | ||||
|                                          QtWidgets.QSizePolicy.Expanding) | ||||
|         self.chat = ChatTextEdit(debug=False) | ||||
|         self.chat_nicklist.addWidget(self.chat) | ||||
|         self.nicklist = GenericListWidget() | ||||
|         if not display_nicklist: | ||||
|             self.nicklist.setVisible(False) | ||||
|         self.chat_nicklist.addWidget(self.nicklist) | ||||
| 
 | ||||
|         # prompt + input | ||||
|         self.hbox_edit = QtWidgets.QHBoxLayout() | ||||
|         self.hbox_edit.setContentsMargins(0, 0, 0, 0) | ||||
|         self.hbox_edit.setSpacing(0) | ||||
|         self.input = InputLineEdit(self.chat) | ||||
|         self.hbox_edit.addWidget(self.input) | ||||
|         prompt_input = QtWidgets.QWidget() | ||||
|         prompt_input.setLayout(self.hbox_edit) | ||||
| 
 | ||||
|         # vbox with title + chat/nicklist + prompt/input | ||||
|         vbox = QtWidgets.QVBoxLayout() | ||||
|         vbox.setContentsMargins(0, 0, 0, 0) | ||||
|         vbox.setSpacing(0) | ||||
|         vbox.addWidget(self.title) | ||||
|         vbox.addWidget(self.chat_nicklist) | ||||
|         vbox.addWidget(prompt_input) | ||||
| 
 | ||||
|         self.setLayout(vbox) | ||||
| 
 | ||||
|     def set_title(self, title): | ||||
|         """Set buffer title.""" | ||||
|         self.title.clear() | ||||
|         if title is not None: | ||||
|             self.title.setText(title) | ||||
| 
 | ||||
|     def set_prompt(self, prompt): | ||||
|         """Set prompt.""" | ||||
|         if self.hbox_edit.count() > 1: | ||||
|             self.hbox_edit.takeAt(0) | ||||
|         if prompt is not None: | ||||
|             label = QtWidgets.QLabel(prompt) | ||||
|             label.setContentsMargins(0, 0, 5, 0) | ||||
|             self.hbox_edit.insertWidget(0, label) | ||||
| 
 | ||||
| 
 | ||||
| class Buffer(QtCore.QObject): | ||||
|     """A WeeChat buffer.""" | ||||
| 
 | ||||
|     bufferInput = Signal(str, str) | ||||
| 
 | ||||
|     def __init__(self, data=None): | ||||
|         QtCore.QObject.__init__(self) | ||||
|         self.data = data or {} | ||||
|         self.nicklist = {} | ||||
|         self.widget = BufferWidget(display_nicklist=self.data.get('nicklist', | ||||
|                                                                   0)) | ||||
|         self.update_title() | ||||
|         self.update_prompt() | ||||
|         self.widget.input.textSent.connect(self.input_text_sent) | ||||
| 
 | ||||
|     def pointer(self): | ||||
|         """Return pointer on buffer.""" | ||||
|         return self.data.get('__path', [''])[0] | ||||
| 
 | ||||
|     def update_title(self): | ||||
|         """Update title.""" | ||||
|         try: | ||||
|             self.widget.set_title( | ||||
|                 color.remove(self.data['title'])) | ||||
|         except Exception:  # noqa: E722 | ||||
|             # TODO: Debug print the exception to be fixed. | ||||
|             # traceback.print_exc() | ||||
|             self.widget.set_title(None) | ||||
| 
 | ||||
|     def update_prompt(self): | ||||
|         """Update prompt.""" | ||||
|         try: | ||||
|             self.widget.set_prompt(self.data['local_variables']['nick']) | ||||
|         except Exception:  # noqa: E722 | ||||
|             self.widget.set_prompt(None) | ||||
| 
 | ||||
|     def input_text_sent(self, text): | ||||
|         """Called when text has to be sent to buffer.""" | ||||
|         if self.data: | ||||
|             self.bufferInput.emit(self.data['full_name'], text) | ||||
| 
 | ||||
|     def nicklist_add_item(self, parent, group, prefix, name, visible): | ||||
|         """Add a group/nick in nicklist.""" | ||||
|         if group: | ||||
|             self.nicklist[name] = { | ||||
|                 'visible': visible, | ||||
|                 'nicks': [] | ||||
|             } | ||||
|         else: | ||||
|             self.nicklist[parent]['nicks'].append({ | ||||
|                 'prefix': prefix, | ||||
|                 'name': name, | ||||
|                 'visible': visible, | ||||
|             }) | ||||
| 
 | ||||
|     def nicklist_remove_item(self, parent, group, name): | ||||
|         """Remove a group/nick from nicklist.""" | ||||
|         if group: | ||||
|             if name in self.nicklist: | ||||
|                 del self.nicklist[name] | ||||
|         else: | ||||
|             if parent in self.nicklist: | ||||
|                 self.nicklist[parent]['nicks'] = [ | ||||
|                     nick for nick in self.nicklist[parent]['nicks'] | ||||
|                     if nick['name'] != name | ||||
|                 ] | ||||
| 
 | ||||
|     def nicklist_update_item(self, parent, group, prefix, name, visible): | ||||
|         """Update a group/nick in nicklist.""" | ||||
|         if group: | ||||
|             if name in self.nicklist: | ||||
|                 self.nicklist[name]['visible'] = visible | ||||
|         else: | ||||
|             if parent in self.nicklist: | ||||
|                 for nick in self.nicklist[parent]['nicks']: | ||||
|                     if nick['name'] == name: | ||||
|                         nick['prefix'] = prefix | ||||
|                         nick['visible'] = visible | ||||
|                         break | ||||
| 
 | ||||
|     def nicklist_refresh(self): | ||||
|         """Refresh nicklist.""" | ||||
|         self.widget.nicklist.clear() | ||||
|         for group in sorted(self.nicklist): | ||||
|             for nick in sorted(self.nicklist[group]['nicks'], | ||||
|                                key=lambda n: n['name']): | ||||
|                 prefix_color = { | ||||
|                     '': '', | ||||
|                     ' ': '', | ||||
|                     '+': 'yellow', | ||||
|                 } | ||||
|                 col = prefix_color.get(nick['prefix'], 'green') | ||||
|                 if col: | ||||
|                     icon = QtGui.QIcon( | ||||
|                         resource_filename(__name__, | ||||
|                                           'data/icons/bullet_%s_8x8.png' % | ||||
|                                           col)) | ||||
|                 else: | ||||
|                     pixmap = QtGui.QPixmap(8, 8) | ||||
|                     pixmap.fill() | ||||
|                     icon = QtGui.QIcon(pixmap) | ||||
|                 item = QtWidgets.QListWidgetItem(icon, nick['name']) | ||||
|                 self.widget.nicklist.addItem(item) | ||||
|                 self.widget.nicklist.setVisible(True) | ||||
							
								
								
									
										142
									
								
								src/qweechat/chat.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,142 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # chat.py - chat area | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Chat area.""" | ||||
| 
 | ||||
| import datetime | ||||
| 
 | ||||
| from  qtpy import QtCore, QtWidgets, QtGui | ||||
| 
 | ||||
| from qweechat.config import color_options | ||||
| from qweechat.weechat import color | ||||
| 
 | ||||
| 
 | ||||
| class ChatTextEdit(QtWidgets.QTextEdit): | ||||
|     """Chat area.""" | ||||
| 
 | ||||
|     def __init__(self, debug, *args): | ||||
|         QtWidgets.QTextEdit.__init__(*(self,) + args) | ||||
|         self.debug = debug | ||||
|         self.readOnly = True | ||||
|         self.setFocusPolicy(QtCore.Qt.NoFocus) | ||||
|         self.setFontFamily('monospace') | ||||
|         self._textcolor = self.textColor() | ||||
|         self._bgcolor = QtGui.QColor('#FFFFFF') | ||||
|         self._setcolorcode = { | ||||
|             'F': (self.setTextColor, self._textcolor), | ||||
|             'B': (self.setTextBackgroundColor, self._bgcolor) | ||||
|         } | ||||
|         self._setfont = { | ||||
|             '*': self.setFontWeight, | ||||
|             '_': self.setFontUnderline, | ||||
|             '/': self.setFontItalic | ||||
|         } | ||||
|         self._fontvalues = { | ||||
|             False: { | ||||
|                 '*': QtGui.QFont.Normal, | ||||
|                 '_': False, | ||||
|                 '/': False | ||||
|             }, | ||||
|             True: { | ||||
|                 '*': QtGui.QFont.Bold, | ||||
|                 '_': True, | ||||
|                 '/': True | ||||
|             } | ||||
|         } | ||||
|         self._color = color.Color(color_options(), self.debug) | ||||
| 
 | ||||
|     def display(self, time, prefix, text, forcecolor=None): | ||||
|         if time == 0: | ||||
|             now = datetime.datetime.now() | ||||
|         else: | ||||
|             now = datetime.datetime.fromtimestamp(float(time)) | ||||
|         self.setTextColor(QtGui.QColor('#999999')) | ||||
|         self.insertPlainText(now.strftime('%H:%M ')) | ||||
|         prefix = self._color.convert(prefix) | ||||
|         text = self._color.convert(text) | ||||
|         if forcecolor: | ||||
|             if prefix: | ||||
|                 prefix = '\x01(F%s)%s' % (forcecolor, prefix) | ||||
|             text = '\x01(F%s)%s' % (forcecolor, text) | ||||
|         if prefix: | ||||
|             self._display_with_colors(prefix + ' ') | ||||
|         if text: | ||||
|             self._display_with_colors(text) | ||||
|             if text[-1:] != '\n': | ||||
|                 self.insertPlainText('\n') | ||||
|         else: | ||||
|             self.insertPlainText('\n') | ||||
|         self.scroll_bottom() | ||||
| 
 | ||||
|     def _display_with_colors(self, string): | ||||
|         self.setTextColor(self._textcolor) | ||||
|         self.setTextBackgroundColor(self._bgcolor) | ||||
|         self._reset_attributes() | ||||
|         items = string.split('\x01') | ||||
|         for i, item in enumerate(items): | ||||
|             if i > 0 and item.startswith('('): | ||||
|                 pos = item.find(')') | ||||
|                 if pos >= 2: | ||||
|                     action = item[1] | ||||
|                     code = item[2:pos] | ||||
|                     if action == '+': | ||||
|                         # set attribute | ||||
|                         self._set_attribute(code[0], True) | ||||
|                     elif action == '-': | ||||
|                         # remove attribute | ||||
|                         self._set_attribute(code[0], False) | ||||
|                     else: | ||||
|                         # reset attributes and color | ||||
|                         if code == 'r': | ||||
|                             self._reset_attributes() | ||||
|                             self._setcolorcode[action][0]( | ||||
|                                 self._setcolorcode[action][1]) | ||||
|                         else: | ||||
|                             # set attributes + color | ||||
|                             while code.startswith(('*', '!', '/', '_', '|', | ||||
|                                                    'r')): | ||||
|                                 if code[0] == 'r': | ||||
|                                     self._reset_attributes() | ||||
|                                 elif code[0] in self._setfont: | ||||
|                                     self._set_attribute( | ||||
|                                         code[0], | ||||
|                                         not self._font[code[0]]) | ||||
|                                 code = code[1:] | ||||
|                             if code: | ||||
|                                 self._setcolorcode[action][0]( | ||||
|                                     QtGui.QColor(code)) | ||||
|                     item = item[pos+1:] | ||||
|             if len(item) > 0: | ||||
|                 self.insertPlainText(item) | ||||
| 
 | ||||
|     def _reset_attributes(self): | ||||
|         self._font = {} | ||||
|         for attr in self._setfont: | ||||
|             self._set_attribute(attr, False) | ||||
| 
 | ||||
|     def _set_attribute(self, attr, value): | ||||
|         self._font[attr] = value | ||||
|         self._setfont[attr](self._fontvalues[self._font[attr]][attr]) | ||||
| 
 | ||||
|     def scroll_bottom(self): | ||||
|         scroll = self.verticalScrollBar() | ||||
|         scroll.setValue(scroll.maximum()) | ||||
							
								
								
									
										137
									
								
								src/qweechat/config.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,137 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # config.py - configuration for QWeeChat | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Configuration for QWeeChat.""" | ||||
| 
 | ||||
| import configparser | ||||
| import os | ||||
| 
 | ||||
| from pathlib import Path | ||||
| 
 | ||||
| CONFIG_DIR = '%s/.config/qweechat' % os.getenv('HOME') | ||||
| CONFIG_FILENAME = '%s/qweechat.conf' % CONFIG_DIR | ||||
| 
 | ||||
| CONFIG_DEFAULT_RELAY_LINES = 50 | ||||
| 
 | ||||
| CONFIG_DEFAULT_SECTIONS = ('relay', 'look', 'color') | ||||
| CONFIG_DEFAULT_OPTIONS = (('relay.hostname', '127.0.0.1'), | ||||
|                           ('relay.port', '9000'), | ||||
|                           ('relay.ssl', 'off'), | ||||
|                           ('relay.password', ''), | ||||
|                           ('relay.autoconnect', 'off'), | ||||
|                           ('relay.lines', str(CONFIG_DEFAULT_RELAY_LINES)), | ||||
|                           ('look.debug', 'off'), | ||||
|                           ('look.statusbar', 'on')) | ||||
| CONFIG_DEFAULT_NOTIFICATION_OPTIONS = tuple | ||||
| 
 | ||||
| # Default colors for WeeChat color options (option name, #rgb value) | ||||
| CONFIG_DEFAULT_COLOR_OPTIONS = ( | ||||
|     ('separator', '#000066'),  # 0 | ||||
|     ('chat', '#000000'),  # 1 | ||||
|     ('chat_time', '#999999'),  # 2 | ||||
|     ('chat_time_delimiters', '#000000'),  # 3 | ||||
|     ('chat_prefix_error', '#FF6633'),  # 4 | ||||
|     ('chat_prefix_network', '#990099'),  # 5 | ||||
|     ('chat_prefix_action', '#000000'),  # 6 | ||||
|     ('chat_prefix_join', '#00CC00'),  # 7 | ||||
|     ('chat_prefix_quit', '#CC0000'),  # 8 | ||||
|     ('chat_prefix_more', '#CC00FF'),  # 9 | ||||
|     ('chat_prefix_suffix', '#330099'),  # 10 | ||||
|     ('chat_buffer', '#000000'),  # 11 | ||||
|     ('chat_server', '#000000'),  # 12 | ||||
|     ('chat_channel', '#000000'),  # 13 | ||||
|     ('chat_nick', '#000000'),  # 14 | ||||
|     ('chat_nick_self', '*#000000'),  # 15 | ||||
|     ('chat_nick_other', '#000000'),  # 16 | ||||
|     ('', '#000000'),  # 17 (nick1 -- obsolete) | ||||
|     ('', '#000000'),  # 18 (nick2 -- obsolete) | ||||
|     ('', '#000000'),  # 19 (nick3 -- obsolete) | ||||
|     ('', '#000000'),  # 20 (nick4 -- obsolete) | ||||
|     ('', '#000000'),  # 21 (nick5 -- obsolete) | ||||
|     ('', '#000000'),  # 22 (nick6 -- obsolete) | ||||
|     ('', '#000000'),  # 23 (nick7 -- obsolete) | ||||
|     ('', '#000000'),  # 24 (nick8 -- obsolete) | ||||
|     ('', '#000000'),  # 25 (nick9 -- obsolete) | ||||
|     ('', '#000000'),  # 26 (nick10 -- obsolete) | ||||
|     ('chat_host', '#666666'),  # 27 | ||||
|     ('chat_delimiters', '#9999FF'),  # 28 | ||||
|     ('chat_highlight', '#3399CC'),  # 29 | ||||
|     ('chat_read_marker', '#000000'),  # 30 | ||||
|     ('chat_text_found', '#000000'),  # 31 | ||||
|     ('chat_value', '#000000'),  # 32 | ||||
|     ('chat_prefix_buffer', '#000000'),  # 33 | ||||
|     ('chat_tags', '#000000'),  # 34 | ||||
|     ('chat_inactive_window', '#000000'),  # 35 | ||||
|     ('chat_inactive_buffer', '#000000'),  # 36 | ||||
|     ('chat_prefix_buffer_inactive_buffer', '#000000'),  # 37 | ||||
|     ('chat_nick_offline', '#000000'),  # 38 | ||||
|     ('chat_nick_offline_highlight', '#000000'),  # 39 | ||||
|     ('chat_nick_prefix', '#000000'),  # 40 | ||||
|     ('chat_nick_suffix', '#000000'),  # 41 | ||||
|     ('emphasis', '#000000'),  # 42 | ||||
|     ('chat_day_change', '#000000'),  # 43 | ||||
| ) | ||||
| config_color_options = [] | ||||
| 
 | ||||
| 
 | ||||
| def read(): | ||||
|     """Read config file.""" | ||||
|     global config_color_options | ||||
|     config = configparser.RawConfigParser() | ||||
|     if os.path.isfile(CONFIG_FILENAME): | ||||
|         config.read(CONFIG_FILENAME) | ||||
| 
 | ||||
|     # add missing sections/options | ||||
|     for section in CONFIG_DEFAULT_SECTIONS: | ||||
|         if not config.has_section(section): | ||||
|             config.add_section(section) | ||||
|     for option in reversed(CONFIG_DEFAULT_OPTIONS): | ||||
|         section, name = option[0].split('.', 1) | ||||
|         if not config.has_option(section, name): | ||||
|             config.set(section, name, option[1]) | ||||
|     section = 'color' | ||||
|     for option in reversed(CONFIG_DEFAULT_COLOR_OPTIONS): | ||||
|         if option[0] and not config.has_option(section, option[0]): | ||||
|             config.set(section, option[0], option[1]) | ||||
| 
 | ||||
|     # build list of color options | ||||
|     config_color_options = [] | ||||
|     for option in CONFIG_DEFAULT_COLOR_OPTIONS: | ||||
|         if option[0]: | ||||
|             config_color_options.append(config.get('color', option[0])) | ||||
|         else: | ||||
|             config_color_options.append('#000000') | ||||
| 
 | ||||
|     return config | ||||
| 
 | ||||
| 
 | ||||
| def write(config): | ||||
|     """Write config file.""" | ||||
|     Path(CONFIG_DIR).mkdir(mode=0o0700, parents=True, exist_ok=True) | ||||
|     with open(CONFIG_FILENAME, 'w') as cfg: | ||||
|         config.write(cfg) | ||||
| 
 | ||||
| 
 | ||||
| def color_options(): | ||||
|     """Return color options.""" | ||||
|     global config_color_options | ||||
|     return config_color_options | ||||
							
								
								
									
										134
									
								
								src/qweechat/connection.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,134 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # connection.py - connection window | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Connection window.""" | ||||
| 
 | ||||
| from  qtpy import QtGui, QtWidgets | ||||
| 
 | ||||
| 
 | ||||
| class ConnectionDialog(QtWidgets.QDialog): | ||||
|     """Connection window.""" | ||||
| 
 | ||||
|     def __init__(self, values, *args): | ||||
|         super().__init__(*args) | ||||
|         self.values = values | ||||
|         self.setModal(True) | ||||
|         self.setWindowTitle('Connect to WeeChat') | ||||
| 
 | ||||
|         grid = QtWidgets.QGridLayout() | ||||
|         grid.setSpacing(10) | ||||
| 
 | ||||
|         self.fields = {} | ||||
|         focus = None | ||||
| 
 | ||||
|         # hostname | ||||
|         grid.addWidget(QtWidgets.QLabel('<b>Hostname</b>'), 0, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setFixedWidth(200) | ||||
|         value = self.values.get('hostname', '') | ||||
|         if value in ['None', None]: | ||||
|             value = '0' | ||||
|         elif type(value) == int: | ||||
|             value = str(value) | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 0, 1) | ||||
|         self.fields['hostname'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'hostname' | ||||
| 
 | ||||
|         # port / SSL | ||||
|         grid.addWidget(QtWidgets.QLabel('<b>Port</b>'), 1, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setFixedWidth(200) | ||||
|         value = self.values.get('port', '') | ||||
|         if value in ['None', None]: | ||||
|             value = '0' | ||||
|         elif type(value) == int: | ||||
|             value = str(value) | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 1, 1) | ||||
|         self.fields['port'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'port' | ||||
| 
 | ||||
|         ssl = QtWidgets.QCheckBox('SSL') | ||||
|         ssl.setChecked(self.values['ssl'] == 'on') | ||||
|         grid.addWidget(ssl, 1, 2) | ||||
|         self.fields['ssl'] = ssl | ||||
| 
 | ||||
|         # password | ||||
|         grid.addWidget(QtWidgets.QLabel('<b>Password</b>'), 2, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setFixedWidth(200) | ||||
|         line_edit.setEchoMode(QtWidgets.QLineEdit.Password) | ||||
|         value = self.values.get('password', '') | ||||
|         if value in ['None', None]: | ||||
|             value = '0' | ||||
|         elif type(value) == int: | ||||
|             value = str(value) | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 2, 1) | ||||
|         self.fields['password'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'password' | ||||
| 
 | ||||
|         # TOTP (Time-Based One-Time Password) | ||||
|         label = QtWidgets.QLabel('TOTP') | ||||
|         label.setToolTip('Time-Based One-Time Password (6 digits)') | ||||
|         grid.addWidget(label, 3, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setPlaceholderText('6 digits') | ||||
|         validator = QtGui.QIntValidator(0, 999999, self) | ||||
|         line_edit.setValidator(validator) | ||||
|         line_edit.setFixedWidth(80) | ||||
|         value = self.values.get('totp', '') | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 3, 1) | ||||
|         self.fields['totp'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'totp' | ||||
| 
 | ||||
|         # lines | ||||
|         grid.addWidget(QtWidgets.QLabel('Lines'), 4, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setFixedWidth(200) | ||||
|         validator = QtGui.QIntValidator(0, 2147483647, self) | ||||
|         line_edit.setValidator(validator) | ||||
|         line_edit.setFixedWidth(80) | ||||
|         value = self.values.get('lines', '') | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 4, 1) | ||||
|         self.fields['lines'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'lines' | ||||
| 
 | ||||
|         self.dialog_buttons = QtWidgets.QDialogButtonBox() | ||||
|         self.dialog_buttons.setStandardButtons( | ||||
|             QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) | ||||
|         self.dialog_buttons.rejected.connect(self.close) | ||||
| 
 | ||||
|         grid.addWidget(self.dialog_buttons, 5, 0, 1, 2) | ||||
|         self.setLayout(grid) | ||||
|         self.show() | ||||
| 
 | ||||
|         if focus: | ||||
|             self.fields[focus].setFocus() | ||||
							
								
								
									
										41
									
								
								src/qweechat/data/icons/README
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,41 @@ | |||
| Copyright and license for images | ||||
| ================================ | ||||
| 
 | ||||
| 
 | ||||
| Files: weechat.png, bullet_green_8x8.png, bullet_yellow_8x8.png | ||||
| 
 | ||||
|   Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
|   Released under GPLv3. | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| Files: application-exit.png, dialog-close.png, dialog-ok-apply.png, | ||||
|        dialog-password.png, dialog-warning.png, document-save.png, | ||||
|        edit-find.png, help-about.png, network-connect.png, | ||||
|        network-disconnect.png, preferences-other.png | ||||
| 
 | ||||
|   Files come from Debian package "oxygen-icon-theme": | ||||
| 
 | ||||
|     The Oxygen Icon Theme | ||||
|       Copyright (C) 2007 Nuno Pinheiro <nuno@oxygen-icons.org> | ||||
|       Copyright (C) 2007 David Vignoni <david@icon-king.com> | ||||
|       Copyright (C) 2007 David Miller <miller@oxygen-icons.org> | ||||
|       Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org> | ||||
|       Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org> | ||||
|       Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org> | ||||
|       and others | ||||
| 
 | ||||
|     License: | ||||
| 
 | ||||
|       This library is free software; you can redistribute it and/or | ||||
|       modify it under the terms of the GNU Lesser General Public | ||||
|       License as published by the Free Software Foundation; either | ||||
|       version 3 of the License, or (at your option) any later version. | ||||
| 
 | ||||
|       This library is distributed in the hope that it will be useful, | ||||
|       but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | ||||
|       Lesser General Public License for more details. | ||||
| 
 | ||||
|       You should have received a copy of the GNU Lesser General Public | ||||
|       License along with this library. If not, see <http://www.gnu.org/licenses/>. | ||||
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/application-exit.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/bullet_green_8x8.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 384 B | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/bullet_yellow_8x8.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 375 B | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/dialog-close.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 813 B | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/dialog-ok-apply.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 597 B | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/dialog-password.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 713 B | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/dialog-warning.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 596 B | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/document-save.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/edit-find.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/help-about.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/network-connect.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/network-disconnect.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/preferences-other.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/qweechat/data/icons/weechat.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										51
									
								
								src/qweechat/debug.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,51 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # debug.py - debug window | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Debug window.""" | ||||
| 
 | ||||
| from  qtpy import QtWidgets | ||||
| 
 | ||||
| from qweechat.chat import ChatTextEdit | ||||
| from qweechat.input import InputLineEdit | ||||
| 
 | ||||
| 
 | ||||
| class DebugDialog(QtWidgets.QDialog): | ||||
|     """Debug dialog.""" | ||||
| 
 | ||||
|     def __init__(self, *args): | ||||
|         QtWidgets.QDialog.__init__(*(self,) + args) | ||||
|         self.resize(800, 600) | ||||
|         self.setWindowTitle('Debug console') | ||||
| 
 | ||||
|         self.chat = ChatTextEdit(debug=True) | ||||
|         self.input = InputLineEdit(self.chat) | ||||
| 
 | ||||
|         vbox = QtWidgets.QVBoxLayout() | ||||
|         vbox.addWidget(self.chat) | ||||
|         vbox.addWidget(self.input) | ||||
| 
 | ||||
|         self.setLayout(vbox) | ||||
|         self.show() | ||||
| 
 | ||||
|     def display_lines(self, lines): | ||||
|         for line in lines: | ||||
|             self.chat.display(*line[0], **line[1]) | ||||
							
								
								
									
										95
									
								
								src/qweechat/input.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,95 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # input.py - input line for chat and debug window | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Input line for chat and debug window.""" | ||||
| 
 | ||||
| from  qtpy import QtCore, QtWidgets | ||||
| from  qtpy.QtCore import Signal | ||||
| 
 | ||||
| class InputLineEdit(QtWidgets.QLineEdit): | ||||
|     """Input line.""" | ||||
| 
 | ||||
|     bufferSwitchPrev = Signal() | ||||
|     bufferSwitchNext = Signal() | ||||
|     textSent = Signal(str) | ||||
| 
 | ||||
|     def __init__(self, scroll_widget): | ||||
|         super().__init__() | ||||
|         self.scroll_widget = scroll_widget | ||||
|         self._history = [] | ||||
|         self._history_index = -1 | ||||
|         self.returnPressed.connect(self._input_return_pressed) | ||||
| 
 | ||||
|     def keyPressEvent(self, event): | ||||
|         key = event.key() | ||||
|         modifiers = event.modifiers() | ||||
|         scroll = self.scroll_widget.verticalScrollBar() | ||||
|         if modifiers == QtCore.Qt.ControlModifier: | ||||
|             if key == QtCore.Qt.Key_PageUp: | ||||
|                 self.bufferSwitchPrev.emit() | ||||
|             elif key == QtCore.Qt.Key_PageDown: | ||||
|                 self.bufferSwitchNext.emit() | ||||
|             else: | ||||
|                 QtWidgets.QLineEdit.keyPressEvent(self, event) | ||||
|         elif modifiers == QtCore.Qt.AltModifier: | ||||
|             if key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Up): | ||||
|                 self.bufferSwitchPrev.emit() | ||||
|             elif key in (QtCore.Qt.Key_Right, QtCore.Qt.Key_Down): | ||||
|                 self.bufferSwitchNext.emit() | ||||
|             elif key == QtCore.Qt.Key_PageUp: | ||||
|                 scroll.setValue(scroll.value() - (scroll.pageStep() / 10)) | ||||
|             elif key == QtCore.Qt.Key_PageDown: | ||||
|                 scroll.setValue(scroll.value() + (scroll.pageStep() / 10)) | ||||
|             elif key == QtCore.Qt.Key_Home: | ||||
|                 scroll.setValue(scroll.minimum()) | ||||
|             elif key == QtCore.Qt.Key_End: | ||||
|                 scroll.setValue(scroll.maximum()) | ||||
|             else: | ||||
|                 QtWidgets.QLineEdit.keyPressEvent(self, event) | ||||
|         elif key == QtCore.Qt.Key_PageUp: | ||||
|             scroll.setValue(scroll.value() - scroll.pageStep()) | ||||
|         elif key == QtCore.Qt.Key_PageDown: | ||||
|             scroll.setValue(scroll.value() + scroll.pageStep()) | ||||
|         elif key == QtCore.Qt.Key_Up: | ||||
|             self._history_navigate(-1) | ||||
|         elif key == QtCore.Qt.Key_Down: | ||||
|             self._history_navigate(1) | ||||
|         else: | ||||
|             QtWidgets.QLineEdit.keyPressEvent(self, event) | ||||
| 
 | ||||
|     def _input_return_pressed(self): | ||||
|         self._history.append(self.text()) | ||||
|         self._history_index = len(self._history) | ||||
|         self.textSent.emit(self.text()) | ||||
|         self.clear() | ||||
| 
 | ||||
|     def _history_navigate(self, direction): | ||||
|         if self._history: | ||||
|             self._history_index += direction | ||||
|             if self._history_index < 0: | ||||
|                 self._history_index = 0 | ||||
|                 return | ||||
|             if self._history_index > len(self._history) - 1: | ||||
|                 self._history_index = len(self._history) | ||||
|                 self.clear() | ||||
|                 return | ||||
|             self.setText(self._history[self._history_index]) | ||||
							
								
								
									
										371
									
								
								src/qweechat/network.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,371 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # network.py - I/O with WeeChat/relay | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """I/O with WeeChat/relay.""" | ||||
| 
 | ||||
| import hashlib | ||||
| import secrets | ||||
| import struct | ||||
| 
 | ||||
| from  qtpy import QtCore, QtNetwork | ||||
| # from PyQt5.QtCore import pyqtSignal as Signal | ||||
| from  qtpy.QtCore import Signal | ||||
| 
 | ||||
| from qweechat import config | ||||
| from qweechat.debug import DebugDialog | ||||
| 
 | ||||
| 
 | ||||
| # list of supported hash algorithms on our side | ||||
| # (the hash algorithm will be negotiated with the remote WeeChat) | ||||
| _HASH_ALGOS_LIST = [ | ||||
|     'plain', | ||||
|     'sha256', | ||||
|     'sha512', | ||||
|     'pbkdf2+sha256', | ||||
|     'pbkdf2+sha512', | ||||
| ] | ||||
| _HASH_ALGOS = ':'.join(_HASH_ALGOS_LIST) | ||||
| 
 | ||||
| # handshake with remote WeeChat (before init) | ||||
| _PROTO_HANDSHAKE = f'(handshake) handshake password_hash_algo={_HASH_ALGOS}\n' | ||||
| 
 | ||||
| # initialize with the password (plain text) | ||||
| _PROTO_INIT_PWD = 'init password=%(password)s%(totp)s\n'  # nosec | ||||
| 
 | ||||
| # initialize with the hashed password | ||||
| _PROTO_INIT_HASH = ('init password_hash=' | ||||
|                     '%(algo)s:%(salt)s%(iter)s:%(hash)s%(totp)s\n') | ||||
| 
 | ||||
| _PROTO_SYNC_CMDS = [ | ||||
|     # get buffers | ||||
|     '(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,' | ||||
|     'type,nicklist,title,local_variables', | ||||
|     # get lines | ||||
|     '(listlines) hdata buffer:gui_buffers(*)/own_lines/last_line(-%(lines)d)/' | ||||
|     'data date,displayed,prefix,message', | ||||
|     # get nicklist for all buffers | ||||
|     '(nicklist) nicklist', | ||||
|     # enable synchronization | ||||
|     'sync', | ||||
| ] | ||||
| 
 | ||||
| STATUS_DISCONNECTED = 'disconnected' | ||||
| STATUS_CONNECTING = 'connecting' | ||||
| STATUS_AUTHENTICATING = 'authenticating' | ||||
| STATUS_CONNECTED = 'connected' | ||||
| 
 | ||||
| NETWORK_STATUS = { | ||||
|     STATUS_DISCONNECTED: { | ||||
|         'label': 'Disconnected', | ||||
|         'color': '#aa0000', | ||||
|         'icon': 'dialog-close.png', | ||||
|     }, | ||||
|     STATUS_CONNECTING: { | ||||
|         'label': 'Connecting…', | ||||
|         'color': '#dd5f00', | ||||
|         'icon': 'dialog-warning.png', | ||||
|     }, | ||||
|     STATUS_AUTHENTICATING: { | ||||
|         'label': 'Authenticating…', | ||||
|         'color': '#007fff', | ||||
|         'icon': 'dialog-password.png', | ||||
|     }, | ||||
|     STATUS_CONNECTED: { | ||||
|         'label': 'Connected', | ||||
|         'color': 'green', | ||||
|         'icon': 'dialog-ok-apply.png', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class Network(QtCore.QObject): | ||||
|     """I/O with WeeChat/relay.""" | ||||
| 
 | ||||
|     statusChanged = Signal(str, str) | ||||
|     messageFromWeechat = Signal(QtCore.QByteArray) | ||||
| 
 | ||||
|     def __init__(self, *args): | ||||
|         super().__init__(*args) | ||||
|         self._init_connection() | ||||
|         self.debug_lines = [] | ||||
|         self.debug_dialog = None | ||||
|         self._lines = config.CONFIG_DEFAULT_RELAY_LINES | ||||
|         self._buffer = QtCore.QByteArray() | ||||
|         self._socket = QtNetwork.QSslSocket() | ||||
|         self._socket.connected.connect(self._socket_connected) | ||||
|         self._socket.readyRead.connect(self._socket_read) | ||||
|         self._socket.disconnected.connect(self._socket_disconnected) | ||||
| 
 | ||||
|     def _init_connection(self): | ||||
|         self.status = STATUS_DISCONNECTED | ||||
|         self._hostname = None | ||||
|         self._port = None | ||||
|         self._ssl = None | ||||
|         self._password = None | ||||
|         self._totp = None | ||||
|         self._handshake_received = False | ||||
|         self._handshake_timer = None | ||||
|         self._handshake_timer = False | ||||
|         self._pwd_hash_algo = None | ||||
|         self._pwd_hash_iter = 0 | ||||
|         self._server_nonce = None | ||||
| 
 | ||||
|     def set_status(self, status): | ||||
|         """Set current status.""" | ||||
|         self.status = status | ||||
|         self.statusChanged.emit(status, None) | ||||
| 
 | ||||
|     def pbkdf2(self, hash_name, salt): | ||||
|         """Return hashed password with PBKDF2-HMAC.""" | ||||
|         return hashlib.pbkdf2_hmac( | ||||
|             hash_name, | ||||
|             password=self._password.encode('utf-8'), | ||||
|             salt=salt, | ||||
|             iterations=self._pwd_hash_iter, | ||||
|         ).hex() | ||||
| 
 | ||||
|     def _build_init_command(self): | ||||
|         """Build the init command to send to WeeChat.""" | ||||
|         totp = f',totp={self._totp}' if self._totp else '' | ||||
|         if self._pwd_hash_algo == 'plain': | ||||
|             cmd = _PROTO_INIT_PWD % { | ||||
|                 'password': self._password, | ||||
|                 'totp': totp, | ||||
|             } | ||||
|         else: | ||||
|             client_nonce = secrets.token_bytes(16) | ||||
|             salt = self._server_nonce + client_nonce | ||||
|             pwd_hash = None | ||||
|             iterations = '' | ||||
|             if self._pwd_hash_algo == 'pbkdf2+sha512': | ||||
|                 pwd_hash = self.pbkdf2('sha512', salt) | ||||
|                 iterations = f':{self._pwd_hash_iter}' | ||||
|             elif self._pwd_hash_algo == 'pbkdf2+sha256': | ||||
|                 pwd_hash = self.pbkdf2('sha256', salt) | ||||
|                 iterations = f':{self._pwd_hash_iter}' | ||||
|             elif self._pwd_hash_algo == 'sha512': | ||||
|                 pwd = salt + self._password.encode('utf-8') | ||||
|                 pwd_hash = hashlib.sha512(pwd).hexdigest() | ||||
|             elif self._pwd_hash_algo == 'sha256': | ||||
|                 pwd = salt + self._password.encode('utf-8') | ||||
|                 pwd_hash = hashlib.sha256(pwd).hexdigest() | ||||
|             if not pwd_hash: | ||||
|                 return None | ||||
|             cmd = _PROTO_INIT_HASH % { | ||||
|                 'algo': self._pwd_hash_algo, | ||||
|                 'salt': bytearray(salt).hex(), | ||||
|                 'iter': iterations, | ||||
|                 'hash': pwd_hash, | ||||
|                 'totp': totp, | ||||
|             } | ||||
|         return cmd | ||||
| 
 | ||||
|     def _build_sync_command(self): | ||||
|         """Build the sync commands to send to WeeChat.""" | ||||
|         cmd = '\n'.join(_PROTO_SYNC_CMDS) + '\n' | ||||
|         return cmd % {'lines': self._lines} | ||||
| 
 | ||||
|     def handshake_timer_expired(self): | ||||
|         if self.status == STATUS_AUTHENTICATING: | ||||
|             self._pwd_hash_algo = 'plain' | ||||
|             self.send_to_weechat(self._build_init_command()) | ||||
|             self.sync_weechat() | ||||
|             self.set_status(STATUS_CONNECTED) | ||||
| 
 | ||||
|     def _socket_connected(self): | ||||
|         """Slot: socket connected.""" | ||||
|         self.set_status(STATUS_AUTHENTICATING) | ||||
|         self.send_to_weechat(_PROTO_HANDSHAKE) | ||||
|         self._handshake_timer = QtCore.QTimer() | ||||
|         self._handshake_timer.setSingleShot(True) | ||||
|         self._handshake_timer.setInterval(2000) | ||||
|         self._handshake_timer.timeout.connect(self.handshake_timer_expired) | ||||
|         self._handshake_timer.start() | ||||
| 
 | ||||
|     def _socket_read(self): | ||||
|         """Slot: data available on socket.""" | ||||
|         data = self._socket.readAll() | ||||
|         self._buffer.append(data) | ||||
|         while len(self._buffer) >= 4: | ||||
|             remainder = None | ||||
|             length = struct.unpack('>i', self._buffer[0:4].data())[0] | ||||
|             if len(self._buffer) < length: | ||||
|                 # partial message, just wait for end of message | ||||
|                 break | ||||
|             # more than one message? | ||||
|             if length < len(self._buffer): | ||||
|                 # save beginning of another message | ||||
|                 remainder = self._buffer[length:] | ||||
|                 self._buffer = self._buffer[0:length] | ||||
|             self.messageFromWeechat.emit(self._buffer) | ||||
|             if not self.is_connected(): | ||||
|                 return | ||||
|             self._buffer.clear() | ||||
|             if remainder: | ||||
|                 self._buffer.append(remainder) | ||||
| 
 | ||||
|     def _socket_disconnected(self): | ||||
|         """Slot: socket disconnected.""" | ||||
|         if self._handshake_timer: | ||||
|             self._handshake_timer.stop() | ||||
|         self._init_connection() | ||||
|         self.set_status(STATUS_DISCONNECTED) | ||||
| 
 | ||||
|     def is_connected(self): | ||||
|         """Return True if the socket is connected, False otherwise.""" | ||||
|         return self.is_state(at='ConnectedState') | ||||
| 
 | ||||
|     def is_state(self, at='ConnectedState'): | ||||
|         """Return True if the socket is connected, False otherwise.""" | ||||
|         if hasattr(QtNetwork.QAbstractSocket, 'ConnectedState'): | ||||
|             if self._socket.state() == getattr(QtNetwork.QAbstractSocket, at): | ||||
|                 return True | ||||
|             return False | ||||
|         if hasattr(QtNetwork.QAbstractSocket, 'SocketState'): | ||||
|             if self._socket.state() == getattr(QtNetwork.QAbstractSocket.SocketState, at): | ||||
|                 return True | ||||
|             return False | ||||
|         return False | ||||
| 
 | ||||
|     def is_ssl(self): | ||||
|         """Return True if SSL is used, False otherwise.""" | ||||
|         return self._ssl | ||||
| 
 | ||||
|     def connect_weechat(self, hostname, port, ssl, password, totp, lines): | ||||
|         """Connect to WeeChat.""" | ||||
|         self._hostname = hostname | ||||
|         try: | ||||
|             self._port = int(port) | ||||
|         except ValueError: | ||||
|             self._port = 0 | ||||
|         self._ssl = ssl | ||||
|         self._password = password | ||||
|         self._totp = totp | ||||
|         try: | ||||
|             self._lines = int(lines) | ||||
|         except ValueError: | ||||
|             self._lines = config.CONFIG_DEFAULT_RELAY_LINES | ||||
|         # AttributeError: type object 'QAbstractSocket' has no attribute 'ConnectedState' | ||||
|         if self.is_connected(): | ||||
|             return | ||||
|         if not self.is_state('UnconnectedState'): | ||||
|             self._socket.abort() | ||||
|         if self._ssl: | ||||
|             self._socket.ignoreSslErrors() | ||||
|             self._socket.connectToHostEncrypted(self._hostname, self._port) | ||||
|         else: | ||||
|             self._socket.connectToHost(self._hostname, self._port) | ||||
|         self.set_status(STATUS_CONNECTING) | ||||
| 
 | ||||
|     def disconnect_weechat(self): | ||||
|         """Disconnect from WeeChat.""" | ||||
|         if self.is_state('UnconnectedState'): | ||||
|             self.set_status(STATUS_DISCONNECTED) | ||||
|             return | ||||
|         if self.is_state('ConnectedState'): | ||||
|             self.send_to_weechat('quit\n') | ||||
|             self._socket.waitForBytesWritten(1000) | ||||
|         else: | ||||
|             self.set_status(STATUS_DISCONNECTED) | ||||
|         self._socket.abort() | ||||
| 
 | ||||
|     def send_to_weechat(self, message): | ||||
|         """Send a message to WeeChat.""" | ||||
|         self.debug_print(0, '<==', message, forcecolor='#AA0000') | ||||
|         self._socket.write(message.encode('utf-8')) | ||||
| 
 | ||||
|     def init_with_handshake(self, response): | ||||
|         """Initialize with WeeChat using the handshake response.""" | ||||
|         self._pwd_hash_algo = response['password_hash_algo'] | ||||
|         self._pwd_hash_iter = int(response['password_hash_iterations']) | ||||
|         self._server_nonce = bytearray.fromhex(response['nonce']) | ||||
|         if self._pwd_hash_algo: | ||||
|             cmd = self._build_init_command() | ||||
|             if cmd: | ||||
|                 self.send_to_weechat(cmd) | ||||
|                 self.sync_weechat() | ||||
|                 self.set_status(STATUS_CONNECTED) | ||||
|                 return | ||||
|         # failed to initialize: disconnect | ||||
|         self.disconnect_weechat() | ||||
| 
 | ||||
|     def desync_weechat(self): | ||||
|         """Desynchronize from WeeChat.""" | ||||
|         self.send_to_weechat('desync\n') | ||||
| 
 | ||||
|     def sync_weechat(self): | ||||
|         """Synchronize with WeeChat.""" | ||||
|         self.send_to_weechat(self._build_sync_command()) | ||||
| 
 | ||||
|     def status_label(self, status): | ||||
|         """Return the label for a given status.""" | ||||
|         return NETWORK_STATUS.get(status, {}).get('label', '') | ||||
| 
 | ||||
|     def status_color(self, status): | ||||
|         """Return the color for a given status.""" | ||||
|         return NETWORK_STATUS.get(status, {}).get('color', 'black') | ||||
| 
 | ||||
|     def status_icon(self, status): | ||||
|         """Return the name of icon for a given status.""" | ||||
|         return NETWORK_STATUS.get(status, {}).get('icon', '') | ||||
| 
 | ||||
|     def get_options(self): | ||||
|         """Get connection options.""" | ||||
|         return { | ||||
|             'hostname': self._hostname, | ||||
|             'port': self._port, | ||||
|             'ssl': 'on' if self._ssl else 'off', | ||||
|             'password': self._password, | ||||
|             'lines': str(self._lines), | ||||
|         } | ||||
| 
 | ||||
|     def debug_print(self, *args, **kwargs): | ||||
|         """Display a debug message.""" | ||||
|         self.debug_lines.append((args, kwargs)) | ||||
|         if self.debug_dialog: | ||||
|             self.debug_dialog.chat.display(*args, **kwargs) | ||||
| 
 | ||||
|     def _debug_dialog_closed(self, result): | ||||
|         """Called when debug dialog is closed.""" | ||||
|         self.debug_dialog = None | ||||
| 
 | ||||
|     def debug_input_text_sent(self, text): | ||||
|         """Send debug buffer input to WeeChat.""" | ||||
|         if self.network.is_connected(): | ||||
|             text = str(text) | ||||
|             pos = text.find(')') | ||||
|             if text.startswith('(') and pos >= 0: | ||||
|                 text = '(debug_%s)%s' % (text[1:pos], text[pos+1:]) | ||||
|             else: | ||||
|                 text = '(debug) %s' % text | ||||
|             self.network.debug_print(0, '<==', text, forcecolor='#AA0000') | ||||
|             self.network.send_to_weechat(text + '\n') | ||||
| 
 | ||||
|     def open_debug_dialog(self): | ||||
|         """Open a dialog with debug messages.""" | ||||
|         if not self.debug_dialog: | ||||
|             self.debug_dialog = DebugDialog() | ||||
|             self.debug_dialog.input.textSent.connect( | ||||
|                 self.debug_input_text_sent) | ||||
|             self.debug_dialog.finished.connect(self._debug_dialog_closed) | ||||
|             self.debug_dialog.display_lines(self.debug_lines) | ||||
|             self.debug_dialog.chat.scroll_bottom() | ||||
							
								
								
									
										509
									
								
								src/qweechat/preferences.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,509 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # preferences.py - preferences dialog box | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Preferences dialog box.""" | ||||
| 
 | ||||
| from   qtpy import QtCore, QtWidgets, QtGui | ||||
| qicon_from_theme = QtGui.QIcon.fromTheme | ||||
| 
 | ||||
| class PreferencesDialog(QtWidgets.QDialog): | ||||
|     """Preferences dialog.""" | ||||
| 
 | ||||
|     def __init__(self, *args): | ||||
|         QtWidgets.QDialog.__init__(*(self,) + args) | ||||
|         self.setModal(True) | ||||
|         self.setWindowTitle('Preferences') | ||||
| 
 | ||||
|         close_button = QtWidgets.QPushButton('Close') | ||||
|         close_button.pressed.connect(self.close) | ||||
| 
 | ||||
|         hbox = QtWidgets.QHBoxLayout() | ||||
|         hbox.addStretch(1) | ||||
|         hbox.addWidget(close_button) | ||||
|         hbox.addStretch(1) | ||||
| 
 | ||||
|         vbox = QtWidgets.QVBoxLayout() | ||||
| 
 | ||||
|         label = QtWidgets.QLabel('Not yet implemented!') | ||||
|         label.setAlignment(QtCore.Qt.AlignHCenter) | ||||
|         vbox.addWidget(label) | ||||
| 
 | ||||
|         label = QtWidgets.QLabel('') | ||||
|         label.setAlignment(QtCore.Qt.AlignHCenter) | ||||
|         vbox.addWidget(label) | ||||
| 
 | ||||
|         vbox.addLayout(hbox) | ||||
| 
 | ||||
|         self.setLayout(vbox) | ||||
|         self.show() | ||||
|          | ||||
| # prepare for https://github.com/weechat/qweechat/pull/8 | ||||
| 
 | ||||
|     def _pane_switch(self, item): | ||||
|         """Switch the visible preference pane.""" | ||||
|         index = self.list_panes.indexOfTopLevelItem(item) | ||||
|         if index >= 0: | ||||
|             self.stacked_panes.setCurrentIndex(index) | ||||
| 
 | ||||
|     def _save_and_close(self): | ||||
|         for widget in (self.stacked_panes.widget(i) | ||||
|                        for i in range(self.stacked_panes.count())): | ||||
|             for key, field in widget.fields.items(): | ||||
|                 if isinstance(field, QtWidgets.QComboBox): | ||||
|                     text = field.itemText(field.currentIndex()) | ||||
|                     data = field.itemData(field.currentIndex()) | ||||
|                     text = data if data else text | ||||
|                 elif isinstance(field, QtWidgets.QCheckBox): | ||||
|                     text = "on" if field.isChecked() else "off" | ||||
|                 else: | ||||
|                     text = field.text() | ||||
|                 self.config.set(widget.section_name, key, str(text)) | ||||
|         write(self.config) | ||||
|         self.parent.apply_preferences() | ||||
|         self.close() | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesNotificationBlock(QtWidgets.QVBoxLayout): | ||||
|     """Display notification settings with drill down to configure.""" | ||||
|     def __init__(self, pane, *args): | ||||
|         QtWidgets.QVBoxLayout.__init__(*(self,) + args) | ||||
|         self.section = "notifications" | ||||
|         self.config = QtWidgets.QApplication.instance().config | ||||
|         self.pane = pane | ||||
|         self.stack = QtWidgets.QStackedWidget() | ||||
| 
 | ||||
|         self.table = QtWidgets.QTableWidget() | ||||
|         fg_color = self.table.palette().text().color().name() | ||||
|         self.action_labels = { | ||||
|             "sound": "Play a sound", | ||||
|             "message": "Show a message in a popup", | ||||
|             "file": "Log to a file", | ||||
|             "taskbar": "Mark taskbar entry", | ||||
|             "tray": "Mark systray/indicator", | ||||
|             "command": "Run a command"} | ||||
|         self.action_icons = { | ||||
|             "sound": qicon_from_theme("media-playback-start"), | ||||
|             "message": qicon_from_theme("dialog-information"), | ||||
|             "file": qicon_from_theme("document-export"), | ||||
|             "taskbar": qicon_from_theme("weechat"), | ||||
|             "tray": utils.qicon_tint("ic_hot", fg_color), | ||||
|             "command": qicon_from_theme("system-run")} | ||||
|         self.icon_widget_qss = "padding:0;min-height:10px;min-width:16px;" | ||||
|         self.table.resizeColumnsToContents() | ||||
|         self.table.setColumnCount(2) | ||||
|         self.table.resizeRowsToContents() | ||||
|         self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) | ||||
|         self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) | ||||
|         self.table.setHorizontalHeaderLabels(["State", "Type"]) | ||||
|         self.table.horizontalHeader().setStretchLastSection(True) | ||||
|         self.table.horizontalHeader().setHighlightSections(False) | ||||
|         self.table.verticalHeader().setVisible(False) | ||||
|         self.table.setShowGrid(False) | ||||
|         self.table.itemSelectionChanged.connect(self._table_row_changed) | ||||
| 
 | ||||
|         self.buftypes = {} | ||||
|         for key, value in CONFIG_DEFAULT_NOTIFICATION_OPTIONS: | ||||
|             buftype, optkey = key.split(".") | ||||
|             if buftype not in self.buftypes: | ||||
|                 self.buftypes[buftype] = {} | ||||
|             self.buftypes[buftype][optkey] = self.config.get(self.section, key) | ||||
|         for buftype, optkey in self.buftypes.items(): | ||||
|             self._insert_type(buftype) | ||||
|         self.update_icons() | ||||
|         self.resize_table() | ||||
|         self.addWidget(self.table) | ||||
|         self.addWidget(self.stack) | ||||
|         self.table.selectRow(0) | ||||
| 
 | ||||
|     def _insert_type(self, buftype): | ||||
|         row = self.table.rowCount() | ||||
|         self.table.insertRow(row) | ||||
|         buftype_item = QtWidgets.QTableWidgetItem(buftype) | ||||
|         buftype_item.setTextAlignment(QtCore.Qt.AlignCenter) | ||||
|         self.table.setItem(row, 0, QtWidgets.QTableWidgetItem()) | ||||
|         self.table.setItem(row, 1, buftype_item) | ||||
|         subgrid = QtWidgets.QGridLayout() | ||||
|         subgrid.setColumnStretch(2, 1) | ||||
|         subgrid.setSpacing(10) | ||||
| 
 | ||||
|         for key, qicon in self.action_icons.items(): | ||||
|             value = self.buftypes[buftype][key] | ||||
|             line = subgrid.rowCount() | ||||
|             label = IconTextLabel(self.action_labels[key], qicon, 16) | ||||
| 
 | ||||
|             checkbox = QtWidgets.QCheckBox() | ||||
|             span = 1 | ||||
|             edit = None | ||||
|             if key in ("message", "taskbar", "tray"): | ||||
|                 checkbox.setChecked(value == "on") | ||||
|                 span = 2 | ||||
|             elif key == "sound": | ||||
|                 edit = PreferencesFileEdit( | ||||
|                     checkbox=checkbox, caption='Select a sound file', | ||||
|                     filter='Audio Files (*.wav *.mp3 *.ogg)') | ||||
|             elif key == "file": | ||||
|                 edit = PreferencesFileEdit(checkbox=checkbox, mode="save") | ||||
|             else: | ||||
|                 edit = PreferencesFileEdit(checkbox=checkbox) | ||||
|             if edit: | ||||
|                 edit.insert(value) | ||||
|                 subgrid.addWidget(edit, line, 2) | ||||
|             else: | ||||
|                 edit = checkbox | ||||
|             subgrid.addWidget(label, line, 1, 1, span) | ||||
|             subgrid.addWidget(checkbox, line, 0) | ||||
|             self.pane.fields[buftype + "." + key] = edit | ||||
|         subpane = QtWidgets.QWidget() | ||||
|         subpane.setLayout(subgrid) | ||||
|         subpane.setMaximumHeight(subgrid.totalMinimumSize().height()) | ||||
|         self.stack.addWidget(subpane) | ||||
| 
 | ||||
|     def resize_table(self): | ||||
|         """Fit the table height to contents.""" | ||||
|         height = self.table.horizontalHeader().height() | ||||
|         height = height * (self.table.rowCount() + 1) | ||||
|         height += self.table.contentsMargins().top() | ||||
|         height += self.table.contentsMargins().bottom() | ||||
|         self.table.setMaximumHeight(height) | ||||
|         self.table.setMinimumHeight(height) | ||||
|         self.table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) | ||||
| 
 | ||||
|     def update_icons(self): | ||||
|         """Draw the correct icons in the left col.""" | ||||
|         for i in range(self.table.rowCount()): | ||||
|             hbox = QtWidgets.QHBoxLayout() | ||||
|             iconset = QtWidgets.QWidget() | ||||
|             buftype = self.table.item(i, 1).text() | ||||
|             for key, qicon in self.action_icons.items(): | ||||
|                 field = self.pane.fields[buftype + "." + key] | ||||
|                 if isinstance(field, QtWidgets.QCheckBox): | ||||
|                     val = "on" if field.isChecked() else "off" | ||||
|                 else: | ||||
|                     val = field.text() | ||||
|                 iconbtn = QtWidgets.QPushButton() | ||||
|                 iconbtn.setContentsMargins(0, 0, 0, 0) | ||||
|                 iconbtn.setFlat(True) | ||||
|                 iconbtn.setFocusPolicy(QtCore.Qt.NoFocus) | ||||
|                 if val and val != "off": | ||||
|                     iconbtn.setIcon(qicon) | ||||
|                 iconbtn.setStyleSheet(self.icon_widget_qss) | ||||
|                 iconbtn.setToolTip(key) | ||||
|                 iconbtn.clicked.connect(lambda i=i: self.table.selectRow(i)) | ||||
|                 hbox.addWidget(iconbtn) | ||||
|             iconset.setLayout(hbox) | ||||
|             self.table.setCellWidget(i, 0, iconset) | ||||
| 
 | ||||
|     def _table_row_changed(self): | ||||
|         row = self.table.selectionModel().selectedRows()[0].row() | ||||
|         self.stack.setCurrentIndex(row) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesTreeWidget(QtWidgets.QTreeWidget): | ||||
|     """Widget with tree list of preferences.""" | ||||
|     def __init__(self, header_label, *args): | ||||
|         QtWidgets.QTreeWidget.__init__(*(self,) + args) | ||||
|         self.setHeaderLabel(header_label) | ||||
|         self.setRootIsDecorated(False) | ||||
|         self.setMaximumWidth(180) | ||||
|         self.setTextElideMode(QtCore.Qt.ElideNone) | ||||
|         self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) | ||||
|         self.setFocusPolicy(QtCore.Qt.NoFocus) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesSliderEdit(QtWidgets.QSlider): | ||||
|     """Percentage slider.""" | ||||
|     def __init__(self, *args): | ||||
|         QtWidgets.QSlider.__init__(*(self,) + args) | ||||
|         self.setMinimum(0) | ||||
|         self.setMaximum(100) | ||||
|         self.setTickPosition(QtWidgets.QSlider.TicksBelow) | ||||
|         self.setTickInterval(5) | ||||
| 
 | ||||
|     def insert(self, percent): | ||||
|         self.setValue(int(percent[:-1])) | ||||
| 
 | ||||
|     def text(self): | ||||
|         return str(self.value()) + "%" | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesColorEdit(QtWidgets.QPushButton): | ||||
|     """Simple color square that changes based on the color selected.""" | ||||
|     def __init__(self, *args): | ||||
|         QtWidgets.QPushButton.__init__(*(self,) + args) | ||||
|         self.color = "#000000" | ||||
|         self.clicked.connect(self._color_picker) | ||||
|         # Some of the configured colors use a astrisk prefix. | ||||
|         # Toggle this on right click. | ||||
|         self.star = False | ||||
|         self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | ||||
|         self.customContextMenuRequested.connect(self._color_star) | ||||
| 
 | ||||
|     def insert(self, color): | ||||
|         """Insert the desired color for the widget.""" | ||||
|         if color[:1] == "*": | ||||
|             self.star = True | ||||
|             color = color[1:] | ||||
|         self.setText("*" if self.star else "") | ||||
|         self.color = color | ||||
|         self.setStyleSheet("background-color: " + color) | ||||
| 
 | ||||
|     def text(self): | ||||
|         """Returns the hex value of the color.""" | ||||
|         return ("*" if self.star else "") + self.color | ||||
| 
 | ||||
|     def _color_picker(self): | ||||
|         color = QtWidgets.QColorDialog.getColor(self.color) | ||||
|         self.insert(color.name()) | ||||
| 
 | ||||
|     def _color_star(self): | ||||
|         self.star = not self.star | ||||
|         self.insert(self.text()) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesFontEdit(QtWidgets.QWidget): | ||||
|     """Font entry and selection.""" | ||||
|     def __init__(self, *args): | ||||
|         QtWidgets.QWidget.__init__(*(self,) + args) | ||||
|         layout = QtWidgets.QHBoxLayout() | ||||
|         self.checkbox = QtWidgets.QCheckBox() | ||||
|         self.edit = QtWidgets.QLineEdit() | ||||
|         self.font = "" | ||||
|         self.qfont = None | ||||
|         self.button = QtWidgets.QPushButton("C&hoose") | ||||
|         self.button.clicked.connect(self._font_picker) | ||||
|         self.checkbox.toggled.connect( | ||||
|             lambda: self._checkbox_toggled(self.checkbox)) | ||||
|         layout.addWidget(self.checkbox) | ||||
|         layout.addWidget(self.edit) | ||||
|         layout.addWidget(self.button) | ||||
|         layout.setContentsMargins(0, 0, 0, 0) | ||||
|         self.setLayout(layout) | ||||
| 
 | ||||
|     def insert(self, font_str): | ||||
|         """Insert the font described by the string.""" | ||||
|         self.font = font_str | ||||
|         self.edit.insert(font_str) | ||||
|         if font_str: | ||||
|             self.qfont = utils.Font.str_to_qfont(font_str) | ||||
|             self.edit.setFont(self.qfont) | ||||
|             self.checkbox.setChecked(True) | ||||
|             self._checkbox_toggled(self.checkbox) | ||||
|         else: | ||||
|             self.checkbox.setChecked(False) | ||||
|             self.qfont = None | ||||
|             self._checkbox_toggled(self.checkbox) | ||||
| 
 | ||||
|     def text(self): | ||||
|         """Returns the human readable font string.""" | ||||
|         return self.font | ||||
| 
 | ||||
|     def _font_picker(self): | ||||
|         font, ok = QtWidgets.QFontDialog.getFont(self.qfont) | ||||
|         if ok: | ||||
|             self.insert(utils.Font.qfont_to_str(font)) | ||||
| 
 | ||||
|     def _checkbox_toggled(self, button): | ||||
|         if button.isChecked() is False and not self.font == "": | ||||
|             self.insert("") | ||||
|         self.edit.setEnabled(button.isChecked()) | ||||
|         self.button.setEnabled(button.isChecked()) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesFileEdit(QtWidgets.QWidget): | ||||
|     """File entry and selection.""" | ||||
|     def __init__(self, checkbox=None, caption="Select a file", filter=None, | ||||
|                  mode="open", *args): | ||||
|         QtWidgets.QWidget.__init__(*(self,) + args) | ||||
|         layout = QtWidgets.QHBoxLayout() | ||||
|         self.caption = caption | ||||
|         self.filter = filter | ||||
|         self.edit = QtWidgets.QLineEdit() | ||||
|         self.file_str = "" | ||||
|         self.mode = mode | ||||
|         self.button = QtWidgets.QPushButton("B&rowse") | ||||
|         self.button.clicked.connect(self._file_picker) | ||||
|         if checkbox: | ||||
|             self.checkbox = checkbox | ||||
|         else: | ||||
|             self.checkbox = QtWidgets.QCheckBox() | ||||
|             layout.addWidget(self.checkbox) | ||||
|         self.checkbox.toggled.connect( | ||||
|             lambda: self._checkbox_toggled(self.checkbox)) | ||||
|         layout.addWidget(self.edit) | ||||
|         layout.addWidget(self.button) | ||||
|         layout.setContentsMargins(0, 0, 0, 0) | ||||
|         self.setLayout(layout) | ||||
| 
 | ||||
|     def insert(self, file_str): | ||||
|         """Insert the file.""" | ||||
|         self.file_str = file_str | ||||
|         self.edit.insert(file_str) | ||||
|         if file_str: | ||||
|             self.checkbox.setChecked(True) | ||||
|             self._checkbox_toggled(self.checkbox) | ||||
|         else: | ||||
|             self.checkbox.setChecked(False) | ||||
|             self._checkbox_toggled(self.checkbox) | ||||
| 
 | ||||
|     def text(self): | ||||
|         """Returns the human readable font string.""" | ||||
|         return self.file_str | ||||
| 
 | ||||
|     def _file_picker(self): | ||||
|         path = "" | ||||
|         if self.mode == "save": | ||||
|             fn = QtWidgets.QFileDialog.getSaveFileName | ||||
|         else: | ||||
|             fn = QtWidgets.QFileDialog.getOpenFileName | ||||
|         filename, fil = fn(self, self.caption, path, self.filter, self.filter) | ||||
|         if filename: | ||||
|             self.insert(filename) | ||||
| 
 | ||||
|     def _checkbox_toggled(self, button): | ||||
|         if button.isChecked() is False and not self.file_str == "": | ||||
|             self.insert("") | ||||
|         self.edit.setEnabled(button.isChecked()) | ||||
|         self.button.setEnabled(button.isChecked()) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesPaneWidget(QtWidgets.QWidget): | ||||
|     """ | ||||
|     Widget with (from top to bottom): | ||||
|     title, chat + nicklist (optional) + prompt/input. | ||||
|     """ | ||||
| 
 | ||||
|     disabled_fields = ["show_hostnames", "hide_nick_changes", | ||||
|                        "hide_join_and_part"] | ||||
| 
 | ||||
|     def __init__(self, section, section_name): | ||||
|         QtWidgets.QWidget.__init__(self) | ||||
|         self.grid = QtWidgets.QGridLayout() | ||||
|         self.grid.setAlignment(QtCore.Qt.AlignTop) | ||||
|         self.section = section | ||||
|         self.section_name = section_name | ||||
|         self.fields = {} | ||||
|         self.setLayout(self.grid) | ||||
|         self.grid.setColumnStretch(2, 1) | ||||
|         self.grid.setSpacing(10) | ||||
|         self.int_validator = QtWidgets.QIntValidator(0, 2147483647, self) | ||||
|         toolbar_icons = [ | ||||
|              ('ToolButtonFollowStyle', 'Default'), | ||||
|              ('ToolButtonIconOnly', 'Icon Only'), | ||||
|              ('ToolButtonTextOnly', 'Text Only'), | ||||
|              ('ToolButtonTextBesideIcon', 'Text Alongside Icons'), | ||||
|              ('ToolButtonTextUnderIcon', 'Text Under Icons')] | ||||
|         tray_options = [ | ||||
|             ('always', 'Always'), | ||||
|             ('unread', 'On Unread Messages'), | ||||
|             ('never', 'Never'), | ||||
|         ] | ||||
|         list_positions = [ | ||||
|             ('left', 'Left'), | ||||
|             ('right', 'Right'), | ||||
|         ] | ||||
|         sort_options = ['A-Z Ranked', 'A-Z', 'Z-A Ranked', 'Z-A'] | ||||
|         focus_opts = ["requested", "always", "never"] | ||||
|         self.comboboxes = {"style": QtWidgets.QStyleFactory.keys(), | ||||
|                            "position": list_positions, | ||||
|                            "toolbar_icons": toolbar_icons, | ||||
|                            "focus_new_tabs": focus_opts, | ||||
|                            "tray_icon": tray_options, | ||||
|                            "sort": sort_options} | ||||
| 
 | ||||
|     def addItem(self, key, value, default): | ||||
|         """Add a key-value pair.""" | ||||
|         line = len(self.fields) | ||||
|         name = key.split(".")[-1:][0].capitalize().replace("_", " ") | ||||
|         label = QtWidgets.QLabel(name) | ||||
|         start = 0 | ||||
| 
 | ||||
|         if self.section == "color": | ||||
|             start = 2 * (line % 2) | ||||
|             line = line // 2 | ||||
|             edit = PreferencesColorEdit() | ||||
|             edit.setFixedWidth(edit.sizeHint().height()) | ||||
|             edit.insert(value) | ||||
|             label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) | ||||
|         elif key == "custom_stylesheet": | ||||
|             edit = PreferencesFileEdit(caption='Select QStyleSheet File', | ||||
|                                        filter='*.qss') | ||||
|             edit.insert(value) | ||||
|         elif name.lower()[-5:] == "sound": | ||||
|             edit = PreferencesFileEdit( | ||||
|                 caption='Select a sound file', | ||||
|                 filter='Audio Files (*.wav *.mp3 *.ogg)') | ||||
|             edit.insert(value) | ||||
|         elif name.lower()[-4:] == "font": | ||||
|             edit = PreferencesFontEdit() | ||||
|             edit.setFixedWidth(200) | ||||
|             edit.insert(value) | ||||
|         elif key in self.comboboxes.keys(): | ||||
|             edit = QtWidgets.QComboBox() | ||||
|             if len(self.comboboxes[key][0]) == 2: | ||||
|                 for keyvalue in self.comboboxes[key]: | ||||
|                     edit.addItem(keyvalue[1], keyvalue[0]) | ||||
|                 # if self.section == "nicks" and key == "position": | ||||
|                 #     edit.addItem("below", "Below Buffer List") | ||||
|                 #     edit.addItem("above", "Above Buffer List") | ||||
|                 edit.setCurrentIndex(edit.findData(value)) | ||||
|             else: | ||||
|                 edit.addItems(self.comboboxes[key]) | ||||
|                 edit.setCurrentIndex(edit.findText(value)) | ||||
|             edit.setFixedWidth(200) | ||||
|         elif default in ["on", "off"]: | ||||
|             edit = QtWidgets.QCheckBox() | ||||
|             edit.setChecked(value == "on") | ||||
|         elif default[-1:] == "%": | ||||
|             edit = PreferencesSliderEdit(QtCore.Qt.Horizontal) | ||||
|             edit.setFixedWidth(200) | ||||
|             edit.insert(value) | ||||
|         else: | ||||
|             edit = QtWidgets.QLineEdit() | ||||
|             edit.setFixedWidth(200) | ||||
|             edit.insert(value) | ||||
|             if default.isdigit() or key == "port": | ||||
|                 edit.setValidator(self.int_validator) | ||||
|         if key == 'password': | ||||
|             edit.setEchoMode(QtWidgets.QLineEdit.Password) | ||||
|         if key in self.disabled_fields: | ||||
|             edit.setDisabled(True) | ||||
|         self.grid.addWidget(label, line, start + 0) | ||||
|         self.grid.addWidget(edit, line, start + 1) | ||||
| 
 | ||||
|         self.fields[key] = edit | ||||
| 
 | ||||
| 
 | ||||
| class IconTextLabel(QtWidgets.QWidget): | ||||
|     """An icon next to text.""" | ||||
|     def __init__(self, text=None, icon=None, extent=None): | ||||
|         QtWidgets.QWidget.__init__(self) | ||||
|         text_label = QtWidgets.QLabel(text) | ||||
|         if not extent: | ||||
|             extent = text_label.height() | ||||
|         icon_label = QtWidgets.QLabel() | ||||
|         pixmap = icon.pixmap(extent, QtWidgets.QIcon.Normal, QtWidgets.QIcon.On) | ||||
|         icon_label.setPixmap(pixmap) | ||||
|         label_layout = QtWidgets.QHBoxLayout() | ||||
|         label_layout.addWidget(icon_label) | ||||
|         label_layout.addWidget(text_label) | ||||
|         label_layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) | ||||
|         self.setLayout(label_layout) | ||||
							
								
								
									
										555
									
								
								src/qweechat/preferences.py.new
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,555 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # preferences.py - preferences dialog box | ||||
| # | ||||
| # Copyright (C) 2016 Ricky Brent <ricky@rickybrent.com> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| from qweechat.config import (CONFIG_DEFAULT_SECTIONS, | ||||
|                              CONFIG_DEFAULT_NOTIFICATION_OPTIONS, | ||||
|                              CONFIG_DEFAULT_OPTIONS, write) | ||||
| import utils | ||||
| 
 | ||||
| from  qtpy import QtCore, QtGui | ||||
| qicon_from_theme = QtGui.QIcon.fromTheme | ||||
| 
 | ||||
| class PreferencesDialog(QtGui.QDialog): | ||||
|     """Preferences dialog.""" | ||||
| 
 | ||||
|     custom_sections = { | ||||
|         "look": "Look", | ||||
|         "input": "Input Box", | ||||
|         "nicks": "Nick List", | ||||
|         "buffers": "Buffer List", | ||||
|         "buffer_flags": False, | ||||
|         "notifications": "Notifications", | ||||
|         "color": "Colors", | ||||
|         "relay": "Relay/Connection" | ||||
|     } | ||||
| 
 | ||||
|     def __init__(self, name, parent, *args): | ||||
|         QtGui.QDialog.__init__(*(self,) + args) | ||||
|         self.setModal(True) | ||||
|         self.setWindowTitle(name) | ||||
|         self.parent = parent | ||||
|         self.config = parent.config | ||||
|         self.stacked_panes = QtGui.QStackedWidget() | ||||
|         self.list_panes = PreferencesTreeWidget("Settings") | ||||
| 
 | ||||
|         splitter = QtGui.QSplitter() | ||||
|         splitter.addWidget(self.list_panes) | ||||
|         splitter.addWidget(self.stacked_panes) | ||||
| 
 | ||||
|         # Follow same order as defaults: | ||||
|         section_panes = {} | ||||
|         for section in CONFIG_DEFAULT_SECTIONS: | ||||
|             item = QtGui.QTreeWidgetItem(section) | ||||
|             name = section | ||||
|             item.setText(0, section.title()) | ||||
|             if section in self.custom_sections: | ||||
|                 if not self.custom_sections[section]: | ||||
|                     continue | ||||
|                 item.setText(0, self.custom_sections[section]) | ||||
|             section_panes[section] = PreferencesPaneWidget(section, name) | ||||
|             self.list_panes.addTopLevelItem(item) | ||||
|             self.stacked_panes.addWidget(section_panes[section]) | ||||
| 
 | ||||
|         for setting, default in CONFIG_DEFAULT_OPTIONS: | ||||
|             section_key = setting.split(".") | ||||
|             section = section_key[0] | ||||
|             key = ".".join(section_key[1:]) | ||||
|             section_panes[section].addItem( | ||||
|                 key, self.config.get(section, key), default) | ||||
|         for key, value in self.config.items("color"): | ||||
|             section_panes["color"].addItem(key, value, False) | ||||
|         notification_field_count = len(section_panes["notifications"].fields) | ||||
|         notification = PreferencesNotificationBlock( | ||||
|             section_panes["notifications"]) | ||||
|         section_panes["notifications"].grid.addLayout( | ||||
|             notification, notification_field_count, 0, 1, -1) | ||||
| 
 | ||||
|         self.list_panes.currentItemChanged.connect(self._pane_switch) | ||||
|         self.list_panes.setCurrentItem(self.list_panes.topLevelItem(0)) | ||||
| 
 | ||||
|         hbox = QtGui.QHBoxLayout() | ||||
|         self.dialog_buttons = QtGui.QDialogButtonBox() | ||||
|         self.dialog_buttons.setStandardButtons( | ||||
|             QtGui.QDialogButtonBox.Save | QtGui.QDialogButtonBox.Cancel) | ||||
|         self.dialog_buttons.rejected.connect(self.close) | ||||
|         self.dialog_buttons.accepted.connect(self._save_and_close) | ||||
| 
 | ||||
|         hbox.addStretch(1) | ||||
|         hbox.addWidget(self.dialog_buttons) | ||||
|         hbox.addStretch(1) | ||||
| 
 | ||||
|         vbox = QtGui.QVBoxLayout() | ||||
|         vbox.addWidget(splitter) | ||||
|         vbox.addLayout(hbox) | ||||
| 
 | ||||
|         self.setLayout(vbox) | ||||
|         self.show() | ||||
| 
 | ||||
|     def _pane_switch(self, item): | ||||
|         """Switch the visible preference pane.""" | ||||
|         index = self.list_panes.indexOfTopLevelItem(item) | ||||
|         if index >= 0: | ||||
|             self.stacked_panes.setCurrentIndex(index) | ||||
| 
 | ||||
|     def _save_and_close(self): | ||||
|         for widget in (self.stacked_panes.widget(i) | ||||
|                        for i in range(self.stacked_panes.count())): | ||||
|             for key, field in widget.fields.items(): | ||||
|                 if isinstance(field, QtGui.QComboBox): | ||||
|                     text = field.itemText(field.currentIndex()) | ||||
|                     data = field.itemData(field.currentIndex()) | ||||
|                     text = data if data else text | ||||
|                 elif isinstance(field, QtGui.QCheckBox): | ||||
|                     text = "on" if field.isChecked() else "off" | ||||
|                 else: | ||||
|                     text = field.text() | ||||
|                 self.config.set(widget.section_name, key, str(text)) | ||||
|         write(self.config) | ||||
|         self.parent.apply_preferences() | ||||
|         self.close() | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesNotificationBlock(QtGui.QVBoxLayout): | ||||
|     """Display notification settings with drill down to configure.""" | ||||
|     def __init__(self, pane, *args): | ||||
|         QtGui.QVBoxLayout.__init__(*(self,) + args) | ||||
|         self.section = "notifications" | ||||
|         self.config = QtGui.QApplication.instance().config | ||||
|         self.pane = pane | ||||
|         self.stack = QtGui.QStackedWidget() | ||||
| 
 | ||||
|         self.table = QtGui.QTableWidget() | ||||
|         fg_color = self.table.palette().text().color().name() | ||||
|         self.action_labels = { | ||||
|             "sound": "Play a sound", | ||||
|             "message": "Show a message in a popup", | ||||
|             "file": "Log to a file", | ||||
|             "taskbar": "Mark taskbar entry", | ||||
|             "tray": "Mark systray/indicator", | ||||
|             "command": "Run a command"} | ||||
|         self.action_icons = { | ||||
|             "sound": qicon_from_theme("media-playback-start"), | ||||
|             "message": qicon_from_theme("dialog-information"), | ||||
|             "file": qicon_from_theme("document-export"), | ||||
|             "taskbar": qicon_from_theme("weechat"), | ||||
|             "tray": utils.qicon_tint("ic_hot", fg_color), | ||||
|             "command": qicon_from_theme("system-run")} | ||||
|         self.icon_widget_qss = "padding:0;min-height:10px;min-width:16px;" | ||||
|         self.table.resizeColumnsToContents() | ||||
|         self.table.setColumnCount(2) | ||||
|         self.table.resizeRowsToContents() | ||||
|         self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) | ||||
|         self.table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) | ||||
|         self.table.setHorizontalHeaderLabels(["State", "Type"]) | ||||
|         self.table.horizontalHeader().setStretchLastSection(True) | ||||
|         self.table.horizontalHeader().setHighlightSections(False) | ||||
|         self.table.verticalHeader().setVisible(False) | ||||
|         self.table.setShowGrid(False) | ||||
|         self.table.itemSelectionChanged.connect(self._table_row_changed) | ||||
| 
 | ||||
|         self.buftypes = {} | ||||
|         for key, value in CONFIG_DEFAULT_NOTIFICATION_OPTIONS: | ||||
|             buftype, optkey = key.split(".") | ||||
|             if buftype not in self.buftypes: | ||||
|                 self.buftypes[buftype] = {} | ||||
|             self.buftypes[buftype][optkey] = self.config.get(self.section, key) | ||||
|         for buftype, optkey in self.buftypes.items(): | ||||
|             self._insert_type(buftype) | ||||
|         self.update_icons() | ||||
|         self.resize_table() | ||||
|         self.addWidget(self.table) | ||||
|         self.addWidget(self.stack) | ||||
|         self.table.selectRow(0) | ||||
| 
 | ||||
|     def _insert_type(self, buftype): | ||||
|         row = self.table.rowCount() | ||||
|         self.table.insertRow(row) | ||||
|         buftype_item = QtGui.QTableWidgetItem(buftype) | ||||
|         buftype_item.setTextAlignment(QtCore.Qt.AlignCenter) | ||||
|         self.table.setItem(row, 0, QtGui.QTableWidgetItem()) | ||||
|         self.table.setItem(row, 1, buftype_item) | ||||
|         subgrid = QtGui.QGridLayout() | ||||
|         subgrid.setColumnStretch(2, 1) | ||||
|         subgrid.setSpacing(10) | ||||
| 
 | ||||
|         for key, qicon in self.action_icons.items(): | ||||
|             value = self.buftypes[buftype][key] | ||||
|             line = subgrid.rowCount() | ||||
|             label = IconTextLabel(self.action_labels[key], qicon, 16) | ||||
| 
 | ||||
|             checkbox = QtGui.QCheckBox() | ||||
|             span = 1 | ||||
|             edit = None | ||||
|             if key in ("message", "taskbar", "tray"): | ||||
|                 checkbox.setChecked(value == "on") | ||||
|                 span = 2 | ||||
|             elif key == "sound": | ||||
|                 edit = PreferencesFileEdit( | ||||
|                     checkbox=checkbox, caption='Select a sound file', | ||||
|                     filter='Audio Files (*.wav *.mp3 *.ogg)') | ||||
|             elif key == "file": | ||||
|                 edit = PreferencesFileEdit(checkbox=checkbox, mode="save") | ||||
|             else: | ||||
|                 edit = PreferencesFileEdit(checkbox=checkbox) | ||||
|             if edit: | ||||
|                 edit.insert(value) | ||||
|                 subgrid.addWidget(edit, line, 2) | ||||
|             else: | ||||
|                 edit = checkbox | ||||
|             subgrid.addWidget(label, line, 1, 1, span) | ||||
|             subgrid.addWidget(checkbox, line, 0) | ||||
|             self.pane.fields[buftype + "." + key] = edit | ||||
|         subpane = QtGui.QWidget() | ||||
|         subpane.setLayout(subgrid) | ||||
|         subpane.setMaximumHeight(subgrid.totalMinimumSize().height()) | ||||
|         self.stack.addWidget(subpane) | ||||
| 
 | ||||
|     def resize_table(self): | ||||
|         """Fit the table height to contents.""" | ||||
|         height = self.table.horizontalHeader().height() | ||||
|         height = height * (self.table.rowCount() + 1) | ||||
|         height += self.table.contentsMargins().top() | ||||
|         height += self.table.contentsMargins().bottom() | ||||
|         self.table.setMaximumHeight(height) | ||||
|         self.table.setMinimumHeight(height) | ||||
|         self.table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) | ||||
| 
 | ||||
|     def update_icons(self): | ||||
|         """Draw the correct icons in the left col.""" | ||||
|         for i in range(self.table.rowCount()): | ||||
|             hbox = QtGui.QHBoxLayout() | ||||
|             iconset = QtGui.QWidget() | ||||
|             buftype = self.table.item(i, 1).text() | ||||
|             for key, qicon in self.action_icons.items(): | ||||
|                 field = self.pane.fields[buftype + "." + key] | ||||
|                 if isinstance(field, QtGui.QCheckBox): | ||||
|                     val = "on" if field.isChecked() else "off" | ||||
|                 else: | ||||
|                     val = field.text() | ||||
|                 iconbtn = QtGui.QPushButton() | ||||
|                 iconbtn.setContentsMargins(0, 0, 0, 0) | ||||
|                 iconbtn.setFlat(True) | ||||
|                 iconbtn.setFocusPolicy(QtCore.Qt.NoFocus) | ||||
|                 if val and val != "off": | ||||
|                     iconbtn.setIcon(qicon) | ||||
|                 iconbtn.setStyleSheet(self.icon_widget_qss) | ||||
|                 iconbtn.setToolTip(key) | ||||
|                 iconbtn.clicked.connect(lambda i=i: self.table.selectRow(i)) | ||||
|                 hbox.addWidget(iconbtn) | ||||
|             iconset.setLayout(hbox) | ||||
|             self.table.setCellWidget(i, 0, iconset) | ||||
| 
 | ||||
|     def _table_row_changed(self): | ||||
|         row = self.table.selectionModel().selectedRows()[0].row() | ||||
|         self.stack.setCurrentIndex(row) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesTreeWidget(QtGui.QTreeWidget): | ||||
|     """Widget with tree list of preferences.""" | ||||
|     def __init__(self, header_label, *args): | ||||
|         QtGui.QTreeWidget.__init__(*(self,) + args) | ||||
|         self.setHeaderLabel(header_label) | ||||
|         self.setRootIsDecorated(False) | ||||
|         self.setMaximumWidth(180) | ||||
|         self.setTextElideMode(QtCore.Qt.ElideNone) | ||||
|         self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) | ||||
|         self.setFocusPolicy(QtCore.Qt.NoFocus) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesSliderEdit(QtGui.QSlider): | ||||
|     """Percentage slider.""" | ||||
|     def __init__(self, *args): | ||||
|         QtGui.QSlider.__init__(*(self,) + args) | ||||
|         self.setMinimum(0) | ||||
|         self.setMaximum(100) | ||||
|         self.setTickPosition(QtGui.QSlider.TicksBelow) | ||||
|         self.setTickInterval(5) | ||||
| 
 | ||||
|     def insert(self, percent): | ||||
|         self.setValue(int(percent[:-1])) | ||||
| 
 | ||||
|     def text(self): | ||||
|         return str(self.value()) + "%" | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesColorEdit(QtGui.QPushButton): | ||||
|     """Simple color square that changes based on the color selected.""" | ||||
|     def __init__(self, *args): | ||||
|         QtGui.QPushButton.__init__(*(self,) + args) | ||||
|         self.color = "#000000" | ||||
|         self.clicked.connect(self._color_picker) | ||||
|         # Some of the configured colors use a astrisk prefix. | ||||
|         # Toggle this on right click. | ||||
|         self.star = False | ||||
|         self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | ||||
|         self.customContextMenuRequested.connect(self._color_star) | ||||
| 
 | ||||
|     def insert(self, color): | ||||
|         """Insert the desired color for the widget.""" | ||||
|         if color[:1] == "*": | ||||
|             self.star = True | ||||
|             color = color[1:] | ||||
|         self.setText("*" if self.star else "") | ||||
|         self.color = color | ||||
|         self.setStyleSheet("background-color: " + color) | ||||
| 
 | ||||
|     def text(self): | ||||
|         """Returns the hex value of the color.""" | ||||
|         return ("*" if self.star else "") + self.color | ||||
| 
 | ||||
|     def _color_picker(self): | ||||
|         color = QtGui.QColorDialog.getColor(self.color) | ||||
|         self.insert(color.name()) | ||||
| 
 | ||||
|     def _color_star(self): | ||||
|         self.star = not self.star | ||||
|         self.insert(self.text()) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesFontEdit(QtGui.QWidget): | ||||
|     """Font entry and selection.""" | ||||
|     def __init__(self, *args): | ||||
|         QtGui.QWidget.__init__(*(self,) + args) | ||||
|         layout = QtGui.QHBoxLayout() | ||||
|         self.checkbox = QtGui.QCheckBox() | ||||
|         self.edit = QtGui.QLineEdit() | ||||
|         self.font = "" | ||||
|         self.qfont = None | ||||
|         self.button = QtGui.QPushButton("C&hoose") | ||||
|         self.button.clicked.connect(self._font_picker) | ||||
|         self.checkbox.toggled.connect( | ||||
|             lambda: self._checkbox_toggled(self.checkbox)) | ||||
|         layout.addWidget(self.checkbox) | ||||
|         layout.addWidget(self.edit) | ||||
|         layout.addWidget(self.button) | ||||
|         layout.setContentsMargins(0, 0, 0, 0) | ||||
|         self.setLayout(layout) | ||||
| 
 | ||||
|     def insert(self, font_str): | ||||
|         """Insert the font described by the string.""" | ||||
|         self.font = font_str | ||||
|         self.edit.insert(font_str) | ||||
|         if font_str: | ||||
|             self.qfont = utils.Font.str_to_qfont(font_str) | ||||
|             self.edit.setFont(self.qfont) | ||||
|             self.checkbox.setChecked(True) | ||||
|             self._checkbox_toggled(self.checkbox) | ||||
|         else: | ||||
|             self.checkbox.setChecked(False) | ||||
|             self.qfont = None | ||||
|             self._checkbox_toggled(self.checkbox) | ||||
| 
 | ||||
|     def text(self): | ||||
|         """Returns the human readable font string.""" | ||||
|         return self.font | ||||
| 
 | ||||
|     def _font_picker(self): | ||||
|         font, ok = QtGui.QFontDialog.getFont(self.qfont) | ||||
|         if ok: | ||||
|             self.insert(utils.Font.qfont_to_str(font)) | ||||
| 
 | ||||
|     def _checkbox_toggled(self, button): | ||||
|         if button.isChecked() is False and not self.font == "": | ||||
|             self.insert("") | ||||
|         self.edit.setEnabled(button.isChecked()) | ||||
|         self.button.setEnabled(button.isChecked()) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesFileEdit(QtGui.QWidget): | ||||
|     """File entry and selection.""" | ||||
|     def __init__(self, checkbox=None, caption="Select a file", filter=None, | ||||
|                  mode="open", *args): | ||||
|         QtGui.QWidget.__init__(*(self,) + args) | ||||
|         layout = QtGui.QHBoxLayout() | ||||
|         self.caption = caption | ||||
|         self.filter = filter | ||||
|         self.edit = QtGui.QLineEdit() | ||||
|         self.file_str = "" | ||||
|         self.mode = mode | ||||
|         self.button = QtGui.QPushButton("B&rowse") | ||||
|         self.button.clicked.connect(self._file_picker) | ||||
|         if checkbox: | ||||
|             self.checkbox = checkbox | ||||
|         else: | ||||
|             self.checkbox = QtGui.QCheckBox() | ||||
|             layout.addWidget(self.checkbox) | ||||
|         self.checkbox.toggled.connect( | ||||
|             lambda: self._checkbox_toggled(self.checkbox)) | ||||
|         layout.addWidget(self.edit) | ||||
|         layout.addWidget(self.button) | ||||
|         layout.setContentsMargins(0, 0, 0, 0) | ||||
|         self.setLayout(layout) | ||||
| 
 | ||||
|     def insert(self, file_str): | ||||
|         """Insert the file.""" | ||||
|         self.file_str = file_str | ||||
|         self.edit.insert(file_str) | ||||
|         if file_str: | ||||
|             self.checkbox.setChecked(True) | ||||
|             self._checkbox_toggled(self.checkbox) | ||||
|         else: | ||||
|             self.checkbox.setChecked(False) | ||||
|             self._checkbox_toggled(self.checkbox) | ||||
| 
 | ||||
|     def text(self): | ||||
|         """Returns the human readable font string.""" | ||||
|         return self.file_str | ||||
| 
 | ||||
|     def _file_picker(self): | ||||
|         path = "" | ||||
|         if self.mode == "save": | ||||
|             fn = QtGui.QFileDialog.getSaveFileName | ||||
|         else: | ||||
|             fn = QtGui.QFileDialog.getOpenFileName | ||||
|         filename, fil = fn(self, self.caption, path, self.filter, self.filter) | ||||
|         if filename: | ||||
|             self.insert(filename) | ||||
| 
 | ||||
|     def _checkbox_toggled(self, button): | ||||
|         if button.isChecked() is False and not self.file_str == "": | ||||
|             self.insert("") | ||||
|         self.edit.setEnabled(button.isChecked()) | ||||
|         self.button.setEnabled(button.isChecked()) | ||||
| 
 | ||||
| 
 | ||||
| class PreferencesPaneWidget(QtGui.QWidget): | ||||
|     """ | ||||
|     Widget with (from top to bottom): | ||||
|     title, chat + nicklist (optional) + prompt/input. | ||||
|     """ | ||||
| 
 | ||||
|     disabled_fields = ["show_hostnames", "hide_nick_changes", | ||||
|                        "hide_join_and_part"] | ||||
| 
 | ||||
|     def __init__(self, section, section_name): | ||||
|         QtGui.QWidget.__init__(self) | ||||
|         self.grid = QtGui.QGridLayout() | ||||
|         self.grid.setAlignment(QtCore.Qt.AlignTop) | ||||
|         self.section = section | ||||
|         self.section_name = section_name | ||||
|         self.fields = {} | ||||
|         self.setLayout(self.grid) | ||||
|         self.grid.setColumnStretch(2, 1) | ||||
|         self.grid.setSpacing(10) | ||||
|         self.int_validator = QtGui.QIntValidator(0, 2147483647, self) | ||||
|         toolbar_icons = [ | ||||
|              ('ToolButtonFollowStyle', 'Default'), | ||||
|              ('ToolButtonIconOnly', 'Icon Only'), | ||||
|              ('ToolButtonTextOnly', 'Text Only'), | ||||
|              ('ToolButtonTextBesideIcon', 'Text Alongside Icons'), | ||||
|              ('ToolButtonTextUnderIcon', 'Text Under Icons')] | ||||
|         tray_options = [ | ||||
|             ('always', 'Always'), | ||||
|             ('unread', 'On Unread Messages'), | ||||
|             ('never', 'Never'), | ||||
|         ] | ||||
|         list_positions = [ | ||||
|             ('left', 'Left'), | ||||
|             ('right', 'Right'), | ||||
|         ] | ||||
|         sort_options = ['A-Z Ranked', 'A-Z', 'Z-A Ranked', 'Z-A'] | ||||
|         focus_opts = ["requested", "always", "never"] | ||||
|         self.comboboxes = {"style": QtGui.QStyleFactory.keys(), | ||||
|                            "position": list_positions, | ||||
|                            "toolbar_icons": toolbar_icons, | ||||
|                            "focus_new_tabs": focus_opts, | ||||
|                            "tray_icon": tray_options, | ||||
|                            "sort": sort_options} | ||||
| 
 | ||||
|     def addItem(self, key, value, default): | ||||
|         """Add a key-value pair.""" | ||||
|         line = len(self.fields) | ||||
|         name = key.split(".")[-1:][0].capitalize().replace("_", " ") | ||||
|         label = QtGui.QLabel(name) | ||||
|         start = 0 | ||||
| 
 | ||||
|         if self.section == "color": | ||||
|             start = 2 * (line % 2) | ||||
|             line = line // 2 | ||||
|             edit = PreferencesColorEdit() | ||||
|             edit.setFixedWidth(edit.sizeHint().height()) | ||||
|             edit.insert(value) | ||||
|             label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) | ||||
|         elif key == "custom_stylesheet": | ||||
|             edit = PreferencesFileEdit(caption='Select QStyleSheet File', | ||||
|                                        filter='*.qss') | ||||
|             edit.insert(value) | ||||
|         elif name.lower()[-5:] == "sound": | ||||
|             edit = PreferencesFileEdit( | ||||
|                 caption='Select a sound file', | ||||
|                 filter='Audio Files (*.wav *.mp3 *.ogg)') | ||||
|             edit.insert(value) | ||||
|         elif name.lower()[-4:] == "font": | ||||
|             edit = PreferencesFontEdit() | ||||
|             edit.setFixedWidth(200) | ||||
|             edit.insert(value) | ||||
|         elif key in self.comboboxes.keys(): | ||||
|             edit = QtGui.QComboBox() | ||||
|             if len(self.comboboxes[key][0]) == 2: | ||||
|                 for keyvalue in self.comboboxes[key]: | ||||
|                     edit.addItem(keyvalue[1], keyvalue[0]) | ||||
|                 # if self.section == "nicks" and key == "position": | ||||
|                 #     edit.addItem("below", "Below Buffer List") | ||||
|                 #     edit.addItem("above", "Above Buffer List") | ||||
|                 edit.setCurrentIndex(edit.findData(value)) | ||||
|             else: | ||||
|                 edit.addItems(self.comboboxes[key]) | ||||
|                 edit.setCurrentIndex(edit.findText(value)) | ||||
|             edit.setFixedWidth(200) | ||||
|         elif default in ["on", "off"]: | ||||
|             edit = QtGui.QCheckBox() | ||||
|             edit.setChecked(value == "on") | ||||
|         elif default[-1:] == "%": | ||||
|             edit = PreferencesSliderEdit(QtCore.Qt.Horizontal) | ||||
|             edit.setFixedWidth(200) | ||||
|             edit.insert(value) | ||||
|         else: | ||||
|             edit = QtGui.QLineEdit() | ||||
|             edit.setFixedWidth(200) | ||||
|             edit.insert(value) | ||||
|             if default.isdigit() or key == "port": | ||||
|                 edit.setValidator(self.int_validator) | ||||
|         if key == 'password': | ||||
|             edit.setEchoMode(QtGui.QLineEdit.Password) | ||||
|         if key in self.disabled_fields: | ||||
|             edit.setDisabled(True) | ||||
|         self.grid.addWidget(label, line, start + 0) | ||||
|         self.grid.addWidget(edit, line, start + 1) | ||||
| 
 | ||||
|         self.fields[key] = edit | ||||
| 
 | ||||
| 
 | ||||
| class IconTextLabel(QtGui.QWidget): | ||||
|     """An icon next to text.""" | ||||
|     def __init__(self, text=None, icon=None, extent=None): | ||||
|         QtGui.QWidget.__init__(self) | ||||
|         text_label = QtGui.QLabel(text) | ||||
|         if not extent: | ||||
|             extent = text_label.height() | ||||
|         icon_label = QtGui.QLabel() | ||||
|         pixmap = icon.pixmap(extent, QtGui.QIcon.Normal, QtGui.QIcon.On) | ||||
|         icon_label.setPixmap(pixmap) | ||||
|         label_layout = QtGui.QHBoxLayout() | ||||
|         label_layout.addWidget(icon_label) | ||||
|         label_layout.addWidget(text_label) | ||||
|         label_layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) | ||||
|         self.setLayout(label_layout) | ||||
							
								
								
									
										568
									
								
								src/qweechat/qweechat.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,568 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # qweechat.py - WeeChat remote GUI using Qt toolkit | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """ | ||||
| QWeeChat is a WeeChat remote GUI using Qt toolkit. | ||||
| 
 | ||||
| It requires requires WeeChat 0.3.7 or newer, running on local or remote host. | ||||
| """ | ||||
| 
 | ||||
| # | ||||
| # History: | ||||
| # | ||||
| # 2011-05-27, Sébastien Helleu <flashcode@flashtux.org>: | ||||
| #     start dev | ||||
| # | ||||
| 
 | ||||
| import sys | ||||
| import traceback | ||||
| import signal | ||||
| from pkg_resources import resource_filename | ||||
| 
 | ||||
| from  qtpy import QtCore, QtGui, QtWidgets | ||||
| 
 | ||||
| from qweechat.config import read, write | ||||
| from qweechat.about import AboutDialog | ||||
| from qweechat.buffer import BufferListWidget, Buffer | ||||
| from qweechat.connection import ConnectionDialog | ||||
| from qweechat.network import Network, STATUS_DISCONNECTED | ||||
| from qweechat.preferences import PreferencesDialog | ||||
| from qweechat.weechat import protocol | ||||
| 
 | ||||
| 
 | ||||
| APP_NAME = 'QWeeChat' | ||||
| AUTHOR = 'Sébastien Helleu' | ||||
| WEECHAT_SITE = 'https://weechat.org/' | ||||
| 
 | ||||
| # not QFrame | ||||
| class MainWindow(QtWidgets.QMainWindow): | ||||
|     """Main window.""" | ||||
| 
 | ||||
|     def __init__(self, *args): | ||||
|         super().__init__(*args) | ||||
| 
 | ||||
|         self.config = read() | ||||
| 
 | ||||
|         self.resize(800, 600) | ||||
|         self.setWindowTitle(APP_NAME) | ||||
| 
 | ||||
|         self.about_dialog = None | ||||
|         self.connection_dialog = None | ||||
|         self.preferences_dialog = None | ||||
| 
 | ||||
|         # network | ||||
|         self.network = Network() | ||||
|         self.network.statusChanged.connect(self._network_status_changed) | ||||
|         self.network.messageFromWeechat.connect(self._network_weechat_msg) | ||||
| 
 | ||||
|         # list of buffers | ||||
|         self.list_buffers = BufferListWidget() | ||||
|         self.list_buffers.currentRowChanged.connect(self._buffer_switch) | ||||
| 
 | ||||
|         # default buffer | ||||
|         self.buffers = [Buffer()] | ||||
|         self.stacked_buffers = QtWidgets.QStackedWidget() | ||||
|         self.stacked_buffers.addWidget(self.buffers[0].widget) | ||||
| 
 | ||||
|         # splitter with buffers + chat/input | ||||
|         splitter = QtWidgets.QSplitter() | ||||
|         splitter.addWidget(self.list_buffers) | ||||
|         splitter.addWidget(self.stacked_buffers) | ||||
| 
 | ||||
|         self.list_buffers.setSizePolicy(QtWidgets.QSizePolicy.Preferred, | ||||
|                                         QtWidgets.QSizePolicy.Preferred) | ||||
|         self.stacked_buffers.setSizePolicy(QtWidgets.QSizePolicy.Expanding, | ||||
|                            QtWidgets.QSizePolicy.Expanding) | ||||
|         # MainWindow | ||||
|         self.setCentralWidget(splitter) | ||||
| 
 | ||||
|         if self.config.getboolean('look', 'statusbar'): | ||||
|             self.statusBar().visible = True | ||||
|         self.statusBar().visible = True | ||||
| 
 | ||||
|         # actions for menu and toolbar | ||||
|         actions_def = { | ||||
|             'connect': [ | ||||
|                 'network-connect.png', | ||||
|                 'Connect to WeeChat', | ||||
|                 'Ctrl+O', | ||||
|                 self.open_connection_dialog, | ||||
|             ], | ||||
|             'disconnect': [ | ||||
|                 'network-disconnect.png', | ||||
|                 'Disconnect from WeeChat', | ||||
|                 'Ctrl+D', | ||||
|                 self.network.disconnect_weechat, | ||||
|             ], | ||||
|             'debug': [ | ||||
|                 'edit-find.png', | ||||
|                 'Open debug console window', | ||||
|                 'Ctrl+B', | ||||
|                 self.network.open_debug_dialog, | ||||
|             ], | ||||
|             'preferences': [ | ||||
|                 'preferences-other.png', | ||||
|                 'Change preferences', | ||||
|                 'Ctrl+P', | ||||
|                 self.open_preferences_dialog, | ||||
|             ], | ||||
|             'about': [ | ||||
|                 'help-about.png', | ||||
|                 'About QWeeChat', | ||||
|                 'Ctrl+H', | ||||
|                 self.open_about_dialog, | ||||
|             ], | ||||
|             'save connection': [ | ||||
|                 'document-save.png', | ||||
|                 'Save connection configuration', | ||||
|                 'Ctrl+S', | ||||
|                 self.save_connection, | ||||
|             ], | ||||
|             'quit': [ | ||||
|                 'application-exit.png', | ||||
|                 'Quit application', | ||||
|                 'Ctrl+Q', | ||||
|                 self.close, | ||||
|             ], | ||||
|         } | ||||
|         self.actions = {} | ||||
|         for name, action in list(actions_def.items()): | ||||
|             self.actions[name] = QtWidgets.QAction( | ||||
|                 QtGui.QIcon( | ||||
|                     resource_filename(__name__, 'data/icons/%s' % action[0])), | ||||
|                 name.capitalize(), self) | ||||
|             self.actions[name].setToolTip(f'{action[1]} ({action[2]})') | ||||
|             self.actions[name].setShortcut(action[2]) | ||||
|             self.actions[name].triggered.connect(action[3]) | ||||
| 
 | ||||
|         # menu | ||||
|         self.menu = self.menuBar() | ||||
|         menu_file = self.menu.addMenu('&File') | ||||
|         menu_file.addActions([self.actions['connect'], | ||||
|                               self.actions['disconnect'], | ||||
|                               self.actions['preferences'], | ||||
|                               self.actions['save connection'], | ||||
|                               self.actions['quit']]) | ||||
|         menu_window = self.menu.addMenu('&Window') | ||||
|         menu_window.addAction(self.actions['debug']) | ||||
|         menu_help = self.menu.addMenu('&Help') | ||||
|         menu_help.addAction(self.actions['about']) | ||||
|         self.network_status = QtWidgets.QLabel() | ||||
|         self.network_status.setFixedHeight(20) | ||||
|         self.network_status.setFixedWidth(200) | ||||
|         self.network_status.setContentsMargins(0, 0, 10, 0) | ||||
|         self.network_status.setAlignment(QtCore.Qt.AlignRight) | ||||
|         if hasattr(self, 'menuBar'): | ||||
|             if hasattr(self.menu, 'setCornerWidget'): | ||||
|                 self.menu.setCornerWidget(self.network_status, | ||||
|                                         QtCore.Qt.TopRightCorner) | ||||
|         self.network_status_set(STATUS_DISCONNECTED) | ||||
| 
 | ||||
|         # toolbar | ||||
|         if hasattr(self, 'addToolBar'): | ||||
|             toolbar = self.addToolBar('toolBar') | ||||
|             toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) | ||||
|             toolbar.addActions([self.actions['connect'], | ||||
|                                 self.actions['disconnect'], | ||||
|                                 self.actions['debug'], | ||||
|                                 self.actions['preferences'], | ||||
|                                 self.actions['about'], | ||||
|                                 self.actions['quit']]) | ||||
| 
 | ||||
|         self.buffers[0].widget.input.setFocus() | ||||
| 
 | ||||
|         # open debug dialog | ||||
|         if self.config.getboolean('look', 'debug'): | ||||
|             self.network.open_debug_dialog() | ||||
| 
 | ||||
|         # auto-connect to relay | ||||
|         if self.config.getboolean('relay', 'autoconnect'): | ||||
|             self.network.connect_weechat( | ||||
|                 hostname=self.config.get('relay', 'hostname', fallback='127.0.0.1'), | ||||
|                 port=self.config.get('relay', 'port', fallback='9000'), | ||||
|                 ssl=self.config.getboolean('relay', 'ssl', fallback=''), | ||||
|                 password=self.config.get('relay', 'password', fallback=''), | ||||
|                 totp=None, | ||||
|                 lines=self.config.get('relay', 'lines', fallback=''), | ||||
|             ) | ||||
| 
 | ||||
|         self.show() | ||||
| 
 | ||||
|     def _buffer_switch(self, index): | ||||
|         """Switch to a buffer.""" | ||||
|         if index >= 0: | ||||
|             self.stacked_buffers.setCurrentIndex(index) | ||||
|             self.stacked_buffers.widget(index).input.setFocus() | ||||
| 
 | ||||
|     def buffer_input(self, full_name, text): | ||||
|         """Send buffer input to WeeChat.""" | ||||
|         if self.network.is_connected(): | ||||
|             message = 'input %s %s\n' % (full_name, text) | ||||
|             self.network.send_to_weechat(message) | ||||
|             self.network.debug_print(0, '<==', message, forcecolor='#AA0000') | ||||
| 
 | ||||
|     def open_preferences_dialog(self): | ||||
|         """Open a dialog with preferences.""" | ||||
|         # TODO: implement the preferences dialog box | ||||
|         self.preferences_dialog = PreferencesDialog(self) | ||||
| 
 | ||||
|     def save_connection(self): | ||||
|         """Save connection configuration.""" | ||||
|         if self.network: | ||||
|             options = self.network.get_options() | ||||
|             for option in options: | ||||
|                 self.config.set('relay', option, options[option]) | ||||
| 
 | ||||
|     def open_about_dialog(self): | ||||
|         """Open a dialog with info about QWeeChat.""" | ||||
|         self.about_dialog = AboutDialog(APP_NAME, AUTHOR, WEECHAT_SITE, self) | ||||
| 
 | ||||
|     def open_connection_dialog(self): | ||||
|         """Open a dialog with connection settings.""" | ||||
|         values = {} | ||||
|         for option in ('hostname', 'port', 'ssl', 'password', 'lines'): | ||||
|             values[option] = self.config.get('relay', option, fallback='') | ||||
|         self.connection_dialog = ConnectionDialog(values, self) | ||||
|         self.connection_dialog.dialog_buttons.accepted.connect( | ||||
|             self.connect_weechat) | ||||
| 
 | ||||
|     def connect_weechat(self): | ||||
|         """Connect to WeeChat.""" | ||||
|         self.network.connect_weechat( | ||||
|             hostname=self.connection_dialog.fields['hostname'].text(), | ||||
|             port=self.connection_dialog.fields['port'].text(), | ||||
|             ssl=self.connection_dialog.fields['ssl'].isChecked(), | ||||
|             password=self.connection_dialog.fields['password'].text(), | ||||
|             totp=self.connection_dialog.fields['totp'].text(), | ||||
|             lines=int(self.connection_dialog.fields['lines'].text()), | ||||
|         ) | ||||
|         self.connection_dialog.close() | ||||
| 
 | ||||
|     def _network_status_changed(self, status, extra): | ||||
|         """Called when the network status has changed.""" | ||||
|         if self.config.getboolean('look', 'statusbar'): | ||||
|             self.statusBar().showMessage(status) | ||||
|         self.network.debug_print(0, '', status, forcecolor='#0000AA') | ||||
|         self.network_status_set(status) | ||||
| 
 | ||||
|     def network_status_set(self, status): | ||||
|         """Set the network status.""" | ||||
|         pal = self.network_status.palette() | ||||
|         try: | ||||
|             pal.setColor(self.network_status.foregroundRole(), | ||||
|                      self.network.status_color(status)) | ||||
|         except: | ||||
|             # dunno | ||||
|             pass | ||||
|         ssl = ' (SSL)' if status != STATUS_DISCONNECTED \ | ||||
|               and self.network.is_ssl() else '' | ||||
|         self.network_status.setPalette(pal) | ||||
|         icon = self.network.status_icon(status) | ||||
|         if icon: | ||||
|             self.network_status.setText( | ||||
|                 '<img src="%s"> %s' % | ||||
|                 (resource_filename(__name__, 'data/icons/%s' % icon), | ||||
|                  self.network.status_label(status) + ssl)) | ||||
|         else: | ||||
|             self.network_status.setText(status.capitalize()) | ||||
|         if status == STATUS_DISCONNECTED: | ||||
|             self.actions['connect'].setEnabled(True) | ||||
|             self.actions['disconnect'].setEnabled(False) | ||||
|         else: | ||||
|             self.actions['connect'].setEnabled(False) | ||||
|             self.actions['disconnect'].setEnabled(True) | ||||
| 
 | ||||
|     def _network_weechat_msg(self, message): | ||||
|         """Called when a message is received from WeeChat.""" | ||||
|         self.network.debug_print( | ||||
|             0, '==>', | ||||
|             'message (%d bytes):\n%s' | ||||
|             % (len(message), | ||||
|                protocol.hex_and_ascii(message.data(), 20)), | ||||
|             forcecolor='#008800', | ||||
|         ) | ||||
|         try: | ||||
|             proto = protocol.Protocol() | ||||
|             message = proto.decode(message.data()) | ||||
|             if message.uncompressed: | ||||
|                 self.network.debug_print( | ||||
|                     0, '==>', | ||||
|                     'message uncompressed (%d bytes):\n%s' | ||||
|                     % (message.size_uncompressed, | ||||
|                        protocol.hex_and_ascii(message.uncompressed, 20)), | ||||
|                     forcecolor='#008800') | ||||
|             self.network.debug_print(0, '', 'Message: %s' % message) | ||||
|             self.parse_message(message) | ||||
|         except Exception:  # noqa: E722 | ||||
|             print('Error while decoding message from WeeChat:\n%s' | ||||
|                   % traceback.format_exc()) | ||||
|             self.network.disconnect_weechat() | ||||
| 
 | ||||
|     def _parse_handshake(self, message): | ||||
|         """Parse a WeeChat message with handshake response.""" | ||||
|         for obj in message.objects: | ||||
|             if obj.objtype != 'htb': | ||||
|                 continue | ||||
|             self.network.init_with_handshake(obj.value) | ||||
|             break | ||||
| 
 | ||||
|     def _parse_listbuffers(self, message): | ||||
|         """Parse a WeeChat message with list of buffers.""" | ||||
|         for obj in message.objects: | ||||
|             if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer': | ||||
|                 continue | ||||
|             self.list_buffers.clear() | ||||
|             while self.stacked_buffers.count() > 0: | ||||
|                 buf = self.stacked_buffers.widget(0) | ||||
|                 self.stacked_buffers.removeWidget(buf) | ||||
|             self.buffers = [] | ||||
|             for item in obj.value['items']: | ||||
|                 buf = self.create_buffer(item) | ||||
|                 self.insert_buffer(len(self.buffers), buf) | ||||
|             self.list_buffers.setCurrentRow(0) | ||||
|             self.buffers[0].widget.input.setFocus() | ||||
| 
 | ||||
|     def _parse_line(self, message): | ||||
|         """Parse a WeeChat message with a buffer line.""" | ||||
|         for obj in message.objects: | ||||
|             lines = [] | ||||
|             if obj.objtype != 'hda' or obj.value['path'][-1] != 'line_data': | ||||
|                 continue | ||||
|             for item in obj.value['items']: | ||||
|                 if message.msgid == 'listlines': | ||||
|                     ptrbuf = item['__path'][0] | ||||
|                 else: | ||||
|                     ptrbuf = item['buffer'] | ||||
|                 index = [i for i, b in enumerate(self.buffers) | ||||
|                          if b.pointer() == ptrbuf] | ||||
|                 if index: | ||||
|                     lines.append( | ||||
|                         (index[0], | ||||
|                          (item['date'], item['prefix'], | ||||
|                           item['message'])) | ||||
|                     ) | ||||
|             if message.msgid == 'listlines': | ||||
|                 lines.reverse() | ||||
|             for line in lines: | ||||
|                 self.buffers[line[0]].widget.chat.display(*line[1]) | ||||
| 
 | ||||
|     def _parse_nicklist(self, message): | ||||
|         """Parse a WeeChat message with a buffer nicklist.""" | ||||
|         buffer_refresh = {} | ||||
|         for obj in message.objects: | ||||
|             if obj.objtype != 'hda' or \ | ||||
|                obj.value['path'][-1] != 'nicklist_item': | ||||
|                 continue | ||||
|             group = '__root' | ||||
|             for item in obj.value['items']: | ||||
|                 index = [i for i, b in enumerate(self.buffers) | ||||
|                          if b.pointer() == item['__path'][0]] | ||||
|                 if index: | ||||
|                     if not index[0] in buffer_refresh: | ||||
|                         self.buffers[index[0]].nicklist = {} | ||||
|                     buffer_refresh[index[0]] = True | ||||
|                     if item['group']: | ||||
|                         group = item['name'] | ||||
|                     self.buffers[index[0]].nicklist_add_item( | ||||
|                         group, item['group'], item['prefix'], item['name'], | ||||
|                         item['visible']) | ||||
|         for index in buffer_refresh: | ||||
|             self.buffers[index].nicklist_refresh() | ||||
| 
 | ||||
|     def _parse_nicklist_diff(self, message): | ||||
|         """Parse a WeeChat message with a buffer nicklist diff.""" | ||||
|         buffer_refresh = {} | ||||
|         for obj in message.objects: | ||||
|             if obj.objtype != 'hda' or \ | ||||
|                obj.value['path'][-1] != 'nicklist_item': | ||||
|                 continue | ||||
|             group = '__root' | ||||
|             for item in obj.value['items']: | ||||
|                 index = [i for i, b in enumerate(self.buffers) | ||||
|                          if b.pointer() == item['__path'][0]] | ||||
|                 if not index: | ||||
|                     continue | ||||
|                 buffer_refresh[index[0]] = True | ||||
|                 if item['_diff'] == ord('^'): | ||||
|                     group = item['name'] | ||||
|                 elif item['_diff'] == ord('+'): | ||||
|                     self.buffers[index[0]].nicklist_add_item( | ||||
|                         group, item['group'], item['prefix'], item['name'], | ||||
|                         item['visible']) | ||||
|                 elif item['_diff'] == ord('-'): | ||||
|                     self.buffers[index[0]].nicklist_remove_item( | ||||
|                         group, item['group'], item['name']) | ||||
|                 elif item['_diff'] == ord('*'): | ||||
|                     self.buffers[index[0]].nicklist_update_item( | ||||
|                         group, item['group'], item['prefix'], item['name'], | ||||
|                         item['visible']) | ||||
|         for index in buffer_refresh: | ||||
|             self.buffers[index].nicklist_refresh() | ||||
| 
 | ||||
|     def _parse_buffer_opened(self, message): | ||||
|         """Parse a WeeChat message with a new buffer (opened).""" | ||||
|         for obj in message.objects: | ||||
|             if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer': | ||||
|                 continue | ||||
|             for item in obj.value['items']: | ||||
|                 buf = self.create_buffer(item) | ||||
|                 index = self.find_buffer_index_for_insert(item['next_buffer']) | ||||
|                 self.insert_buffer(index, buf) | ||||
| 
 | ||||
|     def _parse_buffer(self, message): | ||||
|         """Parse a WeeChat message with a buffer event | ||||
|         (anything except a new buffer). | ||||
|         """ | ||||
|         for obj in message.objects: | ||||
|             if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer': | ||||
|                 continue | ||||
|             for item in obj.value['items']: | ||||
|                 index = [i for i, b in enumerate(self.buffers) | ||||
|                          if b.pointer() == item['__path'][0]] | ||||
|                 if not index: | ||||
|                     continue | ||||
|                 index = index[0] | ||||
|                 if message.msgid == '_buffer_type_changed': | ||||
|                     self.buffers[index].data['type'] = item['type'] | ||||
|                 elif message.msgid in ('_buffer_moved', '_buffer_merged', | ||||
|                                        '_buffer_unmerged'): | ||||
|                     buf = self.buffers[index] | ||||
|                     buf.data['number'] = item['number'] | ||||
|                     self.remove_buffer(index) | ||||
|                     index2 = self.find_buffer_index_for_insert( | ||||
|                         item['next_buffer']) | ||||
|                     self.insert_buffer(index2, buf) | ||||
|                 elif message.msgid == '_buffer_renamed': | ||||
|                     self.buffers[index].data['full_name'] = item['full_name'] | ||||
|                     self.buffers[index].data['short_name'] = item['short_name'] | ||||
|                 elif message.msgid == '_buffer_title_changed': | ||||
|                     self.buffers[index].data['title'] = item['title'] | ||||
|                     self.buffers[index].update_title() | ||||
|                 elif message.msgid == '_buffer_cleared': | ||||
|                     self.buffers[index].widget.chat.clear() | ||||
|                 elif message.msgid.startswith('_buffer_localvar_'): | ||||
|                     self.buffers[index].data['local_variables'] = \ | ||||
|                         item['local_variables'] | ||||
|                     self.buffers[index].update_prompt() | ||||
|                 elif message.msgid == '_buffer_closing': | ||||
|                     self.remove_buffer(index) | ||||
| 
 | ||||
|     def parse_message(self, message): | ||||
|         """Parse a WeeChat message.""" | ||||
|         if message.msgid.startswith('debug'): | ||||
|             self.network.debug_print(0, '', '(debug message, ignored)') | ||||
|         elif message.msgid == 'handshake': | ||||
|             self._parse_handshake(message) | ||||
|         elif message.msgid == 'listbuffers': | ||||
|             self._parse_listbuffers(message) | ||||
|         elif message.msgid in ('listlines', '_buffer_line_added'): | ||||
|             self._parse_line(message) | ||||
|         elif message.msgid in ('_nicklist', 'nicklist'): | ||||
|             self._parse_nicklist(message) | ||||
|         elif message.msgid == '_nicklist_diff': | ||||
|             self._parse_nicklist_diff(message) | ||||
|         elif message.msgid == '_buffer_opened': | ||||
|             self._parse_buffer_opened(message) | ||||
|         elif message.msgid.startswith('_buffer_'): | ||||
|             self._parse_buffer(message) | ||||
|         elif message.msgid == '_upgrade': | ||||
|             self.network.desync_weechat() | ||||
|         elif message.msgid == '_upgrade_ended': | ||||
|             self.network.sync_weechat() | ||||
|         else: | ||||
|             print(f"Unknown message with id {message.msgid}") | ||||
| 
 | ||||
|     def create_buffer(self, item): | ||||
|         """Create a new buffer.""" | ||||
|         buf = Buffer(item) | ||||
|         buf.bufferInput.connect(self.buffer_input) | ||||
|         buf.widget.input.bufferSwitchPrev.connect( | ||||
|             self.list_buffers.switch_prev_buffer) | ||||
|         buf.widget.input.bufferSwitchNext.connect( | ||||
|             self.list_buffers.switch_next_buffer) | ||||
|         return buf | ||||
| 
 | ||||
|     def insert_buffer(self, index, buf): | ||||
|         """Insert a buffer in list.""" | ||||
|         self.buffers.insert(index, buf) | ||||
|         self.list_buffers.insertItem(index, '%s' | ||||
|                                      % (buf.data['local_variables']['name'])) | ||||
|         self.stacked_buffers.insertWidget(index, buf.widget) | ||||
| 
 | ||||
|     def remove_buffer(self, index): | ||||
|         """Remove a buffer.""" | ||||
|         if self.list_buffers.currentRow == index and index > 0: | ||||
|             self.list_buffers.setCurrentRow(index - 1) | ||||
|         self.list_buffers.takeItem(index) | ||||
|         self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index)) | ||||
|         self.buffers.pop(index) | ||||
| 
 | ||||
|     def find_buffer_index_for_insert(self, next_buffer): | ||||
|         """Find position to insert a buffer in list.""" | ||||
|         index = -1 | ||||
|         if next_buffer == '0x0': | ||||
|             index = len(self.buffers) | ||||
|         else: | ||||
|             index = [i for i, b in enumerate(self.buffers) | ||||
|                      if b.pointer() == next_buffer] | ||||
|             if len(index): | ||||
|                 index = index[0] | ||||
|             else: | ||||
|                 index = -1 | ||||
|         if index < 0: | ||||
|             print('Warning: unable to find position for buffer, using end of ' | ||||
|                   'list by default') | ||||
|             index = len(self.buffers) | ||||
|         return index | ||||
| 
 | ||||
|     def closeEvent(self, event): | ||||
|         """Called when QWeeChat window is closed.""" | ||||
|         self.network.disconnect_weechat() | ||||
|         if self.network.debug_dialog: | ||||
|             self.network.debug_dialog.close() | ||||
|         write(self.config) | ||||
|         QtWidgets.QFrame.closeEvent(self, event) | ||||
| 
 | ||||
| def iMain(lArgs=None): | ||||
|     if lArgs is None and len(sys.argv) > 1: | ||||
|         lArgs = sys.argv[1:] | ||||
|     elif lArgs is None: | ||||
|         lArgs = [] | ||||
|     app = QtWidgets.QApplication(lArgs) | ||||
|     app.setStyle(QtWidgets.QStyleFactory.create('Cleanlooks')) | ||||
|     app.setWindowIcon(QtGui.QIcon( | ||||
|         resource_filename(__name__, 'data/icons/weechat.png'))) | ||||
|     main_win = MainWindow() | ||||
|     main_win.show() | ||||
|     i = app.exec_() | ||||
|     return i | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     signal.signal(signal.SIGINT, signal.SIG_DFL) | ||||
|     try: | ||||
|         i = iMain() | ||||
|     except KeyboardInterrupt as e: | ||||
|         i = 0 | ||||
|     except Exception as e: | ||||
|         LOG.exception(f"Exception {e}") | ||||
|         i = 1 | ||||
|     sys.exit(i) | ||||
							
								
								
									
										30
									
								
								src/qweechat/version.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,30 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # version.py - version of QWeeChat | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Version of QWeeChat.""" | ||||
| 
 | ||||
| VERSION = '1.0.0' | ||||
| 
 | ||||
| 
 | ||||
| def qweechat_version(): | ||||
|     """Return QWeeChat version.""" | ||||
|     return VERSION | ||||
							
								
								
									
										19
									
								
								src/qweechat/weechat/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,19 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
							
								
								
									
										201
									
								
								src/qweechat/weechat/color.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,201 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # color.py - remove/replace colors in WeeChat strings | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Remove/replace colors in WeeChat strings.""" | ||||
| 
 | ||||
| import re | ||||
| import logging | ||||
| 
 | ||||
| RE_COLOR_ATTRS = r'[*!/_|]*' | ||||
| RE_COLOR_STD = r'(?:%s\d{2})' % RE_COLOR_ATTRS | ||||
| RE_COLOR_EXT = r'(?:@%s\d{5})' % RE_COLOR_ATTRS | ||||
| RE_COLOR_ANY = r'(?:%s|%s)' % (RE_COLOR_STD, RE_COLOR_EXT) | ||||
| # \x19: color code, \x1A: set attribute, \x1B: remove attribute, \x1C: reset | ||||
| RE_COLOR = re.compile( | ||||
|     r'(\x19(?:\d{2}|F%s|B\d{2}|B@\d{5}|E|\\*%s(~%s)?|@\d{5}|b.|\x1C))|\x1A.|' | ||||
|     r'\x1B.|\x1C' | ||||
|     % (RE_COLOR_ANY, RE_COLOR_ANY, RE_COLOR_ANY)) | ||||
| 
 | ||||
| TERMINAL_COLORS = \ | ||||
|     '000000cd000000cd00cdcd000000cdcd00cd00cdcde5e5e5' \ | ||||
|     '4d4d4dff000000ff00ffff000000ffff00ff00ffffffffff' \ | ||||
|     '00000000002a0000550000800000aa0000d4002a00002a2a' \ | ||||
|     '002a55002a80002aaa002ad400550000552a005555005580' \ | ||||
|     '0055aa0055d400800000802a0080550080800080aa0080d4' \ | ||||
|     '00aa0000aa2a00aa5500aa8000aaaa00aad400d40000d42a' \ | ||||
|     '00d45500d48000d4aa00d4d42a00002a002a2a00552a0080' \ | ||||
|     '2a00aa2a00d42a2a002a2a2a2a2a552a2a802a2aaa2a2ad4' \ | ||||
|     '2a55002a552a2a55552a55802a55aa2a55d42a80002a802a' \ | ||||
|     '2a80552a80802a80aa2a80d42aaa002aaa2a2aaa552aaa80' \ | ||||
|     '2aaaaa2aaad42ad4002ad42a2ad4552ad4802ad4aa2ad4d4' \ | ||||
|     '55000055002a5500555500805500aa5500d4552a00552a2a' \ | ||||
|     '552a55552a80552aaa552ad455550055552a555555555580' \ | ||||
|     '5555aa5555d455800055802a5580555580805580aa5580d4' \ | ||||
|     '55aa0055aa2a55aa5555aa8055aaaa55aad455d40055d42a' \ | ||||
|     '55d45555d48055d4aa55d4d480000080002a800055800080' \ | ||||
|     '8000aa8000d4802a00802a2a802a55802a80802aaa802ad4' \ | ||||
|     '80550080552a8055558055808055aa8055d480800080802a' \ | ||||
|     '8080558080808080aa8080d480aa0080aa2a80aa5580aa80' \ | ||||
|     '80aaaa80aad480d40080d42a80d45580d48080d4aa80d4d4' \ | ||||
|     'aa0000aa002aaa0055aa0080aa00aaaa00d4aa2a00aa2a2a' \ | ||||
|     'aa2a55aa2a80aa2aaaaa2ad4aa5500aa552aaa5555aa5580' \ | ||||
|     'aa55aaaa55d4aa8000aa802aaa8055aa8080aa80aaaa80d4' \ | ||||
|     'aaaa00aaaa2aaaaa55aaaa80aaaaaaaaaad4aad400aad42a' \ | ||||
|     'aad455aad480aad4aaaad4d4d40000d4002ad40055d40080' \ | ||||
|     'd400aad400d4d42a00d42a2ad42a55d42a80d42aaad42ad4' \ | ||||
|     'd45500d4552ad45555d45580d455aad455d4d48000d4802a' \ | ||||
|     'd48055d48080d480aad480d4d4aa00d4aa2ad4aa55d4aa80' \ | ||||
|     'd4aaaad4aad4d4d400d4d42ad4d455d4d480d4d4aad4d4d4' \ | ||||
|     '0808081212121c1c1c2626263030303a3a3a4444444e4e4e' \ | ||||
|     '5858586262626c6c6c7676768080808a8a8a9494949e9e9e' \ | ||||
|     'a8a8a8b2b2b2bcbcbcc6c6c6d0d0d0dadadae4e4e4eeeeee' | ||||
| 
 | ||||
| # WeeChat basic colors (color name, index in terminal colors) | ||||
| WEECHAT_BASIC_COLORS = ( | ||||
|     ('default', 0), ('black', 0), ('darkgray', 8), ('red', 1), | ||||
|     ('lightred', 9), ('green', 2), ('lightgreen', 10), ('brown', 3), | ||||
|     ('yellow', 11), ('blue', 4), ('lightblue', 12), ('magenta', 5), | ||||
|     ('lightmagenta', 13), ('cyan', 6), ('lightcyan', 14), ('gray', 7), | ||||
|     ('white', 0)) | ||||
| 
 | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class Color(): | ||||
|     def __init__(self, color_options, debug=False): | ||||
|         self.color_options = color_options | ||||
|         self.debug = debug | ||||
| 
 | ||||
|     def _rgb_color(self, index): | ||||
|         color = TERMINAL_COLORS[index*6:(index*6)+6] | ||||
|         col_r = int(color[0:2], 16) * 0.85 | ||||
|         col_g = int(color[2:4], 16) * 0.85 | ||||
|         col_b = int(color[4:6], 16) * 0.85 | ||||
|         return '%02x%02x%02x' % (col_r, col_g, col_b) | ||||
| 
 | ||||
|     def _convert_weechat_color(self, color): | ||||
|         try: | ||||
|             index = int(color) | ||||
|             return '\x01(Fr%s)' % self.color_options[index] | ||||
|         except Exception:  # noqa: E722 | ||||
|             log.debug('Error decoding WeeChat color "%s"', color) | ||||
|             return '' | ||||
| 
 | ||||
|     def _convert_terminal_color(self, fg_bg, attrs, color): | ||||
|         try: | ||||
|             index = int(color) | ||||
|             return '\x01(%s%s#%s)' % (fg_bg, attrs, self._rgb_color(index)) | ||||
|         except Exception:  # noqa: E722 | ||||
|             log.debug('Error decoding terminal color "%s"', color) | ||||
|             return '' | ||||
| 
 | ||||
|     def _convert_color_attr(self, fg_bg, color): | ||||
|         extended = False | ||||
|         if color[0].startswith('@'): | ||||
|             extended = True | ||||
|             color = color[1:] | ||||
|         attrs = '' | ||||
|         # keep_attrs = False | ||||
|         while color.startswith(('*', '!', '/', '_', '|')): | ||||
|             # TODO: manage the "keep attributes" flag | ||||
|             # if color[0] == '|': | ||||
|             #    keep_attrs = True | ||||
|             attrs += color[0] | ||||
|             color = color[1:] | ||||
|         if extended: | ||||
|             return self._convert_terminal_color(fg_bg, attrs, color) | ||||
|         try: | ||||
|             index = int(color) | ||||
|             return self._convert_terminal_color(fg_bg, attrs, | ||||
|                                                 WEECHAT_BASIC_COLORS[index][1]) | ||||
|         except Exception:  # noqa: E722 | ||||
|             log.debug('Error decoding color "%s"', color) | ||||
|             return '' | ||||
| 
 | ||||
|     def _attrcode_to_char(self, code): | ||||
|         codes = { | ||||
|             '\x01': '*', | ||||
|             '\x02': '!', | ||||
|             '\x03': '/', | ||||
|             '\x04': '_', | ||||
|         } | ||||
|         return codes.get(code, '') | ||||
| 
 | ||||
|     def _convert_color(self, match): | ||||
|         color = match.group(0) | ||||
|         if color[0] == '\x19': | ||||
|             if color[1] == 'b': | ||||
|                 # bar code, ignored | ||||
|                 return '' | ||||
|             if color[1] == '\x1C': | ||||
|                 # reset | ||||
|                 return '\x01(Fr)\x01(Br)' | ||||
|             if color[1] in ('F', 'B'): | ||||
|                 # foreground or background | ||||
|                 return self._convert_color_attr(color[1], color[2:]) | ||||
|             if color[1] == '*': | ||||
|                 # foreground with optional background | ||||
|                 items = color[2:].split(',') | ||||
|                 str_col = self._convert_color_attr('F', items[0]) | ||||
|                 if len(items) > 1: | ||||
|                     str_col += self._convert_color_attr('B', items[1]) | ||||
|                 return str_col | ||||
|             if color[1] == '@': | ||||
|                 # direct ncurses pair number, ignored | ||||
|                 return '' | ||||
|             if color[1] == 'E': | ||||
|                 # text emphasis, ignored | ||||
|                 return '' | ||||
|             if color[1:].isdigit(): | ||||
|                 return self._convert_weechat_color(int(color[1:])) | ||||
|         elif color[0] == '\x1A': | ||||
|             # set attribute | ||||
|             return '\x01(+%s)' % self._attrcode_to_char(color[1]) | ||||
|         elif color[0] == '\x1B': | ||||
|             # remove attribute | ||||
|             return '\x01(-%s)' % self._attrcode_to_char(color[1]) | ||||
|         elif color[0] == '\x1C': | ||||
|             # reset | ||||
|             return '\x01(Fr)\x01(Br)' | ||||
|         # should never be executed! | ||||
|         return match.group(0) | ||||
| 
 | ||||
|     def _convert_color_debug(self, match): | ||||
|         group = match.group(0) | ||||
|         for code in (0x01, 0x02, 0x03, 0x04, 0x19, 0x1A, 0x1B): | ||||
|             group = group.replace(chr(code), '<x%02X>' % code) | ||||
|         return group | ||||
| 
 | ||||
|     def convert(self, text): | ||||
|         if not text: | ||||
|             return '' | ||||
|         if self.debug: | ||||
|             return RE_COLOR.sub(self._convert_color_debug, text) | ||||
|         return RE_COLOR.sub(self._convert_color, text) | ||||
| 
 | ||||
| 
 | ||||
| def remove(text): | ||||
|     """Remove colors in a WeeChat string.""" | ||||
|     if not text: | ||||
|         return '' | ||||
|     return re.sub(RE_COLOR, '', text) | ||||
							
								
								
									
										361
									
								
								src/qweechat/weechat/protocol.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,361 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # protocol.py - decode binary messages received from WeeChat/relay | ||||
| # | ||||
| # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| # | ||||
| # For info about protocol and format of messages, please read document | ||||
| # "WeeChat Relay Protocol", available at:  https://weechat.org/doc/ | ||||
| # | ||||
| # History: | ||||
| # | ||||
| # 2011-11-23, Sébastien Helleu <flashcode@flashtux.org>: | ||||
| #     start dev | ||||
| # | ||||
| 
 | ||||
| """Decode binary messages received from WeeChat/relay.""" | ||||
| 
 | ||||
| import collections | ||||
| import struct | ||||
| import zlib | ||||
| 
 | ||||
| 
 | ||||
| class WeechatDict(collections.OrderedDict): | ||||
|     def __str__(self): | ||||
|         return '{%s}' % ', '.join( | ||||
|             ['%s: %s' % (repr(key), repr(self[key])) for key in self]) | ||||
| 
 | ||||
| 
 | ||||
| class WeechatObject: | ||||
|     def __init__(self, objtype, value, separator='\n'): | ||||
|         self.objtype = objtype | ||||
|         self.value = value | ||||
|         self.separator = separator | ||||
|         self.indent = '  ' if separator == '\n' else '' | ||||
|         self.separator1 = '\n%s' % self.indent if separator == '\n' else '' | ||||
| 
 | ||||
|     def _str_value(self, val): | ||||
|         if isinstance(val, str) and val is not None: | ||||
|             return '\'%s\'' % val | ||||
|         return str(val) | ||||
| 
 | ||||
|     def _str_value_hdata(self): | ||||
|         lines = ['%skeys: %s%s%spath: %s' % (self.separator1, | ||||
|                                              str(self.value['keys']), | ||||
|                                              self.separator, | ||||
|                                              self.indent, | ||||
|                                              str(self.value['path']))] | ||||
|         for i, item in enumerate(self.value['items']): | ||||
|             lines.append('  item %d:%s%s' % ( | ||||
|                 (i + 1), self.separator, | ||||
|                 self.separator.join( | ||||
|                     ['%s%s: %s' % (self.indent * 2, key, | ||||
|                                    self._str_value(value)) | ||||
|                      for key, value in item.items()]))) | ||||
|         return '\n'.join(lines) | ||||
| 
 | ||||
|     def _str_value_infolist(self): | ||||
|         lines = ['%sname: %s' % (self.separator1, self.value['name'])] | ||||
|         for i, item in enumerate(self.value['items']): | ||||
|             lines.append('  item %d:%s%s' % ( | ||||
|                 (i + 1), self.separator, | ||||
|                 self.separator.join( | ||||
|                     ['%s%s: %s' % (self.indent * 2, key, | ||||
|                                    self._str_value(value)) | ||||
|                      for key, value in item.items()]))) | ||||
|         return '\n'.join(lines) | ||||
| 
 | ||||
|     def _str_value_other(self): | ||||
|         return self._str_value(self.value) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         obj_cb = { | ||||
|             'hda': self._str_value_hdata, | ||||
|             'inl': self._str_value_infolist, | ||||
|         } | ||||
|         return '%s: %s' % (self.objtype, | ||||
|                            obj_cb.get(self.objtype, self._str_value_other)()) | ||||
| 
 | ||||
| 
 | ||||
| class WeechatObjects(list): | ||||
|     def __init__(self, separator='\n'): | ||||
|         super().__init__() | ||||
|         self.separator = separator | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.separator.join([str(obj) for obj in self]) | ||||
| 
 | ||||
| 
 | ||||
| class WeechatMessage: | ||||
|     def __init__(self, size, size_uncompressed, compression, uncompressed, | ||||
|                  msgid, objects): | ||||
|         self.size = size | ||||
|         self.size_uncompressed = size_uncompressed | ||||
|         self.compression = compression | ||||
|         self.uncompressed = uncompressed | ||||
|         self.msgid = msgid | ||||
|         self.objects = objects | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         if self.compression != 0: | ||||
|             return 'size: %d/%d (%d%%), id=\'%s\', objects:\n%s' % ( | ||||
|                 self.size, self.size_uncompressed, | ||||
|                 100 - ((self.size * 100) // self.size_uncompressed), | ||||
|                 self.msgid, self.objects) | ||||
|         return 'size: %d, id=\'%s\', objects:\n%s' % (self.size, | ||||
|                                                       self.msgid, | ||||
|                                                       self.objects) | ||||
| 
 | ||||
| 
 | ||||
| class Protocol: | ||||
|     """Decode binary message received from WeeChat/relay.""" | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self.data = '' | ||||
|         self._obj_cb = { | ||||
|             'chr': self._obj_char, | ||||
|             'int': self._obj_int, | ||||
|             'lon': self._obj_long, | ||||
|             'str': self._obj_str, | ||||
|             'buf': self._obj_buffer, | ||||
|             'ptr': self._obj_ptr, | ||||
|             'tim': self._obj_time, | ||||
|             'htb': self._obj_hashtable, | ||||
|             'hda': self._obj_hdata, | ||||
|             'inf': self._obj_info, | ||||
|             'inl': self._obj_infolist, | ||||
|             'arr': self._obj_array, | ||||
|         } | ||||
| 
 | ||||
|     def _obj_type(self): | ||||
|         """Read type in data (3 chars).""" | ||||
|         if len(self.data) < 3: | ||||
|             self.data = '' | ||||
|             return '' | ||||
|         objtype = self.data[0:3].decode() | ||||
|         self.data = self.data[3:] | ||||
|         return objtype | ||||
| 
 | ||||
|     def _obj_len_data(self, length_size): | ||||
|         """Read length (1 or 4 bytes), then value with this length.""" | ||||
|         if len(self.data) < length_size: | ||||
|             self.data = '' | ||||
|             return None | ||||
|         if length_size == 1: | ||||
|             length = struct.unpack('B', self.data[0:1])[0] | ||||
|             self.data = self.data[1:] | ||||
|         else: | ||||
|             length = self._obj_int() | ||||
|         if length < 0: | ||||
|             return None | ||||
|         if length > 0: | ||||
|             value = self.data[0:length] | ||||
|             self.data = self.data[length:] | ||||
|         else: | ||||
|             value = '' | ||||
|         return value | ||||
| 
 | ||||
|     def _obj_char(self): | ||||
|         """Read a char in data.""" | ||||
|         if len(self.data) < 1: | ||||
|             return 0 | ||||
|         value = struct.unpack('b', self.data[0:1])[0] | ||||
|         self.data = self.data[1:] | ||||
|         return value | ||||
| 
 | ||||
|     def _obj_int(self): | ||||
|         """Read an integer in data (4 bytes).""" | ||||
|         if len(self.data) < 4: | ||||
|             self.data = '' | ||||
|             return 0 | ||||
|         value = struct.unpack('>i', self.data[0:4])[0] | ||||
|         self.data = self.data[4:] | ||||
|         return value | ||||
| 
 | ||||
|     def _obj_long(self): | ||||
|         """Read a long integer in data (length on 1 byte + value as string).""" | ||||
|         value = self._obj_len_data(1) | ||||
|         if value is None: | ||||
|             return None | ||||
|         return int(value) | ||||
| 
 | ||||
|     def _obj_str(self): | ||||
|         """Read a string in data (length on 4 bytes + content).""" | ||||
|         value = self._obj_len_data(4) | ||||
|         if value in ("", None): | ||||
|             return "" | ||||
|         return value.decode() | ||||
| 
 | ||||
|     def _obj_buffer(self): | ||||
|         """Read a buffer in data (length on 4 bytes + data).""" | ||||
|         return self._obj_len_data(4) | ||||
| 
 | ||||
|     def _obj_ptr(self): | ||||
|         """Read a pointer in data (length on 1 byte + value as string).""" | ||||
|         value = self._obj_len_data(1) | ||||
|         if value is None: | ||||
|             return None | ||||
|         return '0x%s' % value | ||||
| 
 | ||||
|     def _obj_time(self): | ||||
|         """Read a time in data (length on 1 byte + value as string).""" | ||||
|         value = self._obj_len_data(1) | ||||
|         if value is None: | ||||
|             return None | ||||
|         return int(value) | ||||
| 
 | ||||
|     def _obj_hashtable(self): | ||||
|         """ | ||||
|         Read a hashtable in data | ||||
|         (type for keys + type for values + count + items). | ||||
|         """ | ||||
|         type_keys = self._obj_type() | ||||
|         type_values = self._obj_type() | ||||
|         count = self._obj_int() | ||||
|         hashtable = WeechatDict() | ||||
|         for _ in range(count): | ||||
|             key = self._obj_cb[type_keys]() | ||||
|             value = self._obj_cb[type_values]() | ||||
|             hashtable[key] = value | ||||
|         return hashtable | ||||
| 
 | ||||
|     def _obj_hdata(self): | ||||
|         """Read a hdata in data.""" | ||||
|         path = self._obj_str() | ||||
|         keys = self._obj_str() | ||||
|         count = self._obj_int() | ||||
|         list_path = path.split('/') if path else [] | ||||
|         list_keys = keys.split(',') if keys else [] | ||||
|         keys_types = [] | ||||
|         dict_keys = WeechatDict() | ||||
|         for key in list_keys: | ||||
|             items = key.split(':') | ||||
|             keys_types.append(items) | ||||
|             dict_keys[items[0]] = items[1] | ||||
|         items = [] | ||||
|         for _ in range(count): | ||||
|             item = WeechatDict() | ||||
|             item['__path'] = [] | ||||
|             pointers = [] | ||||
|             for _ in enumerate(list_path): | ||||
|                 pointers.append(self._obj_ptr()) | ||||
|             for key, objtype in keys_types: | ||||
|                 item[key] = self._obj_cb[objtype]() | ||||
|             item['__path'] = pointers | ||||
|             items.append(item) | ||||
|         return { | ||||
|             'path': list_path, | ||||
|             'keys': dict_keys, | ||||
|             'count': count, | ||||
|             'items': items, | ||||
|         } | ||||
| 
 | ||||
|     def _obj_info(self): | ||||
|         """Read an info in data.""" | ||||
|         name = self._obj_str() | ||||
|         value = self._obj_str() | ||||
|         return (name, value) | ||||
| 
 | ||||
|     def _obj_infolist(self): | ||||
|         """Read an infolist in data.""" | ||||
|         name = self._obj_str() | ||||
|         count_items = self._obj_int() | ||||
|         items = [] | ||||
|         for _ in range(count_items): | ||||
|             count_vars = self._obj_int() | ||||
|             variables = WeechatDict() | ||||
|             for _ in range(count_vars): | ||||
|                 var_name = self._obj_str() | ||||
|                 var_type = self._obj_type() | ||||
|                 var_value = self._obj_cb[var_type]() | ||||
|                 variables[var_name] = var_value | ||||
|             items.append(variables) | ||||
|         return { | ||||
|             'name': name, | ||||
|             'items': items | ||||
|         } | ||||
| 
 | ||||
|     def _obj_array(self): | ||||
|         """Read an array of values in data.""" | ||||
|         type_values = self._obj_type() | ||||
|         count_values = self._obj_int() | ||||
|         values = [] | ||||
|         for _ in range(count_values): | ||||
|             values.append(self._obj_cb[type_values]()) | ||||
|         return values | ||||
| 
 | ||||
|     def decode(self, data, separator='\n'): | ||||
|         """Decode binary data and return list of objects.""" | ||||
|         self.data = data | ||||
|         size = len(self.data) | ||||
|         size_uncompressed = size | ||||
|         uncompressed = None | ||||
|         # uncompress data (if it is compressed) | ||||
|         compression = struct.unpack('b', self.data[4:5])[0] | ||||
|         if compression: | ||||
|             uncompressed = zlib.decompress(self.data[5:]) | ||||
|             size_uncompressed = len(uncompressed) + 5 | ||||
|             uncompressed = b'%s%s%s' % (struct.pack('>i', size_uncompressed), | ||||
|                                         struct.pack('b', 0), uncompressed) | ||||
|             self.data = uncompressed | ||||
|         else: | ||||
|             uncompressed = self.data[:] | ||||
|         # skip length and compression flag | ||||
|         self.data = self.data[5:] | ||||
|         # read id | ||||
|         msgid = self._obj_str() | ||||
|         if msgid is None: | ||||
|             msgid = '' | ||||
|         # read objects | ||||
|         objects = WeechatObjects(separator=separator) | ||||
|         while len(self.data) > 0: | ||||
|             objtype = self._obj_type() | ||||
|             value = self._obj_cb[objtype]() | ||||
|             objects.append(WeechatObject(objtype, value, separator=separator)) | ||||
|         return WeechatMessage(size, size_uncompressed, compression, | ||||
|                               uncompressed, msgid, objects) | ||||
| 
 | ||||
| 
 | ||||
| def hex_and_ascii(data, bytes_per_line=10): | ||||
|     """Convert a QByteArray to hex + ascii output.""" | ||||
|     num_lines = ((len(data) - 1) // bytes_per_line) + 1 | ||||
|     if num_lines == 0: | ||||
|         return '' | ||||
|     lines = [] | ||||
|     for i in range(num_lines): | ||||
|         str_hex = [] | ||||
|         str_ascii = [] | ||||
|         for j in range(bytes_per_line): | ||||
|             # We can't easily iterate over individual bytes, so we are going to | ||||
|             # do it this way. | ||||
|             index = (i*bytes_per_line) + j | ||||
|             char = data[index:index+1] | ||||
|             if not char: | ||||
|                 char = b'x' | ||||
|             byte = struct.unpack('B', char)[0] | ||||
|             str_hex.append(b'%02X' % int(byte)) | ||||
|             if 32 <= byte <= 127: | ||||
|                 str_ascii.append(char) | ||||
|             else: | ||||
|                 str_ascii.append(b'.') | ||||
|         fmt = b'%%-%ds %%s' % ((bytes_per_line * 3) - 1) | ||||
|         lines.append(fmt % (b' '.join(str_hex), | ||||
|                             b''.join(str_ascii))) | ||||
|     return b'\n'.join(lines) | ||||
							
								
								
									
										252
									
								
								src/qweechat/weechat/testproto.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,252 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # testproto.py - command-line program for testing WeeChat/relay protocol | ||||
| # | ||||
| # Copyright (C) 2013-2022 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| 
 | ||||
| """Command-line program for testing WeeChat/relay protocol.""" | ||||
| 
 | ||||
| import argparse | ||||
| import os | ||||
| import select | ||||
| import shlex | ||||
| import socket | ||||
| import struct | ||||
| import sys | ||||
| import time | ||||
| import traceback | ||||
| 
 | ||||
| from qweechat.weechat import protocol | ||||
| 
 | ||||
| qweechat_version = '0.1' | ||||
| 
 | ||||
| NAME = 'qweechat-testproto' | ||||
| 
 | ||||
| 
 | ||||
| class TestProto(object): | ||||
|     """Test of WeeChat/relay protocol.""" | ||||
| 
 | ||||
|     def __init__(self, args): | ||||
|         self.args = args | ||||
|         self.sock = None | ||||
|         self.has_quit = False | ||||
|         self.address = '{self.args.hostname}/{self.args.port} ' \ | ||||
|             '(IPv{0})'.format(6 if self.args.ipv6 else 4, self=self) | ||||
| 
 | ||||
|     def connect(self): | ||||
|         """ | ||||
|         Connect to WeeChat/relay. | ||||
|         Return True if OK, False if error. | ||||
|         """ | ||||
|         inet = socket.AF_INET6 if self.args.ipv6 else socket.AF_INET | ||||
|         try: | ||||
|             self.sock = socket.socket(inet, socket.SOCK_STREAM) | ||||
|             self.sock.connect((self.args.hostname, self.args.port)) | ||||
|         except Exception: | ||||
|             if self.sock: | ||||
|                 self.sock.close() | ||||
|             print('Failed to connect to', self.address) | ||||
|             return False | ||||
| 
 | ||||
|         print(f'Connected to {self.address} socket {self.sock}') | ||||
|         return True | ||||
| 
 | ||||
|     def send(self, messages): | ||||
|         """ | ||||
|         Send a text message to WeeChat/relay. | ||||
|         Return True if OK, False if error. | ||||
|         """ | ||||
|         try: | ||||
|             for msg in messages.split(b'\n'): | ||||
|                 if msg == b'quit': | ||||
|                     self.has_quit = True | ||||
|                 self.sock.sendall(msg + b'\n') | ||||
|                 sys.stdout.write( | ||||
|                     (b'\x1b[33m<-- ' + msg + b'\x1b[0m\n').decode()) | ||||
|         except Exception:  # noqa: E722 | ||||
|             traceback.print_exc() | ||||
|             print('Failed to send message') | ||||
|             return False | ||||
|         return True | ||||
| 
 | ||||
|     def decode(self, message): | ||||
|         """ | ||||
|         Decode a binary message received from WeeChat/relay. | ||||
|         Return True if OK, False if error. | ||||
|         """ | ||||
|         try: | ||||
|             proto = protocol.Protocol() | ||||
|             msgd = proto.decode(message, | ||||
|                                 separator=b'\n' if self.args.debug > 0 | ||||
|                                 else ', ') | ||||
|             print('') | ||||
|             if self.args.debug >= 2 and msgd.uncompressed: | ||||
|                 # display raw message | ||||
|                 print('\x1b[32m--> message uncompressed ({0} bytes):\n' | ||||
|                       '{1}\x1b[0m' | ||||
|                       ''.format(msgd.size_uncompressed, | ||||
|                                 protocol.hex_and_ascii(msgd.uncompressed, 20))) | ||||
|             # display decoded message | ||||
|             print('\x1b[32m--> {0}\x1b[0m'.format(msgd)) | ||||
|         except Exception:  # noqa: E722 | ||||
|             traceback.print_exc() | ||||
|             print('Error while decoding message from WeeChat') | ||||
|             return False | ||||
|         return True | ||||
| 
 | ||||
|     def send_stdin(self): | ||||
|         """ | ||||
|         Send commands from standard input if some data is available. | ||||
|         Return True if OK (it's OK if stdin has no commands), | ||||
|         False if error. | ||||
|         """ | ||||
|         inr = select.select([sys.stdin], [], [], 0)[0] | ||||
|         if inr: | ||||
|             data = os.read(sys.stdin.fileno(), 4096) | ||||
|             if data: | ||||
|                 if not self.send(data.strip()): | ||||
|                     self.sock.close() | ||||
|                     return False | ||||
|             # open stdin to read user commands | ||||
|             sys.stdin = open('/dev/tty') | ||||
|         return True | ||||
| 
 | ||||
|     def mainloop(self): | ||||
|         """ | ||||
|         Main loop: read keyboard, send commands, read socket, | ||||
|         decode/display binary messages received from WeeChat/relay. | ||||
|         Return 0 if OK, 4 if send error, 5 if decode error. | ||||
|         """ | ||||
|         if self.has_quit: | ||||
|             return 0 | ||||
|         message = b'' | ||||
|         recvbuf = b'' | ||||
|         prompt = b'\x1b[36mrelay> \x1b[0m' | ||||
|         sys.stdout.write(prompt.decode()) | ||||
|         sys.stdout.flush() | ||||
|         try: | ||||
|             while not self.has_quit: | ||||
|                 inr = select.select([sys.stdin, self.sock], [], [], 1)[0] | ||||
|                 for _file in inr: | ||||
|                     if _file == sys.stdin: | ||||
|                         buf = os.read(_file.fileno(), 4096) | ||||
|                         if buf: | ||||
|                             message += buf | ||||
|                             if b'\n' in message: | ||||
|                                 messages = message.split(b'\n') | ||||
|                                 msgsent = b'\n'.join(messages[:-1]) | ||||
|                                 if msgsent and not self.send(msgsent): | ||||
|                                     return 4 | ||||
|                                 message = messages[-1] | ||||
|                                 sys.stdout.write((prompt + message).decode()) | ||||
|                                 # sys.stdout.write(prompt + message) | ||||
|                                 sys.stdout.flush() | ||||
|                     else: | ||||
|                         buf = _file.recv(4096) | ||||
|                         if buf: | ||||
|                             recvbuf += buf | ||||
|                             while len(recvbuf) >= 4: | ||||
|                                 remainder = None | ||||
|                                 length = struct.unpack('>i', recvbuf[0:4])[0] | ||||
|                                 if len(recvbuf) < length: | ||||
|                                     # partial message, just wait for the | ||||
|                                     # end of message | ||||
|                                     break | ||||
|                                 # more than one message? | ||||
|                                 if length < len(recvbuf): | ||||
|                                     # save beginning of another message | ||||
|                                     remainder = recvbuf[length:] | ||||
|                                     recvbuf = recvbuf[0:length] | ||||
|                                 if not self.decode(recvbuf): | ||||
|                                     return 5 | ||||
|                                 if remainder: | ||||
|                                     recvbuf = remainder | ||||
|                                 else: | ||||
|                                     recvbuf = b'' | ||||
|                             sys.stdout.write((prompt + message).decode()) | ||||
|                             sys.stdout.flush() | ||||
|         except Exception:  # noqa: E722 | ||||
|             traceback.print_exc() | ||||
|             self.send(b'quit') | ||||
|         return 0 | ||||
| 
 | ||||
|     def __del__(self): | ||||
|         print('Closing connection with', self.address) | ||||
|         time.sleep(0.5) | ||||
|         self.sock.close() | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     """Main function.""" | ||||
|     # parse command line arguments | ||||
|     parser = argparse.ArgumentParser( | ||||
|         formatter_class=argparse.RawDescriptionHelpFormatter, | ||||
|         fromfile_prefix_chars='@', | ||||
|         description='Command-line program for testing WeeChat/relay protocol.', | ||||
|         epilog=''' | ||||
| Environment variable "QWEECHAT_PROTO_OPTIONS" can be set with default options. | ||||
| Argument "@file.txt" can be used to read default options in a file. | ||||
| 
 | ||||
| Some commands can be piped to the script, for example: | ||||
|   echo "init password=xxxx" | {name} localhost 5000 | ||||
|   {name} localhost 5000 < commands.txt | ||||
| 
 | ||||
| The script returns: | ||||
|   0: OK | ||||
|   2: wrong arguments (command line) | ||||
|   3: connection error | ||||
|   4: send error (message sent to WeeChat) | ||||
|   5: decode error (message received from WeeChat) | ||||
| '''.format(name=NAME)) | ||||
|     parser.add_argument('-6', '--ipv6', action='store_true', | ||||
|                         help='connect using IPv6') | ||||
|     parser.add_argument('-d', '--debug', action='count', default=0, | ||||
|                         help='debug mode: long objects view ' | ||||
|                         '(-dd: display raw messages)') | ||||
|     parser.add_argument('-v', '--version', action='version', | ||||
|                         version=qweechat_version) | ||||
|     parser.add_argument('hostname', | ||||
|                         help='hostname (or IP address) of machine running ' | ||||
|                         'WeeChat/relay') | ||||
|     parser.add_argument('port', type=int, | ||||
|                         help='port of machine running WeeChat/relay') | ||||
|     if len(sys.argv) == 1: | ||||
|         parser.print_help() | ||||
|         sys.exit(0) | ||||
|     _args = parser.parse_args( | ||||
|         shlex.split(os.getenv('QWEECHAT_PROTO_OPTIONS') or '') + sys.argv[1:]) | ||||
| 
 | ||||
|     test = TestProto(_args) | ||||
| 
 | ||||
|     # connect to WeeChat/relay | ||||
|     if not test.connect(): | ||||
|         sys.exit(3) | ||||
| 
 | ||||
|     # send commands from standard input if some data is available | ||||
|     if not test.send_stdin(): | ||||
|         sys.exit(4) | ||||
| 
 | ||||
|     # main loop (wait commands, display messages received) | ||||
|     returncode = test.mainloop() | ||||
|     del test | ||||
|     sys.exit(returncode) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
 emdee
						emdee