]> jfr.im git - irc/weechat/qweechat.git/blame - src/qweechat/qweechat.py
Add support of new message "_nicklist_diff"
[irc/weechat/qweechat.git] / src / qweechat / qweechat.py
CommitLineData
7dcf23b1
SH
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
e836cfb0
SH
4# qweechat.py - WeeChat remote GUI using Qt toolkit
5#
e17d5dc0 6# Copyright (C) 2011-2013 Sebastien Helleu <flashcode@flashtux.org>
7dcf23b1
SH
7#
8# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
9#
10# QWeeChat is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 3 of the License, or
13# (at your option) any later version.
14#
15# QWeeChat is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
22#
23
24#
e836cfb0 25# This script requires WeeChat 0.3.7 or newer, running on local or remote host.
7dcf23b1
SH
26#
27# History:
28#
29# 2011-05-27, Sebastien Helleu <flashcode@flashtux.org>:
30# start dev
31#
32
2a0b6adc 33import sys, struct, traceback
7dcf23b1
SH
34import qt_compat
35QtCore = qt_compat.import_module('QtCore')
36QtGui = qt_compat.import_module('QtGui')
37import config
38import weechat.protocol as protocol
7dcf23b1
SH
39from network import Network
40from connection import ConnectionDialog
41from buffer import BufferListWidget, Buffer
42from debug import DebugDialog
43from about import AboutDialog
44
f4848c2e 45NAME = 'QWeeChat'
8320f3a8 46VERSION = '0.0.1-dev'
7dcf23b1
SH
47AUTHOR = 'Sébastien Helleu'
48AUTHOR_MAIL= 'flashcode@flashtux.org'
49WEECHAT_SITE = 'http://www.weechat.org/'
50
cc80cd81
SH
51# number of lines in buffer for debug window
52DEBUG_NUM_LINES = 50
53
7dcf23b1
SH
54
55class MainWindow(QtGui.QMainWindow):
56 """Main window."""
57
58 def __init__(self, *args):
67ae3204 59 QtGui.QMainWindow.__init__(*(self,) + args)
7dcf23b1
SH
60
61 self.config = config.read()
62
7dcf23b1
SH
63 self.resize(1000, 600)
64 self.setWindowTitle(NAME)
65
cc80cd81
SH
66 self.debug_dialog = None
67 self.debug_lines = []
68
7dcf23b1
SH
69 # network
70 self.network = Network()
71 self.network.statusChanged.connect(self.network_status_changed)
72 self.network.messageFromWeechat.connect(self.network_message_from_weechat)
73
74 # list of buffers
75 self.list_buffers = BufferListWidget()
76 self.list_buffers.currentRowChanged.connect(self.buffer_switch)
77
78 # default buffer
79 self.buffers = [Buffer()]
80 self.stacked_buffers = QtGui.QStackedWidget()
81 self.stacked_buffers.addWidget(self.buffers[0].widget)
82
83 # splitter with buffers + chat/input
84 splitter = QtGui.QSplitter()
85 splitter.addWidget(self.list_buffers)
86 splitter.addWidget(self.stacked_buffers)
87
88 self.setCentralWidget(splitter)
89
90 if self.config.getboolean('look', 'statusbar'):
91 self.statusBar().visible = True
92
93 # actions for menu and toolbar
94 actions_def = {'connect' : ['network-connect.png', 'Connect to WeeChat', 'Ctrl+O', self.open_connection_dialog],
95 'disconnect' : ['network-disconnect.png', 'Disconnect from WeeChat', 'Ctrl+D', self.network.disconnect_weechat],
96 'debug' : ['edit-find.png', 'Debug console window', 'Ctrl+B', self.open_debug_dialog],
97 'preferences': ['preferences-other.png', 'Preferences', 'Ctrl+P', self.open_preferences_dialog],
98 'about' : ['help-about.png', 'About', 'Ctrl+H', self.open_about_dialog],
99 'quit' : ['application-exit.png', 'Quit application', 'Ctrl+Q', self.close],
100 }
101 self.actions = {}
773ee7bb 102 for name, action in list(actions_def.items()):
7dcf23b1
SH
103 self.actions[name] = QtGui.QAction(QtGui.QIcon('data/icons/%s' % action[0]), name.capitalize(), self)
104 self.actions[name].setStatusTip(action[1])
105 self.actions[name].setShortcut(action[2])
106 self.actions[name].triggered.connect(action[3])
107
108 # menu
109 self.menu = self.menuBar()
110 menu_file = self.menu.addMenu('&File')
111 menu_file.addActions([self.actions['connect'], self.actions['disconnect'],
112 self.actions['preferences'], self.actions['quit']])
113 menu_window = self.menu.addMenu('&Window')
114 menu_window.addAction(self.actions['debug'])
115 menu_help = self.menu.addMenu('&Help')
116 menu_help.addAction(self.actions['about'])
117 self.network_status = QtGui.QLabel()
118 self.network_status.setFixedHeight(20)
119 self.network_status.setFixedWidth(200)
120 self.network_status.setContentsMargins(0, 0, 10, 0)
121 self.network_status.setAlignment(QtCore.Qt.AlignRight)
122 if hasattr(self.menu, 'setCornerWidget'):
123 self.menu.setCornerWidget(self.network_status, QtCore.Qt.TopRightCorner)
124 self.network_status_set(self.network.status_disconnected, None)
125
126 # toolbar
127 toolbar = self.addToolBar('toolBar')
128 toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
129 toolbar.addActions([self.actions['connect'], self.actions['disconnect'],
130 self.actions['debug'], self.actions['preferences'],
131 self.actions['about'], self.actions['quit']])
132
133 self.buffers[0].widget.input.setFocus()
134
135 # open debug dialog
136 if self.config.getboolean('look', 'debug'):
137 self.open_debug_dialog()
138
139 # auto-connect to relay
140 if self.config.getboolean('relay', 'autoconnect'):
141 self.network.connect_weechat(self.config.get('relay', 'server'),
142 self.config.get('relay', 'port'),
77b25057 143 self.config.get('relay', 'ssl') == 'on',
7dcf23b1
SH
144 self.config.get('relay', 'password'))
145
146 self.show()
147
148 def buffer_switch(self, index):
149 if index >= 0:
150 self.stacked_buffers.setCurrentIndex(index)
151 self.stacked_buffers.widget(index).input.setFocus()
152
153 def buffer_input(self, full_name, text):
154 if self.network.is_connected():
155 message = 'input %s %s\n' % (full_name, text)
156 self.network.send_to_weechat(message)
3a5ec0c1 157 self.debug_display(0, '<==', message, forcecolor='#AA0000')
7dcf23b1
SH
158
159 def open_preferences_dialog(self):
160 pass # TODO
161
cc80cd81
SH
162 def debug_display(self, *args, **kwargs):
163 self.debug_lines.append((args, kwargs))
164 self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:]
165 if self.debug_dialog:
166 self.debug_dialog.chat.display(*args, **kwargs)
167
7dcf23b1 168 def open_debug_dialog(self):
cc80cd81
SH
169 if not self.debug_dialog:
170 self.debug_dialog = DebugDialog(self)
171 self.debug_dialog.input.textSent.connect(self.debug_input_text_sent)
172 self.debug_dialog.finished.connect(self.debug_dialog_closed)
173 self.debug_dialog.display_lines(self.debug_lines)
174 self.debug_dialog.chat.scroll_bottom()
7dcf23b1
SH
175
176 def debug_input_text_sent(self, text):
177 if self.network.is_connected():
178 text = str(text)
179 pos = text.find(')')
180 if text.startswith('(') and pos >= 0:
181 text = '(debug_%s)%s' % (text[1:pos], text[pos+1:])
182 else:
183 text = '(debug) %s' % text
3a5ec0c1 184 self.debug_display(0, '<==', text, forcecolor='#AA0000')
7dcf23b1 185 self.network.send_to_weechat(text + '\n')
7dcf23b1
SH
186
187 def debug_dialog_closed(self, result):
cc80cd81 188 self.debug_dialog = None
7dcf23b1
SH
189
190 def open_about_dialog(self):
191 messages = ['<b>%s</b> %s' % (NAME, VERSION),
e17d5dc0 192 '&copy; 2011-2013 %s &lt;<a href="mailto:%s">%s</a>&gt;' % (AUTHOR, AUTHOR_MAIL, AUTHOR_MAIL),
7dcf23b1 193 '',
b51e6ba7
SH
194 'Running with %s' % ('PySide' if qt_compat.uses_pyside else 'PyQt4'),
195 '',
7dcf23b1
SH
196 'WeeChat site: <a href="%s">%s</a>' % (WEECHAT_SITE, WEECHAT_SITE),
197 '']
198 self.about_dialog = AboutDialog(NAME, messages, self)
199
200 def open_connection_dialog(self):
201 values = {}
77b25057 202 for option in ('server', 'port', 'ssl', 'password'):
7dcf23b1
SH
203 values[option] = self.config.get('relay', option)
204 self.connection_dialog = ConnectionDialog(values, self)
205 self.connection_dialog.dialog_buttons.accepted.connect(self.connect_weechat)
206
207 def connect_weechat(self):
208 self.network.connect_weechat(self.connection_dialog.fields['server'].text(),
209 self.connection_dialog.fields['port'].text(),
77b25057 210 self.connection_dialog.fields['ssl'].isChecked(),
7dcf23b1
SH
211 self.connection_dialog.fields['password'].text())
212 self.connection_dialog.close()
213
214 def network_status_changed(self, status, extra):
215 if self.config.getboolean('look', 'statusbar'):
216 self.statusBar().showMessage(status)
3a5ec0c1 217 self.debug_display(0, '', status, forcecolor='#0000AA')
7dcf23b1
SH
218 self.network_status_set(status, extra)
219
220 def network_status_set(self, status, extra):
221 pal = self.network_status.palette()
77b25057 222 if status == self.network.status_connected:
7dcf23b1
SH
223 pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('green'))
224 else:
225 pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('#aa0000'))
77b25057 226 ssl = ' (SSL)' if status != self.network.status_disconnected and self.network.is_ssl() else ''
7dcf23b1
SH
227 self.network_status.setPalette(pal)
228 icon = self.network.status_icon(status)
229 if icon:
77b25057 230 self.network_status.setText('<img src="data/icons/%s"> %s' % (icon, status.capitalize() + ssl))
7dcf23b1
SH
231 else:
232 self.network_status.setText(status.capitalize())
233 if status == self.network.status_disconnected:
234 self.actions['connect'].setEnabled(True)
235 self.actions['disconnect'].setEnabled(False)
236 else:
237 self.actions['connect'].setEnabled(False)
238 self.actions['disconnect'].setEnabled(True)
239
240 def network_message_from_weechat(self, message):
cc80cd81
SH
241 self.debug_display(0, '==>',
242 'message (%d bytes):\n%s'
243 % (len(message), protocol.hex_and_ascii(message, 20)),
3a5ec0c1 244 forcecolor='#008800')
77b25057
SH
245 try:
246 proto = protocol.Protocol()
247 message = proto.decode(str(message))
248 if message.uncompressed:
249 self.debug_display(0, '==>',
250 'message uncompressed (%d bytes):\n%s'
251 % (message.size_uncompressed,
252 protocol.hex_and_ascii(message.uncompressed, 20)),
253 forcecolor='#008800')
254 self.debug_display(0, '', 'Message: %s' % message)
255 self.parse_message(message)
256 except:
2a0b6adc 257 print('Error while decoding message from WeeChat:\n%s' % traceback.format_exc())
77b25057 258 self.network.disconnect_weechat()
7dcf23b1
SH
259
260 def parse_message(self, message):
261 if message.msgid.startswith('debug'):
cc80cd81 262 self.debug_display(0, '', '(debug message, ignored)')
7dcf23b1
SH
263 return
264 if message.msgid == 'listbuffers':
265 for obj in message.objects:
266 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
267 self.list_buffers.clear()
268 while self.stacked_buffers.count() > 0:
269 buf = self.stacked_buffers.widget(0)
270 self.stacked_buffers.removeWidget(buf)
271 self.buffers = []
272 for item in obj.value['items']:
ca67c3a7
SH
273 buf = self.create_buffer(item)
274 self.insert_buffer(len(self.buffers), buf)
7dcf23b1
SH
275 self.list_buffers.setCurrentRow(0)
276 self.buffers[0].widget.input.setFocus()
ca67c3a7 277 elif message.msgid in ('listlines', '_buffer_line_added'):
7dcf23b1
SH
278 for obj in message.objects:
279 if obj.objtype == 'hda' and obj.value['path'][-1] == 'line_data':
280 for item in obj.value['items']:
ca67c3a7
SH
281 if message.msgid == 'listlines':
282 ptrbuf = item['__path'][0]
283 else:
284 ptrbuf = item['buffer']
285 index = [i for i, b in enumerate(self.buffers) if b.pointer() == ptrbuf]
286 if index:
287 self.buffers[index[0]].widget.chat.display(item['date'],
3a5ec0c1
SH
288 item['prefix'],
289 item['message'])
ca67c3a7 290 elif message.msgid in ('_nicklist', 'nicklist'):
2a0b6adc 291 buffer_refresh = {}
7dcf23b1 292 for obj in message.objects:
ca67c3a7 293 if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item':
2a0b6adc 294 group = '__root'
7dcf23b1 295 for item in obj.value['items']:
ca67c3a7
SH
296 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
297 if index:
2a0b6adc
SH
298 if not index[0] in buffer_refresh:
299 self.buffers[index[0]].nicklist = {}
300 buffer_refresh[index[0]] = True
301 if item['group']:
302 group = item['name']
303 self.buffers[index[0]].nicklist_add_item(group, item['group'], item['prefix'], item['name'], item['visible'])
304 for index in buffer_refresh:
305 self.buffers[index].nicklist_refresh()
306 elif message.msgid == '_nicklist_diff':
307 buffer_refresh = {}
308 for obj in message.objects:
309 if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item':
310 group = '__root'
311 for item in obj.value['items']:
312 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
313 if index:
314 buffer_refresh[index[0]] = True
315 if item['_diff'] == ord('^'):
316 group = item['name']
317 elif item['_diff'] == ord('+'):
318 self.buffers[index[0]].nicklist_add_item(group, item['group'], item['prefix'], item['name'], item['visible'])
319 elif item['_diff'] == ord('-'):
320 self.buffers[index[0]].nicklist_remove_item(group, item['group'], item['name'])
321 elif item['_diff'] == ord('*'):
322 self.buffers[index[0]].nicklist_update_item(group, item['group'], item['prefix'], item['name'], item['visible'])
323 for index in buffer_refresh:
324 self.buffers[index].nicklist_refresh()
ca67c3a7
SH
325 elif message.msgid == '_buffer_opened':
326 for obj in message.objects:
327 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
328 for item in obj.value['items']:
329 buf = self.create_buffer(item)
330 index = self.find_buffer_index_for_insert(item['next_buffer'])
331 self.insert_buffer(index, buf)
332 elif message.msgid.startswith('_buffer_'):
333 for obj in message.objects:
334 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
335 for item in obj.value['items']:
336 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
337 if index:
338 index = index[0]
97936653
SH
339 if message.msgid == '_buffer_type_changed':
340 self.buffers[index].data['type'] = item['type']
341 elif message.msgid in ('_buffer_moved', '_buffer_merged', '_buffer_unmerged'):
ca67c3a7
SH
342 buf = self.buffers[index]
343 buf.data['number'] = item['number']
344 self.remove_buffer(index)
345 index2 = self.find_buffer_index_for_insert(item['next_buffer'])
346 self.insert_buffer(index2, buf)
347 elif message.msgid == '_buffer_renamed':
348 self.buffers[index].data['full_name'] = item['full_name']
349 self.buffers[index].data['short_name'] = item['short_name']
350 elif message.msgid == '_buffer_title_changed':
351 self.buffers[index].data['title'] = item['title']
c728febd
SH
352 self.buffers[index].update_title()
353 elif message.msgid.startswith('_buffer_localvar_'):
354 self.buffers[index].data['local_variables'] = item['local_variables']
355 self.buffers[index].update_prompt()
ca67c3a7
SH
356 elif message.msgid == '_buffer_closing':
357 self.remove_buffer(index)
beaa8775
SH
358 elif message.msgid == '_upgrade':
359 self.network.desync_weechat()
360 elif message.msgid == '_upgrade_ended':
361 self.network.sync_weechat()
ca67c3a7
SH
362
363 def create_buffer(self, item):
364 buf = Buffer(item)
365 buf.bufferInput.connect(self.buffer_input)
366 buf.widget.input.bufferSwitchPrev.connect(self.list_buffers.switch_prev_buffer)
367 buf.widget.input.bufferSwitchNext.connect(self.list_buffers.switch_next_buffer)
368 return buf
369
370 def insert_buffer(self, index, buf):
371 self.buffers.insert(index, buf)
372 self.list_buffers.insertItem(index, '%d. %s' % (buf.data['number'], buf.data['full_name']))
373 self.stacked_buffers.insertWidget(index, buf.widget)
374
375 def remove_buffer(self, index):
376 if self.list_buffers.currentRow == index and index > 0:
377 self.list_buffers.setCurrentRow(index - 1)
378 self.list_buffers.takeItem(index)
379 self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index))
380 self.buffers.pop(index)
381
382 def find_buffer_index_for_insert(self, next_buffer):
383 index = -1
384 if next_buffer == '0x0':
385 index = len(self.buffers)
386 else:
387 index = [i for i, b in enumerate(self.buffers) if b.pointer() == next_buffer]
388 if index:
389 index = index[0]
390 if index < 0:
391 print('Warning: unable to find position for buffer, using end of list by default')
392 index = len(self.buffers)
393 return index
7dcf23b1
SH
394
395 def closeEvent(self, event):
396 self.network.disconnect_weechat()
cc80cd81
SH
397 if self.debug_dialog:
398 self.debug_dialog.close()
7dcf23b1
SH
399 config.write(self.config)
400 QtGui.QMainWindow.closeEvent(self, event)
401
402
403app = QtGui.QApplication(sys.argv)
404app.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
93865c21 405app.setWindowIcon(QtGui.QIcon('data/icons/weechat_icon_32.png'))
7dcf23b1
SH
406main = MainWindow()
407sys.exit(app.exec_())