]> jfr.im git - irc/weechat/qweechat.git/blob - src/qweechat/qweechat.py
Add sync of buffers/nicklists
[irc/weechat/qweechat.git] / src / qweechat / qweechat.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright (C) 2011 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 import weechat.color as color
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.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 apply(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 actions_def.iteritems():
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', '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, color='red')
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.network.send_to_weechat(text + '\n')
184 self.debug_display(0, '<==', text, color='red')
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 %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', '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['password'].text())
208 self.connection_dialog.close()
209
210 def network_status_changed(self, status, extra):
211 if self.config.getboolean('look', 'statusbar'):
212 self.statusBar().showMessage(status)
213 self.debug_display(0, '', status, color='blue')
214 self.network_status_set(status, extra)
215
216 def network_status_set(self, status, extra):
217 pal = self.network_status.palette()
218 if self.network.is_connected():
219 pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('green'))
220 else:
221 pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('#aa0000'))
222 self.network_status.setPalette(pal)
223 icon = self.network.status_icon(status)
224 if icon:
225 self.network_status.setText('<img src="data/icons/%s"> %s' % (icon, status.capitalize()))
226 else:
227 self.network_status.setText(status.capitalize())
228 if status == self.network.status_disconnected:
229 self.actions['connect'].setEnabled(True)
230 self.actions['disconnect'].setEnabled(False)
231 else:
232 self.actions['connect'].setEnabled(False)
233 self.actions['disconnect'].setEnabled(True)
234
235 def network_message_from_weechat(self, message):
236 self.debug_display(0, '==>',
237 'message (%d bytes):\n%s'
238 % (len(message), protocol.hex_and_ascii(message, 20)),
239 color='green')
240 proto = protocol.Protocol()
241 message = proto.decode(str(message))
242 if message.uncompressed:
243 self.debug_display(0, '==>',
244 'message uncompressed (%d bytes):\n%s'
245 % (message.size_uncompressed,
246 protocol.hex_and_ascii(message.uncompressed, 20)),
247 color='green')
248 self.debug_display(0, '', 'Message: %s' % message)
249 self.parse_message(message)
250
251 def parse_message(self, message):
252 if message.msgid.startswith('debug'):
253 self.debug_display(0, '', '(debug message, ignored)')
254 return
255 if message.msgid == 'listbuffers':
256 for obj in message.objects:
257 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
258 self.list_buffers.clear()
259 while self.stacked_buffers.count() > 0:
260 buf = self.stacked_buffers.widget(0)
261 self.stacked_buffers.removeWidget(buf)
262 self.buffers = []
263 for item in obj.value['items']:
264 buf = self.create_buffer(item)
265 self.insert_buffer(len(self.buffers), buf)
266 self.list_buffers.setCurrentRow(0)
267 self.buffers[0].widget.input.setFocus()
268 elif message.msgid in ('listlines', '_buffer_line_added'):
269 for obj in message.objects:
270 if obj.objtype == 'hda' and obj.value['path'][-1] == 'line_data':
271 for item in obj.value['items']:
272 if message.msgid == 'listlines':
273 ptrbuf = item['__path'][0]
274 else:
275 ptrbuf = item['buffer']
276 index = [i for i, b in enumerate(self.buffers) if b.pointer() == ptrbuf]
277 if index:
278 self.buffers[index[0]].widget.chat.display(item['date'],
279 color.remove(item['prefix']),
280 color.remove(item['message']))
281 elif message.msgid in ('_nicklist', 'nicklist'):
282 buffer_nicklist = {}
283 for obj in message.objects:
284 if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item':
285 for item in obj.value['items']:
286 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
287 if index:
288 if not item['__path'][0] in buffer_nicklist:
289 self.buffers[index[0]].remove_all_nicks()
290 buffer_nicklist[item['__path'][0]] = True
291 if not item['group'] and item['visible']:
292 self.buffers[index[0]].add_nick(item['prefix'], item['name'])
293 elif message.msgid == '_buffer_opened':
294 for obj in message.objects:
295 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
296 for item in obj.value['items']:
297 buf = self.create_buffer(item)
298 index = self.find_buffer_index_for_insert(item['next_buffer'])
299 self.insert_buffer(index, buf)
300 elif message.msgid.startswith('_buffer_'):
301 for obj in message.objects:
302 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
303 for item in obj.value['items']:
304 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
305 if index:
306 index = index[0]
307 if message.msgid in ('_buffer_moved', '_buffer_merged'):
308 buf = self.buffers[index]
309 buf.data['number'] = item['number']
310 self.remove_buffer(index)
311 index2 = self.find_buffer_index_for_insert(item['next_buffer'])
312 self.insert_buffer(index2, buf)
313 elif message.msgid == '_buffer_renamed':
314 self.buffers[index].data['full_name'] = item['full_name']
315 self.buffers[index].data['short_name'] = item['short_name']
316 elif message.msgid == '_buffer_title_changed':
317 self.buffers[index].data['title'] = item['title']
318 self.buffers[index].widget.set_title(item['title'])
319 elif message.msgid == '_buffer_closing':
320 self.remove_buffer(index)
321
322 def create_buffer(self, item):
323 buf = Buffer(item)
324 buf.bufferInput.connect(self.buffer_input)
325 buf.widget.input.bufferSwitchPrev.connect(self.list_buffers.switch_prev_buffer)
326 buf.widget.input.bufferSwitchNext.connect(self.list_buffers.switch_next_buffer)
327 return buf
328
329 def insert_buffer(self, index, buf):
330 self.buffers.insert(index, buf)
331 self.list_buffers.insertItem(index, '%d. %s' % (buf.data['number'], buf.data['full_name']))
332 self.stacked_buffers.insertWidget(index, buf.widget)
333
334 def remove_buffer(self, index):
335 if self.list_buffers.currentRow == index and index > 0:
336 self.list_buffers.setCurrentRow(index - 1)
337 self.list_buffers.takeItem(index)
338 self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index))
339 self.buffers.pop(index)
340
341 def find_buffer_index_for_insert(self, next_buffer):
342 index = -1
343 if next_buffer == '0x0':
344 index = len(self.buffers)
345 else:
346 index = [i for i, b in enumerate(self.buffers) if b.pointer() == next_buffer]
347 if index:
348 index = index[0]
349 if index < 0:
350 print('Warning: unable to find position for buffer, using end of list by default')
351 index = len(self.buffers)
352 return index
353
354 def closeEvent(self, event):
355 self.network.disconnect_weechat()
356 if self.debug_dialog:
357 self.debug_dialog.close()
358 config.write(self.config)
359 QtGui.QMainWindow.closeEvent(self, event)
360
361
362 app = QtGui.QApplication(sys.argv)
363 app.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
364 main = MainWindow()
365 sys.exit(app.exec_())