]> jfr.im git - irc/weechat/qweechat.git/blob - src/qweechat/qweechat.py
Move comments with description of python file to top of files
[irc/weechat/qweechat.git] / src / qweechat / qweechat.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 #
4 # qweechat.py - WeeChat remote GUI using Qt toolkit
5 #
6 # Copyright (C) 2011-2013 Sebastien Helleu <flashcode@flashtux.org>
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 #
25 # This script requires WeeChat 0.3.7 or newer, running on local or remote host.
26 #
27 # History:
28 #
29 # 2011-05-27, Sebastien Helleu <flashcode@flashtux.org>:
30 # start dev
31 #
32
33 import sys, struct
34 import qt_compat
35 QtCore = qt_compat.import_module('QtCore')
36 QtGui = qt_compat.import_module('QtGui')
37 import config
38 import weechat.protocol as protocol
39 from network import Network
40 from connection import ConnectionDialog
41 from buffer import BufferListWidget, Buffer
42 from debug import DebugDialog
43 from about import AboutDialog
44
45 NAME = 'QWeeChat'
46 VERSION = '0.0.1-dev'
47 AUTHOR = 'Sébastien Helleu'
48 AUTHOR_MAIL= 'flashcode@flashtux.org'
49 WEECHAT_SITE = 'http://www.weechat.org/'
50
51 # number of lines in buffer for debug window
52 DEBUG_NUM_LINES = 50
53
54
55 class MainWindow(QtGui.QMainWindow):
56 """Main window."""
57
58 def __init__(self, *args):
59 QtGui.QMainWindow.__init__(*(self,) + args)
60
61 self.config = config.read()
62
63 self.resize(1000, 600)
64 self.setWindowTitle(NAME)
65
66 self.debug_dialog = None
67 self.debug_lines = []
68
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 = {}
102 for name, action in list(actions_def.items()):
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'),
143 self.config.get('relay', 'ssl') == 'on',
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)
157 self.debug_display(0, '<==', message, forcecolor='#AA0000')
158
159 def open_preferences_dialog(self):
160 pass # TODO
161
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
168 def open_debug_dialog(self):
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()
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
184 self.debug_display(0, '<==', text, forcecolor='#AA0000')
185 self.network.send_to_weechat(text + '\n')
186
187 def debug_dialog_closed(self, result):
188 self.debug_dialog = None
189
190 def open_about_dialog(self):
191 messages = ['<b>%s</b> %s' % (NAME, VERSION),
192 '&copy; 2011-2013 %s &lt;<a href="mailto:%s">%s</a>&gt;' % (AUTHOR, AUTHOR_MAIL, AUTHOR_MAIL),
193 '',
194 'Running with %s' % ('PySide' if qt_compat.uses_pyside else 'PyQt4'),
195 '',
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 = {}
202 for option in ('server', 'port', 'ssl', 'password'):
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(),
210 self.connection_dialog.fields['ssl'].isChecked(),
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)
217 self.debug_display(0, '', status, forcecolor='#0000AA')
218 self.network_status_set(status, extra)
219
220 def network_status_set(self, status, extra):
221 pal = self.network_status.palette()
222 if status == self.network.status_connected:
223 pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('green'))
224 else:
225 pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('#aa0000'))
226 ssl = ' (SSL)' if status != self.network.status_disconnected and self.network.is_ssl() else ''
227 self.network_status.setPalette(pal)
228 icon = self.network.status_icon(status)
229 if icon:
230 self.network_status.setText('<img src="data/icons/%s"> %s' % (icon, status.capitalize() + ssl))
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):
241 self.debug_display(0, '==>',
242 'message (%d bytes):\n%s'
243 % (len(message), protocol.hex_and_ascii(message, 20)),
244 forcecolor='#008800')
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:
257 print("Error while decoding message from WeeChat")
258 self.network.disconnect_weechat()
259
260 def parse_message(self, message):
261 if message.msgid.startswith('debug'):
262 self.debug_display(0, '', '(debug message, ignored)')
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']:
273 buf = self.create_buffer(item)
274 self.insert_buffer(len(self.buffers), buf)
275 self.list_buffers.setCurrentRow(0)
276 self.buffers[0].widget.input.setFocus()
277 elif message.msgid in ('listlines', '_buffer_line_added'):
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']:
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'],
288 item['prefix'],
289 item['message'])
290 elif message.msgid in ('_nicklist', 'nicklist'):
291 buffer_nicklist = {}
292 for obj in message.objects:
293 if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item':
294 for item in obj.value['items']:
295 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
296 if index:
297 if not item['__path'][0] in buffer_nicklist:
298 self.buffers[index[0]].remove_all_nicks()
299 buffer_nicklist[item['__path'][0]] = True
300 if not item['group'] and item['visible']:
301 self.buffers[index[0]].add_nick(item['prefix'], item['name'])
302 elif message.msgid == '_buffer_opened':
303 for obj in message.objects:
304 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
305 for item in obj.value['items']:
306 buf = self.create_buffer(item)
307 index = self.find_buffer_index_for_insert(item['next_buffer'])
308 self.insert_buffer(index, buf)
309 elif message.msgid.startswith('_buffer_'):
310 for obj in message.objects:
311 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
312 for item in obj.value['items']:
313 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
314 if index:
315 index = index[0]
316 if message.msgid == '_buffer_type_changed':
317 self.buffers[index].data['type'] = item['type']
318 elif message.msgid in ('_buffer_moved', '_buffer_merged', '_buffer_unmerged'):
319 buf = self.buffers[index]
320 buf.data['number'] = item['number']
321 self.remove_buffer(index)
322 index2 = self.find_buffer_index_for_insert(item['next_buffer'])
323 self.insert_buffer(index2, buf)
324 elif message.msgid == '_buffer_renamed':
325 self.buffers[index].data['full_name'] = item['full_name']
326 self.buffers[index].data['short_name'] = item['short_name']
327 elif message.msgid == '_buffer_title_changed':
328 self.buffers[index].data['title'] = item['title']
329 self.buffers[index].update_title()
330 elif message.msgid.startswith('_buffer_localvar_'):
331 self.buffers[index].data['local_variables'] = item['local_variables']
332 self.buffers[index].update_prompt()
333 elif message.msgid == '_buffer_closing':
334 self.remove_buffer(index)
335 elif message.msgid == '_upgrade':
336 self.network.desync_weechat()
337 elif message.msgid == '_upgrade_ended':
338 self.network.sync_weechat()
339
340 def create_buffer(self, item):
341 buf = Buffer(item)
342 buf.bufferInput.connect(self.buffer_input)
343 buf.widget.input.bufferSwitchPrev.connect(self.list_buffers.switch_prev_buffer)
344 buf.widget.input.bufferSwitchNext.connect(self.list_buffers.switch_next_buffer)
345 return buf
346
347 def insert_buffer(self, index, buf):
348 self.buffers.insert(index, buf)
349 self.list_buffers.insertItem(index, '%d. %s' % (buf.data['number'], buf.data['full_name']))
350 self.stacked_buffers.insertWidget(index, buf.widget)
351
352 def remove_buffer(self, index):
353 if self.list_buffers.currentRow == index and index > 0:
354 self.list_buffers.setCurrentRow(index - 1)
355 self.list_buffers.takeItem(index)
356 self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index))
357 self.buffers.pop(index)
358
359 def find_buffer_index_for_insert(self, next_buffer):
360 index = -1
361 if next_buffer == '0x0':
362 index = len(self.buffers)
363 else:
364 index = [i for i, b in enumerate(self.buffers) if b.pointer() == next_buffer]
365 if index:
366 index = index[0]
367 if index < 0:
368 print('Warning: unable to find position for buffer, using end of list by default')
369 index = len(self.buffers)
370 return index
371
372 def closeEvent(self, event):
373 self.network.disconnect_weechat()
374 if self.debug_dialog:
375 self.debug_dialog.close()
376 config.write(self.config)
377 QtGui.QMainWindow.closeEvent(self, event)
378
379
380 app = QtGui.QApplication(sys.argv)
381 app.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
382 app.setWindowIcon(QtGui.QIcon('data/icons/weechat_icon_32.png'))
383 main = MainWindow()
384 sys.exit(app.exec_())