summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.py47
-rw-r--r--src/bookmark.py250
-rw-r--r--src/bookmarks.py289
-rw-r--r--src/config.py68
-rw-r--r--src/connection.py16
-rw-r--r--src/core/commands.py650
-rw-r--r--src/core/completions.py35
-rw-r--r--src/core/core.py400
-rw-r--r--src/core/handlers.py242
-rw-r--r--src/core/structs.py51
-rwxr-xr-xsrc/daemon.py6
-rw-r--r--src/decorators.py96
-rw-r--r--src/events.py5
-rw-r--r--src/fixes.py6
-rwxr-xr-xsrc/keyboard.py3
-rw-r--r--src/logger.py15
-rw-r--r--src/multiuserchat.py7
-rw-r--r--src/pep.py356
-rw-r--r--src/plugin.py16
-rw-r--r--src/plugin_manager.py45
-rw-r--r--src/poezio.py32
-rw-r--r--src/tabs/__init__.py1
-rw-r--r--src/tabs/adhoc_commands_list.py6
-rw-r--r--src/tabs/basetabs.py124
-rw-r--r--src/tabs/bookmarkstab.py145
-rw-r--r--src/tabs/conversationtab.py73
-rw-r--r--src/tabs/listtab.py6
-rw-r--r--src/tabs/muclisttab.py8
-rw-r--r--src/tabs/muctab.py674
-rw-r--r--src/tabs/privatetab.py44
-rw-r--r--src/tabs/rostertab.py546
-rw-r--r--src/tabs/xmltab.py225
-rw-r--r--src/text_buffer.py28
-rwxr-xr-xsrc/theming.py37
-rw-r--r--src/user.py32
-rw-r--r--src/windows/__init__.py5
-rw-r--r--src/windows/bookmark_forms.py278
-rw-r--r--src/windows/data_forms.py1
-rw-r--r--src/windows/funcs.py4
-rw-r--r--src/windows/info_bar.py7
-rw-r--r--src/windows/info_wins.py14
-rw-r--r--src/windows/input_placeholders.py16
-rw-r--r--src/windows/inputs.py7
-rw-r--r--src/windows/muc.py9
-rw-r--r--src/windows/roster_win.py19
-rw-r--r--src/windows/text_win.py309
-rw-r--r--src/xhtml.py5
47 files changed, 3382 insertions, 1876 deletions
diff --git a/src/args.py b/src/args.py
index 6b0108f0..8b1ebbbd 100644
--- a/src/args.py
+++ b/src/args.py
@@ -3,41 +3,26 @@ Module related to the argument parsing
There is a fallback to the deprecated optparse if argparse is not found
"""
-from gettext import gettext as _
from os import path
+from argparse import ArgumentParser, SUPPRESS
def parse_args(CONFIG_PATH=''):
"""
Parse the arguments from the command line
"""
- try:
- from argparse import ArgumentParser, SUPPRESS
- except ImportError:
- from optparse import OptionParser
- from optparse import SUPPRESS_HELP as SUPPRESS
- parser = OptionParser()
- parser.add_option("-f", "--file", dest="filename",
- default=path.join(CONFIG_PATH, 'poezio.cfg'),
- help=_("The config file you want to use"),
- metavar="CONFIG_FILE")
- parser.add_option("-d", "--debug", dest="debug",
- help=_("The file where debug will be written"),
- metavar="DEBUG_FILE")
- parser.add_option("-v", "--version", dest="version",
- help=SUPPRESS, metavar="VERSION",
- default="0.8.3-dev")
- (options, __) = parser.parse_args()
- else:
- parser = ArgumentParser()
- parser.add_argument("-f", "--file", dest="filename",
- default=path.join(CONFIG_PATH, 'poezio.cfg'),
- help=_("The config file you want to use"),
- metavar="CONFIG_FILE")
- parser.add_argument("-d", "--debug", dest="debug",
- help=_("The file where debug will be written"),
- metavar="DEBUG_FILE")
- parser.add_argument("-v", "--version", dest="version",
- help=SUPPRESS, metavar="VERSION",
- default="0.8.3-dev")
- options = parser.parse_args()
+ parser = ArgumentParser('poezio')
+ parser.add_argument("-c", "--check-config", dest="check_config",
+ action='store_true',
+ help='Check the config file')
+ parser.add_argument("-d", "--debug", dest="debug",
+ help="The file where debug will be written",
+ metavar="DEBUG_FILE")
+ parser.add_argument("-f", "--file", dest="filename",
+ default=path.join(CONFIG_PATH, 'poezio.cfg'),
+ help="The config file you want to use",
+ metavar="CONFIG_FILE")
+ parser.add_argument("-v", "--version", dest="version",
+ help=SUPPRESS, metavar="VERSION",
+ default="0.9-dev")
+ options = parser.parse_args()
return options
diff --git a/src/bookmark.py b/src/bookmark.py
deleted file mode 100644
index 15a28c9d..00000000
--- a/src/bookmark.py
+++ /dev/null
@@ -1,250 +0,0 @@
-"""
-Bookmarks module
-
-Therein the bookmark class is defined, representing one conference room.
-This object is used to generate elements for both local and remote
-bookmark storage. It can also parse xml Elements.
-
-This module also defines several functions for retrieving and updating
-bookmarks, both local and remote.
-"""
-
-import functools
-import logging
-from sys import version_info
-
-from slixmpp.plugins.xep_0048 import Bookmarks, Conference
-from common import safeJID
-from config import config
-
-log = logging.getLogger(__name__)
-
-def xml_iter(xml, tag=''):
- if version_info[1] >= 2:
- return xml.iter(tag)
- else:
- return xml.getiterator(tag)
-
-preferred = config.get('use_bookmarks_method').lower()
-if preferred not in ('pep', 'privatexml'):
- preferred = 'privatexml'
-not_preferred = 'privatexml' if preferred == 'pep' else 'pep'
-methods = ('local', preferred, not_preferred)
-
-
-class Bookmark(object):
- possible_methods = methods
-
- def __init__(self, jid, name=None, autojoin=False, nick=None, password=None, method='privatexml'):
- self.jid = jid
- self.name = name or jid
- self.autojoin = autojoin
- self.nick = nick
- self.password = password
- self._method = method
-
- @property
- def method(self):
- return self._method
-
- @method.setter
- def method(self, value):
- if value not in self.possible_methods:
- log.debug('Could not set bookmark storing method: %s', value)
- return
- self._method = value
-
- def __repr__(self):
- return '<%s%s%s>' % (self.jid, ('/'+self.nick) if self.nick else '', '|autojoin' if self.autojoin else '')
-
- def stanza(self):
- """
- Generate a <conference/> stanza from the instance
- """
- el = Conference()
- el['name'] = self.name
- el['jid'] = self.jid
- el['autojoin'] = 'true' if self.autojoin else 'false'
- if self.nick:
- el['nick'] = self.nick
- if self.password:
- el['password'] = self.password
- return el
-
- def local(self):
- """Generate a str for local storage"""
- local = self.jid
- if self.nick:
- local += '/%s' % self.nick
- local += ':'
- if self.password:
- config.set_and_save('password', self.password, section=self.jid)
- return local
-
- @staticmethod
- def parse_from_element(el, method=None):
- """
- Generate a Bookmark object from a <conference/> element
- """
- jid = el.get('jid')
- name = el.get('name')
- autojoin = True if el.get('autojoin', 'false').lower() in ('true', '1') else False
- nick = None
- for n in xml_iter(el, 'nick'):
- nick = n.text
- password = None
- for p in xml_iter(el, 'password'):
- password = p.text
-
- return Bookmark(jid, name, autojoin, nick, password, method)
-
-bookmarks = []
-
-def get_by_jid(value):
- """
- Get a bookmark by bare jid
- """
- for item in bookmarks:
- if item.jid == value:
- return item
-
-def remove(value):
- """
- Remove a bookmark (with its jid or directly the Bookmark object).
- """
- if isinstance(value, str):
- value = get_by_jid(value)
- bookmarks.remove(value)
-
-def stanza_storage(method):
- """Generate a <storage/> stanza with the conference elements."""
- storage = Bookmarks()
- for b in (b for b in bookmarks if b.method == method):
- storage.append(b.stanza())
- return storage
-
-def save_pep(xmpp):
- """Save the remote bookmarks via PEP."""
- xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('pep'),
- method='xep_0223')
-
-def save_privatexml(xmpp):
- """"Save the remote bookmarks with privatexml."""
- xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('privatexml'),
- method='xep_0049')
-
-def save_remote(xmpp, callback, method=preferred):
- """Save the remote bookmarks."""
- method = 'privatexml' if method != 'pep' else 'pep'
-
- if method is 'privatexml':
- xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('privatexml'),
- method='xep_0049',
- callback=callback)
- else:
- xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('pep'),
- method='xep_0223',
- callback=callback)
-
-def save_local():
- """Save the local bookmarks."""
- local = ''.join(bookmark.local() for bookmark in bookmarks if bookmark.method is 'local')
- config.set_and_save('rooms', local)
-
-def save(xmpp, core=None):
- """Save all the bookmarks."""
- save_local()
- def _cb(core, iq):
- if iq["type"] == "error":
- core.information('Could not save bookmarks.', 'Error')
- elif core:
- core.information('Bookmarks saved', 'Info')
- if config.get('use_remote_bookmarks'):
- preferred = config.get('use_bookmarks_method')
- cb = functools.partial(_cb, core)
- save_remote(xmpp, cb, method=preferred)
-
-def get_pep(xmpp, available_methods, callback):
- """Add the remotely stored bookmarks via pep to the list."""
- def _cb(iq):
- if iq["type"] == "error":
- available_methods["pep"] = False
- else:
- available_methods["pep"] = True
- for conf in xml_iter(iq.xml, '{storage:bookmarks}conference'):
- b = Bookmark.parse_from_element(conf, method='pep')
- if not get_by_jid(b.jid):
- bookmarks.append(b)
- if callback:
- callback()
-
- xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0223', callback=_cb)
-
-def get_privatexml(xmpp, available_methods, callback):
- """Add the remotely stored bookmarks via privatexml to the list.
- If both is True, we want to have the result of both methods (privatexml and pep) before calling pep"""
- def _cb(iq):
- if iq["type"] == "error":
- available_methods["privatexml"] = False
- else:
- available_methods["privatexml"] = True
- for conf in xml_iter(iq.xml, '{storage:bookmarks}conference'):
- b = Bookmark.parse_from_element(conf, method='privatexml')
- if not get_by_jid(b.jid):
- bookmarks.append(b)
- if callback:
- callback()
-
- xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0049', callback=_cb)
-
-def get_remote(xmpp, callback):
- """Add the remotely stored bookmarks to the list."""
- if xmpp.anon:
- return
- method = config.get('use_bookmarks_method')
- if not method:
- available_methods = {}
- def _save_and_call_callback():
- # If both methods returned a result, we can now call the given callback
- if callback and "privatexml" in available_methods and "pep" in available_methods:
- save_bookmarks_method(available_methods)
- if callback:
- callback()
- for method in methods[1:]:
- if method == 'pep':
- get_pep(xmpp, available_methods, _save_and_call_callback)
- else:
- get_privatexml(xmpp, available_methods, _save_and_call_callback)
- else:
- if method == 'pep':
- get_pep(xmpp, {}, callback)
- else:
- get_privatexml(xmpp, {}, callback)
-
-def save_bookmarks_method(available_methods):
- pep, privatexml = available_methods["pep"], available_methods["privatexml"]
- if pep and not privatexml:
- config.set_and_save('use_bookmarks_method', 'pep')
- elif privatexml and not pep:
- config.set_and_save('use_bookmarks_method', 'privatexml')
- elif not pep and not privatexml:
- config.set_and_save('use_bookmarks_method', '')
-
-def get_local():
- """Add the locally stored bookmarks to the list."""
- rooms = config.get('rooms')
- if not rooms:
- return
- rooms = rooms.split(':')
- for room in rooms:
- jid = safeJID(room)
- if jid.bare == '':
- continue
- if jid.resource != '':
- nick = jid.resource
- else:
- nick = None
- passwd = config.get_by_tabname('password', jid.bare, fallback=False) or None
- b = Bookmark(jid.bare, autojoin=True, nick=nick, password=passwd, method='local')
- if not get_by_jid(b.jid):
- bookmarks.append(b)
diff --git a/src/bookmarks.py b/src/bookmarks.py
new file mode 100644
index 00000000..c7d26a51
--- /dev/null
+++ b/src/bookmarks.py
@@ -0,0 +1,289 @@
+"""
+Bookmarks module
+
+Therein the bookmark class is defined, representing one conference room.
+This object is used to generate elements for both local and remote
+bookmark storage. It can also parse xml Elements.
+
+This module also defines several functions for retrieving and updating
+bookmarks, both local and remote.
+
+Poezio start scenario:
+
+- upon inital connection, poezio will disco#info the server
+- the available storage methods will be stored in the available_storage dict
+ (either 'pep' or 'privatexml')
+- if only one is available, poezio will set the use_bookmarks_method config option
+ to it. If both are, it will be set to 'privatexml' (or if it was previously set, the
+ value will be kept).
+- it will then query the preferred storages for bookmarks and cache them locally
+ (Bookmark objects with a method='remote' attribute)
+
+Adding a remote bookmark:
+
+- New Bookmark object added to the list with storage='remote'
+- All bookmarks are sent to the storage selected in use_bookmarks_method
+ if there was an error, the user is notified.
+
+
+"""
+
+import functools
+import logging
+
+from slixmpp.plugins.xep_0048 import Bookmarks, Conference, URL
+from slixmpp import JID
+from common import safeJID
+from config import config
+
+log = logging.getLogger(__name__)
+
+
+class Bookmark(object):
+
+ def __init__(self, jid, name=None, autojoin=False, nick=None, password=None, method='local'):
+ self.jid = jid
+ self.name = name or jid
+ self.autojoin = autojoin
+ self.nick = nick
+ self.password = password
+ self._method = method
+
+ @property
+ def method(self):
+ return self._method
+
+ @method.setter
+ def method(self, value):
+ if value not in ('local', 'remote'):
+ log.debug('Could not set bookmark storing method: %s', value)
+ return
+ self._method = value
+
+ def __repr__(self):
+ return '<%s%s|%s>' % (self.jid,
+ ('/'+self.nick) if self.nick else '',
+ self.method)
+
+ def stanza(self):
+ """
+ Generate a <conference/> stanza from the instance
+ """
+ el = Conference()
+ el['name'] = self.name
+ el['jid'] = self.jid
+ el['autojoin'] = 'true' if self.autojoin else 'false'
+ if self.nick:
+ el['nick'] = self.nick
+ if self.password:
+ el['password'] = self.password
+ return el
+
+ def local(self):
+ """Generate a str for local storage"""
+ local = self.jid
+ if self.nick:
+ local += '/%s' % self.nick
+ local += ':'
+ if self.password:
+ config.set_and_save('password', self.password, section=self.jid)
+ return local
+
+ @functools.singledispatch
+ @staticmethod
+ def parse(el):
+ """
+ Generate a Bookmark object from a <conference/> element
+ (this is a fallback for raw XML Elements)
+ """
+ jid = el.get('jid')
+ name = el.get('name')
+ autojoin = True if el.get('autojoin', 'false').lower() in ('true', '1') else False
+ nick = None
+ for n in el.iter('nick'):
+ nick = n.text
+ password = None
+ for p in el.iter('password'):
+ password = p.text
+
+ return Bookmark(jid, name, autojoin, nick, password, method='remote')
+
+ @staticmethod
+ @parse.register(Conference)
+ def parse_from_stanza(el):
+ """
+ Parse a Conference element into a Bookmark object
+ """
+ jid = el['jid']
+ autojoin = el['autojoin']
+ password = el['password']
+ nick = el['nick']
+ name = el['name']
+ return Bookmark(jid, name, autojoin, nick, password, method='remote')
+
+class BookmarkList(object):
+
+ def __init__(self):
+ self.bookmarks = []
+ preferred = config.get('use_bookmarks_method').lower()
+ if preferred not in ('pep', 'privatexml'):
+ preferred = 'privatexml'
+ self.preferred = preferred
+ self.available_storage = {
+ 'privatexml': False,
+ 'pep': False,
+ }
+
+ def __getitem__(self, key):
+ if isinstance(key, (str, JID)):
+ for i in self.bookmarks:
+ if key == i.jid:
+ return i
+ else:
+ return self.bookmarks[key]
+
+ def __in__(self, key):
+ if isinstance(key, (str, JID)):
+ for bookmark in self.bookmarks:
+ if bookmark.jid == key:
+ return True
+ else:
+ return key in self.bookmarks
+ return False
+
+ def remove(self, key):
+ if isinstance(key, (str, JID)):
+ for i in self.bookmarks[:]:
+ if i.jid == key:
+ self.bookmarks.remove(i)
+ else:
+ self.bookmarks.remove(key)
+
+ def __iter__(self):
+ return iter(self.bookmarks)
+
+ def local(self):
+ return [bm for bm in self.bookmarks if bm.method == 'local']
+
+ def remote(self):
+ return [bm for bm in self.bookmarks if bm.method == 'remote']
+
+ def set(self, new):
+ self.bookmarks = new
+
+ def append(self, bookmark):
+ bookmark_exists = self[bookmark.jid]
+ if not bookmark_exists:
+ self.bookmarks.append(bookmark)
+ else:
+ self.bookmarks.remove(bookmark_exists)
+ self.bookmarks.append(bookmark)
+
+ def set_bookmarks_method(self, value):
+ if self.available_storage.get(value):
+ self.preferred = value
+ config.set_and_save('use_bookmarks_method', value)
+
+ def save_remote(self, xmpp, callback):
+ """Save the remote bookmarks."""
+ if not any(self.available_storage.values()):
+ return
+ method = 'xep_0049' if self.preferred == 'privatexml' else 'xep_0223'
+
+ if method:
+ xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage(self.bookmarks),
+ method=method,
+ callback=callback)
+ def save_local(self):
+ """Save the local bookmarks."""
+ local = ''.join(bookmark.local() for bookmark in self if bookmark.method == 'local')
+ config.set_and_save('rooms', local)
+
+ def save(self, xmpp, core=None, callback=None):
+ """Save all the bookmarks."""
+ self.save_local()
+ def _cb(iq):
+ if callback:
+ callback(iq)
+ if iq["type"] == "error" and core:
+ core.information('Could not save remote bookmarks.', 'Error')
+ elif core:
+ core.information('Bookmarks saved', 'Info')
+ if config.get('use_remote_bookmarks'):
+ self.save_remote(xmpp, _cb)
+
+ def get_pep(self, xmpp, callback):
+ """Add the remotely stored bookmarks via pep to the list."""
+ def _cb(iq):
+ if iq['type'] == 'result':
+ for conf in iq['pubsub']['items']['item']['bookmarks']['conferences']:
+ if isinstance(conf, URL):
+ continue
+ b = Bookmark.parse(conf)
+ self.append(b)
+ if callback:
+ callback(iq)
+
+ xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0223', callback=_cb)
+
+ def get_privatexml(self, xmpp, callback):
+ """
+ Fetch the remote bookmarks stored via privatexml.
+ """
+ def _cb(iq):
+ if iq['type'] == 'result':
+ for conf in iq['private']['bookmarks']['conferences']:
+ b = Bookmark.parse(conf)
+ self.append(b)
+ if callback:
+ callback(iq)
+
+ xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0049', callback=_cb)
+
+ def get_remote(self, xmpp, information, callback):
+ """Add the remotely stored bookmarks to the list."""
+ force = config.get('force_remote_bookmarks')
+ if xmpp.anon or not (any(self.available_storage.values()) or force):
+ information('No remote bookmark storage available', 'Warning')
+ return
+
+ if force and not any(self.available_storage.values()):
+ old_callback = callback
+ method = 'pep' if self.preferred == 'pep' else 'privatexml'
+ def new_callback(result):
+ if result['type'] != 'error':
+ self.available_storage[method] = True
+ old_callback(result)
+ else:
+ information('No remote bookmark storage available', 'Warning')
+ callback = new_callback
+
+ if self.preferred == 'pep':
+ self.get_pep(xmpp, callback=callback)
+ else:
+ self.get_privatexml(xmpp, callback=callback)
+
+ def get_local(self):
+ """Add the locally stored bookmarks to the list."""
+ rooms = config.get('rooms')
+ if not rooms:
+ return
+ rooms = rooms.split(':')
+ for room in rooms:
+ jid = safeJID(room)
+ if jid.bare == '':
+ continue
+ if jid.resource != '':
+ nick = jid.resource
+ else:
+ nick = None
+ passwd = config.get_by_tabname('password', jid.bare, fallback=False) or None
+ b = Bookmark(jid.bare, autojoin=True, nick=nick, password=passwd, method='local')
+ self.append(b)
+
+def stanza_storage(bookmarks):
+ """Generate a <storage/> stanza with the conference elements."""
+ storage = Bookmarks()
+ for b in (b for b in bookmarks if b.method == 'remote'):
+ storage.append(b.stanza())
+ return storage
diff --git a/src/config.py b/src/config.py
index 1f0771ca..3ca53dd2 100644
--- a/src/config.py
+++ b/src/config.py
@@ -15,7 +15,7 @@ DEFSECTION = "Poezio"
import logging.config
import os
import sys
-from gettext import gettext as _
+import pkg_resources
from configparser import RawConfigParser, NoOptionError, NoSectionError
from os import environ, makedirs, path, remove
@@ -28,12 +28,13 @@ DEFAULT_CONFIG = {
'add_space_after_completion': True,
'after_completion': ',',
'alternative_nickname': '',
- 'auto_reconnect': False,
+ 'auto_reconnect': True,
'autorejoin_delay': '5',
'autorejoin': False,
'beep_on': 'highlight private invite',
'ca_cert_path': '',
'certificate': '',
+ 'certfile': '',
'ciphers': 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL',
'connection_check_interval': 60,
'connection_timeout_delay': 10,
@@ -41,6 +42,8 @@ DEFAULT_CONFIG = {
'custom_host': '',
'custom_port': '',
'default_nick': '',
+ 'deterministic_nick_colors': True,
+ 'nick_color_aliases': True,
'display_activity_notifications': False,
'display_gaming_notifications': False,
'display_mood_notifications': False,
@@ -58,6 +61,8 @@ DEFAULT_CONFIG = {
'extract_inline_images': True,
'filter_info_messages': '',
'force_encryption': True,
+ 'force_remote_bookmarks': False,
+ 'go_to_previous_tab_on_alt_number': False,
'group_corrections': True,
'hide_exit_join': -1,
'hide_status_change': 120,
@@ -67,11 +72,11 @@ DEFAULT_CONFIG = {
'ignore_private': False,
'information_buffer_popup_on': 'error roster warning help info',
'jid': '',
+ 'keyfile': '',
'lang': 'en',
'lazy_resize': True,
'load_log': 10,
'log_dir': '',
- 'logfile': 'logs',
'log_errors': True,
'max_lines_in_memory': 2048,
'max_messages_in_memory': 2048,
@@ -131,6 +136,8 @@ DEFAULT_CONFIG = {
'var': {
'folded_roster_groups': '',
'info_win_height': 2
+ },
+ 'muc_colors': {
}
}
@@ -152,9 +159,11 @@ class Config(RawConfigParser):
except TypeError: # python < 3.2 sucks
RawConfigParser.read(self, self.file_name)
# Check config integrity and fix it if it’s wrong
- for section in ('bindings', 'var'):
- if not self.has_section(section):
- self.add_section(section)
+ # only when the object is the main config
+ if self.__class__ is Config:
+ for section in ('bindings', 'var'):
+ if not self.has_section(section):
+ self.add_section(section)
def get(self, option, default=None, section=DEFSECTION):
"""
@@ -400,9 +409,9 @@ class Config(RawConfigParser):
elif current.lower() == "true":
value = "false"
else:
- return (_('Could not toggle option: %s.'
- ' Current value is %s.') %
- (option, current or _("empty")),
+ return ('Could not toggle option: %s.'
+ ' Current value is %s.' %
+ (option, current or "empty"),
'Warning')
if self.has_section(section):
RawConfigParser.set(self, section, option, value)
@@ -410,7 +419,7 @@ class Config(RawConfigParser):
self.add_section(section)
RawConfigParser.set(self, section, option, value)
if not self.write_in_file(section, option, value):
- return (_('Unable to write in the config file'), 'Error')
+ return ('Unable to write in the config file', 'Error')
return ("%s=%s" % (option, value), 'Info')
def remove_and_save(self, option, section=DEFSECTION):
@@ -420,8 +429,8 @@ class Config(RawConfigParser):
if self.has_section(section):
RawConfigParser.remove_option(self, section, option)
if not self.remove_in_file(section, option):
- return (_('Unable to save the config file'), 'Error')
- return (_('Option %s deleted') % option, 'Info')
+ return ('Unable to save the config file', 'Error')
+ return ('Option %s deleted' % option, 'Info')
def silent_set(self, option, value, section=DEFSECTION):
"""
@@ -511,6 +520,34 @@ def check_create_cache_dir():
except OSError:
pass
+def check_config():
+ """
+ Check the config file and print results
+ """
+ result = {'missing': [], 'changed': []}
+ for option in DEFAULT_CONFIG['Poezio']:
+ value = config.get(option)
+ if value != DEFAULT_CONFIG['Poezio'][option]:
+ result['changed'].append((option, value, DEFAULT_CONFIG['Poezio'][option]))
+ else:
+ value = config.get(option, default='')
+ upper = value.upper()
+ default = str(DEFAULT_CONFIG['Poezio'][option]).upper()
+ if upper != default:
+ result['missing'].append(option)
+
+ result['changed'].sort(key=lambda x: x[0])
+ result['missing'].sort()
+ if result['changed']:
+ print('\033[1mOptions changed from the default configuration:\033[0m\n')
+ for option, new_value, default in result['changed']:
+ print(' \033[1m%s\033[0m = \033[33m%s\033[0m (default: \033[32m%s\033[0m)' % (option, new_value, default))
+
+ if result['missing']:
+ print('\n\033[1mMissing options:\033[0m (the defaults are used)\n')
+ for option in result['missing']:
+ print(' \033[31m%s\033[0m' % option)
+
def run_cmdline_args(CONFIG_PATH):
"Parse the command line arguments"
global options
@@ -518,9 +555,8 @@ def run_cmdline_args(CONFIG_PATH):
# Copy a default file if none exists
if not path.isfile(options.filename):
- default = path.join(path.dirname(__file__),
- '../data/default_config.cfg')
- other = path.join(path.dirname(__file__), 'default_config.cfg')
+ default = path.join(path.dirname(__file__), '../data/default_config.cfg')
+ other = pkg_resources.resource_filename('poezio', 'default_config.cfg')
if path.isfile(default):
copy2(default, options.filename)
elif path.isfile(other):
@@ -552,7 +588,7 @@ def check_create_log_dir():
home = environ.get('HOME')
data_dir = path.join(home, '.local', 'share')
- LOG_DIR = path.join(data_dir, 'poezio')
+ LOG_DIR = path.join(data_dir, 'poezio', 'logs')
LOG_DIR = path.expanduser(LOG_DIR)
diff --git a/src/connection.py b/src/connection.py
index 1bbe632d..cd2ccedd 100644
--- a/src/connection.py
+++ b/src/connection.py
@@ -30,6 +30,10 @@ class Connection(slixmpp.ClientXMPP):
__init = False
def __init__(self):
resource = config.get('resource')
+
+ keyfile = config.get('keyfile')
+ certfile = config.get('certfile')
+
if config.get('jid'):
# Field used to know if we are anonymous or not.
# many features will be handled differently
@@ -38,7 +42,9 @@ class Connection(slixmpp.ClientXMPP):
jid = '%s' % config.get('jid')
if resource:
jid = '%s/%s'% (jid, resource)
- password = config.get('password') or getpass.getpass()
+ password = config.get('password')
+ if not password and not (keyfile and certfile):
+ password = getpass.getpass()
else: # anonymous auth
self.anon = True
jid = config.get('server')
@@ -57,6 +63,13 @@ class Connection(slixmpp.ClientXMPP):
self['feature_mechanisms'].unencrypted_cram = False
self['feature_mechanisms'].unencrypted_scram = False
+ self.keyfile = config.get('keyfile')
+ self.certfile = config.get('certfile')
+ if keyfile and not certfile:
+ log.error('keyfile is present in configuration file without certfile')
+ elif certfile and not keyfile:
+ log.error('certfile is present in configuration file without keyfile')
+
self.core = None
self.auto_reconnect = config.get('auto_reconnect')
self.reconnect_max_attempts = 0
@@ -127,6 +140,7 @@ class Connection(slixmpp.ClientXMPP):
self.register_plugin('xep_0202')
self.register_plugin('xep_0224')
self.register_plugin('xep_0249')
+ self.register_plugin('xep_0257')
self.register_plugin('xep_0280')
self.register_plugin('xep_0297')
self.register_plugin('xep_0308')
diff --git a/src/core/commands.py b/src/core/commands.py
index 4a8f7f19..3830d72a 100644
--- a/src/core/commands.py
+++ b/src/core/commands.py
@@ -6,37 +6,35 @@ import logging
log = logging.getLogger(__name__)
-import functools
import os
-import sys
from datetime import datetime
-from gettext import gettext as _
from xml.etree import cElementTree as ET
from slixmpp.xmlstream.stanzabase import StanzaBase
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
-import bookmark
import common
import fixes
import pep
import tabs
+from bookmarks import Bookmark
from common import safeJID
-from config import config, options as config_opts
+from config import config, DEFAULT_CONFIG, options as config_opts
import multiuserchat as muc
from plugin import PluginConfig
from roster import roster
from theming import dump_tuple, get_theme
+from decorators import command_args_parser
from . structs import Command, possible_show
-def command_help(self, arg):
+@command_args_parser.quoted(0, 1)
+def command_help(self, args):
"""
- /help <command_name>
+ /help [command_name]
"""
- args = arg.split()
if not args:
color = dump_tuple(get_theme().COLOR_HELP_COMMANDS)
acc = []
@@ -66,8 +64,8 @@ def command_help(self, arg):
buff.extend(acc)
msg = '\n'.join(buff)
- msg += _("\nType /help <command_name> to know what each command does")
- if args:
+ msg += "\nType /help <command_name> to know what each command does"
+ else:
command = args[0].lstrip('/').strip()
if command in self.current_tab().commands:
@@ -75,16 +73,17 @@ def command_help(self, arg):
elif command in self.commands:
tup = self.commands[command]
else:
- self.information(_('Unknown command: %s') % command, 'Error')
+ self.information('Unknown command: %s' % command, 'Error')
return
if isinstance(tup, Command):
- msg = _('Usage: /%s %s\n' % (command, tup.usage))
+ msg = 'Usage: /%s %s\n' % (command, tup.usage)
msg += tup.desc
else:
msg = tup[1]
self.information(msg, 'Help')
-def command_runkey(self, arg):
+@command_args_parser.quoted(1)
+def command_runkey(self, args):
"""
/runkey <key>
"""
@@ -93,7 +92,9 @@ def command_runkey(self, arg):
if key == '^J':
return '\n'
return key
- char = arg.strip()
+ if args is None:
+ return self.command_help('runkey')
+ char = args[0]
func = self.key_func.get(char, None)
if func:
func()
@@ -102,21 +103,20 @@ def command_runkey(self, arg):
if res:
self.refresh_window()
-def command_status(self, arg):
+@command_args_parser.quoted(1, 1, [None])
+def command_status(self, args):
"""
/status <status> [msg]
"""
- args = common.shell_split(arg)
- if len(args) < 1:
- return
+ if args is None:
+ return self.command_help('status')
+
if not args[0] in possible_show.keys():
- self.command_help('status')
- return
+ return self.command_help('status')
+
show = possible_show[args[0]]
- if len(args) == 2:
- msg = args[1]
- else:
- msg = None
+ msg = args[1]
+
pres = self.xmpp.make_presence()
if msg:
pres['status'] = msg
@@ -136,19 +136,15 @@ def command_status(self, arg):
if is_muctab and current.joined and show not in ('away', 'xa'):
current.send_chat_state('active')
-def command_presence(self, arg):
+@command_args_parser.quoted(1, 2, [None, None])
+def command_presence(self, args):
"""
/presence <JID> [type] [status]
"""
- args = common.shell_split(arg)
- if len(args) == 1:
- jid, type, status = args[0], None, None
- elif len(args) == 2:
- jid, type, status = args[0], args[1], None
- elif len(args) == 3:
- jid, type, status = args[0], args[1], args[2]
- else:
- return
+ if args is None:
+ return self.command_help('presence')
+
+ jid, type, status = args[0], args[1], args[2]
if jid == '.' and isinstance(self.current_tab(), tabs.ChatTab):
jid = self.current_tab().name
if type == 'available':
@@ -158,7 +154,7 @@ def command_presence(self, arg):
self.events.trigger('send_normal_presence', pres)
pres.send()
except:
- self.information(_('Could not send directed presence'), 'Error')
+ self.information('Could not send directed presence', 'Error')
log.debug('Could not send directed presence to %s', jid, exc_info=True)
return
tab = self.get_tab_by_name(jid)
@@ -177,24 +173,26 @@ def command_presence(self, arg):
if self.current_tab() in tab.privates:
self.current_tab().send_chat_state(chatstate, True)
-def command_theme(self, arg=''):
+@command_args_parser.quoted(1)
+def command_theme(self, args=None):
"""/theme <theme name>"""
- args = arg.split()
- if args:
- self.command_set('theme %s' % (args[0],))
+ if args is None:
+ return self.command_help('theme')
+ self.command_set('theme %s' % (args[0],))
-def command_win(self, arg):
+@command_args_parser.quoted(1)
+def command_win(self, args):
"""
/win <number>
"""
- arg = arg.strip()
- if not arg:
- self.command_help('win')
- return
+ if args is None:
+ return self.command_help('win')
+
+ nb = args[0]
try:
- nb = int(arg.split()[0])
+ nb = int(nb)
except ValueError:
- nb = arg
+ pass
if self.current_tab_nb == nb:
return
self.previous_tab_nb = self.current_tab_nb
@@ -219,15 +217,15 @@ def command_win(self, arg):
self.current_tab().on_gain_focus()
self.refresh_window()
-def command_move_tab(self, arg):
+@command_args_parser.quoted(2)
+def command_move_tab(self, args):
"""
/move_tab old_pos new_pos
"""
- args = common.shell_split(arg)
- current_tab = self.current_tab()
- if len(args) != 2:
+ if args is None:
return self.command_help('move_tab')
+ current_tab = self.current_tab()
if args[0] == '.':
args[0] = current_tab.nb
if args[1] == '.':
@@ -259,16 +257,16 @@ def command_move_tab(self, arg):
self.current_tab_nb = self.tabs.index(current_tab)
self.refresh_window()
-def command_list(self, arg):
+@command_args_parser.quoted(0, 1)
+def command_list(self, args):
"""
- /list <server>
+ /list [server]
Opens a MucListTab containing the list of the room in the specified server
"""
- arg = arg.split()
- if len(arg) > 1:
+ if args is None:
return self.command_help('list')
- elif arg:
- server = safeJID(arg[0]).server
+ elif args:
+ server = safeJID(args[0])
else:
if not isinstance(self.current_tab(), tabs.MucTab):
return self.information('Please provide a server', 'Error')
@@ -279,26 +277,27 @@ def command_list(self, arg):
self.xmpp.plugin['xep_0030'].get_items(jid=server,
callback=cb)
-def command_version(self, arg):
+@command_args_parser.quoted(1)
+def command_version(self, args):
"""
/version <jid>
"""
def callback(res):
"Callback for /version"
if not res:
- return self.information(_('Could not get the software'
- ' version from %s') % jid,
- _('Warning'))
- version = _('%s is running %s version %s on %s') % (
+ return self.information('Could not get the software'
+ ' version from %s' % jid,
+ 'Warning')
+ version = '%s is running %s version %s on %s' % (
jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
+ res.get('name') or 'an unknown software',
+ res.get('version') or 'unknown',
+ res.get('os') or 'an unknown platform')
self.information(version, 'Info')
- args = common.shell_split(arg)
- if len(args) < 1:
+ if args is None:
return self.command_help('version')
+
jid = safeJID(args[0])
if jid.resource or jid not in roster:
fixes.get_version(self.xmpp, jid, callback=callback)
@@ -308,11 +307,11 @@ def command_version(self, arg):
else:
fixes.get_version(self.xmpp, jid, callback=callback)
-def command_join(self, arg, histo_length=None):
+@command_args_parser.quoted(0, 2)
+def command_join(self, args, histo_length=None):
"""
/join [room][/nick] [password]
"""
- args = common.shell_split(arg)
password = None
if len(args) == 0:
tab = self.current_tab()
@@ -388,13 +387,15 @@ def command_join(self, arg, histo_length=None):
seconds = int(seconds)
else:
seconds = 0
+ if password:
+ tab.password = password
muc.join_groupchat(self, room, nick, password,
histo_length,
current_status.message,
current_status.show,
seconds=seconds)
if not tab:
- self.open_new_room(room, nick)
+ self.open_new_room(room, nick, password=password)
muc.join_groupchat(self, room, nick, password,
histo_length,
current_status.message,
@@ -409,196 +410,162 @@ def command_join(self, arg, histo_length=None):
tab.refresh()
self.doupdate()
-def command_bookmark_local(self, arg=''):
+@command_args_parser.quoted(0, 2)
+def command_bookmark_local(self, args):
"""
/bookmark_local [room][/nick] [password]
"""
- args = common.shell_split(arg)
- nick = None
- password = None
if not args and not isinstance(self.current_tab(), tabs.MucTab):
return
- if not args:
- tab = self.current_tab()
- roomname = tab.name
- if tab.joined and tab.own_nick != self.own_nick:
- nick = tab.own_nick
- elif args[0] == '*':
- new_bookmarks = []
- for tab in self.get_tabs(tabs.MucTab):
- b = bookmark.get_by_jid(tab.name)
- if not b:
- b = bookmark.Bookmark(tab.name,
- autojoin=True,
- method="local")
- new_bookmarks.append(b)
- else:
- b.method = "local"
- new_bookmarks.append(b)
- bookmark.bookmarks.remove(b)
- new_bookmarks.extend(bookmark.bookmarks)
- bookmark.bookmarks = new_bookmarks
- bookmark.save_local()
- bookmark.save_remote(self.xmpp, None)
- self.information('Bookmarks added and saved.', 'Info')
- return
- else:
- info = safeJID(args[0])
- if info.resource != '':
- nick = info.resource
- roomname = info.bare
- if not roomname:
- if not isinstance(self.current_tab(), tabs.MucTab):
- return
- roomname = self.current_tab().name
- if len(args) > 1:
- password = args[1]
-
- bm = bookmark.get_by_jid(roomname)
- if not bm:
- bm = bookmark.Bookmark(jid=roomname)
- bookmark.bookmarks.append(bm)
- self.information('Bookmark added.', 'Info')
- else:
- self.information('Bookmark updated.', 'Info')
- if nick:
- bm.nick = nick
- bm.autojoin = True
- bm.password = password
- bm.method = "local"
- bookmark.save_local()
- self.information(_('Your local bookmarks are now: %s') %
- [b for b in bookmark.bookmarks if b.method == 'local'], 'Info')
+ password = args[1] if len(args) > 1 else None
+ jid = args[0] if args else None
+
+ _add_bookmark(self, jid, True, password, 'local')
-def command_bookmark(self, arg=''):
+@command_args_parser.quoted(0, 3)
+def command_bookmark(self, args):
"""
/bookmark [room][/nick] [autojoin] [password]
"""
+ if not args and not isinstance(self.current_tab(), tabs.MucTab):
+ return
+ jid = args[0] if args else ''
+ password = args[2] if len(args) > 2 else None
if not config.get('use_remote_bookmarks'):
- self.command_bookmark_local(arg)
- return
- args = common.shell_split(arg)
+ return _add_bookmark(self, jid, True, password, 'local')
+
+ if len(args) > 1:
+ autojoin = False if args[1].lower() != 'true' else True
+ else:
+ autojoin = True
+
+ _add_bookmark(self, jid, autojoin, password, 'remote')
+
+def _add_bookmark(self, jid, autojoin, password, method):
nick = None
- if not args and not isinstance(self.current_tab(), tabs.MucTab):
- return
- if not args:
+ if not jid:
tab = self.current_tab()
roomname = tab.name
- if tab.joined:
+ if tab.joined and tab.own_nick != self.own_nick:
nick = tab.own_nick
- autojoin = True
- password = None
- elif args[0] == '*':
- if len(args) > 1:
- autojoin = False if args[1].lower() != 'true' else True
- else:
- autojoin = True
- new_bookmarks = []
- for tab in self.get_tabs(tabs.MucTab):
- b = bookmark.get_by_jid(tab.name)
- if not b:
- b = bookmark.Bookmark(tab.name, autojoin=autojoin,
- method=bookmark.preferred)
- new_bookmarks.append(b)
- else:
- b.method = bookmark.preferred
- bookmark.bookmarks.remove(b)
- new_bookmarks.append(b)
- new_bookmarks.extend(bookmark.bookmarks)
- bookmark.bookmarks = new_bookmarks
- def _cb(self, iq):
- if iq["type"] != "error":
- bookmark.save_local()
- self.information("Bookmarks added.", "Info")
- else:
- self.information("Could not add the bookmarks.", "Info")
- bookmark.save_remote(self.xmpp, functools.partial(_cb, self))
- return
+ if password is None and tab.password is not None:
+ password = tab.password
+ elif jid == '*':
+ return _add_wildcard_bookmarks(self, method)
else:
- info = safeJID(args[0])
- if info.resource != '':
- nick = info.resource
- roomname = info.bare
+ info = safeJID(jid)
+ roomname, nick = info.bare, info.resource
if roomname == '':
if not isinstance(self.current_tab(), tabs.MucTab):
return
roomname = self.current_tab().name
- if len(args) > 1:
- autojoin = False if args[1].lower() != 'true' else True
- else:
- autojoin = True
- if len(args) > 2:
- password = args[2]
- else:
- password = None
- bm = bookmark.get_by_jid(roomname)
- if not bm:
- bm = bookmark.Bookmark(roomname)
- bookmark.bookmarks.append(bm)
- bm.method = config.get('use_bookmarks_method')
+ bookmark = self.bookmarks[roomname]
+ if bookmark is None:
+ bookmark = Bookmark(roomname)
+ self.bookmarks.append(bookmark)
+ bookmark.method = method
+ bookmark.autojoin = autojoin
if nick:
- bm.nick = nick
+ bookmark.nick = nick
if password:
- bm.password = password
- bm.autojoin = autojoin
- def _cb(self, iq):
+ bookmark.password = password
+ def callback(iq):
if iq["type"] != "error":
self.information('Bookmark added.', 'Info')
else:
self.information("Could not add the bookmarks.", "Info")
- bookmark.save_remote(self.xmpp, functools.partial(_cb, self))
- remote = []
- for each in bookmark.bookmarks:
- if each.method in ('pep', 'privatexml'):
- remote.append(each)
+ self.bookmarks.save_local()
+ self.bookmarks.save_remote(self.xmpp, callback)
+
+def _add_wildcard_bookmarks(self, method):
+ new_bookmarks = []
+ for tab in self.get_tabs(tabs.MucTab):
+ bookmark = self.bookmarks[tab.name]
+ if not bookmark:
+ bookmark = Bookmark(tab.name, autojoin=True,
+ method=method)
+ new_bookmarks.append(bookmark)
+ else:
+ bookmark.method = method
+ new_bookmarks.append(bookmark)
+ self.bookmarks.remove(bookmark)
+ new_bookmarks.extend(self.bookmarks.bookmarks)
+ self.bookmarks.set(new_bookmarks)
+ def _cb(iq):
+ if iq["type"] != "error":
+ self.information("Bookmarks saved.", "Info")
+ else:
+ self.information("Could not save the remote bookmarks.", "Info")
+ self.bookmarks.save_local()
+ self.bookmarks.save_remote(self.xmpp, _cb)
-def command_bookmarks(self, arg=''):
+@command_args_parser.ignored
+def command_bookmarks(self):
"""/bookmarks"""
- local = []
- remote = []
- for each in bookmark.bookmarks:
- if each.method in ('pep', 'privatexml'):
- remote.append(each)
- elif each.method == 'local':
- local.append(each)
-
- self.information(_('Your remote bookmarks are: %s') % remote,
- _('Info'))
- self.information(_('Your local bookmarks are: %s') % local,
- _('Info'))
-
-def command_remove_bookmark(self, arg=''):
+ tab = self.get_tab_by_name('Bookmarks', tabs.BookmarksTab)
+ old_tab = self.current_tab()
+ if tab:
+ self.current_tab_nb = tab.nb
+ else:
+ tab = tabs.BookmarksTab(self.bookmarks)
+ self.tabs.append(tab)
+ self.current_tab_nb = tab.nb
+ old_tab.on_lose_focus()
+ tab.on_gain_focus()
+ self.refresh_window()
+
+@command_args_parser.quoted(0, 1)
+def command_remove_bookmark(self, args):
"""/remove_bookmark [jid]"""
- args = common.shell_split(arg)
+
+ def cb(success):
+ if success:
+ self.information('Bookmark deleted', 'Info')
+ else:
+ self.information('Error while deleting the bookmark', 'Error')
+
if not args:
tab = self.current_tab()
- if isinstance(tab, tabs.MucTab) and bookmark.get_by_jid(tab.name):
- bookmark.remove(tab.name)
- bookmark.save(self.xmpp)
- if bookmark.save(self.xmpp):
- self.information('Bookmark deleted', 'Info')
+ if isinstance(tab, tabs.MucTab) and self.bookmarks[tab.name]:
+ self.bookmarks.remove(tab.name)
+ self.bookmarks.save(self.xmpp, callback=cb)
else:
self.information('No bookmark to remove', 'Info')
else:
- if bookmark.get_by_jid(args[0]):
- bookmark.remove(args[0])
- if bookmark.save(self.xmpp):
- self.information('Bookmark deleted', 'Info')
-
+ if self.bookmarks[args[0]]:
+ self.bookmarks.remove(args[0])
+ self.bookmarks.save(self.xmpp, callback=cb)
else:
self.information('No bookmark to remove', 'Info')
-def command_set(self, arg):
+@command_args_parser.quoted(0, 3)
+def command_set(self, args):
"""
/set [module|][section] <option> [value]
"""
- args = common.shell_split(arg)
- if len(args) == 1:
+ if args is None or len(args) == 0:
+ config_dict = config.to_dict()
+ lines = []
+ theme = get_theme()
+ for section_name, section in config_dict.items():
+ lines.append('\x19%(section_col)s}[%(section)s]\x19o' %
+ {
+ 'section': section_name,
+ 'section_col': dump_tuple(theme.COLOR_INFORMATION_TEXT),
+ })
+ for option_name, option_value in section.items():
+ lines.append('%s\x19%s}=\x19o%s' % (option_name,
+ dump_tuple(theme.COLOR_REVISIONS_MESSAGE),
+ option_value))
+ info = ('Current options:\n%s' % '\n'.join(lines), 'Info')
+ elif len(args) == 1:
option = args[0]
value = config.get(option)
+ if value is None and '=' in option:
+ args = option.split('=', 1)
info = ('%s=%s' % (option, value), 'Info')
- elif len(args) == 2:
+ if len(args) == 2:
if '|' in args[0]:
plugin_name, section = args[0].split('|')[:2]
if not section:
@@ -639,44 +606,72 @@ def command_set(self, arg):
plugin_config = self.plugin_manager.plugins[plugin_name].config
info = plugin_config.set_and_save(option, value, section)
else:
- section = args[0]
+ if args[0] == '.':
+ name = safeJID(self.current_tab().name).bare
+ if not name:
+ self.information('Invalid tab to use the "." argument.',
+ 'Error')
+ return
+ section = name
+ else:
+ section = args[0]
option = args[1]
value = args[2]
info = config.set_and_save(option, value, section)
self.trigger_configuration_change(option, value)
- else:
- self.command_help('set')
- return
- self.call_for_resize()
+ elif len(args) > 3:
+ return self.command_help('set')
self.information(*info)
-def command_toggle(self, arg):
+@command_args_parser.quoted(1, 2)
+def command_set_default(self, args):
+ """
+ /set_default [section] <option>
+ """
+ if len(args) == 1:
+ option = args[0]
+ section = 'Poezio'
+ elif len(args) == 2:
+ section = args[0]
+ option = args[1]
+ else:
+ return self.command_help('set_default')
+
+ default_config = DEFAULT_CONFIG.get(section, tuple())
+ if option not in default_config:
+ info = ("Option %s has no default value" % (option), "Error")
+ return self.information(*info)
+ self.command_set('%s %s %s' % (section, option, default_config[option]))
+
+@command_args_parser.quoted(1)
+def command_toggle(self, args):
"""
/toggle <option>
shortcut for /set <option> toggle
"""
- arg = arg.split()
- if arg and arg[0]:
- self.command_set('%s toggle' % arg[0])
+ if args is None:
+ return self.command_help('toggle')
-def command_server_cycle(self, arg=''):
+ if args[0]:
+ self.command_set('%s toggle' % args[0])
+
+@command_args_parser.quoted(1, 1)
+def command_server_cycle(self, args):
"""
Do a /cycle on each room of the given server.
If none, do it on the current tab
"""
- args = common.shell_split(arg)
tab = self.current_tab()
message = ""
- if len(args):
+ if args:
domain = args[0]
- if len(args) > 1:
+ if len(args) == 2:
message = args[1]
else:
if isinstance(tab, tabs.MucTab):
domain = safeJID(tab.name).domain
else:
- self.information(_("No server specified"), "Error")
- return
+ return self.information("No server specified", "Error")
for tab in self.get_tabs(tabs.MucTab):
if tab.name.endswith(domain):
if tab.joined:
@@ -690,7 +685,8 @@ def command_server_cycle(self, arg=''):
else:
self.command_join('"%s/%s"' %(tab.name, tab.own_nick))
-def command_last_activity(self, arg):
+@command_args_parser.quoted(1)
+def command_last_activity(self, args):
"""
/last_activity <jid>
"""
@@ -698,11 +694,11 @@ def command_last_activity(self, arg):
"Callback for the last activity"
if iq['type'] != 'result':
if iq['error']['type'] == 'auth':
- self.information(_('You are not allowed to see the '
- 'activity of this contact.'),
- _('Error'))
+ self.information('You are not allowed to see the '
+ 'activity of this contact.',
+ 'Error')
else:
- self.information(_('Error retrieving the activity'), 'Error')
+ self.information('Error retrieving the activity', 'Error')
return
seconds = iq['last_activity']['seconds']
status = iq['last_activity']['status']
@@ -717,46 +713,47 @@ def command_last_activity(self, arg):
common.parse_secs_to_str(seconds),
(' and his/her last status was %s' % status) if status else '')
self.information(msg, 'Info')
- jid = safeJID(arg)
- if jid == '':
+
+ if args is None:
return self.command_help('last_activity')
+ jid = safeJID(args[0])
self.xmpp.plugin['xep_0012'].get_last_activity(jid,
callback=callback)
-def command_mood(self, arg):
+@command_args_parser.quoted(0, 2)
+def command_mood(self, args):
"""
/mood [<mood> [text]]
"""
- args = common.shell_split(arg)
if not args:
- self.xmpp.plugin['xep_0107'].stop()
- return
+ return self.xmpp.plugin['xep_0107'].stop()
+
mood = args[0]
if mood not in pep.MOODS:
- return self.information(_('%s is not a correct value for a mood.')
- % mood,
- _('Error'))
- if len(args) > 1:
+ return self.information('%s is not a correct value for a mood.'
+ % mood,
+ 'Error')
+ if len(args) == 2:
text = args[1]
else:
text = None
self.xmpp.plugin['xep_0107'].publish_mood(mood, text,
callback=dumb_callback)
-def command_activity(self, arg):
+@command_args_parser.quoted(0, 3)
+def command_activity(self, args):
"""
/activity [<general> [specific] [text]]
"""
- args = common.shell_split(arg)
length = len(args)
if not length:
- self.xmpp.plugin['xep_0108'].stop()
- return
+ return self.xmpp.plugin['xep_0108'].stop()
+
general = args[0]
if general not in pep.ACTIVITIES:
- return self.information(_('%s is not a correct value for an activity')
+ return self.information('%s is not a correct value for an activity'
% general,
- _('Error'))
+ 'Error')
specific = None
text = None
if length == 2:
@@ -768,20 +765,20 @@ def command_activity(self, arg):
specific = args[1]
text = args[2]
if specific and specific not in pep.ACTIVITIES[general]:
- return self.information(_('%s is not a correct value '
- 'for an activity') % specific,
- _('Error'))
+ return self.information('%s is not a correct value '
+ 'for an activity' % specific,
+ 'Error')
self.xmpp.plugin['xep_0108'].publish_activity(general, specific, text,
callback=dumb_callback)
-def command_gaming(self, arg):
+@command_args_parser.quoted(0, 2)
+def command_gaming(self, args):
"""
/gaming [<game name> [server address]]
"""
- args = common.shell_split(arg)
if not args:
- self.xmpp.plugin['xep_0196'].stop()
- return
+ return self.xmpp.plugin['xep_0196'].stop()
+
name = args[0]
if len(args) > 1:
address = args[1]
@@ -791,25 +788,27 @@ def command_gaming(self, arg):
server_address=address,
callback=dumb_callback)
-def command_invite(self, arg):
+@command_args_parser.quoted(2, 1, [None])
+def command_invite(self, args):
"""/invite <to> <room> [reason]"""
- args = common.shell_split(arg)
- if len(args) < 2:
- return
- reason = args[2] if len(args) > 2 else None
+
+ if args is None:
+ return self.command_help('invite')
+
+ reason = args[2]
to = safeJID(args[0])
room = safeJID(args[1]).bare
self.invite(to.full, room, reason=reason)
-def command_decline(self, arg):
+@command_args_parser.quoted(1, 1, [''])
+def command_decline(self, args):
"""/decline <room@server.tld> [reason]"""
- args = common.shell_split(arg)
- if not len(args):
- return
+ if args is None:
+ return self.command_help('decline')
jid = safeJID(args[0])
if jid.bare not in self.pending_invites:
return
- reason = args[1] if len(args) > 1 else ''
+ reason = args[1]
del self.pending_invites[jid.bare]
self.xmpp.plugin['xep_0045'].decline_invite(jid.bare,
self.pending_invites[jid.bare],
@@ -817,7 +816,8 @@ def command_decline(self, arg):
### Commands without a completion in this class ###
-def command_invitations(self, arg=''):
+@command_args_parser.ignored
+def command_invitations(self):
"""/invitations"""
build = ""
for invite in self.pending_invites:
@@ -829,17 +829,16 @@ def command_invitations(self, arg=''):
build = "You do not have any pending invitations."
self.information(build, 'Info')
-def command_quit(self, arg=''):
+@command_args_parser.quoted(0, 1, [None])
+def command_quit(self, args):
"""
- /quit
+ /quit [message]
"""
if not self.xmpp.is_connected():
self.exit()
return
- if len(arg.strip()) != 0:
- msg = arg
- else:
- msg = None
+
+ msg = args[0]
if config.get('enable_user_mood'):
self.xmpp.plugin['xep_0107'].stop()
if config.get('enable_user_activity'):
@@ -851,44 +850,47 @@ def command_quit(self, arg=''):
self.disconnect(msg)
self.xmpp.add_event_handler("disconnected", self.exit, disposable=True)
-def command_destroy_room(self, arg=''):
+@command_args_parser.quoted(0, 1, [''])
+def command_destroy_room(self, args):
"""
/destroy_room [JID]
"""
- room = safeJID(arg).bare
+ room = safeJID(args[0]).bare
if room:
muc.destroy_room(self.xmpp, room)
- elif isinstance(self.current_tab(), tabs.MucTab) and not arg:
+ elif isinstance(self.current_tab(), tabs.MucTab) and not args[0]:
muc.destroy_room(self.xmpp, self.current_tab().general_jid)
else:
- self.information(_('Invalid JID: "%s"') % arg, _('Error'))
+ self.information('Invalid JID: "%s"' % args[0], 'Error')
-def command_bind(self, arg):
+@command_args_parser.quoted(1, 1, [''])
+def command_bind(self, args):
"""
Bind a key.
"""
- args = common.shell_split(arg)
- if len(args) < 1:
+ if args is None:
return self.command_help('bind')
- elif len(args) < 2:
- args.append("")
+
if not config.silent_set(args[0], args[1], section='bindings'):
- self.information(_('Unable to write in the config file'), 'Error')
+ self.information('Unable to write in the config file', 'Error')
+
if args[1]:
self.information('%s is now bound to %s' % (args[0], args[1]), 'Info')
else:
self.information('%s is now unbound' % args[0], 'Info')
-def command_rawxml(self, arg):
+@command_args_parser.raw
+def command_rawxml(self, args):
"""
/rawxml <xml stanza>
"""
- if not arg:
- return
+ if not args:
+ return
+ stanza = args
try:
- stanza = StanzaBase(self.xmpp, xml=ET.fromstring(arg))
+ stanza = StanzaBase(self.xmpp, xml=ET.fromstring(stanza))
if stanza.xml.tag == 'iq' and \
stanza.xml.attrib.get('type') in ('get', 'set') and \
stanza.xml.attrib.get('id'):
@@ -910,78 +912,85 @@ def command_rawxml(self, arg):
stanza.send()
except:
- self.information(_('Could not send custom stanza'), 'Error')
+ self.information('Could not send custom stanza', 'Error')
log.debug('/rawxml: Could not send custom stanza (%s)',
- repr(arg),
+ repr(stanza),
exc_info=True)
-def command_load(self, arg):
+@command_args_parser.quoted(1, 256)
+def command_load(self, args):
"""
/load <plugin> [<otherplugin> …]
+ # TODO: being able to load more than 256 plugins at once, hihi.
"""
- args = arg.split()
for plugin in args:
self.plugin_manager.load(plugin)
-def command_unload(self, arg):
+@command_args_parser.quoted(1, 256)
+def command_unload(self, args):
"""
/unload <plugin> [<otherplugin> …]
"""
- args = arg.split()
for plugin in args:
self.plugin_manager.unload(plugin)
-def command_plugins(self, arg=''):
+@command_args_parser.ignored
+def command_plugins(self):
"""
/plugins
"""
- self.information(_("Plugins currently in use: %s") %
+ self.information("Plugins currently in use: %s" %
repr(list(self.plugin_manager.plugins.keys())),
- _('Info'))
+ 'Info')
-def command_message(self, arg):
+@command_args_parser.quoted(1, 1)
+def command_message(self, args):
"""
/message <jid> [message]
"""
- args = common.shell_split(arg)
- if len(args) < 1:
- self.command_help('message')
- return
+ if args is None:
+ return self.command_help('message')
jid = safeJID(args[0])
if not jid.user and not jid.domain and not jid.resource:
return self.information('Invalid JID.', 'Error')
tab = self.get_conversation_by_jid(jid.full, False, fallback_barejid=False)
- if not tab:
+ muc = self.get_tab_by_name(jid.bare, typ=tabs.MucTab)
+ if not tab and not muc:
tab = self.open_conversation_window(jid.full, focus=True)
+ elif muc:
+ tab = self.get_tab_by_name(jid.full, typ=tabs.PrivateTab)
+ if tab:
+ self.focus_tab_named(tab.name)
+ else:
+ tab = self.open_private_window(jid.bare, jid.resource)
else:
self.focus_tab_named(tab.name)
- if len(args) > 1:
+ if len(args) == 2:
tab.command_say(args[1])
-def command_xml_tab(self, arg=''):
+@command_args_parser.ignored
+def command_xml_tab(self):
"""/xml_tab"""
- self.xml_tab = True
xml_tab = self.focus_tab_named('XMLTab', tabs.XMLTab)
if not xml_tab:
tab = tabs.XMLTab()
self.add_tab(tab, True)
+ self.xml_tab = tab
-def command_adhoc(self, arg):
- arg = arg.split()
- if len(arg) > 1:
+@command_args_parser.quoted(1)
+def command_adhoc(self, args):
+ if not args:
return self.command_help('ad-hoc')
- elif arg:
- jid = safeJID(arg[0])
- else:
- return self.information('Please provide a jid', 'Error')
+ jid = safeJID(args[0])
list_tab = tabs.AdhocCommandsListTab(jid)
self.add_tab(list_tab, True)
cb = list_tab.on_list_received
self.xmpp.plugin['xep_0050'].get_commands(jid=jid, local=False,
callback=cb)
-def command_self(self, arg=None):
+@command_args_parser.ignored
+def command_self(self):
"""
/self
"""
@@ -998,5 +1007,14 @@ def command_self(self, arg=None):
config_opts.version))
self.information(info, 'Info')
+
+@command_args_parser.ignored
+def command_reload(self):
+ """
+ /reload
+ """
+ self.reload_config()
+
def dumb_callback(*args, **kwargs):
"mock callback"
+
diff --git a/src/core/completions.py b/src/core/completions.py
index 7d95321b..f17e916c 100644
--- a/src/core/completions.py
+++ b/src/core/completions.py
@@ -8,7 +8,6 @@ log = logging.getLogger(__name__)
import os
from functools import reduce
-import bookmark
import common
import pep
import tabs
@@ -57,7 +56,7 @@ def completion_theme(self, the_input):
except OSError as e:
log.error('Completion for /theme failed', exc_info=True)
return
- theme_files = [name[:-3] for name in names if name.endswith('.py')]
+ theme_files = [name[:-3] for name in names if name.endswith('.py') and name != '__init__.py']
if not 'default' in theme_files:
theme_files.append('default')
return the_input.new_completion(theme_files, 1, '', quotify=False)
@@ -96,7 +95,7 @@ def completion_join(self, the_input):
relevant_rooms = []
relevant_rooms.extend(sorted(self.pending_invites.keys()))
- bookmarks = {str(elem.jid): False for elem in bookmark.bookmarks}
+ bookmarks = {str(elem.jid): False for elem in self.bookmarks}
for tab in self.get_tabs(tabs.MucTab):
name = tab.name
if name in bookmarks and not tab.joined:
@@ -119,7 +118,6 @@ def completion_join(self, the_input):
return the_input.new_completion(['/%s' % self.own_nick], 1, quotify=True)
else:
return the_input.new_completion(relevant_rooms, 1, quotify=True)
- return True
def completion_version(self, the_input):
@@ -192,7 +190,7 @@ def completion_bookmark(self, the_input):
def completion_remove_bookmark(self, the_input):
"""Completion for /remove_bookmark"""
- return the_input.new_completion([bm.jid for bm in bookmark.bookmarks], 1, quotify=False)
+ return the_input.new_completion([bm.jid for bm in self.bookmarks], 1, quotify=False)
def completion_decline(self, the_input):
@@ -214,9 +212,6 @@ def completion_bind(self, the_input):
return the_input.new_completion(args, n, '', quotify=False)
- return the_input
-
-
def completion_message(self, the_input):
"""Completion for /message"""
n = the_input.get_argument_position(quoted=True)
@@ -304,14 +299,21 @@ def completion_set(self, the_input):
plugin = self.plugin_manager.plugins[plugin_name]
end_list = ['%s|%s' % (plugin_name, section) for section in plugin.config.sections()]
else:
- end_list = config.options('Poezio')
+ end_list = set(config.options('Poezio'))
+ end_list.update(config.default.get('Poezio', {}))
+ end_list = list(end_list)
+ end_list.sort()
elif n == 2:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if not plugin_name in self.plugin_manager.plugins:
return the_input.new_completion([''], n, quotify=True)
plugin = self.plugin_manager.plugins[plugin_name]
- end_list = plugin.config.options(section or plugin_name)
+ end_list = set(plugin.config.options(section or plugin_name))
+ if plugin.config.default:
+ end_list.update(plugin.config.default.get(section or plugin_name, {}))
+ end_list = list(end_list)
+ end_list.sort()
elif not config.has_option('Poezio', args[1]):
if config.has_section(args[1]):
end_list = config.options(args[1])
@@ -336,6 +338,19 @@ def completion_set(self, the_input):
return
return the_input.new_completion(end_list, n, quotify=True)
+
+def completion_set_default(self, the_input):
+ """ Completion for /set_default
+ """
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position(quoted=True)
+ if n >= len(args):
+ args.append('')
+ if n == 1 or (n == 2 and config.has_section(args[1])):
+ return self.completion_set(the_input)
+ return []
+
+
def completion_toggle(self, the_input):
"Completion for /toggle"
return the_input.new_completion(config.options('Poezio'), 1, quotify=False)
diff --git a/src/core/core.py b/src/core/core.py
index 4daeed6c..92c9f987 100644
--- a/src/core/core.py
+++ b/src/core/core.py
@@ -10,7 +10,6 @@ import logging
log = logging.getLogger(__name__)
import asyncio
-import collections
import shutil
import curses
import os
@@ -18,22 +17,19 @@ import pipes
import sys
import time
from threading import Event
-from datetime import datetime
-from gettext import gettext as _
from slixmpp.xmlstream.handler import Callback
-import bookmark
import connection
import decorators
import events
-import fixes
import singleton
import tabs
import theming
import timed_events
import windows
+from bookmarks import BookmarkList
from common import safeJID
from config import config, firstrun
from contact import Contact, Resource
@@ -75,6 +71,7 @@ class Core(object):
self.keyboard = keyboard.Keyboard()
roster.set_node(self.xmpp.client_roster)
decorators.refresh_wrapper.core = self
+ self.bookmarks = BookmarkList()
self.paused = False
self.event = Event()
self.debug = False
@@ -90,7 +87,7 @@ class Core(object):
self.tab_win = windows.GlobalInfoBar()
# Whether the XML tab is opened
- self.xml_tab = False
+ self.xml_tab = None
self.xml_buffer = TextBuffer()
self.tabs = []
@@ -226,6 +223,7 @@ class Core(object):
self.xmpp.add_event_handler("groupchat_subject",
self.on_groupchat_subject)
self.xmpp.add_event_handler("message", self.on_message)
+ self.xmpp.add_event_handler("message_error", self.on_error_message)
self.xmpp.add_event_handler("receipt_received", self.on_receipt)
self.xmpp.add_event_handler("got_online", self.on_got_online)
self.xmpp.add_event_handler("got_offline", self.on_got_offline)
@@ -253,6 +251,7 @@ class Core(object):
self.on_chatstate_inactive)
self.xmpp.add_event_handler("attention", self.on_attention)
self.xmpp.add_event_handler("ssl_cert", self.validate_ssl)
+ self.xmpp.add_event_handler("ssl_invalid_chain", self.ssl_invalid_chain)
self.all_stanzas = Callback('custom matcher',
connection.MatchAll(None),
self.incoming_stanza)
@@ -310,8 +309,14 @@ class Core(object):
theming.update_themes_dir)
self.add_configuration_handler("theme",
self.on_theme_config_change)
+ self.add_configuration_handler("use_bookmarks_method",
+ self.on_bookmarks_method_config_change)
self.add_configuration_handler("password",
self.on_password_change)
+ self.add_configuration_handler("enable_vertical_tab_list",
+ self.on_vertical_tab_list_config_change)
+ self.add_configuration_handler("deterministic_nick_colors",
+ self.on_nick_determinism_changed)
self.add_configuration_handler("", self.on_any_config_change)
@@ -346,6 +351,15 @@ class Core(object):
for callback in self.configuration_change_handlers[option]:
callback(option, value)
+ def on_bookmarks_method_config_change(self, option, value):
+ """
+ Called when the use_bookmarks_method option changes
+ """
+ if value not in ('pep', 'privatexml'):
+ return
+ self.bookmarks.preferred = value
+ self.bookmarks.save(self.xmpp, core=self)
+
def on_gaps_config_change(self, option, value):
"""
Called when the option create_gaps is changed.
@@ -374,6 +388,12 @@ class Core(object):
path = os.path.expanduser(value)
self.plugin_manager.on_plugins_dir_change(path)
+ def on_vertical_tab_list_config_change(self, option, value):
+ """
+ Called when the enable_vertical_tab_list option is changed
+ """
+ self.call_for_resize()
+
def on_plugins_conf_dir_config_change(self, option, value):
"""
Called when the plugins_conf_dir option is changed
@@ -396,19 +416,23 @@ class Core(object):
"""
self.xmpp.password = value
- def sigusr_handler(self, num, stack):
- """
- Handle SIGUSR1 (10)
- When caught, reload all the possible files.
+
+ def on_nick_determinism_changed(self, option, value):
+ """If we change the value to true, we call /recolor on all the MucTabs, to
+ make the current nick colors reflect their deterministic value.
"""
- log.debug("SIGUSR1 caught, reloading the files…")
+ if value.lower() == "true":
+ for tab in self.get_tabs(tabs.MucTab):
+ tab.command_recolor('')
+
+ def reload_config(self):
# reload all log files
log.debug("Reloading the log files…")
logger.reload_all()
log.debug("Log files reloaded.")
# reload the theme
log.debug("Reloading the theme…")
- self.command_theme("")
+ theming.reload_theme()
log.debug("Theme reloaded.")
# reload the config from the disk
log.debug("Reloading the config…")
@@ -428,6 +452,14 @@ class Core(object):
# in case some roster options have changed
roster.modified()
+ def sigusr_handler(self, num, stack):
+ """
+ Handle SIGUSR1 (10)
+ When caught, reload all the possible files.
+ """
+ log.debug("SIGUSR1 caught, reloading the files…")
+ self.reload_config()
+
def exit_from_signal(self, *args, **kwargs):
"""
Quit when receiving SIGHUP or SIGTERM or SIGPIPE
@@ -476,15 +508,15 @@ class Core(object):
default_tab = tabs.RosterInfoTab()
default_tab.on_gain_focus()
self.tabs.append(default_tab)
- self.information(_('Welcome to poezio!'), _('Info'))
+ self.information('Welcome to poezio!', 'Info')
if firstrun:
- self.information(_(
+ self.information(
'It seems that it is the first time you start poezio.\n'
'The online help is here http://doc.poez.io/\n'
'No room is joined by default, but you can join poezio’s'
'chatroom (with /join poezio@muc.poez.io), where you can'
- ' ask for help or tell us how great it is.'),
- _('Help'))
+ ' ask for help or tell us how great it is.',
+ 'Help')
self.refresh_window()
self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid)
@@ -592,7 +624,7 @@ class Core(object):
except ValueError:
pass
else:
- if self.current_tab().nb == nb:
+ if self.current_tab().nb == nb and config.get('go_to_previous_tab_on_alt_number'):
self.go_to_previous_tab()
else:
self.command_win('%d' % nb)
@@ -617,9 +649,9 @@ class Core(object):
self.information_win_size,
'var')
if not ok:
- self.information(_('Unable to save runtime preferences'
- ' in the config file'),
- _('Error'))
+ self.information('Unable to save runtime preferences'
+ ' in the config file',
+ 'Error')
def on_roster_enter_key(self, roster_row):
"""
@@ -675,8 +707,8 @@ class Core(object):
func(arg)
return
else:
- self.information(_("Unknown command (%s)") % (command),
- _('Error'))
+ self.information("Unknown command (%s)" % (command),
+ 'Error')
def exec_command(self, command):
"""
@@ -809,15 +841,15 @@ class Core(object):
msg = msg.replace('\n', '|') if msg else ''
ok = ok and config.silent_set('status_message', msg)
if not ok:
- self.information(_('Unable to save the status in '
- 'the config file'), 'Error')
+ self.information('Unable to save the status in '
+ 'the config file', 'Error')
def get_bookmark_nickname(self, room_name):
"""
Returns the nickname associated with a bookmark
or the default nickname
"""
- bm = bookmark.get_by_jid(room_name)
+ bm = self.bookmarks[room_name]
if bm:
return bm.nick
return self.own_nick
@@ -884,18 +916,18 @@ class Core(object):
if code in DEPRECATED_ERRORS:
body = DEPRECATED_ERRORS[code]
else:
- body = condition or _('Unknown error')
+ body = condition or 'Unknown error'
else:
if code in ERROR_AND_STATUS_CODES:
body = ERROR_AND_STATUS_CODES[code]
else:
- body = condition or _('Unknown error')
+ body = condition or 'Unknown error'
if code:
- message = _('%(from)s: %(code)s - %(msg)s: %(body)s') % {
- 'from': sender, 'msg': msg, 'body': body, 'code': code}
+ message = '%(from)s: %(code)s - %(msg)s: %(body)s' % {
+ 'from': sender, 'msg': msg, 'body': body, 'code': code}
else:
- message = _('%(from)s: %(msg)s: %(body)s') % {
- 'from': sender, 'msg': msg, 'body': body}
+ message = '%(from)s: %(msg)s: %(body)s' % {
+ 'from': sender, 'msg': msg, 'body': body}
return message
@@ -1163,7 +1195,7 @@ class Core(object):
self._current_tab_nb = len(self.tabs) - 1
else:
self._current_tab_nb = value
- if old != self._current_tab_nb:
+ if old != self._current_tab_nb and self.tabs[self._current_tab_nb]:
self.events.trigger('tab_change', old, self._current_tab_nb)
### Opening actions ###
@@ -1209,11 +1241,11 @@ class Core(object):
tab.privates.append(new_tab)
return new_tab
- def open_new_room(self, room, nick, focus=True):
+ def open_new_room(self, room, nick, *, password=None, focus=True):
"""
Open a new tab.MucTab containing a muc Room, using the specified nick
"""
- new_tab = tabs.MucTab(room, nick)
+ new_tab = tabs.MucTab(room, nick, password=password)
self.add_tab(new_tab, focus)
self.refresh_window()
@@ -1262,7 +1294,7 @@ class Core(object):
Disable private tabs when leaving a room
"""
if reason is None:
- reason = _('\x195}You left the chatroom\x193}')
+ reason = '\x195}You left the chatroom\x193}'
for tab in self.get_tabs(tabs.PrivateTab):
if tab.name.startswith(room_name):
tab.deactivate(reason=reason)
@@ -1272,7 +1304,7 @@ class Core(object):
Enable private tabs when joining a room
"""
if reason is None:
- reason = _('\x195}You joined the chatroom\x193}')
+ reason = '\x195}You joined the chatroom\x193}'
for tab in self.get_tabs(tabs.PrivateTab):
if tab.name.startswith(room_name):
tab.activate(reason=reason)
@@ -1286,6 +1318,7 @@ class Core(object):
"""
Close the given tab. If None, close the current one
"""
+ was_current = tab is None
tab = tab or self.current_tab()
if isinstance(tab, tabs.RosterInfoTab):
return # The tab 0 should NEVER be closed
@@ -1293,9 +1326,10 @@ class Core(object):
del tab.commands # and make the object collectable
tab.on_close()
nb = tab.nb
- if self.previous_tab_nb != nb:
- self.current_tab_nb = self.previous_tab_nb
- self.previous_tab_nb = 0
+ if was_current:
+ if self.previous_tab_nb != nb:
+ self.current_tab_nb = self.previous_tab_nb
+ self.previous_tab_nb = 0
if config.get('create_gaps'):
if nb >= len(self.tabs) - 1:
self.tabs.remove(tab)
@@ -1315,7 +1349,8 @@ class Core(object):
self.current_tab_nb = len(self.tabs) - 1
while not self.tabs[self.current_tab_nb]:
self.current_tab_nb -= 1
- self.current_tab().on_gain_focus()
+ if was_current:
+ self.current_tab().on_gain_focus()
self.refresh_window()
import gc
gc.collect()
@@ -1403,9 +1438,11 @@ class Core(object):
"""
Refresh everything
"""
+ nocursor = curses.curs_set(0)
self.current_tab().state = 'current'
self.current_tab().refresh()
self.doupdate()
+ curses.curs_set(nocursor)
def refresh_tab_win(self):
"""
@@ -1556,7 +1593,7 @@ class Core(object):
"""
enabled = config.get('enable_vertical_tab_list')
if not config.silent_set('enable_vertical_tab_list', str(not enabled)):
- self.information(_('Unable to write in the config file'), 'Error')
+ self.information('Unable to write in the config file', 'Error')
self.call_for_resize()
def resize_global_information_win(self):
@@ -1681,214 +1718,225 @@ class Core(object):
Register the commands when poezio starts
"""
self.register_command('help', self.command_help,
- usage=_('[command]'),
+ usage='[command]',
shortdesc='\\_o< KOIN KOIN KOIN',
completion=self.completion_help)
self.register_command('join', self.command_join,
- usage=_("[room_name][@server][/nick] [password]"),
- desc=_("Join the specified room. You can specify a nickname "
- "after a slash (/). If no nickname is specified, you will"
- " use the default_nick in the configuration file. You can"
- " omit the room name: you will then join the room you\'re"
- " looking at (useful if you were kicked). You can also "
- "provide a room_name without specifying a server, the "
- "server of the room you're currently in will be used. You"
- " can also provide a password to join the room.\nExamples"
- ":\n/join room@server.tld\n/join room@server.tld/John\n"
- "/join room2\n/join /me_again\n/join\n/join room@server"
- ".tld/my_nick password\n/join / password"),
- shortdesc=_('Join a room'),
+ usage="[room_name][@server][/nick] [password]",
+ desc="Join the specified room. You can specify a nickname "
+ "after a slash (/). If no nickname is specified, you will"
+ " use the default_nick in the configuration file. You can"
+ " omit the room name: you will then join the room you\'re"
+ " looking at (useful if you were kicked). You can also "
+ "provide a room_name without specifying a server, the "
+ "server of the room you're currently in will be used. You"
+ " can also provide a password to join the room.\nExamples"
+ ":\n/join room@server.tld\n/join room@server.tld/John\n"
+ "/join room2\n/join /me_again\n/join\n/join room@server"
+ ".tld/my_nick password\n/join / password",
+ shortdesc='Join a room',
completion=self.completion_join)
self.register_command('exit', self.command_quit,
- desc=_('Just disconnect from the server and exit poezio.'),
- shortdesc=_('Exit poezio.'))
+ desc='Just disconnect from the server and exit poezio.',
+ shortdesc='Exit poezio.')
self.register_command('quit', self.command_quit,
- desc=_('Just disconnect from the server and exit poezio.'),
- shortdesc=_('Exit poezio.'))
+ desc='Just disconnect from the server and exit poezio.',
+ shortdesc='Exit poezio.')
self.register_command('next', self.rotate_rooms_right,
- shortdesc=_('Go to the next room.'))
+ shortdesc='Go to the next room.')
self.register_command('prev', self.rotate_rooms_left,
- shortdesc=_('Go to the previous room.'))
+ shortdesc='Go to the previous room.')
self.register_command('win', self.command_win,
- usage=_('<number or name>'),
- shortdesc=_('Go to the specified room'),
+ usage='<number or name>',
+ shortdesc='Go to the specified room',
completion=self.completion_win)
self.commands['w'] = self.commands['win']
self.register_command('move_tab', self.command_move_tab,
- usage=_('<source> <destination>'),
- desc=_("Insert the <source> tab at the position of "
- "<destination>. This will make the following tabs shift in"
- " some cases (refer to the documentation). A tab can be "
- "designated by its number or by the beginning of its "
- "address. You can use \".\" as a shortcut for the current "
- "tab."),
- shortdesc=_('Move a tab.'),
+ usage='<source> <destination>',
+ desc="Insert the <source> tab at the position of "
+ "<destination>. This will make the following tabs shift in"
+ " some cases (refer to the documentation). A tab can be "
+ "designated by its number or by the beginning of its "
+ "address. You can use \".\" as a shortcut for the current "
+ "tab.",
+ shortdesc='Move a tab.',
completion=self.completion_move_tab)
self.register_command('destroy_room', self.command_destroy_room,
- usage=_('[room JID]'),
- desc=_('Try to destroy the room [room JID], or the current'
- ' tab if it is a multi-user chat and [room JID] is '
- 'not given.'),
- shortdesc=_('Destroy a room.'),
+ usage='[room JID]',
+ desc='Try to destroy the room [room JID], or the current'
+ ' tab if it is a multi-user chat and [room JID] is '
+ 'not given.',
+ shortdesc='Destroy a room.',
completion=None)
self.register_command('show', self.command_status,
- usage=_('<availability> [status message]'),
- desc=_("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."),
- shortdesc=_('Change your availability.'),
+ usage='<availability> [status message]',
+ desc="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.",
+ shortdesc='Change your availability.',
completion=self.completion_status)
self.commands['status'] = self.commands['show']
self.register_command('bookmark_local', self.command_bookmark_local,
- usage=_("[roomname][/nick] [password]"),
- desc=_("Bookmark 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 syntax 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)"),
- shortdesc=_('Bookmark a room locally.'),
+ usage="[roomname][/nick] [password]",
+ desc="Bookmark 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 syntax 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)",
+ shortdesc='Bookmark a room locally.',
completion=self.completion_bookmark_local)
self.register_command('bookmark', self.command_bookmark,
- usage=_("[roomname][/nick] [autojoin] [password]"),
- desc=_("Bookmark: 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 syntax as /join. Type /help join for syntax "
- "examples. Note that when typing \"/bookmark\" alone, the"
- " room will be bookmarked with the nickname you\'re "
- "currently using in this room (instead of default_nick)."),
- shortdesc=_("Bookmark a room online."),
+ usage="[roomname][/nick] [autojoin] [password]",
+ desc="Bookmark: 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 syntax as /join. Type /help join for syntax "
+ "examples. Note that when typing \"/bookmark\" alone, the"
+ " room will be bookmarked with the nickname you\'re "
+ "currently using in this room (instead of default_nick).",
+ shortdesc="Bookmark a room online.",
completion=self.completion_bookmark)
self.register_command('set', self.command_set,
- usage=_("[plugin|][section] <option> [value]"),
- desc=_("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`. `toggle` can be "
- "used as a special value to toggle a boolean option."),
- shortdesc=_("Set the value of an option"),
+ usage="[plugin|][section] <option> [value]",
+ desc="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`. `toggle` can be "
+ "used as a special value to toggle a boolean option.",
+ shortdesc="Set the value of an option",
completion=self.completion_set)
+ self.register_command('set_default', self.command_set_default,
+ usage="[section] <option>",
+ desc="Set the default value of an option. For example, "
+ "`/set_default resource` will reset the resource "
+ "option. You can also reset options in specific "
+ "sections by doing `/set_default section option`.",
+ shortdesc="Set the default value of an option",
+ completion=self.completion_set_default)
self.register_command('toggle', self.command_toggle,
- usage=_('<option>'),
- desc=_('Shortcut for /set <option> toggle'),
- shortdesc=_('Toggle an option'),
+ usage='<option>',
+ desc='Shortcut for /set <option> toggle',
+ shortdesc='Toggle an option',
completion=self.completion_toggle)
self.register_command('theme', self.command_theme,
- usage=_('[theme name]'),
- desc=_("Reload the theme defined in the config file. If theme"
- "_name is provided, set that theme before reloading it."),
- shortdesc=_('Load a theme'),
+ usage='[theme name]',
+ desc="Reload the theme defined in the config file. If theme"
+ "_name is provided, set that theme before reloading it.",
+ shortdesc='Load a theme',
completion=self.completion_theme)
self.register_command('list', self.command_list,
- usage=_('[server]'),
- desc=_("Get the list of public chatrooms"
- " on the specified server."),
- shortdesc=_('List the rooms.'),
+ usage='[server]',
+ desc="Get the list of public chatrooms"
+ " on the specified server.",
+ shortdesc='List the rooms.',
completion=self.completion_list)
self.register_command('message', self.command_message,
- usage=_('<jid> [optional message]'),
- desc=_("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."),
- shortdesc=_('Send a message'),
+ usage='<jid> [optional message]',
+ desc="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.",
+ shortdesc='Send a message',
completion=self.completion_message)
self.register_command('version', self.command_version,
usage='<jid>',
- desc=_("Get the software version of the given JID (usually its"
- " XMPP client and Operating System)."),
- shortdesc=_('Get the software version of a JID.'),
+ desc="Get the software version of the given JID (usually its"
+ " XMPP client and Operating System).",
+ shortdesc='Get the software version of a JID.',
completion=self.completion_version)
self.register_command('server_cycle', self.command_server_cycle,
- usage=_('[domain] [message]'),
- desc=_('Disconnect and reconnect in all the rooms in domain.'),
- shortdesc=_('Cycle a range of rooms'),
+ usage='[domain] [message]',
+ desc='Disconnect and reconnect in all the rooms in domain.',
+ shortdesc='Cycle a range of rooms',
completion=self.completion_server_cycle)
self.register_command('bind', self.command_bind,
- usage=_('<key> <equ>'),
- desc=_("Bind a key to another key or to a “command”. For "
- "example \"/bind ^H KEY_UP\" makes Control + h do the"
- " same same as the Up key."),
+ usage='<key> <equ>',
+ desc="Bind a key to another key or to a “command”. For "
+ "example \"/bind ^H KEY_UP\" makes Control + h do the"
+ " same same as the Up key.",
completion=self.completion_bind,
- shortdesc=_('Bind a key to another key.'))
+ shortdesc='Bind a key to another key.')
self.register_command('load', self.command_load,
- usage=_('<plugin> [<otherplugin> …]'),
- shortdesc=_('Load the specified plugin(s)'),
+ usage='<plugin> [<otherplugin> …]',
+ shortdesc='Load the specified plugin(s)',
completion=self.plugin_manager.completion_load)
self.register_command('unload', self.command_unload,
- usage=_('<plugin> [<otherplugin> …]'),
- shortdesc=_('Unload the specified plugin(s)'),
+ usage='<plugin> [<otherplugin> …]',
+ shortdesc='Unload the specified plugin(s)',
completion=self.plugin_manager.completion_unload)
self.register_command('plugins', self.command_plugins,
- shortdesc=_('Show the plugins in use.'))
+ shortdesc='Show the plugins in use.')
self.register_command('presence', self.command_presence,
- usage=_('<JID> [type] [status]'),
- desc=_("Send a directed presence to <JID> and using"
- " [type] and [status] if provided."),
- shortdesc=_('Send a directed presence.'),
+ usage='<JID> [type] [status]',
+ desc="Send a directed presence to <JID> and using"
+ " [type] and [status] if provided.",
+ shortdesc='Send a directed presence.',
completion=self.completion_presence)
self.register_command('rawxml', self.command_rawxml,
usage='<xml>',
- shortdesc=_('Send a custom xml stanza.'))
+ shortdesc='Send a custom xml stanza.')
self.register_command('invite', self.command_invite,
- usage=_('<jid> <room> [reason]'),
- desc=_('Invite jid in room with reason.'),
- shortdesc=_('Invite someone in a room.'),
+ usage='<jid> <room> [reason]',
+ desc='Invite jid in room with reason.',
+ shortdesc='Invite someone in a room.',
completion=self.completion_invite)
self.register_command('invitations', self.command_invitations,
- shortdesc=_('Show the pending invitations.'))
+ shortdesc='Show the pending invitations.')
self.register_command('bookmarks', self.command_bookmarks,
- shortdesc=_('Show the current bookmarks.'))
+ shortdesc='Show the current bookmarks.')
self.register_command('remove_bookmark', self.command_remove_bookmark,
usage='[jid]',
- desc=_("Remove the specified bookmark, or the "
- "bookmark on the current tab, if any."),
- shortdesc=_('Remove a bookmark'),
+ desc="Remove the specified bookmark, or the "
+ "bookmark on the current tab, if any.",
+ shortdesc='Remove a bookmark',
completion=self.completion_remove_bookmark)
self.register_command('xml_tab', self.command_xml_tab,
- shortdesc=_('Open an XML tab.'))
+ shortdesc='Open an XML tab.')
self.register_command('runkey', self.command_runkey,
- usage=_('<key>'),
- shortdesc=_('Execute the action defined for <key>.'),
+ usage='<key>',
+ shortdesc='Execute the action defined for <key>.',
completion=self.completion_runkey)
self.register_command('self', self.command_self,
- shortdesc=_('Remind you of who you are.'))
+ shortdesc='Remind you of who you are.')
self.register_command('last_activity', self.command_last_activity,
usage='<jid>',
- desc=_('Informs you of the last activity of a JID.'),
- shortdesc=_('Get the activity of someone.'),
+ desc='Informs you of the last activity of a JID.',
+ shortdesc='Get the activity of someone.',
completion=self.completion_last_activity)
self.register_command('ad-hoc', self.command_adhoc,
usage='<jid>',
- shortdesc=_('List available ad-hoc commands on the given jid'))
+ shortdesc='List available ad-hoc commands on the given jid')
+ self.register_command('reload', self.command_reload,
+ shortdesc='Reload the config. You can achieve the same by '
+ 'sending SIGUSR1 to poezio.')
if config.get('enable_user_activity'):
self.register_command('activity', self.command_activity,
usage='[<general> [specific] [text]]',
- desc=_('Send your current activity to your contacts '
- '(use the completion). Nothing means '
- '"stop broadcasting an activity".'),
- shortdesc=_('Send your activity.'),
+ desc='Send your current activity to your contacts '
+ '(use the completion). Nothing means '
+ '"stop broadcasting an activity".',
+ shortdesc='Send your activity.',
completion=self.completion_activity)
if config.get('enable_user_mood'):
self.register_command('mood', self.command_mood,
usage='[<mood> [text]]',
- desc=_('Send your current mood to your contacts '
- '(use the completion). Nothing means '
- '"stop broadcasting a mood".'),
- shortdesc=_('Send your mood.'),
+ desc='Send your current mood to your contacts '
+ '(use the completion). Nothing means '
+ '"stop broadcasting a mood".',
+ shortdesc='Send your mood.',
completion=self.completion_mood)
if config.get('enable_user_gaming'):
self.register_command('gaming', self.command_gaming,
usage='[<game name> [server address]]',
- desc=_('Send your current gaming activity to '
- 'your contacts. Nothing means "stop '
- 'broadcasting a gaming activity".'),
- shortdesc=_('Send your gaming activity.'),
+ desc='Send your current gaming activity to '
+ 'your contacts. Nothing means "stop '
+ 'broadcasting a gaming activity".',
+ shortdesc='Send your gaming activity.',
completion=None)
####################### XMPP Event Handlers ##################################
@@ -1899,6 +1947,7 @@ class Core(object):
on_groupchat_direct_invitation = handlers.on_groupchat_direct_invitation
on_groupchat_decline = handlers.on_groupchat_decline
on_message = handlers.on_message
+ on_error_message = handlers.on_error_message
on_normal_message = handlers.on_normal_message
on_nick_received = handlers.on_nick_received
on_gaming_event = handlers.on_gaming_event
@@ -1943,9 +1992,11 @@ class Core(object):
on_receipt = handlers.on_receipt
on_attention = handlers.on_attention
room_error = handlers.room_error
+ check_bookmark_storage = handlers.check_bookmark_storage
outgoing_stanza = handlers.outgoing_stanza
incoming_stanza = handlers.incoming_stanza
validate_ssl = handlers.validate_ssl
+ ssl_invalid_chain = handlers.ssl_invalid_chain
on_next_adhoc_step = handlers.on_next_adhoc_step
on_adhoc_error = handlers.on_adhoc_error
cancel_adhoc_command = handlers.cancel_adhoc_command
@@ -1967,6 +2018,7 @@ class Core(object):
command_destroy_room = commands.command_destroy_room
command_remove_bookmark = commands.command_remove_bookmark
command_set = commands.command_set
+ command_set_default = commands.command_set_default
command_toggle = commands.command_toggle
command_server_cycle = commands.command_server_cycle
command_last_activity = commands.command_last_activity
@@ -1986,6 +2038,7 @@ class Core(object):
command_xml_tab = commands.command_xml_tab
command_adhoc = commands.command_adhoc
command_self = commands.command_self
+ command_reload = commands.command_reload
completion_help = completions.completion_help
completion_status = completions.completion_status
completion_presence = completions.completion_presence
@@ -2007,6 +2060,7 @@ class Core(object):
completion_last_activity = completions.completion_last_activity
completion_server_cycle = completions.completion_server_cycle
completion_set = completions.completion_set
+ completion_set_default = completions.completion_set_default
completion_toggle = completions.completion_toggle
completion_bookmark_local = completions.completion_bookmark_local
diff --git a/src/core/handlers.py b/src/core/handlers.py
index 50dca216..828c39d1 100644
--- a/src/core/handlers.py
+++ b/src/core/handlers.py
@@ -12,14 +12,12 @@ import ssl
import sys
import time
from hashlib import sha1, sha512
-from gettext import gettext as _
from os import path
from slixmpp import InvalidJID
-from slixmpp.stanza import Message
-from slixmpp.xmlstream.stanzabase import StanzaBase
+from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
+from xml.etree import ElementTree as ET
-import bookmark
import common
import fixes
import pep
@@ -32,11 +30,64 @@ from config import config, CACHE_DIR
from contact import Resource
from logger import logger
from roster import roster
-from text_buffer import CorrectionError
+from text_buffer import CorrectionError, AckError
from theming import dump_tuple, get_theme
from . commands import dumb_callback
+try:
+ from pygments import highlight
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters import HtmlFormatter
+ LEXER = get_lexer_by_name('xml')
+ FORMATTER = HtmlFormatter(noclasses=True)
+ PYGMENTS = True
+except ImportError:
+ PYGMENTS = False
+
+def _join_initial_rooms(self, bookmarks):
+ """Join all rooms given in the iterator `bookmarks`"""
+ for bm in bookmarks:
+ if not (bm.autojoin or config.get('open_all_bookmarks')):
+ continue
+ tab = self.get_tab_by_name(bm.jid, tabs.MucTab)
+ nick = bm.nick if bm.nick else self.own_nick
+ if not tab:
+ self.open_new_room(bm.jid, nick, focus=False)
+ self.initial_joins.append(bm.jid)
+ histo_length = config.get('muc_history_length')
+ if histo_length == -1:
+ histo_length = None
+ if histo_length is not None:
+ histo_length = str(histo_length)
+ # do not join rooms that do not have autojoin
+ # but display them anyway
+ if bm.autojoin:
+ muc.join_groupchat(self, bm.jid, nick,
+ passwd=bm.password,
+ maxhistory=histo_length,
+ status=self.status.message,
+ show=self.status.show)
+
+def check_bookmark_storage(self, features):
+ private = 'jabber:iq:private' in features
+ pep_ = 'http://jabber.org/protocol/pubsub#publish' in features
+ self.bookmarks.available_storage['private'] = private
+ self.bookmarks.available_storage['pep'] = pep_
+ def _join_remote_only(iq):
+ if iq['type'] == 'error':
+ type_ = iq['error']['type']
+ condition = iq['error']['condition']
+ if not (type_ == 'cancel' and condition == 'item-not-found'):
+ self.information('Unable to fetch the remote'
+ ' bookmarks; %s: %s' % (type_, condition),
+ 'Error')
+ return
+ remote_bookmarks = self.bookmarks.remote()
+ _join_initial_rooms(self, remote_bookmarks)
+ if not self.xmpp.anon and config.get('use_remote_bookmarks'):
+ self.bookmarks.get_remote(self.xmpp, self.information, _join_remote_only)
+
def on_session_start_features(self, _):
"""
Enable carbons & blocking on session start if wanted and possible
@@ -47,11 +98,13 @@ def on_session_start_features(self, _):
features = iq['disco_info']['features']
rostertab = self.get_tab_by_name('Roster', tabs.RosterInfoTab)
rostertab.check_blocking(features)
+ rostertab.check_saslexternal(features)
if (config.get('enable_carbons') and
'urn:xmpp:carbons:2' in features):
self.xmpp.plugin['xep_0280'].enable()
self.xmpp.add_event_handler('carbon_received', self.on_carbon_received)
self.xmpp.add_event_handler('carbon_sent', self.on_carbon_sent)
+ self.check_bookmark_storage(features)
self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.domain,
callback=callback)
@@ -173,11 +226,31 @@ def on_message(self, message):
jid_from = message['from']
for tab in self.get_tabs(tabs.MucTab):
if tab.name == jid_from.bare:
+ if message['type'] == 'chat':
+ return self.on_groupchat_private_message(message)
+ return self.on_normal_message(message)
+
+def on_error_message(self, message):
+ """
+ When receiving any message with type="error"
+ """
+ jid_from = message['from']
+ for tab in self.get_tabs(tabs.MucTab):
+ if tab.name == jid_from.bare:
if message['type'] == 'error':
- return self.room_error(message, jid_from)
+ return self.room_error(message, jid_from.bare)
else:
return self.on_groupchat_private_message(message)
- return self.on_normal_message(message)
+ tab = self.get_conversation_by_jid(message['from'], create=False)
+ error_msg = self.get_error_message(message, deprecated=True)
+ if not tab:
+ return self.information(error_msg, 'Error')
+ error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK),
+ error_msg)
+ if not tab.nack_message('\n' + error, message['id'], message['to']):
+ tab.add_message(error, typ=0)
+ self.refresh_window()
+
def on_normal_message(self, message):
"""
@@ -185,7 +258,7 @@ def on_normal_message(self, message):
muc participant)
"""
if message['type'] == 'error':
- return self.information(self.get_error_message(message, deprecated=True), 'Error')
+ return
elif message['type'] == 'headline' and message['body']:
return self.information('%s says: %s' % (message['from'], message['body']), 'Headline')
@@ -448,7 +521,7 @@ def on_groupchat_message(self, message):
tab = self.get_tab_by_name(room_from, tabs.MucTab)
if not tab:
- self.information(_("message received for a non-existing room: %s") % (room_from))
+ self.information("message received for a non-existing room: %s" % (room_from))
muc.leave_groupchat(self.xmpp, room_from, self.own_nick, msg='')
return
@@ -689,7 +762,10 @@ def on_subscription_request(self, presence):
contact = roster.get_and_set(jid)
roster.update_contact_groups(contact)
contact.pending_in = True
- self.information('%s wants to subscribe to your presence' % jid, 'Roster')
+ self.information('%s wants to subscribe to your presence, '
+ 'use /accept <jid> or /deny <jid> to accept '
+ 'or reject the query.' % jid,
+ 'Roster')
self.get_tab_by_number(0).state = 'highlight'
roster.modified()
if isinstance(self.current_tab(), tabs.RosterInfoTab):
@@ -782,7 +858,7 @@ def on_got_offline(self, presence):
return
jid = presence['from']
if not logger.log_roster_change(jid.bare, 'got offline'):
- self.information(_('Unable to write in the log file'), 'Error')
+ self.information('Unable to write in the log file', 'Error')
# If a resource got offline, display the message in the conversation with this
# precise resource.
if jid.resource:
@@ -806,7 +882,7 @@ def on_got_online(self, presence):
return
roster.modified()
if not logger.log_roster_change(jid.bare, 'got online'):
- self.information(_('Unable to write in the log file'), 'Error')
+ self.information('Unable to write in the log file', 'Error')
resource = Resource(jid.full, {
'priority': presence.get_priority() or 0,
'status': presence['status'],
@@ -843,7 +919,7 @@ def on_failed_connection(self, error):
"""
We cannot contact the remote server
"""
- self.information(_("Connection to remote server failed: %s" % (error,)), _('Error'))
+ self.information("Connection to remote server failed: %s" % (error,), 'Error')
def on_disconnected(self, event):
"""
@@ -854,9 +930,10 @@ def on_disconnected(self, event):
roster.modified()
for tab in self.get_tabs(tabs.MucTab):
tab.disconnect()
- self.information(_("Disconnected from server."), _('Error'))
- if not self.legitimate_disconnect and config.get('auto_reconnect', False):
- self.information(_("Auto-reconnecting."), _('Info'))
+ msg_typ = 'Error' if not self.legitimate_disconnect else 'Info'
+ self.information("Disconnected from server.", msg_typ)
+ if not self.legitimate_disconnect and config.get('auto_reconnect', True):
+ self.information("Auto-reconnecting.", 'Info')
self.xmpp.connect()
def on_stream_error(self, event):
@@ -864,29 +941,29 @@ def on_stream_error(self, event):
When we receive a stream error
"""
if event and event['text']:
- self.information(_('Stream error: %s') % event['text'], _('Error'))
+ self.information('Stream error: %s' % event['text'], 'Error')
def on_failed_all_auth(self, event):
"""
Authentication failed
"""
- self.information(_("Authentication failed (bad credentials?)."),
- _('Error'))
+ self.information("Authentication failed (bad credentials?).",
+ 'Error')
self.legitimate_disconnect = True
def on_no_auth(self, event):
"""
Authentication failed (no mech)
"""
- self.information(_("Authentication failed, no login method available."),
- _('Error'))
+ self.information("Authentication failed, no login method available.",
+ 'Error')
self.legitimate_disconnect = True
def on_connected(self, event):
"""
Remote host responded, but we are not yet authenticated
"""
- self.information(_("Connected to server."), 'Info')
+ self.information("Connected to server.", 'Info')
def on_connecting(self, event):
"""
@@ -901,11 +978,12 @@ def on_session_start(self, event):
self.connection_time = time.time()
if not self.plugins_autoloaded: # Do not reload plugins on reconnection
self.autoload_plugins()
- self.information(_("Authentication success."), 'Info')
- self.information(_("Your JID is %s") % self.xmpp.boundjid.full, 'Info')
+ self.information("Authentication success.", 'Info')
+ self.information("Your JID is %s" % self.xmpp.boundjid.full, 'Info')
if not self.xmpp.anon:
# request the roster
self.xmpp.get_roster()
+ roster.update_contact_groups(self.xmpp.boundjid.bare)
# send initial presence
if config.get('send_initial_presence'):
pres = self.xmpp.make_presence()
@@ -913,37 +991,9 @@ def on_session_start(self, event):
pres['status'] = self.status.message
self.events.trigger('send_normal_presence', pres)
pres.send()
- bookmark.get_local()
- def _join_initial_rooms(bookmarks):
- """Join all rooms given in the iterator `bookmarks`"""
- for bm in bookmarks:
- if bm.autojoin or config.get('open_all_bookmarks'):
- tab = self.get_tab_by_name(bm.jid, tabs.MucTab)
- nick = bm.nick if bm.nick else self.own_nick
- if not tab:
- self.open_new_room(bm.jid, nick, False)
- self.initial_joins.append(bm.jid)
- histo_length = config.get('muc_history_length')
- if histo_length == -1:
- histo_length = None
- if histo_length is not None:
- histo_length = str(histo_length)
- # do not join rooms that do not have autojoin
- # but display them anyway
- if bm.autojoin:
- muc.join_groupchat(self, bm.jid, nick,
- passwd=bm.password,
- maxhistory=histo_length,
- status=self.status.message,
- show=self.status.show)
- def _join_remote_only():
- remote_bookmarks = (bm for bm in bookmark.bookmarks if (bm.method in ("pep", "privatexml")))
- _join_initial_rooms(remote_bookmarks)
- if not self.xmpp.anon and config.get('use_remote_bookmarks'):
- bookmark.get_remote(self.xmpp, _join_remote_only)
- # join all the available bookmarks. As of yet, this is just the local
- # ones
- _join_initial_rooms(bookmark.bookmarks)
+ self.bookmarks.get_local()
+ # join all the available bookmarks. As of yet, this is just the local ones
+ _join_initial_rooms(self, self.bookmarks)
if config.get('enable_user_nick'):
self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback)
@@ -1024,12 +1074,12 @@ def on_groupchat_subject(self, message):
# Do not display the message if the subject did not change or if we
# receive an empty topic when joining the room.
if nick_from:
- tab.add_message(_("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") %
+ tab.add_message("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s" %
{'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'nick':nick_from, 'subject':subject},
time=None,
typ=2)
else:
- tab.add_message(_("\x19%(info_col)s}The subject is: %(subject)s") %
+ tab.add_message("\x19%(info_col)s}The subject is: %(subject)s" %
{'subject':subject, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
time=None,
typ=2)
@@ -1052,7 +1102,10 @@ def on_receipt(self, message):
if not conversation:
return
- conversation.ack_message(msg_id)
+ try:
+ conversation.ack_message(msg_id, self.xmpp.boundjid)
+ except AckError:
+ log.debug('Error while receiving an ack', exc_info=True)
def on_data_form(self, message):
"""
@@ -1083,19 +1136,21 @@ def room_error(self, error, room_name):
Display the error in the tab
"""
tab = self.get_tab_by_name(room_name, tabs.MucTab)
+ if not tab:
+ return
error_message = self.get_error_message(error)
tab.add_message(error_message, highlight=True, nickname='Error',
nick_color=get_theme().COLOR_ERROR_MSG, typ=2)
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)')
+ msg = 'To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)'
tab.add_message(msg, typ=2)
if code == '409':
if config.get('alternative_nickname') != '':
self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname')))
else:
if not tab.joined:
- tab.add_message(_('You can join the room with an other nick, by typing "/join /other_nick"'), typ=2)
+ tab.add_message('You can join the room with an other nick, by typing "/join /other_nick"', typ=2)
self.refresh_window()
def outgoing_stanza(self, stanza):
@@ -1103,7 +1158,20 @@ def outgoing_stanza(self, stanza):
We are sending a new stanza, write it in the xml buffer if needed.
"""
if self.xml_tab:
- self.add_message_to_text_buffer(self.xml_buffer, '\x191}<--\x19o %s' % stanza)
+ if PYGMENTS:
+ xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER)
+ poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True).rstrip('\x19o').strip()
+ else:
+ poezio_colored = '%s' % stanza
+ self.add_message_to_text_buffer(self.xml_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_OUT)
+ try:
+ if self.xml_tab.match_stanza(ElementBase(ET.fromstring(stanza))):
+ self.add_message_to_text_buffer(self.xml_tab.filtered_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_OUT)
+ except:
+ log.debug('', exc_info=True)
+
if isinstance(self.current_tab(), tabs.XMLTab):
self.current_tab().refresh()
self.doupdate()
@@ -1113,11 +1181,27 @@ def incoming_stanza(self, stanza):
We are receiving a new stanza, write it in the xml buffer if needed.
"""
if self.xml_tab:
- self.add_message_to_text_buffer(self.xml_buffer, '\x192}-->\x19o %s' % stanza)
+ if PYGMENTS:
+ xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER)
+ poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True).rstrip('\x19o').strip()
+ else:
+ poezio_colored = '%s' % stanza
+ self.add_message_to_text_buffer(self.xml_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_IN)
+ try:
+ if self.xml_tab.match_stanza(stanza):
+ self.add_message_to_text_buffer(self.xml_tab.filtered_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_IN)
+ except:
+ log.debug('', exc_info=True)
if isinstance(self.current_tab(), tabs.XMLTab):
self.current_tab().refresh()
self.doupdate()
+def ssl_invalid_chain(self, tb):
+ self.information('The certificate sent by the server is invalid.', 'Error')
+ self.disconnect()
+
def validate_ssl(self, pem):
"""
Check the server certificate using the slixmpp ssl_cert event
@@ -1151,40 +1235,34 @@ def validate_ssl(self, pem):
self.information('New certificate found (sha-2 hash:'
' %s)\nPlease validate or abort' % sha2_found_cert,
'Warning')
- input = windows.YesNoInput(text="WARNING! Server certificate has changed, accept? (y/n)")
- self.current_tab().input = input
- input.resize(1, self.current_tab().width, self.current_tab().height-1, 0)
- input.refresh()
- self.doupdate()
- old_loop = asyncio.get_event_loop()
- new_loop = asyncio.new_event_loop()
- asyncio.set_event_loop(new_loop)
- new_loop.add_reader(sys.stdin, self.on_input_readable)
- future = asyncio.Future()
- @asyncio.coroutine
- def check_input(future):
- while input.value is None:
- yield from asyncio.sleep(0.01)
+ def check_input():
self.current_tab().input = saved_input
self.paused = False
if input.value:
self.information('Setting new certificate: old: %s, new: %s' % (cert, sha2_found_cert), 'Info')
log.debug('Setting certificate to %s', sha2_found_cert)
if not config.silent_set('certificate', sha2_found_cert):
- self.information(_('Unable to write in the config file'), 'Error')
+ self.information('Unable to write in the config file', 'Error')
else:
self.information('You refused to validate the certificate. You are now disconnected', 'Info')
- self.xmpp.disconnect()
+ self.disconnect()
new_loop.stop()
asyncio.set_event_loop(old_loop)
- asyncio.async(check_input(future))
+ input = windows.YesNoInput(text="WARNING! Server certificate has changed, accept? (y/n)", callback=check_input)
+ self.current_tab().input = input
+ input.resize(1, self.current_tab().width, self.current_tab().height-1, 0)
+ input.refresh()
+ self.doupdate()
+ old_loop = asyncio.get_event_loop()
+ new_loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(new_loop)
+ new_loop.add_reader(sys.stdin, self.on_input_readable)
+ curses.beep()
new_loop.run_forever()
-
-
else:
log.debug('First time. Setting certificate to %s', sha2_found_cert)
if not config.silent_set('certificate', sha2_found_cert):
- self.information(_('Unable to write in the config file'), 'Error')
+ self.information('Unable to write in the config file', 'Error')
def _composing_tab_state(tab, state):
"""
diff --git a/src/core/structs.py b/src/core/structs.py
index d97acd9f..4ce0ef43 100644
--- a/src/core/structs.py
+++ b/src/core/structs.py
@@ -2,39 +2,38 @@
Module defining structures useful to the core class and related methods
"""
import collections
-from gettext import gettext as _
# http://xmpp.org/extensions/xep-0045.html#errorstatus
ERROR_AND_STATUS_CODES = {
- '401': _('A password is required'),
- '403': _('Permission denied'),
- '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'),
- '409': _('This nickname is already in use or has been reserved'),
- '503': _('The maximum number of users has been reached'),
+ '401': 'A password is required',
+ '403': 'Permission denied',
+ '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',
+ '409': 'This nickname is already in use or has been reserved',
+ '503': 'The maximum number of users has been reached',
}
# http://xmpp.org/extensions/xep-0086.html
DEPRECATED_ERRORS = {
- '302': _('Redirect'),
- '400': _('Bad request'),
- '401': _('Not authorized'),
- '402': _('Payment required'),
- '403': _('Forbidden'),
- '404': _('Not found'),
- '405': _('Not allowed'),
- '406': _('Not acceptable'),
- '407': _('Registration required'),
- '408': _('Request timeout'),
- '409': _('Conflict'),
- '500': _('Internal server error'),
- '501': _('Feature not implemented'),
- '502': _('Remote server error'),
- '503': _('Service unavailable'),
- '504': _('Remote server timeout'),
- '510': _('Disconnected'),
+ '302': 'Redirect',
+ '400': 'Bad request',
+ '401': 'Not authorized',
+ '402': 'Payment required',
+ '403': 'Forbidden',
+ '404': 'Not found',
+ '405': 'Not allowed',
+ '406': 'Not acceptable',
+ '407': 'Registration required',
+ '408': 'Request timeout',
+ '409': 'Conflict',
+ '500': 'Internal server error',
+ '501': 'Feature not implemented',
+ '502': 'Remote server error',
+ '503': 'Service unavailable',
+ '504': 'Remote server timeout',
+ '510': 'Disconnected',
}
possible_show = {'available':None,
diff --git a/src/daemon.py b/src/daemon.py
index 395054a7..6325d8df 100755
--- a/src/daemon.py
+++ b/src/daemon.py
@@ -25,11 +25,7 @@ import subprocess
import shlex
import logging
-try:
- from subprocess import DEVNULL # Only in python >= 3.3
-except ImportError:
- import os
- DEVNULL = open(os.devnull, 'wb')
+from subprocess import DEVNULL
log = logging.getLogger(__name__)
diff --git a/src/decorators.py b/src/decorators.py
index 251d8749..c4ea6563 100644
--- a/src/decorators.py
+++ b/src/decorators.py
@@ -2,6 +2,8 @@
Module containing various decorators
"""
+import common
+
class RefreshWrapper(object):
def __init__(self):
self.core = None
@@ -41,3 +43,97 @@ class RefreshWrapper(object):
return wrap
refresh_wrapper = RefreshWrapper()
+
+class CommandArgParser(object):
+ """Modify the string argument of the function into a list of strings
+ containing the right number of extracted arguments, or None if we don’t
+ have enough.
+ """
+ @staticmethod
+ def raw(func):
+ """Just call the function with a single string, which is the original string
+ untouched
+ """
+ def wrap(self, args, *a, **kw):
+ return func(self, args, *a, **kw)
+ return wrap
+
+ @staticmethod
+ def ignored(func):
+ """
+ Call the function without any argument
+ """
+ def wrap(self, args, *a, **kw):
+ return func(self, *a, **kw)
+ return wrap
+
+ @staticmethod
+ def quoted(mandatory, optional=0, defaults=[],
+ ignore_trailing_arguments=False):
+
+ """The function receives a list with a number of arguments that is between
+ the numbers `mandatory` and `optional`.
+
+ If the string doesn’t contain at least `mandatory` arguments, we return
+ None because the given arguments are invalid.
+
+ If there are any remaining arguments after `mandatory` and `optional`
+ arguments have been found (and “ignore_trailing_arguments" is not True),
+ we happen them to the last argument of the list.
+
+ An argument is a string (with or without whitespaces) between to quotes
+ ("), or a whitespace separated word (if not inside quotes).
+
+ The argument `defaults` is a list of strings that are used when an
+ optional argument is missing. For example if we accept one optional
+ argument, zero is available but we have one value in the `defaults`
+ list, we use that string inplace. The `defaults` list can only
+ replace missing optional arguments, not mandatory ones. And it
+ should not contain more than `mandatory` values. Also you cannot
+
+ Example:
+ This method needs at least one argument, and accepts up to 3
+ arguments
+
+ >> @command_args_parser.quoted(1, 2, ['default for first arg'], False)
+ >> def f(args):
+ >> print(args)
+
+ >> f('coucou les amis') # We have one mandatory and two optional
+ ['coucou', 'les', 'amis']
+ >> f('"coucou les amis" "PROUT PROUT"') # One mandator and only one optional,
+ # no default for the second
+ ['coucou les amis', 'PROUT PROUT']
+ >> f('') # Not enough args for mandatory number
+ None
+ >> f('"coucou les potes"') # One mandatory, and use the default value
+ # for the first optional
+ ['coucou les potes, 'default for first arg']
+ >> f('"un et demi" deux trois quatre cinq six') # We have three trailing arguments
+ ['un et demi', 'deux', 'trois quatre cinq six']
+
+ """
+ def first(func):
+ def second(self, args, *a, **kw):
+ default_args = defaults
+ args = common.shell_split(args)
+ if len(args) < mandatory:
+ return func(self, None, *a, **kw)
+ res, args = args[:mandatory], args[mandatory:]
+ if optional == -1:
+ opt_args = args[:]
+ else:
+ opt_args = args[:optional]
+
+ if opt_args:
+ res += opt_args
+ args = args[len(opt_args):]
+ default_args = default_args[len(opt_args):]
+ res += default_args
+ if args and res and not ignore_trailing_arguments:
+ res[-1] += " " + " ".join(args)
+ return func(self, res, *a, **kw)
+ return second
+ return first
+
+command_args_parser = CommandArgParser()
diff --git a/src/events.py b/src/events.py
index 50711022..15ef3e35 100644
--- a/src/events.py
+++ b/src/events.py
@@ -10,9 +10,6 @@ The list of available events is here:
http://poezio.eu/doc/en/plugins.html#_poezio_events
"""
-import logging
-log = logging.getLogger(__name__)
-
class EventHandler(object):
"""
A class keeping a list of possible events that are triggered
@@ -71,9 +68,7 @@ class EventHandler(object):
"""
callbacks = self.events.get(name, None)
if callbacks is None:
- log.debug('%s: No such event.', name)
return
- log.debug('Event %s triggered, callbacks: %s', name, callbacks)
for callback in callbacks:
callback(*args, **kwargs)
diff --git a/src/fixes.py b/src/fixes.py
index 1c5da7c8..3840a093 100644
--- a/src/fixes.py
+++ b/src/fixes.py
@@ -41,12 +41,12 @@ def get_version(xmpp, jid, callback=None, **kwargs):
def get_room_form(xmpp, room, callback):
def _cb(result):
if result["type"] == "error":
- callback(None)
+ return callback(None)
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
if xform is None:
- callback(None)
+ return callback(None)
form = xmpp.plugin['xep_0004'].buildForm(xform)
- callback(form)
+ return callback(form)
iq = xmpp.make_iq_get(ito=room)
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
diff --git a/src/keyboard.py b/src/keyboard.py
index ec1e7d0a..ccf9e752 100755
--- a/src/keyboard.py
+++ b/src/keyboard.py
@@ -66,6 +66,9 @@ def get_char_list(s):
if key == '^[':
try:
part = s.get_wch()
+ if part == '[':
+ # CTRL+arrow and meta+arrow keys have a long format
+ part += s.get_wch() + s.get_wch() + s.get_wch() + s.get_wch()
except curses.error:
pass
except ValueError: # invalid input
diff --git a/src/logger.py b/src/logger.py
index 85c7a746..7efa8f61 100644
--- a/src/logger.py
+++ b/src/logger.py
@@ -25,9 +25,7 @@ import logging
log = logging.getLogger(__name__)
-from config import LOG_DIR
-
-log_dir = os.path.join(LOG_DIR, 'logs')
+from config import LOG_DIR as log_dir
message_log_re = re.compile(r'MR (\d{4})(\d{2})(\d{2})T'
r'(\d{2}):(\d{2}):(\d{2})Z '
@@ -119,10 +117,15 @@ class Logger(object):
try:
fd = open(os.path.join(log_dir, jid), 'rb')
- except:
+ except FileNotFoundError:
+ log.info('Non-existing log file (%s)',
+ os.path.join(log_dir, jid),
+ exc_info=True)
+ return
+ except OSError:
log.error('Unable to open the log file (%s)',
- os.path.join(log_dir, jid),
- exc_info=True)
+ os.path.join(log_dir, jid),
+ exc_info=True)
return
if not fd:
return
diff --git a/src/multiuserchat.py b/src/multiuserchat.py
index 92d09a60..80e2c706 100644
--- a/src/multiuserchat.py
+++ b/src/multiuserchat.py
@@ -11,7 +11,6 @@ Add some facilities that are not available on the XEP_0045
slix plugin
"""
-from gettext import gettext as _
from xml.etree import cElementTree as ET
from common import safeJID
@@ -43,10 +42,10 @@ def destroy_room(xmpp, room, reason='', altroom=''):
iq.append(query)
def callback(iq):
if not iq or iq['type'] == 'error':
- xmpp.core.information(_('Unable to destroy room %s') % room,
- _('Info'))
+ xmpp.core.information('Unable to destroy room %s' % room,
+ 'Info')
else:
- xmpp.core.information(_('Room %s destroyed') % room, _('Info'))
+ xmpp.core.information('Room %s destroyed' % room, 'Info')
iq.send(callback=callback)
return True
diff --git a/src/pep.py b/src/pep.py
index 0478b5fd..0f7a1ced 100644
--- a/src/pep.py
+++ b/src/pep.py
@@ -3,89 +3,87 @@ Collection of mappings for PEP moods/activities
extracted directly from the XEP
"""
-from gettext import gettext as _
-
MOODS = {
- 'afraid': _('Afraid'),
- 'amazed': _('Amazed'),
- 'angry': _('Angry'),
- 'amorous': _('Amorous'),
- 'annoyed': _('Annoyed'),
- 'anxious': _('Anxious'),
- 'aroused': _('Aroused'),
- 'ashamed': _('Ashamed'),
- 'bored': _('Bored'),
- 'brave': _('Brave'),
- 'calm': _('Calm'),
- 'cautious': _('Cautious'),
- 'cold': _('Cold'),
- 'confident': _('Confident'),
- 'confused': _('Confused'),
- 'contemplative': _('Contemplative'),
- 'contented': _('Contented'),
- 'cranky': _('Cranky'),
- 'crazy': _('Crazy'),
- 'creative': _('Creative'),
- 'curious': _('Curious'),
- 'dejected': _('Dejected'),
- 'depressed': _('Depressed'),
- 'disappointed': _('Disappointed'),
- 'disgusted': _('Disgusted'),
- 'dismayed': _('Dismayed'),
- 'distracted': _('Distracted'),
- 'embarrassed': _('Embarrassed'),
- 'envious': _('Envious'),
- 'excited': _('Excited'),
- 'flirtatious': _('Flirtatious'),
- 'frustrated': _('Frustrated'),
- 'grumpy': _('Grumpy'),
- 'guilty': _('Guilty'),
- 'happy': _('Happy'),
- 'hopeful': _('Hopeful'),
- 'hot': _('Hot'),
- 'humbled': _('Humbled'),
- 'humiliated': _('Humiliated'),
- 'hungry': _('Hungry'),
- 'hurt': _('Hurt'),
- 'impressed': _('Impressed'),
- 'in_awe': _('In awe'),
- 'in_love': _('In love'),
- 'indignant': _('Indignant'),
- 'interested': _('Interested'),
- 'intoxicated': _('Intoxicated'),
- 'invincible': _('Invincible'),
- 'jealous': _('Jealous'),
- 'lonely': _('Lonely'),
- 'lucky': _('Lucky'),
- 'mean': _('Mean'),
- 'moody': _('Moody'),
- 'nervous': _('Nervous'),
- 'neutral': _('Neutral'),
- 'offended': _('Offended'),
- 'outraged': _('Outraged'),
- 'playful': _('Playful'),
- 'proud': _('Proud'),
- 'relaxed': _('Relaxed'),
- 'relieved': _('Relieved'),
- 'remorseful': _('Remorseful'),
- 'restless': _('Restless'),
- 'sad': _('Sad'),
- 'sarcastic': _('Sarcastic'),
- 'serious': _('Serious'),
- 'shocked': _('Shocked'),
- 'shy': _('Shy'),
- 'sick': _('Sick'),
- 'sleepy': _('Sleepy'),
- 'spontaneous': _('Spontaneous'),
- 'stressed': _('Stressed'),
- 'strong': _('Strong'),
- 'surprised': _('Surprised'),
- 'thankful': _('Thankful'),
- 'thirsty': _('Thirsty'),
- 'tired': _('Tired'),
- 'undefined': _('Undefined'),
- 'weak': _('Weak'),
- 'worried': _('Worried')
+ 'afraid': 'Afraid',
+ 'amazed': 'Amazed',
+ 'angry': 'Angry',
+ 'amorous': 'Amorous',
+ 'annoyed': 'Annoyed',
+ 'anxious': 'Anxious',
+ 'aroused': 'Aroused',
+ 'ashamed': 'Ashamed',
+ 'bored': 'Bored',
+ 'brave': 'Brave',
+ 'calm': 'Calm',
+ 'cautious': 'Cautious',
+ 'cold': 'Cold',
+ 'confident': 'Confident',
+ 'confused': 'Confused',
+ 'contemplative': 'Contemplative',
+ 'contented': 'Contented',
+ 'cranky': 'Cranky',
+ 'crazy': 'Crazy',
+ 'creative': 'Creative',
+ 'curious': 'Curious',
+ 'dejected': 'Dejected',
+ 'depressed': 'Depressed',
+ 'disappointed': 'Disappointed',
+ 'disgusted': 'Disgusted',
+ 'dismayed': 'Dismayed',
+ 'distracted': 'Distracted',
+ 'embarrassed': 'Embarrassed',
+ 'envious': 'Envious',
+ 'excited': 'Excited',
+ 'flirtatious': 'Flirtatious',
+ 'frustrated': 'Frustrated',
+ 'grumpy': 'Grumpy',
+ 'guilty': 'Guilty',
+ 'happy': 'Happy',
+ 'hopeful': 'Hopeful',
+ 'hot': 'Hot',
+ 'humbled': 'Humbled',
+ 'humiliated': 'Humiliated',
+ 'hungry': 'Hungry',
+ 'hurt': 'Hurt',
+ 'impressed': 'Impressed',
+ 'in_awe': 'In awe',
+ 'in_love': 'In love',
+ 'indignant': 'Indignant',
+ 'interested': 'Interested',
+ 'intoxicated': 'Intoxicated',
+ 'invincible': 'Invincible',
+ 'jealous': 'Jealous',
+ 'lonely': 'Lonely',
+ 'lucky': 'Lucky',
+ 'mean': 'Mean',
+ 'moody': 'Moody',
+ 'nervous': 'Nervous',
+ 'neutral': 'Neutral',
+ 'offended': 'Offended',
+ 'outraged': 'Outraged',
+ 'playful': 'Playful',
+ 'proud': 'Proud',
+ 'relaxed': 'Relaxed',
+ 'relieved': 'Relieved',
+ 'remorseful': 'Remorseful',
+ 'restless': 'Restless',
+ 'sad': 'Sad',
+ 'sarcastic': 'Sarcastic',
+ 'serious': 'Serious',
+ 'shocked': 'Shocked',
+ 'shy': 'Shy',
+ 'sick': 'Sick',
+ 'sleepy': 'Sleepy',
+ 'spontaneous': 'Spontaneous',
+ 'stressed': 'Stressed',
+ 'strong': 'Strong',
+ 'surprised': 'Surprised',
+ 'thankful': 'Thankful',
+ 'thirsty': 'Thirsty',
+ 'tired': 'Tired',
+ 'undefined': 'Undefined',
+ 'weak': 'Weak',
+ 'worried': 'Worried'
}
@@ -93,131 +91,131 @@ MOODS = {
ACTIVITIES = {
'doing_chores': {
- 'category': _('Doing_chores'),
-
- 'buying_groceries': _('Buying groceries'),
- 'cleaning': _('Cleaning'),
- 'cooking': _('Cooking'),
- 'doing_maintenance': _('Doing maintenance'),
- 'doing_the_dishes': _('Doing the dishes'),
- 'doing_the_laundry': _('Doing the laundry'),
- 'gardening': _('Gardening'),
- 'running_an_errand': _('Running an errand'),
- 'walking_the_dog': _('Walking the dog'),
- 'other': _('Other'),
+ 'category': 'Doing_chores',
+
+ 'buying_groceries': 'Buying groceries',
+ 'cleaning': 'Cleaning',
+ 'cooking': 'Cooking',
+ 'doing_maintenance': 'Doing maintenance',
+ 'doing_the_dishes': 'Doing the dishes',
+ 'doing_the_laundry': 'Doing the laundry',
+ 'gardening': 'Gardening',
+ 'running_an_errand': 'Running an errand',
+ 'walking_the_dog': 'Walking the dog',
+ 'other': 'Other',
},
'drinking': {
- 'category': _('Drinking'),
+ 'category': 'Drinking',
- 'having_a_beer': _('Having a beer'),
- 'having_coffee': _('Having coffee'),
- 'having_tea': _('Having tea'),
- 'other': _('Other'),
+ 'having_a_beer': 'Having a beer',
+ 'having_coffee': 'Having coffee',
+ 'having_tea': 'Having tea',
+ 'other': 'Other',
},
'eating': {
- 'category':_('Eating'),
+ 'category':'Eating',
- 'having_breakfast': _('Having breakfast'),
- 'having_a_snack': _('Having a snack'),
- 'having_dinner': _('Having dinner'),
- 'having_lunch': _('Having lunch'),
- 'other': _('Other'),
+ 'having_breakfast': 'Having breakfast',
+ 'having_a_snack': 'Having a snack',
+ 'having_dinner': 'Having dinner',
+ 'having_lunch': 'Having lunch',
+ 'other': 'Other',
},
'exercising': {
- 'category': _('Exercising'),
-
- 'cycling': _('Cycling'),
- 'dancing': _('Dancing'),
- 'hiking': _('Hiking'),
- 'jogging': _('Jogging'),
- 'playing_sports': _('Playing sports'),
- 'running': _('Running'),
- 'skiing': _('Skiing'),
- 'swimming': _('Swimming'),
- 'working_out': _('Working out'),
- 'other': _('Other'),
+ 'category': 'Exercising',
+
+ 'cycling': 'Cycling',
+ 'dancing': 'Dancing',
+ 'hiking': 'Hiking',
+ 'jogging': 'Jogging',
+ 'playing_sports': 'Playing sports',
+ 'running': 'Running',
+ 'skiing': 'Skiing',
+ 'swimming': 'Swimming',
+ 'working_out': 'Working out',
+ 'other': 'Other',
},
'grooming': {
- 'category': _('Grooming'),
-
- 'at_the_spa': _('At the spa'),
- 'brushing_teeth': _('Brushing teeth'),
- 'getting_a_haircut': _('Getting a haircut'),
- 'shaving': _('Shaving'),
- 'taking_a_bath': _('Taking a bath'),
- 'taking_a_shower': _('Taking a shower'),
- 'other': _('Other'),
+ 'category': 'Grooming',
+
+ 'at_the_spa': 'At the spa',
+ 'brushing_teeth': 'Brushing teeth',
+ 'getting_a_haircut': 'Getting a haircut',
+ 'shaving': 'Shaving',
+ 'taking_a_bath': 'Taking a bath',
+ 'taking_a_shower': 'Taking a shower',
+ 'other': 'Other',
},
'having_appointment': {
- 'category': _('Having appointment'),
+ 'category': 'Having appointment',
- 'other': _('Other'),
+ 'other': 'Other',
},
'inactive': {
- 'category': _('Inactive'),
-
- 'day_off': _('Day_off'),
- 'hanging_out': _('Hanging out'),
- 'hiding': _('Hiding'),
- 'on_vacation': _('On vacation'),
- 'praying': _('Praying'),
- 'scheduled_holiday': _('Scheduled holiday'),
- 'sleeping': _('Sleeping'),
- 'thinking': _('Thinking'),
- 'other': _('Other'),
+ 'category': 'Inactive',
+
+ 'day_off': 'Day_off',
+ 'hanging_out': 'Hanging out',
+ 'hiding': 'Hiding',
+ 'on_vacation': 'On vacation',
+ 'praying': 'Praying',
+ 'scheduled_holiday': 'Scheduled holiday',
+ 'sleeping': 'Sleeping',
+ 'thinking': 'Thinking',
+ 'other': 'Other',
},
'relaxing': {
- 'category': _('Relaxing'),
-
- 'fishing': _('Fishing'),
- 'gaming': _('Gaming'),
- 'going_out': _('Going out'),
- 'partying': _('Partying'),
- 'reading': _('Reading'),
- 'rehearsing': _('Rehearsing'),
- 'shopping': _('Shopping'),
- 'smoking': _('Smoking'),
- 'socializing': _('Socializing'),
- 'sunbathing': _('Sunbathing'),
- 'watching_a_movie': _('Watching a movie'),
- 'watching_tv': _('Watching tv'),
- 'other': _('Other'),
+ 'category': 'Relaxing',
+
+ 'fishing': 'Fishing',
+ 'gaming': 'Gaming',
+ 'going_out': 'Going out',
+ 'partying': 'Partying',
+ 'reading': 'Reading',
+ 'rehearsing': 'Rehearsing',
+ 'shopping': 'Shopping',
+ 'smoking': 'Smoking',
+ 'socializing': 'Socializing',
+ 'sunbathing': 'Sunbathing',
+ 'watching_a_movie': 'Watching a movie',
+ 'watching_tv': 'Watching tv',
+ 'other': 'Other',
},
'talking': {
- 'category': _('Talking'),
+ 'category': 'Talking',
- 'in_real_life': _('In real life'),
- 'on_the_phone': _('On the phone'),
- 'on_video_phone': _('On video phone'),
- 'other': _('Other'),
+ 'in_real_life': 'In real life',
+ 'on_the_phone': 'On the phone',
+ 'on_video_phone': 'On video phone',
+ 'other': 'Other',
},
'traveling': {
- 'category': _('Traveling'),
-
- 'commuting': _('Commuting'),
- 'driving': _('Driving'),
- 'in_a_car': _('In a car'),
- 'on_a_bus': _('On a bus'),
- 'on_a_plane': _('On a plane'),
- 'on_a_train': _('On a train'),
- 'on_a_trip': _('On a trip'),
- 'walking': _('Walking'),
- 'cycling': _('Cycling'),
- 'other': _('Other'),
+ 'category': 'Traveling',
+
+ 'commuting': 'Commuting',
+ 'driving': 'Driving',
+ 'in_a_car': 'In a car',
+ 'on_a_bus': 'On a bus',
+ 'on_a_plane': 'On a plane',
+ 'on_a_train': 'On a train',
+ 'on_a_trip': 'On a trip',
+ 'walking': 'Walking',
+ 'cycling': 'Cycling',
+ 'other': 'Other',
},
'undefined': {
- 'category': _('Undefined'),
+ 'category': 'Undefined',
- 'other': _('Other'),
+ 'other': 'Other',
},
'working': {
- 'category': _('Working'),
+ 'category': 'Working',
- 'coding': _('Coding'),
- 'in_a_meeting': _('In a meeting'),
- 'writing': _('Writing'),
- 'studying': _('Studying'),
- 'other': _('Other'),
+ 'coding': 'Coding',
+ 'in_a_meeting': 'In a meeting',
+ 'writing': 'Writing',
+ 'studying': 'Studying',
+ 'other': 'Other',
}
}
diff --git a/src/plugin.py b/src/plugin.py
index eb2a89e3..eca6baf2 100644
--- a/src/plugin.py
+++ b/src/plugin.py
@@ -19,12 +19,12 @@ class PluginConfig(config.Config):
They are accessible inside the plugin with self.config
and behave like the core Config object.
"""
- def __init__(self, filename, module_name):
- config.Config.__init__(self, filename)
+ def __init__(self, filename, module_name, default=None):
+ config.Config.__init__(self, filename, default=default)
self.module_name = module_name
self.read()
- def get(self, option, default, section=None):
+ def get(self, option, default=None, section=None):
if not section:
section = self.module_name
return config.Config.get(self, option, default, section)
@@ -80,6 +80,7 @@ class SafetyMetaclass(type):
if inspect.stack()[1][1] == inspect.getfile(f):
raise
elif SafetyMetaclass.core:
+ log.error('Error in a plugin', exc_info=True)
SafetyMetaclass.core.information(traceback.format_exc())
return None
return helper
@@ -364,12 +365,19 @@ class BasePlugin(object, metaclass=SafetyMetaclass):
Class that all plugins derive from.
"""
+ default_config = None
+
def __init__(self, plugin_api, core, plugins_conf_dir):
self.core = core
# More hack; luckily we'll never have more than one core object
SafetyMetaclass.core = core
conf = os.path.join(plugins_conf_dir, self.__module__+'.cfg')
- self.config = PluginConfig(conf, self.__module__)
+ try:
+ self.config = PluginConfig(conf, self.__module__,
+ default=self.default_config)
+ except Exception:
+ log.debug('Error while creating the plugin config', exc_info=True)
+ self.config = PluginConfig(conf, self.__module__)
self._api = plugin_api[self.name]
self.init()
diff --git a/src/plugin_manager.py b/src/plugin_manager.py
index d4cc7384..549753a9 100644
--- a/src/plugin_manager.py
+++ b/src/plugin_manager.py
@@ -5,12 +5,9 @@ the API together. Defines also a bunch of variables related to the
plugin env.
"""
-import imp
import os
from os import path
import logging
-from gettext import gettext as _
-from sys import version_info
import core
import tabs
@@ -44,9 +41,8 @@ class PluginManager(object):
self.tab_keys = {}
self.roster_elements = {}
- if version_info[1] >= 3: # 3.3 & >
- from importlib import machinery
- self.finder = machinery.PathFinder()
+ from importlib import machinery
+ self.finder = machinery.PathFinder()
self.initial_set_plugins_dir()
self.initial_set_plugins_conf_dir()
@@ -70,29 +66,16 @@ class PluginManager(object):
try:
module = None
- if version_info[1] < 3: # < 3.3
- if name in self.modules:
- imp.acquire_lock()
- module = imp.reload(self.modules[name])
- else:
- file, filename, info = imp.find_module(name,
- self.load_path)
- imp.acquire_lock()
- module = imp.load_module(name, file, filename, info)
- else: # 3.3 & >
- loader = self.finder.find_module(name, self.load_path)
- if not loader:
- self.core.information('Could not find plugin: %s' % name)
- return
- module = loader.load_module()
-
+ loader = self.finder.find_module(name, self.load_path)
+ if not loader:
+ self.core.information('Could not find plugin: %s' % name)
+ return
+ module = loader.load_module()
except Exception as e:
log.debug("Could not load plugin %s", name, exc_info=True)
self.core.information("Could not load plugin %s: %s" % (name, e),
'Error')
finally:
- if version_info[1] < 3 and imp.lock_held():
- imp.release_lock()
if not module:
return
@@ -109,8 +92,8 @@ class PluginManager(object):
except Exception as e:
log.error('Error while loading the plugin %s', name, exc_info=True)
if notify:
- self.core.information(_('Unable to load the plugin %s: %s') %
- (name, e),
+ self.core.information('Unable to load the plugin %s: %s' %
+ (name, e),
'Error')
self.unload(name, notify=False)
else:
@@ -147,8 +130,8 @@ class PluginManager(object):
self.core.information('Plugin %s unloaded' % name, 'Info')
except Exception as e:
log.debug("Could not unload plugin %s", name, exc_info=True)
- self.core.information(_("Could not unload plugin %s: %s") %
- (name, e),
+ self.core.information("Could not unload plugin %s: %s" %
+ (name, e),
'Error')
def add_command(self, module_name, name, handler, help,
@@ -157,7 +140,7 @@ class PluginManager(object):
Add a global command.
"""
if name in self.core.commands:
- raise Exception(_("Command '%s' already exists") % (name,))
+ raise Exception("Command '%s' already exists" % (name,))
commands = self.commands[module_name]
commands[name] = core.Command(handler, help, completion, short, usage)
@@ -244,7 +227,7 @@ class PluginManager(object):
already exists.
"""
if key in self.core.key_func:
- raise Exception(_("Key '%s' already exists") % (key,))
+ raise Exception("Key '%s' already exists" % (key,))
keys = self.keys[module_name]
keys[key] = handler
self.core.key_func[key] = handler
@@ -295,7 +278,7 @@ class PluginManager(object):
except:
pass
except OSError as e:
- self.core.information(_('Completion failed: %s' % e), 'Error')
+ self.core.information('Completion failed: %s' % e, 'Error')
return
plugins_files = [name[:-3] for name in names if name.endswith('.py')
and name != '__init__.py' and not name.startswith('.')]
diff --git a/src/poezio.py b/src/poezio.py
index 9a26e135..7a83f510 100644
--- a/src/poezio.py
+++ b/src/poezio.py
@@ -19,6 +19,29 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
import singleton
+def test_curses():
+ """
+ Check if the system ncurses linked with python has unicode capabilities.
+ """
+ import curses
+ if hasattr(curses, 'unget_wch'):
+ return True
+ print("""\
+ERROR: The current python executable is linked with a ncurses version that \
+has no unicode capabilities.
+
+This could mean that:
+ - python was built on a system where readline is linked against \
+libncurses and not libncursesw
+ - python was built without ncursesw headers available
+
+Please file a bug for your distribution or fix that on your system and then \
+recompile python.
+Poezio is currently unable to read your input or draw its interface properly,\
+ so it will now exit.""")
+ return False
+
+
def main():
"""
Enter point
@@ -36,6 +59,10 @@ def main():
from config import options
+ if options.check_config:
+ config.check_config()
+ sys.exit(0)
+
import theming
theming.update_themes_dir()
@@ -75,4 +102,7 @@ def main():
pass
if __name__ == '__main__':
- main()
+ if test_curses():
+ main()
+ else:
+ sys.exit(1)
diff --git a/src/tabs/__init__.py b/src/tabs/__init__.py
index eaf41a2f..d0a881a6 100644
--- a/src/tabs/__init__.py
+++ b/src/tabs/__init__.py
@@ -10,3 +10,4 @@ from . listtab import ListTab
from . muclisttab import MucListTab
from . adhoc_commands_list import AdhocCommandsListTab
from . data_forms import DataFormsTab
+from . bookmarkstab import BookmarksTab
diff --git a/src/tabs/adhoc_commands_list.py b/src/tabs/adhoc_commands_list.py
index 7f5abf6a..10ebf22b 100644
--- a/src/tabs/adhoc_commands_list.py
+++ b/src/tabs/adhoc_commands_list.py
@@ -4,8 +4,6 @@ select one of them and start executing it, or just close the tab and do
nothing.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -20,7 +18,7 @@ class AdhocCommandsListTab(ListTab):
def __init__(self, jid):
ListTab.__init__(self, jid.full,
"“Enter”: execute selected command.",
- _('Ad-hoc commands of JID %s (Loading)') % jid,
+ 'Ad-hoc commands of JID %s (Loading)' % jid,
(('Node', 0), ('Description', 1)))
self.key_func['^M'] = self.execute_selected_command
@@ -50,7 +48,7 @@ class AdhocCommandsListTab(ListTab):
yield item
items = [(item['node'], item['name'] or '', item['jid']) for item in get_items()]
self.listview.set_lines(items)
- self.info_header.message = _('Ad-hoc commands of JID %s') % self.name
+ self.info_header.message = 'Ad-hoc commands of JID %s' % self.name
if self.core.current_tab() is self:
self.refresh()
else:
diff --git a/src/tabs/basetabs.py b/src/tabs/basetabs.py
index 0a55640c..30ddf239 100644
--- a/src/tabs/basetabs.py
+++ b/src/tabs/basetabs.py
@@ -13,8 +13,6 @@ This module also defines ChatTabs, the parent class for all tabs
revolving around chats.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -35,7 +33,7 @@ from decorators import refresh_wrapper
from logger import logger
from text_buffer import TextBuffer
from theming import get_theme, dump_tuple
-
+from decorators import command_args_parser
# getters for tab colors (lambdas, so that they are dynamic)
STATE_COLORS = {
@@ -254,7 +252,6 @@ class Tab(object):
return False # There's no completion function
else:
return command[2](the_input)
- return True
return False
def execute_command(self, provided_text):
@@ -282,14 +279,15 @@ class Tab(object):
if self.missing_command_callback is not None:
error_handled = self.missing_command_callback(low)
if not error_handled:
- self.core.information(_("Unknown command (%s)") %
- (command),
- _('Error'))
+ self.core.information("Unknown command (%s)" %
+ (command),
+ 'Error')
if command in ('correct', 'say'): # hack
arg = xhtml.convert_simple_to_full_colors(arg)
else:
arg = xhtml.clean_text_simple(arg)
if func:
+ self.input.reset_completion()
func(arg)
return True
else:
@@ -455,16 +453,16 @@ class ChatTab(Tab):
self.key_func['M-/'] = self.last_words_completion
self.key_func['^M'] = self.on_enter
self.register_command('say', self.command_say,
- usage=_('<message>'),
- shortdesc=_('Send the message.'))
+ usage='<message>',
+ shortdesc='Send the message.')
self.register_command('xhtml', self.command_xhtml,
- usage=_('<custom xhtml>'),
- shortdesc=_('Send custom XHTML.'))
+ usage='<custom xhtml>',
+ shortdesc='Send custom XHTML.')
self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the current buffer.'))
+ shortdesc='Clear the current buffer.')
self.register_command('correct', self.command_correct,
- desc=_('Fix the last message with whatever you want.'),
- shortdesc=_('Correct the last message.'),
+ desc='Fix the last message with whatever you want.',
+ shortdesc='Correct the last message.',
completion=self.completion_correct)
self.chat_state = None
self.update_commands()
@@ -492,7 +490,7 @@ class ChatTab(Tab):
"""
name = safeJID(self.name).bare
if not logger.log_message(name, nickname, txt, date=time, typ=typ):
- self.core.information(_('Unable to write in the log file'), 'Error')
+ self.core.information('Unable to write in the log file', 'Error')
def add_message(self, txt, time=None, nickname=None, forced_user=None,
nick_color=None, identifier=None, jid=None, history=None,
@@ -544,11 +542,12 @@ class ChatTab(Tab):
self.command_say(xhtml.convert_simple_to_full_colors(txt))
self.cancel_paused_delay()
- def command_xhtml(self, arg):
+ @command_args_parser.raw
+ def command_xhtml(self, xhtml):
""""
/xhtml <custom xhtml>
"""
- message = self.generate_xhtml_message(arg)
+ message = self.generate_xhtml_message(xhtml)
if message:
message.send()
@@ -573,7 +572,7 @@ class ChatTab(Tab):
return self.name
@refresh_wrapper.always
- def command_clear(self, args):
+ def command_clear(self, ignored):
"""
/clear
"""
@@ -637,6 +636,7 @@ class ChatTab(Tab):
self.core.remove_timed_event(self.timed_event_paused)
self.timed_event_paused = None
+ @command_args_parser.raw
def command_correct(self, line):
"""
/correct <fixed message>
@@ -645,7 +645,7 @@ class ChatTab(Tab):
self.core.command_help('correct')
return
if not self.last_sent_message:
- self.core.information(_('There is no message to correct.'))
+ self.core.information('There is no message to correct.')
return
self.command_say(line, correct=True)
@@ -672,6 +672,7 @@ class ChatTab(Tab):
if self.text_win.pos != 0:
self.state = 'scrolled'
+ @command_args_parser.raw
def command_say(self, line, correct=False):
pass
@@ -707,20 +708,67 @@ class OneToOneTab(ChatTab):
# change this to True or False when
# we know that the remote user wants chatstates, or not.
# None means we don’t know yet, and we send only "active" chatstates
- self.remote_wants_chatstates = None
+ self._remote_wants_chatstates = None
self.remote_supports_attention = True
self.remote_supports_receipts = True
self.check_features()
- def ack_message(self, msg_id):
+ @property
+ def remote_wants_chatstates(self):
+ return self._remote_wants_chatstates
+
+ @remote_wants_chatstates.setter
+ def remote_wants_chatstates(self, value):
+ old_value = self._remote_wants_chatstates
+ self._remote_wants_chatstates = value
+ if (old_value is None and value != None) or \
+ (old_value != value and value != None):
+ ok = get_theme().CHAR_OK
+ nope = get_theme().CHAR_EMPTY
+ support = ok if value else nope
+ if value:
+ msg = '\x19%s}Contact supports chat states [%s].'
+ else:
+ msg = '\x19%s}Contact does not support chat states [%s].'
+ color = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
+ msg = msg % (color, support)
+ self.add_message(msg, typ=0)
+ self.core.refresh_window()
+
+ def ack_message(self, msg_id, msg_jid):
"""
Ack a message
"""
- new_msg = self._text_buffer.ack_message(msg_id)
+ new_msg = self._text_buffer.ack_message(msg_id, msg_jid)
if new_msg:
self.text_win.modify_message(msg_id, new_msg)
self.core.refresh_window()
+ def nack_message(self, error, msg_id, msg_jid):
+ """
+ Ack a message
+ """
+ new_msg = self._text_buffer.nack_message(error, msg_id, msg_jid)
+ if new_msg:
+ self.text_win.modify_message(msg_id, new_msg)
+ self.core.refresh_window()
+ return True
+ return False
+
+ @command_args_parser.raw
+ def command_xhtml(self, xhtml_data):
+ message = self.generate_xhtml_message(xhtml_data)
+ if message:
+ if self.remote_supports_receipts:
+ message._add_receipt = True
+ if self.remote_wants_chatstates:
+ message['chat_sate'] = 'active'
+ message.send()
+ body = xhtml.xhtml_to_poezio_colors(xhtml_data, force=True)
+ self._text_buffer.add_message(body, nickname=self.core.own_nick,
+ identifier=message['id'],)
+ self.refresh()
+
def check_features(self):
"check the features supported by the other party"
if safeJID(self.get_dest_jid()).resource:
@@ -728,8 +776,9 @@ class OneToOneTab(ChatTab):
jid=self.get_dest_jid(), timeout=5,
callback=self.features_checked)
- def command_attention(self, message=''):
- "/attention [message]"
+ @command_args_parser.raw
+ def command_attention(self, message):
+ """/attention [message]"""
if message is not '':
self.command_say(message, attention=True)
else:
@@ -738,6 +787,7 @@ class OneToOneTab(ChatTab):
msg['attention'] = True
msg.send()
+ @command_args_parser.raw
def command_say(self, line, correct=False, attention=False):
pass
@@ -746,11 +796,11 @@ class OneToOneTab(ChatTab):
return False
if command_name == 'correct':
- feature = _('message correction')
+ feature = 'message correction'
elif command_name == 'attention':
- feature = _('attention requests')
- msg = _('%s does not support %s, therefore the /%s '
- 'command is currently disabled in this tab.')
+ feature = 'attention requests'
+ msg = ('%s does not support %s, therefore the /%s '
+ 'command is currently disabled in this tab.')
msg = msg % (self.name, feature, command_name)
self.core.information(msg, 'Info')
return True
@@ -760,11 +810,11 @@ class OneToOneTab(ChatTab):
if 'urn:xmpp:attention:0' in features:
self.remote_supports_attention = True
self.register_command('attention', self.command_attention,
- usage=_('[message]'),
- shortdesc=_('Request the attention.'),
- desc=_('Attention: Request the attention of '
- 'the contact. Can also send a message'
- ' along with the attention.'))
+ usage='[message]',
+ shortdesc='Request the attention.',
+ desc='Attention: Request the attention of '
+ 'the contact. Can also send a message'
+ ' along with the attention.')
else:
self.remote_supports_attention = False
return self.remote_supports_attention
@@ -776,8 +826,8 @@ class OneToOneTab(ChatTab):
del self.commands['correct']
elif not 'correct' in self.commands:
self.register_command('correct', self.command_correct,
- desc=_('Fix the last message with whatever you want.'),
- shortdesc=_('Correct the last message.'),
+ desc='Fix the last message with whatever you want.',
+ shortdesc='Correct the last message.',
completion=self.completion_correct)
return 'correct' in self.commands
@@ -814,8 +864,8 @@ class OneToOneTab(ChatTab):
attention = ok if attention else nope
receipts = ok if receipts else nope
- msg = _('\x19%s}Contact supports: correction [%s], '
- 'attention [%s], receipts [%s].')
+ msg = ('\x19%s}Contact supports: correction [%s], '
+ 'attention [%s], receipts [%s].')
color = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
msg = msg % (color, correct, attention, receipts)
self.add_message(msg, typ=0)
diff --git a/src/tabs/bookmarkstab.py b/src/tabs/bookmarkstab.py
new file mode 100644
index 00000000..7f5069ea
--- /dev/null
+++ b/src/tabs/bookmarkstab.py
@@ -0,0 +1,145 @@
+"""
+Defines the data-forms Tab
+"""
+
+import logging
+log = logging.getLogger(__name__)
+
+import windows
+from bookmarks import Bookmark, BookmarkList, stanza_storage
+from tabs import Tab
+from common import safeJID
+
+
+class BookmarksTab(Tab):
+ """
+ A tab displaying lines of bookmarks, each bookmark having
+ a 4 widgets to set the jid/password/autojoin/storage method
+ """
+ plugin_commands = {}
+ def __init__(self, bookmarks: BookmarkList):
+ Tab.__init__(self)
+ self.name = "Bookmarks"
+ self.bookmarks = bookmarks
+ self.new_bookmarks = []
+ self.removed_bookmarks = []
+ self.header_win = windows.ColumnHeaderWin(('room@server/nickname',
+ 'password',
+ 'autojoin',
+ 'storage'))
+ self.bookmarks_win = windows.BookmarksWin(self.bookmarks,
+ self.height-4,
+ self.width, 1, 0)
+ self.help_win = windows.HelpText('Ctrl+Y: save, Ctrl+G: cancel, '
+ '↑↓: change lines, tab: change '
+ 'column, M-a: add bookmark, C-k'
+ ': delete bookmark')
+ self.info_header = windows.BookmarksInfoWin()
+ self.key_func['KEY_UP'] = self.bookmarks_win.go_to_previous_line_input
+ self.key_func['KEY_DOWN'] = self.bookmarks_win.go_to_next_line_input
+ self.key_func['^I'] = self.bookmarks_win.go_to_next_horizontal_input
+ self.key_func['^G'] = self.on_cancel
+ self.key_func['^Y'] = self.on_save
+ self.key_func['M-a'] = self.add_bookmark
+ self.key_func['^K'] = self.del_bookmark
+ self.resize()
+ self.update_commands()
+
+ def add_bookmark(self):
+ new_bookmark = Bookmark(safeJID('room@example.tld/nick'), method='local')
+ self.new_bookmarks.append(new_bookmark)
+ self.bookmarks_win.add_bookmark(new_bookmark)
+
+ def del_bookmark(self):
+ current = self.bookmarks_win.del_current_bookmark()
+ if current in self.new_bookmarks:
+ self.new_bookmarks.remove(current)
+ else:
+ self.removed_bookmarks.append(current)
+
+ def on_cancel(self):
+ self.core.close_tab()
+ return True
+
+ def on_save(self):
+ self.bookmarks_win.save()
+ if find_duplicates(self.new_bookmarks):
+ self.core.information('Duplicate bookmarks in list (saving aborted)', 'Error')
+ return
+ for bm in self.new_bookmarks:
+ if safeJID(bm.jid):
+ if not self.bookmarks[bm.jid]:
+ self.bookmarks.append(bm)
+ else:
+ self.core.information('Invalid JID for bookmark: %s/%s' % (bm.jid, bm.nick), 'Error')
+ return
+
+ for bm in self.removed_bookmarks:
+ if bm in self.bookmarks:
+ self.bookmarks.remove(bm)
+
+ def send_cb(success):
+ if success:
+ self.core.information('Bookmarks saved.', 'Info')
+ else:
+ self.core.information('Remote bookmarks not saved.', 'Error')
+ log.debug('alerte %s', str(stanza_storage(self.bookmarks.bookmarks)))
+ self.bookmarks.save(self.core.xmpp, callback=send_cb)
+ self.core.close_tab()
+ return True
+
+ def on_input(self, key, raw=False):
+ if key in self.key_func:
+ res = self.key_func[key]()
+ if res:
+ return res
+ self.bookmarks_win.refresh_current_input()
+ else:
+ self.bookmarks_win.on_input(key)
+
+ def resize(self):
+ self.need_resize = False
+ self.header_win.resize_columns({
+ 'room@server/nickname': self.width//3,
+ 'password': self.width//3,
+ 'autojoin': self.width//6,
+ 'storage': self.width//6
+ })
+ info_height = self.core.information_win_size
+ tab_height = Tab.tab_win_height()
+ self.header_win.resize(1, self.width, 0, 0)
+ self.bookmarks_win.resize(self.height - 3 - tab_height - info_height,
+ self.width, 1, 0)
+ self.help_win.resize(1, self.width, self.height - 1, 0)
+ self.info_header.resize(1, self.width,
+ self.height - 2 - tab_height - info_height, 0)
+
+ def on_info_win_size_changed(self):
+ if self.core.information_win_size >= self.height - 3:
+ return
+ info_height = self.core.information_win_size
+ tab_height = Tab.tab_win_height()
+ self.bookmarks_win.resize(self.height - 3 - tab_height - info_height,
+ self.width, 1, 0)
+ self.info_header.resize(1, self.width,
+ self.height - 2 - tab_height - info_height, 0)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ self.header_win.refresh()
+ self.refresh_tab_win()
+ self.help_win.refresh()
+ self.info_header.refresh(self.bookmarks.preferred)
+ self.info_win.refresh()
+ self.bookmarks_win.refresh()
+
+
+def find_duplicates(bm_list):
+ jids = set()
+ for bookmark in bm_list:
+ if bookmark.jid in jids:
+ return True
+ jids.add(bookmark.jid)
+ return False
+
diff --git a/src/tabs/conversationtab.py b/src/tabs/conversationtab.py
index 52c503d7..1d8c60a4 100644
--- a/src/tabs/conversationtab.py
+++ b/src/tabs/conversationtab.py
@@ -11,8 +11,6 @@ There are two different instances of a ConversationTab:
the time.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -29,6 +27,7 @@ from config import config
from decorators import refresh_wrapper
from roster import roster
from theming import get_theme, dump_tuple
+from decorators import command_args_parser
class ConversationTab(OneToOneTab):
"""
@@ -53,18 +52,18 @@ class ConversationTab(OneToOneTab):
self.key_func['^I'] = self.completion
# commands
self.register_command('unquery', self.command_unquery,
- shortdesc=_('Close the tab.'))
+ shortdesc='Close the tab.')
self.register_command('close', self.command_unquery,
- shortdesc=_('Close the tab.'))
+ shortdesc='Close the tab.')
self.register_command('version', self.command_version,
- desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
- shortdesc=_('Get the software version of the user.'))
+ desc='Get the software version of the current interlocutor (usually its XMPP client and Operating System).',
+ shortdesc='Get the software version of the user.')
self.register_command('info', self.command_info,
- shortdesc=_('Get the status of the contact.'))
+ shortdesc='Get the status of the contact.')
self.register_command('last_activity', self.command_last_activity,
- usage=_('[jid]'),
- desc=_('Get the last activity of the given or the current contact.'),
- shortdesc=_('Get the activity.'),
+ usage='[jid]',
+ desc='Get the last activity of the given or the current contact.',
+ shortdesc='Get the activity.',
completion=self.core.completion_last_activity)
self.resize()
self.update_commands()
@@ -88,6 +87,7 @@ class ConversationTab(OneToOneTab):
def completion(self):
self.complete_commands(self.input)
+ @command_args_parser.raw
def command_say(self, line, attention=False, correct=False):
msg = self.core.xmpp.make_message(self.get_dest_jid())
msg['type'] = 'chat'
@@ -149,19 +149,13 @@ class ConversationTab(OneToOneTab):
self.text_win.refresh()
self.input.refresh()
- def command_xhtml(self, arg):
- message = self.generate_xhtml_message(arg)
- if message:
- message.send()
- self.core.add_message_to_text_buffer(self._text_buffer, message['body'], None, self.core.own_nick)
- self.refresh()
-
- def command_last_activity(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_last_activity(self, args):
"""
- /activity [jid]
+ /last_activity [jid]
"""
- if arg.strip():
- return self.core.command_last_activity(arg)
+ if args and args[0]:
+ return self.core.command_last_activity(args[0])
def callback(iq):
if iq['type'] != 'result':
@@ -188,10 +182,11 @@ class ConversationTab(OneToOneTab):
self.add_message(msg)
self.core.refresh_window()
- self.core.xmpp.plugin['xep_0012'].get_last_activity(self.general_jid, callback=callback)
+ self.core.xmpp.plugin['xep_0012'].get_last_activity(self.get_dest_jid(), callback=callback)
@refresh_wrapper.conditional
- def command_info(self, arg):
+ @command_args_parser.ignored
+ def command_info(self):
contact = roster[self.get_dest_jid()]
jid = safeJID(self.get_dest_jid())
if contact:
@@ -202,7 +197,7 @@ class ConversationTab(OneToOneTab):
else:
resource = None
if resource:
- status = (_('Status: %s') % resource.status) if resource.status else ''
+ status = ('Status: %s' % resource.status) if resource.status else ''
self._text_buffer.add_message("\x19%(info_col)s}Show: %(show)s, %(status)s\x19o" % {
'show': resource.show or 'available', 'status': status, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
return True
@@ -210,23 +205,25 @@ class ConversationTab(OneToOneTab):
self._text_buffer.add_message("\x19%(info_col)s}No information available\x19o" % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
return True
- def command_unquery(self, arg):
+ @command_args_parser.ignored
+ def command_unquery(self):
self.core.close_tab()
- def command_version(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_version(self, args):
"""
- /version
+ /version [jid]
"""
def callback(res):
if not res:
return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
version = '%s is running %s version %s on %s' % (jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
+ res.get('name') or 'an unknown software',
+ res.get('version') or 'unknown',
+ res.get('os') or 'an unknown platform')
self.core.information(version, 'Info')
- if arg:
- return self.core.command_version(arg)
+ if args:
+ return self.core.command_version(args[0])
jid = safeJID(self.name)
if not jid.resource:
if jid in roster:
@@ -381,7 +378,7 @@ class DynamicConversationTab(ConversationTab):
self.info_header = windows.DynamicConversationInfoWin()
ConversationTab.__init__(self, jid)
self.register_command('unlock', self.unlock_command,
- shortdesc=_('Unlock the conversation from a particular resource.'))
+ shortdesc='Unlock the conversation from a particular resource.')
def lock(self, resource):
"""
@@ -393,8 +390,8 @@ class DynamicConversationTab(ConversationTab):
info = '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
jid_c = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID)
- message = _('%(info)sConversation locked to '
- '%(jid_c)s%(jid)s/%(resource)s%(info)s.') % {
+ message = ('%(info)sConversation locked to '
+ '%(jid_c)s%(jid)s/%(resource)s%(info)s.') % {
'info': info,
'jid_c': jid_c,
'jid': self.name,
@@ -418,14 +415,14 @@ class DynamicConversationTab(ConversationTab):
jid_c = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID)
if from_:
- message = _('%(info)sConversation unlocked (received activity'
- ' from %(jid_c)s%(jid)s%(info)s).') % {
+ message = ('%(info)sConversation unlocked (received activity'
+ ' from %(jid_c)s%(jid)s%(info)s).') % {
'info': info,
'jid_c': jid_c,
'jid': from_}
self.add_message(message, typ=0)
else:
- message = _('%sConversation unlocked.') % info
+ message = '%sConversation unlocked.' % info
self.add_message(message, typ=0)
def get_dest_jid(self):
diff --git a/src/tabs/listtab.py b/src/tabs/listtab.py
index c5aab5eb..7021c8e3 100644
--- a/src/tabs/listtab.py
+++ b/src/tabs/listtab.py
@@ -4,8 +4,6 @@ sortable list. It should be inherited, to actually provide methods that
insert items in the list, and that lets the user interact with them.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -52,7 +50,7 @@ class ListTab(Tab):
self.key_func['KEY_RIGHT'] = self.list_header.sel_column_right
self.key_func[' '] = self.sort_by
self.register_command('close', self.close,
- shortdesc=_('Close this tab.'))
+ shortdesc='Close this tab.')
self.resize()
self.update_keys()
self.update_commands()
@@ -121,7 +119,7 @@ class ListTab(Tab):
"""
If there's an error (retrieving the values etc)
"""
- self._error_message = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code}
+ self._error_message = 'Error: %(code)s - %(msg)s: %(body)s' % {'msg':msg, 'body':body, 'code':code}
self.info_header.message = self._error_message
self.info_header.refresh()
curses.doupdate()
diff --git a/src/tabs/muclisttab.py b/src/tabs/muclisttab.py
index 55d5c2bd..c26fb268 100644
--- a/src/tabs/muclisttab.py
+++ b/src/tabs/muclisttab.py
@@ -4,8 +4,6 @@ A MucListTab is a tab listing the rooms on a conference server.
It has no functionnality except scrolling the list, and allowing the
user to join the rooms.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -22,9 +20,9 @@ class MucListTab(ListTab):
plugin_keys = {}
def __init__(self, server):
- ListTab.__init__(self, server,
+ ListTab.__init__(self, server.full,
"“j”: join room.",
- _('Chatroom list on server %s (Loading)') % server,
+ 'Chatroom list on server %s (Loading)' % server,
(('node-part', 0), ('name', 2), ('users', 3)))
self.key_func['j'] = self.join_selected
self.key_func['J'] = self.join_selected_no_focus
@@ -56,7 +54,7 @@ class MucListTab(ListTab):
item[0],
item[2] or '', '') for item in get_items()]
self.listview.set_lines(items)
- self.info_header.message = _('Chatroom list on server %s') % self.name
+ self.info_header.message = 'Chatroom list on server %s' % self.name
if self.core.current_tab() is self:
self.refresh()
else:
diff --git a/src/tabs/muctab.py b/src/tabs/muctab.py
index 8ac9b7e2..d4b13258 100644
--- a/src/tabs/muctab.py
+++ b/src/tabs/muctab.py
@@ -7,8 +7,6 @@ It keeps track of many things such as part/joins, maintains an
user list, and updates private tabs when necessary.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -29,7 +27,7 @@ import windows
import xhtml
from common import safeJID
from config import config
-from decorators import refresh_wrapper
+from decorators import refresh_wrapper, command_args_parser
from logger import logger
from roster import roster
from theming import get_theme, dump_tuple
@@ -37,11 +35,11 @@ from user import User
SHOW_NAME = {
- 'dnd': _('busy'),
- 'away': _('away'),
- 'xa': _('not available'),
- 'chat': _('chatty'),
- '': _('available')
+ 'dnd': 'busy',
+ 'away': 'away',
+ 'xa': 'not available',
+ 'chat': 'chatty',
+ '': 'available'
}
NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
@@ -55,13 +53,14 @@ class MucTab(ChatTab):
message_type = 'groupchat'
plugin_commands = {}
plugin_keys = {}
- def __init__(self, jid, nick):
+ def __init__(self, jid, nick, password=None):
self.joined = False
ChatTab.__init__(self, jid)
if self.joined == False:
self._state = 'disconnected'
self.own_nick = nick
self.name = jid
+ self.password = password
self.users = []
self.privates = [] # private conversations
self.topic = ''
@@ -88,106 +87,112 @@ class MucTab(ChatTab):
self.key_func['M-p'] = self.go_to_prev_hl
# commands
self.register_command('ignore', self.command_ignore,
- usage=_('<nickname>'),
- desc=_('Ignore a specified nickname.'),
- shortdesc=_('Ignore someone'),
+ usage='<nickname>',
+ desc='Ignore a specified nickname.',
+ shortdesc='Ignore someone',
completion=self.completion_ignore)
self.register_command('unignore', self.command_unignore,
- usage=_('<nickname>'),
- desc=_('Remove the specified nickname from the ignore list.'),
- shortdesc=_('Unignore someone.'),
+ usage='<nickname>',
+ desc='Remove the specified nickname from the ignore list.',
+ shortdesc='Unignore someone.',
completion=self.completion_unignore)
self.register_command('kick', self.command_kick,
- usage=_('<nick> [reason]'),
- desc=_('Kick the user with the specified nickname.'
- ' You also can give an optional reason.'),
- shortdesc=_('Kick someone.'),
+ usage='<nick> [reason]',
+ desc='Kick the user with the specified nickname.'
+ ' You also can give an optional reason.',
+ shortdesc='Kick someone.',
completion=self.completion_quoted)
self.register_command('ban', self.command_ban,
- usage=_('<nick> [reason]'),
- desc=_('Ban the user with the specified nickname.'
- ' You also can give an optional reason.'),
+ usage='<nick> [reason]',
+ desc='Ban the user with the specified nickname.'
+ ' You also can give an optional reason.',
shortdesc='Ban someone',
completion=self.completion_quoted)
self.register_command('role', self.command_role,
- usage=_('<nick> <role> [reason]'),
- desc=_('Set the role of an user. Roles can be:'
- ' none, visitor, participant, moderator.'
- ' You also can give an optional reason.'),
- shortdesc=_('Set the role of an user.'),
+ usage='<nick> <role> [reason]',
+ desc='Set the role of an user. Roles can be:'
+ ' none, visitor, participant, moderator.'
+ ' You also can give an optional reason.',
+ shortdesc='Set the role of an user.',
completion=self.completion_role)
self.register_command('affiliation', self.command_affiliation,
- usage=_('<nick or jid> <affiliation>'),
- desc=_('Set the affiliation of an user. Affiliations can be:'
- ' outcast, none, member, admin, owner.'),
- shortdesc=_('Set the affiliation of an user.'),
+ usage='<nick or jid> <affiliation>',
+ desc='Set the affiliation of an user. Affiliations can be:'
+ ' outcast, none, member, admin, owner.',
+ shortdesc='Set the affiliation of an user.',
completion=self.completion_affiliation)
self.register_command('topic', self.command_topic,
- usage=_('<subject>'),
- desc=_('Change the subject of the room.'),
- shortdesc=_('Change the subject.'),
+ usage='<subject>',
+ desc='Change the subject of the room.',
+ shortdesc='Change the subject.',
completion=self.completion_topic)
self.register_command('query', self.command_query,
- usage=_('<nick> [message]'),
- desc=_('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.'),
- shortdesc=_('Query an user.'),
+ usage='<nick> [message]',
+ desc='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.',
+ shortdesc='Query an user.',
completion=self.completion_quoted)
self.register_command('part', self.command_part,
- usage=_('[message]'),
- desc=_('Disconnect from a room. You can'
- ' specify an optional message.'),
- shortdesc=_('Leave the room.'))
+ usage='[message]',
+ desc='Disconnect from a room. You can'
+ ' specify an optional message.',
+ shortdesc='Leave the room.')
self.register_command('close', self.command_close,
- usage=_('[message]'),
- desc=_('Disconnect from a room and close the tab.'
- ' You can specify an optional message if '
- 'you are still connected.'),
- shortdesc=_('Close the tab.'))
+ usage='[message]',
+ desc='Disconnect from a room and close the tab.'
+ ' You can specify an optional message if '
+ 'you are still connected.',
+ shortdesc='Close the tab.')
self.register_command('nick', self.command_nick,
- usage=_('<nickname>'),
- desc=_('Change your nickname in the current room.'),
- shortdesc=_('Change your nickname.'),
+ usage='<nickname>',
+ desc='Change your nickname in the current room.',
+ shortdesc='Change your nickname.',
completion=self.completion_nick)
self.register_command('recolor', self.command_recolor,
- usage=_('[random]'),
- desc=_('Re-assign a color to all participants of the'
- ' current room, based on the last time they talked.'
- ' Use this if the participants currently talking '
- 'have too many identical colors. Use /recolor random'
- ' for a non-deterministic result.'),
- shortdesc=_('Change the nicks colors.'),
+ usage='[random]',
+ desc='Re-assign a color to all participants of the'
+ ' current room, based on the last time they talked.'
+ ' Use this if the participants currently talking '
+ 'have too many identical colors. Use /recolor random'
+ ' for a non-deterministic result.',
+ shortdesc='Change the nicks colors.',
completion=self.completion_recolor)
+ self.register_command('color', self.command_color,
+ usage='<nick> <color>',
+ desc='Fix a color for a nick. Use "unset" instead of a color'
+ ' to remove the attribution',
+ shortdesc='Fix a color for a nick.',
+ completion=self.completion_color)
self.register_command('cycle', self.command_cycle,
- usage=_('[message]'),
- desc=_('Leave the current room and rejoin it immediately.'),
- shortdesc=_('Leave and re-join the room.'))
+ usage='[message]',
+ desc='Leave the current room and rejoin it immediately.',
+ shortdesc='Leave and re-join the room.')
self.register_command('info', self.command_info,
- usage=_('<nickname>'),
- desc=_('Display some information about the user '
- 'in the MUC: its/his/her role, affiliation,'
- ' status and status message.'),
- shortdesc=_('Show an user\'s infos.'),
+ usage='<nickname>',
+ desc='Display some information about the user '
+ 'in the MUC: its/his/her role, affiliation,'
+ ' status and status message.',
+ shortdesc='Show an user\'s infos.',
completion=self.completion_info)
self.register_command('configure', self.command_configure,
- desc=_('Configure the current room, through a form.'),
- shortdesc=_('Configure the room.'))
+ desc='Configure the current room, through a form.',
+ shortdesc='Configure the room.')
self.register_command('version', self.command_version,
- usage=_('<jid or nick>'),
- desc=_('Get the software version of the given JID'
- ' or nick in room (usually its XMPP client'
- ' and Operating System).'),
- shortdesc=_('Get the software version of a jid.'),
+ usage='<jid or nick>',
+ desc='Get the software version of the given JID'
+ ' or nick in room (usually its XMPP client'
+ ' and Operating System).',
+ shortdesc='Get the software version of a jid.',
completion=self.completion_version)
self.register_command('names', self.command_names,
- desc=_('Get the users in the room with their roles.'),
- shortdesc=_('List the users.'))
+ desc='Get the users in the room with their roles.',
+ shortdesc='List the users.')
self.register_command('invite', self.command_invite,
- desc=_('Invite a contact to this room'),
- usage=_('<jid> [reason]'),
- shortdesc=_('Invite a contact to this room'),
+ desc='Invite a contact to this room',
+ usage='<jid> [reason]',
+ shortdesc='Invite a contact to this room',
completion=self.completion_invite)
if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks
@@ -263,6 +268,21 @@ class MucTab(ChatTab):
return the_input.new_completion(['random'], 1, '', quotify=False)
return True
+ def completion_color(self, the_input):
+ """Completion for /color"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ userlist = [user.nick for user in self.users]
+ if self.own_nick in userlist:
+ userlist.remove(self.own_nick)
+ return the_input.new_completion(userlist, 1, '', quotify=True)
+ elif n == 2:
+ colors = [i for i in xhtml.colors if i]
+ colors.sort()
+ colors.append('unset')
+ colors.append('random')
+ return the_input.new_completion(colors, 2, '', quotify=False)
+
def completion_ignore(self, the_input):
"""Completion for /ignore"""
userlist = [user.nick for user in self.users]
@@ -302,15 +322,12 @@ class MucTab(ChatTab):
return the_input.new_completion(possible_affiliations, 2, '',
quotify=True)
+ @command_args_parser.quoted(1, 1, [''])
def command_invite(self, args):
"""/invite <jid> [reason]"""
- args = common.shell_split(args)
- if len(args) == 1:
- jid, reason = args[0], ''
- elif len(args) == 2:
- jid, reason = args
- else:
+ if args is None:
return self.core.command_help('invite')
+ jid, reason = args
self.core.command_invite('%s %s "%s"' % (jid, self.name, reason))
def completion_invite(self, the_input):
@@ -329,15 +346,17 @@ class MucTab(ChatTab):
self.user_win.refresh(self.users)
self.input.refresh()
- def command_info(self, arg):
+ @command_args_parser.quoted(1)
+ def command_info(self, args):
"""
/info <nick>
"""
- if not arg:
+ if args is None:
return self.core.command_help('info')
- user = self.get_user_by_name(arg)
+ nick = args[0]
+ user = self.get_user_by_name(nick)
if not user:
- return self.core.information(_("Unknown user: %s") % arg)
+ return self.core.information("Unknown user: %s" % nick)
theme = get_theme()
if user.jid:
user_jid = ' (\x19%s}%s\x19o)' % (
@@ -345,10 +364,10 @@ class MucTab(ChatTab):
user.jid)
else:
user_jid = ''
- info = _('\x19%s}%s\x19o%s: show: \x19%s}%s\x19o, affiliation:'
- ' \x19%s}%s\x19o, role: \x19%s}%s\x19o%s') % (
+ info = ('\x19%s}%s\x19o%s: show: \x19%s}%s\x19o, affiliation:'
+ ' \x19%s}%s\x19o, role: \x19%s}%s\x19o%s') % (
dump_tuple(user.color),
- arg,
+ nick,
user_jid,
dump_tuple(theme.color_show(user.show)),
user.show or 'Available',
@@ -360,19 +379,20 @@ class MucTab(ChatTab):
self.add_message(info, typ=0)
self.core.refresh_window()
- def command_configure(self, arg):
+ @command_args_parser.quoted(0)
+ def command_configure(self, ignored):
"""
/configure
"""
def on_form_received(form):
if not form:
self.core.information(
- _('Could not retrieve the configuration form'),
- _('Error'))
+ 'Could not retrieve the configuration form',
+ 'Error')
return
self.core.open_new_form(form, self.cancel_config, self.send_config)
- form = fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
+ fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
def cancel_config(self, form):
"""
@@ -388,30 +408,53 @@ class MucTab(ChatTab):
muc.configure_room(self.core.xmpp, self.name, form)
self.core.close_tab()
- def command_cycle(self, arg):
+ @command_args_parser.raw
+ def command_cycle(self, msg):
"""/cycle [reason]"""
- self.command_part(arg)
+ self.command_part(msg)
self.disconnect()
self.user_win.pos = 0
self.core.disable_private_tabs(self.name)
self.core.command_join('"/%s"' % self.own_nick)
- def command_recolor(self, arg):
+ @command_args_parser.quoted(0, 1, [''])
+ def command_recolor(self, args):
"""
/recolor [random]
Re-assign color to the participants of the room
"""
- arg = arg.strip()
+ deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
+ if deterministic:
+ for user in self.users:
+ if user.nick == self.own_nick:
+ continue
+ color = self.search_for_color(user.nick)
+ if color != '':
+ continue
+ user.set_deterministic_color()
+ if args[0] == 'random':
+ self.core.information('"random" was provided, but poezio is '
+ 'configured to use deterministic colors',
+ 'Warning')
+ self.user_win.refresh(self.users)
+ self.input.refresh()
+ return
compare_users = lambda x: x.last_talked
users = list(self.users)
sorted_users = sorted(users, key=compare_users, reverse=True)
+ full_sorted_users = sorted_users[:]
# search our own user, to remove it from the list
- for user in sorted_users:
+ # Also remove users whose color is fixed
+ for user in full_sorted_users:
+ color = self.search_for_color(user.nick)
if user.nick == self.own_nick:
sorted_users.remove(user)
user.color = get_theme().COLOR_OWN_NICK
+ elif color != '':
+ sorted_users.remove(user)
+ user.change_color(color, deterministic)
colors = list(get_theme().LIST_COLOR_NICKNAMES)
- if arg and arg == 'random':
+ if args[0] == 'random':
random.shuffle(colors)
for i, user in enumerate(sorted_users):
user.color = colors[i % len(colors)]
@@ -420,41 +463,86 @@ class MucTab(ChatTab):
self.text_win.refresh()
self.input.refresh()
- def command_version(self, arg):
+ @command_args_parser.quoted(2, 2, [''])
+ def command_color(self, args):
+ """
+ /color <nick> <color>
+ Fix a color for a nick.
+ Use "unset" instead of a color to remove the attribution.
+ User "random" to attribute a random color.
+ """
+ if args is None:
+ return self.core.command_help('color')
+ nick = args[0]
+ color = args[1].lower()
+ user = self.get_user_by_name(nick)
+ if not color in xhtml.colors and color not in ('unset', 'random'):
+ return self.core.information("Unknown color: %s" % color, 'Error')
+ if user and user.nick == self.own_nick:
+ return self.core.information("You cannot change the color of your"
+ " own nick.", 'Error')
+ if color == 'unset':
+ if config.remove_and_save(nick, 'muc_colors'):
+ self.core.information('Color for nick %s unset' % (nick))
+ else:
+ if color == 'random':
+ color = random.choice(list(xhtml.colors))
+ if user:
+ user.change_color(color)
+ config.set_and_save(nick, color, 'muc_colors')
+ nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name)
+ if nick_color_aliases:
+ # if any user in the room has a nick which is an alias of the
+ # nick, update its color
+ for tab in self.core.get_tabs(MucTab):
+ for u in tab.users:
+ nick_alias = re.sub('^_*', '', u.nick)
+ nick_alias = re.sub('_*$', '', nick_alias)
+ if nick_alias == nick:
+ u.change_color(color)
+ self.text_win.rebuild_everything(self._text_buffer)
+ self.user_win.refresh(self.users)
+ self.text_win.refresh()
+ self.input.refresh()
+
+ @command_args_parser.quoted(1)
+ def command_version(self, args):
"""
/version <jid or nick>
"""
def callback(res):
if not res:
- return self.core.information(_('Could not get the software '
- 'version from %s') % (jid,),
- _('Warning'))
- version = _('%s is running %s version %s on %s') % (
+ return self.core.information('Could not get the software '
+ 'version from %s' % (jid,),
+ 'Warning')
+ version = '%s is running %s version %s on %s' % (
jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
+ res.get('name') or 'an unknown software',
+ res.get('version') or 'unknown',
+ res.get('os') or 'an unknown platform')
self.core.information(version, 'Info')
- if not arg:
+ if args is None:
return self.core.command_help('version')
- if arg in [user.nick for user in self.users]:
+ nick = args[0]
+ if nick in [user.nick for user in self.users]:
jid = safeJID(self.name).bare
- jid = safeJID(jid + '/' + arg)
+ jid = safeJID(jid + '/' + nick)
else:
- jid = safeJID(arg)
+ jid = safeJID(nick)
fixes.get_version(self.core.xmpp, jid,
- callback=callback)
+ callback=callback)
- def command_nick(self, arg):
+ @command_args_parser.quoted(1)
+ def command_nick(self, args):
"""
/nick <nickname>
"""
- if not arg:
+ if args is None:
return self.core.command_help('nick')
- nick = arg
+ nick = args[0]
if not self.joined:
- return self.core.information(_('/nick only works in joined rooms'),
- _('Info'))
+ return self.core.information('/nick only works in joined rooms',
+ 'Info')
current_status = self.core.get_status()
if not safeJID(self.name + '/' + nick):
return self.core.information('Invalid nick', 'Info')
@@ -462,11 +550,12 @@ class MucTab(ChatTab):
current_status.message,
current_status.show)
- def command_part(self, arg):
+ @command_args_parser.quoted(0, 1, [''])
+ def command_part(self, args):
"""
/part [msg]
"""
- arg = arg.strip()
+ arg = args[0]
msg = None
if self.joined:
info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
@@ -480,24 +569,24 @@ class MucTab(ChatTab):
color = 3
if arg:
- msg = _('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
- 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
- ' left the chatroom'
- ' (\x19o%(reason)s\x19%(info_col)s})') % {
- 'info_col': info_col, 'reason': arg,
- 'spec': char_quit, 'color': color,
- 'color_spec': spec_col,
- 'nick': self.own_nick,
- }
+ msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
+ 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
+ ' left the chatroom'
+ ' (\x19o%(reason)s\x19%(info_col)s})') % {
+ 'info_col': info_col, 'reason': arg,
+ 'spec': char_quit, 'color': color,
+ 'color_spec': spec_col,
+ 'nick': self.own_nick,
+ }
else:
- msg = _('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
- 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
- ' left the chatroom') % {
- 'info_col': info_col,
- 'spec': char_quit, 'color': color,
- 'color_spec': spec_col,
- 'nick': self.own_nick,
- }
+ msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
+ 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
+ ' left the chatroom') % {
+ 'info_col': info_col,
+ 'spec': char_quit, 'color': color,
+ 'color_spec': spec_col,
+ 'nick': self.own_nick,
+ }
self.add_message(msg, typ=2)
self.disconnect()
@@ -507,49 +596,52 @@ class MucTab(ChatTab):
self.refresh()
self.core.doupdate()
- def command_close(self, arg):
+ @command_args_parser.raw
+ def command_close(self, msg):
"""
/close [msg]
"""
- self.command_part(arg)
+ self.command_part(msg)
self.core.close_tab()
- def command_query(self, arg):
+ @command_args_parser.quoted(1, 1)
+ def command_query(self, args):
"""
/query <nick> [message]
"""
- args = common.shell_split(arg)
- if len(args) < 1:
- return
+ if args is None:
+ return self.core.command_help('query')
nick = args[0]
r = None
for user in self.users:
if user.nick == nick:
r = self.core.open_private_window(self.name, user.nick)
- if r and len(args) > 1:
+ if r and len(args) == 2:
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')
+ self.core.information("Cannot find user: %s" % nick, 'Error')
- def command_topic(self, arg):
+ @command_args_parser.raw
+ def command_topic(self, subject):
"""
/topic [new topic]
"""
- if not arg.strip():
+ if not subject:
self._text_buffer.add_message(
- _("\x19%s}The subject of the room is: %s %s") %
+ "\x19%s}The subject of the room is: %s %s" %
(dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
self.topic,
'(set by %s)' % self.topic_from if self.topic_from
else ''))
self.refresh()
return
- subject = arg
+
muc.change_subject(self.core.xmpp, self.name, subject)
- def command_names(self, arg=None):
+ @command_args_parser.quoted(0)
+ def command_names(self, args):
"""
/names
"""
@@ -620,29 +712,28 @@ class MucTab(ChatTab):
return the_input.new_completion(word_list, 1, quotify=True)
- def command_kick(self, arg):
+ @command_args_parser.quoted(1, 1)
+ def command_kick(self, args):
"""
/kick <nick> [reason]
"""
- args = common.shell_split(arg)
- if not args:
- self.core.command_help('kick')
+ if args is None:
+ return self.core.command_help('kick')
+ if len(args) == 2:
+ msg = ' "%s"' % args[1]
else:
- if len(args) > 1:
- msg = ' "%s"' % args[1]
- else:
- msg = ''
- self.command_role('"'+args[0]+ '" none'+msg)
+ msg = ''
+ self.command_role('"'+args[0]+ '" none'+msg)
- def command_ban(self, arg):
+ @command_args_parser.quoted(1, 1)
+ def command_ban(self, args):
"""
/ban <nick> [reason]
"""
def callback(iq):
if iq['type'] == 'error':
self.core.room_error(iq, self.name)
- args = common.shell_split(arg)
- if not args:
+ if args is None:
return self.core.command_help('ban')
if len(args) > 1:
msg = args[1]
@@ -661,7 +752,8 @@ class MucTab(ChatTab):
if not res:
self.core.information('Could not ban user', 'Error')
- def command_role(self, arg):
+ @command_args_parser.quoted(2, 1, [''])
+ def command_role(self, args):
"""
/role <nick> <role> [reason]
Changes the role of an user
@@ -670,24 +762,25 @@ class MucTab(ChatTab):
def callback(iq):
if iq['type'] == 'error':
self.core.room_error(iq, self.name)
- args = common.shell_split(arg)
- if len(args) < 2:
- self.core.command_help('role')
- return
- nick, role = args[0], args[1]
- if len(args) > 2:
- reason = ' '.join(args[2:])
- else:
- reason = ''
- if not self.joined or \
- not role in ('none', 'visitor', 'participant', 'moderator'):
- return
+
+ if args is None:
+ return self.core.command_help('role')
+
+ nick, role, reason = args[0], args[1].lower(), args[2]
+
+ valid_roles = ('none', 'visitor', 'participant', 'moderator')
+
+ if not self.joined or role not in valid_roles:
+ return self.core.information('The role must be one of ' + ', '.join(valid_roles),
+ 'Error')
+
if not safeJID(self.name + '/' + nick):
- return self.core('Invalid nick', 'Info')
+ return self.core.information('Invalid nick', 'Info')
muc.set_user_role(self.core.xmpp, self.name, nick, reason, role,
callback=callback)
- def command_affiliation(self, arg):
+ @command_args_parser.quoted(2)
+ def command_affiliation(self, args):
"""
/affiliation <nick> <role>
Changes the affiliation of an user
@@ -696,16 +789,20 @@ class MucTab(ChatTab):
def callback(iq):
if iq['type'] == 'error':
self.core.room_error(iq, self.name)
- args = common.shell_split(arg)
- if len(args) < 2:
- self.core.command_help('affiliation')
- return
+
+ if args is None:
+ return self.core.command_help('affiliation')
+
nick, affiliation = args[0], args[1].lower()
+
if not self.joined:
return
- if affiliation not in ('outcast', 'none', 'member', 'admin', 'owner'):
- self.core.command_help('affiliation')
- return
+
+ valid_affiliations = ('outcast', 'none', 'member', 'admin', 'owner')
+ if affiliation not in valid_affiliations:
+ return self.core.information('The affiliation must be one of ' + ', '.join(valid_affiliations),
+ 'Error')
+
if nick in [user.nick for user in self.users]:
res = muc.set_user_affiliation(self.core.xmpp, self.name,
affiliation, nick=nick,
@@ -715,8 +812,9 @@ class MucTab(ChatTab):
affiliation, jid=safeJID(nick),
callback=callback)
if not res:
- self.core.information(_('Could not set affiliation'), _('Error'))
+ self.core.information('Could not set affiliation', 'Error')
+ @command_args_parser.raw
def command_say(self, line, correct=False):
"""
/say <message>
@@ -755,45 +853,48 @@ class MucTab(ChatTab):
msg.send()
self.chat_state = needed
- def command_xhtml(self, arg):
- message = self.generate_xhtml_message(arg)
+ @command_args_parser.raw
+ def command_xhtml(self, msg):
+ message = self.generate_xhtml_message(msg)
if message:
message['type'] = 'groupchat'
message.send()
- def command_ignore(self, arg):
+ @command_args_parser.quoted(1)
+ def command_ignore(self, args):
"""
/ignore <nick>
"""
- if not arg:
- self.core.command_help('ignore')
- return
- nick = arg
+ if args is None:
+ return self.core.command_help('ignore')
+
+ nick = args[0]
user = self.get_user_by_name(nick)
if not user:
- self.core.information(_('%s is not in the room') % nick)
+ self.core.information('%s is not in the room' % nick)
elif user in self.ignores:
- self.core.information(_('%s is already ignored') % nick)
+ self.core.information('%s is already ignored' % nick)
else:
self.ignores.append(user)
- self.core.information(_("%s is now ignored") % nick, 'info')
+ self.core.information("%s is now ignored" % nick, 'info')
- def command_unignore(self, arg):
+ @command_args_parser.quoted(1)
+ def command_unignore(self, args):
"""
/unignore <nick>
"""
- if not arg:
- self.core.command_help('unignore')
- return
- nick = arg
+ if args is None:
+ return self.core.command_help('unignore')
+
+ nick = args[0]
user = self.get_user_by_name(nick)
if not user:
- self.core.information(_('%s is not in the room') % nick)
+ self.core.information('%s is not in the room' % nick)
elif user not in self.ignores:
- self.core.information(_('%s is not ignored') % nick)
+ self.core.information('%s is not ignored' % nick)
else:
self.ignores.remove(user)
- self.core.information(_('%s is now unignored') % nick)
+ self.core.information('%s is now unignored' % nick)
def completion_unignore(self, the_input):
if the_input.get_argument_position() == 1:
@@ -980,12 +1081,14 @@ class MucTab(ChatTab):
role = presence['muc']['role']
jid = presence['muc']['jid']
typ = presence['type']
+ deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
+ color = self.search_for_color(from_nick)
if not self.joined: # user in the room BEFORE us.
# ignore redondant presence message, see bug #1509
if (from_nick not in [user.nick for user in self.users]
and typ != "unavailable"):
new_user = User(from_nick, affiliation, show,
- status, role, jid)
+ status, role, jid, deterministic, color)
self.users.append(new_user)
self.core.events.trigger('muc_join', presence, self)
if '110' in status_codes or self.own_nick == from_nick:
@@ -1015,9 +1118,9 @@ class MucTab(ChatTab):
spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR)
self.add_message(
- _('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You '
- '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined'
- ' the chatroom') %
+ '\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You '
+ '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined'
+ ' the chatroom' %
{
'nick': from_nick,
'spec': get_theme().CHAR_JOIN,
@@ -1028,21 +1131,21 @@ class MucTab(ChatTab):
typ=2)
if '201' in status_codes:
self.add_message(
- _('\x19%(info_col)s}Info: The room '
- 'has been created') %
+ '\x19%(info_col)s}Info: The room '
+ 'has been created' %
{'info_col': info_col},
typ=0)
if '170' in status_codes:
self.add_message(
- _('\x19%(warn_col)s}Warning:\x19%(info_col)s}'
- ' This room is publicly logged') %
+ '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
+ ' This room is publicly logged' %
{'info_col': info_col,
'warn_col': warn_col},
typ=0)
if '100' in status_codes:
self.add_message(
- _('\x19%(warn_col)s}Warning:\x19%(info_col)s}'
- ' This room is not anonymous.') %
+ '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
+ ' This room is not anonymous.' %
{'info_col': info_col,
'warn_col': warn_col},
typ=0)
@@ -1065,7 +1168,7 @@ class MucTab(ChatTab):
if not user:
self.core.events.trigger('muc_join', presence, self)
self.on_user_join(from_nick, affiliation, show, status, role,
- jid)
+ jid, color)
# nick change
elif change_nick:
self.core.events.trigger('muc_nickchange', presence, self)
@@ -1105,8 +1208,8 @@ class MucTab(ChatTab):
def on_non_member_kicked(self):
"""We have been kicked because the MUC is members-only"""
self.add_message(
- _('\x19%(info_col)s}You have been kicked because you '
- 'are not a member and the room is now members-only.') % {
+ '\x19%(info_col)s}You have been kicked because you '
+ 'are not a member and the room is now members-only.' % {
'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
typ=2)
self.disconnect()
@@ -1114,18 +1217,19 @@ class MucTab(ChatTab):
def on_muc_shutdown(self):
"""We have been kicked because the MUC service is shutting down"""
self.add_message(
- _('\x19%(info_col)s}You have been kicked because the'
- ' MUC service is shutting down.') % {
+ '\x19%(info_col)s}You have been kicked because the'
+ ' MUC service is shutting down.' % {
'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
typ=2)
self.disconnect()
- def on_user_join(self, from_nick, affiliation, show, status, role, jid):
+ def on_user_join(self, from_nick, affiliation, show, status, role, jid, color):
"""
When a new user joins the groupchat
"""
+ deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
user = User(from_nick, affiliation,
- show, status, role, jid)
+ show, status, role, jid, deterministic, color)
self.users.append(user)
hide_exit_join = config.get_by_tabname('hide_exit_join',
self.general_jid)
@@ -1139,17 +1243,17 @@ class MucTab(ChatTab):
spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR)
char_join = get_theme().CHAR_JOIN
if not jid.full:
- msg = _('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s'
- '\x19%(info_col)s} joined the chatroom') % {
+ msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s'
+ '\x19%(info_col)s} joined the chatroom') % {
'nick': from_nick, 'spec': char_join,
'color': color,
'info_col': info_col,
'color_spec': spec_col,
}
else:
- msg = _('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s '
- '\x19%(info_col)s}(\x19%(jid_color)s}%(jid)s\x19'
- '%(info_col)s}) joined the chatroom') % {
+ msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s '
+ '\x19%(info_col)s}(\x19%(jid_color)s}%(jid)s\x19'
+ '%(info_col)s}) joined the chatroom') % {
'spec': char_join, 'nick': from_nick,
'color':color, 'jid':jid.full,
'info_col': info_col,
@@ -1166,6 +1270,12 @@ class MucTab(ChatTab):
self.own_nick = new_nick
# also change our nick in all private discussions of this room
self.core.on_muc_own_nickchange(self)
+ else:
+ color = config.get_by_tabname(new_nick, 'muc_colors')
+ if color != '':
+ deterministic = config.get_by_tabname('deterministic_nick_colors',
+ self.name)
+ user.change_color(color, deterministic)
user.change_nick(new_nick)
if config.get_by_tabname('display_user_color_in_join_part',
@@ -1174,8 +1284,8 @@ class MucTab(ChatTab):
else:
color = 3
info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
- self.add_message(_('\x19%(color)s}%(old)s\x19%(info_col)s} is'
- ' now known as \x19%(color)s}%(new)s') % {
+ self.add_message('\x19%(color)s}%(old)s\x19%(info_col)s} is'
+ ' now known as \x19%(color)s}%(new)s' % {
'old':from_nick, 'new':new_nick,
'color':color, 'info_col': info_col},
typ=2)
@@ -1198,13 +1308,13 @@ class MucTab(ChatTab):
if from_nick == self.own_nick: # we are banned
if by:
- kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s}'
- ' have been banned by \x194}%(by)s') % {
+ kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}'
+ ' have been banned by \x194}%(by)s') % {
'spec': char_kick, 'by': by,
'info_col': info_col}
else:
- kick_msg = _('\x191}%(spec)s \x193}You\x19'
- '%(info_col)s} have been banned.') % {
+ kick_msg = ('\x191}%(spec)s \x193}You\x19'
+ '%(info_col)s} have been banned.') % {
'spec': char_kick, 'info_col': info_col}
self.core.disable_private_tabs(self.name, reason=kick_msg)
self.disconnect()
@@ -1233,20 +1343,20 @@ class MucTab(ChatTab):
color = 3
if by:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}'
- '%(nick)s\x19%(info_col)s} '
- 'has been banned by \x194}%(by)s') % {
+ kick_msg = ('\x191}%(spec)s \x19%(color)s}'
+ '%(nick)s\x19%(info_col)s} '
+ 'has been banned by \x194}%(by)s') % {
'spec': char_kick, 'nick': from_nick,
'color': color, 'by': by,
'info_col': info_col}
else:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s'
- '\x19%(info_col)s} has been banned') % {
+ kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
+ '\x19%(info_col)s} has been banned') % {
'spec': char_kick, 'nick': from_nick,
'color': color, 'info_col': info_col}
if reason is not None and reason.text:
- kick_msg += _('\x19%(info_col)s} Reason: \x196}'
- '%(reason)s\x19%(info_col)s}') % {
+ kick_msg += ('\x19%(info_col)s} Reason: \x196}'
+ '%(reason)s\x19%(info_col)s}') % {
'reason': reason.text, 'info_col': info_col}
self.add_message(kick_msg, typ=2)
@@ -1266,14 +1376,14 @@ class MucTab(ChatTab):
by = actor_elem.get('nick') or actor_elem.get('jid')
if from_nick == self.own_nick: # we are kicked
if by:
- kick_msg = _('\x191}%(spec)s \x193}You\x19'
- '%(info_col)s} have been kicked'
- ' by \x193}%(by)s') % {
+ kick_msg = ('\x191}%(spec)s \x193}You\x19'
+ '%(info_col)s} have been kicked'
+ ' by \x193}%(by)s') % {
'spec': char_kick, 'by': by,
'info_col': info_col}
else:
- kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s}'
- ' have been kicked.') % {
+ kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}'
+ ' have been kicked.') % {
'spec': char_kick,
'info_col': info_col}
self.core.disable_private_tabs(self.name, reason=kick_msg)
@@ -1302,19 +1412,19 @@ class MucTab(ChatTab):
else:
color = 3
if by:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s'
- '\x19%(info_col)s} has been kicked by '
- '\x193}%(by)s') % {
+ kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
+ '\x19%(info_col)s} has been kicked by '
+ '\x193}%(by)s') % {
'spec': char_kick, 'nick':from_nick,
'color':color, 'by':by, 'info_col': info_col}
else:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s'
- '\x19%(info_col)s} has been kicked') % {
+ kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
+ '\x19%(info_col)s} has been kicked') % {
'spec': char_kick, 'nick': from_nick,
'color':color, 'info_col': info_col}
if reason is not None and reason.text:
- kick_msg += _('\x19%(info_col)s} Reason: \x196}'
- '%(reason)s') % {
+ kick_msg += ('\x19%(info_col)s} Reason: \x196}'
+ '%(reason)s') % {
'reason': reason.text, 'info_col': info_col}
self.add_message(kick_msg, typ=2)
@@ -1343,19 +1453,19 @@ class MucTab(ChatTab):
spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)
if not jid.full:
- leave_msg = _('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
- '%(nick)s\x19%(info_col)s} has left the '
- 'chatroom') % {
+ leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
+ '%(nick)s\x19%(info_col)s} has left the '
+ 'chatroom') % {
'nick':from_nick, 'color':color,
'spec':get_theme().CHAR_QUIT,
'info_col': info_col,
'color_spec': spec_col}
else:
jid_col = dump_tuple(get_theme().COLOR_MUC_JID)
- leave_msg = _('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
- '%(nick)s\x19%(info_col)s} (\x19%(jid_col)s}'
- '%(jid)s\x19%(info_col)s}) has left the '
- 'chatroom') % {
+ leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
+ '%(nick)s\x19%(info_col)s} (\x19%(jid_col)s}'
+ '%(jid)s\x19%(info_col)s}) has left the '
+ 'chatroom') % {
'spec':get_theme().CHAR_QUIT,
'nick':from_nick, 'color':color,
'jid':jid.full, 'info_col': info_col,
@@ -1381,33 +1491,29 @@ class MucTab(ChatTab):
else:
color = 3
if from_nick == self.own_nick:
- msg = _('\x19%(color)s}You\x19%(info_col)s} changed: ') % {
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
- 'color': color}
+ msg = '\x19%(color)s}You\x19%(info_col)s} changed: ' % {
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
+ 'color': color}
else:
- msg = _('\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ') % {
- 'nick': from_nick, 'color': color,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- if show not in SHOW_NAME:
- self.core.information(_("%s from room %s sent an invalid show: %s")
- % (from_nick, from_room, show),
- _("Warning"))
+ msg = '\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ' % {
+ 'nick': from_nick, 'color': color,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
if affiliation != user.affiliation:
- msg += _('affiliation: %s, ') % affiliation
+ msg += 'affiliation: %s, ' % affiliation
display_message = True
if role != user.role:
- msg += _('role: %s, ') % role
+ msg += 'role: %s, ' % role
display_message = True
if show != user.show and show in SHOW_NAME:
- msg += _('show: %s, ') % SHOW_NAME[show]
+ msg += 'show: %s, ' % SHOW_NAME[show]
display_message = True
if status != user.status:
# if the user sets his status to nothing
if status:
- msg += _('status: %s, ') % status
+ msg += 'status: %s, ' % status
display_message = True
elif show in SHOW_NAME and show == user.show:
- msg += _('show: %s, ') % SHOW_NAME[show]
+ msg += 'show: %s, ' % SHOW_NAME[show]
display_message = True
if not display_message:
return
@@ -1461,8 +1567,8 @@ class MucTab(ChatTab):
"""
if time is None and self.joined: # don't log the history messages
if not logger.log_message(self.name, nickname, txt, typ=typ):
- self.core.information(_('Unable to write in the log file'),
- _('Error'))
+ self.core.information('Unable to write in the log file',
+ 'Error')
def do_highlight(self, txt, time, nickname):
"""
@@ -1581,6 +1687,22 @@ class MucTab(ChatTab):
else: # Re-send a self-ping in a few seconds
self.enable_self_ping_event()
+ def search_for_color(self, nick):
+ """
+ Search for the color of a nick in the config file.
+ Also, look at the colors of its possible aliases if nick_color_aliases
+ is set.
+ """
+ color = config.get_by_tabname(nick, 'muc_colors')
+ if color != '':
+ return color
+ nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name)
+ if nick_color_aliases:
+ nick_alias = re.sub('^_*', '', nick)
+ nick_alias = re.sub('_*$', '', nick_alias)
+ color = config.get_by_tabname(nick_alias, 'muc_colors')
+ return color
+
def on_self_ping_failed(self, iq):
self.command_part("the MUC server is not responding")
self.core.refresh_window()
diff --git a/src/tabs/privatetab.py b/src/tabs/privatetab.py
index 4c01cd70..a715a922 100644
--- a/src/tabs/privatetab.py
+++ b/src/tabs/privatetab.py
@@ -10,8 +10,6 @@ both participant’s nicks. It also has slightly different features than
the ConversationTab (such as tab-completion on nicks from the room).
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -27,6 +25,7 @@ from config import config
from decorators import refresh_wrapper
from logger import logger
from theming import get_theme, dump_tuple
+from decorators import command_args_parser
class PrivateTab(OneToOneTab):
"""
@@ -48,15 +47,15 @@ class PrivateTab(OneToOneTab):
self.key_func['^I'] = self.completion
# commands
self.register_command('info', self.command_info,
- desc=_('Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'),
- shortdesc=_('Info about the user.'))
+ desc='Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.',
+ shortdesc='Info about the user.')
self.register_command('unquery', self.command_unquery,
- shortdesc=_('Close the tab.'))
+ shortdesc='Close the tab.')
self.register_command('close', self.command_unquery,
- shortdesc=_('Close the tab.'))
+ shortdesc='Close the tab.')
self.register_command('version', self.command_version,
- desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
- shortdesc=_('Get the software version of a jid.'))
+ desc='Get the software version of the current interlocutor (usually its XMPP client and Operating System).',
+ shortdesc='Get the software version of a jid.')
self.resize()
self.parent_muc = self.core.get_tab_by_name(safeJID(name).bare, MucTab)
self.on = True
@@ -87,13 +86,14 @@ class PrivateTab(OneToOneTab):
def load_logs(self, log_nb):
logs = logger.get_logs(safeJID(self.name).full.replace('/', '\\'), log_nb)
+ return logs
def log_message(self, txt, nickname, time=None, typ=1):
"""
Log the messages in the archives.
"""
if not logger.log_message(self.name, nickname, txt, date=time, typ=typ):
- self.core.information(_('Unable to write in the log file'), 'Error')
+ self.core.information('Unable to write in the log file', 'Error')
def on_close(self):
self.parent_muc.privates.remove(self)
@@ -120,6 +120,7 @@ class PrivateTab(OneToOneTab):
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_after)
+ @command_args_parser.raw
def command_say(self, line, attention=False, correct=False):
if not self.on:
return
@@ -182,13 +183,15 @@ class PrivateTab(OneToOneTab):
self.text_win.refresh()
self.input.refresh()
- def command_unquery(self, arg):
+ @command_args_parser.ignored
+ def command_unquery(self):
"""
/unquery
"""
self.core.close_tab()
- def command_version(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_version(self, args):
"""
/version
"""
@@ -196,22 +199,23 @@ class PrivateTab(OneToOneTab):
if not res:
return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
version = '%s is running %s version %s on %s' % (jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
+ res.get('name') or 'an unknown software',
+ res.get('version') or 'unknown',
+ res.get('os') or 'an unknown platform')
self.core.information(version, 'Info')
- if arg:
- return self.core.command_version(arg)
+ if args:
+ return self.core.command_version(args[0])
jid = safeJID(self.name)
fixes.get_version(self.core.xmpp, jid,
callback=callback)
+ @command_args_parser.quoted(0, 1)
def command_info(self, arg):
"""
/info
"""
- if arg:
- self.parent_muc.command_info(arg)
+ if arg and arg[0]:
+ self.parent_muc.command_info(arg[0])
else:
user = safeJID(self.name).resource
self.parent_muc.command_info(user)
@@ -319,9 +323,9 @@ class PrivateTab(OneToOneTab):
"""
self.deactivate()
if not status_message:
- 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': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
+ 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': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
else:
- 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': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
+ 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': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
return self.core.current_tab() is self
@refresh_wrapper.conditional
diff --git a/src/tabs/rostertab.py b/src/tabs/rostertab.py
index 878e89ed..aaff7de3 100644
--- a/src/tabs/rostertab.py
+++ b/src/tabs/rostertab.py
@@ -5,15 +5,16 @@ rectangle shows the current contact info.
This module also includes functions to match users in the roster.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
+import base64
import curses
import difflib
import os
+import ssl
from os import getenv, path
+from functools import partial
from . import Tab
@@ -25,6 +26,7 @@ from contact import Contact, Resource
from decorators import refresh_wrapper
from roster import RosterGroup, roster
from theming import get_theme, dump_tuple
+from decorators import command_args_parser
class RosterInfoTab(Tab):
"""
@@ -44,107 +46,315 @@ class RosterInfoTab(Tab):
self.input = self.default_help_message
self.state = 'normal'
self.key_func['^I'] = self.completion
- self.key_func[' '] = self.on_space
self.key_func["/"] = self.on_slash
- self.key_func["KEY_UP"] = self.move_cursor_up
- self.key_func["KEY_DOWN"] = self.move_cursor_down
- self.key_func["M-u"] = self.move_cursor_to_next_contact
- self.key_func["M-y"] = self.move_cursor_to_prev_contact
- self.key_func["M-U"] = self.move_cursor_to_next_group
- self.key_func["M-Y"] = self.move_cursor_to_prev_group
- self.key_func["M-[1;5B"] = self.move_cursor_to_next_group
- self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group
- self.key_func["l"] = self.command_last_activity
- self.key_func["o"] = self.toggle_offline_show
- self.key_func["v"] = self.get_contact_version
- self.key_func["i"] = self.show_contact_info
- self.key_func["n"] = self.change_contact_name
- self.key_func["s"] = self.start_search
- self.key_func["S"] = self.start_search_slow
- self.register_command('deny', self.command_deny,
- usage=_('[jid]'),
- desc=_('Deny your presence to the provided JID (or the selected contact in your roster), who is asking you to be in his/here roster.'),
- shortdesc=_('Deny an user your presence.'),
- completion=self.completion_deny)
- self.register_command('accept', self.command_accept,
- usage=_('[jid]'),
- desc=_('Allow the provided JID (or the selected contact in your roster), to see your presence.'),
- shortdesc=_('Allow an user your presence.'),
- completion=self.completion_deny)
- self.register_command('add', self.command_add,
- usage=_('<jid>'),
- desc=_('Add the specified JID to your roster, ask him to allow you to see his presence, and allow him to see your presence.'),
- shortdesc=_('Add an user to your roster.'))
- self.register_command('name', self.command_name,
- usage=_('<jid> <name>'),
- shortdesc=_('Set the given JID\'s name.'),
- completion=self.completion_name)
- self.register_command('groupadd', self.command_groupadd,
- usage=_('<jid> <group>'),
- desc=_('Add the given JID to the given group.'),
- shortdesc=_('Add an user to a group'),
- completion=self.completion_groupadd)
- self.register_command('groupmove', self.command_groupmove,
- usage=_('<jid> <old group> <new group>'),
- desc=_('Move the given JID from the old group to the new group.'),
- shortdesc=_('Move an user to another group.'),
- completion=self.completion_groupmove)
- self.register_command('groupremove', self.command_groupremove,
- usage=_('<jid> <group>'),
- desc=_('Remove the given JID from the given group.'),
- shortdesc=_('Remove an user from a group.'),
- completion=self.completion_groupremove)
- self.register_command('remove', self.command_remove,
- usage=_('[jid]'),
- desc=_('Remove the specified JID from your roster. This wil unsubscribe you from its presence, cancel its subscription to yours, and remove the item from your roster.'),
- shortdesc=_('Remove an user from your roster.'),
- completion=self.completion_remove)
+ # disable most of the roster features when in anonymous mode
+ if not self.core.xmpp.anon:
+ self.key_func[' '] = self.on_space
+ self.key_func["KEY_UP"] = self.move_cursor_up
+ self.key_func["KEY_DOWN"] = self.move_cursor_down
+ self.key_func["M-u"] = self.move_cursor_to_next_contact
+ self.key_func["M-y"] = self.move_cursor_to_prev_contact
+ self.key_func["M-U"] = self.move_cursor_to_next_group
+ self.key_func["M-Y"] = self.move_cursor_to_prev_group
+ self.key_func["M-[1;5B"] = self.move_cursor_to_next_group
+ self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group
+ self.key_func["l"] = self.command_last_activity
+ self.key_func["o"] = self.toggle_offline_show
+ self.key_func["v"] = self.get_contact_version
+ self.key_func["i"] = self.show_contact_info
+ self.key_func["s"] = self.start_search
+ self.key_func["S"] = self.start_search_slow
+ self.key_func["n"] = self.change_contact_name
+ self.register_command('deny', self.command_deny,
+ usage='[jid]',
+ desc='Deny your presence to the provided JID (or the '
+ 'selected contact in your roster), who is asking'
+ 'you to be in his/here roster.',
+ shortdesc='Deny an user your presence.',
+ completion=self.completion_deny)
+ self.register_command('accept', self.command_accept,
+ usage='[jid]',
+ desc='Allow the provided JID (or the selected contact '
+ 'in your roster), to see your presence.',
+ shortdesc='Allow an user your presence.',
+ completion=self.completion_deny)
+ self.register_command('add', self.command_add,
+ usage='<jid>',
+ desc='Add the specified JID to your roster, ask him to'
+ ' allow you to see his presence, and allow him to'
+ ' see your presence.',
+ shortdesc='Add an user to your roster.')
+ self.register_command('name', self.command_name,
+ usage='<jid> [name]',
+ shortdesc='Set the given JID\'s name.',
+ completion=self.completion_name)
+ self.register_command('groupadd', self.command_groupadd,
+ usage='<jid> <group>',
+ desc='Add the given JID to the given group.',
+ shortdesc='Add an user to a group',
+ completion=self.completion_groupadd)
+ self.register_command('groupmove', self.command_groupmove,
+ usage='<jid> <old group> <new group>',
+ desc='Move the given JID from the old group to the new group.',
+ shortdesc='Move an user to another group.',
+ completion=self.completion_groupmove)
+ self.register_command('groupremove', self.command_groupremove,
+ usage='<jid> <group>',
+ desc='Remove the given JID from the given group.',
+ shortdesc='Remove an user from a group.',
+ completion=self.completion_groupremove)
+ self.register_command('remove', self.command_remove,
+ usage='[jid]',
+ desc='Remove the specified JID from your roster. This '
+ 'will unsubscribe you from its presence, cancel '
+ 'its subscription to yours, and remove the item '
+ 'from your roster.',
+ shortdesc='Remove an user from your roster.',
+ completion=self.completion_remove)
+ self.register_command('export', self.command_export,
+ usage='[/path/to/file]',
+ desc='Export your contacts into /path/to/file if '
+ 'specified, or $HOME/poezio_contacts if not.',
+ shortdesc='Export your roster to a file.',
+ completion=partial(self.completion_file, 1))
+ self.register_command('import', self.command_import,
+ usage='[/path/to/file]',
+ desc='Import your contacts from /path/to/file if '
+ 'specified, or $HOME/poezio_contacts if not.',
+ shortdesc='Import your roster from a file.',
+ completion=partial(self.completion_file, 1))
+ self.register_command('password', self.command_password,
+ usage='<password>',
+ shortdesc='Change your password')
+
self.register_command('reconnect', self.command_reconnect,
- desc=_('Disconnect from the remote server if you are currently connected and then connect to it again.'),
- shortdesc=_('Disconnect and reconnect to the server.'))
+ desc='Disconnect from the remote server if you are '
+ 'currently connected and then connect to it again.',
+ shortdesc='Disconnect and reconnect to the server.')
self.register_command('disconnect', self.command_disconnect,
- desc=_('Disconnect from the remote server.'),
- shortdesc=_('Disconnect from the server.'))
- self.register_command('export', self.command_export,
- usage=_('[/path/to/file]'),
- desc=_('Export your contacts into /path/to/file if specified, or $HOME/poezio_contacts if not.'),
- shortdesc=_('Export your roster to a file.'),
- completion=self.completion_file)
- self.register_command('import', self.command_import,
- usage=_('[/path/to/file]'),
- desc=_('Import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not.'),
- shortdesc=_('Import your roster from a file.'),
- completion=self.completion_file)
+ desc='Disconnect from the remote server.',
+ shortdesc='Disconnect from the server.')
self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the info buffer.'))
+ shortdesc='Clear the info buffer.')
self.register_command('last_activity', self.command_last_activity,
- usage=_('<jid>'),
- desc=_('Informs you of the last activity of a JID.'),
- shortdesc=_('Get the activity of someone.'),
+ usage='<jid>',
+ desc='Informs you of the last activity of a JID.',
+ shortdesc='Get the activity of someone.',
completion=self.core.completion_last_activity)
- self.register_command('password', self.command_password,
- usage='<password>',
- shortdesc=_('Change your password'))
self.resize()
self.update_commands()
self.update_keys()
def check_blocking(self, features):
- if 'urn:xmpp:blocking' in features:
+ if 'urn:xmpp:blocking' in features and not self.core.xmpp.anon:
self.register_command('block', self.command_block,
- usage=_('[jid]'),
- shortdesc=_('Prevent a JID from talking to you.'),
+ usage='[jid]',
+ shortdesc='Prevent a JID from talking to you.',
completion=self.completion_block)
self.register_command('unblock', self.command_unblock,
- usage=_('[jid]'),
- shortdesc=_('Allow a JID to talk to you.'),
+ usage='[jid]',
+ shortdesc='Allow a JID to talk to you.',
completion=self.completion_unblock)
self.register_command('list_blocks', self.command_list_blocks,
- shortdesc=_('Show the blocked contacts.'))
+ shortdesc='Show the blocked contacts.')
self.core.xmpp.del_event_handler('session_start', self.check_blocking)
self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message)
+ def check_saslexternal(self, features):
+ if 'urn:xmpp:saslcert:1' in features and not self.core.xmpp.anon:
+ self.register_command('certs', self.command_certs,
+ desc='List the fingerprints of certificates'
+ ' which can connect to your account.',
+ shortdesc='List allowed client certs.')
+ self.register_command('cert_add', self.command_cert_add,
+ desc='Add a client certificate to the authorized ones. '
+ 'It must have an unique name and be contained in '
+ 'a PEM file. [management] is a boolean indicating'
+ ' if a client connected using this certificate can'
+ ' manage the certificates itself.',
+ shortdesc='Add a client certificate.',
+ usage='<name> <certificate path> [management]',
+ completion=self.completion_cert_add)
+ self.register_command('cert_disable', self.command_cert_disable,
+ desc='Remove a certificate from the list '
+ 'of allowed ones. Clients currently '
+ 'using this certificate will not be '
+ 'forcefully disconnected.',
+ shortdesc='Disable a certificate',
+ usage='<name>')
+ self.register_command('cert_revoke', self.command_cert_revoke,
+ desc='Remove a certificate from the list '
+ 'of allowed ones. Clients currently '
+ 'using this certificate will be '
+ 'forcefully disconnected.',
+ shortdesc='Revoke a certificate',
+ usage='<name>')
+ self.register_command('cert_fetch', self.command_cert_fetch,
+ desc='Retrieve a certificate with its '
+ 'name. It will be stored in <path>.',
+ shortdesc='Fetch a certificate',
+ usage='<name> <path>',
+ completion=self.completion_cert_fetch)
+
+ @command_args_parser.ignored
+ def command_certs(self):
+ """
+ /certs
+ """
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to retrieve the certificate list.',
+ 'Error')
+ return
+ certs = []
+ for item in iq['sasl_certs']['items']:
+ users = '\n'.join(item['users'])
+ certs.append((item['name'], users))
+
+ if not certs:
+ return self.core.information('No certificates found', 'Info')
+ msg = 'Certificates:\n'
+ msg += '\n'.join(((' %s%s' % (item[0] + (': ' if item[1] else ''), item[1])) for item in certs))
+ self.core.information(msg, 'Info')
+
+ self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb, timeout=3)
+
+ @command_args_parser.quoted(2, 1)
+ def command_cert_add(self, args):
+ """
+ /cert_add <name> <certfile> [cert-management]
+ """
+ if not args or len(args) < 2:
+ return self.core.command_help('cert_add')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to add the certificate.', 'Error')
+ else:
+ self.core.information('Certificate added.', 'Info')
+
+ name = args[0]
+
+ try:
+ with open(args[1]) as fd:
+ crt = fd.read()
+ crt = crt.replace(ssl.PEM_FOOTER, '').replace(ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '')
+ except Exception as e:
+ self.core.information('Unable to read the certificate: %s' % e, 'Error')
+ return
+
+ if len(args) > 2:
+ management = args[2]
+ if management:
+ management = management.lower()
+ if management not in ('false', '0'):
+ management = True
+ else:
+ management = False
+ else:
+ management = False
+ else:
+ management = True
+
+ self.core.xmpp.plugin['xep_0257'].add_cert(name, crt, callback=cb,
+ allow_management=management)
+
+ def completion_cert_add(self, the_input):
+ """
+ completion for /cert_add <name> <path> [management]
+ """
+ text = the_input.get_text()
+ args = common.shell_split(text)
+ n = the_input.get_argument_position()
+ log.debug('%s %s %s', the_input.text, n, the_input.pos)
+ if n == 1:
+ return
+ elif n == 2:
+ return self.completion_file(2, the_input)
+ elif n == 3:
+ return the_input.new_completion(['true', 'false'], n)
+
+ @command_args_parser.quoted(1)
+ def command_cert_disable(self, args):
+ """
+ /cert_disable <name>
+ """
+ if not args:
+ return self.core.command_help('cert_disable')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to disable the certificate.', 'Error')
+ else:
+ self.core.information('Certificate disabled.', 'Info')
+
+ name = args[0]
+
+ self.core.xmpp.plugin['xep_0257'].disable_cert(name, callback=cb)
+
+ @command_args_parser.quoted(1)
+ def command_cert_revoke(self, args):
+ """
+ /cert_revoke <name>
+ """
+ if not args:
+ return self.core.command_help('cert_revoke')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to revoke the certificate.', 'Error')
+ else:
+ self.core.information('Certificate revoked.', 'Info')
+
+ name = args[0]
+
+ self.core.xmpp.plugin['xep_0257'].revoke_cert(name, callback=cb)
+
+
+ @command_args_parser.quoted(2)
+ def command_cert_fetch(self, args):
+ """
+ /cert_fetch <name> <path>
+ """
+ if not args or len(args) < 2:
+ return self.core.command_help('cert_fetch')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to fetch the certificate.',
+ 'Error')
+ return
+
+ cert = None
+ for item in iq['sasl_certs']['items']:
+ if item['name'] == name:
+ cert = base64.b64decode(item['x509cert'])
+ break
+
+ if not cert:
+ return self.core.information('Certificate not found.', 'Info')
+
+ cert = ssl.DER_cert_to_PEM_cert(cert)
+ with open(path, 'w') as fd:
+ fd.write(cert)
+
+ self.core.information('File stored at %s' % path, 'Info')
+
+ name = args[0]
+ path = args[1]
+
+ self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb)
+
+ def completion_cert_fetch(self, the_input):
+ """
+ completion for /cert_fetch <name> <path>
+ """
+ text = the_input.get_text()
+ args = common.shell_split(text)
+ n = the_input.get_argument_position()
+ log.debug('%s %s %s', the_input.text, n, the_input.pos)
+ if n == 1:
+ return
+ elif n == 2:
+ return self.completion_file(2, the_input)
+
def on_blocked_message(self, message):
"""
When we try to send a message to a blocked contact
@@ -158,7 +368,8 @@ class RosterInfoTab(Tab):
}
tab.add_message(message)
- def command_block(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_block(self, args):
"""
/block [jid]
"""
@@ -169,8 +380,8 @@ class RosterInfoTab(Tab):
return self.core.information('Contact blocked.', 'Info')
item = self.roster_win.selected_row
- if arg:
- jid = safeJID(arg)
+ if args:
+ jid = safeJID(args[0])
elif isinstance(item, Contact):
jid = item.bare_jid
elif isinstance(item, Resource):
@@ -185,7 +396,8 @@ class RosterInfoTab(Tab):
jids = roster.jids()
return the_input.new_completion(jids, 1, '', quotify=False)
- def command_unblock(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_unblock(self, args):
"""
/unblock [jid]
"""
@@ -196,8 +408,8 @@ class RosterInfoTab(Tab):
return self.core.information('Contact unblocked.', 'Info')
item = self.roster_win.selected_row
- if arg:
- jid = safeJID(arg)
+ if args:
+ jid = safeJID(args[0])
elif isinstance(item, Contact):
jid = item.bare_jid
elif isinstance(item, Resource):
@@ -218,7 +430,8 @@ class RosterInfoTab(Tab):
self.core.xmpp.plugin['xep_0191'].get_blocked(callback=on_result)
return True
- def command_list_blocks(self, arg=None):
+ @command_args_parser.ignored
+ def command_list_blocks(self):
"""
/list_blocks
"""
@@ -236,7 +449,8 @@ class RosterInfoTab(Tab):
self.core.xmpp.plugin['xep_0191'].get_blocked(callback=callback)
- def command_reconnect(self, args=None):
+ @command_args_parser.ignored
+ def command_reconnect(self):
"""
/reconnect
"""
@@ -245,19 +459,21 @@ class RosterInfoTab(Tab):
else:
self.core.xmpp.connect()
- def command_disconnect(self, args=None):
+ @command_args_parser.ignored
+ def command_disconnect(self):
"""
/disconnect
"""
self.core.disconnect()
- def command_last_activity(self, arg=None):
+ @command_args_parser.quoted(0, 1)
+ def command_last_activity(self, args):
"""
/activity [jid]
"""
item = self.roster_win.selected_row
- if arg:
- jid = arg
+ if args:
+ jid = args[0]
elif isinstance(item, Contact):
jid = item.bare_jid
elif isinstance(item, Resource):
@@ -311,31 +527,45 @@ class RosterInfoTab(Tab):
not self.input.help_message:
self.complete_commands(self.input)
- def completion_file(self, the_input):
+ def completion_file(self, complete_number, the_input):
"""
- Completion for /import and /export
+ Generic quoted completion for files/paths
+ (use functools.partial to use directly as a completion
+ for a command)
"""
text = the_input.get_text()
- args = text.split()
- n = len(args)
- if n == 1:
- home = os.getenv('HOME') or '/'
- return the_input.auto_completion([home, '/tmp'], '')
- else:
- the_path = text[text.index(' ')+1:]
+ args = common.shell_split(text)
+ n = the_input.get_argument_position()
+ if n == complete_number:
+ if args[n-1] == '' or len(args) < n+1:
+ home = os.getenv('HOME') or '/'
+ return the_input.new_completion([home, '/tmp'], n, quotify=True)
+ path_ = args[n]
+ if path.isdir(path_):
+ dir_ = path_
+ base = ''
+ else:
+ dir_ = path.dirname(path_)
+ base = path.basename(path_)
try:
- names = os.listdir(the_path)
- except:
+ names = os.listdir(dir_)
+ except OSError:
names = []
+ names_filtered = [name for name in names if name.startswith(base)]
+ if names_filtered:
+ names = names_filtered
+ if not names:
+ names = [path_]
end_list = []
for name in names:
- value = os.path.join(the_path, name)
+ value = os.path.join(dir_, name)
if not name.startswith('.'):
end_list.append(value)
- return the_input.auto_completion(end_list, '')
+ return the_input.new_completion(end_list, n, quotify=True)
- def command_clear(self, arg=''):
+ @command_args_parser.ignored
+ def command_clear(self):
"""
/clear
"""
@@ -344,7 +574,8 @@ class RosterInfoTab(Tab):
self.core.information_win.rebuild_everything(self.core.information_buffer)
self.refresh()
- def command_password(self, arg):
+ @command_args_parser.quoted(1)
+ def command_password(self, args):
"""
/password <password>
"""
@@ -352,19 +583,18 @@ class RosterInfoTab(Tab):
if iq['type'] == 'result':
self.core.information('Password updated', 'Account')
if config.get('password'):
- config.silent_set('password', arg)
+ config.silent_set('password', args[0])
else:
self.core.information('Unable to change the password', 'Account')
- self.core.xmpp.plugin['xep_0077'].change_password(arg, callback=callback)
-
+ self.core.xmpp.plugin['xep_0077'].change_password(args[0], callback=callback)
-
- def command_deny(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_deny(self, args):
"""
/deny [jid]
Denies a JID from our roster
"""
- if not arg:
+ if not args:
item = self.roster_win.selected_row
if isinstance(item, Contact):
jid = item.bare_jid
@@ -372,7 +602,7 @@ class RosterInfoTab(Tab):
self.core.information('No subscription to deny')
return
else:
- jid = safeJID(arg).bare
+ jid = safeJID(args[0]).bare
if not jid in [jid for jid in roster.jids()]:
self.core.information('No subscription to deny')
return
@@ -383,14 +613,15 @@ class RosterInfoTab(Tab):
self.core.information('Subscription to %s was revoked' % jid,
'Roster')
+ @command_args_parser.quoted(1)
def command_add(self, args):
"""
Add the specified JID to the roster, and set automatically
accept the reverse subscription
"""
- jid = safeJID(safeJID(args.strip()).bare)
+ jid = safeJID(safeJID(args[0]).bare)
if not jid:
- self.core.information(_('No JID specified'), 'Error')
+ self.core.information('No JID specified', 'Error')
return
if jid in roster and roster[jid].subscription in ('to', 'both'):
return self.core.information('Already subscribed.', 'Roster')
@@ -398,7 +629,8 @@ class RosterInfoTab(Tab):
roster.modified()
self.core.information('%s was added to the roster' % jid, 'Roster')
- def command_name(self, arg):
+ @command_args_parser.quoted(1, 1)
+ def command_name(self, args):
"""
Set a name for the specified JID in your roster
"""
@@ -406,15 +638,14 @@ class RosterInfoTab(Tab):
if not iq:
self.core.information('The name could not be set.', 'Error')
log.debug('Error in /name:\n%s', iq)
- args = common.shell_split(arg)
- if not args:
+ if args is None:
return self.core.command_help('name')
jid = safeJID(args[0]).bare
name = args[1] if len(args) == 2 else ''
contact = roster[jid]
if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
+ self.core.information('No such JID in roster', 'Error')
return
groups = set(contact.groups)
@@ -424,24 +655,24 @@ class RosterInfoTab(Tab):
self.core.xmpp.update_roster(jid, name=name, groups=groups,
subscription=subscription, callback=callback)
+ @command_args_parser.quoted(2)
def command_groupadd(self, args):
"""
Add the specified JID to the specified group
"""
- args = common.shell_split(args)
- if len(args) != 2:
- return
+ if args is None:
+ return self.core.command_help('groupadd')
jid = safeJID(args[0]).bare
group = args[1]
contact = roster[jid]
if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
+ self.core.information('No such JID in roster', 'Error')
return
new_groups = set(contact.groups)
if group in new_groups:
- self.core.information(_('JID already in group'), 'Error')
+ self.core.information('JID already in group', 'Error')
return
roster.modified()
@@ -464,12 +695,12 @@ class RosterInfoTab(Tab):
self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
subscription=subscription, callback=callback)
- def command_groupmove(self, arg):
+ @command_args_parser.quoted(3)
+ def command_groupmove(self, args):
"""
Remove the specified JID from the first specified group and add it to the second one
"""
- args = common.shell_split(arg)
- if len(args) != 3:
+ if args is None:
return self.core.command_help('groupmove')
jid = safeJID(args[0]).bare
group_from = args[1]
@@ -477,7 +708,7 @@ class RosterInfoTab(Tab):
contact = roster[jid]
if not contact:
- self.core.information(_('No such JID in roster'), 'Error')
+ self.core.information('No such JID in roster', 'Error')
return
new_groups = set(contact.groups)
@@ -485,19 +716,19 @@ class RosterInfoTab(Tab):
new_groups.remove('none')
if group_to == 'none' or group_from == 'none':
- self.core.information(_('"none" is not a group.'), 'Error')
+ self.core.information('"none" is not a group.', 'Error')
return
if group_from not in new_groups:
- self.core.information(_('JID not in first group'), 'Error')
+ self.core.information('JID not in first group', 'Error')
return
if group_to in new_groups:
- self.core.information(_('JID already in second group'), 'Error')
+ self.core.information('JID already in second group', 'Error')
return
if group_to == group_from:
- self.core.information(_('The groups are the same.'), 'Error')
+ self.core.information('The groups are the same.', 'Error')
return
roster.modified()
@@ -519,19 +750,20 @@ class RosterInfoTab(Tab):
self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
subscription=subscription, callback=callback)
+ @command_args_parser.quoted(2)
def command_groupremove(self, args):
"""
Remove the specified JID from the specified group
"""
- args = common.shell_split(args)
- if len(args) != 2:
- return
+ if args is None:
+ return self.core.command_help('groupremove')
+
jid = safeJID(args[0]).bare
group = args[1]
contact = roster[jid]
if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
+ self.core.information('No such JID in roster', 'Error')
return
new_groups = set(contact.groups)
@@ -540,7 +772,7 @@ class RosterInfoTab(Tab):
except KeyError:
pass
if group not in new_groups:
- self.core.information(_('JID not in group'), 'Error')
+ self.core.information('JID not in group', 'Error')
return
roster.modified()
@@ -559,13 +791,14 @@ class RosterInfoTab(Tab):
self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
subscription=subscription, callback=callback)
+ @command_args_parser.quoted(0, 1)
def command_remove(self, args):
"""
Remove the specified JID from the roster. i.e.: unsubscribe
from its presence, and cancel its subscription to our.
"""
- if args.strip():
- jid = safeJID(args.strip()).bare
+ if args:
+ jid = safeJID(args[0]).bare
else:
item = self.roster_win.selected_row
if isinstance(item, Contact):
@@ -576,12 +809,12 @@ class RosterInfoTab(Tab):
roster.remove(jid)
del roster[jid]
- def command_import(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_import(self, args):
"""
Import the contacts
"""
- args = common.shell_split(arg)
- if len(args):
+ if args:
if args[0].startswith('/'):
filepath = args[0]
else:
@@ -603,12 +836,12 @@ class RosterInfoTab(Tab):
self.command_add(jid.lstrip('\n'))
self.core.information('Contacts imported from %s' % filepath, 'Info')
- def command_export(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_export(self, args):
"""
Export the contacts
"""
- args = common.shell_split(arg)
- if len(args):
+ if args:
if args[0].startswith('/'):
filepath = args[0]
else:
@@ -697,11 +930,12 @@ class RosterInfoTab(Tab):
if contact.pending_in)
return the_input.new_completion(jids, 1, '', quotify=False)
- def command_accept(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_accept(self, args):
"""
Accept a JID from in roster. Authorize it AND subscribe to it
"""
- if not arg:
+ if not args:
item = self.roster_win.selected_row
if isinstance(item, Contact):
jid = item.bare_jid
@@ -709,7 +943,7 @@ class RosterInfoTab(Tab):
self.core.information('No subscription to accept')
return
else:
- jid = safeJID(arg).bare
+ jid = safeJID(args[0]).bare
nodepart = safeJID(jid).user
jid = safeJID(jid)
# crappy transports putting resources inside the node part
@@ -769,13 +1003,15 @@ class RosterInfoTab(Tab):
success = config.silent_set(option, str(not value))
roster.modified()
if not success:
- self.core.information(_('Unable to write in the config file'), 'Error')
+ self.core.information('Unable to write in the config file', 'Error')
return True
def on_slash(self):
"""
'/' is pressed, we enter "input mode"
"""
+ if isinstance(self.input, windows.YesNoInput):
+ return
curses.curs_set(1)
self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
self.input.resize(1, self.width, self.height-1, 0)
@@ -951,6 +1187,8 @@ class RosterInfoTab(Tab):
Start the search. The input should appear with a short instruction
in it.
"""
+ if isinstance(self.input, windows.YesNoInput):
+ return
curses.curs_set(1)
self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter)
self.input.resize(1, self.width, self.height-1, 0)
@@ -961,6 +1199,8 @@ class RosterInfoTab(Tab):
@refresh_wrapper.always
def start_search_slow(self):
+ if isinstance(self.input, windows.YesNoInput):
+ return
curses.curs_set(1)
self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter_slow)
self.input.resize(1, self.width, self.height-1, 0)
diff --git a/src/tabs/xmltab.py b/src/tabs/xmltab.py
index 083e97c5..6899cd6f 100644
--- a/src/tabs/xmltab.py
+++ b/src/tabs/xmltab.py
@@ -5,52 +5,104 @@ in order to only show the relevant ones, and it can also be frozen or
unfrozen on demand so that the relevant information is not drowned by
the traffic.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
import curses
import os
from slixmpp.xmlstream import matcher
-from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.tostring import tostring
+from slixmpp.xmlstream.stanzabase import ElementBase
+from xml.etree import ElementTree as ET
from . import Tab
+import text_buffer
import windows
from xhtml import clean_text
+from decorators import command_args_parser
+from common import safeJID
+
+
+class MatchJID(object):
+
+ def __init__(self, jid, dest=''):
+ self.jid = jid
+ self.dest = dest
+
+ def match(self, xml):
+ from_ = safeJID(xml['from'])
+ to_ = safeJID(xml['to'])
+ if self.jid.full == self.jid.bare:
+ from_ = from_.bare
+ to_ = to_.bare
+
+ if self.dest == 'from':
+ return from_ == self.jid
+ elif self.dest == 'to':
+ return to_ == self.jid
+ return self.jid in (from_, to_)
+
+ def __repr__(self):
+ return '%s%s%s' % (self.dest, ': ' if self.dest else '', self.jid)
+
+MATCHERS_MAPPINGS = {
+ MatchJID: ('JID', lambda obj: repr(obj)),
+ matcher.MatcherId: ('ID', lambda obj: obj._criteria),
+ matcher.MatchXMLMask: ('XMLMask', lambda obj: tostring(obj._criteria)),
+ matcher.MatchXPath: ('XPath', lambda obj: obj._criteria)
+}
class XMLTab(Tab):
def __init__(self):
Tab.__init__(self)
self.state = 'normal'
self.name = 'XMLTab'
- self.text_win = windows.TextWin()
- self.core.xml_buffer.add_window(self.text_win)
+ self.filters = []
+
+ self.core_buffer = self.core.xml_buffer
+ self.filtered_buffer = text_buffer.TextBuffer()
+
self.info_header = windows.XMLInfoWin()
+ self.text_win = windows.XMLTextWin()
+ self.core_buffer.add_window(self.text_win)
self.default_help_message = windows.HelpText("/ to enter a command")
+
self.register_command('close', self.close,
- shortdesc=_("Close this tab."))
+ shortdesc="Close this tab.")
self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the current buffer.'))
+ shortdesc='Clear the current buffer.')
self.register_command('reset', self.command_reset,
- shortdesc=_('Reset the stanza filter.'))
+ shortdesc='Reset the stanza filter.')
self.register_command('filter_id', self.command_filter_id,
usage='<id>',
- desc=_('Show only the stanzas with the id <id>.'),
- shortdesc=_('Filter by id.'))
+ desc='Show only the stanzas with the id <id>.',
+ shortdesc='Filter by id.')
self.register_command('filter_xpath', self.command_filter_xpath,
usage='<xpath>',
- desc=_('Show only the stanzas matching the xpath <xpath>.'),
- shortdesc=_('Filter by XPath.'))
+ desc='Show only the stanzas matching the xpath <xpath>.'
+ ' Any occurrences of %n will be replaced by jabber:client.',
+ shortdesc='Filter by XPath.')
+ self.register_command('filter_jid', self.command_filter_jid,
+ usage='<jid>',
+ desc='Show only the stanzas matching the jid <jid> in from= or to=.',
+ shortdesc='Filter by JID.')
+ self.register_command('filter_from', self.command_filter_from,
+ usage='<jid>',
+ desc='Show only the stanzas matching the jid <jid> in from=.',
+ shortdesc='Filter by JID from.')
+ self.register_command('filter_to', self.command_filter_to,
+ usage='<jid>',
+ desc='Show only the stanzas matching the jid <jid> in to=.',
+ shortdesc='Filter by JID to.')
self.register_command('filter_xmlmask', self.command_filter_xmlmask,
- usage=_('<xml mask>'),
- desc=_('Show only the stanzas matching the given xml mask.'),
- shortdesc=_('Filter by xml mask.'))
+ usage='<xml mask>',
+ desc='Show only the stanzas matching the given xml mask.',
+ shortdesc='Filter by xml mask.')
self.register_command('dump', self.command_dump,
- usage=_('<filename>'),
- desc=_('Writes the content of the XML buffer into a file.'),
- shortdesc=_('Write in a file.'))
+ usage='<filename>',
+ desc='Writes the content of the XML buffer into a file.',
+ shortdesc='Write in a file.')
self.input = self.default_help_message
self.key_func['^T'] = self.close
self.key_func['^I'] = self.completion
@@ -63,6 +115,34 @@ class XMLTab(Tab):
self.filter_type = ''
self.filter = ''
+ def gen_filter_repr(self):
+ if not self.filters:
+ self.filter_type = ''
+ self.filter = ''
+ return
+ filter_types = map(lambda x: MATCHERS_MAPPINGS[type(x)][0], self.filters)
+ filter_strings = map(lambda x: MATCHERS_MAPPINGS[type(x)][1](x), self.filters)
+ self.filter_type = ','.join(filter_types)
+ self.filter = ','.join(filter_strings)
+
+ def update_filters(self, matcher):
+ if not self.filters:
+ messages = self.core_buffer.messages[:]
+ self.filtered_buffer.messages = []
+ self.core_buffer.del_window(self.text_win)
+ self.filtered_buffer.add_window(self.text_win)
+ else:
+ messages = self.filtered_buffer.messages
+ self.filtered_buffer.messages = []
+ self.filters.append(matcher)
+ new_messages = []
+ for msg in messages:
+ if self.match_stanza(ElementBase(ET.fromstring(clean_text(msg.txt)))):
+ new_messages.append(msg)
+ self.filtered_buffer.messages = new_messages
+ self.text_win.rebuild_everything(self.filtered_buffer)
+ self.gen_filter_repr()
+
def on_freeze(self):
"""
Freeze the display.
@@ -70,58 +150,94 @@ class XMLTab(Tab):
self.text_win.toggle_lock()
self.refresh()
- def command_filter_xmlmask(self, arg):
+ def match_stanza(self, stanza):
+ for matcher in self.filters:
+ if not matcher.match(stanza):
+ return False
+ return True
+
+ @command_args_parser.raw
+ def command_filter_xmlmask(self, mask):
"""/filter_xmlmask <xml mask>"""
try:
- handler = Callback('custom matcher', matcher.MatchXMLMask(arg),
- self.core.incoming_stanza)
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(handler)
- self.filter_type = "XML Mask Filter"
- self.filter = arg
+ self.update_filters(matcher.MatchXMLMask(mask))
self.refresh()
- except:
- self.core.information('Invalid XML Mask', 'Error')
+ except Exception as e:
+ self.core.information('Invalid XML Mask: %s' % e, 'Error')
self.command_reset('')
- def command_filter_id(self, arg):
+ @command_args_parser.raw
+ def command_filter_to(self, jid):
+ """/filter_jid_to <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj, dest='to'))
+ self.refresh()
+
+ @command_args_parser.raw
+ def command_filter_from(self, jid):
+ """/filter_jid_from <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj, dest='from'))
+ self.refresh()
+
+ @command_args_parser.raw
+ def command_filter_jid(self, jid):
+ """/filter_jid <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj))
+ self.refresh()
+
+ @command_args_parser.quoted(1)
+ def command_filter_id(self, args):
"""/filter_id <id>"""
- self.core.xmpp.remove_handler('custom matcher')
- handler = Callback('custom matcher', matcher.MatcherId(arg),
- self.core.incoming_stanza)
- self.core.xmpp.register_handler(handler)
- self.filter_type = "Id Filter"
- self.filter = arg
+ if args is None:
+ return self.core.command_help('filter_id')
+
+ self.update_filters(matcher.MatcherId(args[0]))
self.refresh()
- def command_filter_xpath(self, arg):
+ @command_args_parser.raw
+ def command_filter_xpath(self, xpath):
"""/filter_xpath <xpath>"""
try:
- handler = Callback('custom matcher', matcher.MatchXPath(
- arg.replace('%n', self.core.xmpp.default_ns)),
- self.core.incoming_stanza)
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(handler)
- self.filter_type = "XPath Filter"
- self.filter = arg
+ self.update_filters(matcher.MatchXPath(xpath.replace('%n', self.core.xmpp.default_ns)))
self.refresh()
except:
self.core.information('Invalid XML Path', 'Error')
self.command_reset('')
- def command_reset(self, arg):
+ @command_args_parser.ignored
+ def command_reset(self):
"""/reset"""
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(self.core.all_stanzas)
+ if self.filters:
+ self.filters = []
+ self.filtered_buffer.del_window(self.text_win)
+ self.core_buffer.add_window(self.text_win)
+ self.text_win.rebuild_everything(self.core_buffer)
self.filter_type = ''
self.filter = ''
self.refresh()
- def command_dump(self, arg):
+ @command_args_parser.quoted(1)
+ def command_dump(self, args):
"""/dump <filename>"""
- xml = self.core.xml_buffer.messages[:]
- text = '\n'.join(('%s %s' % (msg.str_time, clean_text(msg.txt)) for msg in xml))
- filename = os.path.expandvars(os.path.expanduser(arg))
+ if args is None:
+ return self.core.command_help('dump')
+ if self.filters:
+ xml = self.filtered_buffer.messages[:]
+ else:
+ xml = self.core_buffer.messages[:]
+ text = '\n'.join(('%s %s %s' % (msg.str_time, msg.nickname, clean_text(msg.txt)) for msg in xml))
+ filename = os.path.expandvars(os.path.expanduser(args[0]))
try:
with open(filename, 'w') as fd:
fd.write(text)
@@ -151,12 +267,17 @@ class XMLTab(Tab):
def on_scroll_down(self):
return self.text_win.scroll_down(self.text_win.height-1)
- def command_clear(self, args):
+ @command_args_parser.ignored
+ def command_clear(self):
"""
/clear
"""
- self.core.xml_buffer.messages = []
- self.text_win.rebuild_everything(self.core.xml_buffer)
+ if self.filters:
+ buffer = self.core_buffer
+ else:
+ buffer = self.filtered_buffer
+ buffer.messages = []
+ self.text_win.rebuild_everything(buffer)
self.refresh()
self.core.doupdate()
diff --git a/src/text_buffer.py b/src/text_buffer.py
index 59aa96e1..6bc3ee23 100644
--- a/src/text_buffer.py
+++ b/src/text_buffer.py
@@ -24,6 +24,9 @@ Message = collections.namedtuple('Message', message_fields)
class CorrectionError(Exception):
pass
+class AckError(Exception):
+ pass
+
def other_elems(self):
"Helper for the repr_message function"
acc = ['Message(']
@@ -84,7 +87,7 @@ class TextBuffer(object):
@staticmethod
def make_message(txt, time, nickname, nick_color, history, user,
identifier, str_time=None, highlight=False,
- old_message=None, revisions=0, jid=None, ack=None):
+ old_message=None, revisions=0, jid=None, ack=0):
"""
Create a new Message object with parameters, check for /me messages,
and delayed messages
@@ -125,7 +128,7 @@ class TextBuffer(object):
def add_message(self, txt, time=None, nickname=None,
nick_color=None, history=None, user=None, highlight=False,
- identifier=None, str_time=None, jid=None, ack=None):
+ identifier=None, str_time=None, jid=None, ack=0):
"""
Create a message and add it to the text buffer
"""
@@ -161,16 +164,31 @@ class TextBuffer(object):
return i
return -1
- def ack_message(self, old_id):
+ def ack_message(self, old_id, jid):
+ """Mark a message as acked"""
+ return self.edit_ack(1, old_id, jid)
+
+ def nack_message(self, error, old_id, jid):
+ """Mark a message as errored"""
+ return self.edit_ack(-1, old_id, jid, append=error)
+
+ def edit_ack(self, value, old_id, jid, append=''):
"""
- Ack a message
+ Edit the ack status of a message, and optionally
+ append some text.
"""
i = self._find_message(old_id)
if i == -1:
return
msg = self.messages[i]
+ if msg.jid != jid:
+ raise AckError('Wrong JID for message id %s (was %s, expected %s)' %
+ (old_id, msg.jid, jid))
+
new_msg = list(msg)
- new_msg[12] = True
+ new_msg[12] = value
+ if append:
+ new_msg[0] = new_msg[0] + append
new_msg = Message(*new_msg)
self.messages[i] = new_msg
return new_msg
diff --git a/src/theming.py b/src/theming.py
index 1e9d6c40..ae71e48f 100755
--- a/src/theming.py
+++ b/src/theming.py
@@ -69,20 +69,18 @@ log = logging.getLogger(__name__)
from config import config
import curses
-import imp
import os
from os import path
-from sys import version_info
-if version_info[1] >= 3:
- from importlib import machinery
- finder = machinery.PathFinder()
+from importlib import machinery
+finder = machinery.PathFinder()
class Theme(object):
"""
- The theme class, from which all theme should inherit.
- All of the following value can be replaced in subclasses, in
+ The theme class, from which all themes should inherit.
+ All of the following values can be replaced in subclasses, in
order to create a new theme.
+
Do not edit this file if you want to change the theme to suit your
needs. Create a new theme and share it if you think it can be useful
for others.
@@ -178,6 +176,13 @@ class Theme(object):
CHAR_AFFILIATION_MEMBER = '+'
CHAR_AFFILIATION_NONE = '-'
+
+ # XML Tab
+ CHAR_XML_IN = 'IN '
+ CHAR_XML_OUT = 'OUT'
+ COLOR_XML_IN = (1, -1)
+ COLOR_XML_OUT = (2, -1)
+
# Color for the /me message
COLOR_ME_MESSAGE = (6, -1)
@@ -305,6 +310,7 @@ class Theme(object):
CHAR_ERROR = '✖'
CHAR_EMPTY = ' '
CHAR_ACK_RECEIVED = CHAR_OK
+ CHAR_NACK = CHAR_ERROR
CHAR_COLUMN_ASC = ' ▲'
CHAR_COLUMN_DESC = ' ▼'
CHAR_ROSTER_ERROR = CHAR_ERROR
@@ -319,6 +325,7 @@ class Theme(object):
CHAR_ROSTER_NONE = '⇹'
COLOR_CHAR_ACK = (2, -1)
+ COLOR_CHAR_NACK = (1, -1)
COLOR_ROSTER_GAMING = (6, -1)
COLOR_ROSTER_MOOD = (2, -1)
@@ -493,21 +500,13 @@ def reload_theme():
new_theme = None
exc = None
try:
- if version_info[1] < 3:
- file, filename, info = imp.find_module(theme_name, load_path)
- imp.acquire_lock()
- new_theme = imp.load_module(theme_name, file, filename, info)
- else:
- loader = finder.find_module(theme_name, load_path)
- if not loader:
- return 'Failed to load the theme %s' % theme_name
- new_theme = loader.load_module()
+ loader = finder.find_module(theme_name, load_path)
+ if not loader:
+ return 'Failed to load the theme %s' % theme_name
+ new_theme = loader.load_module()
except Exception as e:
log.error('Failed to load the theme %s', theme_name, exc_info=True)
exc = e
- finally:
- if version_info[1] < 3 and imp.lock_held():
- imp.release_lock()
if not new_theme:
return 'Failed to load theme: %s' % exc
diff --git a/src/user.py b/src/user.py
index 0d29569f..b1796bc3 100644
--- a/src/user.py
+++ b/src/user.py
@@ -12,9 +12,14 @@ An user is a MUC participant, not a roster contact (see contact.py)
from random import choice
from datetime import timedelta, datetime
+from hashlib import md5
+import xhtml
from theming import get_theme
+import logging
+log = logging.getLogger(__name__)
+
ROLE_DICT = {
'':0,
'none':0,
@@ -27,14 +32,26 @@ class User(object):
"""
keep trace of an user in a Room
"""
- def __init__(self, nick, affiliation, show, status, role, jid):
+ def __init__(self, nick, affiliation, show, status, role, jid, deterministic=True, color=''):
self.last_talked = datetime(1, 1, 1) # The oldest possible time
self.update(affiliation, show, status, role)
self.change_nick(nick)
- self.color = choice(get_theme().LIST_COLOR_NICKNAMES)
+ if color != '':
+ self.change_color(color, deterministic)
+ else:
+ if deterministic:
+ self.set_deterministic_color()
+ else:
+ self.color = choice(get_theme().LIST_COLOR_NICKNAMES)
self.jid = jid
self.chatstate = None
+ def set_deterministic_color(self):
+ theme = get_theme()
+ mod = len(theme.LIST_COLOR_NICKNAMES)
+ nick_pos = int(md5(self.nick.encode('utf-8')).hexdigest(), 16) % mod
+ self.color = theme.LIST_COLOR_NICKNAMES[nick_pos]
+
def update(self, affiliation, show, status, role):
self.affiliation = affiliation
self.show = show
@@ -46,6 +63,17 @@ class User(object):
def change_nick(self, nick):
self.nick = nick
+ def change_color(self, color_name, deterministic=False):
+ color = xhtml.colors.get(color_name)
+ if color == None:
+ log.error('Unknown color "%s"' % color_name)
+ if deterministic:
+ self.set_deterministic_color()
+ else:
+ self.color = choice(get_theme().LIST_COLOR_NICKNAMES)
+ else:
+ self.color = (color, -1)
+
def set_last_talked(self, time):
"""
time: datetime object
diff --git a/src/windows/__init__.py b/src/windows/__init__.py
index 9e165201..5ec73961 100644
--- a/src/windows/__init__.py
+++ b/src/windows/__init__.py
@@ -5,15 +5,16 @@ used to display information on the screen
from . base_wins import Win
from . data_forms import FormWin
+from . bookmark_forms import BookmarksWin
from . info_bar import GlobalInfoBar, VerticalGlobalInfoBar
from . info_wins import InfoWin, XMLInfoWin, PrivateInfoWin, MucListInfoWin, \
ConversationInfoWin, DynamicConversationInfoWin, MucInfoWin, \
- ConversationStatusMessageWin
+ ConversationStatusMessageWin, BookmarksInfoWin
from . input_placeholders import HelpText, YesNoInput
from . inputs import Input, HistoryInput, MessageInput, CommandInput
from . list import ListWin, ColumnHeaderWin
from . misc import VerticalSeparator
from . muc import UserList, Topic
from . roster_win import RosterWin, ContactInfoWin
-from . text_win import TextWin
+from . text_win import TextWin, XMLTextWin
diff --git a/src/windows/bookmark_forms.py b/src/windows/bookmark_forms.py
new file mode 100644
index 00000000..7cbd30cc
--- /dev/null
+++ b/src/windows/bookmark_forms.py
@@ -0,0 +1,278 @@
+"""
+Windows used inthe bookmarkstab
+"""
+import curses
+
+from . import Win
+from . inputs import Input
+from . data_forms import FieldInput
+from theming import to_curses_attr, get_theme
+from common import safeJID
+
+class BookmarkJIDInput(FieldInput, Input):
+ def __init__(self, field):
+ FieldInput.__init__(self, field)
+ Input.__init__(self)
+ jid = safeJID(field.jid)
+ jid.resource = field.nick
+ self.text = jid.full
+ self.pos = len(self.text)
+ self.color = get_theme().COLOR_NORMAL_TEXT
+
+ def save(self):
+ jid = safeJID(self.get_text())
+ self._field.jid = jid.bare
+ self._field.name = jid.bare
+ self._field.nick = jid.resource
+
+ def get_help_message(self):
+ return 'Edit the text'
+
+class BookmarkMethodInput(FieldInput, Win):
+ def __init__(self, field):
+ FieldInput.__init__(self, field)
+ Win.__init__(self)
+ self.options = ('local', 'remote')
+ # val_pos is the position of the currently selected option
+ self.val_pos = self.options.index(field.method)
+
+ def do_command(self, key):
+ if key == 'KEY_LEFT':
+ if self.val_pos > 0:
+ self.val_pos -= 1
+ elif key == 'KEY_RIGHT':
+ if self.val_pos < len(self.options)-1:
+ self.val_pos += 1
+ else:
+ return
+ self.refresh()
+
+ def refresh(self):
+ self._win.erase()
+ self._win.attron(to_curses_attr(self.color))
+ self.addnstr(0, 0, ' '*self.width, self.width)
+ if self.val_pos > 0:
+ self.addstr(0, 0, '←')
+ if self.val_pos < len(self.options)-1:
+ self.addstr(0, self.width-1, '→')
+ if self.options:
+ option = self.options[self.val_pos]
+ self.addstr(0, self.width//2-len(option)//2, option)
+ self._win.attroff(to_curses_attr(self.color))
+ self._refresh()
+
+ def save(self):
+ self._field.method = self.options[self.val_pos]
+
+ def get_help_message(self):
+ return '←, →: Select a value amongst the others'
+
+class BookmarkPasswordInput(FieldInput, Input):
+ def __init__(self, field):
+ FieldInput.__init__(self, field)
+ Input.__init__(self)
+ self.text = field.password or ''
+ self.pos = len(self.text)
+ self.color = get_theme().COLOR_NORMAL_TEXT
+
+ def rewrite_text(self):
+ self._win.erase()
+ if self.color:
+ self._win.attron(to_curses_attr(self.color))
+ self.addstr('*'*len(self.text[self.view_pos:self.view_pos+self.width-1]))
+ if self.color:
+ (y, x) = self._win.getyx()
+ size = self.width-x
+ self.addnstr(' '*size, size, to_curses_attr(self.color))
+ self.addstr(0, self.pos, '')
+ if self.color:
+ self._win.attroff(to_curses_attr(self.color))
+ self._refresh()
+
+ def save(self):
+ self._field.password = self.get_text() or None
+
+ def get_help_message(self):
+ return 'Edit the secret text'
+
+class BookmarkAutojoinWin(FieldInput, Win):
+ def __init__(self, field):
+ FieldInput.__init__(self, field)
+ Win.__init__(self)
+ self.last_key = 'KEY_RIGHT'
+ self.value = field.autojoin
+
+ def do_command(self, key):
+ if key == 'KEY_LEFT' or key == 'KEY_RIGHT':
+ self.value = not self.value
+ self.last_key = key
+ self.refresh()
+
+ def refresh(self):
+ self._win.erase()
+ self._win.attron(to_curses_attr(self.color))
+ format_string = '←{:^%s}→' % 7
+ inp = format_string.format(repr(self.value))
+ self.addstr(0, 0, inp)
+ if self.last_key == 'KEY_RIGHT':
+ self.move(0, 8)
+ else:
+ self.move(0, 0)
+ self._win.attroff(to_curses_attr(self.color))
+ self._refresh()
+
+ def save(self):
+ self._field.autojoin = self.value
+
+ def get_help_message(self):
+ return '← and →: change the value between True and False'
+
+
+class BookmarksWin(Win):
+ def __init__(self, bookmarks, height, width, y, x):
+ self._win = Win._tab_win.derwin(height, width, y, x)
+ self.scroll_pos = 0
+ self._current_input = 0
+ self.current_horizontal_input = 0
+ self._bookmarks = list(bookmarks)
+ self.lines = []
+ for bookmark in sorted(self._bookmarks, key=lambda x: x.jid):
+ self.lines.append((BookmarkJIDInput(bookmark),
+ BookmarkPasswordInput(bookmark),
+ BookmarkAutojoinWin(bookmark),
+ BookmarkMethodInput(bookmark)))
+
+ @property
+ def current_input(self):
+ return self._current_input
+
+ @current_input.setter
+ def current_input(self, value):
+ if 0 <= self._current_input < len(self.lines):
+ if 0 <= value < len(self.lines):
+ self.lines[self._current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT)
+ self._current_input = value
+ else:
+ self._current_input = 0
+
+ def add_bookmark(self, bookmark):
+ self.lines.append((BookmarkJIDInput(bookmark),
+ BookmarkPasswordInput(bookmark),
+ BookmarkAutojoinWin(bookmark),
+ BookmarkMethodInput(bookmark)))
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT)
+ self.current_horizontal_input = 0
+ self.current_input = len(self.lines) - 1
+ if self.current_input - self.scroll_pos > self.height-1:
+ self.scroll_pos = self.current_input - self.height + 1
+ self.refresh()
+
+ def del_current_bookmark(self):
+ if self.lines:
+ bm = self.lines[self.current_input][0]._field
+ to_delete = self.current_input
+ self.current_input -= 1
+ del self.lines[to_delete]
+ if self.scroll_pos:
+ self.scroll_pos -= 1
+ self.refresh()
+ return bm
+
+ def resize(self, height, width, y, x):
+ self.height = height
+ self.width = width
+ self._win = Win._tab_win.derwin(height, width, y, x)
+ # Adjust the scroll position, if resizing made the window too small
+ # for the cursor to be visible
+ while self.current_input - self.scroll_pos > self.height-1:
+ self.scroll_pos += 1
+
+ def go_to_next_line_input(self):
+ if not self.lines:
+ return
+ if self.current_input == len(self.lines) - 1:
+ return
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT)
+ # Adjust the scroll position if the current_input would be outside
+ # of the visible area
+ if self.current_input + 1 - self.scroll_pos > self.height-1:
+ self.current_input += 1
+ self.scroll_pos += 1
+ self.refresh()
+ else:
+ self.current_input += 1
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW)
+
+ def go_to_previous_line_input(self):
+ if not self.lines:
+ return
+ if self.current_input == 0:
+ return
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT)
+ self.current_input -= 1
+ # Adjust the scroll position if the current_input would be outside
+ # of the visible area
+ if self.current_input < self.scroll_pos:
+ self.scroll_pos = self.current_input
+ self.refresh()
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW)
+
+ def go_to_next_horizontal_input(self):
+ if not self.lines:
+ return
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT)
+ self.current_horizontal_input += 1
+ if self.current_horizontal_input > 3:
+ self.current_horizontal_input = 0
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW)
+
+ def go_to_previous_horizontal_input(self):
+ if not self.lines:
+ return
+ if self.current_horizontal_input == 0:
+ return
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT)
+ self.current_horizontal_input -= 1
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW)
+
+ def on_input(self, key):
+ if not self.lines:
+ return
+ self.lines[self.current_input][self.current_horizontal_input].do_command(key)
+
+ def refresh(self):
+ # store the cursor status
+ self._win.erase()
+ y = - self.scroll_pos
+ for i in range(len(self.lines)):
+ self.lines[i][0].resize(1, self.width//3, y + 1, 0)
+ self.lines[i][1].resize(1, self.width//3, y + 1, self.width//3)
+ self.lines[i][2].resize(1, self.width//6, y + 1, 2*self.width//3)
+ self.lines[i][3].resize(1, self.width//6, y + 1, 5*self.width//6)
+ y += 1
+ self._refresh()
+ for i, inp in enumerate(self.lines):
+ if i < self.scroll_pos:
+ continue
+ if i >= self.height + self.scroll_pos:
+ break
+ for j in range(4):
+ inp[j].refresh()
+
+ if self.lines and self.current_input < self.height-1:
+ self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW)
+ self.lines[self.current_input][self.current_horizontal_input].refresh()
+ if not self.lines:
+ curses.curs_set(0)
+ else:
+ curses.curs_set(1)
+
+ def refresh_current_input(self):
+ if self.lines:
+ self.lines[self.current_input][self.current_horizontal_input].refresh()
+
+ def save(self):
+ for line in self.lines:
+ for item in line:
+ item.save()
+
diff --git a/src/windows/data_forms.py b/src/windows/data_forms.py
index d6e2cc66..86f33350 100644
--- a/src/windows/data_forms.py
+++ b/src/windows/data_forms.py
@@ -469,4 +469,3 @@ class FormWin(object):
return self.inputs[self.current_input]['input'].get_help_message()
return ''
-
diff --git a/src/windows/funcs.py b/src/windows/funcs.py
index d58d4683..f1401628 100644
--- a/src/windows/funcs.py
+++ b/src/windows/funcs.py
@@ -4,7 +4,6 @@ Standalone functions used by the modules
import string
-from config import config
from . base_wins import FORMAT_CHAR, format_chars
def find_first_format_char(text, chars=None):
@@ -19,8 +18,7 @@ def find_first_format_char(text, chars=None):
pos = p
return pos
-def truncate_nick(nick, size=None):
- size = size or config.get('max_nick_length')
+def truncate_nick(nick, size=10):
if size < 1:
size = 1
if nick and len(nick) > size:
diff --git a/src/windows/info_bar.py b/src/windows/info_bar.py
index e66343c5..abd956cd 100644
--- a/src/windows/info_bar.py
+++ b/src/windows/info_bar.py
@@ -28,6 +28,7 @@ class GlobalInfoBar(Win):
show_names = config.get('show_tab_names')
show_nums = config.get('show_tab_numbers')
use_nicks = config.get('use_tab_nicks')
+ show_inactive = config.get('show_inactive_tabs')
# ignore any remaining gap tabs if the feature is not enabled
if create_gaps:
sorted_tabs = self.core.tabs[:]
@@ -37,8 +38,7 @@ class GlobalInfoBar(Win):
for nb, tab in enumerate(sorted_tabs):
if not tab: continue
color = tab.color
- if not config.get('show_inactive_tabs') and\
- color is get_theme().COLOR_TAB_NORMAL:
+ if not show_inactive and color is get_theme().COLOR_TAB_NORMAL:
continue
try:
if show_nums or not show_names:
@@ -87,9 +87,10 @@ class VerticalGlobalInfoBar(Win):
sorted_tabs = sorted_tabs[-height:]
else:
sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2]
+ asc_sort = (config.get('vertical_tab_list_sort') == 'asc')
for y, tab in enumerate(sorted_tabs):
color = tab.vertical_color
- if not config.get('vertical_tab_list_sort') != 'asc':
+ if asc_sort:
y = height - y - 1
self.addstr(y, 0, "%2d" % tab.nb,
to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER))
diff --git a/src/windows/info_wins.py b/src/windows/info_wins.py
index 766afb75..80af4602 100644
--- a/src/windows/info_wins.py
+++ b/src/windows/info_wins.py
@@ -293,3 +293,17 @@ class ConversationStatusMessageWin(InfoWin):
def write_status_message(self, resource):
self.addstr(resource.status, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+class BookmarksInfoWin(InfoWin):
+ def __init__(self):
+ InfoWin.__init__(self)
+
+ def refresh(self, preferred):
+ log.debug('Refresh: %s', self.__class__.__name__)
+ self._win.erase()
+ self.write_remote_status(preferred)
+ self.finish_line(get_theme().COLOR_INFORMATION_BAR)
+ self._refresh()
+
+ def write_remote_status(self, preferred):
+ self.addstr('Remote storage: %s' % preferred, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+
diff --git a/src/windows/input_placeholders.py b/src/windows/input_placeholders.py
index 8bcf1524..496417d1 100644
--- a/src/windows/input_placeholders.py
+++ b/src/windows/input_placeholders.py
@@ -41,7 +41,7 @@ class YesNoInput(Win):
A Window just displaying a Yes/No input
Used to ask a confirmation
"""
- def __init__(self, text=''):
+ def __init__(self, text='', callback=None):
Win.__init__(self)
self.key_func = {
'y' : self.on_yes,
@@ -49,6 +49,7 @@ class YesNoInput(Win):
}
self.txt = text
self.value = None
+ self.callback = callback
def on_yes(self):
self.value = True
@@ -68,17 +69,8 @@ class YesNoInput(Win):
def do_command(self, key, raw=False):
if key.lower() in self.key_func:
self.key_func[key]()
-
- def prompt(self):
- """Monopolizes the input while waiting for a recognized keypress"""
- def cb(key):
- if key in self.key_func:
- self.key_func[key]()
- if self.value is None:
- # We didn’t finish with this prompt, continue monopolizing
- # it again until value is set
- keyboard.continuation_keys_callback = cb
- keyboard.continuation_keys_callback = cb
+ if self.value is not None and self.callback is not None:
+ return self.callback()
def on_delete(self):
return
diff --git a/src/windows/inputs.py b/src/windows/inputs.py
index d345443b..12d3a9a2 100644
--- a/src/windows/inputs.py
+++ b/src/windows/inputs.py
@@ -43,6 +43,8 @@ class Input(Win):
'^D': self.key_dc,
'M-b': self.jump_word_left,
"M-[1;5D": self.jump_word_left,
+ "kRIT5": self.jump_word_right,
+ "kLFT5": self.jump_word_left,
'^W': self.delete_word,
'M-d': self.delete_next_word,
'^K': self.delete_end_of_line,
@@ -534,6 +536,11 @@ class Input(Win):
if self.view_pos < 0:
self.view_pos = 0
+ # text small enough to fit inside the window entirely:
+ # remove scrolling if present
+ if poopt.wcswidth(self.text) < self.width:
+ self.view_pos = 0
+
assert(self.pos >= self.view_pos and
self.pos <= self.view_pos + max(self.width, 3))
diff --git a/src/windows/muc.py b/src/windows/muc.py
index 7e3541ba..c4e8df6e 100644
--- a/src/windows/muc.py
+++ b/src/windows/muc.py
@@ -37,7 +37,8 @@ class UserList(Win):
if config.get('hide_user_list'):
return # do not refresh if this win is hidden.
self._win.erase()
- if config.get('user_list_sort').lower() == 'asc':
+ asc_sort = (config.get('user_list_sort').lower() == 'asc')
+ if asc_sort:
y, x = self._win.getmaxyx()
y -= 1
users = sorted(users)
@@ -55,7 +56,7 @@ class UserList(Win):
self.addstr(y, 2,
poopt.cut_by_columns(user.nick, self.width - 2),
to_curses_attr(user.color))
- if config.get('user_list_sort').lower() == 'asc':
+ if asc_sort:
y -= 1
else:
y += 1
@@ -63,12 +64,12 @@ class UserList(Win):
break
# draw indicators of position in the list
if self.pos > 0:
- if config.get('user_list_sort').lower() == 'asc':
+ if asc_sort:
self.draw_plus(self.height-1)
else:
self.draw_plus(0)
if self.pos + self.height < len(users):
- if config.get('user_list_sort').lower() == 'asc':
+ if asc_sort:
self.draw_plus(0)
else:
self.draw_plus(self.height-1)
diff --git a/src/windows/roster_win.py b/src/windows/roster_win.py
index 6ecb6128..a2e2badd 100644
--- a/src/windows/roster_win.py
+++ b/src/windows/roster_win.py
@@ -145,6 +145,12 @@ class RosterWin(Win):
# draw the roster from the cache
roster_view = self.roster_cache[self.start_pos-1:self.start_pos+self.height]
+ options = {
+ 'show_roster_sub': config.get('show_roster_subscriptions'),
+ 'show_s2s_errors': config.get('show_s2s_errors'),
+ 'show_roster_jids': config.get('show_roster_jids')
+ }
+
for item in roster_view:
draw_selected = False
if y -2 + self.start_pos == self.pos:
@@ -155,7 +161,7 @@ class RosterWin(Win):
self.draw_group(y, item, draw_selected)
group = item.name
elif isinstance(item, Contact):
- self.draw_contact_line(y, item, draw_selected, group)
+ self.draw_contact_line(y, item, draw_selected, group, **options)
elif isinstance(item, Resource):
self.draw_resource_line(y, item, draw_selected)
@@ -206,7 +212,8 @@ class RosterWin(Win):
return name
return name[:self.width - added - 1] + '…'
- def draw_contact_line(self, y, contact, colored, group):
+ def draw_contact_line(self, y, contact, colored, group, show_roster_sub=False,
+ show_s2s_errors=True, show_roster_jids=False):
"""
Draw on a line all informations about one contact.
This is basically the highest priority resource's informations
@@ -229,15 +236,13 @@ class RosterWin(Win):
self.addstr(y, 0, ' ')
self.addstr(theme.CHAR_STATUS, to_curses_attr(color))
- show_roster_sub = config.get('show_roster_subscriptions')
-
self.addstr(' ')
if resource:
self.addstr('[+] ' if contact.folded(group) else '[-] ')
added += 4
if contact.ask:
added += len(get_theme().CHAR_ROSTER_ASKED)
- if config.get('show_s2s_errors') and contact.error:
+ if show_s2s_errors and contact.error:
added += len(get_theme().CHAR_ROSTER_ERROR)
if contact.tune:
added += len(get_theme().CHAR_ROSTER_TUNE)
@@ -250,7 +255,7 @@ class RosterWin(Win):
if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'):
added += len(theme.char_subscription(contact.subscription, keep=show_roster_sub))
- if not config.get('show_roster_jids') and contact.name:
+ if not show_roster_jids and contact.name:
display_name = '%s' % contact.name
elif contact.name and contact.name != contact.bare_jid:
display_name = '%s (%s)' % (contact.name, contact.bare_jid)
@@ -268,7 +273,7 @@ class RosterWin(Win):
self.addstr(theme.char_subscription(contact.subscription, keep=show_roster_sub), to_curses_attr(theme.COLOR_ROSTER_SUBSCRIPTION))
if contact.ask:
self.addstr(get_theme().CHAR_ROSTER_ASKED, to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT))
- if config.get('show_s2s_errors') and contact.error:
+ if show_s2s_errors and contact.error:
self.addstr(get_theme().CHAR_ROSTER_ERROR, to_curses_attr(get_theme().COLOR_ROSTER_ERROR))
if contact.tune:
self.addstr(get_theme().CHAR_ROSTER_TUNE, to_curses_attr(get_theme().COLOR_ROSTER_TUNE))
diff --git a/src/windows/text_win.py b/src/windows/text_win.py
index 6fe74f41..59c5230b 100644
--- a/src/windows/text_win.py
+++ b/src/windows/text_win.py
@@ -18,7 +18,7 @@ from config import config
from theming import to_curses_attr, get_theme, dump_tuple
-class TextWin(Win):
+class BaseTextWin(Win):
def __init__(self, lines_nb_limit=None):
if lines_nb_limit is None:
lines_nb_limit = config.get('max_lines_in_memory')
@@ -30,19 +30,6 @@ class TextWin(Win):
self.lock = False
self.lock_buffer = []
-
- # the Lines of the highlights in that buffer
- self.highlights = []
- # the current HL position in that list NaN means that we’re not on
- # an hl. -1 is a valid position (it's before the first hl of the
- # list. i.e the separator, in the case where there’s no hl before
- # it.)
- self.hl_pos = float('nan')
-
- # Keep track of the number of hl after the separator.
- # This is useful to make “go to next highlight“ work after a “move to separator”.
- self.nb_of_highlights_after_separator = 0
-
self.separator_after = None
def toggle_lock(self):
@@ -60,6 +47,114 @@ class TextWin(Win):
self.built_lines.append(line)
self.lock = False
+ def scroll_up(self, dist=14):
+ pos = self.pos
+ self.pos += dist
+ if self.pos + self.height > len(self.built_lines):
+ self.pos = len(self.built_lines) - self.height
+ if self.pos < 0:
+ self.pos = 0
+ return self.pos != pos
+
+ def scroll_down(self, dist=14):
+ pos = self.pos
+ self.pos -= dist
+ if self.pos <= 0:
+ self.pos = 0
+ return self.pos != pos
+
+ def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False, nick_size=10):
+ """
+ Take one message, build it and add it to the list
+ Return the number of lines that are built for the given
+ message.
+ """
+ lines = self.build_message(message, timestamp=timestamp, nick_size=nick_size)
+ if self.lock:
+ self.lock_buffer.extend(lines)
+ else:
+ self.built_lines.extend(lines)
+ if not lines or not lines[0]:
+ return 0
+ if clean:
+ while len(self.built_lines) > self.lines_nb_limit:
+ self.built_lines.pop(0)
+ return len(lines)
+
+ def build_message(self, message, timestamp=False, nick_size=10):
+ """
+ Build a list of lines from a message, without adding it
+ to a list
+ """
+ pass
+
+ def refresh(self):
+ pass
+
+ def write_text(self, y, x, txt):
+ """
+ write the text of a line.
+ """
+ self.addstr_colored(txt, y, x)
+
+ def write_time(self, time):
+ """
+ Write the date on the yth line of the window
+ """
+ if time:
+ self.addstr(time)
+ self.addstr(' ')
+
+ def resize(self, height, width, y, x, room=None):
+ if hasattr(self, 'width'):
+ old_width = self.width
+ else:
+ old_width = None
+ self._resize(height, width, y, x)
+ if room and self.width != old_width:
+ self.rebuild_everything(room)
+
+ # reposition the scrolling after resize
+ # (see #2450)
+ buf_size = len(self.built_lines)
+ if buf_size - self.pos < self.height:
+ self.pos = buf_size - self.height
+ if self.pos < 0:
+ self.pos = 0
+
+ def rebuild_everything(self, room):
+ self.built_lines = []
+ with_timestamps = config.get('show_timestamps')
+ nick_size = config.get('max_nick_length')
+ for message in room.messages:
+ self.build_new_message(message, clean=False, timestamp=with_timestamps, nick_size=nick_size)
+ if self.separator_after is message:
+ self.build_new_message(None)
+ while len(self.built_lines) > self.lines_nb_limit:
+ self.built_lines.pop(0)
+
+ def __del__(self):
+ log.debug('** TextWin: deleting %s built lines', (len(self.built_lines)))
+ del self.built_lines
+
+class TextWin(BaseTextWin):
+ def __init__(self, lines_nb_limit=None):
+ BaseTextWin.__init__(self, lines_nb_limit)
+
+ # the Lines of the highlights in that buffer
+ self.highlights = []
+ # the current HL position in that list NaN means that we’re not on
+ # an hl. -1 is a valid position (it's before the first hl of the
+ # list. i.e the separator, in the case where there’s no hl before
+ # it.)
+ self.hl_pos = float('nan')
+
+ # Keep track of the number of hl after the separator.
+ # This is useful to make “go to next highlight“ work after a “move to separator”.
+ self.nb_of_highlights_after_separator = 0
+
+ self.separator_after = None
+
def next_highlight(self):
"""
Go to the next highlight in the buffer.
@@ -130,22 +225,6 @@ class TextWin(Win):
if self.pos < 0 or self.pos >= len(self.built_lines):
self.pos = 0
- def scroll_up(self, dist=14):
- pos = self.pos
- self.pos += dist
- if self.pos + self.height > len(self.built_lines):
- self.pos = len(self.built_lines) - self.height
- if self.pos < 0:
- self.pos = 0
- return self.pos != pos
-
- def scroll_down(self, dist=14):
- pos = self.pos
- self.pos -= dist
- if self.pos <= 0:
- self.pos = 0
- return self.pos != pos
-
def scroll_to_separator(self):
"""
Scroll until separator is centered. If no separator is
@@ -187,13 +266,13 @@ class TextWin(Win):
if room and room.messages:
self.separator_after = room.messages[-1]
- def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False):
+ def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False, nick_size=10):
"""
Take one message, build it and add it to the list
Return the number of lines that are built for the given
message.
"""
- lines = self.build_message(message, timestamp=timestamp)
+ lines = self.build_message(message, timestamp=timestamp, nick_size=nick_size)
if self.lock:
self.lock_buffer.extend(lines)
else:
@@ -210,7 +289,7 @@ class TextWin(Win):
self.built_lines.pop(0)
return len(lines)
- def build_message(self, message, timestamp=False):
+ def build_message(self, message, timestamp=False, nick_size=10):
"""
Build a list of lines from a message, without adding it
to a list
@@ -226,10 +305,13 @@ class TextWin(Win):
else:
default_color = None
ret = []
- nick = truncate_nick(message.nickname)
+ nick = truncate_nick(message.nickname, nick_size)
offset = 0
if message.ack:
- offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1
+ if message.ack > 0:
+ offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1
+ else:
+ offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1
if nick:
offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length
if message.revisions > 0:
@@ -268,12 +350,14 @@ class TextWin(Win):
else:
lines = self.built_lines[-self.height-self.pos:-self.pos]
with_timestamps = config.get("show_timestamps")
+ nick_size = config.get("max_nick_length")
self._win.move(0, 0)
self._win.erase()
for y, line in enumerate(lines):
if line:
msg = line.msg
if line.start_pos == 0:
+ nick = truncate_nick(msg.nickname, nick_size)
if msg.nick_color:
color = msg.nick_color
elif msg.user:
@@ -283,18 +367,21 @@ class TextWin(Win):
if with_timestamps:
self.write_time(msg.str_time)
if msg.ack:
- self.write_ack()
+ if msg.ack > 0:
+ self.write_ack()
+ else:
+ self.write_nack()
if msg.me:
self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE))
self.addstr('* ')
- self.write_nickname(msg.nickname, color, msg.highlight)
+ self.write_nickname(nick, color, msg.highlight)
if msg.revisions:
self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE))
self.addstr('%d' % msg.revisions)
self._win.attrset(0)
self.addstr(' ')
else:
- self.write_nickname(msg.nickname, color, msg.highlight)
+ self.write_nickname(nick, color, msg.highlight)
if msg.revisions:
self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE))
self.addstr('%d' % msg.revisions)
@@ -317,8 +404,7 @@ class TextWin(Win):
# Offset for the nickname (if any)
# plus a space and a > after it
if line.msg.nickname:
- offset += poopt.wcswidth(
- truncate_nick(line.msg.nickname))
+ offset += poopt.wcswidth(truncate_nick(line.msg.nickname, nick_size))
if line.msg.me:
offset += 3
else:
@@ -326,8 +412,11 @@ class TextWin(Win):
offset += ceil(log10(line.msg.revisions + 1))
if line.msg.ack:
- offset += 1 + poopt.wcswidth(
- get_theme().CHAR_ACK_RECEIVED)
+ if msg.ack > 0:
+ offset += 1 + poopt.wcswidth(
+ get_theme().CHAR_ACK_RECEIVED)
+ else:
+ offset += 1 + poopt.wcswidth(get_theme().CHAR_NACK)
self.write_text(y, offset,
line.prepend+line.msg.txt[line.start_pos:line.end_pos])
@@ -343,12 +432,6 @@ class TextWin(Win):
self.width,
to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR))
- def write_text(self, y, x, txt):
- """
- write the text of a line.
- """
- self.addstr_colored(txt, y, x)
-
def write_ack(self):
color = get_theme().COLOR_CHAR_ACK
self._win.attron(to_curses_attr(color))
@@ -356,6 +439,13 @@ class TextWin(Win):
self._win.attroff(to_curses_attr(color))
self.addstr(' ')
+ def write_nack(self):
+ color = get_theme().COLOR_CHAR_NACK
+ self._win.attron(to_curses_attr(color))
+ self.addstr(get_theme().CHAR_NACK)
+ self._win.attroff(to_curses_attr(color))
+ self.addstr(' ')
+
def write_nickname(self, nickname, color, highlight=False):
"""
Write the nickname, using the user's color
@@ -371,53 +461,19 @@ class TextWin(Win):
color = hl_color
if color:
self._win.attron(to_curses_attr(color))
- self.addstr(truncate_nick(nickname))
+ self.addstr(nickname)
if color:
self._win.attroff(to_curses_attr(color))
if highlight and hl_color == "reverse":
self._win.attroff(curses.A_REVERSE)
- def write_time(self, time):
- """
- Write the date on the yth line of the window
- """
- if time:
- self.addstr(time)
- self.addstr(' ')
-
- def resize(self, height, width, y, x, room=None):
- if hasattr(self, 'width'):
- old_width = self.width
- else:
- old_width = None
- self._resize(height, width, y, x)
- if room and self.width != old_width:
- self.rebuild_everything(room)
-
- # reposition the scrolling after resize
- # (see #2450)
- buf_size = len(self.built_lines)
- if buf_size - self.pos < self.height:
- self.pos = buf_size - self.height
- if self.pos < 0:
- self.pos = 0
-
- def rebuild_everything(self, room):
- self.built_lines = []
- with_timestamps = config.get('show_timestamps')
- for message in room.messages:
- self.build_new_message(message, clean=False, timestamp=with_timestamps)
- if self.separator_after is message:
- self.build_new_message(None)
- while len(self.built_lines) > self.lines_nb_limit:
- self.built_lines.pop(0)
-
def modify_message(self, old_id, message):
"""
Find a message, and replace it with a new one
(instead of rebuilding everything in order to correct a message)
"""
with_timestamps = config.get('show_timestamps')
+ nick_size = config.get('max_nick_length')
for i in range(len(self.built_lines)-1, -1, -1):
if self.built_lines[i] and self.built_lines[i].msg.identifier == old_id:
index = i
@@ -425,7 +481,7 @@ class TextWin(Win):
self.built_lines.pop(index)
index -= 1
index += 1
- lines = self.build_message(message, timestamp=with_timestamps)
+ lines = self.build_message(message, timestamp=with_timestamps, nick_size=nick_size)
for line in lines:
self.built_lines.insert(index, line)
index += 1
@@ -435,3 +491,86 @@ class TextWin(Win):
log.debug('** TextWin: deleting %s built lines', (len(self.built_lines)))
del self.built_lines
+class XMLTextWin(BaseTextWin):
+ def __init__(self):
+ BaseTextWin.__init__(self)
+
+ def refresh(self):
+ log.debug('Refresh: %s', self.__class__.__name__)
+ theme = get_theme()
+ if self.height <= 0:
+ return
+ if self.pos == 0:
+ lines = self.built_lines[-self.height:]
+ else:
+ lines = self.built_lines[-self.height-self.pos:-self.pos]
+ self._win.move(0, 0)
+ self._win.erase()
+ for y, line in enumerate(lines):
+ if line:
+ msg = line.msg
+ if line.start_pos == 0:
+ if msg.nickname == theme.CHAR_XML_OUT:
+ color = theme.COLOR_XML_OUT
+ elif msg.nickname == theme.CHAR_XML_IN:
+ color = theme.COLOR_XML_IN
+ self.write_time(msg.str_time)
+ self.write_prefix(msg.nickname, color)
+ self.addstr(' ')
+ if y != self.height-1:
+ self.addstr('\n')
+ self._win.attrset(0)
+ for y, line in enumerate(lines):
+ offset = 0
+ # Offset for the timestamp (if any) plus a space after it
+ offset += len(line.msg.str_time)
+ # space
+ offset += 1
+
+ # Offset for the prefix
+ offset += poopt.wcswidth(truncate_nick(line.msg.nickname))
+ # space
+ offset += 1
+
+ self.write_text(y, offset,
+ line.prepend+line.msg.txt[line.start_pos:line.end_pos])
+ if y != self.height-1:
+ self.addstr('\n')
+ self._win.attrset(0)
+ self._refresh()
+
+ def build_message(self, message, timestamp=False, nick_size=10):
+ txt = message.txt
+ ret = []
+ default_color = None
+ nick = truncate_nick(message.nickname, nick_size)
+ offset = 0
+ if nick:
+ offset += poopt.wcswidth(nick) + 1 # + nick + ' ' length
+ if message.str_time:
+ offset += 1 + len(message.str_time)
+ if get_theme().CHAR_TIME_LEFT and message.str_time:
+ offset += 1
+ if get_theme().CHAR_TIME_RIGHT and message.str_time:
+ offset += 1
+ lines = poopt.cut_text(txt, self.width-offset-1)
+ prepend = default_color if default_color else ''
+ attrs = []
+ for line in lines:
+ saved = Line(msg=message, start_pos=line[0], end_pos=line[1], prepend=prepend)
+ attrs = parse_attrs(message.txt[line[0]:line[1]], attrs)
+ if attrs:
+ prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs)
+ else:
+ if default_color:
+ prepend = default_color
+ else:
+ prepend = ''
+ ret.append(saved)
+ return ret
+
+ def write_prefix(self, nickname, color):
+ self._win.attron(to_curses_attr(color))
+ self.addstr(truncate_nick(nickname))
+ self._win.attroff(to_curses_attr(color))
+
diff --git a/src/xhtml.py b/src/xhtml.py
index 01e2dfcd..b84ce943 100644
--- a/src/xhtml.py
+++ b/src/xhtml.py
@@ -183,6 +183,8 @@ whitespace_re = re.compile(r'\s+')
xhtml_attr_re = re.compile(r'\x19-?\d[^}]*}|\x19[buaio]')
xhtml_data_re = re.compile(r'data:image/([a-z]+);base64,(.+)')
+poezio_color_double = re.compile(r'(?:\x19\d+}|\x19\d)+(\x19\d|\x19\d+})')
+poezio_format_trim = re.compile(r'(\x19\d+}|\x19\d|\x19[buaio]|\x19o)+\x19o')
xhtml_simple_attr_re = re.compile(r'\x19\d')
@@ -303,7 +305,8 @@ class XHTMLHandler(sax.ContentHandler):
@property
def result(self):
- return ''.join(self.builder).strip()
+ sanitized = re.sub(poezio_color_double, r'\1', ''.join(self.builder).strip())
+ return re.sub(poezio_format_trim, '\x19o', sanitized)
def append_formatting(self, formatting):
self.formatting.append(formatting)