]>
jfr.im git - irc/weechat/qweechat.git/blob - qweechat/qweechat.py
76db5a45a1461e6ba3efbd16eb5f5155d6f6f7f8
1 # -*- coding: utf-8 -*-
3 # qweechat.py - WeeChat remote GUI using Qt toolkit
5 # Copyright (C) 2011-2021 Sébastien Helleu <flashcode@flashtux.org>
7 # This file is part of QWeeChat, a Qt remote GUI for WeeChat.
9 # QWeeChat is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # QWeeChat is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
24 QWeeChat is a WeeChat remote GUI using Qt toolkit.
26 It requires requires WeeChat 0.3.7 or newer, running on local or remote host.
32 # 2011-05-27, Sébastien Helleu <flashcode@flashtux.org>:
38 from pkg_resources
import resource_filename
40 from qweechat
import config
41 from qweechat
.weechat
import protocol
42 from qweechat
.network
import Network
43 from qweechat
.connection
import ConnectionDialog
44 from qweechat
.buffer import BufferListWidget
, Buffer
45 from qweechat
.debug
import DebugDialog
46 from qweechat
.about
import AboutDialog
47 from qweechat
.version
import qweechat_version
49 from PySide6
.QtWidgets
import QApplication
50 from PySide6
import QtGui
, QtWidgets
, QtCore
53 # QtCore = qt_compat.import_module('QtCore')
54 # QtGui = qt_compat.import_module('QtGui')
57 AUTHOR
= 'Sébastien Helleu'
58 AUTHOR_MAIL
= 'flashcode@flashtux.org'
59 WEECHAT_SITE
= 'https://weechat.org/'
61 # number of lines in buffer for debug window
65 class MainWindow(QtWidgets
.QMainWindow
):
68 def __init__(self
, *args
):
71 self
.config
= config
.read()
73 self
.resize(1000, 600)
74 self
.setWindowTitle(NAME
)
76 self
.debug_dialog
= None
79 self
.about_dialog
= None
80 self
.connection_dialog
= None
81 self
.preferences_dialog
= None
84 self
.network
= Network()
85 self
.network
.statusChanged
.connect(self
._network
_status
_changed
)
86 self
.network
.messageFromWeechat
.connect(self
._network
_weechat
_msg
)
89 self
.list_buffers
= BufferListWidget()
90 self
.list_buffers
.currentRowChanged
.connect(self
._buffer
_switch
)
93 self
.buffers
= [Buffer()]
94 self
.stacked_buffers
= QtWidgets
.QStackedWidget()
95 self
.stacked_buffers
.addWidget(self
.buffers
[0].widget
)
97 # splitter with buffers + chat/input
98 splitter
= QtWidgets
.QSplitter()
99 splitter
.addWidget(self
.list_buffers
)
100 splitter
.addWidget(self
.stacked_buffers
)
102 self
.setCentralWidget(splitter
)
104 if self
.config
.getboolean('look', 'statusbar'):
105 self
.statusBar().visible
= True
107 # actions for menu and toolbar
110 'network-connect.png', 'Connect to WeeChat',
111 'Ctrl+O', self
.open_connection_dialog
],
113 'network-disconnect.png', 'Disconnect from WeeChat',
114 'Ctrl+D', self
.network
.disconnect_weechat
],
116 'edit-find.png', 'Debug console window',
117 'Ctrl+B', self
.open_debug_dialog
],
119 'preferences-other.png', 'Preferences',
120 'Ctrl+P', self
.open_preferences_dialog
],
122 'help-about.png', 'About',
123 'Ctrl+H', self
.open_about_dialog
],
125 'document-save.png', 'Save connection configuration',
126 'Ctrl+S', self
.save_connection
],
128 'application-exit.png', 'Quit application',
129 'Ctrl+Q', self
.close
],
132 for name
, action
in list(actions_def
.items()):
133 self
.actions
[name
] = QtGui
.QAction(
135 resource_filename(__name__
, 'data/icons/%s' % action
[0])),
136 name
.capitalize(), self
)
137 self
.actions
[name
].setStatusTip(action
[1])
138 self
.actions
[name
].setShortcut(action
[2])
139 self
.actions
[name
].triggered
.connect(action
[3])
142 self
.menu
= self
.menuBar()
143 menu_file
= self
.menu
.addMenu('&File')
144 menu_file
.addActions([self
.actions
['connect'],
145 self
.actions
['disconnect'],
146 self
.actions
['preferences'],
147 self
.actions
['save connection'],
148 self
.actions
['quit']])
149 menu_window
= self
.menu
.addMenu('&Window')
150 menu_window
.addAction(self
.actions
['debug'])
151 menu_help
= self
.menu
.addMenu('&Help')
152 menu_help
.addAction(self
.actions
['about'])
153 self
.network_status
= QtWidgets
.QLabel()
154 self
.network_status
.setFixedHeight(20)
155 self
.network_status
.setFixedWidth(200)
156 self
.network_status
.setContentsMargins(0, 0, 10, 0)
157 self
.network_status
.setAlignment(QtCore
.Qt
.AlignRight
)
158 if hasattr(self
.menu
, 'setCornerWidget'):
159 self
.menu
.setCornerWidget(self
.network_status
,
160 QtCore
.Qt
.TopRightCorner
)
161 self
.network_status_set(self
.network
.status_disconnected
)
164 toolbar
= self
.addToolBar('toolBar')
165 toolbar
.setToolButtonStyle(QtCore
.Qt
.ToolButtonTextUnderIcon
)
166 toolbar
.addActions([self
.actions
['connect'],
167 self
.actions
['disconnect'],
168 self
.actions
['debug'],
169 self
.actions
['preferences'],
170 self
.actions
['about'],
171 self
.actions
['quit']])
173 self
.buffers
[0].widget
.input.setFocus()
176 if self
.config
.getboolean('look', 'debug'):
177 self
.open_debug_dialog()
179 # auto-connect to relay
180 if self
.config
.getboolean('relay', 'autoconnect'):
181 self
.network
.connect_weechat(self
.config
.get('relay', 'server'),
182 self
.config
.get('relay', 'port'),
183 self
.config
.getboolean('relay',
185 self
.config
.get('relay', 'password'),
186 self
.config
.get('relay', 'lines'))
190 def _buffer_switch(self
, index
):
191 """Switch to a buffer."""
193 self
.stacked_buffers
.setCurrentIndex(index
)
194 self
.stacked_buffers
.widget(index
).input.setFocus()
196 def buffer_input(self
, full_name
, text
):
197 """Send buffer input to WeeChat."""
198 if self
.network
.is_connected():
199 message
= 'input %s %s\n' % (full_name
, text
)
200 self
.network
.send_to_weechat(message
)
201 self
.debug_display(0, '<==', message
, forcecolor
='#AA0000')
203 def open_preferences_dialog(self
):
204 """Open a dialog with preferences."""
205 # TODO: implement the preferences dialog box
206 messages
= ['Not yet implemented!',
208 self
.preferences_dialog
= AboutDialog('Preferences', messages
, self
)
210 def save_connection(self
):
211 """Save connection configuration."""
213 options
= self
.network
.get_options()
214 for option
in options
.keys():
215 self
.config
.set('relay', option
, options
[option
])
217 def debug_display(self
, *args
, **kwargs
):
218 """Display a debug message."""
219 self
.debug_lines
.append((args
, kwargs
))
220 self
.debug_lines
= self
.debug_lines
[-DEBUG_NUM_LINES
:]
221 if self
.debug_dialog
:
222 self
.debug_dialog
.chat
.display(*args
, **kwargs
)
224 def open_debug_dialog(self
):
225 """Open a dialog with debug messages."""
226 if not self
.debug_dialog
:
227 self
.debug_dialog
= DebugDialog(self
)
228 self
.debug_dialog
.input.textSent
.connect(
229 self
.debug_input_text_sent
)
230 self
.debug_dialog
.finished
.connect(self
._debug
_dialog
_closed
)
231 self
.debug_dialog
.display_lines(self
.debug_lines
)
232 self
.debug_dialog
.chat
.scroll_bottom()
234 def debug_input_text_sent(self
, text
):
235 """Send debug buffer input to WeeChat."""
236 if self
.network
.is_connected():
239 if text
.startswith('(') and pos
>= 0:
240 text
= '(debug_%s)%s' % (text
[1:pos
], text
[pos
+1:])
242 text
= '(debug) %s' % text
243 self
.debug_display(0, '<==', text
, forcecolor
='#AA0000')
244 self
.network
.send_to_weechat(text
+ '\n')
246 def _debug_dialog_closed(self
, result
):
247 """Called when debug dialog is closed."""
248 self
.debug_dialog
= None
250 def open_about_dialog(self
):
251 """Open a dialog with info about QWeeChat."""
252 messages
= ['<b>%s</b> %s' % (NAME
, qweechat_version()),
253 '© 2011-2020 %s <<a href="mailto:%s">%s</a>>'
254 % (AUTHOR
, AUTHOR_MAIL
, AUTHOR_MAIL
),
256 'Running with PySide6',
258 'WeeChat site: <a href="%s">%s</a>'
259 % (WEECHAT_SITE
, WEECHAT_SITE
),
261 self
.about_dialog
= AboutDialog(NAME
, messages
, self
)
263 def open_connection_dialog(self
):
264 """Open a dialog with connection settings."""
266 for option
in ('server', 'port', 'ssl', 'password', 'lines'):
267 values
[option
] = self
.config
.get('relay', option
)
268 self
.connection_dialog
= ConnectionDialog(values
, self
)
269 self
.connection_dialog
.dialog_buttons
.accepted
.connect(
270 self
.connect_weechat
)
272 def connect_weechat(self
):
273 """Connect to WeeChat."""
274 self
.network
.connect_weechat(
275 self
.connection_dialog
.fields
['server'].text(),
276 self
.connection_dialog
.fields
['port'].text(),
277 self
.connection_dialog
.fields
['ssl'].isChecked(),
278 self
.connection_dialog
.fields
['password'].text(),
279 int(self
.connection_dialog
.fields
['lines'].text()))
280 self
.connection_dialog
.close()
282 def _network_status_changed(self
, status
, extra
):
283 """Called when the network status has changed."""
284 if self
.config
.getboolean('look', 'statusbar'):
285 self
.statusBar().showMessage(status
)
286 self
.debug_display(0, '', status
, forcecolor
='#0000AA')
287 self
.network_status_set(status
)
289 def network_status_set(self
, status
):
290 """Set the network status."""
291 pal
= self
.network_status
.palette()
292 if status
== self
.network
.status_connected
:
293 pal
.setColor(self
.network_status
.foregroundRole(),
294 QtGui
.QColor('green'))
296 pal
.setColor(self
.network_status
.foregroundRole(),
297 QtGui
.QColor('#aa0000'))
298 ssl
= ' (SSL)' if status
!= self
.network
.status_disconnected \
299 and self
.network
.is_ssl() else ''
300 self
.network_status
.setPalette(pal
)
301 icon
= self
.network
.status_icon(status
)
303 self
.network_status
.setText(
304 '<img src="%s"> %s' %
305 (resource_filename(__name__
, 'data/icons/%s' % icon
),
306 status
.capitalize() + ssl
))
308 self
.network_status
.setText(status
.capitalize())
309 if status
== self
.network
.status_disconnected
:
310 self
.actions
['connect'].setEnabled(True)
311 self
.actions
['disconnect'].setEnabled(False)
313 self
.actions
['connect'].setEnabled(False)
314 self
.actions
['disconnect'].setEnabled(True)
316 def _network_weechat_msg(self
, message
):
317 """Called when a message is received from WeeChat."""
318 # self.debug_display(0, '==>',
319 # 'message (%d bytes):\n%s'
321 # protocol.hex_and_ascii(message, 20)),
322 # forcecolor='#008800')
324 proto
= protocol
.Protocol()
325 message
= proto
.decode(message
.data())
326 if message
.uncompressed
:
329 'message uncompressed (%d bytes):\n%s'
330 % (message
.size_uncompressed
,
331 protocol
.hex_and_ascii(message
.uncompressed
, 20)),
332 forcecolor
='#008800')
333 self
.debug_display(0, '', 'Message: %s' % message
)
334 self
.parse_message(message
)
335 except Exception: # noqa: E722
336 print('Error while decoding message from WeeChat:\n%s'
337 % traceback
.format_exc())
338 self
.network
.disconnect_weechat()
340 def _parse_listbuffers(self
, message
):
341 """Parse a WeeChat with list of buffers."""
342 for obj
in message
.objects
:
343 if obj
.objtype
!= 'hda' or obj
.value
['path'][-1] != 'buffer':
345 self
.list_buffers
.clear()
346 while self
.stacked_buffers
.count() > 0:
347 buf
= self
.stacked_buffers
.widget(0)
348 self
.stacked_buffers
.removeWidget(buf
)
350 for item
in obj
.value
['items']:
351 buf
= self
.create_buffer(item
)
352 self
.insert_buffer(len(self
.buffers
), buf
)
353 self
.list_buffers
.setCurrentRow(0)
354 self
.buffers
[0].widget
.input.setFocus()
356 def _parse_line(self
, message
):
357 """Parse a WeeChat message with a buffer line."""
358 for obj
in message
.objects
:
360 if obj
.objtype
!= 'hda' or obj
.value
['path'][-1] != 'line_data':
362 for item
in obj
.value
['items']:
363 if message
.msgid
== 'listlines':
364 ptrbuf
= item
['__path'][0]
366 ptrbuf
= item
['buffer']
367 index
= [i
for i
, b
in enumerate(self
.buffers
)
368 if b
.pointer() == ptrbuf
]
372 (item
['date'], item
['prefix'],
375 if message
.msgid
== 'listlines':
378 self
.buffers
[line
[0]].widget
.chat
.display(*line
[1])
380 def _parse_nicklist(self
, message
):
381 """Parse a WeeChat message with a buffer nicklist."""
383 for obj
in message
.objects
:
384 if obj
.objtype
!= 'hda' or \
385 obj
.value
['path'][-1] != 'nicklist_item':
388 for item
in obj
.value
['items']:
389 index
= [i
for i
, b
in enumerate(self
.buffers
)
390 if b
.pointer() == item
['__path'][0]]
392 if not index
[0] in buffer_refresh
:
393 self
.buffers
[index
[0]].nicklist
= {}
394 buffer_refresh
[index
[0]] = True
397 self
.buffers
[index
[0]].nicklist_add_item(
398 group
, item
['group'], item
['prefix'], item
['name'],
400 for index
in buffer_refresh
:
401 self
.buffers
[index
].nicklist_refresh()
403 def _parse_nicklist_diff(self
, message
):
404 """Parse a WeeChat message with a buffer nicklist diff."""
406 for obj
in message
.objects
:
407 if obj
.objtype
!= 'hda' or \
408 obj
.value
['path'][-1] != 'nicklist_item':
411 for item
in obj
.value
['items']:
412 index
= [i
for i
, b
in enumerate(self
.buffers
)
413 if b
.pointer() == item
['__path'][0]]
416 buffer_refresh
[index
[0]] = True
417 if item
['_diff'] == ord('^'):
419 elif item
['_diff'] == ord('+'):
420 self
.buffers
[index
[0]].nicklist_add_item(
421 group
, item
['group'], item
['prefix'], item
['name'],
423 elif item
['_diff'] == ord('-'):
424 self
.buffers
[index
[0]].nicklist_remove_item(
425 group
, item
['group'], item
['name'])
426 elif item
['_diff'] == ord('*'):
427 self
.buffers
[index
[0]].nicklist_update_item(
428 group
, item
['group'], item
['prefix'], item
['name'],
430 for index
in buffer_refresh
:
431 self
.buffers
[index
].nicklist_refresh()
433 def _parse_buffer_opened(self
, message
):
434 """Parse a WeeChat message with a new buffer (opened)."""
435 for obj
in message
.objects
:
436 if obj
.objtype
!= 'hda' or obj
.value
['path'][-1] != 'buffer':
438 for item
in obj
.value
['items']:
439 buf
= self
.create_buffer(item
)
440 index
= self
.find_buffer_index_for_insert(item
['next_buffer'])
441 self
.insert_buffer(index
, buf
)
443 def _parse_buffer(self
, message
):
444 """Parse a WeeChat message with a buffer event
445 (anything except a new buffer).
447 for obj
in message
.objects
:
448 if obj
.objtype
!= 'hda' or obj
.value
['path'][-1] != 'buffer':
450 for item
in obj
.value
['items']:
451 index
= [i
for i
, b
in enumerate(self
.buffers
)
452 if b
.pointer() == item
['__path'][0]]
456 if message
.msgid
== '_buffer_type_changed':
457 self
.buffers
[index
].data
['type'] = item
['type']
458 elif message
.msgid
in ('_buffer_moved', '_buffer_merged',
460 buf
= self
.buffers
[index
]
461 buf
.data
['number'] = item
['number']
462 self
.remove_buffer(index
)
463 index2
= self
.find_buffer_index_for_insert(
465 self
.insert_buffer(index2
, buf
)
466 elif message
.msgid
== '_buffer_renamed':
467 self
.buffers
[index
].data
['full_name'] = item
['full_name']
468 self
.buffers
[index
].data
['short_name'] = item
['short_name']
469 elif message
.msgid
== '_buffer_title_changed':
470 self
.buffers
[index
].data
['title'] = item
['title']
471 self
.buffers
[index
].update_title()
472 elif message
.msgid
== '_buffer_cleared':
473 self
.buffers
[index
].widget
.chat
.clear()
474 elif message
.msgid
.startswith('_buffer_localvar_'):
475 self
.buffers
[index
].data
['local_variables'] = \
476 item
['local_variables']
477 self
.buffers
[index
].update_prompt()
478 elif message
.msgid
== '_buffer_closing':
479 self
.remove_buffer(index
)
481 def parse_message(self
, message
):
482 """Parse a WeeChat message."""
483 if message
.msgid
.startswith('debug'):
484 self
.debug_display(0, '', '(debug message, ignored)')
485 elif message
.msgid
== 'listbuffers':
486 self
._parse
_listbuffers
(message
)
487 elif message
.msgid
in ('listlines', '_buffer_line_added'):
488 self
._parse
_line
(message
)
489 elif message
.msgid
in ('_nicklist', 'nicklist'):
490 self
._parse
_nicklist
(message
)
491 elif message
.msgid
== '_nicklist_diff':
492 self
._parse
_nicklist
_diff
(message
)
493 elif message
.msgid
== '_buffer_opened':
494 self
._parse
_buffer
_opened
(message
)
495 elif message
.msgid
.startswith('_buffer_'):
496 self
._parse
_buffer
(message
)
497 elif message
.msgid
== '_upgrade':
498 self
.network
.desync_weechat()
499 elif message
.msgid
== '_upgrade_ended':
500 self
.network
.sync_weechat()
502 print(f
"Unknown message with id {message.msgid}")
504 def create_buffer(self
, item
):
505 """Create a new buffer."""
507 buf
.bufferInput
.connect(self
.buffer_input
)
508 buf
.widget
.input.bufferSwitchPrev
.connect(
509 self
.list_buffers
.switch_prev_buffer
)
510 buf
.widget
.input.bufferSwitchNext
.connect(
511 self
.list_buffers
.switch_next_buffer
)
514 def insert_buffer(self
, index
, buf
):
515 """Insert a buffer in list."""
516 self
.buffers
.insert(index
, buf
)
517 self
.list_buffers
.insertItem(index
, '%s'
518 % (buf
.data
['local_variables']['name']))
519 self
.stacked_buffers
.insertWidget(index
, buf
.widget
)
520 self
._reorder
_buffers
()
522 def _reorder_buffers(self
):
523 """Order buffers by server."""
526 def remove_buffer(self
, index
):
527 """Remove a buffer."""
528 if self
.list_buffers
.currentRow
== index
and index
> 0:
529 self
.list_buffers
.setCurrentRow(index
- 1)
530 self
.list_buffers
.takeItem(index
)
531 self
.stacked_buffers
.removeWidget(self
.stacked_buffers
.widget(index
))
532 self
.buffers
.pop(index
)
534 def find_buffer_index_for_insert(self
, next_buffer
):
535 """Find position to insert a buffer in list."""
537 if next_buffer
== '0x0':
538 index
= len(self
.buffers
)
540 index
= [i
for i
, b
in enumerate(self
.buffers
)
541 if b
.pointer() == next_buffer
]
545 print('Warning: unable to find position for buffer, using end of '
547 index
= len(self
.buffers
)
550 def closeEvent(self
, event
):
551 """Called when QWeeChat window is closed."""
552 self
.network
.disconnect_weechat()
553 if self
.debug_dialog
:
554 self
.debug_dialog
.close()
555 config
.write(self
.config
)
556 QtWidgets
.QMainWindow
.closeEvent(self
, event
)
560 app
= QApplication(sys
.argv
)
561 app
.setStyle(QtWidgets
.QStyleFactory
.create('Cleanlooks'))
562 app
.setWindowIcon(QtGui
.QIcon(
563 resource_filename(__name__
, 'data/icons/weechat.png')))
566 sys
.exit(app
.exec_())
569 if __name__
== '__main__':