summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG5
-rw-r--r--data/default_config.cfg20
-rw-r--r--src/common.py8
-rw-r--r--src/connection.py1
-rw-r--r--src/core.py86
-rw-r--r--src/logger.py10
-rw-r--r--src/pubsub.py307
-rw-r--r--src/room.py9
-rw-r--r--src/roster.py4
-rw-r--r--src/tabs.py36
-rw-r--r--src/text_buffer.py2
-rw-r--r--src/windows.py33
12 files changed, 465 insertions, 56 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 98af618b..83e3b812 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,18 +2,21 @@ This file describes the new features in each poezio release.
For more detailed changelog, see the roadmap:
http://dev.louiz.org/project/poezio/roadmap
+
* Poezio 0.7.2 - dev
- Chatstate notifications (in private AND in MUCs)
- /message command to talk to any JID
- /version command to get the software version of an entity
--
+- /bind command, and keys can be bound in the config file
- Multiline edition
+
* Poezio 0.7.1 - 2 Feb 2010
- /status command to globally change the status
- /win command now accepts part of tab name as argument
- bugfixes
+
* Poezio 0.7 - 14 jan 2010
Codename ”Koshie & Mathieui”
- Library changed from xmpppy to SleekXMPP
diff --git a/data/default_config.cfg b/data/default_config.cfg
index c376a73c..53357009 100644
--- a/data/default_config.cfg
+++ b/data/default_config.cfg
@@ -136,6 +136,16 @@ log_dir =
# with no activity, set to true. Else, set to false
show_inactive_tabs = true
+# The terminal can beep on various event. Put the event you want in a list
+# (separated by spaces).
+# The events can be
+# - highlight (when you are highlighted in a MUC)
+# - private (when a new private message is received, from your contacts or
+# someone from a MUC)
+# - message (any message from a MUC)
+beep_on = highlight private
+
+
# Theme
# If themes_dir is not set, logs will searched for in $XDG_DATA_HOME/poezio/themes,
@@ -179,6 +189,16 @@ send_time = true
max_messages_in_memory = 2048
max_lines_in_memory = 2048
+[bindings]
+# Bindings are keyboard shortcut aliases. You can use them
+# to define your own keys and bind them with some functions
+# The syntaxe is
+# key = bind
+# where ^x means Control + x
+# and M-x means Alt + x
+# The example turns Alt + i into a tab key
+M-i = ^I
+
[var]
# You should not edit this section, it is just used by poezio
# to save various data across restarts
diff --git a/src/common.py b/src/common.py
index 9435dab5..0bc93c8d 100644
--- a/src/common.py
+++ b/src/common.py
@@ -42,6 +42,8 @@ import curses
import time
import shlex
+from config import config
+
ROOM_STATE_NONE = 11
ROOM_STATE_CURRENT = 10
ROOM_STATE_PRIVATE = 15
@@ -211,3 +213,9 @@ def curses_color_pair(color):
if color < 0:
return curses.color_pair(-color) | curses.A_BOLD
return curses.color_pair(color)
+
+def replace_key_with_bound(key):
+ if config.has_option('bindings', key):
+ return config.get(key, key, 'bindings')
+ else:
+ return key
diff --git a/src/connection.py b/src/connection.py
index 8bef6eb2..d021f44b 100644
--- a/src/connection.py
+++ b/src/connection.py
@@ -56,6 +56,7 @@ class Connection(sleekxmpp.ClientXMPP):
self.register_plugin('xep_0030')
self.register_plugin('xep_0004')
self.register_plugin('xep_0045')
+ self.register_plugin('xep_0060')
self.register_plugin('xep_0071')
self.register_plugin('xep_0085')
if config.get('send_poezio_info', 'true') == 'true':
diff --git a/src/core.py b/src/core.py
index 4aa1b2b3..df7f3cad 100644
--- a/src/core.py
+++ b/src/core.py
@@ -129,17 +129,16 @@ class Core(object):
'version': (self.command_version, _('Usage: /version <jid>\nVersion: get the software version of the given JID (usually its XMPP client and Operating System)'), None),
'connect': (self.command_reconnect, _('Usage: /connect\nConnect: disconnect from the remote server if you are currently connected and then connect to it again'), None),
'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: disconnect and reconnects in all the rooms in domain.'), None),
+ 'bind': (self.command_bind, _('Usage: /bind <key> <equ>\nBind: bind a key to an other key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same than the Up key.')),
}
self.key_func = {
"KEY_PPAGE": self.scroll_page_up,
"KEY_NPAGE": self.scroll_page_down,
"KEY_F(5)": self.rotate_rooms_left,
- "M-[1;6D": self.rotate_rooms_left,
"^P": self.rotate_rooms_left,
'kLFT3': self.rotate_rooms_left,
"KEY_F(6)": self.rotate_rooms_right,
- "M-[1;6C": self.rotate_rooms_right,
"^N": self.rotate_rooms_right,
'kRIT3': self.rotate_rooms_right,
"KEY_F(7)": self.shrink_information_win,
@@ -335,6 +334,7 @@ class Core(object):
def on_got_offline(self, presence):
jid = presence['from']
contact = roster.get_contact_by_jid(jid.bare)
+ logger.log_roster_change(jid.bare, 'got offline')
if not contact:
return
log.debug('on_got_offline: %s' % presence)
@@ -356,6 +356,7 @@ class Core(object):
if not contact:
# Todo, handle presence comming from contacts not in roster
return
+ logger.log_roster_change(jid.bare, 'got online')
resource = contact.get_resource_by_fulljid(jid.full)
assert not resource
resource = Resource(jid.full)
@@ -485,7 +486,7 @@ class Core(object):
for tab in self.tabs:
if tab.get_name() == jid_from.bare and isinstance(tab, tabs.MucTab):
if message['type'] == 'error':
- return self.room_error(message, tab.get_room().name)
+ return self.room_error(message, jid_from)
else:
return self.on_groupchat_private_message(message)
return self.on_normal_message(message)
@@ -513,6 +514,8 @@ class Core(object):
conversation.remote_wants_chatstates = True
else:
conversation.remote_wants_chatstates = False
+ if 'private' in config.get('beep_on', 'highlight private').split():
+ curses.beep()
logger.log_message(jid.full.replace('/', '\\'), nick_from, body)
if conversation is self.current_tab():
self.refresh_window()
@@ -550,6 +553,8 @@ class Core(object):
jid = message['from']
body = xhtml.get_body_from_message_stanza(message)
if not body:
+ if message['type'] == 'error':
+ self.information(self.get_error_message_from_error_stanza(message), 'Error')
return
conversation = self.get_tab_of_conversation_with_jid(jid, create=True)
if roster.get_contact_by_jid(jid.bare):
@@ -563,6 +568,8 @@ class Core(object):
else:
conversation.remote_wants_chatstates = False
logger.log_message(jid.bare, remote_nick, body)
+ if 'private' in config.get('beep_on', 'highlight private').split():
+ curses.beep()
if self.current_tab() is not conversation:
conversation.set_color_state(theme.COLOR_TAB_PRIVATE)
self.refresh_tab_win()
@@ -668,7 +675,8 @@ class Core(object):
"""
# curses.ungetch(0) # FIXME
while self.running:
- char_list = self.read_keyboard()
+ char_list = [common.replace_key_with_bound(key)\
+ for key in self.read_keyboard()]
# Special case for M-x where x is a number
if len(char_list) == 1:
char = char_list[0]
@@ -767,7 +775,8 @@ class Core(object):
def refresh_tab_win(self):
self.current_tab().tab_win.refresh()
- self.current_tab().input.refresh()
+ if self.current_tab().input:
+ self.current_tab().input.refresh()
self.doupdate()
def add_tab(self, new_tab, focus=False):
@@ -850,26 +859,34 @@ class Core(object):
self.current_tab().on_scroll_up()
self.refresh_window()
- def room_error(self, error, room_name):
+ def get_error_message_from_error_stanza(self, stanza):
"""
- Display the error on the room window
+ Takes a stanza of the form <message type='error'><error/></message>
+ and return a well formed string containing the error informations
"""
- room = self.get_room_by_name(room_name)
- msg = error['error']['type']
- condition = error['error']['condition']
- code = error['error']['code']
- body = error['error']['text']
+ msg = stanza['error']['type']
+ condition = stanza['error']['condition']
+ code = stanza['error']['code']
+ body = stanza['error']['text']
if not body:
if code in ERROR_AND_STATUS_CODES:
body = ERROR_AND_STATUS_CODES[code]
else:
body = condition or _('Unknown error')
if code:
- msg = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code}
- self.add_message_to_text_buffer(room, msg)
+ message = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code}
else:
- msg = _('Error: %(msg)s: %(body)s') % {'msg':msg, 'body':body}
- self.add_message_to_text_buffer(room, msg)
+ message = _('Error: %(msg)s: %(body)s') % {'msg':msg, 'body':body}
+ return message
+
+ def room_error(self, error, room_name):
+ """
+ Display the error on the room window
+ """
+ room = self.get_room_by_name(room_name)
+ error_message = self.get_error_message_from_error_stanza(error)
+ self.add_message_to_text_buffer(room, error_message)
+ code = error['error']['code']
if code == '401':
msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)')
self.add_message_to_text_buffer(room, msg)
@@ -968,13 +985,15 @@ class Core(object):
body = xhtml.get_body_from_message_stanza(message)
if body:
date = date if delayed == True else None
- self.add_message_to_text_buffer(room, body, date, nick_from)
+ self.add_message_to_text_buffer(room, body, date, nick_from, history=True if date else False)
if tab is self.current_tab():
tab.text_win.refresh(tab._room)
tab.info_header.refresh(tab._room, tab.text_win)
self.refresh_tab_win()
+ if 'message' in config.get('beep_on', 'highlight private').split():
+ curses.beep()
- def add_message_to_text_buffer(self, room, txt, time=None, nickname=None):
+ def add_message_to_text_buffer(self, room, txt, time=None, nickname=None, history=None):
"""
Add the message to the room if possible, else, add it to the Info window
(in the Info tab of the info window in the RosterTab)
@@ -982,7 +1001,7 @@ class Core(object):
if not room:
self.information('Trying to add a message in no room: %s' % txt, 'Error')
else:
- room.add_message(txt, time, nickname)
+ room.add_message(txt, time, nickname, history=history)
def command_help(self, arg):
"""
@@ -1073,7 +1092,7 @@ class Core(object):
"""
/reconnect
"""
- self.disconnect(True)
+ self.disconnect(reconnect=True)
def command_list(self, arg):
"""
@@ -1221,6 +1240,7 @@ class Core(object):
else: # no server could be found, print a message and return
self.information(_("You didn't specify a server for the room you want to join"), 'Error')
return
+ room = room.lower()
r = self.get_room_by_name(room)
if len(args) == 2: # a password is provided
password = args[1]
@@ -1229,7 +1249,6 @@ class Core(object):
return
if room.startswith('@'):
room = room[1:]
- room = room.lower()
current_status = self.get_status()
if r and not r.joined:
muc.join_groupchat(self.xmpp, room, nick, password,
@@ -1359,6 +1378,16 @@ class Core(object):
tab.get_room().joined = False
self.command_join(tab.get_name())
+ def command_bind(self, arg):
+ """
+ Bind a key.
+ """
+ args = common.shell_split(arg)
+ if len(args) != 2:
+ return self.command_help('bind')
+ config.set_and_save(args[0], args[1], section='bindings')
+ self.information('%s is now bound to %s' % (args[0], args[1]), 'Info')
+
def go_to_room_number(self):
"""
Read 2 more chars and go to the tab
@@ -1378,15 +1407,19 @@ class Core(object):
def information(self, msg, typ=''):
"""
- Displays an informational message in the "Info" room window
+ Displays an informational message in the "Info" buffer
"""
nb_lines = self.information_buffer.add_message(msg, nickname=typ)
if typ != '' and typ.lower() in config.get('information_buffer_popup_on',
'error roster warning help info').split():
popup_time = config.get('popup_time', 4) + (nb_lines - 1) * 2
self.pop_information_win_up(nb_lines, popup_time)
+ else:
+ if self.information_win_size != 0:
+ self.information_win.refresh(self.information_buffer)
+ self.current_tab().input.refresh()
- def disconnect(self, msg=None):
+ def disconnect(self, msg=None, reconnect=False):
"""
Disconnect from remote server and correctly set the states of all
parts of the client (for example, set the MucTabs as not joined, etc)
@@ -1394,13 +1427,10 @@ class Core(object):
for tab in self.tabs:
if isinstance(tab, tabs.MucTab):
muc.leave_groupchat(self.xmpp, tab.get_room().name, tab.get_room().own_nick, msg)
+ roster.empty()
self.save_config()
# Ugly fix thanks to gmail servers
- try:
- sys.stderr = None
- self.xmpp.disconnect(False)
- except:
- pass
+ self.xmpp.disconnect(reconnect)
def command_quit(self, arg):
"""
diff --git a/src/logger.py b/src/logger.py
index ad615f9b..d87eaa6b 100644
--- a/src/logger.py
+++ b/src/logger.py
@@ -34,6 +34,7 @@ class Logger(object):
"""
def __init__(self):
self.logfile = config.get('logfile', 'logs')
+ self.roster_logfile = None
# a dict of 'groupchatname': file-object (opened)
self.fds = dict()
@@ -81,4 +82,13 @@ class Logger(object):
else:
fd.flush() # TODO do something better here?
+ def log_roster_change(self, jid, message):
+ if not self.roster_logfile:
+ try:
+ self.roster_logfile = open(os.path.join(DATA_HOME, 'logs', 'roster.log'), 'a')
+ except IOError:
+ return
+ self.roster_logfile.write('%s %s %s\n' % (datetime.now().strftime('%d-%m-%y [%H:%M:%S]'), jid, message))
+ self.roster_logfile.flush()
+
logger = Logger()
diff --git a/src/pubsub.py b/src/pubsub.py
new file mode 100644
index 00000000..af9ca001
--- /dev/null
+++ b/src/pubsub.py
@@ -0,0 +1,307 @@
+# Copyright 2010-2011 Le Coz Florent <louiz@louiz.org>
+#
+# This file is part of Poezio.
+#
+# Poezio 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, version 3 of the License.
+#
+# Poezio 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 Poezio. If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+log = logging.getLogger(__name__)
+
+import curses
+
+import windows
+import tabs
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+class PubsubNode(object):
+ node_type = None # unknown yet
+ def __init__(self, name, parent=None):
+ self.items = []
+ self.name = name
+ self.parent = parent
+
+
+class LeafNode(PubsubNode):
+ node_type = "leaf"
+ def __init__(self, name, parent=None):
+ PubsubNode.__init__(self, name, parent)
+
+
+class CollectionNode(PubsubNode):
+ node_type = "collection"
+ def __init__(self, name, parent=None):
+ PubsubNode.__init__(self, name, parent)
+ self.subnodes = []
+
+
+class PubsubItem(object):
+ def __init__(self, idd, content):
+ self.id = idd
+ self.content = content
+
+ def to_dict(self, columns):
+ """
+ returns a dict of the values listed in columns
+ """
+ ret = {}
+ for col in columns:
+ ret[col] = self.__dict__.get(col) or ''
+ return ret
+
+class PubsubBrowserTab(tabs.Tab):
+ """
+ A tab containing a pubsub browser letting the user
+ list nodes and items, view, add and delete items, etc
+ """
+ def __init__(self, server):
+ """
+ Server is the name of the pubsub server, for example:
+ pubsub.example.com
+ All action done in this tab will be made on that server.
+ """
+ tabs.Tab.__init__(self)
+ self.current_node = None # the subnode we are listing. None means the root
+ self.server = server
+ self.nodes = [] # the lower level of nodes
+
+ self.tab_win = windows.GlobalInfoBar()
+ self.upper_message = windows.Topic()
+ self.upper_message.set_message('Pubsub server: %s/%s' % (self.server,self.current_node or ''))
+
+ # Node List View
+ node_columns = ('node', 'name',)
+ self.node_list_header = windows.ColumnHeaderWin(node_columns)
+ self.node_listview = windows.ListWin(node_columns)
+
+ # Item List View
+ item_columns = ('id',)
+ self.item_list_header = windows.ColumnHeaderWin(item_columns)
+ self.item_listview = windows.ListWin(item_columns)
+
+ # Item viewer
+ self.item_viewer = windows.SimpleTextWin('')
+ self.default_help_message = windows.HelpText("“c”: create a node.")
+ self.input = self.default_help_message
+
+ self.key_func['c'] = self.command_create_node
+ self.key_func["M-KEY_DOWN"] = self.scroll_node_down
+ self.key_func["M-KEY_UP"] = self.scroll_node_up
+ self.key_func["KEY_DOWN"] = self.item_listview.move_cursor_down
+ self.key_func["KEY_UP"] = self.item_listview.move_cursor_up
+ self.key_func["^M"] = self.open_selected_item
+ self.resize()
+
+ self.get_nodes()
+
+ def resize(self):
+ self.upper_message.resize(1, self.width, 0, 0)
+ self.tab_win.resize(1, self.width, self.height-2, 0)
+
+ column_size = {'node': self.width//4,
+ 'name': self.width//4,}
+ self.node_list_header.resize_columns(column_size)
+ self.node_list_header.resize(1, self.width//2, 1, 0)
+ self.node_listview.resize_columns(column_size)
+ self.node_listview.resize(self.height//2-2, self.width//2, 2, 0)
+
+ column_size = {'id': self.width//2,}
+ self.item_list_header.resize_columns(column_size)
+ self.item_list_header.resize(self.height//2+1, self.width//2, self.height//2, 0)
+ self.item_listview.resize_columns(column_size)
+ self.item_listview.resize(self.height//2-3, self.width//2, self.height//2+1, 0)
+
+ self.item_viewer.resize(self.height-3, self.width//2+1, 1, self.width//2)
+ self.input.resize(1, self.width, self.height-1, 0)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s'%self.__class__.__name__)
+ self.upper_message.refresh()
+ self.node_list_header.refresh()
+ self.node_listview.refresh()
+ self.item_list_header.refresh()
+ self.item_listview.refresh()
+ self.item_viewer.refresh()
+ self.tab_win.refresh()
+ self.input.refresh()
+
+ def get_name(self):
+ return '%s@@pubsubbrowser' % (self.server,)
+
+ def on_input(self, key):
+ res = self.input.do_command(key)
+ if res:
+ return True
+ if key in self.key_func:
+ return self.key_func[key]()
+
+ def get_selected_node_name(self):
+ """
+ From the node_view_list, returns the node name of the selected
+ one. None can be returned
+ """
+ line = self.node_listview.get_selected_row()
+ if not line:
+ return None
+ return line['node']
+
+ def get_node_by_name(self, name):
+ """
+ in the currently browsed node (or on the root), return the node with that name
+ """
+ nodes = self.current_node and self.current_node.subnodes or self.nodes
+ for node in nodes:
+ if node.name == name:
+ return node
+ return None
+
+ def get_item_by_id(self, idd):
+ """
+ in the currently selected node, return the item with that id
+ """
+ selected_node_name = self.get_selected_node_name()
+ if not selected_node_name:
+ return None
+ selected_node = self.get_node_by_name(selected_node_name)
+ if not selected_node:
+ return None
+ for item in selected_node.items:
+ if item.id == idd:
+ return item
+ return None
+
+ def get_selected_item_id(self):
+ """
+ returns the id of the currently selected item
+ """
+ line = self.item_listview.get_selected_row()
+ if not line:
+ return None
+ return line['id']
+
+ def get_items(self, node):
+ """
+ Get all items in the given node
+ """
+ items = self.core.xmpp.plugin['xep_0060'].get_items(self.server, node.name)
+ item_list = []
+ if items:
+ for it in items:
+ item_list.append(PubsubItem(it.attrib['id'], it))
+ node.items = item_list
+ log.debug('get_selected_node_name: %s' % self.get_selected_node_name())
+ if self.get_selected_node_name() == node.name:
+ self.display_items_from_node(node)
+ log.debug('Item on node %s: %s' % (node.name, item_list))
+
+ def display_items_from_node(self, node):
+ """
+ takes a node, and set fill the item_listview with that
+ node’s items
+ """
+ columns = self.item_list_header.get_columns()
+ self.item_listview.empty()
+ log.debug('display_items_from_node: %s' % node.items)
+ for item in node.items:
+ self.item_listview.lines.append(item.to_dict(columns))
+
+ def add_nodes(self, node_list, parent=None):
+ """
+ Add Node objects to the list of the parent.
+ If parent is None, they are added to the root list.
+ If the current selected node is parent, we add
+ them directly to the node_listview
+ """
+ log.debug('Adding nodes to %s: %s' % (node_list, parent,))
+ if not parent:
+ list_to_append = self.nodes
+ else:
+ list_to_append = parent.nodes
+ self.node_listview.add_lines(node_list)
+ for node in node_list:
+ new_node = LeafNode(node['node'])
+ list_to_append.append(new_node)
+ self.get_items(new_node)
+
+ def get_nodes(self, node=None):
+ """
+ Get all subnodes of the given node. If no node is given, get
+ the root nodes
+ """
+ nodes = self.core.xmpp.plugin['xep_0060'].get_nodes(self.server)
+ lines = [{'name': nodes[node] or '',
+ 'node': node} for node in nodes.keys()]
+ self.add_nodes(lines)
+
+ def create_node(self, node_name):
+ if node_name:
+ res = self.core.xmpp.plugin['xep_0060'].create_node(self.server, node_name)
+ if res:
+ self.node_listview.add_lines([{'name': '', 'node': node_name}])
+ self.reset_help_message()
+ return True
+
+ def reset_help_message(self, txt=None):
+ """
+ Just reset the help message when a command ends
+ """
+ curses.curs_set(0)
+ self.input = self.default_help_message
+ return True
+
+ def command_create_node(self):
+ """
+ Prompt for a node name and create it on Enter key
+ """
+ curses.curs_set(1)
+ self.input = windows.CommandInput("[Create node]", self.reset_help_message, self.create_node, None)
+ self.input.resize(1, self.width, self.height-1, 0)
+ return True
+
+ def scroll_node_up(self):
+ """
+ scroll the node up, and update the item list if needed
+ """
+ selected_node_before = self.get_selected_node_name()
+ self.node_listview.move_cursor_up()
+ selected_node_after = self.get_selected_node_name()
+ if selected_node_after is not selected_node_before:
+ self.display_items_from_node(self.get_node_by_name(selected_node_after))
+ return True
+ return False
+
+ def scroll_node_down(self):
+ """
+ scroll the node down, and update the item list if needed
+ """
+ selected_node_before = self.get_selected_node_name()
+ self.node_listview.move_cursor_down()
+ selected_node_after = self.get_selected_node_name()
+ if selected_node_after is not selected_node_before:
+ self.display_items_from_node(self.get_node_by_name(selected_node_after))
+ return True
+ return False
+
+ def open_selected_item(self):
+ """
+ displays the currently selected item in the item view window
+ """
+ selected_item = self.get_item_by_id(self.get_selected_item_id())
+ if not selected_item:
+ return
+ log.debug('Content: %s'%ET.tostring(selected_item.content))
+ self.item_viewer._text = str(ET.tostring(selected_item.content))
+ self.item_viewer.rebuild_text()
+ return True
diff --git a/src/room.py b/src/room.py
index 45ebddbd..5d4c4ce6 100644
--- a/src/room.py
+++ b/src/room.py
@@ -24,6 +24,7 @@ import common
import theme
import logging
+import curses
log = logging.getLogger(__name__)
@@ -77,6 +78,10 @@ class Room(TextBuffer):
self.set_color_state(theme.COLOR_TAB_HIGHLIGHT)
color = theme.COLOR_HIGHLIGHT_NICK
break
+ if color:
+ beep_on = config.get('beep_on', 'highlight private').split()
+ if 'highlight' in beep_on and 'message' not in beep_on:
+ curses.beep()
return color
def get_user_by_name(self, nick):
@@ -95,7 +100,7 @@ class Room(TextBuffer):
"""
self.color_state = color
- def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None):
+ def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None, history=None):
"""
Note that user can be None even if nickname is not None. It happens
when we receive an history message said by someone who is not
@@ -130,7 +135,7 @@ class Room(TextBuffer):
self.messages.append(message)
for window in self.windows: # make the associated windows
# build the lines from the new message
- nb = window.build_new_message(message)
+ nb = window.build_new_message(message, history=history)
if window.pos != 0:
window.scroll_up(nb)
return nb
diff --git a/src/roster.py b/src/roster.py
index aed5f5a0..afe83c9e 100644
--- a/src/roster.py
+++ b/src/roster.py
@@ -45,6 +45,10 @@ class Roster(object):
except IOError:
return
+ def empty(self):
+ self._contacts = {}
+ self._roster_groups = []
+
def add_contact(self, contact, jid):
"""
Add a contact to the contact list
diff --git a/src/tabs.py b/src/tabs.py
index 4ea3c280..8d4e6447 100644
--- a/src/tabs.py
+++ b/src/tabs.py
@@ -269,6 +269,7 @@ class ChatTab(Tab):
self.commands['say'] = (self.command_say,
_("""Usage: /say <message>\nSay: Just send the message.
Useful if you want your message to begin with a '/'"""), None)
+ self.chat_state = None
def last_words_completion(self):
"""
@@ -308,6 +309,7 @@ class ChatTab(Tab):
msg = self.core.xmpp.make_message(self.get_name())
msg['type'] = self.message_type
msg['chat_state'] = state
+ self.chat_state = state
msg.send()
def send_composing_chat_state(self, empty_before, empty_after):
@@ -316,8 +318,13 @@ class ChatTab(Tab):
on the the current status of the input
"""
if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates:
- if empty_after:
+ if self.chat_state == "composing" and not empty_after:
+ self.cancel_paused_delay()
+ self.set_paused_delay(True)
+ elif empty_after and not self.chat_state == 'active':
+ self.cancel_paused_delay()
self.send_chat_state("active")
+ elif empty_after:
self.cancel_paused_delay()
elif empty_before or (self.timed_event_paused is not None and not self.timed_event_paused()):
self.cancel_paused_delay()
@@ -388,7 +395,6 @@ class MucTab(ChatTab):
self.ignores = [] # set of Users
# keys
self.key_func['^I'] = self.completion
- self.key_func['M-i'] = self.completion
self.key_func['M-u'] = self.scroll_user_list_down
self.key_func['M-y'] = self.scroll_user_list_up
# commands
@@ -912,6 +918,7 @@ class MucTab(ChatTab):
if status:
leave_msg += ' (%s)' % status
room.add_message(leave_msg)
+ self.core.refresh_window()
self.core.on_user_left_private_conversation(from_room, from_nick, status)
def on_user_change_status(self, room, user, from_nick, from_room, affiliation, role, show, status):
@@ -971,7 +978,6 @@ class PrivateTab(ChatTab):
self.input = windows.MessageInput()
# keys
self.key_func['^I'] = self.completion
- self.key_func['M-i'] = self.completion
# commands
self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None)
self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None)
@@ -983,7 +989,11 @@ class PrivateTab(ChatTab):
def command_say(self, line):
msg = self.core.xmpp.make_message(self.get_name())
msg['type'] = 'chat'
- msg['body'] = line
+ if line.find('\x19') == -1:
+ msg['body'] = line
+ else:
+ msg['body'] = xhtml.clean_text(line)
+ msg['xhtml_im'] = xhtml.poezio_colors_to_html(line)
if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates is not False:
msg['chat_state'] = 'active'
msg.send()
@@ -1041,7 +1051,8 @@ class PrivateTab(ChatTab):
empty_before = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
self.input.do_command(key)
empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
- self.send_composing_chat_state(empty_before, empty_after)
+ if self.core.get_tab_by_name(JID(self.get_room().name).bare, MucTab).get_room().joined:
+ self.send_composing_chat_state(empty_before, empty_after)
return False
def on_lose_focus(self):
@@ -1054,7 +1065,7 @@ class PrivateTab(ChatTab):
def on_gain_focus(self):
self._room.set_color_state(theme.COLOR_TAB_CURRENT)
curses.curs_set(1)
- if config.get('send_chat_states', 'true') == 'true' and not self.input.get_text():
+ if self.get_room().joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text():
self.send_chat_state('active')
def on_scroll_up(self):
@@ -1089,9 +1100,9 @@ class PrivateTab(ChatTab):
The user left the associated MUC
"""
if not status_message:
- self.get_room().add_message(_('%(spec)s "[%(nick)s]" has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')})
+ self.get_room().add_message(_('\x191%(spec)s \x193%(nick)s\x195 has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')})
else:
- self.get_room().add_message(_('%(spec)s "[%(nick)s]" has left the room "(%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT, 'status': status_message.replace('"', '\\"')})
+ self.get_room().add_message(_('\x191%(spec)s \x193%(nick)s\x195 has left the room (%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT, 'status': status_message.replace('"', '\\"')})
class RosterInfoTab(Tab):
"""
@@ -1110,7 +1121,6 @@ class RosterInfoTab(Tab):
self.input = self.default_help_message
self.set_color_state(theme.COLOR_TAB_NORMAL)
self.key_func['^I'] = self.completion
- self.key_func['M-i'] = self.completion
self.key_func[' '] = self.on_space
self.key_func["/"] = self.on_slash
self.key_func["KEY_UP"] = self.move_cursor_up
@@ -1457,7 +1467,6 @@ class ConversationTab(ChatTab):
self.input = windows.MessageInput()
# keys
self.key_func['^I'] = self.completion
- self.key_func['M-i'] = self.completion
# commands
self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None)
self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None)
@@ -1469,7 +1478,11 @@ class ConversationTab(ChatTab):
def command_say(self, line):
msg = self.core.xmpp.make_message(self.get_name())
msg['type'] = 'chat'
- msg['body'] = line
+ if line.find('\x19') == -1:
+ msg['body'] = line
+ else:
+ msg['body'] = xhtml.clean_text(line)
+ msg['xhtml_im'] = xhtml.poezio_colors_to_html(line)
if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates is not False:
msg['chat_state'] = 'active'
msg.send()
@@ -1585,7 +1598,6 @@ class MucListTab(Tab):
self.key_func["KEY_DOWN"] = self.listview.move_cursor_down
self.key_func["KEY_UP"] = self.listview.move_cursor_up
self.key_func['^I'] = self.completion
- self.key_func['M-i'] = self.completion
self.key_func["/"] = self.on_slash
self.key_func['j'] = self.join_selected
self.key_func['J'] = self.join_selected_no_focus
diff --git a/src/text_buffer.py b/src/text_buffer.py
index e0d0fc1c..a3b5b1fb 100644
--- a/src/text_buffer.py
+++ b/src/text_buffer.py
@@ -44,7 +44,7 @@ class TextBuffer(object):
def add_window(self, win):
self.windows.append(win)
- def add_message(self, txt, time=None, nickname=None, nick_color=None):
+ def add_message(self, txt, time=None, nickname=None, nick_color=None, history=None):
msg = Message(txt='%s\x19o'%(txt,), nick_color=nick_color,
time=time or datetime.now(), nickname=nickname, user=None)
self.messages.append(msg)
diff --git a/src/windows.py b/src/windows.py
index 1d54f7ed..d4d9b2a7 100644
--- a/src/windows.py
+++ b/src/windows.py
@@ -538,7 +538,7 @@ class TextWin(Win):
if None not in self.built_lines:
self.built_lines.append(None)
- def build_new_message(self, message):
+ def build_new_message(self, message, history=None):
"""
Take one message, build it and add it to the list
Return the number of lines that are built for the given
@@ -570,7 +570,10 @@ class TextWin(Win):
else:
txt = txt.replace('\t', ' ')
# length of the time
- offset = 9
+ if history:
+ offset = 20
+ else:
+ offset = 9
if theme.CHAR_TIME_RIGHT:
offset += 1
if theme.CHAR_TIME_RIGHT:
@@ -596,7 +599,10 @@ class TextWin(Win):
else:
color = None
if first:
- time = message.time.strftime("%H:%M:%S")
+ if history:
+ time = message.time.strftime("%Y-%m-%d %H:%M:%S")
+ else:
+ time = message.time.strftime("%H:%M:%S")
nickname = nick
else:
time = None
@@ -1326,7 +1332,7 @@ class RosterWin(Win):
self.roster_len = len(roster)
while self.roster_len and self.pos >= self.roster_len:
self.move_cursor_up()
- # self._win.erase()
+ self._win.erase()
self._win.move(0, 0)
self.draw_roster_information(roster)
y = 1
@@ -1363,10 +1369,6 @@ class RosterWin(Win):
y += 1
if y-self.start_pos+1 == self.height:
break
- line = ' '*self.width
- while y < self.height:
- self.addstr(y, 0, line)
- y += 1
if self.start_pos > 1:
self.draw_plus(1)
if self.start_pos + self.height-2 < self.roster_len:
@@ -1516,6 +1518,14 @@ class ListWin(Win):
self._selected_row = 0
self._starting_pos = 0 # The column number from which we start the refresh
+ def empty(self):
+ """
+ emtpy the list and reset some important values as well
+ """
+ self.lines = []
+ self._selected_row = 0
+ self._starting_pos = 0
+
def resize_columns(self, dic):
"""
Resize the width of the columns
@@ -1607,6 +1617,9 @@ class ColumnHeaderWin(Win):
def resize_columns(self, dic):
self._columns_sizes = dic
+ def get_columns(self):
+ return self._columns
+
def refresh(self):
log.debug('Refresh: %s'%self.__class__.__name__)
with g_lock:
@@ -1626,10 +1639,6 @@ class SimpleTextWin(Win):
self._text = text
self.built_lines = []
- def resize(self, height, width, y, x, stdscr):
- self._resize(height, width, y, x, stdscr)
- self.rebuild_text()
-
def rebuild_text(self):
"""
Transform the text in lines than can then be