]> jfr.im git - irc/weechat/qweechat.git/blob - qweechat/qweechat.py
Fix the about dialog.
[irc/weechat/qweechat.git] / qweechat / qweechat.py
1 # -*- coding: utf-8 -*-
2 #
3 # qweechat.py - WeeChat remote GUI using Qt toolkit
4 #
5 # Copyright (C) 2011-2021 Sébastien Helleu <flashcode@flashtux.org>
6 #
7 # This file is part of QWeeChat, a Qt remote GUI for WeeChat.
8 #
9 # QWeeChat is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # QWeeChat is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
21 #
22
23 """
24 QWeeChat is a WeeChat remote GUI using Qt toolkit.
25
26 It requires requires WeeChat 0.3.7 or newer, running on local or remote host.
27 """
28
29 #
30 # History:
31 #
32 # 2011-05-27, Sébastien Helleu <flashcode@flashtux.org>:
33 # start dev
34 #
35
36 import sys
37 import traceback
38 from pkg_resources import resource_filename
39 # import qt_compat
40 from qweechat import config
41 from qweechat.weechat import protocol
42 from qweechat.network import Network
43 from qweechat.connection import ConnectionDialog
44 from qweechat.buffer import BufferListWidget, Buffer
45 from qweechat.debug import DebugDialog
46 from qweechat.about import AboutDialog
47 from qweechat.version import qweechat_version
48
49 from PySide6.QtWidgets import (
50 QApplication, QLabel, QPushButton, QVBoxLayout, QWidget)
51 from PySide6.QtCore import Qt, Slot
52 from PySide6 import QtGui, QtWidgets, QtCore
53
54
55 # QtCore = qt_compat.import_module('QtCore')
56 # QtGui = qt_compat.import_module('QtGui')
57
58 NAME = 'QWeeChat'
59 AUTHOR = 'Sébastien Helleu'
60 AUTHOR_MAIL = 'flashcode@flashtux.org'
61 WEECHAT_SITE = 'https://weechat.org/'
62
63 # number of lines in buffer for debug window
64 DEBUG_NUM_LINES = 50
65
66
67 class MainWindow(QtWidgets.QMainWindow):
68 """Main window."""
69
70 def __init__(self, *args):
71 super().__init__()
72
73 self.config = config.read()
74
75 self.resize(1000, 600)
76 self.setWindowTitle(NAME)
77
78 self.debug_dialog = None
79 self.debug_lines = []
80
81 self.about_dialog = None
82 self.connection_dialog = None
83 self.preferences_dialog = None
84
85 # network
86 self.network = Network()
87 self.network.statusChanged.connect(self._network_status_changed)
88 self.network.messageFromWeechat.connect(self._network_weechat_msg)
89
90 # list of buffers
91 self.list_buffers = BufferListWidget()
92 self.list_buffers.currentRowChanged.connect(self._buffer_switch)
93
94 # default buffer
95 self.buffers = [Buffer()]
96 self.stacked_buffers = QtWidgets.QStackedWidget()
97 self.stacked_buffers.addWidget(self.buffers[0].widget)
98
99 # splitter with buffers + chat/input
100 splitter = QtWidgets.QSplitter()
101 splitter.addWidget(self.list_buffers)
102 splitter.addWidget(self.stacked_buffers)
103
104 self.setCentralWidget(splitter)
105
106 if self.config.getboolean('look', 'statusbar'):
107 self.statusBar().visible = True
108
109 # actions for menu and toolbar
110 actions_def = {
111 'connect': [
112 'network-connect.png', 'Connect to WeeChat',
113 'Ctrl+O', self.open_connection_dialog],
114 'disconnect': [
115 'network-disconnect.png', 'Disconnect from WeeChat',
116 'Ctrl+D', self.network.disconnect_weechat],
117 'debug': [
118 'edit-find.png', 'Debug console window',
119 'Ctrl+B', self.open_debug_dialog],
120 'preferences': [
121 'preferences-other.png', 'Preferences',
122 'Ctrl+P', self.open_preferences_dialog],
123 'about': [
124 'help-about.png', 'About',
125 'Ctrl+H', self.open_about_dialog],
126 'save connection': [
127 'document-save.png', 'Save connection configuration',
128 'Ctrl+S', self.save_connection],
129 'quit': [
130 'application-exit.png', 'Quit application',
131 'Ctrl+Q', self.close],
132 }
133 self.actions = {}
134 for name, action in list(actions_def.items()):
135 self.actions[name] = QtGui.QAction(
136 QtGui.QIcon(
137 resource_filename(__name__, 'data/icons/%s' % action[0])),
138 name.capitalize(), self)
139 self.actions[name].setStatusTip(action[1])
140 self.actions[name].setShortcut(action[2])
141 self.actions[name].triggered.connect(action[3])
142
143 # menu
144 self.menu = self.menuBar()
145 menu_file = self.menu.addMenu('&File')
146 menu_file.addActions([self.actions['connect'],
147 self.actions['disconnect'],
148 self.actions['preferences'],
149 self.actions['save connection'],
150 self.actions['quit']])
151 menu_window = self.menu.addMenu('&Window')
152 menu_window.addAction(self.actions['debug'])
153 menu_help = self.menu.addMenu('&Help')
154 menu_help.addAction(self.actions['about'])
155 self.network_status = QtWidgets.QLabel()
156 self.network_status.setFixedHeight(20)
157 self.network_status.setFixedWidth(200)
158 self.network_status.setContentsMargins(0, 0, 10, 0)
159 self.network_status.setAlignment(QtCore.Qt.AlignRight)
160 if hasattr(self.menu, 'setCornerWidget'):
161 self.menu.setCornerWidget(self.network_status,
162 QtCore.Qt.TopRightCorner)
163 self.network_status_set(self.network.status_disconnected)
164
165 # toolbar
166 toolbar = self.addToolBar('toolBar')
167 toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
168 toolbar.addActions([self.actions['connect'],
169 self.actions['disconnect'],
170 self.actions['debug'],
171 self.actions['preferences'],
172 self.actions['about'],
173 self.actions['quit']])
174
175 self.buffers[0].widget.input.setFocus()
176
177 # open debug dialog
178 if self.config.getboolean('look', 'debug'):
179 self.open_debug_dialog()
180
181 # auto-connect to relay
182 if self.config.getboolean('relay', 'autoconnect'):
183 self.network.connect_weechat(self.config.get('relay', 'server'),
184 self.config.get('relay', 'port'),
185 self.config.getboolean('relay',
186 'ssl'),
187 self.config.get('relay', 'password'),
188 self.config.get('relay', 'lines'))
189
190 self.show()
191
192 def _buffer_switch(self, index):
193 """Switch to a buffer."""
194 if index >= 0:
195 self.stacked_buffers.setCurrentIndex(index)
196 self.stacked_buffers.widget(index).input.setFocus()
197
198 def buffer_input(self, full_name, text):
199 """Send buffer input to WeeChat."""
200 if self.network.is_connected():
201 message = 'input %s %s\n' % (full_name, text)
202 self.network.send_to_weechat(message)
203 self.debug_display(0, '<==', message, forcecolor='#AA0000')
204
205 def open_preferences_dialog(self):
206 """Open a dialog with preferences."""
207 # TODO: implement the preferences dialog box
208 messages = ['Not yet implemented!',
209 '']
210 self.preferences_dialog = AboutDialog('Preferences', messages, self)
211
212 def save_connection(self):
213 """Save connection configuration."""
214 if self.network:
215 options = self.network.get_options()
216 for option in options.keys():
217 self.config.set('relay', option, options[option])
218
219 def debug_display(self, *args, **kwargs):
220 """Display a debug message."""
221 self.debug_lines.append((args, kwargs))
222 self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:]
223 if self.debug_dialog:
224 self.debug_dialog.chat.display(*args, **kwargs)
225
226 def open_debug_dialog(self):
227 """Open a dialog with debug messages."""
228 if not self.debug_dialog:
229 self.debug_dialog = DebugDialog(self)
230 self.debug_dialog.input.textSent.connect(
231 self.debug_input_text_sent)
232 self.debug_dialog.finished.connect(self._debug_dialog_closed)
233 self.debug_dialog.display_lines(self.debug_lines)
234 self.debug_dialog.chat.scroll_bottom()
235
236 def debug_input_text_sent(self, text):
237 """Send debug buffer input to WeeChat."""
238 if self.network.is_connected():
239 text = str(text)
240 pos = text.find(')')
241 if text.startswith('(') and pos >= 0:
242 text = '(debug_%s)%s' % (text[1:pos], text[pos+1:])
243 else:
244 text = '(debug) %s' % text
245 self.debug_display(0, '<==', text, forcecolor='#AA0000')
246 self.network.send_to_weechat(text + '\n')
247
248 def _debug_dialog_closed(self, result):
249 """Called when debug dialog is closed."""
250 self.debug_dialog = None
251
252 def open_about_dialog(self):
253 """Open a dialog with info about QWeeChat."""
254 messages = ['<b>%s</b> %s' % (NAME, qweechat_version()),
255 '&copy; 2011-2020 %s &lt;<a href="mailto:%s">%s</a>&gt;'
256 % (AUTHOR, AUTHOR_MAIL, AUTHOR_MAIL),
257 '',
258 'Running with PySide6',
259 '',
260 'WeeChat site: <a href="%s">%s</a>'
261 % (WEECHAT_SITE, WEECHAT_SITE),
262 '']
263 self.about_dialog = AboutDialog(NAME, messages, self)
264
265 def open_connection_dialog(self):
266 """Open a dialog with connection settings."""
267 values = {}
268 for option in ('server', 'port', 'ssl', 'password', 'lines'):
269 values[option] = self.config.get('relay', option)
270 self.connection_dialog = ConnectionDialog(values, self)
271 self.connection_dialog.dialog_buttons.accepted.connect(
272 self.connect_weechat)
273
274 def connect_weechat(self):
275 """Connect to WeeChat."""
276 self.network.connect_weechat(
277 self.connection_dialog.fields['server'].text(),
278 self.connection_dialog.fields['port'].text(),
279 self.connection_dialog.fields['ssl'].isChecked(),
280 self.connection_dialog.fields['password'].text(),
281 int(self.connection_dialog.fields['lines'].text()))
282 self.connection_dialog.close()
283
284 def _network_status_changed(self, status, extra):
285 """Called when the network status has changed."""
286 if self.config.getboolean('look', 'statusbar'):
287 self.statusBar().showMessage(status)
288 self.debug_display(0, '', status, forcecolor='#0000AA')
289 self.network_status_set(status)
290
291 def network_status_set(self, status):
292 """Set the network status."""
293 pal = self.network_status.palette()
294 if status == self.network.status_connected:
295 pal.setColor(self.network_status.foregroundRole(),
296 QtGui.QColor('green'))
297 else:
298 pal.setColor(self.network_status.foregroundRole(),
299 QtGui.QColor('#aa0000'))
300 ssl = ' (SSL)' if status != self.network.status_disconnected \
301 and self.network.is_ssl() else ''
302 self.network_status.setPalette(pal)
303 icon = self.network.status_icon(status)
304 if icon:
305 self.network_status.setText(
306 '<img src="%s"> %s' %
307 (resource_filename(__name__, 'data/icons/%s' % icon),
308 status.capitalize() + ssl))
309 else:
310 self.network_status.setText(status.capitalize())
311 if status == self.network.status_disconnected:
312 self.actions['connect'].setEnabled(True)
313 self.actions['disconnect'].setEnabled(False)
314 else:
315 self.actions['connect'].setEnabled(False)
316 self.actions['disconnect'].setEnabled(True)
317
318 def _network_weechat_msg(self, message):
319 """Called when a message is received from WeeChat."""
320 # self.debug_display(0, '==>',
321 # 'message (%d bytes):\n%s'
322 # % (len(message),
323 # protocol.hex_and_ascii(message, 20)),
324 # forcecolor='#008800')
325 try:
326 proto = protocol.Protocol()
327 message = proto.decode(message.data())
328 if message.uncompressed:
329 self.debug_display(
330 0, '==>',
331 'message uncompressed (%d bytes):\n%s'
332 % (message.size_uncompressed,
333 protocol.hex_and_ascii(message.uncompressed, 20)),
334 forcecolor='#008800')
335 self.debug_display(0, '', 'Message: %s' % message)
336 self.parse_message(message)
337 except Exception: # noqa: E722
338 print('Error while decoding message from WeeChat:\n%s'
339 % traceback.format_exc())
340 self.network.disconnect_weechat()
341
342 def _parse_listbuffers(self, message):
343 """Parse a WeeChat with list of buffers."""
344 for obj in message.objects:
345 if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
346 continue
347 self.list_buffers.clear()
348 while self.stacked_buffers.count() > 0:
349 buf = self.stacked_buffers.widget(0)
350 self.stacked_buffers.removeWidget(buf)
351 self.buffers = []
352 for item in obj.value['items']:
353 buf = self.create_buffer(item)
354 self.insert_buffer(len(self.buffers), buf)
355 self.list_buffers.setCurrentRow(0)
356 self.buffers[0].widget.input.setFocus()
357
358 def _parse_line(self, message):
359 """Parse a WeeChat message with a buffer line."""
360 for obj in message.objects:
361 lines = []
362 if obj.objtype != 'hda' or obj.value['path'][-1] != 'line_data':
363 continue
364 for item in obj.value['items']:
365 if message.msgid == 'listlines':
366 ptrbuf = item['__path'][0]
367 else:
368 ptrbuf = item['buffer']
369 index = [i for i, b in enumerate(self.buffers)
370 if b.pointer() == ptrbuf]
371 if index:
372 lines.append(
373 (index[0],
374 (item['date'], item['prefix'],
375 item['message']))
376 )
377 if message.msgid == 'listlines':
378 lines.reverse()
379 for line in lines:
380 self.buffers[line[0]].widget.chat.display(*line[1])
381
382 def _parse_nicklist(self, message):
383 """Parse a WeeChat message with a buffer nicklist."""
384 buffer_refresh = {}
385 for obj in message.objects:
386 if obj.objtype != 'hda' or \
387 obj.value['path'][-1] != 'nicklist_item':
388 continue
389 group = '__root'
390 for item in obj.value['items']:
391 index = [i for i, b in enumerate(self.buffers)
392 if b.pointer() == item['__path'][0]]
393 if index:
394 if not index[0] in buffer_refresh:
395 self.buffers[index[0]].nicklist = {}
396 buffer_refresh[index[0]] = True
397 if item['group']:
398 group = item['name']
399 self.buffers[index[0]].nicklist_add_item(
400 group, item['group'], item['prefix'], item['name'],
401 item['visible'])
402 for index in buffer_refresh:
403 self.buffers[index].nicklist_refresh()
404
405 def _parse_nicklist_diff(self, message):
406 """Parse a WeeChat message with a buffer nicklist diff."""
407 buffer_refresh = {}
408 for obj in message.objects:
409 if obj.objtype != 'hda' or \
410 obj.value['path'][-1] != 'nicklist_item':
411 continue
412 group = '__root'
413 for item in obj.value['items']:
414 index = [i for i, b in enumerate(self.buffers)
415 if b.pointer() == item['__path'][0]]
416 if not index:
417 continue
418 buffer_refresh[index[0]] = True
419 if item['_diff'] == ord('^'):
420 group = item['name']
421 elif item['_diff'] == ord('+'):
422 self.buffers[index[0]].nicklist_add_item(
423 group, item['group'], item['prefix'], item['name'],
424 item['visible'])
425 elif item['_diff'] == ord('-'):
426 self.buffers[index[0]].nicklist_remove_item(
427 group, item['group'], item['name'])
428 elif item['_diff'] == ord('*'):
429 self.buffers[index[0]].nicklist_update_item(
430 group, item['group'], item['prefix'], item['name'],
431 item['visible'])
432 for index in buffer_refresh:
433 self.buffers[index].nicklist_refresh()
434
435 def _parse_buffer_opened(self, message):
436 """Parse a WeeChat message with a new buffer (opened)."""
437 for obj in message.objects:
438 if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
439 continue
440 for item in obj.value['items']:
441 buf = self.create_buffer(item)
442 index = self.find_buffer_index_for_insert(item['next_buffer'])
443 self.insert_buffer(index, buf)
444
445 def _parse_buffer(self, message):
446 """Parse a WeeChat message with a buffer event
447 (anything except a new buffer).
448 """
449 for obj in message.objects:
450 if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
451 continue
452 for item in obj.value['items']:
453 index = [i for i, b in enumerate(self.buffers)
454 if b.pointer() == item['__path'][0]]
455 if not index:
456 continue
457 index = index[0]
458 if message.msgid == '_buffer_type_changed':
459 self.buffers[index].data['type'] = item['type']
460 elif message.msgid in ('_buffer_moved', '_buffer_merged',
461 '_buffer_unmerged'):
462 buf = self.buffers[index]
463 buf.data['number'] = item['number']
464 self.remove_buffer(index)
465 index2 = self.find_buffer_index_for_insert(
466 item['next_buffer'])
467 self.insert_buffer(index2, buf)
468 elif message.msgid == '_buffer_renamed':
469 self.buffers[index].data['full_name'] = item['full_name']
470 self.buffers[index].data['short_name'] = item['short_name']
471 elif message.msgid == '_buffer_title_changed':
472 self.buffers[index].data['title'] = item['title']
473 self.buffers[index].update_title()
474 elif message.msgid == '_buffer_cleared':
475 self.buffers[index].widget.chat.clear()
476 elif message.msgid.startswith('_buffer_localvar_'):
477 self.buffers[index].data['local_variables'] = \
478 item['local_variables']
479 self.buffers[index].update_prompt()
480 elif message.msgid == '_buffer_closing':
481 self.remove_buffer(index)
482
483 def parse_message(self, message):
484 """Parse a WeeChat message."""
485 if message.msgid.startswith('debug'):
486 self.debug_display(0, '', '(debug message, ignored)')
487 elif message.msgid == 'listbuffers':
488 self._parse_listbuffers(message)
489 elif message.msgid in ('listlines', '_buffer_line_added'):
490 self._parse_line(message)
491 elif message.msgid in ('_nicklist', 'nicklist'):
492 self._parse_nicklist(message)
493 elif message.msgid == '_nicklist_diff':
494 self._parse_nicklist_diff(message)
495 elif message.msgid == '_buffer_opened':
496 self._parse_buffer_opened(message)
497 elif message.msgid.startswith('_buffer_'):
498 self._parse_buffer(message)
499 elif message.msgid == '_upgrade':
500 self.network.desync_weechat()
501 elif message.msgid == '_upgrade_ended':
502 self.network.sync_weechat()
503 else:
504 print(f"Unknown message with id {message.msgid}")
505
506 def create_buffer(self, item):
507 """Create a new buffer."""
508 buf = Buffer(item)
509 buf.bufferInput.connect(self.buffer_input)
510 buf.widget.input.bufferSwitchPrev.connect(
511 self.list_buffers.switch_prev_buffer)
512 buf.widget.input.bufferSwitchNext.connect(
513 self.list_buffers.switch_next_buffer)
514 return buf
515
516 def insert_buffer(self, index, buf):
517 """Insert a buffer in list."""
518 self.buffers.insert(index, buf)
519 self.list_buffers.insertItem(index, '%d. %s'
520 % (buf.data['number'],
521 buf.data['full_name']))
522 self.stacked_buffers.insertWidget(index, buf.widget)
523
524 def remove_buffer(self, index):
525 """Remove a buffer."""
526 if self.list_buffers.currentRow == index and index > 0:
527 self.list_buffers.setCurrentRow(index - 1)
528 self.list_buffers.takeItem(index)
529 self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index))
530 self.buffers.pop(index)
531
532 def find_buffer_index_for_insert(self, next_buffer):
533 """Find position to insert a buffer in list."""
534 index = -1
535 if next_buffer == '0x0':
536 index = len(self.buffers)
537 else:
538 index = [i for i, b in enumerate(self.buffers)
539 if b.pointer() == next_buffer]
540 if index:
541 index = index[0]
542 if index < 0:
543 print('Warning: unable to find position for buffer, using end of '
544 'list by default')
545 index = len(self.buffers)
546 return index
547
548 def closeEvent(self, event):
549 """Called when QWeeChat window is closed."""
550 self.network.disconnect_weechat()
551 if self.debug_dialog:
552 self.debug_dialog.close()
553 config.write(self.config)
554 QtWidgets.QMainWindow.closeEvent(self, event)
555
556 def main():
557 app = QApplication(sys.argv)
558 app.setStyle(QtWidgets.QStyleFactory.create('Cleanlooks'))
559 app.setWindowIcon(QtGui.QIcon(
560 resource_filename(__name__, 'data/icons/weechat.png')))
561 main = MainWindow()
562 main.show()
563 sys.exit(app.exec_())
564
565
566 if __name__ == '__main__':
567 main()