Initial commit

This commit is contained in:
Sebastien Helleu 2011-12-06 21:27:09 +01:00
commit 7dcf23b195
29 changed files with 2144 additions and 0 deletions

0
src/qweechat/__init__.py Normal file
View file

55
src/qweechat/about.py Normal file
View file

@ -0,0 +1,55 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# About dialog box.
#
import qt_compat
QtCore = qt_compat.import_module('QtCore')
QtGui = qt_compat.import_module('QtGui')
class AboutDialog(QtGui.QDialog):
"""About dialog."""
def __init__(self, name, messages, *args):
apply(QtGui.QDialog.__init__, (self,) + args)
self.setModal(True)
self.setWindowTitle(name)
close_button = QtGui.QPushButton('Close')
close_button.pressed.connect(self.close)
hbox = QtGui.QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(close_button)
hbox.addStretch(1)
vbox = QtGui.QVBoxLayout()
for msg in messages:
label = QtGui.QLabel(msg.decode('utf-8'))
label.setAlignment(QtCore.Qt.AlignHCenter)
vbox.addWidget(label)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.show()

146
src/qweechat/buffer.py Normal file
View file

@ -0,0 +1,146 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# Buffers.
#
import qt_compat
QtCore = qt_compat.import_module('QtCore')
QtGui = qt_compat.import_module('QtGui')
from chat import ChatTextEdit
from input import InputLineEdit
class BufferListWidget(QtGui.QListWidget):
"""Widget with list of buffers."""
def __init__(self, *args):
apply(QtGui.QListWidget.__init__, (self,) + args)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setMinimumWidth(120)
self.setMaximumWidth(200)
# TODO: do a dynamic size for widget
#self.setMinimumWidth(self.sizeHintForColumn(0))
#self.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)
#self.setResizeMode(QtGui.QListView.Adjust)
self.setFocusPolicy(QtCore.Qt.NoFocus)
pal = self.palette()
pal.setColor(QtGui.QPalette.Highlight, QtGui.QColor('#ddddff'))
pal.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor('black'))
self.setPalette(pal)
# TODO: do a dynamic size for widget
def sizeHint(self):
s = QtCore.QSize()
s.setHeight(super(BufferListWidget,self).sizeHint().height())
s.setWidth(self.sizeHintForColumn(0))
return s
def switch_prev_buffer(self):
if self.currentRow() > 0:
self.setCurrentRow(self.currentRow() - 1)
else:
self.setCurrentRow(self.count() - 1)
def switch_next_buffer(self):
if self.currentRow() < self.count() - 1:
self.setCurrentRow(self.currentRow() + 1)
else:
self.setCurrentRow(0)
class BufferWidget(QtGui.QWidget):
"""Widget with (from top to bottom): title, chat + nicklist (optional) + prompt/input."""
def __init__(self, display_nicklist=False):
QtGui.QWidget.__init__(self)
# title
self.title = QtGui.QLineEdit()
# splitter with chat + nicklist
self.chat_nicklist = QtGui.QSplitter()
self.chat_nicklist.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.chat = ChatTextEdit()
self.chat_nicklist.addWidget(self.chat)
self.nicklist = QtGui.QListWidget()
self.nicklist.setMaximumWidth(100)
self.nicklist.setFocusPolicy(QtCore.Qt.NoFocus)
if not display_nicklist:
self.nicklist.setVisible(False)
self.chat_nicklist.addWidget(self.nicklist)
# prompt + input
hbox_edit = QtGui.QHBoxLayout()
hbox_edit.setContentsMargins(0, 0, 0, 0)
hbox_edit.setSpacing(0)
self.prompt = QtGui.QLabel('FlashCode')
self.prompt.setContentsMargins(0, 0, 5, 0)
hbox_edit.addWidget(self.prompt)
self.input = InputLineEdit(self.chat)
hbox_edit.addWidget(self.input)
prompt_input = QtGui.QWidget()
prompt_input.setLayout(hbox_edit)
prompt_input.setContentsMargins(0, 0, 0, 0)
# vbox with title + chat/nicklist + prompt/input
vbox = QtGui.QVBoxLayout()
vbox.setContentsMargins(0, 0, 0, 0)
vbox.setSpacing(0)
vbox.addWidget(self.title)
vbox.addWidget(self.chat_nicklist)
vbox.addWidget(prompt_input)
self.setLayout(vbox)
def set_title(self, title):
"""Set buffer title."""
self.title.clear()
if not title is None:
self.title.setText(title)
class Buffer(QtCore.QObject):
"""A WeeChat buffer."""
bufferInput = qt_compat.Signal(str, str)
def __init__(self, data={}):
QtCore.QObject.__init__(self)
self.data = data
self.widget = BufferWidget(display_nicklist=self.data.get('nicklist', 0))
if self.data and self.data['title']:
self.widget.set_title(self.data['title'])
self.widget.input.textSent.connect(self.input_text_sent)
def pointer(self):
"""Return pointer on buffer."""
return self.data.get('__path', [''])[0]
def input_text_sent(self, text):
"""Called when text has to be sent to buffer."""
if self.data:
self.bufferInput.emit(self.data['full_name'], text)
def add_nick(self, prefix, nick):
"""Add a nick to nicklist."""
self.widget.nicklist.addItem('%s%s' % (prefix, nick))

63
src/qweechat/chat.py Normal file
View file

@ -0,0 +1,63 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# Chat area.
#
import datetime
import qt_compat
QtCore = qt_compat.import_module('QtCore')
QtGui = qt_compat.import_module('QtGui')
class ChatTextEdit(QtGui.QTextEdit):
"""Chat area."""
def __init__(self, *args):
apply(QtGui.QTextEdit.__init__, (self,) + args)
self.readOnly = True
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.setFontFamily('monospace')
def display(self, time, prefix, text, color=None):
oldcolor = self.textColor()
if time == 0:
d = datetime.datetime.now()
else:
d = datetime.datetime.fromtimestamp(float(time))
self.setTextColor(QtGui.QColor('#999999'))
self.insertPlainText(d.strftime('%H:%M '))
self.setTextColor(oldcolor)
if prefix:
self.insertPlainText(str(prefix).decode('utf-8') + ' ')
if color:
self.setTextColor(QtGui.QColor(color))
self.insertPlainText(str(text).decode('utf-8'))
if text[-1:] != '\n':
self.insertPlainText('\n')
if color:
self.setTextColor(oldcolor)
self.scroll_bottom()
def scroll_bottom(self):
bar = self.verticalScrollBar()
bar.setValue(bar.maximum())

61
src/qweechat/config.py Normal file
View file

@ -0,0 +1,61 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# Configuration for QWeeChat (~/.qweechat/qweechat.conf)
#
import os, ConfigParser
CONFIG_DIR = '%s/.qweechat' % os.getenv('HOME')
CONFIG_FILENAME = '%s/qweechat.conf' % CONFIG_DIR
CONFIG_DEFAULT_SECTIONS = ('relay', 'look')
CONFIG_DEFAULT_OPTIONS = (('relay.server', ''),
('relay.port', ''),
('relay.password', ''),
('relay.autoconnect', 'off'),
('look.debug', 'off'),
('look.statusbar', 'off'))
def read():
"""Read config file."""
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])
return config
def write(config):
"""Write config file."""
if not os.path.exists(CONFIG_DIR):
os.mkdir(CONFIG_DIR, 0755)
with open(CONFIG_FILENAME, 'wb') as cfg:
config.write(cfg)

View file

@ -0,0 +1,57 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# Connection window.
#
import qt_compat
QtGui = qt_compat.import_module('QtGui')
class ConnectionDialog(QtGui.QDialog):
"""Connection window with server/port/password fields."""
def __init__(self, values, *args):
apply(QtGui.QDialog.__init__, (self,) + args)
self.values = values
self.setModal(True)
grid = QtGui.QGridLayout()
grid.setSpacing(10)
self.fields = {}
for y, field in enumerate(('server', 'port', 'password')):
grid.addWidget(QtGui.QLabel(field.capitalize()), y, 0)
lineEdit = QtGui.QLineEdit()
if field == 'password':
lineEdit.setEchoMode(QtGui.QLineEdit.Password)
lineEdit.insert(self.values[field])
grid.addWidget(lineEdit, y, 1)
self.fields[field] = lineEdit
self.dialog_buttons = QtGui.QDialogButtonBox()
self.dialog_buttons.setStandardButtons(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
self.dialog_buttons.rejected.connect(self.close)
grid.addWidget(self.dialog_buttons, 3, 0, 1, 2)
self.setLayout(grid)
self.show()

47
src/qweechat/debug.py Normal file
View file

@ -0,0 +1,47 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# Debug window.
#
import qt_compat
QtGui = qt_compat.import_module('QtGui')
from chat import ChatTextEdit
from input import InputLineEdit
class DebugDialog(QtGui.QDialog):
"""Debug dialog."""
def __init__(self, *args):
apply(QtGui.QDialog.__init__, (self,) + args)
self.resize(640, 480)
self.setWindowTitle('Debug console')
self.chat = ChatTextEdit()
self.input = InputLineEdit(self.chat)
vbox = QtGui.QVBoxLayout()
vbox.addWidget(self.chat)
vbox.addWidget(self.input)
self.setLayout(vbox)

88
src/qweechat/input.py Normal file
View file

@ -0,0 +1,88 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# Input line (for chat or debug window).
#
import qt_compat
QtCore = qt_compat.import_module('QtCore')
QtGui = qt_compat.import_module('QtGui')
class InputLineEdit(QtGui.QLineEdit):
"""Input line."""
bufferSwitchPrev = qt_compat.Signal()
bufferSwitchNext = qt_compat.Signal()
textSent = qt_compat.Signal(str)
def __init__(self, scroll_widget):
QtGui.QLineEdit.__init__(self)
self.scroll_widget = scroll_widget
self._history = []
self._history_index = -1
self.returnPressed.connect(self._input_return_pressed)
def keyPressEvent(self, event):
key = event.key()
modifiers = event.modifiers()
bar = self.scroll_widget.verticalScrollBar()
if modifiers == QtCore.Qt.ControlModifier and key == QtCore.Qt.Key_PageUp:
self.bufferSwitchPrev.emit()
elif modifiers == QtCore.Qt.ControlModifier and key == QtCore.Qt.Key_PageDown:
self.bufferSwitchNext.emit()
elif modifiers == QtCore.Qt.AltModifier and key == QtCore.Qt.Key_PageUp:
bar.setValue(bar.value() - (bar.pageStep() / 10))
elif modifiers == QtCore.Qt.AltModifier and key == QtCore.Qt.Key_PageDown:
bar.setValue(bar.value() + (bar.pageStep() / 10))
elif key == QtCore.Qt.Key_PageUp:
bar.setValue(bar.value() - bar.pageStep())
elif key == QtCore.Qt.Key_PageDown:
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:
self._history_navigate(-1)
elif key == QtCore.Qt.Key_Down:
self._history_navigate(1)
else:
QtGui.QLineEdit.keyPressEvent(self, event)
def _input_return_pressed(self):
self._history.append(str(self.text()))
self._history_index = len(self._history)
self.textSent.emit(self.text())
self.clear()
def _history_navigate(self, direction):
if self._history:
self._history_index += direction
if self._history_index < 0:
self._history_index = 0
return
if self._history_index > len(self._history) - 1:
self._history_index = len(self._history)
self.clear()
return
self.setText(self._history[self._history_index])

127
src/qweechat/network.py Normal file
View file

@ -0,0 +1,127 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# I/O with WeeChat/relay.
#
import struct
import qt_compat
QtCore = qt_compat.import_module('QtCore')
QtNetwork = qt_compat.import_module('QtNetwork')
_PROTO_INIT_CMDS = ['init password=%(password)s,compression=gzip',
'(listbuffers) hdata buffer:gui_buffers(*) number,full_name,nicklist,title',
'(listlines) hdata buffer:gui_buffers(*)/own_lines/first_line(*)/data date,displayed,prefix,message',
'(nicklist) nicklist',
'']
class Network(QtCore.QObject):
"""I/O with WeeChat/relay."""
statusChanged = qt_compat.Signal(str, str)
messageFromWeechat = qt_compat.Signal(QtCore.QByteArray)
def __init__(self, *args):
apply(QtCore.QObject.__init__, (self,) + args)
self.status_disconnected = 'disconnected'
self.status_connecting = 'connecting...'
self.status_connected = 'connected'
self._server = None
self._port = None
self._password = None
self._buffer = QtCore.QByteArray()
self._socket = QtNetwork.QTcpSocket()
self._socket.connected.connect(self._socket_connected)
self._socket.error.connect(self._socket_error)
self._socket.readyRead.connect(self._socket_read)
self._socket.disconnected.connect(self._socket_disconnected)
def _socket_connected(self):
"""Slot: socket connected."""
self.statusChanged.emit(self.status_connected, None)
if self._password:
self._socket.write('\n'.join(_PROTO_INIT_CMDS) % {'password': str(self._password)})
def _socket_error(self, error):
"""Slot: socket error."""
self.statusChanged.emit(self.status_disconnected, 'Failed, error: %s' % self._socket.errorString())
def _socket_read(self):
"""Slot: data available on socket."""
avail = self._socket.bytesAvailable()
bytes = self._socket.read(avail)
self._buffer.append(bytes)
while len(self._buffer) >= 4:
remainder = None
length = struct.unpack('>i', self._buffer[0:4])[0]
if len(self._buffer) < length:
# partial message, just wait for end of message
break
# more than one message?
if length < len(self._buffer):
# save beginning of another message
remainder = self._buffer[length:]
self._buffer = self._buffer[0:length]
self.messageFromWeechat.emit(self._buffer)
self._buffer.clear()
if remainder:
self._buffer.append(remainder)
def _socket_disconnected(self):
"""Slot: socket disconnected."""
self._server = None
self._port = None
self._password = None
self.statusChanged.emit(self.status_disconnected, None)
def is_connected(self):
return self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState
def connect_weechat(self, server, port, password):
self._server = server
try:
self._port = int(port)
except:
self._port = 0
self._password = password
if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState:
return
if self._socket.state() != QtNetwork.QAbstractSocket.UnconnectedState:
self._socket.abort()
self._socket.connectToHost(self._server, self._port)
self.statusChanged.emit(self.status_connecting, None)
def disconnect_weechat(self):
if self._socket.state() != QtNetwork.QAbstractSocket.UnconnectedState:
if self._socket.state() != QtNetwork.QAbstractSocket.ConnectedState:
self.statusChanged.emit(self.status_disconnected, None)
self._socket.abort()
def send_to_weechat(self, message):
self._socket.write(message)
def status_icon(self, status):
icon = {self.status_disconnected: 'dialog-close.png',
self.status_connecting: 'dialog-close.png',
self.status_connected: 'dialog-ok-apply.png'}
return icon.get(status, '')

53
src/qweechat/qt_compat.py Normal file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python
#
# File downloaded from: https://github.com/epage/PythonUtils/blob/master/util/qt_compat.py
# Author: epage
# License: LGPL 2.1
#
from __future__ import with_statement
from __future__ import division
_TRY_PYSIDE = True
try:
if not _TRY_PYSIDE:
raise ImportError()
import PySide.QtCore as _QtCore
QtCore = _QtCore
USES_PYSIDE = True
except ImportError:
import sip
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)
import PyQt4.QtCore as _QtCore
QtCore = _QtCore
USES_PYSIDE = False
def _pyside_import_module(moduleName):
pyside = __import__('PySide', globals(), locals(), [moduleName], -1)
return getattr(pyside, moduleName)
def _pyqt4_import_module(moduleName):
pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1)
return getattr(pyside, moduleName)
if USES_PYSIDE:
import_module = _pyside_import_module
Signal = QtCore.Signal
Slot = QtCore.Slot
Property = QtCore.Property
else:
import_module = _pyqt4_import_module
Signal = QtCore.pyqtSignal
Slot = QtCore.pyqtSlot
Property = QtCore.pyqtProperty
if __name__ == "__main__":
pass

290
src/qweechat/qweechat.py Executable file
View file

@ -0,0 +1,290 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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 - WeeChat remote GUI using Qt toolkit.
# (this script requires WeeChat 0.3.7 or newer, running on local or remote host)
#
# History:
#
# 2011-05-27, Sebastien Helleu <flashcode@flashtux.org>:
# start dev
#
import sys, struct
import qt_compat
QtCore = qt_compat.import_module('QtCore')
QtGui = qt_compat.import_module('QtGui')
import config
import weechat.protocol as protocol
import weechat.color as color
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.1-dev'
AUTHOR = 'Sébastien Helleu'
AUTHOR_MAIL= 'flashcode@flashtux.org'
WEECHAT_SITE = 'http://www.weechat.org/'
class MainWindow(QtGui.QMainWindow):
"""Main window."""
def __init__(self, *args):
apply(QtGui.QMainWindow.__init__, (self,) + args)
self.config = config.read()
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.resize(1000, 600)
self.setWindowTitle(NAME)
# 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],
'quit' : ['application-exit.png', 'Quit application', 'Ctrl+Q', self.close],
}
self.actions = {}
for name, action in actions_def.iteritems():
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['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.get('relay', 'password'))
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_dialog.chat.display(0, '<==', message, color='red')
def open_preferences_dialog(self):
pass # TODO
def open_debug_dialog(self):
self.debug_dialog.chat.scroll_bottom()
self.debug_dialog.show()
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.network.send_to_weechat(text + '\n')
self.debug_dialog.chat.display(0, '<==', text, color='red')
def debug_dialog_closed(self, result):
self.debug_dialog.hide()
def open_about_dialog(self):
messages = ['<b>%s</b> %s' % (NAME, VERSION),
'&copy; 2011 %s &lt;<a href="mailto:%s">%s</a>&gt;' % (AUTHOR, AUTHOR_MAIL, AUTHOR_MAIL),
'',
'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', 'password'):
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['password'].text())
self.connection_dialog.close()
def network_status_changed(self, status, extra):
if self.config.getboolean('look', 'statusbar'):
self.statusBar().showMessage(status)
self.debug_dialog.chat.display(0, '', status, color='blue')
self.network_status_set(status, extra)
def network_status_set(self, status, extra):
pal = self.network_status.palette()
if self.network.is_connected():
pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('green'))
else:
pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('#aa0000'))
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()))
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_dialog.chat.display(0, '==>',
'message (%d bytes):\n%s'
% (len(message), protocol.hex_and_ascii(message, 20)),
color='green')
proto = protocol.Protocol()
message = proto.decode(str(message))
if message.uncompressed:
self.debug_dialog.chat.display(0, '==>',
'message uncompressed (%d bytes):\n%s'
% (message.size_uncompressed,
protocol.hex_and_ascii(message.uncompressed, 20)),
color='green')
self.debug_dialog.chat.display(0, '', 'Message: %s' % message)
self.parse_message(message)
def parse_message(self, message):
if message.msgid.startswith('debug'):
self.debug_dialog.chat.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']:
self.list_buffers.addItem('%d. %s' % (item['number'], item['full_name']))
self.buffers.append(Buffer(item))
self.stacked_buffers.addWidget(self.buffers[-1].widget)
self.buffers[-1].bufferInput.connect(self.buffer_input)
self.buffers[-1].widget.input.bufferSwitchPrev.connect(self.list_buffers.switch_prev_buffer)
self.buffers[-1].widget.input.bufferSwitchNext.connect(self.list_buffers.switch_next_buffer)
self.list_buffers.setCurrentRow(0)
self.buffers[0].widget.input.setFocus()
elif message.msgid == 'listlines':
for obj in message.objects:
if obj.objtype == 'hda' and obj.value['path'][-1] == 'line_data':
for item in obj.value['items']:
pointer = item['__path'][0]
buf = filter(lambda b: b.pointer() == pointer, self.buffers)
if buf:
buf[0].widget.chat.display(item['date'],
color.remove(item['prefix']),
color.remove(item['message']))
elif message.msgid == 'nicklist':
for obj in message.objects:
if obj.objtype == 'hda' and obj.value['path'][-1] == 'nick_group':
for item in obj.value['items']:
if not item['group'] and item['visible']:
pointer = item['__path'][0]
buf = filter(lambda b: b.pointer() == pointer, self.buffers)
if buf:
buf[0].add_nick(item['prefix'], item['name'])
def closeEvent(self, event):
self.network.disconnect_weechat()
self.debug_dialog.close()
config.write(self.config)
QtGui.QMainWindow.closeEvent(self, event)
app = QtGui.QApplication(sys.argv)
app.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
main = MainWindow()
sys.exit(app.exec_())

View file

View file

@ -0,0 +1,43 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# Remove or replace colors in WeeChat strings.
#
import re
RE_COLOR_ATTRS = r'[*!/_|]*'
RE_COLOR_STD = r'(?:%s\d{2})' % 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 = re.compile(r'(\x19(?:\d{2}|F%s|B\d{2}|B@\d{5}|\\*%s(,%s)?|@\d{5}|b.|\x1C))|\x1A.|\x1B.|\x1C'
% (RE_COLOR_ANY, RE_COLOR_ANY, RE_COLOR_ANY))
def _replace_color(match):
return match.group(0)
def remove(text):
"""Remove colors in a WeeChat string."""
if not text:
return text
return re.sub(RE_COLOR, '', text)
#return RE_COLOR.sub(_replace_color, text)

View file

@ -0,0 +1,286 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastien 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/>.
#
#
# Decode binary messages received from WeeChat/relay.
#
# For info about protocol and format of messages, please read document "WeeChat Relay Protocol",
# available at: http://www.weechat.org/doc/
#
# History:
#
# 2011-11-23, Sebastien Helleu <flashcode@flashtux.org>:
# start dev
#
import struct, zlib
class WeechatObject:
def __init__(self, objtype, value):
self.objtype = objtype;
self.value = value
def _str_value(self, v):
if type(v) is str and not v is None:
return '\'%s\'' % v
return str(v)
def _str_value_hdata(self):
lines = ['',
' keys: %s' % str(self.value['keys']),
' path: %s' % str(self.value['path'])]
for i, item in enumerate(self.value['items']):
lines.append(' item %d:' % (i + 1))
lines.append('\n'.join([' %s: %s' % (key, self._str_value(value)) for key, value in iter(sorted(item.iteritems()))]))
return '\n'.join(lines)
def _str_value_infolist(self):
lines = ['', ' name: %s' % self.value['name']]
for i, item in enumerate(self.value['items']):
lines.append(' item %d:' % (i + 1))
lines.append('\n'.join([' %s: %s' % (key, self._str_value(value)) for key, value in iter(sorted(item.iteritems()))]))
return '\n'.join(lines)
def _str_value_other(self):
return self._str_value(self.value)
def __str__(self):
self._obj_cb = {'hda': self._str_value_hdata,
'lis': self._str_value_infolist,
}
return '%s: %s' % (self.objtype, self._obj_cb.get(self.objtype, self._str_value_other)())
class WeechatObjects(list):
def __str__(self):
return '\n'.join([str(obj) for obj in self])
class WeechatMessage:
def __init__(self, size, size_uncompressed, compression, uncompressed, msgid, objects):
self.size = size
self.size_uncompressed = size_uncompressed
self.compression = compression
self.uncompressed = uncompressed
self.msgid = msgid
self.objects = objects
def __str__(self):
if self.compression != 0:
return 'size: %d/%d (%d%%), id=\'%s\', objects:\n%s' % (
self.size, self.size_uncompressed,
100 - ((self.size * 100) / self.size_uncompressed),
self.msgid, self.objects)
else:
return 'size: %d, id=\'%s\', objects:\n%s' % (self.size, self.msgid, self.objects)
class Protocol:
"""Decode binary message received from WeeChat/relay."""
def __init__(self):
self._obj_cb = {'chr': self._obj_char,
'int': self._obj_int,
'lon': self._obj_long,
'str': self._obj_str,
'buf': self._obj_buffer,
'ptr': self._obj_ptr,
'tim': self._obj_time,
'hda': self._obj_hdata,
'inf': self._obj_info,
'lis': self._obj_infolist,
}
def _obj_len_data(self, length_size):
"""Read length (1 or 4 bytes), then value with this length."""
if len(self.data) < length_size:
self.data = ''
return None
if length_size == 1:
length = struct.unpack('B', self.data[0:1])[0]
self.data = self.data[1:]
else:
length = self._obj_int()
if length < 0:
return None
if length > 0:
value = self.data[0:length]
self.data = self.data[length:]
else:
value = ''
return value
def _obj_char(self):
"""Read a char in data."""
if len(self.data) < 1:
return 0
value = struct.unpack('b', self.data[0:1])[0]
self.data = self.data[1:]
return value
def _obj_int(self):
"""Read an integer in data (4 bytes)."""
if len(self.data) < 4:
self.data = ''
return 0
value = struct.unpack('>i', self.data[0:4])[0]
self.data = self.data[4:]
return value
def _obj_long(self):
"""Read a long integer in data (length on 1 byte + value as string)."""
value = self._obj_len_data(1)
if value is None:
return None
return int(str(value))
def _obj_str(self):
"""Read a string in data (length on 4 bytes + content)."""
value = self._obj_len_data(4)
if value is None:
return None
return str(value)
def _obj_buffer(self):
"""Read a buffer in data (length on 4 bytes + data)."""
return self._obj_len_data(4)
def _obj_ptr(self):
"""Read a pointer in data (length on 1 byte + value as string)."""
value = self._obj_len_data(1)
if value is None:
return None
return '0x%s' % str(value)
def _obj_time(self):
"""Read a time in data (length on 1 byte + value as string)."""
value = self._obj_len_data(1)
if value is None:
return None
return str(value)
def _obj_hdata(self):
"""Read a hdata in data."""
path = self._obj_str()
keys = self._obj_str()
count = self._obj_int()
list_path = path.split('/')
list_keys = keys.split(',')
keys_types = []
dict_keys = {}
for key in list_keys:
items = key.split(':')
keys_types.append(items)
dict_keys[items[0]] = items[1]
items = []
for i in xrange(0, count):
item = {}
pointers = []
for p in xrange(0, len(list_path)):
pointers.append(self._obj_ptr())
for key, objtype in keys_types:
item[key] = self._obj_cb[objtype]()
item['__path'] = pointers
items.append(item)
return {'path': list_path,
'keys': dict_keys,
'count': count,
'items': items,
}
def _obj_info(self):
"""Read an info in data."""
name = self._obj_str()
value = self._obj_str()
return (name, value)
def _obj_infolist(self):
"""Read an infolist in data."""
name = self._obj_str()
count_items = self._obj_int()
items = []
for i in xrange(0, count_items):
count_vars = self._obj_int()
variables = {}
for v in xrange(0, count_vars):
var_name = self._obj_str()
var_type = self._obj_type()
var_value = self._obj_cb[var_type]()
variables[var_name] = var_value
items.append(variables)
return {'name': name, 'items': items}
def _obj_type(self):
"""Read type in data (3 chars)."""
if len(self.data) < 3:
self.data = ''
return ''
objtype = str(self.data[0:3])
self.data = self.data[3:]
return objtype
def decode(self, data):
"""Decode binary data and return list of objects."""
self.data = data
size = len(self.data)
size_uncompressed = size
uncompressed = None
# uncompress data (if it is compressed)
compression = struct.unpack('b', self.data[4:5])[0]
if compression:
uncompressed = zlib.decompress(self.data[5:])
size_uncompressed = len(uncompressed) + 5
uncompressed = '%s%s%s' % (struct.pack('>i', size_uncompressed), struct.pack('b', 0), uncompressed)
self.data = uncompressed
# skip length and compression flag
self.data = self.data[5:]
# read id
msgid = self._obj_str()
if msgid is None:
msgid = ''
# read objects
objects = WeechatObjects()
while len(self.data) > 0:
objtype = self._obj_type()
value = self._obj_cb[objtype]()
objects.append(WeechatObject(objtype, value))
return WeechatMessage(size, size_uncompressed, compression, uncompressed, msgid, objects)
def hex_and_ascii(data, bytes_per_line=10):
"""Convert a QByteArray to hex + ascii output."""
num_lines = ((len(data) - 1) / bytes_per_line) + 1
if num_lines == 0:
return ''
lines = []
for i in xrange(0, num_lines):
str_hex = []
str_ascii = []
for char in data[i*bytes_per_line:(i*bytes_per_line)+bytes_per_line]:
byte = struct.unpack('B', char)[0]
str_hex.append('%02X' % int(byte))
if byte >= 32 and byte <= 127:
str_ascii.append(char)
else:
str_ascii.append('.')
fmt = '%%-%ds %%s' % ((bytes_per_line * 3) - 1)
lines.append(fmt % (' '.join(str_hex), ''.join(str_ascii)))
return '\n'.join(lines)