]> jfr.im git - irc/weechat/qweechat.git/blame - qweechat/qweechat.py
Fix PEP8 errors
[irc/weechat/qweechat.git] / qweechat / qweechat.py
CommitLineData
77df9d06
SH
1# -*- coding: utf-8 -*-
2#
3# qweechat.py - WeeChat remote GUI using Qt toolkit
4#
bcea4dd1 5# Copyright (C) 2011-2016 Sébastien Helleu <flashcode@flashtux.org>
77df9d06
SH
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"""
24QWeeChat is a WeeChat remote GUI using Qt toolkit.
25
26It 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
36import sys
37import traceback
38from pkg_resources import resource_filename
39import qt_compat
77df9d06
SH
40import config
41import weechat.protocol as protocol
42from network import Network
43from connection import ConnectionDialog
44from buffer import BufferListWidget, Buffer
45from debug import DebugDialog
46from about import AboutDialog
184c3dc6 47from version import qweechat_version
77df9d06 48
356719e0
SH
49QtCore = qt_compat.import_module('QtCore')
50QtGui = qt_compat.import_module('QtGui')
51
77df9d06 52NAME = 'QWeeChat'
77df9d06
SH
53AUTHOR = 'Sébastien Helleu'
54AUTHOR_MAIL = 'flashcode@flashtux.org'
9036a59a 55WEECHAT_SITE = 'https://weechat.org/'
77df9d06
SH
56
57# number of lines in buffer for debug window
58DEBUG_NUM_LINES = 50
59
60
356719e0 61class MainWindow(QtGui.QMainWindow):
77df9d06
SH
62 """Main window."""
63
64 def __init__(self, *args):
356719e0 65 QtGui.QMainWindow.__init__(*(self,) + args)
77df9d06
SH
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
0bd929dc 77 self.preferences_dialog = None
77df9d06
SH
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()]
356719e0 90 self.stacked_buffers = QtGui.QStackedWidget()
77df9d06
SH
91 self.stacked_buffers.addWidget(self.buffers[0].widget)
92
93 # splitter with buffers + chat/input
356719e0 94 splitter = QtGui.QSplitter()
77df9d06
SH
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()):
356719e0
SH
129 self.actions[name] = QtGui.QAction(
130 QtGui.QIcon(
77df9d06
SH
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'])
356719e0 149 self.network_status = QtGui.QLabel()
77df9d06
SH
150 self.network_status.setFixedHeight(20)
151 self.network_status.setFixedWidth(200)
152 self.network_status.setContentsMargins(0, 0, 10, 0)
356719e0 153 self.network_status.setAlignment(QtCore.Qt.AlignRight)
77df9d06
SH
154 if hasattr(self.menu, 'setCornerWidget'):
155 self.menu.setCornerWidget(self.network_status,
356719e0 156 QtCore.Qt.TopRightCorner)
77df9d06
SH
157 self.network_status_set(self.network.status_disconnected)
158
159 # toolbar
160 toolbar = self.addToolBar('toolBar')
356719e0 161 toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
77df9d06
SH
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."""
0bd929dc
SH
201 # TODO: implement the preferences dialog box
202 messages = ['Not yet implemented!',
203 '']
204 self.preferences_dialog = AboutDialog('Preferences', messages, self)
77df9d06
SH
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."""
184c3dc6 248 messages = ['<b>%s</b> %s' % (NAME, qweechat_version()),
bcea4dd1 249 '&copy; 2011-2016 %s &lt;<a href="mailto:%s">%s</a>&gt;'
77df9d06
SH
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(),
356719e0 291 QtGui.QColor('green'))
77df9d06
SH
292 else:
293 pal.setColor(self.network_status.foregroundRole(),
356719e0 294 QtGui.QColor('#aa0000'))
77df9d06
SH
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:
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:
e38ef663
SH
367 lines.append(
368 (index[0],
369 (item['date'], item['prefix'],
370 item['message']))
371 )
77df9d06
SH
372 if message.msgid == 'listlines':
373 lines.reverse()
374 for line in lines:
e38ef663 375 self.buffers[line[0]].widget.chat.display(*line[1])
77df9d06
SH
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()
8217c4e2
SH
469 elif message.msgid == '_buffer_cleared':
470 self.buffers[index].widget.chat.clear()
77df9d06
SH
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)
356719e0 547 QtGui.QMainWindow.closeEvent(self, event)
77df9d06
SH
548
549
356719e0
SH
550app = QtGui.QApplication(sys.argv)
551app.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
552app.setWindowIcon(QtGui.QIcon(
77df9d06
SH
553 resource_filename(__name__, 'data/icons/weechat_icon_32.png')))
554main = MainWindow()
555sys.exit(app.exec_())