2 # -*- coding: utf-8 -*-
4 # qweechat.py - WeeChat remote GUI using Qt toolkit
6 # Copyright (C) 2011-2013 Sebastien Helleu <flashcode@flashtux.org>
8 # This file is part of QWeeChat, a Qt remote GUI for WeeChat.
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.
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.
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/>.
25 # This script requires WeeChat 0.3.7 or newer, running on local or remote host.
29 # 2011-05-27, Sebastien Helleu <flashcode@flashtux.org>:
35 QtCore
= qt_compat
.import_module('QtCore')
36 QtGui
= qt_compat
.import_module('QtGui')
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
47 AUTHOR
= 'Sébastien Helleu'
48 AUTHOR_MAIL
= 'flashcode@flashtux.org'
49 WEECHAT_SITE
= 'http://www.weechat.org/'
51 # number of lines in buffer for debug window
55 class MainWindow(QtGui
.QMainWindow
):
58 def __init__(self
, *args
):
59 QtGui
.QMainWindow
.__init
__(*(self
,) + args
)
61 self
.config
= config
.read()
63 self
.resize(1000, 600)
64 self
.setWindowTitle(NAME
)
66 self
.debug_dialog
= None
70 self
.network
= Network()
71 self
.network
.statusChanged
.connect(self
.network_status_changed
)
72 self
.network
.messageFromWeechat
.connect(self
.network_message_from_weechat
)
75 self
.list_buffers
= BufferListWidget()
76 self
.list_buffers
.currentRowChanged
.connect(self
.buffer_switch
)
79 self
.buffers
= [Buffer()]
80 self
.stacked_buffers
= QtGui
.QStackedWidget()
81 self
.stacked_buffers
.addWidget(self
.buffers
[0].widget
)
83 # splitter with buffers + chat/input
84 splitter
= QtGui
.QSplitter()
85 splitter
.addWidget(self
.list_buffers
)
86 splitter
.addWidget(self
.stacked_buffers
)
88 self
.setCentralWidget(splitter
)
90 if self
.config
.getboolean('look', 'statusbar'):
91 self
.statusBar().visible
= True
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
],
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])
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)
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']])
133 self
.buffers
[0].widget
.input.setFocus()
136 if self
.config
.getboolean('look', 'debug'):
137 self
.open_debug_dialog()
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'))
148 def buffer_switch(self
, index
):
150 self
.stacked_buffers
.setCurrentIndex(index
)
151 self
.stacked_buffers
.widget(index
).input.setFocus()
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')
159 def open_preferences_dialog(self
):
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
)
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()
176 def debug_input_text_sent(self
, text
):
177 if self
.network
.is_connected():
180 if text
.startswith('(') and pos
>= 0:
181 text
= '(debug_%s)%s' % (text
[1:pos
], text
[pos
+1:])
183 text
= '(debug) %s' % text
184 self
.debug_display(0, '<==', text
, forcecolor
='#AA0000')
185 self
.network
.send_to_weechat(text
+ '\n')
187 def debug_dialog_closed(self
, result
):
188 self
.debug_dialog
= None
190 def open_about_dialog(self
):
191 messages
= ['<b>%s</b> %s' % (NAME
, VERSION
),
192 '© 2011-2013 %s <<a href="mailto:%s">%s</a>>' % (AUTHOR
, AUTHOR_MAIL
, AUTHOR_MAIL
),
194 'Running with %s' % ('PySide' if qt_compat
.uses_pyside
else 'PyQt4'),
196 'WeeChat site: <a href="%s">%s</a>' % (WEECHAT_SITE
, WEECHAT_SITE
),
198 self
.about_dialog
= AboutDialog(NAME
, messages
, self
)
200 def open_connection_dialog(self
):
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
)
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()
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
)
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'))
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
)
230 self
.network_status
.setText('<img src="data/icons/%s"> %s' % (icon
, status
.capitalize() + ssl
))
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)
237 self
.actions
['connect'].setEnabled(False)
238 self
.actions
['disconnect'].setEnabled(True)
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')
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
)
257 print("Error while decoding message from WeeChat")
258 self
.network
.disconnect_weechat()
260 def parse_message(self
, message
):
261 if message
.msgid
.startswith('debug'):
262 self
.debug_display(0, '', '(debug message, ignored)')
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
)
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]
284 ptrbuf
= item
['buffer']
285 index
= [i
for i
, b
in enumerate(self
.buffers
) if b
.pointer() == ptrbuf
]
287 self
.buffers
[index
[0]].widget
.chat
.display(item
['date'],
290 elif message
.msgid
in ('_nicklist', '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]]
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]]
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()
340 def create_buffer(self
, 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
)
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
)
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
)
359 def find_buffer_index_for_insert(self
, next_buffer
):
361 if next_buffer
== '0x0':
362 index
= len(self
.buffers
)
364 index
= [i
for i
, b
in enumerate(self
.buffers
) if b
.pointer() == next_buffer
]
368 print('Warning: unable to find position for buffer, using end of list by default')
369 index
= len(self
.buffers
)
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
)
380 app
= QtGui
.QApplication(sys
.argv
)
381 app
.setStyle(QtGui
.QStyleFactory
.create('Cleanlooks'))
382 app
.setWindowIcon(QtGui
.QIcon('data/icons/weechat_icon_32.png'))
384 sys
.exit(app
.exec_())