summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorent Le Coz <louiz@louiz.org>2012-02-15 20:10:00 +0100
committerFlorent Le Coz <louiz@louiz.org>2012-02-15 20:10:00 +0100
commit695a7ebebaf6a277420dcb42cd96d92d77df0379 (patch)
tree8c9da1e5bf988356ad110f42113949aab94a5866 /src
parent0606c2b351ac87f110e1240d2f4ba1b94b275930 (diff)
parentb89cd8fd8322bf8aa23130398a0a70defcba708d (diff)
downloadpoezio-695a7ebebaf6a277420dcb42cd96d92d77df0379.tar.gz
poezio-695a7ebebaf6a277420dcb42cd96d92d77df0379.tar.bz2
poezio-695a7ebebaf6a277420dcb42cd96d92d77df0379.tar.xz
poezio-695a7ebebaf6a277420dcb42cd96d92d77df0379.zip
Merge branch 'master' of https://git.louiz.org/poezio
Diffstat (limited to 'src')
-rw-r--r--src/bookmark.py2
-rw-r--r--src/common.py11
-rw-r--r--src/config.py27
-rw-r--r--src/connection.py5
-rw-r--r--src/core.py180
-rwxr-xr-xsrc/daemon.py5
-rw-r--r--src/data_forms.py24
-rw-r--r--src/multiuserchat.py18
-rw-r--r--src/tabs.py208
-rw-r--r--src/text_buffer.py3
-rw-r--r--src/theming.py34
-rw-r--r--src/windows.py68
-rw-r--r--src/xhtml.py8
13 files changed, 364 insertions, 229 deletions
diff --git a/src/bookmark.py b/src/bookmark.py
index 45616d93..b63ddbdf 100644
--- a/src/bookmark.py
+++ b/src/bookmark.py
@@ -78,7 +78,7 @@ class Bookmark(object):
"""
jid = el.get('jid')
name = el.get('name')
- autojoin = True if el.get('autojoin', False) == 'true' else False
+ autojoin = True if el.get('autojoin', 'false').lower() in ('true', '1') else False
nick = None
for n in iter(el, 'nick'):
nick = nick.text
diff --git a/src/common.py b/src/common.py
index 2da7835b..26b5dd0f 100644
--- a/src/common.py
+++ b/src/common.py
@@ -235,13 +235,16 @@ def parse_secs_to_str(duration=0):
result += '%ss' % secs if secs else ''
return result
-def parse_command_args_to_alias(args, strto):
+def parse_command_args_to_alias(arg, strto):
"""
Parse command parameters.
Numbers can be from 0 to 9.
- >>> parse_command_args_to_alias(['sdf', 'koin'], '%0 %1')
- "sdf koin"
+ >>> parse_command_args_to_alias('sdf koin', '%1 %0')
+ "koin sdf"
"""
+ if '%' not in strto:
+ return strto + arg
+ args = shell_split(arg)
l = len(args)
dest = ''
var_num = False
@@ -250,7 +253,7 @@ def parse_command_args_to_alias(args, strto):
if not var_num:
dest += i
elif i in string.digits:
- if int(i) < l:
+ if 0 <= int(i) < l:
dest += args[int(i)]
var_num = False
elif i == '%':
diff --git a/src/config.py b/src/config.py
index af9c9fbe..2ee4abb1 100644
--- a/src/config.py
+++ b/src/config.py
@@ -113,16 +113,31 @@ class Config(RawConfigParser):
df.close()
result_lines = []
we_are_in_the_right_section = False
+ written = False
+ section_found = False
for line in lines_before:
if line.startswith('['): # check the section
+ if we_are_in_the_right_section and not written:
+ result_lines.append('%s= %s' % (option, value))
if line == '[%s]' % section:
we_are_in_the_right_section = True
+ section_found = True
else:
we_are_in_the_right_section = False
if (line.startswith('%s ' % (option,)) or
- line.startswith('%s=' % (option,))) and we_are_in_the_right_section:
- line = '%s = %s' % (option, value)
+ line.startswith('%s=' % (option,)) or
+ line.startswith('%s = ' % (option,))) and we_are_in_the_right_section:
+ line = '%s= %s' % (option, value)
+ written = True
result_lines.append(line)
+
+ if not section_found:
+ result_lines.append('[%s]' % section)
+ result_lines.append('%s= %s' % (option, value))
+ elif not written:
+ result_lines.append('%s= %s' % (option, value))
+
+
df = open(self.file_name, 'w')
for line in result_lines:
df.write('%s\n' % line)
@@ -133,11 +148,11 @@ class Config(RawConfigParser):
set the value in the configuration then save it
to the file
"""
- try:
+ if self.has_section(section):
+ RawConfigParser.set(self, section, option, value)
+ else:
+ self.add_section(section)
RawConfigParser.set(self, section, option, value)
- except NoSectionError:
- # TODO, add this section if it didn't exist
- return
self.write_in_file(section, option, value)
def set(self, option, value, section=DEFSECTION):
diff --git a/src/connection.py b/src/connection.py
index 352a1d5b..8a7c1ea7 100644
--- a/src/connection.py
+++ b/src/connection.py
@@ -40,9 +40,9 @@ class Connection(sleekxmpp.ClientXMPP):
self.anon = True
jid = '%s/%s' % (config.get('server', 'anon.louiz.org'), resource)
password = None
- sleekxmpp.ClientXMPP.__init__(self, jid, password, ssl=True)
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.core = None
- self.auto_reconnect = False
+ self.auto_reconnect = True if config.get('auto_reconnect', 'false').lower() in ('true', '1') else False
self.auto_authorize = None
self.register_plugin('xep_0030')
self.register_plugin('xep_0004')
@@ -59,6 +59,7 @@ class Connection(sleekxmpp.ClientXMPP):
self.register_plugin('xep_0092', pconfig=info)
if config.get('send_time', 'true') == 'true':
self.register_plugin('xep_0202')
+ self.register_plugin('xep_0224')
def start(self):
# TODO, try multiple servers
diff --git a/src/core.py b/src/core.py
index 3bb0436e..dfea7dd3 100644
--- a/src/core.py
+++ b/src/core.py
@@ -60,7 +60,7 @@ from daemon import Executor
ERROR_AND_STATUS_CODES = {
'401': _('A password is required'),
'403': _('Permission denied'),
- '404': _('The room does\'nt exist'),
+ '404': _('The room doesn’t exist'),
'405': _('Your are not allowed to create a new room'),
'406': _('A reserved nick must be used'),
'407': _('You are not in the member list'),
@@ -128,7 +128,7 @@ class Core(object):
'status': (self.command_status, _('Usage: /status <availability> [status message]\nStatus: Sets your availability and (optionally) your status message. The <availability> argument is one of \"available, chat, away, afk, dnd, busy, xa\" and the optional [status message] argument will be your status message.'), self.completion_status),
'bookmark_local': (self.command_bookmark_local, _("Usage: /bookmark_local [roomname][/nick]\nBookmark Local: Bookmark locally the specified room (you will then auto-join it on each poezio start). This commands uses almost the same syntaxe as /join. Type /help join for syntaxe examples. Note that when typing \"/bookmark\" on its own, the room will be bookmarked with the nickname you\'re currently using in this room (instead of default_nick)"), self.completion_bookmark_local),
'bookmark': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick] [autojoin] [password]\nBookmark: Bookmark online the specified room (you will then auto-join it on each poezio start if autojoin is specified and is 'true'). This commands uses almost the same syntaxe as /join. Type /help join for syntaxe examples. Note that when typing \"/bookmark\" on its own, the room will be bookmarked with the nickname you\'re currently using in this room (instead of default_nick)"), self.completion_bookmark),
- 'set': (self.command_set, _("Usage: /set <option> [value]\nSet: Set the value of the option in your configuration file. You can, for example, change your default nickname by doing `/set default_nick toto` or your resource with `/set resource blabla`. You can also set an empty value (nothing) by providing no [value] after <option>."), self.completion_set),
+ 'set': (self.command_set, _("Usage: /set [plugin|][section] <option> [value]\nSet: Set the value of an option in your configuration file. You can, for example, change your default nickname by doing `/set default_nick toto` or your resource with `/set resource blabla`. You can also set options in specific sections with `/set bindings M-i ^i` or in specific plugin with `/set mpd_client| host 127.0.0.1`"), None),
'theme': (self.command_theme, _('Usage: /theme [theme_name]\nTheme: Reload the theme defined in the config file. If theme_name is provided, set that theme before reloading it.'), self.completion_theme),
'list': (self.command_list, _('Usage: /list\nList: Get the list of public chatrooms on the specified server.'), self.completion_list),
'message': (self.command_message, _('Usage: /message <jid> [optional message]\nMessage: Open a conversation with the specified JID (even if it is not in our roster), and send a message to it, if the message is specified.'), self.completion_version),
@@ -141,9 +141,9 @@ class Core(object):
'plugins': (self.command_plugins, _('Usage: /plugins\nPlugins: Show the plugins in use.'), None),
'presence': (self.command_presence, _('Usage: /presence <JID> [type] [status]\nPresence: Send a directed presence to <JID> and using [type] and [status] if provided.'), self.completion_presence),
'rawxml': (self.command_rawxml, _('Usage: /rawxml\nRawXML: Send a custom xml stanza.'), None),
- 'set_plugin': (self.command_set_plugin, _("Usage: /set_plugin <plugin> <option> [value]\nSet Plugin: Set the value of the option in a plugin configuration file."), self.completion_set_plugin),
'invite': (self.command_invite, _("Usage: /invite <jid> <room> [reason]\nInvite: Invite jid in room with reason."), self.completion_invite),
'decline': (self.command_decline, _("Usage: /decline <room> [reason]\nDecline: Decline the invitation to room with or without reason."), self.completion_decline),
+ 'invitations': (self.command_invitations, _("Usage: /invites\nInvites: Show the pending invitations."), None),
'bookmarks': (self.command_bookmarks, _("Usage: /bookmarks\nBookmarks: Show the current bookmarks."), None),
'remove_bookmark': (self.command_remove_bookmark, _("Usage: /remove_bookmark [jid]\nRemove Bookmark: Remove the specified bookmark, or the bookmark on the current tab, if any."), self.completion_remove_bookmark),
'xml_tab': (self.command_xml_tab, _("Usage: /xml_tab\nXML Tab: Open an XML tab."), None),
@@ -193,6 +193,7 @@ class Core(object):
self.xmpp.add_event_handler("chatstate_paused", self.on_chatstate_paused)
self.xmpp.add_event_handler("chatstate_gone", self.on_chatstate_gone)
self.xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive)
+ self.xmpp.add_event_handler("attention", self.on_attention)
self.xmpp.register_handler(Callback('ALL THE STANZAS', connection.MatchAll(None), self.incoming_stanza))
self.timed_events = set()
@@ -371,9 +372,12 @@ class Core(object):
if password:
msg += ". The password is \"%s\"." % password
self.information(msg, 'Info')
+ if 'invite' in config.get('beep_on', 'invite').split():
+ curses.beep()
self.pending_invites[jid.bare] = inviter.full
def command_invite(self, arg):
+ """/invite <to> <room> [reason]"""
args = common.shell_split(arg)
if len(args) < 2:
return
@@ -383,14 +387,15 @@ class Core(object):
self.xmpp.plugin['xep_0045'].invite(room, to, reason)
def completion_invite(self, the_input):
+ """Completion for /invite"""
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
- if len(args) == 1:
+ if n == 2:
return the_input.auto_completion([contact.bare_jid for contact in roster.get_contacts()], '')
- elif len(args) == 2:
+ elif n == 3:
rooms = []
for tab in self.tabs:
if isinstance(tab, tabs.MucTab) and tab.joined:
@@ -398,6 +403,7 @@ class Core(object):
return the_input.auto_completion(rooms, '')
def command_decline(self, arg):
+ """/decline <room@server.tld> [reason]"""
args = common.shell_split(arg)
if not len(args):
return
@@ -405,17 +411,30 @@ class Core(object):
if jid.bare not in self.pending_invites:
return
reason = args[1] if len(args) > 1 else ''
+ del self.pending_invites[jid.bare]
self.xmpp.plugin['xep_0045'].decline_invite(jid.bare, self.pending_invites[jid.bare], reason)
def completion_decline(self, the_input):
+ """Completion for /decline"""
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
- if len(args) == 1:
+ if n == 2:
return the_input.auto_completion(list(self.pending_invites.keys()), '')
+ def command_invitations(self, arg):
+ """/invitations"""
+ build = ""
+ for invite in self.pending_invites:
+ build += "%s by %s" % (invite, JID(self.pending_invites[invite]).bare)
+ if self.pending_invites:
+ build = "You are invited to the following rooms:\n" + build
+ else:
+ build = "You are do not have any pending invitation."
+ self.information(build, 'Info')
+
def on_groupchat_decline(self, decline):
pass
@@ -484,6 +503,21 @@ class Core(object):
tab.input.refresh()
self.doupdate()
+ def on_attention(self, message):
+ jid_from = message['from']
+ self.information('%s requests your attention!' % jid_from, 'Info')
+ for tab in self.tabs:
+ if tab.get_name() == jid_from:
+ tab.state = 'attention'
+ self.refresh_tab_win()
+ return
+ for tab in self.tabs:
+ if tab.get_name() == jid_from.bare:
+ tab.state = 'attention'
+ self.refresh_tab_win()
+ return
+ self.information('%s tab not found.' % jid_from, 'Error')
+
def open_new_form(self, form, on_cancel, on_send, **kwargs):
"""
Open a new tab containing the form
@@ -749,20 +783,25 @@ class Core(object):
"""
jid = message['from']
body = xhtml.get_body_from_message_stanza(message)
- conversation = self.get_tab_of_conversation_with_jid(jid, create=False)
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)
self.events.trigger('conversation_msg', message, conversation)
- body = xhtml.get_body_from_message_stanza(message)
if roster.get_contact_by_jid(jid.bare):
remote_nick = roster.get_contact_by_jid(jid.bare).name or jid.user
else:
remote_nick = jid.user
- conversation._text_buffer.add_message(body, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER)
- if conversation.remote_wants_chatstates is None:
+ delay_tag = message.find('{urn:xmpp:delay}delay')
+ if delay_tag is not None:
+ delayed = True
+ date = common.datetime_tuple(delay_tag.attrib['stamp'])
+ else:
+ delayed = False
+ date = None
+ conversation._text_buffer.add_message(body, date, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER, history=delayed)
+ if conversation.remote_wants_chatstates is None and not delayed:
if message['chat_state']:
conversation.remote_wants_chatstates = True
else:
@@ -1046,31 +1085,16 @@ class Core(object):
def go_to_important_room(self):
"""
- Go to the next room with activity, in this order:
- - A personal conversation with a new message
- - A Muc with an highlight
- - A Muc with any new message
+ Go to the next room with activity, in the order defined in the
+ dict tabs.STATE_PRIORITY
"""
- for tab in self.tabs:
- if tab.state == 'private':
- self.command_win('%s' % tab.nb)
- return
- for tab in self.tabs:
- if tab.state == 'highlight':
- self.command_win('%s' % tab.nb)
- return
- for tab in self.tabs:
- if tab.state == 'message':
- self.command_win('%s' % tab.nb)
- return
- for tab in self.tabs:
- if tab.state == 'disconnected':
- self.command_win('%s' % tab.nb)
- return
- for tab in self.tabs:
- if isinstance(tab, tabs.ChatTab) and not tab.input.is_empty():
- self.command_win('%s' % tab.nb)
- return
+ priority = tabs.STATE_PRIORITY
+ sorted_tabs = sorted(self.tabs, key=lambda tab: priority[tab.state],
+ reverse=True)
+ tab = sorted_tabs.pop(0) if sorted_tabs else None
+ if priority[tab.state] < 0 or not tab:
+ return
+ self.command_win('%s' % tab.nb)
def rotate_rooms_right(self, args=None):
"""
@@ -1827,18 +1851,32 @@ class Core(object):
def command_set(self, arg):
"""
- /set <option> [value]
+ /set [module|][section] <option> <value>
"""
- args = arg.split()
- if len(args) != 2 and len(args) != 1:
+ args = common.shell_split(arg)
+ if len(args) != 2 and len(args) != 3:
self.command_help('set')
return
- option = args[0]
if len(args) == 2:
+ option = args[0]
value = args[1]
- else:
- value = ''
- config.set_and_save(option, value)
+ config.set_and_save(option, value)
+ elif len(args) == 3:
+ if '|' in args[0]:
+ plugin_name, section = args[0].split('|')
+ if not section:
+ section = plugin_name
+ option = args[1]
+ value = args[2]
+ if not plugin_name in self.plugin_manager.plugins:
+ return
+ plugin = self.plugin_manager.plugins[plugin_name]
+ plugin.config.set_and_save(option, value, section)
+ else:
+ section = args[0]
+ option = args[1]
+ value = args[2]
+ config.set_and_save(option, value, section)
msg = "%s=%s" % (option, value)
self.information(msg, 'Info')
@@ -1858,61 +1896,6 @@ class Core(object):
serv_list.append(serv)
return the_input.auto_completion(serv_list, ' ')
- def completion_set(self, the_input):
- """Completion for /set"""
- txt = the_input.get_text()
- args = txt.split()
- n = len(args)
- if txt.endswith(' '):
- n += 1
- if n == 2:
- return the_input.auto_completion(config.options('Poezio'), '')
- elif n == 3:
- return the_input.auto_completion([config.get(args[1], '')], '')
-
- def command_set_plugin(self, arg):
- """
- /set_plugin <plugin> <option> [value]
- """
- args = arg.split()
- if len(args) != 3 and len(args) != 2:
- self.command_help('set_plugin')
- return
- plugin_name = args[0]
- if not plugin_name in self.plugin_manager.plugins:
- return
- plugin = self.plugin_manager.plugins[plugin_name]
- option = args[1]
- if len(args) == 3:
- value = args[2]
- else:
- value = ''
- plugin.config.set_and_save(option, value, plugin_name)
- if not plugin.config.write():
- self.core.information('Could not save the plugin config', 'Error')
- return
- msg = "%s=%s" % (option, value)
- self.information(msg, 'Info')
-
- def completion_set_plugin(self, the_input):
- """Completion for /set_plugin"""
- txt = the_input.get_text()
- args = txt.split()
- n = len(args)
- if txt.endswith(' '):
- n += 1
-
- if n == 2:
- return the_input.auto_completion(list(self.plugin_manager.plugins.keys()), '')
- elif n == 3:
- if not args[1] in self.plugin_manager.plugins:
- return
- return the_input.auto_completion(self.plugin_manager.plugins[args[1]].config.options(args[1]), '')
- elif n == 4:
- if not args[1] in self.plugin_manager.plugins:
- return
- return the_input.auto_completion([self.plugin_manager.plugins[args[1]].config.get(args[2], '', args[1])], ' ')
-
def close_tab(self, tab=None):
"""
Close the given tab. If None, close the current one
@@ -2145,7 +2128,10 @@ class Core(object):
self.remote_fifo = None
else:
e = Executor(command.strip())
- e.start()
+ try:
+ e.start()
+ except ValueError as e: # whenever shlex fails
+ self.information('%s' % (e,), 'Error')
def get_conversation_messages(self):
"""
diff --git a/src/daemon.py b/src/daemon.py
index b413f465..4b4c0b79 100755
--- a/src/daemon.py
+++ b/src/daemon.py
@@ -22,7 +22,7 @@ command on your local machine.
import sys
import threading
import subprocess
-
+import shlex
import logging
log = logging.getLogger(__name__)
@@ -40,7 +40,8 @@ class Executor(threading.Thread):
def run(self):
log.info('executing %s' % (self.command.strip(),))
- subprocess.call(self.command.split())
+ command = shlex.split(self.command)
+ subprocess.call(command)
def main():
while True:
diff --git a/src/data_forms.py b/src/data_forms.py
index 2d17a304..d38f392a 100644
--- a/src/data_forms.py
+++ b/src/data_forms.py
@@ -67,15 +67,16 @@ class DataFormsTab(Tab):
def resize(self):
self.need_resize = False
self.topic_win.resize(1, self.width, 0, 0)
- self.tab_win.resize(1, self.width, self.height-2, 0)
- self.form_win.resize(self.height-4, self.width, 1, 0)
+ self.form_win.resize(self.height-3 - Tab.tab_win_height(), self.width, 1, 0)
self.help_win.resize(1, self.width, self.height-1, 0)
- self.help_win_dyn.resize(1, self.width, self.height-3, 0)
+ self.help_win_dyn.resize(1, self.width, self.height-2 - Tab.tab_win_height(), 0)
self.lines = []
def refresh(self):
+ if self.need_resize:
+ self.resize()
self.topic_win.refresh(self._form['title'])
- self.tab_win.refresh()
+ self.refresh_tab_win()
self.help_win.refresh()
self.help_win_dyn.refresh(self.form_win.get_help_message())
self.form_win.refresh()
@@ -88,7 +89,7 @@ class FieldInput(object):
"""
def __init__(self, field):
self._field = field
- self.color = (14, -1)
+ self.color = get_theme().COLOR_NORMAL_TEXT
def set_color(self, color):
self.color = color
@@ -131,6 +132,7 @@ class ColoredLabel(windows.Win):
def refresh(self):
with g_lock:
+ self._win.erase()
self._win.attron(to_curses_attr(self.color))
self.addstr(0, 0, self.text)
self._win.attroff(to_curses_attr(self.color))
@@ -157,7 +159,7 @@ class DummyInput(FieldInput, windows.Win):
class ColoredLabel(windows.Win):
def __init__(self, text):
self.text = text
- self.color = (14, -1)
+ self.color = get_theme().COLOR_NORMAL_TEXT
windows.Win.__init__(self)
def resize(self, height, width, y, x):
@@ -169,6 +171,7 @@ class ColoredLabel(windows.Win):
def refresh(self):
with g_lock:
+ self._win.erase()
self._win.attron(to_curses_attr(self.color))
self.addstr(0, 0, self.text)
self._win.attroff(to_curses_attr(self.color))
@@ -189,6 +192,7 @@ class BooleanWin(FieldInput, windows.Win):
def refresh(self):
with g_lock:
+ self._win.erase()
self._win.attron(to_curses_attr(self.color))
self.addnstr(0, 0, ' '*(8), self.width)
self.addstr(0, 2, "%s"%self.value)
@@ -253,6 +257,7 @@ class TextMultiWin(FieldInput, windows.Win):
def refresh(self):
if not self.edition_input:
with g_lock:
+ self._win.erase()
self._win.attron(to_curses_attr(self.color))
self.addnstr(0, 0, ' '*self.width, self.width)
option = self.options[self.val_pos]
@@ -305,6 +310,7 @@ class ListMultiWin(FieldInput, windows.Win):
def refresh(self):
with g_lock:
+ self._win.erase()
self._win.attron(to_curses_attr(self.color))
self.addnstr(0, 0, ' '*self.width, self.width)
if self.val_pos > 0:
@@ -351,6 +357,7 @@ class ListSingleWin(FieldInput, windows.Win):
def refresh(self):
with g_lock:
+ self._win.erase()
self._win.attron(to_curses_attr(self.color))
self.addnstr(0, 0, ' '*self.width, self.width)
if self.val_pos > 0:
@@ -377,7 +384,7 @@ class TextSingleWin(FieldInput, windows.Input):
self.text = field.getValue() if isinstance(field.getValue(), str)\
else ""
self.pos = len(self.text)
- self.color = (14, -1)
+ self.color = get_theme().COLOR_NORMAL_TEXT
def reply(self):
self._field['label'] = ''
@@ -427,7 +434,7 @@ class FormWin(object):
}
def __init__(self, form, height, width, y, x):
self._form = form
- self._win = curses.newwin(height, width, y, x)
+ self._win = windows.Win._tab_win.derwin(height, width, y, x)
self.current_input = 0
self.inputs = [] # dict list
for (name, field) in self._form.getFields().items():
@@ -447,7 +454,6 @@ class FormWin(object):
'input':inp})
def resize(self, height, width, y, x):
- self._win.resize(height, width)
self.height = height
self.width = width
if self.current_input >= self.height-2:
diff --git a/src/multiuserchat.py b/src/multiuserchat.py
index f537c2c1..3f0c80b8 100644
--- a/src/multiuserchat.py
+++ b/src/multiuserchat.py
@@ -88,21 +88,11 @@ def set_user_role(xmpp, jid, nick, reason, role):
except Exception as e:
return e.iq
-def set_user_affiliation(xmpp, jid, nick, reason, affiliation):
+def set_user_affiliation(xmpp, muc_jid, affiliation, nick=None, jid=None, reason=None):
"""
(try to) Set the affiliation of a MUC user
"""
- iq = xmpp.makeIqSet()
- query = ET.Element('{%s}query' % NS_MUC_ADMIN)
- item = ET.Element('{%s}item' % NS_MUC_ADMIN, {'nick':nick, 'affiliation':affiliation})
- if reason:
- reason_el = ET.Element('{%s}reason' % NS_MUC_ADMIN)
- reason_el.text = reason
- item.append(reason_el)
- query.append(item)
- iq.append(query)
- iq['to'] = jid
try:
- return iq.send()
- except Exception as e:
- return e.iq
+ return xmpp.plugin['xep_0045'].set_affiliation(muc_jid, jid, nick, affiliation)
+ except:
+ return False
diff --git a/src/tabs.py b/src/tabs.py
index e696f46d..c98bbed4 100644
--- a/src/tabs.py
+++ b/src/tabs.py
@@ -62,33 +62,36 @@ NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
STATE_COLORS = {
'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED,
+ 'joined': lambda: get_theme().COLOR_TAB_JOINED,
'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE,
'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT,
'private': lambda: get_theme().COLOR_TAB_PRIVATE,
'normal': lambda: get_theme().COLOR_TAB_NORMAL,
'current': lambda: get_theme().COLOR_TAB_CURRENT,
-# 'attention': lambda: get_theme().COLOR_TAB_ATTENTION,
+ 'attention': lambda: get_theme().COLOR_TAB_ATTENTION,
}
VERTICAL_STATE_COLORS = {
'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED,
+ 'joined': lambda: get_theme().COLOR_VERTICAL_TAB_JOINED,
'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE,
'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT,
'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE,
'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL,
'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT,
-# 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION,
+ 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION,
}
STATE_PRIORITY = {
'normal': -1,
'current': -1,
- 'disconnected': 0,
'message': 1,
+ 'joined': 1,
'highlight': 2,
'private': 2,
-# 'attention': 3
+ 'disconnected': 3,
+ 'attention': 3
}
class Tab(object):
@@ -155,8 +158,8 @@ class Tab(object):
if not value in STATE_COLORS:
log.debug("Invalid value for tab state: %s", value)
elif STATE_PRIORITY[value] < STATE_PRIORITY[self._state] and \
- value != 'current':
- log.debug("Did not set status because of lower priority, asked: %s, kept: %s", (value, self.state))
+ value != 'current' and value != 'joined':
+ log.debug("Did not set status because of lower priority, asked: %s, kept: %s", value, self._state)
else:
self._state = value
@@ -349,6 +352,7 @@ class ChatTab(Tab):
# if that’s None, then no paused chatstate was sent recently
# if that’s a weakref returning None, then a paused chatstate was sent
# since the last input
+ self.remote_supports_attention = False
self.key_func['M-v'] = self.move_separator
self.key_func['M-/'] = self.last_words_completion
self.key_func['^M'] = self.on_enter
@@ -495,6 +499,7 @@ class ChatTab(Tab):
def command_say(self, line):
raise NotImplementedError
+
class MucTab(ChatTab):
"""
The tab containing a multi-user-chat room.
@@ -531,7 +536,7 @@ class MucTab(ChatTab):
self.commands['unignore'] = (self.command_unignore, _("Usage: /unignore <nickname>\nUnignore: Remove the specified nickname from the ignore list."), self.completion_unignore)
self.commands['kick'] = (self.command_kick, _("Usage: /kick <nick> [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason."), self.completion_ignore)
self.commands['role'] = (self.command_role, _("Usage: /role <nick> <role> [reason]\nRole: Set the role of an user. Roles can be: none, visitor, participant, moderator. You also can give an optional reason."), self.completion_role)
- self.commands['affiliation'] = (self.command_affiliation, _("Usage: /affiliation <nick> <affiliation> [reason]\nAffiliation: Set the affiliation of an user. Affiliations can be: none, member, admin, owner. You also can give an optional reason."), self.completion_affiliation)
+ self.commands['affiliation'] = (self.command_affiliation, _("Usage: /affiliation <nick or jid> <affiliation>\nAffiliation: Set the affiliation of an user. Affiliations can be: outcast, none, member, admin, owner."), self.completion_affiliation)
self.commands['topic'] = (self.command_topic, _("Usage: /topic <subject>\nTopic: Change the subject of the room."), self.completion_topic)
self.commands['query'] = (self.command_query, _('Usage: /query <nick> [message]\nQuery: Open a private conversation with <nick>. This nick has to be present in the room you\'re currently in. If you specified a message after the nickname, it will immediately be sent to this user.'), self.completion_ignore)
self.commands['part'] = (self.command_part, _("Usage: /part [message]\nPart: Disconnect from a room. You can specify an optional message."), None)
@@ -543,6 +548,10 @@ class MucTab(ChatTab):
self.commands['configure'] = (self.command_configure, _('Usage: /configure\nConfigure: Configure the current room, through a form.'), None)
self.commands['version'] = (self.command_version, _('Usage: /version <jid or nick>\nVersion: Get the software version of the given JID or nick in room (usually its XMPP client and Operating System).'), self.completion_version)
self.commands['names'] = (self.command_names, _('Usage: /names\nNames: Get the list of the users in the room, and the list of the people assuming the different roles.'), None)
+
+ if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks
+ del self.commands["nick"]
+
self.resize()
self.update_commands()
self.update_keys()
@@ -600,6 +609,10 @@ class MucTab(ChatTab):
n += 1
if n == 2:
userlist = [user.nick for user in self.users]
+ userlist.remove(self.own_nick)
+ jidlist = [user.jid.bare for user in self.users]
+ jidlist.remove(self.core.xmpp.boundjid.bare)
+ userlist.extend(jidlist)
return the_input.auto_completion(userlist, '')
elif n == 3:
possible_affiliations = ['none', 'member', 'admin', 'owner']
@@ -678,6 +691,7 @@ class MucTab(ChatTab):
for i, user in enumerate(sorted_users):
user.color = colors[i % len(colors)]
self.text_win.rebuild_everything(self._text_buffer)
+ self.user_win.refresh(self.users)
self.text_win.refresh()
self.input.refresh()
@@ -758,7 +772,7 @@ class MucTab(ChatTab):
if user.nick == nick:
r = self.core.open_private_window(self.name, user.nick)
if r and len(args) > 1:
- msg = arg[len(nick)+1:]
+ msg = args[1]
self.core.current_tab().command_say(xhtml.convert_simple_to_full_colors(msg))
if not r:
self.core.information(_("Cannot find user: %s" % nick), 'Error')
@@ -787,25 +801,36 @@ class MucTab(ChatTab):
color_participant = get_theme().COLOR_USER_PARTICIPANT[0]
color_information = get_theme().COLOR_INFORMATION_TEXT[0]
visitors, moderators, participants, others = [], [], [], []
+ aff = {
+ 'owner': lambda: get_theme().CHAR_AFFILIATION_OWNER,
+ 'admin': lambda: get_theme().CHAR_AFFILIATION_ADMIN,
+ 'member': lambda: get_theme().CHAR_AFFILIATION_MEMBER,
+ 'none': lambda: get_theme().CHAR_AFFILIATION_NONE,
+ }
+
for user in self.users:
if user.role == 'visitor':
- visitors.append(user.nick)
+ visitors.append((user.nick, aff[user.affiliation]()))
elif user.role == 'participant':
- participants.append(user.nick)
+ participants.append((user.nick, aff[user.affiliation]()))
elif user.role == 'moderator':
- moderators.append(user.nick)
+ moderators.append((user.nick, aff[user.affiliation]()))
else:
- others.append(user.nick)
+ others.append((user.nick, aff[user.affiliation]()))
message = 'Users: %s \n' % len(self.users)
for moderator in moderators:
- message += ' \x19%s}%s\x19o -' % (color_moderator, moderator)
+ message += ' [%s] \x19%s}%s\x19o -' % (moderator[1],
+ color_moderator, moderator[0])
for participant in participants:
- message += ' \x19%s}%s\x19o -' % (color_participant, participant)
+ message += ' [%s] \x19%s}%s\x19o -' % (participant[1],
+ color_participant, participant[0])
for visitor in visitors:
- message += ' \x19%s}%s\x19o -' % (color_visitor, visitor)
+ message += ' [%s] \x19%s}%s\x19o -' % (visitor[1],
+ color_visitor, visitor[0])
for other in others:
- message += ' \x19%s}%s\x19o -' % (color_other, other)
+ message += ' [%s] \x19%s}%s\x19o -' % (other[1],
+ color_other, other[0])
message = message[:-2]
self._text_buffer.add_message(message)
@@ -814,7 +839,7 @@ class MucTab(ChatTab):
def completion_topic(self, the_input):
current_topic = self.topic
- return the_input.auto_completion([current_topic], '')
+ return the_input.auto_completion([current_topic], '', quotify=False)
def command_kick(self, arg):
"""
@@ -854,7 +879,7 @@ class MucTab(ChatTab):
def command_affiliation(self, arg):
"""
- /affiliation <nick> <role> [reason]
+ /affiliation <nick> <role>
Changes the affiliation of an user
roles can be: none, visitor, participant, moderator
"""
@@ -867,14 +892,14 @@ class MucTab(ChatTab):
reason = ' '.join(args[2:])
else:
reason = ''
- if not self.joined or \
- not affiliation in ('none', 'member', 'admin', 'owner'):
-# replace this ↑ with this ↓ when the ban list support is done
-# not affiliation in ('outcast', 'none', 'member', 'admin', 'owner'):
+ if not self.joined:
return
- res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), nick, reason, affiliation)
- if res['type'] == 'error':
- self.core.room_error(res, self.get_name())
+ if nick in [user.nick for user in self.users]:
+ res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), affiliation, nick=nick)
+ else:
+ res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), affiliation, jid=nick)
+ if not res:
+ self.core.information('Could not set affiliation', 'Error')
def command_say(self, line):
needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active'
@@ -1055,12 +1080,14 @@ class MucTab(ChatTab):
self.users.append(new_user)
if from_nick == self.own_nick:
self.joined = True
+ if self != self.core.current_tab():
+ self.state = 'joined'
if self.core.current_tab() == self and self.core.status.show not in ('xa', 'away'):
self.send_chat_state('active')
new_user.color = get_theme().COLOR_OWN_NICK
- self.add_message(_("\x195}Your nickname is \x193}%s") % (from_nick))
+ self.add_message(_("\x19%(info_col)s}Your nickname is \x193}%(nick)s") % {'nick': from_nick, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
if '170' in status_codes:
- self.add_message('\x191}Warning: \x195}this room is publicly logged')
+ self.add_message('\x191}Warning: \x19%(info_col)s}this room is publicly logged' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
else:
change_nick = '303' in status_codes
kick = '307' in status_codes and typ == 'unavailable'
@@ -1089,6 +1116,9 @@ class MucTab(ChatTab):
self.info_header.refresh(self, self.text_win)
self.input.refresh()
self.core.doupdate()
+ else:
+ self.core.current_tab().refresh_tab_win()
+ self.core.doupdate()
def on_user_join(self, from_nick, affiliation, show, status, role, jid):
"""
@@ -1101,9 +1131,9 @@ class MucTab(ChatTab):
if hide_exit_join != 0:
color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
if not jid.full:
- self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x195} joined the room' % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_JOIN})
+ self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} joined the room' % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_JOIN, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
else:
- self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s \x195}(\x194}%(jid)s\x195}) joined the room' % {'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'color':color, 'jid':jid.full})
+ self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s \x19%(info_col)s}(\x194}%(jid)s\x19%(info_col)s}) joined the room' % {'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'color':color, 'jid':jid.full, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
self.core.on_user_rejoined_private_conversation(self.name, from_nick)
def on_user_nick_change(self, presence, user, from_nick, from_room):
@@ -1116,7 +1146,7 @@ class MucTab(ChatTab):
_tab.own_nick = new_nick
user.change_nick(new_nick)
color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
- self.add_message('\x19%(color)d}%(old)s\x195} is now known as \x19%(color)d}%(new)s' % {'old':from_nick, 'new':new_nick, 'color':color})
+ self.add_message('\x19%(color)d}%(old)s\x19%(info_col)s} is now known as \x19%(color)d}%(new)s' % {'old':from_nick, 'new':new_nick, 'color':color, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
# rename the private tabs if needed
self.core.rename_private_tabs(self.name, from_nick, new_nick)
@@ -1134,17 +1164,17 @@ class MucTab(ChatTab):
self.refresh_tab_win()
self.core.doupdate()
if by:
- kick_msg = _('\x191}%(spec)s \x193}You\x195} have been banned by \x194}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by}
+ kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been banned by \x194}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
else:
- kick_msg = _('\x191}%(spec)s \x193}You\x195} have been banned.') % {'spec':get_theme().CHAR_KICK}
+ kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been banned.') % {'spec':get_theme().CHAR_KICK, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
else:
color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
if by:
- kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been banned by \x194}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by}
+ kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has been banned by \x194}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
else:
- kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"'), 'color':color}
+ kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
if reason is not None and reason.text:
- kick_msg += _('\x195} Reason: \x196}%(reason)s\x195}') % {'reason': reason.text}
+ kick_msg += _('\x19%(info_col)s} Reason: \x196}%(reason)s\x19%(info_col)s}') % {'reason': reason.text, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
self._text_buffer.add_message(kick_msg)
def on_user_kicked(self, presence, user, from_nick):
@@ -1161,20 +1191,20 @@ class MucTab(ChatTab):
self.refresh_tab_win()
self.core.doupdate()
if by:
- kick_msg = _('\x191}%(spec)s \x193}You\x195} have been kicked by \x193}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by}
+ kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been kicked by \x193}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
else:
- kick_msg = _('\x191}%(spec)s \x193}You\x195} have been kicked.') % {'spec':get_theme().CHAR_KICK}
+ kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been kicked.') % {'spec':get_theme().CHAR_KICK, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
# try to auto-rejoin
if config.get_by_tabname('autorejoin', 'false', self.general_jid, True) == 'true':
muc.join_groupchat(self.core.xmpp, self.name, self.own_nick)
else:
color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
if by:
- kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been kicked by \x193}%(by)s') % {'spec':get_theme().CHAR_KICK.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'color':color, 'by':by.replace('"', '\\"')}
+ kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has been kicked by \x193}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
else:
- kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been kicked') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"'), 'color':color}
+ kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has been kicked') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
if reason is not None and reason.text:
- kick_msg += _('\x195} Reason: \x196}%(reason)s') % {'reason': reason.text}
+ kick_msg += _('\x19%(info_col)s} Reason: \x196}%(reason)s') % {'reason': reason.text, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
self.add_message(kick_msg)
def on_user_leave_groupchat(self, user, jid, status, from_nick, from_room):
@@ -1192,9 +1222,9 @@ class MucTab(ChatTab):
if hide_exit_join == -1 or user.has_talked_since(hide_exit_join):
color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
if not jid.full:
- leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has left the room') % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_QUIT}
+ leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_QUIT, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
else:
- leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} (\x194}%(jid)s\x195}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'color':color, 'jid':jid.full}
+ leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} (\x194}%(jid)s\x19%(info_col)s}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'color':color, 'jid':jid.full, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
if status:
leave_msg += ' (%s)' % status
self.add_message(leave_msg)
@@ -1210,9 +1240,9 @@ class MucTab(ChatTab):
# to be displayed has changed
color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
if from_nick == self.own_nick:
- msg = _('\x193}You\x195} changed: ')
+ msg = _('\x193}You\x19%(info_col)s} changed: ') % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
else:
- msg = _('\x19%(color)d}%(nick)s\x195} changed: ') % {'nick': from_nick.replace('"', '\\"'), 'color': color}
+ msg = _('\x19%(color)d}%(nick)s\x19%(info_col)s} changed: ') % {'nick': from_nick, 'color': color, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
if show not in SHOW_NAME:
self.core.information("%s from room %s sent an invalid show: %s" %\
(from_nick, from_room, show), "warning")
@@ -1318,6 +1348,7 @@ class MucTab(ChatTab):
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
in the room anymore
+ Return True if the message highlighted us. False otherwise.
"""
self.log_message(txt, time, nickname)
user = self.get_user_by_name(nickname) if nickname is not None else None
@@ -1331,14 +1362,16 @@ class MucTab(ChatTab):
if self.state != 'highlight':
self.state = 'message'
nick_color = nick_color or None
+ highlight = False
if (not nickname or time) and not txt.startswith('/me '):
- txt = '\x195}%s' % (txt,)
+ txt = '\x19%(info_col)s}%(txt)s' % {'txt':txt, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}
else: # TODO
highlight = self.do_highlight(txt, time, nickname)
if highlight:
nick_color = highlight
time = time or datetime.now()
self._text_buffer.add_message(txt, time, nickname, nick_color, history, user)
+ return highlight
class PrivateTab(ChatTab):
"""
@@ -1355,6 +1388,7 @@ class PrivateTab(ChatTab):
self._text_buffer.add_window(self.text_win)
self.info_header = windows.PrivateInfoWin()
self.input = windows.MessageInput()
+ self.check_attention()
# keys
self.key_func['^I'] = self.completion
# commands
@@ -1375,12 +1409,13 @@ class PrivateTab(ChatTab):
def completion(self):
self.complete_commands(self.input)
- def command_say(self, line):
+ def command_say(self, line, attention=False):
if not self.on:
return
msg = self.core.xmpp.make_message(self.get_name())
msg['type'] = 'chat'
msg['body'] = line
+ logger.log_message(self.get_name().replace('/', '\\'), self.own_nick, line)
# trigger the event BEFORE looking for colors.
# This lets a plugin insert \x19xxx} colors, that will
# be converted in xhtml.
@@ -1392,12 +1427,34 @@ class PrivateTab(ChatTab):
if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False:
needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active'
msg['chat_state'] = needed
+ if attention and self.remote_supports_attention:
+ msg['attention'] = True
self.core.events.trigger('private_say_after', msg, self)
msg.send()
self.cancel_paused_delay()
self.text_win.refresh()
self.input.refresh()
+ def command_attention(self, message=''):
+ if message is not '':
+ self.command_say(message, attention=True)
+ else:
+ msg = self.core.xmpp.make_message(self.get_name())
+ msg['type'] = 'chat'
+ msg['attention'] = True
+ msg.send()
+
+ def check_attention(self):
+ self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_name(), block=False, timeout=5, callback=self.on_attention_checked)
+
+ def on_attention_checked(self, iq):
+ if 'urn:xmpp:attention:0' in iq['disco_info'].get_features():
+ self.core.information('Attention is supported', 'Info')
+ self.remote_supports_attention = True
+ self.commands['attention'] = (self.command_attention, _('Usage: /attention [message]\nAttention: Require the attention of the contact. Can also send a message along with the attention.'), None)
+ else:
+ self.remote_supports_attention = False
+
def command_unquery(self, arg):
"""
/unquery
@@ -1416,6 +1473,8 @@ class PrivateTab(ChatTab):
res.get('version') or _('unknown'),
res.get('os') or _('on an unknown platform'))
self.core.information(version, 'Info')
+ if arg:
+ return self.core.command_version(arg)
jid = self.name
self.core.xmpp.plugin['xep_0092'].get_version(jid, callback=callback)
@@ -1503,7 +1562,7 @@ class PrivateTab(ChatTab):
The user changed her nick in the corresponding muc: update the tab’s name and
display a message.
"""
- self.add_message('\x193}%(old)s\x195} is now known as \x193}%(new)s' % {'old':old_nick, 'new':new_nick})
+ self.add_message('\x193}%(old)s\x19%(info_col)s} is now known as \x193}%(new)s' % {'old':old_nick, 'new':new_nick, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
new_jid = JID(self.name).bare+'/'+new_nick
self.name = new_jid
@@ -1513,9 +1572,9 @@ class PrivateTab(ChatTab):
"""
self.deactivate()
if not status_message:
- self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT.replace('"', '\\"')})
+ self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
else:
- self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room (%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT, 'status': status_message.replace('"', '\\"')})
+ self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room (%(status)s)"') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'status': status_message, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
if self.core.current_tab() is self:
self.refresh()
self.core.doupdate()
@@ -1531,7 +1590,7 @@ class PrivateTab(ChatTab):
user = tab.get_user_by_name(nick)
if user:
color = user.color[0]
- self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x195} joined the room' % {'nick':nick, 'color': color, 'spec':get_theme().CHAR_JOIN})
+ self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} joined the room' % {'nick':nick, 'color': color, 'spec':get_theme().CHAR_JOIN, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
if self.core.current_tab() is self:
self.refresh()
self.core.doupdate()
@@ -1663,7 +1722,7 @@ class RosterInfoTab(Tab):
roster.remove_contact(jid)
except Exception as e:
import traceback
- log.debug(_('Traceback when removing %s from the roster:\n')+traceback.format_exc())
+ log.debug(_('Traceback when removing %s from the roster:\n' % jid)+traceback.format_exc())
def command_add(self, args):
"""
@@ -2090,6 +2149,7 @@ class ConversationTab(ChatTab):
self.upper_bar = windows.ConversationStatusMessageWin()
self.info_header = windows.ConversationInfoWin()
self.input = windows.MessageInput()
+ self.check_attention()
# keys
self.key_func['^I'] = self.completion
# commands
@@ -2119,7 +2179,7 @@ class ConversationTab(ChatTab):
def completion(self):
self.complete_commands(self.input)
- def command_say(self, line):
+ def command_say(self, line, attention=False):
msg = self.core.xmpp.make_message(self.get_name())
msg['type'] = 'chat'
msg['body'] = line
@@ -2135,6 +2195,8 @@ class ConversationTab(ChatTab):
if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False:
needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active'
msg['chat_state'] = needed
+ if attention and self.remote_supports_attention:
+ msg['attention'] = True
self.core.events.trigger('conversation_say_after', msg, self)
msg.send()
logger.log_message(JID(self.get_name()).bare, self.core.own_nick, line)
@@ -2150,10 +2212,30 @@ class ConversationTab(ChatTab):
else:
resource = contact.get_highest_priority_resource()
if resource:
- self._text_buffer.add_message("\x195}Status: %s\x193}" %resource.status, None, None, None, None, None)
+ self._text_buffer.add_message("\x19%(info_col)s}Status: %(status)s\x193}" % {'status': resource.status, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}, None, None, None, None, None)
self.refresh()
self.core.doupdate()
+ def command_attention(self, message=''):
+ if message is not '':
+ self.command_say(message, attention=True)
+ else:
+ msg = self.core.xmpp.make_message(self.get_name())
+ msg['type'] = 'chat'
+ msg['attention'] = True
+ msg.send()
+
+ def check_attention(self):
+ self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_name(), block=False, timeout=5, callback=self.on_attention_checked)
+
+ def on_attention_checked(self, iq):
+ if 'urn:xmpp:attention:0' in iq['disco_info'].get_features():
+ self.core.information('Attention is supported', 'Info')
+ self.remote_supports_attention = True
+ self.commands['attention'] = (self.command_attention, _('Usage: /attention [message]\nAttention: Require the attention of the contact. Can also send a message along with the attention.'), None)
+ else:
+ self.remote_supports_attention = False
+
def command_unquery(self, arg):
self.core.close_tab()
@@ -2169,6 +2251,8 @@ class ConversationTab(ChatTab):
res.get('version') or _('unknown'),
res.get('os') or _('on an unknown platform'))
self.core.information(version, 'Info')
+ if arg:
+ return self.core.command_version(arg)
jid = self._name
self.core.xmpp.plugin['xep_0092'].get_version(jid, callback=callback)
@@ -2363,7 +2447,23 @@ class MucListTab(Tab):
'name': item[2] or '' ,'users': ''} for item in iq['disco_items'].get_items()]
self.listview.add_lines(items)
self.upper_message.set_message('Chatroom list on server %s' % self.name)
- self.upper_message.refresh()
+ if self.core.current_tab() is self:
+ self.listview.refresh()
+ self.upper_message.refresh()
+ else:
+ self.state = 'highlight'
+ self.refresh_tab_win()
+ curses.doupdate()
+
+ def sort_by(self):
+ if self.list_header.get_order():
+ self.listview.sort_by_column(col_name=self.list_header.get_sel_column(),asc=False)
+ self.list_header.set_order(False)
+ self.list_header.refresh()
+ else:
+ self.listview.sort_by_column(col_name=self.list_header.get_sel_column(),asc=True)
+ self.list_header.set_order(True)
+ self.list_header.refresh()
curses.doupdate()
def sort_by(self):
diff --git a/src/text_buffer.py b/src/text_buffer.py
index 3541b9c1..9b717882 100644
--- a/src/text_buffer.py
+++ b/src/text_buffer.py
@@ -16,6 +16,7 @@ import collections
from datetime import datetime
from config import config
+from theming import get_theme
Message = collections.namedtuple('Message', 'txt nick_color time str_time nickname user')
@@ -44,7 +45,7 @@ class TextBuffer(object):
else:
color = None
# TODO: display the bg color too.
- txt = ("\x19%s}* \x195}" % (color or 5,))+ nickname + ' ' + txt[4:]
+ txt = ("\x19%(info_col)s}* \x19%(col)s}" % {'col':color or 5, 'info_col':get_theme().COLOR_INFORMATION_TEXT[0]})+ nickname + ' \x19%(info_col)s}' % {'info_col':get_theme().COLOR_INFORMATION_TEXT[0]} + txt[4:]
nickname = None
msg = Message(txt='%s\x19o'%(txt.replace('\t', ' '),), nick_color=nick_color,
time=time, str_time=time.strftime("%Y-%m-%d %H:%M:%S")\
diff --git a/src/theming.py b/src/theming.py
index 7e90a5a7..c29d044d 100644
--- a/src/theming.py
+++ b/src/theming.py
@@ -11,16 +11,17 @@ used when drawing the interface.
Colors are numbers from -1 to 7 (if only 8 colors are supported) or -1 to 255
if 256 colors are available.
-We check the number of available colors at startup, and we load a theme accordingly.
-A 8 color theme should NEVER use colors not in the -1 -> 7 range. We won't check that
-at run time. If the case occurs, the THEME should be fixed.
+If only 8 colors are available, all colors > 8 are converted using the
+table_256_to_16 dict.
+
XHTML-IM colors are converted to -1 -> 255 colors if available, or directly to
-1 -> 8 if we are in 8-color-mode.
A pair_color is a background-foreground pair. All possible pairs are not created
at startup, because that would create 256*256 pairs, and almost all of them
would never be used.
-So, a theme should define color tuples, like (200, -1), and when they are to
+
+A theme should define color tuples, like (200, -1), and when they are to
be used by poezio's interface, they will be created once, and kept in a list for
later usage.
A color tuple is of the form (foreground, background, optional)
@@ -78,7 +79,7 @@ class Theme(object):
"""
# Message text color
COLOR_NORMAL_TEXT = (-1, -1)
- COLOR_INFORMATION_TEXT = (137, -1) # TODO
+ COLOR_INFORMATION_TEXT = (5, -1) # TODO
COLOR_HIGHLIGHT_NICK = (3, 5, 'b')
# User list color
@@ -100,6 +101,13 @@ class Theme(object):
CHAR_CHATSTATE_COMPOSING = 'X'
CHAR_CHATSTATE_PAUSED = 'p'
+ # These characters are used for the affiliation in the user list
+ # in a MUC
+ CHAR_AFFILIATION_OWNER = '~'
+ CHAR_AFFILIATION_ADMIN = '&'
+ CHAR_AFFILIATION_MEMBER = '+'
+ CHAR_AFFILIATION_NONE = '-'
+
# Separators
COLOR_VERTICAL_SEPARATOR = (4, -1)
COLOR_NEW_TEXT_SEPARATOR = (2, -1)
@@ -114,17 +122,21 @@ class Theme(object):
# Tabs
COLOR_TAB_NORMAL = (7, 4)
+ COLOR_TAB_JOINED = (82, 4)
COLOR_TAB_CURRENT = (7, 6)
COLOR_TAB_NEW_MESSAGE = (7, 5)
- COLOR_TAB_HIGHLIGHT = (7, 1)
+ COLOR_TAB_HIGHLIGHT = (7, 3)
COLOR_TAB_PRIVATE = (7, 2)
+ COLOR_TAB_ATTENTION = (7, 1)
COLOR_TAB_DISCONNECTED = (7, 8)
COLOR_VERTICAL_TAB_NORMAL = (4, -1)
+ COLOR_VERTICAL_TAB_JOINED = (82, -1)
COLOR_VERTICAL_TAB_CURRENT = (7, 4)
COLOR_VERTICAL_TAB_NEW_MESSAGE = (5, -1)
- COLOR_VERTICAL_TAB_HIGHLIGHT = (1, -1)
+ COLOR_VERTICAL_TAB_HIGHLIGHT = (3, -1)
COLOR_VERTICAL_TAB_PRIVATE = (2, -1)
+ COLOR_VERTICAL_TAB_ATTENTION = (1, -1)
COLOR_VERTICAL_TAB_DISCONNECTED = (8, -1)
# Nickname colors
@@ -161,8 +173,8 @@ class Theme(object):
CHAR_JOIN = '--->'
CHAR_QUIT = '<---'
CHAR_KICK = '-!-'
- CHAR_COLUMN_ASC = ' ▲'
- CHAR_COLUMN_DESC =' ▼'
+ CHAR_COLUMN_ASC = ' ▲'
+ CHAR_COLUMN_DESC =' ▼'
COLOR_JOIN_CHAR = (4, -1)
COLOR_QUIT_CHAR = (1, -1)
@@ -270,8 +282,8 @@ def reload_theme():
file_path = os.path.join(themes_dir, theme_name)+'.py'
log.debug('Theme file to load: %s' %(file_path,))
new_theme = imp.load_source('theme', os.path.join(themes_dir, theme_name)+'.py')
- except:
- return 'Theme not found'
+ except Exception as e:
+ return 'Failed to load theme: %s' % (e,)
theme = new_theme.theme
if __name__ == '__main__':
diff --git a/src/windows.py b/src/windows.py
index 68518086..b8d276f0 100644
--- a/src/windows.py
+++ b/src/windows.py
@@ -199,6 +199,10 @@ class UserList(Win):
'none': lambda: get_theme().COLOR_USER_NONE,
'': lambda: get_theme().COLOR_USER_NONE
}
+ self.symbol_affiliation = {'owner': lambda: get_theme().CHAR_AFFILIATION_OWNER,
+ 'admin': lambda: get_theme().CHAR_AFFILIATION_ADMIN,
+ 'member': lambda: get_theme().CHAR_AFFILIATION_MEMBER,
+ 'none': lambda: get_theme().CHAR_AFFILIATION_NONE, }
self.color_show = {'xa': lambda: get_theme().COLOR_STATUS_XA,
'none': lambda: get_theme().COLOR_STATUS_NONE,
'': lambda: get_theme().COLOR_STATUS_NONE,
@@ -206,7 +210,6 @@ class UserList(Win):
'away': lambda: get_theme().COLOR_STATUS_AWAY,
'chat': lambda: get_theme().COLOR_STATUS_CHAT
}
-
def scroll_up(self):
self.pos += self.height-1
@@ -222,30 +225,24 @@ class UserList(Win):
log.debug('Refresh: %s',self.__class__.__name__)
with g_lock:
self._win.erase()
- y = 0
- users = sorted(users)
+ if config.get('user_list_sort', 'desc').lower() == 'asc':
+ y, x = self._win.getmaxyx()
+ y -= 1
+ users = sorted(users, reverse=True)
+ else:
+ y = 0
+ users = sorted(users)
+
if self.pos >= len(users) and self.pos != 0:
self.pos = len(users)-1
for user in users[self.pos:]:
- if not user.role in self.color_role:
- role_col = get_theme().COLOR_USER_NONE
- else:
- role_col = self.color_role[user.role]()
- if not user.show in self.color_show:
- show_col = get_theme().COLOR_STATUS_NONE
+ self.draw_role_affiliation(y, user)
+ self.draw_status_chatstate(y, user)
+ self.addstr(y, 2, user.nick[:self.width-2], to_curses_attr(user.color))
+ if config.get('user_list_sort', 'desc').lower() == 'asc':
+ y -= 1
else:
- show_col = self.color_show[user.show]()
- if user.chatstate == 'composing':
- char = get_theme().CHAR_CHATSTATE_COMPOSING
- elif user.chatstate == 'active':
- char = get_theme().CHAR_CHATSTATE_ACTIVE
- elif user.chatstate == 'paused':
- char = get_theme().CHAR_CHATSTATE_PAUSED
- else:
- char = get_theme().CHAR_STATUS
- self.addstr(y, 0, char, to_curses_attr(show_col))
- self.addstr(y, 1, user.nick[:self.width-2], to_curses_attr(role_col))
- y += 1
+ y += 1
if y == self.height:
break
# draw indicators of position in the list
@@ -255,6 +252,29 @@ class UserList(Win):
self.draw_plus(self.height-1)
self._refresh()
+ def draw_role_affiliation(self, y, user):
+ if not user.role in self.color_role:
+ color = get_theme().COLOR_USER_NONE
+ else:
+ color = self.color_role[user.role]()
+ symbol = self.symbol_affiliation.get(user.affiliation, lambda: '-')()
+ self.addstr(y, 1, symbol, to_curses_attr(color))
+
+ def draw_status_chatstate(self, y, user):
+ if not user.show in self.color_show:
+ show_col = get_theme().COLOR_STATUS_NONE
+ else:
+ show_col = self.color_show[user.show]()
+ if user.chatstate == 'composing':
+ char = get_theme().CHAR_CHATSTATE_COMPOSING
+ elif user.chatstate == 'active':
+ char = get_theme().CHAR_CHATSTATE_ACTIVE
+ elif user.chatstate == 'paused':
+ char = get_theme().CHAR_CHATSTATE_PAUSED
+ else:
+ char = get_theme().CHAR_STATUS
+ self.addstr(y, 0, char, to_curses_attr(show_col))
+
def resize(self, height, width, y, x):
with g_lock:
self._resize(height, width, y, x)
@@ -302,7 +322,7 @@ class GlobalInfoBar(Win):
for tab in sorted_tabs:
color = tab.color
if config.get('show_inactive_tabs', 'true') == 'false' and\
- color == get_theme().COLOR_TAB_NORMAL:
+ color is get_theme().COLOR_TAB_NORMAL:
continue
try:
self.addstr("%s" % str(tab.nb), to_curses_attr(color))
@@ -334,7 +354,7 @@ class VerticalGlobalInfoBar(Win):
sorted_tabs = sorted(self.core.tabs, key=comp)
if config.get('show_inactive_tabs', 'true') == 'false':
sorted_tabs = [tab for tab in sorted_tabs if\
- tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL]
+ tab.vertical_color is not get_theme().COLOR_VERTICAL_TAB_NORMAL]
nb_tabs = len(sorted_tabs)
if nb_tabs >= height:
for y, tab in enumerate(sorted_tabs):
@@ -1659,8 +1679,6 @@ class ListWin(Win):
if not lines:
return
self.lines += lines
- self.refresh()
- curses.doupdate()
def get_selected_row(self):
"""
diff --git a/src/xhtml.py b/src/xhtml.py
index e7a045fa..cf7a5fc0 100644
--- a/src/xhtml.py
+++ b/src/xhtml.py
@@ -194,7 +194,9 @@ def get_body_from_message_stanza(message):
if config.get('enable_xhtml_im', 'true') == 'true':
xhtml_body = message['xhtml_im']
if xhtml_body:
- return xhtml_to_poezio_colors(xhtml_body)
+ content = xhtml_to_poezio_colors(xhtml_body)
+ content = content if content else message['body']
+ return content or " "
return message['body']
def ncurses_color_to_html(color):
@@ -288,9 +290,9 @@ def xhtml_to_poezio_colors(text):
for elem in elems:
if elem.tag == '{http://www.w3.org/1999/xhtml}a':
if 'href' in elem.attrib and elem.attrib['href'] != elem.text:
- message += '\x19u%s\x19o (%s)' % (trim(elem.attrib['href']), trim(elem.text))
+ message += '\x19u%s\x19o (%s)' % (trim(elem.attrib['href']), trim(elem.text if elem.text else ""))
else:
- message += '\x19u' + elem.text + '\x19o'
+ message += '\x19u' + (elem.text if elem.text else "") + '\x19o'
elif elem.tag == '{http://www.w3.org/1999/xhtml}blockquote':
message += '“'
elif elem.tag == '{http://www.w3.org/1999/xhtml}body':