Code refactoring, fix setup.py

All changes:
- full PEP8 compliance
- move sources from src/qweechat/ to qweechat/
- move data from data/icons/ to qweechat/data/icons/
- sources validated with PEP8
- use setuptools in setup.py, fix path of data files
This commit is contained in:
Sébastien Helleu 2014-05-08 17:40:31 +02:00
parent 42f3541246
commit 77df9d06f7
33 changed files with 953 additions and 728 deletions

5
.gitignore vendored
View file

@ -1,6 +1,9 @@
# Ignored files for Git # Ignored files for Git
*.pyc
*.pyo
MANIFEST MANIFEST
build/* build/*
dist/* dist/*
*.pyc qweechat.egg-info/*
src/qweechat.egg-info/*

View file

@ -27,35 +27,10 @@ Following packages are *required*:
* Python 2.x >= 2.6 * Python 2.x >= 2.6
* PySide (recommended, packages: python.pyside.*) or PyQt4 (python-qt4) * PySide (recommended, packages: python.pyside.*) or PyQt4 (python-qt4)
=== Run without install === Install via source distribution
Extract files from archive and run qweechat.py:
---- ----
$ tar xvzf qweechat-x.y.tar.gz $ python setup.py install
$ cd qweechat-x.y
$ python src/qweechat/qweechat.py
----
=== Run with install
Extract files from archive and install using script 'setup.py':
----
$ tar xvzf qweechat-x.y.tar.gz
$ cd qweechat-x.y
----
To install in your home:
----
$ python setup.py install --home=~/qweechat
----
To install in system directories (as root):
----
# python setup.py install
---- ----
== WeeChat setup == WeeChat setup

View file

@ -21,6 +21,7 @@
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>. # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
# #
from pkg_resources import resource_filename
import qt_compat import qt_compat
QtCore = qt_compat.import_module('QtCore') QtCore = qt_compat.import_module('QtCore')
QtGui = qt_compat.import_module('QtGui') QtGui = qt_compat.import_module('QtGui')
@ -85,7 +86,10 @@ class BufferListWidget(GenericListWidget):
class BufferWidget(QtGui.QWidget): class BufferWidget(QtGui.QWidget):
"""Widget with (from top to bottom): title, chat + nicklist (optional) + prompt/input.""" """
Widget with (from top to bottom):
title, chat + nicklist (optional) + prompt/input.
"""
def __init__(self, display_nicklist=False): def __init__(self, display_nicklist=False):
QtGui.QWidget.__init__(self) QtGui.QWidget.__init__(self)
@ -96,7 +100,8 @@ class BufferWidget(QtGui.QWidget):
# splitter with chat + nicklist # splitter with chat + nicklist
self.chat_nicklist = QtGui.QSplitter() self.chat_nicklist = QtGui.QSplitter()
self.chat_nicklist.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.chat_nicklist.setSizePolicy(QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding)
self.chat = ChatTextEdit(debug=False) self.chat = ChatTextEdit(debug=False)
self.chat_nicklist.addWidget(self.chat) self.chat_nicklist.addWidget(self.chat)
self.nicklist = GenericListWidget() self.nicklist = GenericListWidget()
@ -148,7 +153,8 @@ class Buffer(QtCore.QObject):
QtCore.QObject.__init__(self) QtCore.QObject.__init__(self)
self.data = data self.data = data
self.nicklist = {} self.nicklist = {}
self.widget = BufferWidget(display_nicklist=self.data.get('nicklist', 0)) self.widget = BufferWidget(display_nicklist=self.data.get('nicklist',
0))
self.update_title() self.update_title()
self.update_prompt() self.update_prompt()
self.widget.input.textSent.connect(self.input_text_sent) self.widget.input.textSent.connect(self.input_text_sent)
@ -160,7 +166,8 @@ class Buffer(QtCore.QObject):
def update_title(self): def update_title(self):
"""Update title.""" """Update title."""
try: try:
self.widget.set_title(color.remove(self.data['title'].decode('utf-8'))) self.widget.set_title(
color.remove(self.data['title'].decode('utf-8')))
except: except:
self.widget.set_title(None) self.widget.set_title(None)
@ -179,12 +186,14 @@ class Buffer(QtCore.QObject):
def nicklist_add_item(self, parent, group, prefix, name, visible): def nicklist_add_item(self, parent, group, prefix, name, visible):
"""Add a group/nick in nicklist.""" """Add a group/nick in nicklist."""
if group: if group:
self.nicklist[name] = { 'visible': visible, self.nicklist[name] = {
'nicks': [] } 'visible': visible,
'nicks': []
}
else: else:
self.nicklist[parent]['nicks'].append({ 'prefix': prefix, self.nicklist[parent]['nicks'].append({'prefix': prefix,
'name': name, 'name': name,
'visible': visible }) 'visible': visible})
def nicklist_remove_item(self, parent, group, name): def nicklist_remove_item(self, parent, group, name):
"""Remove a group/nick from nicklist.""" """Remove a group/nick from nicklist."""
@ -193,7 +202,10 @@ class Buffer(QtCore.QObject):
del self.nicklist[name] del self.nicklist[name]
else: else:
if parent in self.nicklist: if parent in self.nicklist:
self.nicklist[parent]['nicks'] = [nick for nick in self.nicklist[parent]['nicks'] if nick['name'] != name] self.nicklist[parent]['nicks'] = [
nick for nick in self.nicklist[parent]['nicks']
if nick['name'] != name
]
def nicklist_update_item(self, parent, group, prefix, name, visible): def nicklist_update_item(self, parent, group, prefix, name, visible):
"""Update a group/nick in nicklist.""" """Update a group/nick in nicklist."""
@ -212,11 +224,15 @@ class Buffer(QtCore.QObject):
"""Refresh nicklist.""" """Refresh nicklist."""
self.widget.nicklist.clear() self.widget.nicklist.clear()
for group in sorted(self.nicklist): for group in sorted(self.nicklist):
for nick in sorted(self.nicklist[group]['nicks'], key=lambda n:n['name']): for nick in sorted(self.nicklist[group]['nicks'],
prefix_color = { '': '', ' ': '', '+': 'yellow' } key=lambda n: n['name']):
prefix_color = {'': '', ' ': '', '+': 'yellow'}
color = prefix_color.get(nick['prefix'], 'green') color = prefix_color.get(nick['prefix'], 'green')
if color: if color:
icon = QtGui.QIcon('data/icons/bullet_%s_8x8.png' % color) icon = QtGui.QIcon(
resource_filename(__name__,
'data/icons/bullet_%s_8x8.png' %
color))
else: else:
pixmap = QtGui.QPixmap(8, 8) pixmap = QtGui.QPixmap(8, 8)
pixmap.fill() pixmap.fill()

View file

@ -40,13 +40,27 @@ class ChatTextEdit(QtGui.QTextEdit):
self.setFontFamily('monospace') self.setFontFamily('monospace')
self._textcolor = self.textColor() self._textcolor = self.textColor()
self._bgcolor = QtGui.QColor('#FFFFFF') self._bgcolor = QtGui.QColor('#FFFFFF')
self._setcolorcode = { 'F': (self.setTextColor, self._textcolor), self._setcolorcode = {
'B': (self.setTextBackgroundColor, self._bgcolor) } 'F': (self.setTextColor, self._textcolor),
self._setfont = { '*': self.setFontWeight, 'B': (self.setTextBackgroundColor, self._bgcolor)
}
self._setfont = {
'*': self.setFontWeight,
'_': self.setFontUnderline, '_': self.setFontUnderline,
'/': self.setFontItalic } '/': self.setFontItalic
self._fontvalues = { False: { '*': QtGui.QFont.Normal, '_': False, '/': False }, }
True: { '*': QtGui.QFont.Bold, '_': True, '/': True } } self._fontvalues = {
False: {
'*': QtGui.QFont.Normal,
'_': False,
'/': False
},
True: {
'*': QtGui.QFont.Bold,
'_': True,
'/': True
}
}
self._color = color.Color(config.color_options(), self.debug) self._color = color.Color(config.color_options(), self.debug)
def display(self, time, prefix, text, forcecolor=None): def display(self, time, prefix, text, forcecolor=None):
@ -93,17 +107,22 @@ class ChatTextEdit(QtGui.QTextEdit):
# reset attributes and color # reset attributes and color
if code == 'r': if code == 'r':
self._reset_attributes() self._reset_attributes()
self._setcolorcode[action][0](self._setcolorcode[action][1]) self._setcolorcode[action][0](
self._setcolorcode[action][1])
else: else:
# set attributes + color # set attributes + color
while code.startswith(('*', '!', '/', '_', '|', 'r')): while code.startswith(('*', '!', '/', '_', '|',
'r')):
if code[0] == 'r': if code[0] == 'r':
self._reset_attributes() self._reset_attributes()
elif code[0] in self._setfont: elif code[0] in self._setfont:
self._set_attribute(code[0], not self._font[code[0]]) self._set_attribute(
code[0],
not self._font[code[0]])
code = code[1:] code = code[1:]
if code: if code:
self._setcolorcode[action][0](QtGui.QColor(code)) self._setcolorcode[action][0](
QtGui.QColor(code))
item = item[pos+1:] item = item[pos+1:]
if len(item) > 0: if len(item) > 0:
self.insertPlainText(item) self.insertPlainText(item)

133
qweechat/config.py Normal file
View file

@ -0,0 +1,133 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# config.py - configuration for QWeeChat (~/.qweechat/qweechat.conf)
#
# Copyright (C) 2011-2014 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# QWeeChat is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
import ConfigParser
import os
CONFIG_DIR = '%s/.qweechat' % os.getenv('HOME')
CONFIG_FILENAME = '%s/qweechat.conf' % CONFIG_DIR
CONFIG_DEFAULT_RELAY_LINES = 50
CONFIG_DEFAULT_SECTIONS = ('relay', 'look', 'color')
CONFIG_DEFAULT_OPTIONS = (('relay.server', ''),
('relay.port', ''),
('relay.ssl', 'off'),
('relay.password', ''),
('relay.autoconnect', 'off'),
('relay.lines', str(CONFIG_DEFAULT_RELAY_LINES)),
('look.debug', 'off'),
('look.statusbar', 'off'))
# Default colors for WeeChat color options (option name, #rgb value)
CONFIG_DEFAULT_COLOR_OPTIONS = (
('separator', '#000066'), # 0
('chat', '#000000'), # 1
('chat_time', '#999999'), # 2
('chat_time_delimiters', '#000000'), # 3
('chat_prefix_error', '#FF6633'), # 4
('chat_prefix_network', '#990099'), # 5
('chat_prefix_action', '#000000'), # 6
('chat_prefix_join', '#00CC00'), # 7
('chat_prefix_quit', '#CC0000'), # 8
('chat_prefix_more', '#CC00FF'), # 9
('chat_prefix_suffix', '#330099'), # 10
('chat_buffer', '#000000'), # 11
('chat_server', '#000000'), # 12
('chat_channel', '#000000'), # 13
('chat_nick', '#000000'), # 14
('chat_nick_self', '*#000000'), # 15
('chat_nick_other', '#000000'), # 16
('', '#000000'), # 17 (nick1 -- obsolete)
('', '#000000'), # 18 (nick2 -- obsolete)
('', '#000000'), # 19 (nick3 -- obsolete)
('', '#000000'), # 20 (nick4 -- obsolete)
('', '#000000'), # 21 (nick5 -- obsolete)
('', '#000000'), # 22 (nick6 -- obsolete)
('', '#000000'), # 23 (nick7 -- obsolete)
('', '#000000'), # 24 (nick8 -- obsolete)
('', '#000000'), # 25 (nick9 -- obsolete)
('', '#000000'), # 26 (nick10 -- obsolete)
('chat_host', '#666666'), # 27
('chat_delimiters', '#9999FF'), # 28
('chat_highlight', '#3399CC'), # 29
('chat_read_marker', '#000000'), # 30
('chat_text_found', '#000000'), # 31
('chat_value', '#000000'), # 32
('chat_prefix_buffer', '#000000'), # 33
('chat_tags', '#000000'), # 34
('chat_inactive_window', '#000000'), # 35
('chat_inactive_buffer', '#000000'), # 36
('chat_prefix_buffer_inactive_buffer', '#000000'), # 37
('chat_nick_offline', '#000000'), # 38
('chat_nick_offline_highlight', '#000000'), # 39
('chat_nick_prefix', '#000000'), # 40
('chat_nick_suffix', '#000000'), # 41
('emphasis', '#000000'), # 42
('chat_day_change', '#000000'), # 43
)
config_color_options = []
def read():
"""Read config file."""
global config_color_options
config = ConfigParser.RawConfigParser()
if os.path.isfile(CONFIG_FILENAME):
config.read(CONFIG_FILENAME)
# add missing sections/options
for section in CONFIG_DEFAULT_SECTIONS:
if not config.has_section(section):
config.add_section(section)
for option in reversed(CONFIG_DEFAULT_OPTIONS):
section, name = option[0].split('.', 1)
if not config.has_option(section, name):
config.set(section, name, option[1])
section = 'color'
for option in reversed(CONFIG_DEFAULT_COLOR_OPTIONS):
if option[0] and not config.has_option(section, option[0]):
config.set(section, option[0], option[1])
# build list of color options
config_color_options = []
for option in CONFIG_DEFAULT_COLOR_OPTIONS:
if option[0]:
config_color_options.append(config.get('color', option[0]))
else:
config_color_options.append('#000000')
return config
def write(config):
"""Write config file."""
if not os.path.exists(CONFIG_DIR):
os.mkdir(CONFIG_DIR, 0o0755)
with open(CONFIG_FILENAME, 'wb') as cfg:
config.write(cfg)
def color_options():
global config_color_options
return config_color_options

View file

@ -57,7 +57,8 @@ class ConnectionDialog(QtGui.QDialog):
self.fields['ssl'] = ssl self.fields['ssl'] = ssl
self.dialog_buttons = QtGui.QDialogButtonBox() self.dialog_buttons = QtGui.QDialogButtonBox()
self.dialog_buttons.setStandardButtons(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) self.dialog_buttons.setStandardButtons(
QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
self.dialog_buttons.rejected.connect(self.close) self.dialog_buttons.rejected.connect(self.close)
grid.addWidget(self.dialog_buttons, 4, 0, 1, 2) grid.addWidget(self.dialog_buttons, 4, 0, 1, 2)

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 384 B

After

Width:  |  Height:  |  Size: 384 B

View file

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

View file

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 813 B

View file

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 597 B

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -44,26 +44,32 @@ class InputLineEdit(QtGui.QLineEdit):
key = event.key() key = event.key()
modifiers = event.modifiers() modifiers = event.modifiers()
bar = self.scroll_widget.verticalScrollBar() bar = self.scroll_widget.verticalScrollBar()
if modifiers == QtCore.Qt.ControlModifier and key == QtCore.Qt.Key_PageUp: if modifiers == QtCore.Qt.ControlModifier:
if key == QtCore.Qt.Key_PageUp:
self.bufferSwitchPrev.emit() self.bufferSwitchPrev.emit()
elif modifiers == QtCore.Qt.AltModifier and key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Up): elif key == QtCore.Qt.Key_PageDown:
self.bufferSwitchNext.emit()
else:
QtGui.QLineEdit.keyPressEvent(self, event)
elif modifiers == QtCore.Qt.AltModifier:
if key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Up):
self.bufferSwitchPrev.emit() self.bufferSwitchPrev.emit()
elif modifiers == QtCore.Qt.ControlModifier and key == QtCore.Qt.Key_PageDown: elif key in (QtCore.Qt.Key_Right, QtCore.Qt.Key_Down):
self.bufferSwitchNext.emit() self.bufferSwitchNext.emit()
elif modifiers == QtCore.Qt.AltModifier and key in (QtCore.Qt.Key_Right, QtCore.Qt.Key_Down): elif key == QtCore.Qt.Key_PageUp:
self.bufferSwitchNext.emit()
elif modifiers == QtCore.Qt.AltModifier and key == QtCore.Qt.Key_PageUp:
bar.setValue(bar.value() - (bar.pageStep() / 10)) bar.setValue(bar.value() - (bar.pageStep() / 10))
elif modifiers == QtCore.Qt.AltModifier and key == QtCore.Qt.Key_PageDown: elif key == QtCore.Qt.Key_PageDown:
bar.setValue(bar.value() + (bar.pageStep() / 10)) bar.setValue(bar.value() + (bar.pageStep() / 10))
elif key == QtCore.Qt.Key_Home:
bar.setValue(bar.minimum())
elif key == QtCore.Qt.Key_End:
bar.setValue(bar.maximum())
else:
QtGui.QLineEdit.keyPressEvent(self, event)
elif key == QtCore.Qt.Key_PageUp: elif key == QtCore.Qt.Key_PageUp:
bar.setValue(bar.value() - bar.pageStep()) bar.setValue(bar.value() - bar.pageStep())
elif key == QtCore.Qt.Key_PageDown: elif key == QtCore.Qt.Key_PageDown:
bar.setValue(bar.value() + bar.pageStep()) bar.setValue(bar.value() + bar.pageStep())
elif modifiers == QtCore.Qt.AltModifier and key == QtCore.Qt.Key_Home:
bar.setValue(bar.minimum())
elif modifiers == QtCore.Qt.AltModifier and key == QtCore.Qt.Key_End:
bar.setValue(bar.maximum())
elif key == QtCore.Qt.Key_Up: elif key == QtCore.Qt.Key_Up:
self._history_navigate(-1) self._history_navigate(-1)
elif key == QtCore.Qt.Key_Down: elif key == QtCore.Qt.Key_Down:

View file

@ -28,11 +28,20 @@ QtNetwork = qt_compat.import_module('QtNetwork')
import config import config
_PROTO_INIT_CMD = ['init password=%(password)s'] _PROTO_INIT_CMD = ['init password=%(password)s']
_PROTO_SYNC_CMDS = ['(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,type,nicklist,title,local_variables',
'(listlines) hdata buffer:gui_buffers(*)/own_lines/last_line(-%(lines)d)/data date,displayed,prefix,message', _PROTO_SYNC_CMDS = [
'(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,'
'type,nicklist,title,local_variables',
'(listlines) hdata buffer:gui_buffers(*)/own_lines/last_line(-%(lines)d)/'
'data date,displayed,prefix,message',
'(nicklist) nicklist', '(nicklist) nicklist',
'sync', 'sync',
'']
''
]
class Network(QtCore.QObject): class Network(QtCore.QObject):
@ -68,7 +77,9 @@ class Network(QtCore.QObject):
def _socket_error(self, error): def _socket_error(self, error):
"""Slot: socket error.""" """Slot: socket error."""
self.statusChanged.emit(self.status_disconnected, 'Failed, error: %s' % self._socket.errorString()) self.statusChanged.emit(
self.status_disconnected,
'Failed, error: %s' % self._socket.errorString())
def _socket_read(self): def _socket_read(self):
"""Slot: data available on socket.""" """Slot: data available on socket."""
@ -129,7 +140,8 @@ class Network(QtCore.QObject):
self.statusChanged.emit(self.status_connecting, None) self.statusChanged.emit(self.status_connecting, None)
def disconnect_weechat(self): def disconnect_weechat(self):
if self._socket.state() != QtNetwork.QAbstractSocket.UnconnectedState: if self._socket.state() == QtNetwork.QAbstractSocket.UnconnectedState:
return
if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState: if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState:
self.send_to_weechat('quit\n') self.send_to_weechat('quit\n')
self._socket.waitForBytesWritten(1000) self._socket.waitForBytesWritten(1000)

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# File downloaded from: https://github.com/epage/PythonUtils/blob/master/util/qt_compat.py # File downloaded from:
# https://github.com/epage/PythonUtils/blob/master/util/qt_compat.py
# Author: epage # Author: epage
# License: LGPL 2.1 # License: LGPL 2.1
# #

545
qweechat/qweechat.py Normal file
View file

@ -0,0 +1,545 @@
# -*- coding: utf-8 -*-
#
# qweechat.py - WeeChat remote GUI using Qt toolkit
#
# Copyright (C) 2011-2014 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# QWeeChat is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""
QWeeChat is a WeeChat remote GUI using Qt toolkit.
It requires requires WeeChat 0.3.7 or newer, running on local or remote host.
"""
#
# History:
#
# 2011-05-27, Sébastien Helleu <flashcode@flashtux.org>:
# start dev
#
import sys
import traceback
from pkg_resources import resource_filename
import qt_compat
QTCORE = qt_compat.import_module('QtCore')
QTGUI = qt_compat.import_module('QtGui')
import config
import weechat.protocol as protocol
from network import Network
from connection import ConnectionDialog
from buffer import BufferListWidget, Buffer
from debug import DebugDialog
from about import AboutDialog
NAME = 'QWeeChat'
VERSION = '0.0.1-dev'
AUTHOR = 'Sébastien Helleu'
AUTHOR_MAIL = 'flashcode@flashtux.org'
WEECHAT_SITE = 'http://weechat.org/'
# number of lines in buffer for debug window
DEBUG_NUM_LINES = 50
class MainWindow(QTGUI.QMainWindow):
"""Main window."""
def __init__(self, *args):
QTGUI.QMainWindow.__init__(*(self,) + args)
self.config = config.read()
self.resize(1000, 600)
self.setWindowTitle(NAME)
self.debug_dialog = None
self.debug_lines = []
self.about_dialog = None
self.connection_dialog = None
# network
self.network = Network()
self.network.statusChanged.connect(self._network_status_changed)
self.network.messageFromWeechat.connect(self._network_weechat_msg)
# list of buffers
self.list_buffers = BufferListWidget()
self.list_buffers.currentRowChanged.connect(self._buffer_switch)
# default buffer
self.buffers = [Buffer()]
self.stacked_buffers = QTGUI.QStackedWidget()
self.stacked_buffers.addWidget(self.buffers[0].widget)
# splitter with buffers + chat/input
splitter = QTGUI.QSplitter()
splitter.addWidget(self.list_buffers)
splitter.addWidget(self.stacked_buffers)
self.setCentralWidget(splitter)
if self.config.getboolean('look', 'statusbar'):
self.statusBar().visible = True
# actions for menu and toolbar
actions_def = {
'connect': [
'network-connect.png', 'Connect to WeeChat',
'Ctrl+O', self.open_connection_dialog],
'disconnect': [
'network-disconnect.png', 'Disconnect from WeeChat',
'Ctrl+D', self.network.disconnect_weechat],
'debug': [
'edit-find.png', 'Debug console window',
'Ctrl+B', self.open_debug_dialog],
'preferences': [
'preferences-other.png', 'Preferences',
'Ctrl+P', self.open_preferences_dialog],
'about': [
'help-about.png', 'About',
'Ctrl+H', self.open_about_dialog],
'save connection': [
'document-save.png', 'Save connection configuration',
'Ctrl+S', self.save_connection],
'quit': [
'application-exit.png', 'Quit application',
'Ctrl+Q', self.close],
}
self.actions = {}
for name, action in list(actions_def.items()):
self.actions[name] = QTGUI.QAction(
QTGUI.QIcon(
resource_filename(__name__, 'data/icons/%s' % action[0])),
name.capitalize(), self)
self.actions[name].setStatusTip(action[1])
self.actions[name].setShortcut(action[2])
self.actions[name].triggered.connect(action[3])
# menu
self.menu = self.menuBar()
menu_file = self.menu.addMenu('&File')
menu_file.addActions([self.actions['connect'],
self.actions['disconnect'],
self.actions['preferences'],
self.actions['save connection'],
self.actions['quit']])
menu_window = self.menu.addMenu('&Window')
menu_window.addAction(self.actions['debug'])
menu_help = self.menu.addMenu('&Help')
menu_help.addAction(self.actions['about'])
self.network_status = QTGUI.QLabel()
self.network_status.setFixedHeight(20)
self.network_status.setFixedWidth(200)
self.network_status.setContentsMargins(0, 0, 10, 0)
self.network_status.setAlignment(QTCORE.Qt.AlignRight)
if hasattr(self.menu, 'setCornerWidget'):
self.menu.setCornerWidget(self.network_status,
QTCORE.Qt.TopRightCorner)
self.network_status_set(self.network.status_disconnected)
# toolbar
toolbar = self.addToolBar('toolBar')
toolbar.setToolButtonStyle(QTCORE.Qt.ToolButtonTextUnderIcon)
toolbar.addActions([self.actions['connect'],
self.actions['disconnect'],
self.actions['debug'],
self.actions['preferences'],
self.actions['about'],
self.actions['quit']])
self.buffers[0].widget.input.setFocus()
# open debug dialog
if self.config.getboolean('look', 'debug'):
self.open_debug_dialog()
# auto-connect to relay
if self.config.getboolean('relay', 'autoconnect'):
self.network.connect_weechat(self.config.get('relay', 'server'),
self.config.get('relay', 'port'),
self.config.getboolean('relay',
'ssl'),
self.config.get('relay', 'password'),
self.config.get('relay', 'lines'))
self.show()
def _buffer_switch(self, index):
"""Switch to a buffer."""
if index >= 0:
self.stacked_buffers.setCurrentIndex(index)
self.stacked_buffers.widget(index).input.setFocus()
def buffer_input(self, full_name, text):
"""Send buffer input to WeeChat."""
if self.network.is_connected():
message = 'input %s %s\n' % (full_name, text)
self.network.send_to_weechat(message)
self.debug_display(0, '<==', message, forcecolor='#AA0000')
def open_preferences_dialog(self):
"""Open a dialog with preferences."""
pass # TODO
def save_connection(self):
"""Save connection configuration."""
if self.network:
options = self.network.get_options()
for option in options.keys():
self.config.set('relay', option, options[option])
def debug_display(self, *args, **kwargs):
"""Display a debug message."""
self.debug_lines.append((args, kwargs))
self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:]
if self.debug_dialog:
self.debug_dialog.chat.display(*args, **kwargs)
def open_debug_dialog(self):
"""Open a dialog with debug messages."""
if not self.debug_dialog:
self.debug_dialog = DebugDialog(self)
self.debug_dialog.input.textSent.connect(
self.debug_input_text_sent)
self.debug_dialog.finished.connect(self._debug_dialog_closed)
self.debug_dialog.display_lines(self.debug_lines)
self.debug_dialog.chat.scroll_bottom()
def debug_input_text_sent(self, text):
"""Send debug buffer input to WeeChat."""
if self.network.is_connected():
text = str(text)
pos = text.find(')')
if text.startswith('(') and pos >= 0:
text = '(debug_%s)%s' % (text[1:pos], text[pos+1:])
else:
text = '(debug) %s' % text
self.debug_display(0, '<==', text, forcecolor='#AA0000')
self.network.send_to_weechat(text + '\n')
def _debug_dialog_closed(self, result):
"""Called when debug dialog is closed."""
self.debug_dialog = None
def open_about_dialog(self):
"""Open a dialog with info about QWeeChat."""
messages = ['<b>%s</b> %s' % (NAME, VERSION),
'&copy; 2011-2014 %s &lt;<a href="mailto:%s">%s</a>&gt;'
% (AUTHOR, AUTHOR_MAIL, AUTHOR_MAIL),
'',
'Running with %s' % ('PySide' if qt_compat.uses_pyside
else 'PyQt4'),
'',
'WeeChat site: <a href="%s">%s</a>'
% (WEECHAT_SITE, WEECHAT_SITE),
'']
self.about_dialog = AboutDialog(NAME, messages, self)
def open_connection_dialog(self):
"""Open a dialog with connection settings."""
values = {}
for option in ('server', 'port', 'ssl', 'password', 'lines'):
values[option] = self.config.get('relay', option)
self.connection_dialog = ConnectionDialog(values, self)
self.connection_dialog.dialog_buttons.accepted.connect(
self.connect_weechat)
def connect_weechat(self):
"""Connect to WeeChat."""
self.network.connect_weechat(
self.connection_dialog.fields['server'].text(),
self.connection_dialog.fields['port'].text(),
self.connection_dialog.fields['ssl'].isChecked(),
self.connection_dialog.fields['password'].text(),
int(self.connection_dialog.fields['lines'].text()))
self.connection_dialog.close()
def _network_status_changed(self, status, extra):
"""Called when the network status has changed."""
if self.config.getboolean('look', 'statusbar'):
self.statusBar().showMessage(status)
self.debug_display(0, '', status, forcecolor='#0000AA')
self.network_status_set(status)
def network_status_set(self, status):
"""Set the network status."""
pal = self.network_status.palette()
if status == self.network.status_connected:
pal.setColor(self.network_status.foregroundRole(),
QTGUI.QColor('green'))
else:
pal.setColor(self.network_status.foregroundRole(),
QTGUI.QColor('#aa0000'))
ssl = ' (SSL)' if status != self.network.status_disconnected \
and self.network.is_ssl() else ''
self.network_status.setPalette(pal)
icon = self.network.status_icon(status)
if icon:
self.network_status.setText(
'<img src="%s"> %s' %
(resource_filename(__name__, 'data/icons/%s' % icon),
status.capitalize() + ssl))
else:
self.network_status.setText(status.capitalize())
if status == self.network.status_disconnected:
self.actions['connect'].setEnabled(True)
self.actions['disconnect'].setEnabled(False)
else:
self.actions['connect'].setEnabled(False)
self.actions['disconnect'].setEnabled(True)
def _network_weechat_msg(self, message):
"""Called when a message is received from WeeChat."""
self.debug_display(0, '==>',
'message (%d bytes):\n%s'
% (len(message),
protocol.hex_and_ascii(message, 20)),
forcecolor='#008800')
try:
proto = protocol.Protocol()
message = proto.decode(str(message))
if message.uncompressed:
self.debug_display(
0, '==>',
'message uncompressed (%d bytes):\n%s'
% (message.size_uncompressed,
protocol.hex_and_ascii(message.uncompressed, 20)),
forcecolor='#008800')
self.debug_display(0, '', 'Message: %s' % message)
self.parse_message(message)
except:
print('Error while decoding message from WeeChat:\n%s'
% traceback.format_exc())
self.network.disconnect_weechat()
def _parse_listbuffers(self, message):
"""Parse a WeeChat with list of buffers."""
for obj in message.objects:
if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
continue
self.list_buffers.clear()
while self.stacked_buffers.count() > 0:
buf = self.stacked_buffers.widget(0)
self.stacked_buffers.removeWidget(buf)
self.buffers = []
for item in obj.value['items']:
buf = self.create_buffer(item)
self.insert_buffer(len(self.buffers), buf)
self.list_buffers.setCurrentRow(0)
self.buffers[0].widget.input.setFocus()
def _parse_line(self, message):
"""Parse a WeeChat message with a buffer line."""
for obj in message.objects:
lines = []
if obj.objtype != 'hda' or obj.value['path'][-1] != 'line_data':
continue
for item in obj.value['items']:
if message.msgid == 'listlines':
ptrbuf = item['__path'][0]
else:
ptrbuf = item['buffer']
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == ptrbuf]
if index:
lines.append((item['date'], item['prefix'],
item['message']))
if message.msgid == 'listlines':
lines.reverse()
for line in lines:
self.buffers[index[0]].widget.chat.display(*line)
def _parse_nicklist(self, message):
"""Parse a WeeChat message with a buffer nicklist."""
buffer_refresh = {}
for obj in message.objects:
if obj.objtype != 'hda' or \
obj.value['path'][-1] != 'nicklist_item':
continue
group = '__root'
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == item['__path'][0]]
if index:
if not index[0] in buffer_refresh:
self.buffers[index[0]].nicklist = {}
buffer_refresh[index[0]] = True
if item['group']:
group = item['name']
self.buffers[index[0]].nicklist_add_item(
group, item['group'], item['prefix'], item['name'],
item['visible'])
for index in buffer_refresh:
self.buffers[index].nicklist_refresh()
def _parse_nicklist_diff(self, message):
"""Parse a WeeChat message with a buffer nicklist diff."""
buffer_refresh = {}
for obj in message.objects:
if obj.objtype != 'hda' or \
obj.value['path'][-1] != 'nicklist_item':
continue
group = '__root'
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == item['__path'][0]]
if not index:
continue
buffer_refresh[index[0]] = True
if item['_diff'] == ord('^'):
group = item['name']
elif item['_diff'] == ord('+'):
self.buffers[index[0]].nicklist_add_item(
group, item['group'], item['prefix'], item['name'],
item['visible'])
elif item['_diff'] == ord('-'):
self.buffers[index[0]].nicklist_remove_item(
group, item['group'], item['name'])
elif item['_diff'] == ord('*'):
self.buffers[index[0]].nicklist_update_item(
group, item['group'], item['prefix'], item['name'],
item['visible'])
for index in buffer_refresh:
self.buffers[index].nicklist_refresh()
def _parse_buffer_opened(self, message):
"""Parse a WeeChat message with a new buffer (opened)."""
for obj in message.objects:
if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
continue
for item in obj.value['items']:
buf = self.create_buffer(item)
index = self.find_buffer_index_for_insert(item['next_buffer'])
self.insert_buffer(index, buf)
def _parse_buffer(self, message):
"""Parse a WeeChat message with a buffer event
(anything except a new buffer).
"""
for obj in message.objects:
if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
continue
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == item['__path'][0]]
if not index:
continue
index = index[0]
if message.msgid == '_buffer_type_changed':
self.buffers[index].data['type'] = item['type']
elif message.msgid in ('_buffer_moved', '_buffer_merged',
'_buffer_unmerged'):
buf = self.buffers[index]
buf.data['number'] = item['number']
self.remove_buffer(index)
index2 = self.find_buffer_index_for_insert(
item['next_buffer'])
self.insert_buffer(index2, buf)
elif message.msgid == '_buffer_renamed':
self.buffers[index].data['full_name'] = item['full_name']
self.buffers[index].data['short_name'] = item['short_name']
elif message.msgid == '_buffer_title_changed':
self.buffers[index].data['title'] = item['title']
self.buffers[index].update_title()
elif message.msgid.startswith('_buffer_localvar_'):
self.buffers[index].data['local_variables'] = \
item['local_variables']
self.buffers[index].update_prompt()
elif message.msgid == '_buffer_closing':
self.remove_buffer(index)
def parse_message(self, message):
"""Parse a WeeChat message."""
if message.msgid.startswith('debug'):
self.debug_display(0, '', '(debug message, ignored)')
elif message.msgid == 'listbuffers':
self._parse_listbuffers(message)
elif message.msgid in ('listlines', '_buffer_line_added'):
self._parse_line(message)
elif message.msgid in ('_nicklist', 'nicklist'):
self._parse_nicklist(message)
elif message.msgid == '_nicklist_diff':
self._parse_nicklist_diff(message)
elif message.msgid == '_buffer_opened':
self._parse_buffer_opened(message)
elif message.msgid.startswith('_buffer_'):
self._parse_buffer(message)
elif message.msgid == '_upgrade':
self.network.desync_weechat()
elif message.msgid == '_upgrade_ended':
self.network.sync_weechat()
def create_buffer(self, item):
"""Create a new buffer."""
buf = Buffer(item)
buf.bufferInput.connect(self.buffer_input)
buf.widget.input.bufferSwitchPrev.connect(
self.list_buffers.switch_prev_buffer)
buf.widget.input.bufferSwitchNext.connect(
self.list_buffers.switch_next_buffer)
return buf
def insert_buffer(self, index, buf):
"""Insert a buffer in list."""
self.buffers.insert(index, buf)
self.list_buffers.insertItem(index, '%d. %s'
% (buf.data['number'],
buf.data['full_name'].decode('utf-8')))
self.stacked_buffers.insertWidget(index, buf.widget)
def remove_buffer(self, index):
"""Remove a buffer."""
if self.list_buffers.currentRow == index and index > 0:
self.list_buffers.setCurrentRow(index - 1)
self.list_buffers.takeItem(index)
self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index))
self.buffers.pop(index)
def find_buffer_index_for_insert(self, next_buffer):
"""Find position to insert a buffer in list."""
index = -1
if next_buffer == '0x0':
index = len(self.buffers)
else:
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == next_buffer]
if index:
index = index[0]
if index < 0:
print('Warning: unable to find position for buffer, using end of '
'list by default')
index = len(self.buffers)
return index
def closeEvent(self, event):
"""Called when QWeeChat window is closed."""
self.network.disconnect_weechat()
if self.debug_dialog:
self.debug_dialog.close()
config.write(self.config)
QTGUI.QMainWindow.closeEvent(self, event)
app = QTGUI.QApplication(sys.argv)
app.setStyle(QTGUI.QStyleFactory.create('Cleanlooks'))
app.setWindowIcon(QTGUI.QIcon(
resource_filename(__name__, 'data/icons/weechat_icon_32.png')))
main = MainWindow()
sys.exit(app.exec_())

View file

@ -28,34 +28,54 @@ RE_COLOR_STD = r'(?:%s\d{2})' % RE_COLOR_ATTRS
RE_COLOR_EXT = r'(?:@%s\d{5})' % RE_COLOR_ATTRS RE_COLOR_EXT = r'(?:@%s\d{5})' % RE_COLOR_ATTRS
RE_COLOR_ANY = r'(?:%s|%s)' % (RE_COLOR_STD, RE_COLOR_EXT) RE_COLOR_ANY = r'(?:%s|%s)' % (RE_COLOR_STD, RE_COLOR_EXT)
# \x19: color code, \x1A: set attribute, \x1B: remove attribute, \x1C: reset # \x19: color code, \x1A: set attribute, \x1B: remove attribute, \x1C: reset
RE_COLOR = re.compile(r'(\x19(?:\d{2}|F%s|B\d{2}|B@\d{5}|E|\\*%s(,%s)?|@\d{5}|b.|\x1C))|\x1A.|\x1B.|\x1C' RE_COLOR = re.compile(
r'(\x19(?:\d{2}|F%s|B\d{2}|B@\d{5}|E|\\*%s(,%s)?|@\d{5}|b.|\x1C))|\x1A.|'
r'\x1B.|\x1C'
% (RE_COLOR_ANY, RE_COLOR_ANY, RE_COLOR_ANY)) % (RE_COLOR_ANY, RE_COLOR_ANY, RE_COLOR_ANY))
TERMINAL_COLORS = \ TERMINAL_COLORS = \
'000000cd000000cd00cdcd000000cdcd00cd00cdcde5e5e54d4d4dff000000ff00ffff000000ffff00ff00ffffffffff' \ '000000cd000000cd00cdcd000000cdcd00cd00cdcde5e5e5' \
'00000000002a0000550000800000aa0000d4002a00002a2a002a55002a80002aaa002ad400550000552a005555005580' \ '4d4d4dff000000ff00ffff000000ffff00ff00ffffffffff' \
'0055aa0055d400800000802a0080550080800080aa0080d400aa0000aa2a00aa5500aa8000aaaa00aad400d40000d42a' \ '00000000002a0000550000800000aa0000d4002a00002a2a' \
'00d45500d48000d4aa00d4d42a00002a002a2a00552a00802a00aa2a00d42a2a002a2a2a2a2a552a2a802a2aaa2a2ad4' \ '002a55002a80002aaa002ad400550000552a005555005580' \
'2a55002a552a2a55552a55802a55aa2a55d42a80002a802a2a80552a80802a80aa2a80d42aaa002aaa2a2aaa552aaa80' \ '0055aa0055d400800000802a0080550080800080aa0080d4' \
'2aaaaa2aaad42ad4002ad42a2ad4552ad4802ad4aa2ad4d455000055002a5500555500805500aa5500d4552a00552a2a' \ '00aa0000aa2a00aa5500aa8000aaaa00aad400d40000d42a' \
'552a55552a80552aaa552ad455550055552a5555555555805555aa5555d455800055802a5580555580805580aa5580d4' \ '00d45500d48000d4aa00d4d42a00002a002a2a00552a0080' \
'55aa0055aa2a55aa5555aa8055aaaa55aad455d40055d42a55d45555d48055d4aa55d4d480000080002a800055800080' \ '2a00aa2a00d42a2a002a2a2a2a2a552a2a802a2aaa2a2ad4' \
'8000aa8000d4802a00802a2a802a55802a80802aaa802ad480550080552a8055558055808055aa8055d480800080802a' \ '2a55002a552a2a55552a55802a55aa2a55d42a80002a802a' \
'8080558080808080aa8080d480aa0080aa2a80aa5580aa8080aaaa80aad480d40080d42a80d45580d48080d4aa80d4d4' \ '2a80552a80802a80aa2a80d42aaa002aaa2a2aaa552aaa80' \
'aa0000aa002aaa0055aa0080aa00aaaa00d4aa2a00aa2a2aaa2a55aa2a80aa2aaaaa2ad4aa5500aa552aaa5555aa5580' \ '2aaaaa2aaad42ad4002ad42a2ad4552ad4802ad4aa2ad4d4' \
'aa55aaaa55d4aa8000aa802aaa8055aa8080aa80aaaa80d4aaaa00aaaa2aaaaa55aaaa80aaaaaaaaaad4aad400aad42a' \ '55000055002a5500555500805500aa5500d4552a00552a2a' \
'aad455aad480aad4aaaad4d4d40000d4002ad40055d40080d400aad400d4d42a00d42a2ad42a55d42a80d42aaad42ad4' \ '552a55552a80552aaa552ad455550055552a555555555580' \
'd45500d4552ad45555d45580d455aad455d4d48000d4802ad48055d48080d480aad480d4d4aa00d4aa2ad4aa55d4aa80' \ '5555aa5555d455800055802a5580555580805580aa5580d4' \
'd4aaaad4aad4d4d400d4d42ad4d455d4d480d4d4aad4d4d40808081212121c1c1c2626263030303a3a3a4444444e4e4e' \ '55aa0055aa2a55aa5555aa8055aaaa55aad455d40055d42a' \
'5858586262626c6c6c7676768080808a8a8a9494949e9e9ea8a8a8b2b2b2bcbcbcc6c6c6d0d0d0dadadae4e4e4eeeeee' '55d45555d48055d4aa55d4d480000080002a800055800080' \
'8000aa8000d4802a00802a2a802a55802a80802aaa802ad4' \
'80550080552a8055558055808055aa8055d480800080802a' \
'8080558080808080aa8080d480aa0080aa2a80aa5580aa80' \
'80aaaa80aad480d40080d42a80d45580d48080d4aa80d4d4' \
'aa0000aa002aaa0055aa0080aa00aaaa00d4aa2a00aa2a2a' \
'aa2a55aa2a80aa2aaaaa2ad4aa5500aa552aaa5555aa5580' \
'aa55aaaa55d4aa8000aa802aaa8055aa8080aa80aaaa80d4' \
'aaaa00aaaa2aaaaa55aaaa80aaaaaaaaaad4aad400aad42a' \
'aad455aad480aad4aaaad4d4d40000d4002ad40055d40080' \
'd400aad400d4d42a00d42a2ad42a55d42a80d42aaad42ad4' \
'd45500d4552ad45555d45580d455aad455d4d48000d4802a' \
'd48055d48080d480aad480d4d4aa00d4aa2ad4aa55d4aa80' \
'd4aaaad4aad4d4d400d4d42ad4d455d4d480d4d4aad4d4d4' \
'0808081212121c1c1c2626263030303a3a3a4444444e4e4e' \
'5858586262626c6c6c7676768080808a8a8a9494949e9e9e' \
'a8a8a8b2b2b2bcbcbcc6c6c6d0d0d0dadadae4e4e4eeeeee'
# WeeChat basic colors (color name, index in terminal colors) # WeeChat basic colors (color name, index in terminal colors)
WEECHAT_BASIC_COLORS = (('default', 0), ('black', 0), ('darkgray', 8), ('red', 1), WEECHAT_BASIC_COLORS = (
('default', 0), ('black', 0), ('darkgray', 8), ('red', 1),
('lightred', 9), ('green', 2), ('lightgreen', 10), ('brown', 3), ('lightred', 9), ('green', 2), ('lightgreen', 10), ('brown', 3),
('yellow', 11), ('blue', 4), ('lightblue', 12), ('magenta', 5), ('yellow', 11), ('blue', 4), ('lightblue', 12), ('magenta', 5),
('lightmagenta', 13), ('cyan', 6), ('lightcyan', 14), ('gray', 7), ('lightmagenta', 13), ('cyan', 6), ('lightcyan', 14), ('gray', 7),
('white', 0)) ('white', 0))
class Color(): class Color():
def __init__(self, color_options, debug=False): def __init__(self, color_options, debug=False):
self.color_options = color_options self.color_options = color_options
@ -90,23 +110,25 @@ class Color():
extended = True extended = True
color = color[1:] color = color[1:]
attrs = '' attrs = ''
keep_attrs = False # keep_attrs = False
while color.startswith(('*', '!', '/', '_', '|')): while color.startswith(('*', '!', '/', '_', '|')):
if color[0] == '|': # TODO: manage the "keep attributes" flag
keep_attrs = True # if color[0] == '|':
# keep_attrs = True
attrs += color[0] attrs += color[0]
color = color[1:] color = color[1:]
if extended: if extended:
return self._convert_terminal_color(fg_bg, attrs, color) return self._convert_terminal_color(fg_bg, attrs, color)
try: try:
index = int(color) index = int(color)
return self._convert_terminal_color(fg_bg, attrs, WEECHAT_BASIC_COLORS[index][1]) return self._convert_terminal_color(fg_bg, attrs,
WEECHAT_BASIC_COLORS[index][1])
except: except:
print('Error decoding color "%s"' % color) print('Error decoding color "%s"' % color)
return '' return ''
def _attrcode_to_char(self, code): def _attrcode_to_char(self, code):
codes = { '\x01': '*', '\x02': '!', '\x03': '/', '\x04': '_' } codes = {'\x01': '*', '\x02': '!', '\x03': '/', '\x04': '_'}
return codes.get(code, '') return codes.get(code, '')
def _convert_color(self, match): def _convert_color(self, match):
@ -164,6 +186,7 @@ class Color():
else: else:
return RE_COLOR.sub(self._convert_color, text) return RE_COLOR.sub(self._convert_color, text)
def remove(text): def remove(text):
"""Remove colors in a WeeChat string.""" """Remove colors in a WeeChat string."""
if not text: if not text:

View file

@ -31,20 +31,24 @@
# start dev # start dev
# #
import collections, struct, zlib import collections
import struct
import zlib
if hasattr(collections, 'OrderedDict'): if hasattr(collections, 'OrderedDict'):
# python >= 2.7 # python >= 2.7
class WeechatDict(collections.OrderedDict): class WeechatDict(collections.OrderedDict):
def __str__(self): def __str__(self):
return '{%s}' % ', '.join(['%s: %s' % (repr(key), repr(self[key])) for key in self]) return '{%s}' % ', '.join(
['%s: %s' % (repr(key), repr(self[key])) for key in self])
else: else:
# python <= 2.6 # python <= 2.6
WeechatDict = dict WeechatDict = dict
class WeechatObject: class WeechatObject:
def __init__(self, objtype, value, separator='\n'): def __init__(self, objtype, value, separator='\n'):
self.objtype = objtype; self.objtype = objtype
self.value = value self.value = value
self.separator = separator self.separator = separator
self.indent = ' ' if separator == '\n' else '' self.indent = ' ' if separator == '\n' else ''
@ -56,17 +60,29 @@ class WeechatObject:
return str(v) return str(v)
def _str_value_hdata(self): def _str_value_hdata(self):
lines = ['%skeys: %s%s%spath: %s' % (self.separator1, str(self.value['keys']), self.separator, self.indent, str(self.value['path']))] lines = ['%skeys: %s%s%spath: %s' % (self.separator1,
str(self.value['keys']),
self.separator,
self.indent,
str(self.value['path']))]
for i, item in enumerate(self.value['items']): for i, item in enumerate(self.value['items']):
lines.append(' item %d:%s%s' % ((i + 1), self.separator, lines.append(' item %d:%s%s' % (
self.separator.join(['%s%s: %s' % (self.indent * 2, key, self._str_value(value)) for key, value in item.items()]))) (i + 1), self.separator,
self.separator.join(
['%s%s: %s' % (self.indent * 2, key,
self._str_value(value))
for key, value in item.items()])))
return '\n'.join(lines) return '\n'.join(lines)
def _str_value_infolist(self): def _str_value_infolist(self):
lines = ['%sname: %s' % (self.separator1, self.value['name'])] lines = ['%sname: %s' % (self.separator1, self.value['name'])]
for i, item in enumerate(self.value['items']): for i, item in enumerate(self.value['items']):
lines.append(' item %d:%s%s' % ((i + 1), self.separator, lines.append(' item %d:%s%s' % (
self.separator.join(['%s%s: %s' % (self.indent * 2, key, self._str_value(value)) for key, value in item.items()]))) (i + 1), self.separator,
self.separator.join(
['%s%s: %s' % (self.indent * 2, key,
self._str_value(value))
for key, value in item.items()])))
return '\n'.join(lines) return '\n'.join(lines)
def _str_value_other(self): def _str_value_other(self):
@ -76,7 +92,9 @@ class WeechatObject:
self._obj_cb = {'hda': self._str_value_hdata, self._obj_cb = {'hda': self._str_value_hdata,
'inl': self._str_value_infolist, 'inl': self._str_value_infolist,
} }
return '%s: %s' % (self.objtype, self._obj_cb.get(self.objtype, self._str_value_other)()) return '%s: %s' % (self.objtype,
self._obj_cb.get(self.objtype,
self._str_value_other)())
class WeechatObjects(list): class WeechatObjects(list):
@ -88,7 +106,8 @@ class WeechatObjects(list):
class WeechatMessage: class WeechatMessage:
def __init__(self, size, size_uncompressed, compression, uncompressed, msgid, objects): def __init__(self, size, size_uncompressed, compression, uncompressed,
msgid, objects):
self.size = size self.size = size
self.size_uncompressed = size_uncompressed self.size_uncompressed = size_uncompressed
self.compression = compression self.compression = compression
@ -103,7 +122,9 @@ class WeechatMessage:
100 - ((self.size * 100) // self.size_uncompressed), 100 - ((self.size * 100) // self.size_uncompressed),
self.msgid, self.objects) self.msgid, self.objects)
else: else:
return 'size: %d, id=\'%s\', objects:\n%s' % (self.size, self.msgid, self.objects) return 'size: %d, id=\'%s\', objects:\n%s' % (self.size,
self.msgid,
self.objects)
class Protocol: class Protocol:
@ -202,7 +223,10 @@ class Protocol:
return int(str(value)) return int(str(value))
def _obj_hashtable(self): def _obj_hashtable(self):
"""Read a hashtable in data (type for keys + type for values + count + items).""" """
Read a hashtable in data
(type for keys + type for values + count + items).
"""
type_keys = self._obj_type() type_keys = self._obj_type()
type_values = self._obj_type() type_values = self._obj_type()
count = self._obj_int() count = self._obj_int()
@ -285,7 +309,8 @@ class Protocol:
if compression: if compression:
uncompressed = zlib.decompress(self.data[5:]) uncompressed = zlib.decompress(self.data[5:])
size_uncompressed = len(uncompressed) + 5 size_uncompressed = len(uncompressed) + 5
uncompressed = '%s%s%s' % (struct.pack('>i', size_uncompressed), struct.pack('b', 0), uncompressed) uncompressed = '%s%s%s' % (struct.pack('>i', size_uncompressed),
struct.pack('b', 0), uncompressed)
self.data = uncompressed self.data = uncompressed
else: else:
uncompressed = self.data[:] uncompressed = self.data[:]
@ -301,7 +326,8 @@ class Protocol:
objtype = self._obj_type() objtype = self._obj_type()
value = self._obj_cb[objtype]() value = self._obj_cb[objtype]()
objects.append(WeechatObject(objtype, value, separator=separator)) objects.append(WeechatObject(objtype, value, separator=separator))
return WeechatMessage(size, size_uncompressed, compression, uncompressed, msgid, objects) return WeechatMessage(size, size_uncompressed, compression,
uncompressed, msgid, objects)
def hex_and_ascii(data, bytes_per_line=10): def hex_and_ascii(data, bytes_per_line=10):

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# testproto.py - command-line program for testing protocol WeeChat/relay # testproto.py - command-line program for testing WeeChat/relay protocol
# #
# Copyright (C) 2013-2014 Sébastien Helleu <flashcode@flashtux.org> # Copyright (C) 2013-2014 Sébastien Helleu <flashcode@flashtux.org>
# #
@ -21,6 +20,10 @@
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>. # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
# #
"""
Command-line program for testing WeeChat/relay protocol.
"""
from __future__ import print_function from __future__ import print_function
import argparse import argparse
@ -35,8 +38,11 @@ import traceback
import protocol # WeeChat/relay protocol import protocol # WeeChat/relay protocol
NAME = 'qweechat-testproto'
class TestProto:
class TestProto(object):
"""Test of WeeChat/relay protocol."""
def __init__(self, args): def __init__(self, args):
self.args = args self.args = args
@ -110,11 +116,11 @@ class TestProto:
Return True if OK (it's OK if stdin has no commands), Return True if OK (it's OK if stdin has no commands),
False if error. False if error.
""" """
inr, outr, exceptr = select.select([sys.stdin], [], [], 0) inr = select.select([sys.stdin], [], [], 0)[0]
if inr: if inr:
data = os.read(sys.stdin.fileno(), 4096) data = os.read(sys.stdin.fileno(), 4096)
if data: if data:
if not test.send(data.strip()): if not self.send(data.strip()):
#self.sock.close() #self.sock.close()
return False return False
# open stdin to read user commands # open stdin to read user commands
@ -136,11 +142,10 @@ class TestProto:
sys.stdout.flush() sys.stdout.flush()
try: try:
while not self.has_quit: while not self.has_quit:
inr, outr, exceptr = select.select([sys.stdin, self.sock], inr = select.select([sys.stdin, self.sock], [], [], 1)[0]
[], [], 1) for _file in inr:
for fd in inr: if _file == sys.stdin:
if fd == sys.stdin: buf = os.read(_file.fileno(), 4096)
buf = os.read(fd.fileno(), 4096)
if buf: if buf:
message += buf message += buf
if '\n' in message: if '\n' in message:
@ -152,7 +157,7 @@ class TestProto:
sys.stdout.write(prompt + message) sys.stdout.write(prompt + message)
sys.stdout.flush() sys.stdout.flush()
else: else:
buf = fd.recv(4096) buf = _file.recv(4096)
if buf: if buf:
recvbuf += buf recvbuf += buf
while len(recvbuf) >= 4: while len(recvbuf) >= 4:
@ -186,19 +191,20 @@ class TestProto:
self.sock.close() self.sock.close()
if __name__ == "__main__": def main():
"""Main function."""
# parse command line arguments # parse command line arguments
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
fromfile_prefix_chars='@', fromfile_prefix_chars='@',
description='Command-line program for testing protocol WeeChat/relay.', description='Command-line program for testing WeeChat/relay protocol.',
epilog=''' epilog='''
Environment variable "TESTPROTO_OPTIONS" can be set with default options. Environment variable "QWEECHAT_PROTO_OPTIONS" can be set with default options.
Argument "@file.txt" can be used to read default options in a file. Argument "@file.txt" can be used to read default options in a file.
Some commands can be piped to the script, for example: Some commands can be piped to the script, for example:
echo "init password=xxxx" | python {0} localhost 5000 echo "init password=xxxx" | {name} localhost 5000
python {0} localhost 5000 < commands.txt {name} localhost 5000 < commands.txt
The script returns: The script returns:
0: OK 0: OK
@ -206,7 +212,7 @@ The script returns:
3: connection error 3: connection error
4: send error (message sent to WeeChat) 4: send error (message sent to WeeChat)
5: decode error (message received from WeeChat) 5: decode error (message received from WeeChat)
'''.format(sys.argv[0])) '''.format(name=NAME))
parser.add_argument('-6', '--ipv6', action='store_true', parser.add_argument('-6', '--ipv6', action='store_true',
help='connect using IPv6') help='connect using IPv6')
parser.add_argument('-v', '--verbose', action='count', default=0, parser.add_argument('-v', '--verbose', action='count', default=0,
@ -220,10 +226,10 @@ The script returns:
if len(sys.argv) == 1: if len(sys.argv) == 1:
parser.print_help() parser.print_help()
sys.exit(0) sys.exit(0)
args = parser.parse_args( _args = parser.parse_args(
shlex.split(os.getenv('TESTPROTO_OPTIONS') or '') + sys.argv[1:]) shlex.split(os.getenv('QWEECHAT_PROTO_OPTIONS') or '') + sys.argv[1:])
test = TestProto(args) test = TestProto(_args)
# connect to WeeChat/relay # connect to WeeChat/relay
if not test.connect(): if not test.connect():
@ -234,6 +240,10 @@ The script returns:
sys.exit(4) sys.exit(4)
# main loop (wait commands, display messages received) # main loop (wait commands, display messages received)
rc = test.mainloop() returncode = test.mainloop()
del test del test
sys.exit(rc) sys.exit(returncode)
if __name__ == "__main__":
main()

40
setup.py Executable file → Normal file
View file

@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (C) 2011-2014 Sébastien Helleu <flashcode@flashtux.org> # Copyright (C) 2011-2014 Sébastien Helleu <flashcode@flashtux.org>
@ -19,35 +18,40 @@
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>. # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
# #
import os from setuptools import setup
from distutils.core import setup
def listfiles(dir): DESCRIPTION = 'Qt remote GUI for WeeChat'
return ['%s/%s' % (dir, f) for f in os.listdir(dir)]
setup(name='qweechat', setup(
name='qweechat',
version='0.0.1-dev', version='0.0.1-dev',
description='Qt remote GUI for WeeChat', description=DESCRIPTION,
long_description='Qt remote GUI for WeeChat', long_description=DESCRIPTION,
author='Sébastien Helleu', author='Sébastien Helleu',
author_email='flashcode@flashtux.org', author_email='flashcode@flashtux.org',
url='http://weechat.org/', url='http://weechat.org/',
license='GPL3', license='GPL3',
classifiers = ['Development Status :: 2 - Pre-Alpha', keywords='weechat qt gui',
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: X11 Applications :: Qt', 'Environment :: X11 Applications :: Qt',
'Intended Audience :: End Users/Desktop', 'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License (GPL)', 'License :: OSI Approved :: GNU General Public License v3 '
'or later (GPLv3+)',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Topic :: Communications :: Chat', 'Topic :: Communications :: Chat',
], ],
platforms='OS Independent', packages=['qweechat', 'qweechat.weechat'],
packages=['qweechat', include_package_data=True,
'qweechat.weechat', package_data={'qweechat': ['data/icons/*.png']},
entry_points = {
'gui_scripts': [
'qweechat = qweechat.qweechat',
], ],
package_dir={'qweechat': 'src/qweechat', 'console_scripts': [
'qweechat.weechat': 'src/qweechat/weechat', 'qweechat-testproto = qweechat.weechat.testproto:main',
}, ]
data_files=[('data/icons', listfiles('data/icons'))] }
) )

View file

@ -1,130 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# config.py - configuration for QWeeChat (~/.qweechat/qweechat.conf)
#
# Copyright (C) 2011-2014 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# QWeeChat is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
import os, ConfigParser
import weechat.color as color
CONFIG_DIR = '%s/.qweechat' % os.getenv('HOME')
CONFIG_FILENAME = '%s/qweechat.conf' % CONFIG_DIR
CONFIG_DEFAULT_RELAY_LINES = 50
CONFIG_DEFAULT_SECTIONS = ('relay', 'look', 'color')
CONFIG_DEFAULT_OPTIONS = (('relay.server', ''),
('relay.port', ''),
('relay.ssl', 'off'),
('relay.password', ''),
('relay.autoconnect', 'off'),
('relay.lines', str(CONFIG_DEFAULT_RELAY_LINES)),
('look.debug', 'off'),
('look.statusbar', 'off'))
# Default colors for WeeChat color options (option name, #rgb value)
CONFIG_DEFAULT_COLOR_OPTIONS = (('separator', '#000066'), # 0
('chat', '#000000'), # 1
('chat_time', '#999999'), # 2
('chat_time_delimiters', '#000000'), # 3
('chat_prefix_error', '#FF6633'), # 4
('chat_prefix_network', '#990099'), # 5
('chat_prefix_action', '#000000'), # 6
('chat_prefix_join', '#00CC00'), # 7
('chat_prefix_quit', '#CC0000'), # 8
('chat_prefix_more', '#CC00FF'), # 9
('chat_prefix_suffix', '#330099'), # 10
('chat_buffer', '#000000'), # 11
('chat_server', '#000000'), # 12
('chat_channel', '#000000'), # 13
('chat_nick', '#000000'), # 14
('chat_nick_self', '*#000000'), # 15
('chat_nick_other', '#000000'), # 16
('', '#000000'), # 17 (nick1 -- obsolete)
('', '#000000'), # 18 (nick2 -- obsolete)
('', '#000000'), # 19 (nick3 -- obsolete)
('', '#000000'), # 20 (nick4 -- obsolete)
('', '#000000'), # 21 (nick5 -- obsolete)
('', '#000000'), # 22 (nick6 -- obsolete)
('', '#000000'), # 23 (nick7 -- obsolete)
('', '#000000'), # 24 (nick8 -- obsolete)
('', '#000000'), # 25 (nick9 -- obsolete)
('', '#000000'), # 26 (nick10 -- obsolete)
('chat_host', '#666666'), # 27
('chat_delimiters', '#9999FF'), # 28
('chat_highlight', '#3399CC'), # 29
('chat_read_marker', '#000000'), # 30
('chat_text_found', '#000000'), # 31
('chat_value', '#000000'), # 32
('chat_prefix_buffer', '#000000'), # 33
('chat_tags', '#000000'), # 34
('chat_inactive_window', '#000000'), # 35
('chat_inactive_buffer', '#000000'), # 36
('chat_prefix_buffer_inactive_buffer', '#000000'), # 37
('chat_nick_offline', '#000000'), # 38
('chat_nick_offline_highlight', '#000000'), # 39
('chat_nick_prefix', '#000000'), # 40
('chat_nick_suffix', '#000000'), # 41
('emphasis', '#000000'), # 42
('chat_day_change', '#000000'), #43
)
config_color_options = []
def read():
"""Read config file."""
global config_color_options
config = ConfigParser.RawConfigParser()
if os.path.isfile(CONFIG_FILENAME):
config.read(CONFIG_FILENAME)
# add missing sections/options
for section in CONFIG_DEFAULT_SECTIONS:
if not config.has_section(section):
config.add_section(section)
for option in reversed(CONFIG_DEFAULT_OPTIONS):
section, name = option[0].split('.', 1)
if not config.has_option(section, name):
config.set(section, name, option[1])
section = 'color'
for option in reversed(CONFIG_DEFAULT_COLOR_OPTIONS):
if option[0] and not config.has_option(section, option[0]):
config.set(section, option[0], option[1])
# build list of color options
config_color_options = []
for option in CONFIG_DEFAULT_COLOR_OPTIONS:
if option[0]:
config_color_options.append(config.get('color', option[0]))
else:
config_color_options.append('#000000')
return config
def write(config):
"""Write config file."""
if not os.path.exists(CONFIG_DIR):
os.mkdir(CONFIG_DIR, 0o0755)
with open(CONFIG_FILENAME, 'wb') as cfg:
config.write(cfg)
def color_options():
global config_color_options
return config_color_options

View file

@ -1,419 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# qweechat.py - WeeChat remote GUI using Qt toolkit
#
# Copyright (C) 2011-2014 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# QWeeChat is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
#
# This script requires WeeChat 0.3.7 or newer, running on local or remote host.
#
# History:
#
# 2011-05-27, Sébastien Helleu <flashcode@flashtux.org>:
# start dev
#
import sys, struct, traceback
import qt_compat
QtCore = qt_compat.import_module('QtCore')
QtGui = qt_compat.import_module('QtGui')
import config
import weechat.protocol as protocol
from network import Network
from connection import ConnectionDialog
from buffer import BufferListWidget, Buffer
from debug import DebugDialog
from about import AboutDialog
NAME = 'QWeeChat'
VERSION = '0.0.1-dev'
AUTHOR = 'Sébastien Helleu'
AUTHOR_MAIL= 'flashcode@flashtux.org'
WEECHAT_SITE = 'http://weechat.org/'
# number of lines in buffer for debug window
DEBUG_NUM_LINES = 50
class MainWindow(QtGui.QMainWindow):
"""Main window."""
def __init__(self, *args):
QtGui.QMainWindow.__init__(*(self,) + args)
self.config = config.read()
self.resize(1000, 600)
self.setWindowTitle(NAME)
self.debug_dialog = None
self.debug_lines = []
# network
self.network = Network()
self.network.statusChanged.connect(self.network_status_changed)
self.network.messageFromWeechat.connect(self.network_message_from_weechat)
# list of buffers
self.list_buffers = BufferListWidget()
self.list_buffers.currentRowChanged.connect(self.buffer_switch)
# default buffer
self.buffers = [Buffer()]
self.stacked_buffers = QtGui.QStackedWidget()
self.stacked_buffers.addWidget(self.buffers[0].widget)
# splitter with buffers + chat/input
splitter = QtGui.QSplitter()
splitter.addWidget(self.list_buffers)
splitter.addWidget(self.stacked_buffers)
self.setCentralWidget(splitter)
if self.config.getboolean('look', 'statusbar'):
self.statusBar().visible = True
# actions for menu and toolbar
actions_def = {'connect' : ['network-connect.png', 'Connect to WeeChat', 'Ctrl+O', self.open_connection_dialog],
'disconnect' : ['network-disconnect.png', 'Disconnect from WeeChat', 'Ctrl+D', self.network.disconnect_weechat],
'debug' : ['edit-find.png', 'Debug console window', 'Ctrl+B', self.open_debug_dialog],
'preferences' : ['preferences-other.png', 'Preferences', 'Ctrl+P', self.open_preferences_dialog],
'about' : ['help-about.png', 'About', 'Ctrl+H', self.open_about_dialog],
'save connection': ['document-save.png', 'Save connection configuration', 'Ctrl+S', self.save_connection],
'quit' : ['application-exit.png', 'Quit application', 'Ctrl+Q', self.close],
}
self.actions = {}
for name, action in list(actions_def.items()):
self.actions[name] = QtGui.QAction(QtGui.QIcon('data/icons/%s' % action[0]), name.capitalize(), self)
self.actions[name].setStatusTip(action[1])
self.actions[name].setShortcut(action[2])
self.actions[name].triggered.connect(action[3])
# menu
self.menu = self.menuBar()
menu_file = self.menu.addMenu('&File')
menu_file.addActions([self.actions['connect'], self.actions['disconnect'],
self.actions['preferences'], self.actions['save connection'], self.actions['quit']])
menu_window = self.menu.addMenu('&Window')
menu_window.addAction(self.actions['debug'])
menu_help = self.menu.addMenu('&Help')
menu_help.addAction(self.actions['about'])
self.network_status = QtGui.QLabel()
self.network_status.setFixedHeight(20)
self.network_status.setFixedWidth(200)
self.network_status.setContentsMargins(0, 0, 10, 0)
self.network_status.setAlignment(QtCore.Qt.AlignRight)
if hasattr(self.menu, 'setCornerWidget'):
self.menu.setCornerWidget(self.network_status, QtCore.Qt.TopRightCorner)
self.network_status_set(self.network.status_disconnected, None)
# toolbar
toolbar = self.addToolBar('toolBar')
toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
toolbar.addActions([self.actions['connect'], self.actions['disconnect'],
self.actions['debug'], self.actions['preferences'],
self.actions['about'], self.actions['quit']])
self.buffers[0].widget.input.setFocus()
# open debug dialog
if self.config.getboolean('look', 'debug'):
self.open_debug_dialog()
# auto-connect to relay
if self.config.getboolean('relay', 'autoconnect'):
self.network.connect_weechat(self.config.get('relay', 'server'),
self.config.get('relay', 'port'),
self.config.getboolean('relay', 'ssl'),
self.config.get('relay', 'password'),
self.config.get('relay', 'lines'))
self.show()
def buffer_switch(self, index):
if index >= 0:
self.stacked_buffers.setCurrentIndex(index)
self.stacked_buffers.widget(index).input.setFocus()
def buffer_input(self, full_name, text):
if self.network.is_connected():
message = 'input %s %s\n' % (full_name, text)
self.network.send_to_weechat(message)
self.debug_display(0, '<==', message, forcecolor='#AA0000')
def open_preferences_dialog(self):
pass # TODO
def save_connection(self):
if self.network:
options = self.network.get_options()
for option in options.keys():
self.config.set('relay', option, options[option])
def debug_display(self, *args, **kwargs):
self.debug_lines.append((args, kwargs))
self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:]
if self.debug_dialog:
self.debug_dialog.chat.display(*args, **kwargs)
def open_debug_dialog(self):
if not self.debug_dialog:
self.debug_dialog = DebugDialog(self)
self.debug_dialog.input.textSent.connect(self.debug_input_text_sent)
self.debug_dialog.finished.connect(self.debug_dialog_closed)
self.debug_dialog.display_lines(self.debug_lines)
self.debug_dialog.chat.scroll_bottom()
def debug_input_text_sent(self, text):
if self.network.is_connected():
text = str(text)
pos = text.find(')')
if text.startswith('(') and pos >= 0:
text = '(debug_%s)%s' % (text[1:pos], text[pos+1:])
else:
text = '(debug) %s' % text
self.debug_display(0, '<==', text, forcecolor='#AA0000')
self.network.send_to_weechat(text + '\n')
def debug_dialog_closed(self, result):
self.debug_dialog = None
def open_about_dialog(self):
messages = ['<b>%s</b> %s' % (NAME, VERSION),
'&copy; 2011-2014 %s &lt;<a href="mailto:%s">%s</a>&gt;' % (AUTHOR, AUTHOR_MAIL, AUTHOR_MAIL),
'',
'Running with %s' % ('PySide' if qt_compat.uses_pyside else 'PyQt4'),
'',
'WeeChat site: <a href="%s">%s</a>' % (WEECHAT_SITE, WEECHAT_SITE),
'']
self.about_dialog = AboutDialog(NAME, messages, self)
def open_connection_dialog(self):
values = {}
for option in ('server', 'port', 'ssl', 'password', 'lines'):
values[option] = self.config.get('relay', option)
self.connection_dialog = ConnectionDialog(values, self)
self.connection_dialog.dialog_buttons.accepted.connect(self.connect_weechat)
def connect_weechat(self):
self.network.connect_weechat(self.connection_dialog.fields['server'].text(),
self.connection_dialog.fields['port'].text(),
self.connection_dialog.fields['ssl'].isChecked(),
self.connection_dialog.fields['password'].text(),
int(self.connection_dialog.fields['lines'].text()))
self.connection_dialog.close()
def network_status_changed(self, status, extra):
if self.config.getboolean('look', 'statusbar'):
self.statusBar().showMessage(status)
self.debug_display(0, '', status, forcecolor='#0000AA')
self.network_status_set(status, extra)
def network_status_set(self, status, extra):
pal = self.network_status.palette()
if status == self.network.status_connected:
pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('green'))
else:
pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('#aa0000'))
ssl = ' (SSL)' if status != self.network.status_disconnected and self.network.is_ssl() else ''
self.network_status.setPalette(pal)
icon = self.network.status_icon(status)
if icon:
self.network_status.setText('<img src="data/icons/%s"> %s' % (icon, status.capitalize() + ssl))
else:
self.network_status.setText(status.capitalize())
if status == self.network.status_disconnected:
self.actions['connect'].setEnabled(True)
self.actions['disconnect'].setEnabled(False)
else:
self.actions['connect'].setEnabled(False)
self.actions['disconnect'].setEnabled(True)
def network_message_from_weechat(self, message):
self.debug_display(0, '==>',
'message (%d bytes):\n%s'
% (len(message), protocol.hex_and_ascii(message, 20)),
forcecolor='#008800')
try:
proto = protocol.Protocol()
message = proto.decode(str(message))
if message.uncompressed:
self.debug_display(0, '==>',
'message uncompressed (%d bytes):\n%s'
% (message.size_uncompressed,
protocol.hex_and_ascii(message.uncompressed, 20)),
forcecolor='#008800')
self.debug_display(0, '', 'Message: %s' % message)
self.parse_message(message)
except:
print('Error while decoding message from WeeChat:\n%s' % traceback.format_exc())
self.network.disconnect_weechat()
def parse_message(self, message):
if message.msgid.startswith('debug'):
self.debug_display(0, '', '(debug message, ignored)')
return
if message.msgid == 'listbuffers':
for obj in message.objects:
if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
self.list_buffers.clear()
while self.stacked_buffers.count() > 0:
buf = self.stacked_buffers.widget(0)
self.stacked_buffers.removeWidget(buf)
self.buffers = []
for item in obj.value['items']:
buf = self.create_buffer(item)
self.insert_buffer(len(self.buffers), buf)
self.list_buffers.setCurrentRow(0)
self.buffers[0].widget.input.setFocus()
elif message.msgid in ('listlines', '_buffer_line_added'):
for obj in message.objects:
lines = []
if obj.objtype == 'hda' and obj.value['path'][-1] == 'line_data':
for item in obj.value['items']:
if message.msgid == 'listlines':
ptrbuf = item['__path'][0]
else:
ptrbuf = item['buffer']
index = [i for i, b in enumerate(self.buffers) if b.pointer() == ptrbuf]
if index:
lines.append((item['date'], item['prefix'], item['message']))
if message.msgid == 'listlines':
lines.reverse()
for line in lines:
self.buffers[index[0]].widget.chat.display(*line)
elif message.msgid in ('_nicklist', 'nicklist'):
buffer_refresh = {}
for obj in message.objects:
if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item':
group = '__root'
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
if index:
if not index[0] in buffer_refresh:
self.buffers[index[0]].nicklist = {}
buffer_refresh[index[0]] = True
if item['group']:
group = item['name']
self.buffers[index[0]].nicklist_add_item(group, item['group'], item['prefix'], item['name'], item['visible'])
for index in buffer_refresh:
self.buffers[index].nicklist_refresh()
elif message.msgid == '_nicklist_diff':
buffer_refresh = {}
for obj in message.objects:
if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item':
group = '__root'
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
if index:
buffer_refresh[index[0]] = True
if item['_diff'] == ord('^'):
group = item['name']
elif item['_diff'] == ord('+'):
self.buffers[index[0]].nicklist_add_item(group, item['group'], item['prefix'], item['name'], item['visible'])
elif item['_diff'] == ord('-'):
self.buffers[index[0]].nicklist_remove_item(group, item['group'], item['name'])
elif item['_diff'] == ord('*'):
self.buffers[index[0]].nicklist_update_item(group, item['group'], item['prefix'], item['name'], item['visible'])
for index in buffer_refresh:
self.buffers[index].nicklist_refresh()
elif message.msgid == '_buffer_opened':
for obj in message.objects:
if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
for item in obj.value['items']:
buf = self.create_buffer(item)
index = self.find_buffer_index_for_insert(item['next_buffer'])
self.insert_buffer(index, buf)
elif message.msgid.startswith('_buffer_'):
for obj in message.objects:
if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer':
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]]
if index:
index = index[0]
if message.msgid == '_buffer_type_changed':
self.buffers[index].data['type'] = item['type']
elif message.msgid in ('_buffer_moved', '_buffer_merged', '_buffer_unmerged'):
buf = self.buffers[index]
buf.data['number'] = item['number']
self.remove_buffer(index)
index2 = self.find_buffer_index_for_insert(item['next_buffer'])
self.insert_buffer(index2, buf)
elif message.msgid == '_buffer_renamed':
self.buffers[index].data['full_name'] = item['full_name']
self.buffers[index].data['short_name'] = item['short_name']
elif message.msgid == '_buffer_title_changed':
self.buffers[index].data['title'] = item['title']
self.buffers[index].update_title()
elif message.msgid.startswith('_buffer_localvar_'):
self.buffers[index].data['local_variables'] = item['local_variables']
self.buffers[index].update_prompt()
elif message.msgid == '_buffer_closing':
self.remove_buffer(index)
elif message.msgid == '_upgrade':
self.network.desync_weechat()
elif message.msgid == '_upgrade_ended':
self.network.sync_weechat()
def create_buffer(self, item):
buf = Buffer(item)
buf.bufferInput.connect(self.buffer_input)
buf.widget.input.bufferSwitchPrev.connect(self.list_buffers.switch_prev_buffer)
buf.widget.input.bufferSwitchNext.connect(self.list_buffers.switch_next_buffer)
return buf
def insert_buffer(self, index, buf):
self.buffers.insert(index, buf)
self.list_buffers.insertItem(index, '%d. %s' % (buf.data['number'], buf.data['full_name'].decode('utf-8')))
self.stacked_buffers.insertWidget(index, buf.widget)
def remove_buffer(self, index):
if self.list_buffers.currentRow == index and index > 0:
self.list_buffers.setCurrentRow(index - 1)
self.list_buffers.takeItem(index)
self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index))
self.buffers.pop(index)
def find_buffer_index_for_insert(self, next_buffer):
index = -1
if next_buffer == '0x0':
index = len(self.buffers)
else:
index = [i for i, b in enumerate(self.buffers) if b.pointer() == next_buffer]
if index:
index = index[0]
if index < 0:
print('Warning: unable to find position for buffer, using end of list by default')
index = len(self.buffers)
return index
def closeEvent(self, event):
self.network.disconnect_weechat()
if self.debug_dialog:
self.debug_dialog.close()
config.write(self.config)
QtGui.QMainWindow.closeEvent(self, event)
app = QtGui.QApplication(sys.argv)
app.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
app.setWindowIcon(QtGui.QIcon('data/icons/weechat_icon_32.png'))
main = MainWindow()
sys.exit(app.exec_())