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