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