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