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