]> jfr.im git - irc/weechat/qweechat.git/blame - src/qweechat/qweechat.py
Move comments with description of python file to top of files
[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
33import sys, struct
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:
257 print("Error while decoding message from WeeChat")
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
SH
290 elif message.msgid in ('_nicklist', 'nicklist'):
291 buffer_nicklist = {}
7dcf23b1 292 for obj in message.objects:
ca67c3a7 293 if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item':
7dcf23b1 294 for item in obj.value['items']:
ca67c3a7
SH
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]
97936653
SH
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'):
ca67c3a7
SH
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']
c728febd
SH
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()
ca67c3a7
SH
333 elif message.msgid == '_buffer_closing':
334 self.remove_buffer(index)
beaa8775
SH
335 elif message.msgid == '_upgrade':
336 self.network.desync_weechat()
337 elif message.msgid == '_upgrade_ended':
338 self.network.sync_weechat()
ca67c3a7
SH
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
7dcf23b1
SH
371
372 def closeEvent(self, event):
373 self.network.disconnect_weechat()
cc80cd81
SH
374 if self.debug_dialog:
375 self.debug_dialog.close()
7dcf23b1
SH
376 config.write(self.config)
377 QtGui.QMainWindow.closeEvent(self, event)
378
379
380app = QtGui.QApplication(sys.argv)
381app.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
93865c21 382app.setWindowIcon(QtGui.QIcon('data/icons/weechat_icon_32.png'))
7dcf23b1
SH
383main = MainWindow()
384sys.exit(app.exec_())