]> jfr.im git - irc/weechat/qweechat.git/blob - src/qweechat/qweechat.py
Replace calls to "apply(f, args)" with "f(*args)"
[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 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.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 actions_def.iteritems():
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', 'password'))
143
144 self.show()
145
146 def buffer_switch(self, index):
147 if index >= 0:
148 self.stacked_buffers.setCurrentIndex(index)
149 self.stacked_buffers.widget(index).input.setFocus()
150
151 def buffer_input(self, full_name, text):
152 if self.network.is_connected():
153 message = 'input %s %s\n' % (full_name, text)
154 self.network.send_to_weechat(message)
155 self.debug_display(0, '<==', message, forcecolor='#AA0000')
156
157 def open_preferences_dialog(self):
158 pass # TODO
159
160 def debug_display(self, *args, **kwargs):
161 self.debug_lines.append((args, kwargs))
162 self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:]
163 if self.debug_dialog:
164 self.debug_dialog.chat.display(*args, **kwargs)
165
166 def open_debug_dialog(self):
167 if not self.debug_dialog:
168 self.debug_dialog = DebugDialog(self)
169 self.debug_dialog.input.textSent.connect(self.debug_input_text_sent)
170 self.debug_dialog.finished.connect(self.debug_dialog_closed)
171 self.debug_dialog.display_lines(self.debug_lines)
172 self.debug_dialog.chat.scroll_bottom()
173
174 def debug_input_text_sent(self, text):
175 if self.network.is_connected():
176 text = str(text)
177 pos = text.find(')')
178 if text.startswith('(') and pos >= 0:
179 text = '(debug_%s)%s' % (text[1:pos], text[pos+1:])
180 else:
181 text = '(debug) %s' % text
182 self.debug_display(0, '<==', text, forcecolor='#AA0000')
183 self.network.send_to_weechat(text + '\n')
184
185 def debug_dialog_closed(self, result):
186 self.debug_dialog = None
187
188 def open_about_dialog(self):
189 messages = ['<b>%s</b> %s' % (NAME, VERSION),
190 '&copy; 2011 %s &lt;<a href="mailto:%s">%s</a>&gt;' % (AUTHOR, AUTHOR_MAIL, AUTHOR_MAIL),
191 '',
192 'WeeChat site: <a href="%s">%s</a>' % (WEECHAT_SITE, WEECHAT_SITE),
193 '']
194 self.about_dialog = AboutDialog(NAME, messages, self)
195
196 def open_connection_dialog(self):
197 values = {}
198 for option in ('server', 'port', 'password'):
199 values[option] = self.config.get('relay', option)
200 self.connection_dialog = ConnectionDialog(values, self)
201 self.connection_dialog.dialog_buttons.accepted.connect(self.connect_weechat)
202
203 def connect_weechat(self):
204 self.network.connect_weechat(self.connection_dialog.fields['server'].text(),
205 self.connection_dialog.fields['port'].text(),
206 self.connection_dialog.fields['password'].text())
207 self.connection_dialog.close()
208
209 def network_status_changed(self, status, extra):
210 if self.config.getboolean('look', 'statusbar'):
211 self.statusBar().showMessage(status)
212 self.debug_display(0, '', status, forcecolor='#0000AA')
213 self.network_status_set(status, extra)
214
215 def network_status_set(self, status, extra):
216 pal = self.network_status.palette()
217 if self.network.is_connected():
218 pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('green'))
219 else:
220 pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('#aa0000'))
221 self.network_status.setPalette(pal)
222 icon = self.network.status_icon(status)
223 if icon:
224 self.network_status.setText('<img src="data/icons/%s"> %s' % (icon, status.capitalize()))
225 else:
226 self.network_status.setText(status.capitalize())
227 if status == self.network.status_disconnected:
228 self.actions['connect'].setEnabled(True)
229 self.actions['disconnect'].setEnabled(False)
230 else:
231 self.actions['connect'].setEnabled(False)
232 self.actions['disconnect'].setEnabled(True)
233
234 def network_message_from_weechat(self, message):
235 self.debug_display(0, '==>',
236 'message (%d bytes):\n%s'
237 % (len(message), protocol.hex_and_ascii(message, 20)),
238 forcecolor='#008800')
239 proto = protocol.Protocol()
240 message = proto.decode(str(message))
241 if message.uncompressed:
242 self.debug_display(0, '==>',
243 'message uncompressed (%d bytes):\n%s'
244 % (message.size_uncompressed,
245 protocol.hex_and_ascii(message.uncompressed, 20)),
246 forcecolor='#008800')
247 self.debug_display(0, '', 'Message: %s' % message)
248 self.parse_message(message)
249
250 def parse_message(self, message):
251 if message.msgid.startswith('debug'):
252 self.debug_display(0, '', '(debug message, ignored)')
253 return
254 if message.msgid == 'listbuffers':
255 for obj in message.objects:
256 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
257 self.list_buffers.clear()
258 while self.stacked_buffers.count() > 0:
259 buf = self.stacked_buffers.widget(0)
260 self.stacked_buffers.removeWidget(buf)
261 self.buffers = []
262 for item in obj.value['items']:
263 buf = self.create_buffer(item)
264 self.insert_buffer(len(self.buffers), buf)
265 self.list_buffers.setCurrentRow(0)
266 self.buffers[0].widget.input.setFocus()
267 elif message.msgid in ('listlines', '_buffer_line_added'):
268 for obj in message.objects:
269 if obj.objtype == 'hda' and obj.value['path'][-1] == 'line_data':
270 for item in obj.value['items']:
271 if message.msgid == 'listlines':
272 ptrbuf = item['__path'][0]
273 else:
274 ptrbuf = item['buffer']
275 index = [i for i, b in enumerate(self.buffers) if b.pointer() == ptrbuf]
276 if index:
277 self.buffers[index[0]].widget.chat.display(item['date'],
278 item['prefix'],
279 item['message'])
280 elif message.msgid in ('_nicklist', 'nicklist'):
281 buffer_nicklist = {}
282 for obj in message.objects:
283 if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item':
284 for item in obj.value['items']:
285 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
286 if index:
287 if not item['__path'][0] in buffer_nicklist:
288 self.buffers[index[0]].remove_all_nicks()
289 buffer_nicklist[item['__path'][0]] = True
290 if not item['group'] and item['visible']:
291 self.buffers[index[0]].add_nick(item['prefix'], item['name'])
292 elif message.msgid == '_buffer_opened':
293 for obj in message.objects:
294 if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
295 for item in obj.value['items']:
296 buf = self.create_buffer(item)
297 index = self.find_buffer_index_for_insert(item['next_buffer'])
298 self.insert_buffer(index, buf)
299 elif message.msgid.startswith('_buffer_'):
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 index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
304 if index:
305 index = index[0]
306 if message.msgid in ('_buffer_moved', '_buffer_merged'):
307 buf = self.buffers[index]
308 buf.data['number'] = item['number']
309 self.remove_buffer(index)
310 index2 = self.find_buffer_index_for_insert(item['next_buffer'])
311 self.insert_buffer(index2, buf)
312 elif message.msgid == '_buffer_renamed':
313 self.buffers[index].data['full_name'] = item['full_name']
314 self.buffers[index].data['short_name'] = item['short_name']
315 elif message.msgid == '_buffer_title_changed':
316 self.buffers[index].data['title'] = item['title']
317 self.buffers[index].update_title()
318 elif message.msgid.startswith('_buffer_localvar_'):
319 self.buffers[index].data['local_variables'] = item['local_variables']
320 self.buffers[index].update_prompt()
321 elif message.msgid == '_buffer_closing':
322 self.remove_buffer(index)
323
324 def create_buffer(self, item):
325 buf = Buffer(item)
326 buf.bufferInput.connect(self.buffer_input)
327 buf.widget.input.bufferSwitchPrev.connect(self.list_buffers.switch_prev_buffer)
328 buf.widget.input.bufferSwitchNext.connect(self.list_buffers.switch_next_buffer)
329 return buf
330
331 def insert_buffer(self, index, buf):
332 self.buffers.insert(index, buf)
333 self.list_buffers.insertItem(index, '%d. %s' % (buf.data['number'], buf.data['full_name']))
334 self.stacked_buffers.insertWidget(index, buf.widget)
335
336 def remove_buffer(self, index):
337 if self.list_buffers.currentRow == index and index > 0:
338 self.list_buffers.setCurrentRow(index - 1)
339 self.list_buffers.takeItem(index)
340 self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index))
341 self.buffers.pop(index)
342
343 def find_buffer_index_for_insert(self, next_buffer):
344 index = -1
345 if next_buffer == '0x0':
346 index = len(self.buffers)
347 else:
348 index = [i for i, b in enumerate(self.buffers) if b.pointer() == next_buffer]
349 if index:
350 index = index[0]
351 if index < 0:
352 print('Warning: unable to find position for buffer, using end of list by default')
353 index = len(self.buffers)
354 return index
355
356 def closeEvent(self, event):
357 self.network.disconnect_weechat()
358 if self.debug_dialog:
359 self.debug_dialog.close()
360 config.write(self.config)
361 QtGui.QMainWindow.closeEvent(self, event)
362
363
364 app = QtGui.QApplication(sys.argv)
365 app.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
366 app.setWindowIcon(QtGui.QIcon('data/icons/weechat_icon_32.png'))
367 main = MainWindow()
368 sys.exit(app.exec_())