diff options
author | mathieui <mathieui@mathieui.net> | 2017-11-12 15:03:09 +0100 |
---|---|---|
committer | mathieui <mathieui@mathieui.net> | 2017-11-12 15:03:09 +0100 |
commit | d55cc5872503567775f0d7a7731d6f489bf2299b (patch) | |
tree | 725f9e7b8144d36054447b3c82edfb45bda8df1d | |
parent | 92496db823db34f7f7fb1ab31eaef093a707c3e8 (diff) | |
download | poezio-d55cc5872503567775f0d7a7731d6f489bf2299b.tar.gz poezio-d55cc5872503567775f0d7a7731d6f489bf2299b.tar.bz2 poezio-d55cc5872503567775f0d7a7731d6f489bf2299b.tar.xz poezio-d55cc5872503567775f0d7a7731d6f489bf2299b.zip |
yapf -ir
65 files changed, 4361 insertions, 2830 deletions
diff --git a/poezio/__main__.py b/poezio/__main__.py index b8696bb5..b0ba98e4 100644 --- a/poezio/__main__.py +++ b/poezio/__main__.py @@ -8,5 +8,6 @@ def run(): sys.exit(1) return 0 + if __name__ == '__main__': run() diff --git a/poezio/args.py b/poezio/args.py index 63e77927..71ae7488 100644 --- a/poezio/args.py +++ b/poezio/args.py @@ -6,23 +6,37 @@ There is a fallback to the deprecated optparse if argparse is not found from os import path from argparse import ArgumentParser, SUPPRESS + def parse_args(CONFIG_PATH=''): """ Parse the arguments from the command line """ 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="1.0-dev") + 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="1.0-dev") options = parser.parse_args() return options diff --git a/poezio/asyncio.py b/poezio/asyncio.py index 2b02a91f..d333ffa6 100644 --- a/poezio/asyncio.py +++ b/poezio/asyncio.py @@ -28,6 +28,7 @@ def monkey_patch_asyncio_slixmpp(): if self._idle: handle = self._idle.popleft() handle._run() + cls = asyncio.get_event_loop().__class__ cls._idle = collections.deque() cls.idle_call = idle_call @@ -35,8 +36,8 @@ def monkey_patch_asyncio_slixmpp(): cls._run_once = my_run_once spawn_event = slixmpp.xmlstream.XMLStream._spawn_event + def patchy(self, xml): self.loop.idle_call(functools.partial(spawn_event, self, xml)) - slixmpp.xmlstream.XMLStream._spawn_event = patchy - + slixmpp.xmlstream.XMLStream._spawn_event = patchy diff --git a/poezio/bookmarks.py b/poezio/bookmarks.py index 81d006b7..e007ddd1 100644 --- a/poezio/bookmarks.py +++ b/poezio/bookmarks.py @@ -40,8 +40,13 @@ log = logging.getLogger(__name__) class Bookmark(object): - - def __init__(self, jid, name=None, autojoin=False, nick=None, password=None, method='local'): + 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 @@ -61,9 +66,8 @@ class Bookmark(object): self._method = value def __repr__(self): - return '<%s%s|%s>' % (self.jid, - ('/'+self.nick) if self.nick else '', - self.method) + return '<%s%s|%s>' % (self.jid, ('/' + self.nick) + if self.nick else '', self.method) def stanza(self): """ @@ -98,7 +102,8 @@ class Bookmark(object): """ jid = el.get('jid') name = el.get('name') - autojoin = True if el.get('autojoin', 'false').lower() in ('true', '1') else False + autojoin = True if el.get('autojoin', + 'false').lower() in ('true', '1') else False nick = None for n in el.iter('nick'): nick = n.text @@ -121,8 +126,8 @@ class Bookmark(object): name = el['name'] return Bookmark(jid, name, autojoin, nick, password, method='remote') -class BookmarkList(object): +class BookmarkList(object): def __init__(self): self.bookmarks = [] preferred = config.get('use_bookmarks_method').lower() @@ -191,17 +196,21 @@ class BookmarkList(object): 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) + 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') + 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) @@ -209,14 +218,17 @@ class BookmarkList(object): 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']: + for conf in iq['pubsub']['items']['item']['bookmarks'][ + 'conferences']: if isinstance(conf, URL): continue b = Bookmark.parse(conf) @@ -230,6 +242,7 @@ class BookmarkList(object): """ Fetch the remote bookmarks stored via privatexml. """ + def _cb(iq): if iq['type'] == 'result': for conf in iq['private']['bookmarks']['conferences']: @@ -250,12 +263,15 @@ class BookmarkList(object): 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') + information('No remote bookmark storage available', + 'Warning') + callback = new_callback if self.preferred == 'pep': @@ -277,10 +293,17 @@ class BookmarkList(object): 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') + 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() diff --git a/poezio/common.py b/poezio/common.py index 7168fe18..0c6a77a8 100644 --- a/poezio/common.py +++ b/poezio/common.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Various useful functions. """ @@ -45,6 +44,7 @@ def get_base64_from_file(path): mime_type = mimetypes.guess_type(path)[0] return (encoded, mime_type, sha1) + def _get_output_of_command(command): """ Runs a command and returns its output. @@ -54,10 +54,12 @@ def _get_output_of_command(command): :rtype: :py:class:`str` """ try: - return subprocess.check_output(command.split()).decode('utf-8').split('\n') + return subprocess.check_output( + command.split()).decode('utf-8').split('\n') except subprocess.CalledProcessError: return None + def _is_in_path(command, return_abs_path=False): """ Check if *command* is in the $PATH or not. @@ -81,6 +83,7 @@ def _is_in_path(command, return_abs_path=False): pass return False + DISTRO_INFO = { 'Arch Linux': '/etc/arch-release', 'Aurox Linux': '/etc/aurox-release', @@ -103,6 +106,7 @@ DISTRO_INFO = { 'Redhat Linux': '/etc/redhat-release' } + def get_os_info(): """ Returns a detailed and well formated string containing @@ -116,10 +120,12 @@ def get_os_info(): full_path_to_executable = _is_in_path(executable, return_abs_path=True) if full_path_to_executable: command = executable + params - process = subprocess.Popen([command], shell=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - close_fds=True) + process = subprocess.Popen( + [command], + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + close_fds=True) process.wait() output = process.stdout.readline().decode('utf-8').strip() # some distros put n/a in places, so remove those @@ -136,7 +142,7 @@ def get_os_info(): text = _get_output_of_command(path_to_file)[0] else: fdes = open(path_to_file, encoding='utf-8') - text = fdes.readline().strip() # get only first line + text = fdes.readline().strip() # get only first line fdes.close() if path_to_file.endswith('version'): # sourcemage_version and slackware-version files @@ -158,11 +164,12 @@ def get_os_info(): # our last chance, ask uname and strip it uname_output = _get_output_of_command('uname -sr') if uname_output is not None: - os_info = uname_output[0] # only first line + os_info = uname_output[0] # only first line return os_info os_info = 'N/A' return os_info + def _datetime_tuple(timestamp): """ Convert a timestamp using strptime and the format: %Y%m%dT%H:%M:%S. @@ -194,7 +201,7 @@ def _datetime_tuple(timestamp): tz_msg = timedelta(seconds=tz_mod * tz_msg) ret -= tz_msg except ValueError: - pass # ignore if we got a badly-formatted offset + pass # ignore if we got a badly-formatted offset # convert UTC to local time, with DST etc. if time.daylight and time.localtime().tm_isdst: tz = timedelta(seconds=-time.altzone) @@ -203,6 +210,7 @@ def _datetime_tuple(timestamp): ret += tz return ret + def get_utc_time(local_time=None): """ Get the current UTC time @@ -225,6 +233,7 @@ def get_utc_time(local_time=None): return utc_time + def get_local_time(utc_time): """ Get the local time from an UTC time @@ -240,6 +249,7 @@ def get_local_time(utc_time): return local_time + def find_delayed_tag(message): """ Check if a message is delayed or not. @@ -266,6 +276,7 @@ def find_delayed_tag(message): date = None return (delayed, date) + def shell_split(st): """ Split a string correctly according to the quotes @@ -288,6 +299,7 @@ def shell_split(st): w = sh.get_token() return ret + def find_argument(pos, text, quoted=True): """ Split an input into a list of arguments, return the number of the @@ -308,6 +320,7 @@ def find_argument(pos, text, quoted=True): else: return _find_argument_unquoted(pos, text) + def _find_argument_quoted(pos, text): """ Get the number of the argument at position pos in @@ -324,6 +337,7 @@ def _find_argument_quoted(pos, text): return count + 1 + def _find_argument_unquoted(pos, text): """ Get the number of the argument at position pos in @@ -341,6 +355,7 @@ def _find_argument_unquoted(pos, text): argnum = i return argnum + 1 + def parse_str_to_secs(duration=''): """ Parse a string of with a number of d, h, m, s. @@ -368,6 +383,7 @@ def parse_str_to_secs(duration=''): result += int(tmp) return result + def parse_secs_to_str(duration=0): """ Do the reverse operation of :py:func:`parse_str_to_secs`. @@ -397,6 +413,7 @@ def parse_secs_to_str(duration=0): result = '0s' return result + def format_tune_string(infos): """ Contruct a string from a dict created from an "User tune" event. @@ -434,6 +451,7 @@ def format_tune_string(infos): elems.append('[' + mins + ':' + secs + ']') return ' '.join(elems) + def format_gaming_string(infos): """ Construct a string from a dict containing "user gaming" information. @@ -452,6 +470,7 @@ def format_gaming_string(infos): return '%s on %s' % (name, server_address) return name + def safeJID(*args, **kwargs): """ Construct a :py:class:`slixmpp.JID` object from a string. @@ -463,4 +482,3 @@ def safeJID(*args, **kwargs): return JID(*args, **kwargs) except InvalidJID: return JID('') - diff --git a/poezio/config.py b/poezio/config.py index bef1c1a6..24e771fd 100644 --- a/poezio/config.py +++ b/poezio/config.py @@ -144,14 +144,15 @@ DEFAULT_CONFIG = { 'folded_roster_groups': '', 'info_win_height': 2 }, - 'muc_colors': { - } + 'muc_colors': {} } + class Config(RawConfigParser): """ load/save the config to a file """ + def __init__(self, file_name, default=None): RawConfigParser.__init__(self, None) # make the options case sensitive @@ -198,8 +199,12 @@ class Config(RawConfigParser): return default return res - def get_by_tabname(self, option, tabname, - fallback=True, fallback_server=True, default=''): + def get_by_tabname(self, + option, + tabname, + fallback=True, + fallback_server=True, + default=''): """ Try to get the value for the option. First we look in a section named `tabname`, if the option is not present @@ -232,7 +237,6 @@ class Config(RawConfigParser): return self.get(option, default) return default - def __get(self, option, section=DEFSECTION, **kwargs): """ facility for RawConfigParser.get @@ -331,11 +335,7 @@ class Config(RawConfigParser): prefix, file = path.split(self.file_name) filename = path.join(prefix, '.%s.tmp' % file) fd = os.fdopen( - os.open( - filename, - os.O_WRONLY | os.O_CREAT, - 0o600), - 'w') + os.open(filename, os.O_WRONLY | os.O_CREAT, 0o600), 'w') for line in lines: fd.write('%s\n' % line) fd.close() @@ -362,9 +362,10 @@ class Config(RawConfigParser): with open(self.file_name, 'r', encoding='utf-8') as df: lines_before = [line.strip() for line in df] except OSError: - log.error('Unable to read the config file %s', - self.file_name, - exc_info=True) + log.error( + 'Unable to read the config file %s', + self.file_name, + exc_info=True) return tuple() else: lines_before = [] @@ -415,8 +416,7 @@ class Config(RawConfigParser): else: return ('Could not toggle option: %s.' ' Current value is %s.' % - (option, current or "empty"), - 'Warning') + (option, current or "empty"), 'Warning') if self.has_section(section): RawConfigParser.set(self, section, option, value) else: @@ -477,12 +477,13 @@ def find_line(lines, start, end, option): """ current = start for line in lines[start:end]: - if (line.startswith('%s ' % option) or - line.startswith('%s=' % option)): + if (line.startswith('%s ' % option) + or line.startswith('%s=' % option)): return current current += 1 return -1 + def file_ok(filepath): """ Returns True if the file exists and is readable and writeable, @@ -492,6 +493,7 @@ def file_ok(filepath): val &= os.access(filepath, os.R_OK | os.W_OK) return bool(val) + def check_create_config_dir(): """ create the configuration directory if it doesn't exist @@ -507,6 +509,7 @@ def check_create_config_dir(): pass return CONFIG_PATH + def check_create_cache_dir(): """ create the cache directory if it doesn't exist @@ -525,6 +528,7 @@ def check_create_cache_dir(): except OSError: pass + def check_config(): """ Check the config file and print results @@ -533,7 +537,8 @@ def check_config(): 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])) + result['changed'].append((option, value, + DEFAULT_CONFIG['Poezio'][option])) else: value = config.get(option, default='') upper = value.upper() @@ -544,15 +549,19 @@ def check_config(): 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') + 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)) + 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 @@ -560,7 +569,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') + 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) @@ -577,6 +587,7 @@ def run_cmdline_args(CONFIG_PATH): global firstrun firstrun = True + def create_global_config(): "Create the global config object, or crash" try: @@ -589,6 +600,7 @@ def create_global_config(): traceback.print_exc(limit=0) sys.exit(1) + def check_create_log_dir(): "Create the poezio logging directory if it doesn’t exist" global LOG_DIR @@ -610,29 +622,29 @@ def check_create_log_dir(): except: pass + def setup_logging(): "Change the logging config according to the cmdline options and config" if config.get('log_errors'): LOGGING_CONFIG['root']['handlers'].append('error') LOGGING_CONFIG['handlers']['error'] = { - 'level': 'ERROR', - 'class': 'logging.FileHandler', - 'filename': path.join(LOG_DIR, 'errors.log'), - 'formatter': 'simple', - } + 'level': 'ERROR', + 'class': 'logging.FileHandler', + 'filename': path.join(LOG_DIR, 'errors.log'), + 'formatter': 'simple', + } logging.disable(logging.WARNING) if options.debug: LOGGING_CONFIG['root']['handlers'].append('debug') LOGGING_CONFIG['handlers']['debug'] = { - 'level':'DEBUG', - 'class':'logging.FileHandler', - 'filename': options.debug, - 'formatter': 'simple', - } + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': options.debug, + 'formatter': 'simple', + } logging.disable(logging.NOTSET) - if LOGGING_CONFIG['root']['handlers']: logging.config.dictConfig(LOGGING_CONFIG) else: @@ -642,6 +654,7 @@ def setup_logging(): global log log = logging.getLogger(__name__) + def post_logging_setup(): # common imports slixmpp, which creates then its loggers, so # it needs to be after logger configuration @@ -649,6 +662,7 @@ def post_logging_setup(): global safeJID safeJID = JID + LOGGING_CONFIG = { 'version': 1, 'disable_existing_loggers': True, @@ -657,12 +671,11 @@ LOGGING_CONFIG = { 'format': '%(asctime)s %(levelname)s:%(module)s:%(message)s' } }, - 'handlers': { - }, + 'handlers': {}, 'root': { - 'handlers': [], - 'propagate': True, - 'level': 'DEBUG', + 'handlers': [], + 'propagate': True, + 'level': 'DEBUG', } } diff --git a/poezio/connection.py b/poezio/connection.py index d00f714f..99a19c11 100644 --- a/poezio/connection.py +++ b/poezio/connection.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Defines the Connection class """ @@ -12,7 +11,6 @@ Defines the Connection class import logging log = logging.getLogger(__name__) - import getpass import subprocess import sys @@ -25,12 +23,14 @@ from poezio import fixes from poezio.common import safeJID from poezio.config import config, options + class Connection(slixmpp.ClientXMPP): """ Receives everything from Jabber and emits the appropriate signals """ __init = False + def __init__(self): keyfile = config.get('keyfile') certfile = config.get('certfile') @@ -43,27 +43,35 @@ class Connection(slixmpp.ClientXMPP): jid = '%s' % config.get('jid') password = config.get('password') eval_password = config.get('eval_password') - if not password and not eval_password and not (keyfile and certfile): + if not password and not eval_password and not (keyfile + and certfile): password = getpass.getpass() elif not password and not (keyfile and certfile): - sys.stderr.write("No password or certificates provided, using the eval_password command.\n") - process = subprocess.Popen(['sh', '-c', eval_password], stdin=subprocess.PIPE, - stdout=subprocess.PIPE, close_fds=True) + sys.stderr.write( + "No password or certificates provided, using the eval_password command.\n" + ) + process = subprocess.Popen( + ['sh', '-c', eval_password], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + close_fds=True) code = process.wait() if code != 0: - sys.stderr.write('The eval_password command (%s) returned a ' - 'nonzero status code: %s.\n' % (eval_password, code)) + sys.stderr.write( + 'The eval_password command (%s) returned a ' + 'nonzero status code: %s.\n' % (eval_password, code)) sys.stderr.write('Poezio will now exit\n') sys.exit(code) - password = process.stdout.readline().decode('utf-8').strip('\n') - else: # anonymous auth + password = process.stdout.readline().decode('utf-8').strip( + '\n') + else: # anonymous auth self.anon = True jid = config.get('server') password = None jid = safeJID(jid) # TODO: use the system language - slixmpp.ClientXMPP.__init__(self, jid, password, - lang=config.get('lang')) + slixmpp.ClientXMPP.__init__( + self, jid, password, lang=config.get('lang')) force_encryption = config.get('force_encryption') if force_encryption: @@ -75,18 +83,19 @@ class Connection(slixmpp.ClientXMPP): 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') + 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') + log.error( + 'certfile is present in configuration file without keyfile') self.core = None self.auto_reconnect = config.get('auto_reconnect') self.auto_authorize = None # prosody defaults, lowest is AES128-SHA, it should be a minimum # for anything that came out after 2002 - self.ciphers = config.get('ciphers', - 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK' - ':!SRP:!3DES:!aNULL') + self.ciphers = config.get('ciphers', 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK' + ':!SRP:!3DES:!aNULL') self.ca_certs = config.get('ca_cert_path') or None interval = config.get('whitespace_interval') if int(interval) > 0: @@ -116,7 +125,8 @@ class Connection(slixmpp.ClientXMPP): XEP_0184._filter_add_receipt_request = fixes._filter_add_receipt_request self.register_plugin('xep_0184') self.plugin['xep_0184'].auto_ack = config.get('ack_message_receipts') - self.plugin['xep_0184'].auto_request = config.get('request_message_receipts') + self.plugin['xep_0184'].auto_request = config.get( + 'request_message_receipts') self.register_plugin('xep_0191') if config.get('enable_smacks'): @@ -139,16 +149,15 @@ class Connection(slixmpp.ClientXMPP): self.register_plugin('xep_0196') if config.get('send_poezio_info'): - info = {'name':'poezio', - 'version': options.version} + info = {'name': 'poezio', 'version': options.version} if config.get('send_os_info'): info['os'] = common.get_os_info() self.plugin['xep_0030'].set_identities( - identities={('client', 'console', None, 'Poezio')}) + identities={('client', 'console', None, 'Poezio')}) else: info = {'name': '', 'version': ''} self.plugin['xep_0030'].set_identities( - identities={('client', 'console', None, '')}) + identities={('client', 'console', None, '')}) self.register_plugin('xep_0092', pconfig=info) if config.get('send_time'): self.register_plugin('xep_0202') @@ -212,10 +221,12 @@ class Connection(slixmpp.ClientXMPP): self.core.handler.outgoing_stanza(data) slixmpp.ClientXMPP.send_raw(self, data) + class MatchAll(slixmpp.xmlstream.matcher.base.MatcherBase): """ Callback to retrieve all the stanzas for the XML tab """ + def match(self, xml): "match everything" return True diff --git a/poezio/contact.py b/poezio/contact.py index 090fed92..4b233088 100644 --- a/poezio/contact.py +++ b/poezio/contact.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Defines the Resource and Contact classes, which are used in the roster. @@ -16,16 +15,18 @@ log = logging.getLogger(__name__) from poezio.common import safeJID from collections import defaultdict + class Resource(object): """ Defines a roster item. It's a precise resource. """ + def __init__(self, jid, data): """ data: the dict to use as a source """ - self._jid = jid # Full jid + self._jid = jid # Full jid self._data = data @property @@ -52,12 +53,14 @@ class Resource(object): return False return self.jid == value.jid and self._data == value._data + class Contact(object): """ This a way to gather multiple resources from the same bare JID. This class contains zero or more Resource object and useful methods to get the resource with the highest priority, etc """ + def __init__(self, item): """ item: a slixmpp RosterItem pointing to that contact @@ -118,17 +121,17 @@ class Contact(object): @property def resources(self): """List of the available resources as Resource objects""" - return (Resource( - '%s%s' % (self.bare_jid, ('/' + key) if key else ''), - self.__item.resources[key] - ) for key in self.__item.resources.keys()) + return (Resource('%s%s' % (self.bare_jid, ('/' + key) + if key else ''), self.__item.resources[key]) + for key in self.__item.resources.keys()) @property def subscription(self): return self.__item['subscription'] def __contains__(self, value): - return value in self.__item.resources or safeJID(value).resource in self.__item.resources + return value in self.__item.resources or safeJID( + value).resource in self.__item.resources def __len__(self): """Number of resources""" diff --git a/poezio/core/__init__.py b/poezio/core/__init__.py index 0c6d63d9..cfe4c179 100644 --- a/poezio/core/__init__.py +++ b/poezio/core/__init__.py @@ -5,4 +5,3 @@ __all__ = ['Core', 'Command', 'Status'] from poezio.core.core import Core from poezio.core.structs import Command, Status - diff --git a/poezio/core/commands.py b/poezio/core/commands.py index 5a28182b..ab0ced9a 100644 --- a/poezio/core/commands.py +++ b/poezio/core/commands.py @@ -46,10 +46,8 @@ class CommandCore: buff = ['Global commands:'] for name, command in self.core.commands.items(): if isinstance(command, Command): - acc.append(' \x19%s}%s\x19o - %s' % ( - color, - name, - command.short_desc)) + acc.append(' \x19%s}%s\x19o - %s' % (color, name, + command.short_desc)) else: acc.append(' \x19%s}%s\x19o' % (color, name)) acc = sorted(acc) @@ -59,10 +57,8 @@ class CommandCore: tab_commands = self.core.current_tab().commands for name, command in tab_commands.items(): if isinstance(command, Command): - acc.append(' \x19%s}%s\x19o - %s' % ( - color, - name, - command.short_desc)) + acc.append(' \x19%s}%s\x19o - %s' % (color, name, + command.short_desc)) else: acc.append(' \x19%s}%s\x19o' % (color, name)) acc = sorted(acc) @@ -93,11 +89,13 @@ class CommandCore: """ /runkey <key> """ + def replace_line_breaks(key): "replace ^J with \n" if key == '^J': return '\n' return key + if args is None: return self.help('runkey') char = args[0] @@ -135,7 +133,8 @@ class CommandCore: current.send_chat_state('inactive') for tab in self.core.tabs: if isinstance(tab, tabs.MucTab) and tab.joined: - muc.change_show(self.core.xmpp, tab.name, tab.own_nick, show, msg) + muc.change_show(self.core.xmpp, tab.name, tab.own_nick, show, + msg) if hasattr(tab, 'directed_presence'): del tab.directed_presence self.core.set_status(show, msg) @@ -156,12 +155,14 @@ class CommandCore: if ptype == 'available': ptype = None try: - pres = self.core.xmpp.make_presence(pto=jid, ptype=ptype, pstatus=status) + pres = self.core.xmpp.make_presence( + pto=jid, ptype=ptype, pstatus=status) self.core.events.trigger('send_normal_presence', pres) pres.send() except (XMPPError, NotConnectedError): self.core.information('Could not send directed presence', 'Error') - log.debug('Could not send directed presence to %s', jid, exc_info=True) + log.debug( + 'Could not send directed presence to %s', jid, exc_info=True) return tab = self.core.get_tab_by_name(jid) if tab: @@ -184,7 +185,7 @@ class CommandCore: """/theme <theme name>""" if args is None: return self.help('theme') - self.set('theme %s' % (args[0],)) + self.set('theme %s' % (args[0], )) @command_args_parser.quoted(1) def win(self, args): @@ -254,10 +255,12 @@ class CommandCore: if not old_tab and value == tab.name: old_tab = tab if not old_tab: - self.core.information("Tab %s does not exist" % args[0], "Error") + self.core.information("Tab %s does not exist" % args[0], + "Error") return None ref = old_tab.nb return ref + old = get_nb_from_value(args[0]) new = get_nb_from_value(args[1]) if new is None or old is None: @@ -281,19 +284,20 @@ class CommandCore: jid = safeJID(args[0]) else: if not isinstance(self.core.current_tab(), tabs.MucTab): - return self.core.information('Please provide a server', 'Error') + return self.core.information('Please provide a server', + 'Error') jid = safeJID(self.core.current_tab().name) list_tab = tabs.MucListTab(self.core, jid) self.core.add_tab(list_tab, True) cb = list_tab.on_muc_list_item_received - self.core.xmpp.plugin['xep_0030'].get_items(jid=jid, - callback=cb) + self.core.xmpp.plugin['xep_0030'].get_items(jid=jid, callback=cb) @command_args_parser.quoted(1) def version(self, args): """ /version <jid> """ + def callback(res): "Callback for /version" if not res: @@ -301,10 +305,9 @@ class CommandCore: ' 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') + jid, 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 args is None: @@ -315,7 +318,8 @@ class CommandCore: fixes.get_version(self.core.xmpp, jid, callback=callback) elif jid in roster: for resource in roster[jid].resources: - fixes.get_version(self.core.xmpp, resource.jid, callback=callback) + fixes.get_version( + self.core.xmpp, resource.jid, callback=callback) def _empty_join(self): tab = self.core.current_tab() @@ -372,7 +376,7 @@ class CommandCore: else: room, nick = self._parse_join_jid(args[0]) if not room and not nick: - return # nothing was parsed + return # nothing was parsed room = room.lower() if nick == '': @@ -467,11 +471,13 @@ class CommandCore: bookmark.nick = nick if password: bookmark.password = password + def callback(iq): if iq["type"] != "error": self.core.information('Bookmark added.', 'Info') else: self.core.information("Could not add the bookmarks.", "Info") + self.core.bookmarks.save_local() self.core.bookmarks.save_remote(self.core.xmpp, callback) @@ -480,8 +486,7 @@ class CommandCore: for tab in self.core.get_tabs(tabs.MucTab): bookmark = self.core.bookmarks[tab.name] if not bookmark: - bookmark = Bookmark(tab.name, autojoin=True, - method=method) + bookmark = Bookmark(tab.name, autojoin=True, method=method) new_bookmarks.append(bookmark) else: bookmark.method = method @@ -489,11 +494,14 @@ class CommandCore: self.core.bookmarks.remove(bookmark) new_bookmarks.extend(self.core.bookmarks.bookmarks) self.core.bookmarks.set(new_bookmarks) + def _cb(iq): if iq["type"] != "error": self.core.information("Bookmarks saved.", "Info") else: - self.core.information("Could not save the remote bookmarks.", "Info") + self.core.information("Could not save the remote bookmarks.", + "Info") + self.core.bookmarks.save_local() self.core.bookmarks.save_remote(self.core.xmpp, _cb) @@ -520,7 +528,8 @@ class CommandCore: if success: self.core.information('Bookmark deleted', 'Info') else: - self.core.information('Error while deleting the bookmark', 'Error') + self.core.information('Error while deleting the bookmark', + 'Error') if not args: tab = self.core.current_tab() @@ -546,15 +555,17 @@ class CommandCore: 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), - }) + 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)) + 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] @@ -573,7 +584,8 @@ class CommandCore: file_name = os.path.join(file_name, plugin_name + '.cfg') plugin_config = PluginConfig(file_name, plugin_name) else: - plugin_config = self.core.plugin_manager.plugins[plugin_name].config + plugin_config = self.core.plugin_manager.plugins[ + plugin_name].config value = plugin_config.get(option, default='', section=section) info = ('%s=%s' % (option, value), 'Info') else: @@ -600,14 +612,15 @@ class CommandCore: file_name = os.path.join(file_name, plugin_name + '.cfg') plugin_config = PluginConfig(file_name, plugin_name) else: - plugin_config = self.core.plugin_manager.plugins[plugin_name].config + plugin_config = self.core.plugin_manager.plugins[ + plugin_name].config info = plugin_config.set_and_save(option, value, section) else: if args[0] == '.': name = safeJID(self.core.current_tab().name).bare if not name: - self.core.information('Invalid tab to use the "." argument.', - 'Error') + self.core.information( + 'Invalid tab to use the "." argument.', 'Error') return section = name else: @@ -679,35 +692,35 @@ class CommandCore: """ /last_activity <jid> """ + def callback(iq): "Callback for the last activity" if iq['type'] != 'result': if iq['error']['type'] == 'auth': self.core.information('You are not allowed to see the ' - 'activity of this contact.', - 'Error') + 'activity of this contact.', 'Error') else: - self.core.information('Error retrieving the activity', 'Error') + self.core.information('Error retrieving the activity', + 'Error') return seconds = iq['last_activity']['seconds'] status = iq['last_activity']['status'] from_ = iq['from'] if not safeJID(from_).user: msg = 'The uptime of %s is %s.' % ( - from_, - common.parse_secs_to_str(seconds)) + from_, common.parse_secs_to_str(seconds)) else: msg = 'The last activity of %s was %s ago%s' % ( - from_, - common.parse_secs_to_str(seconds), - (' and his/her last status was %s' % status) if status else '') + from_, common.parse_secs_to_str(seconds), + (' and his/her last status was %s' % status) + if status else '') self.core.information(msg, 'Info') if args is None: return self.help('last_activity') jid = safeJID(args[0]) - self.core.xmpp.plugin['xep_0012'].get_last_activity(jid, - callback=callback) + self.core.xmpp.plugin['xep_0012'].get_last_activity( + jid, callback=callback) @command_args_parser.quoted(0, 2) def mood(self, args): @@ -719,15 +732,14 @@ class CommandCore: mood = args[0] if mood not in pep.MOODS: - return self.core.information('%s is not a correct value for a mood.' - % mood, - 'Error') + return self.core.information( + '%s is not a correct value for a mood.' % mood, 'Error') if len(args) == 2: text = args[1] else: text = None - self.core.xmpp.plugin['xep_0107'].publish_mood(mood, text, - callback=dumb_callback) + self.core.xmpp.plugin['xep_0107'].publish_mood( + mood, text, callback=dumb_callback) @command_args_parser.quoted(0, 3) def activity(self, args): @@ -740,9 +752,8 @@ class CommandCore: general = args[0] if general not in pep.ACTIVITIES: - return self.core.information('%s is not a correct value for an activity' - % general, - 'Error') + return self.core.information( + '%s is not a correct value for an activity' % general, 'Error') specific = None text = None if length == 2: @@ -755,10 +766,9 @@ class CommandCore: text = args[2] if specific and specific not in pep.ACTIVITIES[general]: return self.core.information('%s is not a correct value ' - 'for an activity' % specific, - 'Error') - self.core.xmpp.plugin['xep_0108'].publish_activity(general, specific, text, - callback=dumb_callback) + 'for an activity' % specific, 'Error') + self.core.xmpp.plugin['xep_0108'].publish_activity( + general, specific, text, callback=dumb_callback) @command_args_parser.quoted(0, 2) def gaming(self, args): @@ -773,9 +783,8 @@ class CommandCore: address = args[1] else: address = None - return self.core.xmpp.plugin['xep_0196'].publish_gaming(name=name, - server_address=address, - callback=dumb_callback) + return self.core.xmpp.plugin['xep_0196'].publish_gaming( + name=name, server_address=address, callback=dumb_callback) @command_args_parser.quoted(2, 1, [None]) def invite(self, args): @@ -800,9 +809,9 @@ class CommandCore: return reason = args[1] del self.core.pending_invites[jid.bare] - self.core.xmpp.plugin['xep_0045'].decline_invite(jid.bare, - self.core.pending_invites[jid.bare], - reason) + self.core.xmpp.plugin['xep_0045'].decline_invite( + jid.bare, self.core.pending_invites[jid.bare], reason) + ### Commands without a completion in this class ### @@ -811,8 +820,8 @@ class CommandCore: """/invitations""" build = "" for invite in self.core.pending_invites: - build += "%s by %s" % (invite, - safeJID(self.core.pending_invites[invite]).bare) + build += "%s by %s" % ( + invite, safeJID(self.core.pending_invites[invite]).bare) if self.core.pending_invites: build = "You are invited to the following rooms:\n" + build else: @@ -838,7 +847,8 @@ class CommandCore: self.core.save_config() self.core.plugin_manager.disable_plugins() self.core.disconnect(msg) - self.core.xmpp.add_event_handler("disconnected", self.core.exit, disposable=True) + self.core.xmpp.add_event_handler( + "disconnected", self.core.exit, disposable=True) @command_args_parser.quoted(0, 1, ['']) def destroy_room(self, args): @@ -849,7 +859,8 @@ class CommandCore: if room: muc.destroy_room(self.core.xmpp, room) elif isinstance(self.core.current_tab(), tabs.MucTab) and not args[0]: - muc.destroy_room(self.core.xmpp, self.core.current_tab().general_jid) + muc.destroy_room(self.core.xmpp, + self.core.current_tab().general_jid) else: self.core.information('Invalid JID: "%s"' % args[0], 'Error') @@ -862,12 +873,15 @@ class CommandCore: return self.help('bind') if not config.silent_set(args[0], args[1], section='bindings'): - self.core.information('Unable to write in the config file', 'Error') + self.core.information('Unable to write in the config file', + 'Error') if args[1]: - self.core.information('%s is now bound to %s' % (args[0], args[1]), 'Info') + self.core.information('%s is now bound to %s' % (args[0], args[1]), + 'Info') else: - self.core.information('%s is now reset to the default binding' % args[0], 'Info') + self.core.information( + '%s is now reset to the default binding' % args[0], 'Info') @command_args_parser.raw def rawxml(self, args): @@ -881,7 +895,8 @@ class CommandCore: stanza = args try: stanza = StanzaBase(self.core.xmpp, xml=ET.fromstring(stanza)) - if stanza.xml.tag == 'iq' and stanza.xml.attrib.get('type') in ('get', 'set'): + if stanza.xml.tag == 'iq' and stanza.xml.attrib.get('type') in ( + 'get', 'set'): iq_id = stanza.xml.attrib.get('id') if not iq_id: iq_id = self.core.xmpp.new_id() @@ -893,18 +908,15 @@ class CommandCore: self.core.xmpp.remove_handler('Iq %s' % iq_id) self.core.xmpp.register_handler( - Callback('Iq %s' % iq_id, - StanzaPath('iq@id=%s' % iq_id), - iqfunc - ) - ) + Callback('Iq %s' % iq_id, + StanzaPath('iq@id=%s' % iq_id), iqfunc)) stanza.send() except: self.core.information('Could not send custom stanza', 'Error') - log.debug('/rawxml: Could not send custom stanza (%s)', - repr(stanza), - exc_info=True) - + log.debug( + '/rawxml: Could not send custom stanza (%s)', + repr(stanza), + exc_info=True) @command_args_parser.quoted(1, 256) def load(self, args): @@ -928,9 +940,8 @@ class CommandCore: """ /plugins """ - self.core.information("Plugins currently in use: %s" % - repr(list(self.core.plugin_manager.plugins.keys())), - 'Info') + self.core.information("Plugins currently in use: %s" % repr( + list(self.core.plugin_manager.plugins.keys())), 'Info') @command_args_parser.quoted(1, 1) def message(self, args): @@ -942,7 +953,8 @@ class CommandCore: jid = safeJID(args[0]) if not jid.user and not jid.domain and not jid.resource: return self.core.information('Invalid JID.', 'Error') - tab = self.core.get_conversation_by_jid(jid.full, False, fallback_barejid=False) + tab = self.core.get_conversation_by_jid( + jid.full, False, fallback_barejid=False) muc = self.core.get_tab_by_name(jid.bare, typ=tabs.MucTab) if not tab and not muc: tab = self.core.open_conversation_window(jid.full, focus=True) @@ -977,8 +989,8 @@ class CommandCore: list_tab = tabs.AdhocCommandsListTab(self.core, jid) self.core.add_tab(list_tab, True) cb = list_tab.on_list_received - self.core.xmpp.plugin['xep_0050'].get_commands(jid=jid, local=False, - callback=cb) + self.core.xmpp.plugin['xep_0050'].get_commands( + jid=jid, local=False, callback=cb) @command_args_parser.ignored def self_(self): @@ -990,15 +1002,11 @@ class CommandCore: nick = self.core.own_nick jid = self.core.xmpp.boundjid.full info = ('Your JID is %s\nYour current status is "%s" (%s)' - '\nYour default nickname is %s\nYou are running poezio %s' % ( - jid, - message if message else '', - show if show else 'available', - nick, - config_opts.version)) + '\nYour default nickname is %s\nYou are running poezio %s' % + (jid, message if message else '', show + if show else 'available', nick, config_opts.version)) self.core.information(info, 'Info') - @command_args_parser.ignored def reload(self): """ @@ -1006,6 +1014,6 @@ class CommandCore: """ self.core.reload_config() + def dumb_callback(*args, **kwargs): "mock callback" - diff --git a/poezio/core/completions.py b/poezio/core/completions.py index 634ab6b3..5e7e510f 100644 --- a/poezio/core/completions.py +++ b/poezio/core/completions.py @@ -17,13 +17,15 @@ from poezio.roster import roster from poezio.core.structs import POSSIBLE_SHOW, Completion + class CompletionCore: def __init__(self, core): self.core = core def help(self, the_input): """Completion for /help.""" - commands = sorted(self.core.commands.keys()) + sorted(self.core.current_tab().commands.keys()) + commands = sorted(self.core.commands.keys()) + sorted( + self.core.current_tab().commands.keys()) return Completion(the_input.new_completion, commands, 1, quotify=False) def status(self, the_input): @@ -31,8 +33,11 @@ class CompletionCore: Completion of /status """ if the_input.get_argument_position() == 1: - return Completion(the_input.new_completion, [status for status in POSSIBLE_SHOW], 1, ' ', quotify=False) - + return Completion( + the_input.new_completion, [status for status in POSSIBLE_SHOW], + 1, + ' ', + quotify=False) def presence(self, the_input): """ @@ -40,29 +45,38 @@ class CompletionCore: """ arg = the_input.get_argument_position() if arg == 1: - return Completion(the_input.auto_completion, [jid for jid in roster.jids()], '', quotify=True) + return Completion( + the_input.auto_completion, [jid for jid in roster.jids()], + '', + quotify=True) elif arg == 2: - return Completion(the_input.auto_completion, [status for status in POSSIBLE_SHOW], '', quotify=True) - + return Completion( + the_input.auto_completion, + [status for status in POSSIBLE_SHOW], + '', + quotify=True) def theme(self, the_input): """ Completion for /theme""" themes_dir = config.get('themes_dir') - themes_dir = (themes_dir or - os.path.join(os.environ.get('XDG_DATA_HOME') or - os.path.join(os.environ.get('HOME'), '.local', 'share'), - 'poezio', 'themes')) + themes_dir = (themes_dir or os.path.join( + os.environ.get('XDG_DATA_HOME') + or os.path.join(os.environ.get('HOME'), '.local', 'share'), + 'poezio', 'themes')) themes_dir = os.path.expanduser(themes_dir) try: names = os.listdir(themes_dir) except OSError: log.error('Completion for /theme failed', exc_info=True) return False - theme_files = [name[:-3] for name in names if name.endswith('.py') and name != '__init__.py'] + theme_files = [ + name[:-3] for name in names + if name.endswith('.py') and name != '__init__.py' + ] if 'default' not in theme_files: theme_files.append('default') - return Completion(the_input.new_completion, theme_files, 1, '', quotify=False) - + return Completion( + the_input.new_completion, theme_files, 1, '', quotify=False) def win(self, the_input): """Completion for /win""" @@ -72,7 +86,6 @@ class CompletionCore: l = [i[1] for i in l] return Completion(the_input.new_completion, l, 1, '', quotify=False) - def join(self, the_input): """ Completion for /join @@ -97,7 +110,9 @@ class CompletionCore: relevant_rooms = [] relevant_rooms.extend(sorted(self.core.pending_invites.keys())) - bookmarks = [(str(elem.jid) if not elem.nick else '%s/%s' % (elem.jid, elem.nick)) for elem in self.core.bookmarks] + bookmarks = [(str(elem.jid) + if not elem.nick else '%s/%s' % (elem.jid, elem.nick)) + for elem in self.core.bookmarks] to_suggest = [] for bookmark in bookmarks: tab = self.core.get_tab_by_name(bookmark, tabs.MucTab) @@ -113,31 +128,39 @@ class CompletionCore: serv_list = [] for tab in self.core.get_tabs(tabs.MucTab): if tab.joined: - serv_list.append('%s@%s' % (jid.user, safeJID(tab.name).host)) + serv_list.append('%s@%s' % (jid.user, + safeJID(tab.name).host)) serv_list.extend(relevant_rooms) - return Completion(the_input.new_completion, serv_list, 1, quotify=True) + return Completion( + the_input.new_completion, serv_list, 1, quotify=True) elif args[1].startswith('/'): # we completing only a resource - return Completion(the_input.new_completion, ['/%s' % self.core.own_nick], 1, quotify=True) + return Completion( + the_input.new_completion, ['/%s' % self.core.own_nick], + 1, + quotify=True) else: - return Completion(the_input.new_completion, relevant_rooms, 1, quotify=True) - + return Completion( + the_input.new_completion, relevant_rooms, 1, quotify=True) def version(self, the_input): """Completion for /version""" - comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), []) - return Completion(the_input.new_completion, sorted(comp), 1, quotify=False) - + comp = reduce(lambda x, y: x + [i.jid for i in y], + (roster[jid].resources for jid in roster.jids() + if len(roster[jid])), []) + return Completion( + the_input.new_completion, sorted(comp), 1, quotify=False) def list(self, the_input): """Completion for /list""" muc_serv_list = [] - for tab in self.core.get_tabs(tabs.MucTab): # TODO, also from an history + for tab in self.core.get_tabs( + tabs.MucTab): # TODO, also from an history if tab.name not in muc_serv_list: muc_serv_list.append(safeJID(tab.name).server) if muc_serv_list: - return Completion(the_input.new_completion, muc_serv_list, 1, quotify=False) - + return Completion( + the_input.new_completion, muc_serv_list, 1, quotify=False) def move_tab(self, the_input): """Completion for /move_tab""" @@ -145,8 +168,8 @@ class CompletionCore: if n == 1: nodes = [tab.name for tab in self.core.tabs if tab] nodes.remove('Roster') - return Completion(the_input.new_completion, nodes, 1, ' ', quotify=True) - + return Completion( + the_input.new_completion, nodes, 1, ' ', quotify=True) def runkey(self, the_input): """ @@ -157,14 +180,14 @@ class CompletionCore: list_.extend(self.core.current_tab().key_func.keys()) return Completion(the_input.new_completion, list_, 1, quotify=False) - def bookmark(self, the_input): """Completion for /bookmark""" args = common.shell_split(the_input.text) n = the_input.get_argument_position(quoted=True) if n == 2: - return Completion(the_input.new_completion, ['true', 'false'], 2, quotify=True) + return Completion( + the_input.new_completion, ['true', 'false'], 2, quotify=True) if n >= 3: return False @@ -175,7 +198,8 @@ class CompletionCore: if jid.server and (jid.resource or jid.full.endswith('/')): tab = self.core.get_tab_by_name(jid.bare, tabs.MucTab) nicks = [tab.own_nick] if tab else [] - default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' + default = os.environ.get('USER') if os.environ.get( + 'USER') else 'poezio' nick = config.get('default_nick') if not nick: if default not in nicks: @@ -184,29 +208,37 @@ class CompletionCore: if nick not in nicks: nicks.append(nick) jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] - return Completion(the_input.new_completion, jids_list, 1, quotify=True) + return Completion( + the_input.new_completion, jids_list, 1, quotify=True) muc_list = [tab.name for tab in self.core.get_tabs(tabs.MucTab)] muc_list.sort() muc_list.append('*') return Completion(the_input.new_completion, muc_list, 1, quotify=True) - def remove_bookmark(self, the_input): """Completion for /remove_bookmark""" - return Completion(the_input.new_completion, [bm.jid for bm in self.core.bookmarks], 1, quotify=False) - + return Completion( + the_input.new_completion, [bm.jid for bm in self.core.bookmarks], + 1, + quotify=False) def decline(self, the_input): """Completion for /decline""" n = the_input.get_argument_position(quoted=True) if n == 1: - return Completion(the_input.auto_completion, sorted(self.core.pending_invites.keys()), 1, '', quotify=True) - + return Completion( + the_input.auto_completion, + sorted(self.core.pending_invites.keys()), + 1, + '', + quotify=True) def bind(self, the_input): n = the_input.get_argument_position() if n == 1: - args = [key for key in self.core.key_func if not key.startswith('_')] + args = [ + key for key in self.core.key_func if not key.startswith('_') + ] elif n == 2: args = [key for key in self.core.key_func] else: @@ -214,7 +246,6 @@ class CompletionCore: return Completion(the_input.new_completion, args, n, '', quotify=False) - def message(self, the_input): """Completion for /message""" n = the_input.get_argument_position(quoted=True) @@ -231,14 +262,17 @@ class CompletionCore: l.append(jid) return Completion(the_input.new_completion, l, 1, '', quotify=True) - def invite(self, the_input): """Completion for /invite""" n = the_input.get_argument_position(quoted=True) if n == 1: - comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), []) + comp = reduce(lambda x, y: x + [i.jid for i in y], + (roster[jid].resources for jid in roster.jids() + if len(roster[jid])), []) comp = sorted(comp) - bares = sorted(roster[contact].bare_jid for contact in roster.jids() if len(roster[contact])) + bares = sorted( + roster[contact].bare_jid for contact in roster.jids() + if len(roster[contact])) off = sorted(jid for jid in roster.jids() if jid not in bares) comp = comp + bares + off return Completion(the_input.new_completion, comp, n, quotify=True) @@ -248,15 +282,19 @@ class CompletionCore: if tab.joined: rooms.append(tab.name) rooms.sort() - return Completion(the_input.new_completion, rooms, n, '', quotify=True) - + return Completion( + the_input.new_completion, rooms, n, '', quotify=True) def activity(self, the_input): """Completion for /activity""" n = the_input.get_argument_position(quoted=True) args = common.shell_split(the_input.text) if n == 1: - return Completion(the_input.new_completion, sorted(pep.ACTIVITIES.keys()), n, quotify=True) + return Completion( + the_input.new_completion, + sorted(pep.ACTIVITIES.keys()), + n, + quotify=True) elif n == 2: if args[1] in pep.ACTIVITIES: l = list(pep.ACTIVITIES[args[1]]) @@ -264,13 +302,15 @@ class CompletionCore: l.sort() return Completion(the_input.new_completion, l, n, quotify=True) - def mood(self, the_input): """Completion for /mood""" n = the_input.get_argument_position(quoted=True) if n == 1: - return Completion(the_input.new_completion, sorted(pep.MOODS.keys()), 1, quotify=True) - + return Completion( + the_input.new_completion, + sorted(pep.MOODS.keys()), + 1, + quotify=True) def last_activity(self, the_input): """ @@ -279,9 +319,11 @@ class CompletionCore: n = the_input.get_argument_position(quoted=False) if n >= 2: return False - comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), []) - return Completion(the_input.new_completion, sorted(comp), 1, '', quotify=False) - + comp = reduce(lambda x, y: x + [i.jid for i in y], + (roster[jid].resources for jid in roster.jids() + if len(roster[jid])), []) + return Completion( + the_input.new_completion, sorted(comp), 1, '', quotify=False) def server_cycle(self, the_input): """Completion for /server_cycle""" @@ -291,7 +333,6 @@ class CompletionCore: serv_list.add(serv) return Completion(the_input.new_completion, sorted(serv_list), 1, ' ') - def set(self, the_input): """Completion for /set""" args = common.shell_split(the_input.text) @@ -302,9 +343,13 @@ class CompletionCore: if '|' in args[1]: plugin_name, section = args[1].split('|')[:2] if plugin_name not in self.core.plugin_manager.plugins: - return Completion(the_input.new_completion, [], n, quotify=True) + return Completion( + the_input.new_completion, [], n, quotify=True) plugin = self.core.plugin_manager.plugins[plugin_name] - end_list = ['%s|%s' % (plugin_name, section) for section in plugin.config.sections()] + end_list = [ + '%s|%s' % (plugin_name, section) + for section in plugin.config.sections() + ] else: end_list = set(config.options('Poezio')) end_list.update(config.default.get('Poezio', {})) @@ -314,11 +359,13 @@ class CompletionCore: if '|' in args[1]: plugin_name, section = args[1].split('|')[:2] if plugin_name not in self.core.plugin_manager.plugins: - return Completion(the_input.new_completion, [''], n, quotify=True) + return Completion( + the_input.new_completion, [''], n, quotify=True) plugin = self.core.plugin_manager.plugins[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.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]): @@ -333,9 +380,14 @@ class CompletionCore: if '|' in args[1]: plugin_name, section = args[1].split('|')[:2] if plugin_name not in self.core.plugin_manager.plugins: - return Completion(the_input.new_completion, [''], n, quotify=True) + return Completion( + the_input.new_completion, [''], n, quotify=True) plugin = self.core.plugin_manager.plugins[plugin_name] - end_list = [str(plugin.config.get(args[2], '', section or plugin_name)), ''] + end_list = [ + str( + plugin.config.get(args[2], '', section + or plugin_name)), '' + ] else: if not config.has_section(args[1]): end_list = [''] @@ -345,7 +397,6 @@ class CompletionCore: return False return Completion(the_input.new_completion, end_list, n, quotify=True) - def set_default(self, the_input): """ Completion for /set_default """ @@ -357,11 +408,13 @@ class CompletionCore: return Completion(self.set, the_input) return False - def toggle(self, the_input): "Completion for /toggle" - return Completion(the_input.new_completion, config.options('Poezio'), 1, quotify=False) - + return Completion( + the_input.new_completion, + config.options('Poezio'), + 1, + quotify=False) def bookmark_local(self, the_input): """Completion for /bookmark_local""" @@ -377,7 +430,8 @@ class CompletionCore: if jid.server and (jid.resource or jid.full.endswith('/')): tab = self.core.get_tab_by_name(jid.bare, tabs.MucTab) nicks = [tab.own_nick] if tab else [] - default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' + default = os.environ.get('USER') if os.environ.get( + 'USER') else 'poezio' nick = config.get('default_nick') if not nick: if default not in nicks: @@ -386,8 +440,8 @@ class CompletionCore: if nick not in nicks: nicks.append(nick) jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] - return Completion(the_input.new_completion, jids_list, 1, quotify=True) + return Completion( + the_input.new_completion, jids_list, 1, quotify=True) muc_list = [tab.name for tab in self.core.get_tabs(tabs.MucTab)] muc_list.append('*') return Completion(the_input.new_completion, muc_list, 1, quotify=True) - diff --git a/poezio/core/core.py b/poezio/core/core.py index 33bbbc54..7678c747 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -66,8 +66,7 @@ class Core(object): self.stdscr = None status = config.get('status') status = POSSIBLE_SHOW.get(status, None) - self.status = Status(show=status, - message=config.get('status_message')) + self.status = Status(show=status, message=config.get('status_message')) self.running = True self.xmpp = connection.Connection() self.xmpp.core = self @@ -81,7 +80,8 @@ class Core(object): # that are displayed in almost all tabs, in an # information window. self.information_buffer = TextBuffer() - self.information_win_size = config.get('info_win_height', section='var') + self.information_win_size = config.get( + 'info_win_height', section='var') self.information_win = windows.TextWin(300) self.information_buffer.add_window(self.information_win) self.left_tab_win = None @@ -164,7 +164,7 @@ class Core(object): 'M-D': self.scroll_info_up, 'M-C': self.scroll_info_down, 'M-k': self.escape_next_key, - ######## actions mappings ########## + ######## actions mappings ########## '_noop': lambda *args, **kwargs: None, '_bookmark': self.command.bookmark, '_bookmark_local': self.command.bookmark_local, @@ -188,25 +188,30 @@ class Core(object): '_show_plugins': self.command.plugins, '_show_xmltab': self.command.xml_tab, '_toggle_pane': self.toggle_left_pane, - ###### status actions ###### + ###### status actions ###### '_available': lambda: self.command.status('available'), '_away': lambda: self.command.status('away'), '_chat': lambda: self.command.status('chat'), '_dnd': lambda: self.command.status('dnd'), '_xa': lambda: self.command.status('xa'), - ##### Custom actions ######## + ##### Custom actions ######## '_exc_': self.try_execute, } self.key_func.update(key_func) # Add handlers self.xmpp.add_event_handler('connected', self.handler.on_connected) - self.xmpp.add_event_handler('connection_failed', self.handler.on_failed_connection) - self.xmpp.add_event_handler('disconnected', self.handler.on_disconnected) - self.xmpp.add_event_handler('stream_error', self.handler.on_stream_error) - self.xmpp.add_event_handler('failed_all_auth', self.handler.on_failed_all_auth) + self.xmpp.add_event_handler('connection_failed', + self.handler.on_failed_connection) + self.xmpp.add_event_handler('disconnected', + self.handler.on_disconnected) + self.xmpp.add_event_handler('stream_error', + self.handler.on_stream_error) + self.xmpp.add_event_handler('failed_all_auth', + self.handler.on_failed_all_auth) self.xmpp.add_event_handler('no_auth', self.handler.on_no_auth) - self.xmpp.add_event_handler("session_start", self.handler.on_session_start) + self.xmpp.add_event_handler("session_start", + self.handler.on_session_start) self.xmpp.add_event_handler("session_start", self.handler.on_session_start_features) self.xmpp.add_event_handler("groupchat_presence", @@ -215,8 +220,9 @@ class Core(object): self.handler.on_groupchat_message) self.xmpp.add_event_handler("groupchat_invite", self.handler.on_groupchat_invitation) - self.xmpp.add_event_handler("groupchat_direct_invite", - self.handler.on_groupchat_direct_invitation) + self.xmpp.add_event_handler( + "groupchat_direct_invite", + self.handler.on_groupchat_direct_invitation) self.xmpp.add_event_handler("groupchat_decline", self.handler.on_groupchat_decline) self.xmpp.add_event_handler("groupchat_config_status", @@ -224,13 +230,17 @@ class Core(object): self.xmpp.add_event_handler("groupchat_subject", self.handler.on_groupchat_subject) self.xmpp.add_event_handler("message", self.handler.on_message) - self.xmpp.add_event_handler("message_error", self.handler.on_error_message) - self.xmpp.add_event_handler("receipt_received", self.handler.on_receipt) + self.xmpp.add_event_handler("message_error", + self.handler.on_error_message) + self.xmpp.add_event_handler("receipt_received", + self.handler.on_receipt) self.xmpp.add_event_handler("got_online", self.handler.on_got_online) self.xmpp.add_event_handler("got_offline", self.handler.on_got_offline) - self.xmpp.add_event_handler("roster_update", self.handler.on_roster_update) + self.xmpp.add_event_handler("roster_update", + self.handler.on_roster_update) self.xmpp.add_event_handler("changed_status", self.handler.on_presence) - self.xmpp.add_event_handler("presence_error", self.handler.on_presence_error) + self.xmpp.add_event_handler("presence_error", + self.handler.on_presence_error) self.xmpp.add_event_handler("roster_subscription_request", self.handler.on_subscription_request) self.xmpp.add_event_handler("roster_subscription_authorized", @@ -252,8 +262,10 @@ class Core(object): self.handler.on_chatstate_inactive) self.xmpp.add_event_handler("attention", self.handler.on_attention) self.xmpp.add_event_handler("ssl_cert", self.handler.validate_ssl) - self.xmpp.add_event_handler("ssl_invalid_chain", self.handler.ssl_invalid_chain) - self.xmpp.add_event_handler('carbon_received', self.handler.on_carbon_received) + self.xmpp.add_event_handler("ssl_invalid_chain", + self.handler.ssl_invalid_chain) + self.xmpp.add_event_handler('carbon_received', + self.handler.on_carbon_received) self.xmpp.add_event_handler('carbon_sent', self.handler.on_carbon_sent) self.xmpp.add_event_handler('http_confirm', self.handler.http_confirm) @@ -315,14 +327,11 @@ class Core(object): self.xmpp.set_keepalive_values) self.add_configuration_handler("connection_check_interval", self.xmpp.set_keepalive_values) - self.add_configuration_handler("themes_dir", - theming.update_themes_dir) - self.add_configuration_handler("theme", - self.on_theme_config_change) + self.add_configuration_handler("themes_dir", 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("password", self.on_password_change) self.add_configuration_handler("enable_vertical_tab_list", self.on_vertical_tab_list_config_change) self.add_configuration_handler("vertical_tab_list_size", @@ -394,14 +403,15 @@ class Core(object): """ Called when the request_message_receipts option changes """ - self.xmpp.plugin['xep_0184'].auto_request = config.get(option, - default=True) + self.xmpp.plugin['xep_0184'].auto_request = config.get( + option, default=True) def on_ack_receipts_config_change(self, option, value): """ Called when the ack_message_receipts option changes """ - self.xmpp.plugin['xep_0184'].auto_ack = config.get(option, default=True) + self.xmpp.plugin['xep_0184'].auto_ack = config.get( + option, default=True) def on_plugins_dir_config_change(self, option, value): """ @@ -438,7 +448,6 @@ class Core(object): """ self.xmpp.password = value - 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. @@ -501,10 +510,10 @@ class Core(object): """ sig = args[0] signals = { - 1: 'SIGHUP', - 13: 'SIGPIPE', - 15: 'SIGTERM', - } + 1: 'SIGHUP', + 13: 'SIGPIPE', + 15: 'SIGTERM', + } log.error("%s received. Exiting…", signals[sig]) if config.get('enable_user_mood'): @@ -547,13 +556,12 @@ class Core(object): 'The online help is here http://doc.poez.io/\n' 'No room is joined by default, but you can join poezio’s' ' room (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) def exit(self, event=None): - log.debug("exit(%s)", event) + log.debug("exit(%s)", event) asyncio.get_event_loop().stop() def on_exception(self, typ, value, trace): @@ -592,11 +600,13 @@ class Core(object): """ main loop waiting for the user to press a key """ + def replace_line_breaks(key): "replace ^J with \n" if key == '^J': return '\n' return key + def separate_chars_from_bindings(char_list): """ returns a list of lists. For example if you give @@ -638,7 +648,7 @@ class Core(object): log.debug("Input is readable.") big_char_list = [replace_key_with_bound(key)\ for key in self.read_keyboard()] - log.debug("Got from keyboard: %s", (big_char_list,)) + log.debug("Got from keyboard: %s", (big_char_list, )) # whether to refresh after ALL keys have been handled for char_list in separate_chars_from_bindings(big_char_list): @@ -651,7 +661,8 @@ class Core(object): except ValueError: pass else: - if self.current_tab().nb == nb and config.get('go_to_previous_tab_on_alt_number'): + 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) @@ -673,12 +684,10 @@ class Core(object): """ ok = roster.save_to_config_file() ok = ok and config.silent_set('info_win_height', - self.information_win_size, - 'var') + self.information_win_size, 'var') if not ok: self.information('Unable to save runtime preferences' - ' in the config file', - 'Error') + ' in the config file', 'Error') def on_roster_enter_key(self, roster_row): """ @@ -690,9 +699,8 @@ class Core(object): else: self.focus_tab_named(roster_row.bare_jid) if isinstance(roster_row, Resource): - if not self.get_conversation_by_jid(roster_row.jid, - False, - fallback_barejid=False): + if not self.get_conversation_by_jid( + roster_row.jid, False, fallback_barejid=False): self.open_conversation_window(roster_row.jid) else: self.focus_tab_named(roster_row.jid) @@ -716,7 +724,6 @@ class Core(object): """ self.do_command(text, True) - ##################### Anything related to command execution ################### def execute(self, line): @@ -727,15 +734,14 @@ class Core(object): return if line.startswith('/'): command = line.strip().split()[0][1:] - arg = line[2+len(command):] # jump the '/' and the ' ' + arg = line[2 + len(command):] # jump the '/' and the ' ' # example. on "/link 0 open", command = "link" and arg = "0 open" if command in self.commands: func = self.commands[command].func func(arg) return else: - self.information("Unknown command (%s)" % (command), - 'Error') + self.information("Unknown command (%s)" % (command), 'Error') def exec_command(self, command): """ @@ -770,16 +776,15 @@ class Core(object): fifo_path = config.get('remote_fifo_path') if not self.remote_fifo: try: - self.remote_fifo = Fifo(os.path.join(fifo_path, - 'poezio.fifo'), - 'w') + self.remote_fifo = Fifo( + os.path.join(fifo_path, 'poezio.fifo'), 'w') except (OSError, IOError) as exc: - log.error('Could not open the fifo for writing (%s)', - os.path.join(fifo_path, './', 'poezio.fifo'), - exc_info=True) + log.error( + 'Could not open the fifo for writing (%s)', + os.path.join(fifo_path, './', 'poezio.fifo'), + exc_info=True) self.information('Could not open the fifo ' - 'file for writing: %s' % exc, - 'Error') + 'file for writing: %s' % exc, 'Error') return args = (pipes.quote(arg.replace('\n', ' ')) for arg in command) @@ -787,10 +792,11 @@ class Core(object): try: self.remote_fifo.write(command_str) except (IOError) as exc: - log.error('Could not write in the fifo (%s): %s', - os.path.join(fifo_path, './', 'poezio.fifo'), - repr(command), - exc_info=True) + log.error( + 'Could not write in the fifo (%s): %s', + os.path.join(fifo_path, './', 'poezio.fifo'), + repr(command), + exc_info=True) self.information('Could not execute %s: %s' % (command, exc), 'Error') self.remote_fifo = None @@ -799,12 +805,12 @@ class Core(object): try: executor.start() except ValueError as exc: - log.error('Could not execute command (%s)', - repr(command), - exc_info=True) + log.error( + 'Could not execute command (%s)', + repr(command), + exc_info=True) self.information('%s' % exc, 'Error') - def do_command(self, key, raw): """ Execute the action associated with a key @@ -824,7 +830,6 @@ class Core(object): else: self.current_tab().on_input(key, raw) - def try_execute(self, line): """ Try to execute a command in the current tab @@ -835,7 +840,6 @@ class Core(object): except: log.error('Execute failed (%s)', line, exc_info=True) - ########################## TImed Events ####################################### def remove_timed_event(self, event): @@ -844,9 +848,8 @@ class Core(object): def add_timed_event(self, event): """Add a new timed event""" - event.handler = asyncio.get_event_loop().call_later(event.delay, - event.callback, - *event.args) + event.handler = asyncio.get_event_loop().call_later( + event.delay, event.callback, *event.args) ####################### XMPP-related actions ################################## @@ -894,7 +897,10 @@ class Core(object): if reconnect: # Add a one-time event to reconnect as soon as we are # effectively disconnected - self.xmpp.add_event_handler('disconnected', lambda event: self.xmpp.connect(), disposable=True) + self.xmpp.add_event_handler( + 'disconnected', + lambda event: self.xmpp.connect(), + disposable=True) def send_message(self, msg): """ @@ -913,20 +919,19 @@ class Core(object): or a mediated one if it does not. TODO: allow passwords """ + def callback(iq): if not iq: return if 'jabber:x:conference' in iq['disco_info'].get_features(): self.xmpp.plugin['xep_0249'].send_invitation( - jid, - room, - reason=reason) - else: # fallback - self.xmpp.plugin['xep_0045'].invite(room, jid, - reason=reason or '') + jid, room, reason=reason) + else: # fallback + self.xmpp.plugin['xep_0045'].invite( + room, jid, reason=reason or '') - self.xmpp.plugin['xep_0030'].get_info(jid=jid, timeout=5, - callback=callback) + self.xmpp.plugin['xep_0030'].get_info( + jid=jid, timeout=5, callback=callback) def get_error_message(self, stanza, deprecated=False): """ @@ -951,16 +956,22 @@ class Core(object): body = condition or 'Unknown error' if code: message = '%(from)s: %(code)s - %(msg)s: %(body)s' % { - 'from': sender, 'msg': msg, 'body': body, 'code': code} + 'from': sender, + 'msg': msg, + 'body': body, + 'code': code + } else: message = '%(from)s: %(msg)s: %(body)s' % { - 'from': sender, 'msg': msg, 'body': body} + 'from': sender, + 'msg': msg, + 'body': body + } return message - ####################### Tab logic-related things ############################## - ### Tab getters ### +### Tab getters ### def get_tabs(self, cls=None): "Get all the tabs of a type" @@ -1002,8 +1013,8 @@ class Core(object): # We create a dynamic conversation with the bare Jid if # nothing was found (and we lock it to the resource # later) - conversation = self.open_conversation_window(jid.bare, - False) + conversation = self.open_conversation_window( + jid.bare, False) else: conversation = None return conversation @@ -1062,7 +1073,8 @@ class Core(object): if not target: if new_pos < len(self.tabs): old_tab = self.tabs[old_pos] - self.tabs[new_pos], self.tabs[old_pos] = old_tab, tabs.GapTab(self) + self.tabs[new_pos], self.tabs[old_pos] = old_tab, tabs.GapTab( + self) else: self.tabs.append(self.tabs[old_pos]) self.tabs[old_pos] = tabs.GapTab(self) @@ -1139,6 +1151,7 @@ class Core(object): Read 2 more chars and go to the tab with the given number """ + def read_next_digit(digit): try: int(digit) @@ -1156,6 +1169,7 @@ class Core(object): else: # We need to read more digits keyboard.continuation_keys_callback = read_next_digit + keyboard.continuation_keys_callback = read_next_digit def go_to_roster(self): @@ -1164,7 +1178,7 @@ class Core(object): def go_to_previous_tab(self): "Go to the previous tab" - self.command.win('%s' % (self.previous_tab_nb,)) + self.command.win('%s' % (self.previous_tab_nb, )) def go_to_important_room(self): """ @@ -1183,15 +1197,14 @@ class Core(object): else: tab_refs[tab.state].append(tab) # sort the state by priority and remove those with negative priority - states = sorted(tab_refs.keys(), - key=(lambda x: priority.get(x, 0)), - reverse=True) + states = sorted( + tab_refs.keys(), key=(lambda x: priority.get(x, 0)), reverse=True) states = [state for state in states if priority.get(state, -1) >= 0] for state in states: for tab in tab_refs[state]: - if (tab.nb < self.current_tab_nb and - tab_refs[state][-1].nb > self.current_tab_nb): + if (tab.nb < self.current_tab_nb + and tab_refs[state][-1].nb > self.current_tab_nb): continue self.command.win('%s' % tab.nb) return @@ -1202,7 +1215,7 @@ class Core(object): for tab in self.tabs: if tab.name == tab_name: if (type_ and (isinstance(tab, type_))) or not type_: - self.command.win('%s' % (tab.nb,)) + self.command.win('%s' % (tab.nb, )) return True return False @@ -1249,7 +1262,7 @@ class Core(object): """ Open a Private conversation in a MUC and focus if needed. """ - complete_jid = room_name+'/'+user_nick + complete_jid = room_name + '/' + user_nick # if the room exists, focus it and return for tab in self.get_tabs(tabs.PrivateTab): if tab.name == complete_jid: @@ -1301,12 +1314,14 @@ class Core(object): if tab: tab.rename_user(old_nick, user) - def on_user_left_private_conversation(self, room_name, user, status_message): + def on_user_left_private_conversation(self, room_name, user, + status_message): """ The user left the MUC: add a message in the associated private conversation """ - tab = self.get_tab_by_name('%s/%s' % (room_name, user.nick), tabs.PrivateTab) + tab = self.get_tab_by_name('%s/%s' % (room_name, user.nick), + tabs.PrivateTab) if tab: tab.user_left(status_message, user) @@ -1315,7 +1330,8 @@ class Core(object): The user joined a MUC: add a message in the associated private conversation """ - tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab) + tab = self.get_tab_by_name('%s/%s' % (room_name, nick), + tabs.PrivateTab) if tab: tab.user_rejoined(nick) @@ -1352,10 +1368,10 @@ class Core(object): if tab is None: tab = self.current_tab() if isinstance(tab, tabs.RosterInfoTab): - return # The tab 0 should NEVER be closed + return # The tab 0 should NEVER be closed tab.on_close() - del tab.key_func # Remove self references - del tab.commands # and make the object collectable + del tab.key_func # Remove self references + del tab.commands # and make the object collectable nb = tab.nb if was_current: if self.previous_tab_nb != nb: @@ -1365,7 +1381,7 @@ class Core(object): if nb >= len(self.tabs) - 1: self.tabs.remove(tab) nb -= 1 - while not self.tabs[nb]: # remove the trailing gaps + while not self.tabs[nb]: # remove the trailing gaps self.tabs.pop() nb -= 1 else: @@ -1397,7 +1413,6 @@ class Core(object): if self.current_tab() is tab: self.refresh_window() - ####################### Curses and ui-related stuff ########################### def doupdate(self): @@ -1412,18 +1427,21 @@ class Core(object): """ filter_types = config.get('information_buffer_type_filter').split(':') if typ.lower() in filter_types: - log.debug('Did not show the message:\n\t%s> %s \n\tdue to information_popup_type_filter configuration', typ, msg) + log.debug( + 'Did not show the message:\n\t%s> %s \n\tdue to information_popup_type_filter configuration', + typ, msg) return False filter_messages = config.get('filter_info_messages').split(':') for words in filter_messages: if words and words in msg: - log.debug('Did not show the message:\n\t%s> %s \n\tdue to filter_info_messages configuration', typ, msg) + log.debug( + 'Did not show the message:\n\t%s> %s \n\tdue to filter_info_messages configuration', + typ, msg) return False colors = get_theme().INFO_COLORS color = colors.get(typ.lower(), colors.get('default', None)) - nb_lines = self.information_buffer.add_message(msg, - nickname=typ, - nick_color=color) + nb_lines = self.information_buffer.add_message( + msg, nickname=typ, nick_color=color) popup_on = config.get('information_buffer_popup_on').split() if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() @@ -1449,8 +1467,8 @@ class Core(object): curses.start_color() curses.use_default_colors() theming.reload_theme() - curses.ungetch(" ") # H4X: without this, the screen is - stdscr.getkey() # erased on the first "getkey()" + curses.ungetch(" ") # H4X: without this, the screen is + stdscr.getkey() # erased on the first "getkey()" def reset_curses(self): """ @@ -1604,9 +1622,8 @@ class Core(object): if time <= 0 or size <= 0: return result = self.grow_information_win(size) - timed_event = timed_events.DelayedEvent(time, - self.shrink_information_win, - result) + timed_event = timed_events.DelayedEvent( + time, self.shrink_information_win, result) self.add_timed_event(timed_event) self.refresh_window() @@ -1627,12 +1644,10 @@ class Core(object): self.information_win_size = tabs.Tab.height - 6 if tabs.Tab.height < 6: self.information_win_size = 0 - height = (tabs.Tab.height - 1 - self.information_win_size - - tabs.Tab.tab_win_height()) - self.information_win.resize(self.information_win_size, - tabs.Tab.width, - height, - 0) + height = (tabs.Tab.height - 1 - self.information_win_size - + tabs.Tab.tab_win_height()) + self.information_win.resize(self.information_win_size, tabs.Tab.width, + height, 0) def resize_global_info_bar(self): """ @@ -1645,16 +1660,15 @@ class Core(object): return try: height, _ = self.stdscr.getmaxyx() - truncated_win = self.stdscr.subwin(height, - config.get('vertical_tab_list_size'), - 0, 0) + truncated_win = self.stdscr.subwin( + height, config.get('vertical_tab_list_size'), 0, 0) except: log.error('Curses error on infobar resize', exc_info=True) return - self.left_tab_win = windows.VerticalGlobalInfoBar(self, truncated_win) + self.left_tab_win = windows.VerticalGlobalInfoBar( + self, truncated_win) elif not self.size.core_degrade_y: - self.tab_win.resize(1, tabs.Tab.width, - tabs.Tab.height - 2, 0) + self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0) self.left_tab_win = None def add_message_to_text_buffer(self, buff, txt, nickname=None): @@ -1663,7 +1677,8 @@ class Core(object): (in the Info tab of the info window in the RosterTab) """ if not buff: - self.information('Trying to add a message in no room: %s' % txt, 'Error') + self.information('Trying to add a message in no room: %s' % txt, + 'Error') return buff.add_message(txt, nickname=nickname) @@ -1683,11 +1698,11 @@ class Core(object): # the screen that they can occupy, and we draw the tab list on the # remaining space, on the left height, width = self.stdscr.getmaxyx() - if (config.get('enable_vertical_tab_list') and - not self.size.core_degrade_x): + if (config.get('enable_vertical_tab_list') + and not self.size.core_degrade_x): try: scr = self.stdscr.subwin(0, - config.get('vertical_tab_list_size')) + config.get('vertical_tab_list_size')) except: log.error('Curses error on resize', exc_info=True) return @@ -1739,227 +1754,300 @@ class Core(object): """ Register the commands when poezio starts """ - self.register_command('help', self.command.help, - 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', - completion=self.completion.join) - self.register_command('exit', self.command.quit, - 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.') - self.register_command('next', self.rotate_rooms_right, - shortdesc='Go to the next room.') - self.register_command('prev', self.rotate_rooms_left, - shortdesc='Go to the previous room.') - self.register_command('win', self.command.win, - usage='<number or name>', - shortdesc='Go to the specified room', - completion=self.completion.win) + self.register_command( + 'help', + self.command.help, + 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', + completion=self.completion.join) + self.register_command( + 'exit', + self.command.quit, + 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.') + self.register_command( + 'next', self.rotate_rooms_right, shortdesc='Go to the next room.') + self.register_command( + 'prev', + self.rotate_rooms_left, + shortdesc='Go to the previous room.') + self.register_command( + 'win', + self.command.win, + 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.', - 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.', - 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.', - completion=self.completion.status) + 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.', + 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.', + 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.', + 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.', - 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.", - 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", - 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', - 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', - completion=self.completion.theme) - self.register_command('list', self.command.list, - usage='[server]', - desc="Get the list of public rooms" - " 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', - 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.', - 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', - 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.", - completion=self.completion.bind, - shortdesc='Bind a key to another key.') - self.register_command('load', self.command.load, - 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)', - completion=self.plugin_manager.completion_unload) - self.register_command('plugins', self.command.plugins, - 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.', - completion=self.completion.presence) - self.register_command('rawxml', self.command.rawxml, - usage='<xml>', - 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.', - completion=self.completion.invite) - self.register_command('invitations', self.command.invitations, - shortdesc='Show the pending invitations.') - self.register_command('bookmarks', self.command.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', - completion=self.completion.remove_bookmark) - self.register_command('xml_tab', self.command.xml_tab, - shortdesc='Open an XML tab.') - self.register_command('runkey', self.command.runkey, - 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.') - 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.', - 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') - self.register_command('reload', self.command.reload, - shortdesc='Reload the config. You can achieve the same by ' - 'sending SIGUSR1 to poezio.') + 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.', + 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.", + 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", + 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', + 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', + completion=self.completion.theme) + self.register_command( + 'list', + self.command.list, + usage='[server]', + desc="Get the list of public rooms" + " 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', + 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.', + 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', + 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.", + completion=self.completion.bind, + shortdesc='Bind a key to another key.') + self.register_command( + 'load', + self.command.load, + 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)', + completion=self.plugin_manager.completion_unload) + self.register_command( + 'plugins', + self.command.plugins, + 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.', + completion=self.completion.presence) + self.register_command( + 'rawxml', + self.command.rawxml, + usage='<xml>', + 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.', + completion=self.completion.invite) + self.register_command( + 'invitations', + self.command.invitations, + shortdesc='Show the pending invitations.') + self.register_command( + 'bookmarks', + self.command.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', + completion=self.completion.remove_bookmark) + self.register_command( + 'xml_tab', self.command.xml_tab, shortdesc='Open an XML tab.') + self.register_command( + 'runkey', + self.command.runkey, + 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.') + 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.', + 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') + 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.', - completion=self.completion.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.', + 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.', - completion=self.completion.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.', + 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.', - completion=None) + 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.', + completion=None) + ####################### Random things to move ################################# @@ -1971,22 +2059,26 @@ class Core(object): 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, - password=bm.password) + self.open_new_room( + bm.jid, nick, focus=False, password=bm.password) self.initial_joins.append(bm.jid) # 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, - status=self.status.message, - show=self.status.show) + muc.join_groupchat( + self, + bm.jid, + nick, + passwd=bm.password, + 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'] @@ -1998,8 +2090,10 @@ class Core(object): return remote_bookmarks = self.bookmarks.remote() self.join_initial_rooms(remote_bookmarks) + if not self.xmpp.anon and config.get('use_remote_bookmarks'): - self.bookmarks.get_remote(self.xmpp, self.information, _join_remote_only) + self.bookmarks.get_remote(self.xmpp, self.information, + _join_remote_only) def room_error(self, error, room_name): """ @@ -2009,32 +2103,42 @@ class Core(object): 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) + 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)' 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'))) + 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() - class KeyDict(dict): """ A dict, with a wrapper for get() that will return a custom value if the key starts with _exc_ """ + def get(self, k, d=None): if isinstance(k, str) and k.startswith('_exc_') and len(k) > 5: return lambda: dict.get(self, '_exc_')(k[5:]) return dict.get(self, k, d) + def replace_key_with_bound(key): """ Replace an inputted key with the one defined as its replacement @@ -2044,5 +2148,3 @@ def replace_key_with_bound(key): if not bind: bind = key return bind - - diff --git a/poezio/core/handlers.py b/poezio/core/handlers.py index e6d54558..ac6ae573 100644 --- a/poezio/core/handlers.py +++ b/poezio/core/handlers.py @@ -80,6 +80,7 @@ class HandlerCore: """ Enable carbons & blocking on session start if wanted and possible """ + def callback(iq): if not iq: return @@ -87,21 +88,23 @@ class HandlerCore: rostertab = self.core.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): + if (config.get('enable_carbons') + and 'urn:xmpp:carbons:2' in features): self.core.xmpp.plugin['xep_0280'].enable() self.core.check_bookmark_storage(features) - self.core.xmpp.plugin['xep_0030'].get_info(jid=self.core.xmpp.boundjid.domain, - callback=callback) + self.core.xmpp.plugin['xep_0030'].get_info( + jid=self.core.xmpp.boundjid.domain, callback=callback) def on_carbon_received(self, message): """ Carbon <received/> received """ + def ignore_message(recv): log.debug('%s has category conference, ignoring carbon', recv['from'].server) + def receive_message(recv): recv['to'] = self.core.xmpp.boundjid.full if recv['receipt']: @@ -109,12 +112,14 @@ class HandlerCore: self.on_normal_message(recv) recv = message['carbon_received'] - if (recv['from'].bare not in roster or - roster[recv['from'].bare].subscription == 'none'): - fixes.has_identity(self.core.xmpp, recv['from'].server, - identity='conference', - on_true=functools.partial(ignore_message, recv), - on_false=functools.partial(receive_message, recv)) + if (recv['from'].bare not in roster + or roster[recv['from'].bare].subscription == 'none'): + fixes.has_identity( + self.core.xmpp, + recv['from'].server, + identity='conference', + on_true=functools.partial(ignore_message, recv), + on_false=functools.partial(receive_message, recv)) return else: receive_message(recv) @@ -123,20 +128,24 @@ class HandlerCore: """ Carbon <sent/> received """ + def ignore_message(sent): log.debug('%s has category conference, ignoring carbon', sent['to'].server) + def send_message(sent): sent['from'] = self.core.xmpp.boundjid.full self.on_normal_message(sent) sent = message['carbon_sent'] - if (sent['to'].bare not in roster or - roster[sent['to'].bare].subscription == 'none'): - fixes.has_identity(self.core.xmpp, sent['to'].server, - identity='conference', - on_true=functools.partial(ignore_message, sent), - on_false=functools.partial(send_message, sent)) + if (sent['to'].bare not in roster + or roster[sent['to'].bare].subscription == 'none'): + fixes.has_identity( + self.core.xmpp, + sent['to'].server, + identity='conference', + on_true=functools.partial(ignore_message, sent), + on_false=functools.partial(send_message, sent)) else: send_message(sent) @@ -150,7 +159,11 @@ class HandlerCore: if jid.bare in self.core.pending_invites: return # there are 2 'x' tags in the messages, making message['x'] useless - invite = StanzaBase(self.core.xmpp, xml=message.xml.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite')) + invite = StanzaBase( + self.core.xmpp, + xml=message.xml.find( + '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite' + )) inviter = invite['from'] reason = invite['reason'] password = invite['password'] @@ -204,7 +217,9 @@ class HandlerCore: When receiving private message from a muc OR a normal message (from one of our contacts) """ - if message.xml.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') is not None: + if message.xml.find( + '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite' + ) is not None: return if message['type'] == 'groupchat': return @@ -228,7 +243,8 @@ class HandlerCore: self.core.room_error(message, jid_from.bare) else: text = self.core.get_error_message(message) - p_tab = self.core.get_tab_by_name(jid_from.full, tabs.PrivateTab) + p_tab = self.core.get_tab_by_name(jid_from.full, + tabs.PrivateTab) if p_tab: p_tab.add_error(text) else: @@ -240,12 +256,11 @@ class HandlerCore: self.core.information(error_msg, 'Error') return error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK), - error_msg) + error_msg) if not tab.nack_message('\n' + error, message['id'], message['to']): tab.add_message(error, typ=0) self.core.refresh_window() - def on_normal_message(self, message): """ When receiving "normal" messages (not a private message from a @@ -254,14 +269,18 @@ class HandlerCore: if message['type'] == 'error': return elif message['type'] == 'headline' and message['body']: - return self.core.information('%s says: %s' % (message['from'], message['body']), 'Headline') + return self.core.information( + '%s says: %s' % (message['from'], message['body']), 'Headline') - use_xhtml = config.get_by_tabname('enable_xhtml_im', message['from'].bare) + use_xhtml = config.get_by_tabname('enable_xhtml_im', + message['from'].bare) tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') extract_images = config.get('extract_inline_images') - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) + body = xhtml.get_body_from_message_stanza( + message, + use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) if not body: if not self.core.xmpp.plugin['xep_0380'].has_eme(message): return @@ -279,7 +298,8 @@ class HandlerCore: remote_nick = roster[conv_jid.bare].name # check for a received nick if not remote_nick and config.get('enable_user_nick'): - if message.xml.find('{http://jabber.org/protocol/nick}nick') is not None: + if message.xml.find( + '{http://jabber.org/protocol/nick}nick') is not None: remote_nick = message['nick']['nick'] if not remote_nick: remote_nick = conv_jid.user @@ -298,7 +318,8 @@ class HandlerCore: return conversation = self.core.get_conversation_by_jid(conv_jid, create=True) - if isinstance(conversation, tabs.DynamicConversationTab) and conv_jid.resource: + if isinstance(conversation, + tabs.DynamicConversationTab) and conv_jid.resource: conversation.lock(conv_jid.resource) if not own and not conversation.nick: @@ -312,9 +333,11 @@ class HandlerCore: self.core.events.trigger('conversation_msg', message, conversation) if not message['body']: return - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) + body = xhtml.get_body_from_message_stanza( + message, + use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) delayed, date = common.find_delayed_tag(message) def try_modify(): @@ -324,21 +347,27 @@ class HandlerCore: if replaced_id and config.get_by_tabname('group_corrections', conv_jid.bare): try: - conversation.modify_message(body, replaced_id, message['id'], jid=jid, - nickname=remote_nick) + conversation.modify_message( + body, + replaced_id, + message['id'], + jid=jid, + nickname=remote_nick) return True except CorrectionError: log.debug('Unable to correct a message', exc_info=True) return False if not try_modify(): - conversation.add_message(body, date, - nickname=remote_nick, - nick_color=color, - history=delayed, - identifier=message['id'], - jid=jid, - typ=1) + conversation.add_message( + body, + date, + nickname=remote_nick, + nick_color=color, + history=delayed, + identifier=message['id'], + jid=jid, + typ=1) if conversation.remote_wants_chatstates is None and not delayed: if message['chat_state']: @@ -366,7 +395,8 @@ class HandlerCore: return log.debug('Received 0084 avatar update from %s', jid) try: - metadata = msg['pubsub_event']['items']['item']['avatar_metadata']['items'] + metadata = msg['pubsub_event']['items']['item']['avatar_metadata'][ + 'items'] except Exception: log.debug('Failed getting metadata from 0084:', exc_info=True) return @@ -387,12 +417,16 @@ class HandlerCore: # If we didn’t have any, query the data instead. if not info['url']: try: - result = yield from self.core.xmpp['xep_0084'].retrieve_avatar(jid, - avatar_hash, - timeout=60) - contact.avatar = result['pubsub']['items']['item']['avatar_data']['value'] + result = yield from self.core.xmpp[ + 'xep_0084'].retrieve_avatar( + jid, avatar_hash, timeout=60) + contact.avatar = result['pubsub']['items']['item'][ + 'avatar_data']['value'] except Exception: - log.debug('Failed retrieving 0084 data from %s:', jid, exc_info=True) + log.debug( + 'Failed retrieving 0084 data from %s:', + jid, + exc_info=True) continue log.debug('Received %s avatar: %s', jid, info['type']) @@ -427,9 +461,8 @@ class HandlerCore: # If we didn’t have any, query the vCard instead. try: - result = yield from self.core.xmpp['xep_0054'].get_vcard(jid, - cached=True, - timeout=60) + result = yield from self.core.xmpp['xep_0054'].get_vcard( + jid, cached=True, timeout=60) avatar = result['vcard_temp']['PHOTO'] contact.avatar = avatar['BINVAL'] except Exception: @@ -473,25 +506,32 @@ class HandlerCore: item = item['gaming'] # only name and server_address are used for now contact.gaming = { - 'character_name': item['character_name'], - 'character_profile': item['character_profile'], - 'name': item['name'], - 'level': item['level'], - 'uri': item['uri'], - 'server_name': item['server_name'], - 'server_address': item['server_address'], - } + 'character_name': item['character_name'], + 'character_profile': item['character_profile'], + 'name': item['name'], + 'level': item['level'], + 'uri': item['uri'], + 'server_name': item['server_name'], + 'server_address': item['server_address'], + } else: contact.gaming = {} if contact.gaming: - logger.log_roster_change(contact.bare_jid, 'is playing %s' % (common.format_gaming_string(contact.gaming))) + logger.log_roster_change( + contact.bare_jid, 'is playing %s' % + (common.format_gaming_string(contact.gaming))) - if old_gaming != contact.gaming and config.get_by_tabname('display_gaming_notifications', contact.bare_jid): + if old_gaming != contact.gaming and config.get_by_tabname( + 'display_gaming_notifications', contact.bare_jid): if contact.gaming: - self.core.information('%s is playing %s' % (contact.bare_jid, common.format_gaming_string(contact.gaming)), 'Gaming') + self.core.information( + '%s is playing %s' % + (contact.bare_jid, + common.format_gaming_string(contact.gaming)), 'Gaming') else: - self.core.information(contact.bare_jid + ' stopped playing.', 'Gaming') + self.core.information(contact.bare_jid + ' stopped playing.', + 'Gaming') def on_mood_event(self, message): """ @@ -518,13 +558,18 @@ class HandlerCore: contact.mood = '' if contact.mood: - logger.log_roster_change(contact.bare_jid, 'has now the mood: %s' % contact.mood) + logger.log_roster_change(contact.bare_jid, + 'has now the mood: %s' % contact.mood) - if old_mood != contact.mood and config.get_by_tabname('display_mood_notifications', contact.bare_jid): + if old_mood != contact.mood and config.get_by_tabname( + 'display_mood_notifications', contact.bare_jid): if contact.mood: - self.core.information('Mood from '+ contact.bare_jid + ': ' + contact.mood, 'Mood') + self.core.information( + 'Mood from ' + contact.bare_jid + ': ' + contact.mood, + 'Mood') else: - self.core.information(contact.bare_jid + ' stopped having his/her mood.', 'Mood') + self.core.information( + contact.bare_jid + ' stopped having his/her mood.', 'Mood') def on_activity_event(self, message): """ @@ -537,7 +582,8 @@ class HandlerCore: roster.modified() item = message['pubsub_event']['items']['item'] old_activity = contact.activity - if item.xml.find('{http://jabber.org/protocol/activity}activity') is not None: + if item.xml.find( + '{http://jabber.org/protocol/activity}activity') is not None: try: activity = item['activity']['value'] except ValueError: @@ -557,13 +603,18 @@ class HandlerCore: contact.activity = '' if contact.activity: - logger.log_roster_change(contact.bare_jid, 'has now the activity %s' % contact.activity) + logger.log_roster_change( + contact.bare_jid, 'has now the activity %s' % contact.activity) - if old_activity != contact.activity and config.get_by_tabname('display_activity_notifications', contact.bare_jid): + if old_activity != contact.activity and config.get_by_tabname( + 'display_activity_notifications', contact.bare_jid): if contact.activity: - self.core.information('Activity from '+ contact.bare_jid + ': ' + contact.activity, 'Activity') + self.core.information('Activity from ' + contact.bare_jid + + ': ' + contact.activity, 'Activity') else: - self.core.information(contact.bare_jid + ' stopped doing his/her activity.', 'Activity') + self.core.information( + contact.bare_jid + ' stopped doing his/her activity.', + 'Activity') def on_tune_event(self, message): """ @@ -579,27 +630,31 @@ class HandlerCore: if item.xml.find('{http://jabber.org/protocol/tune}tune') is not None: item = item['tune'] contact.tune = { - 'artist': item['artist'], - 'length': item['length'], - 'rating': item['rating'], - 'source': item['source'], - 'title': item['title'], - 'track': item['track'], - 'uri': item['uri'] - } + 'artist': item['artist'], + 'length': item['length'], + 'rating': item['rating'], + 'source': item['source'], + 'title': item['title'], + 'track': item['track'], + 'uri': item['uri'] + } else: contact.tune = {} if contact.tune: - logger.log_roster_change(message['from'].bare, 'is now listening to %s' % common.format_tune_string(contact.tune)) + logger.log_roster_change(message['from'].bare, + 'is now listening to %s' % + common.format_tune_string(contact.tune)) - if old_tune != contact.tune and config.get_by_tabname('display_tune_notifications', contact.bare_jid): + if old_tune != contact.tune and config.get_by_tabname( + 'display_tune_notifications', contact.bare_jid): if contact.tune: self.core.information( - 'Tune from '+ message['from'].bare + ': ' + common.format_tune_string(contact.tune), - 'Tune') + 'Tune from ' + message['from'].bare + ': ' + + common.format_tune_string(contact.tune), 'Tune') else: - self.core.information(contact.bare_jid + ' stopped listening to music.', 'Tune') + self.core.information( + contact.bare_jid + ' stopped listening to music.', 'Tune') def on_groupchat_message(self, message): """ @@ -609,14 +664,16 @@ class HandlerCore: return room_from = message['from'].bare - if message['type'] == 'error': # Check if it's an error + if message['type'] == 'error': # Check if it's an error self.core.room_error(message, room_from) return tab = self.core.get_tab_by_name(room_from, tabs.MucTab) if not tab: - self.core.information("message received for a non-existing room: %s" % (room_from)) - muc.leave_groupchat(self.core.xmpp, room_from, self.core.own_nick, msg='') + self.core.information( + "message received for a non-existing room: %s" % (room_from)) + muc.leave_groupchat( + self.core.xmpp, room_from, self.core.own_nick, msg='') return nick_from = message['mucnick'] @@ -628,9 +685,11 @@ class HandlerCore: use_xhtml = config.get_by_tabname('enable_xhtml_im', room_from) tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') extract_images = config.get('extract_inline_images') - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) + body = xhtml.get_body_from_message_stanza( + message, + use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) if not body: return @@ -639,18 +698,29 @@ class HandlerCore: replaced = False if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None: replaced_id = message['replace']['id'] - if replaced_id is not '' and config.get_by_tabname('group_corrections', - message['from'].bare): + if replaced_id is not '' and config.get_by_tabname( + 'group_corrections', message['from'].bare): try: delayed_date = date or datetime.now() - if tab.modify_message(body, replaced_id, message['id'], + if tab.modify_message( + body, + replaced_id, + message['id'], time=delayed_date, - nickname=nick_from, user=user): + nickname=nick_from, + user=user): self.core.events.trigger('highlight', message, tab) replaced = True except CorrectionError: log.debug('Unable to correct a message', exc_info=True) - if not replaced and tab.add_message(body, date, nick_from, history=delayed, identifier=message['id'], jid=message['from'], typ=1): + if not replaced and tab.add_message( + body, + date, + nick_from, + history=delayed, + identifier=message['id'], + jid=message['from'], + typ=1): self.core.events.trigger('highlight', message, tab) if message['from'].resource == tab.own_nick: @@ -693,45 +763,61 @@ class HandlerCore: use_xhtml = config.get_by_tabname('enable_xhtml_im', jid.bare) tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') extract_images = config.get('extract_inline_images') - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) - tab = self.core.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation + body = xhtml.get_body_from_message_stanza( + message, + use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) + tab = self.core.get_tab_by_name( + jid.full, + tabs.PrivateTab) # get the tab with the private conversation ignore = config.get_by_tabname('ignore_private', room_from) - if not tab: # It's the first message we receive: create the tab + if not tab: # It's the first message we receive: create the tab if body and not ignore: - tab = self.core.open_private_window(room_from, nick_from, False) + tab = self.core.open_private_window(room_from, nick_from, + False) if ignore: self.core.events.trigger('ignored_private', message, tab) msg = config.get_by_tabname('private_auto_response', room_from) if msg and body: - self.core.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat') + self.core.xmpp.send_message( + mto=jid.full, mbody=msg, mtype='chat') return tab.last_remote_message = datetime.now() self.core.events.trigger('private_msg', message, tab) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) + body = xhtml.get_body_from_message_stanza( + message, + use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) if not body or not tab: return replaced = False user = tab.parent_muc.get_user_by_name(nick_from) if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None: replaced_id = message['replace']['id'] - if replaced_id is not '' and config.get_by_tabname('group_corrections', - room_from): + if replaced_id is not '' and config.get_by_tabname( + 'group_corrections', room_from): try: - tab.modify_message(body, replaced_id, message['id'], user=user, jid=message['from'], - nickname=nick_from) + tab.modify_message( + body, + replaced_id, + message['id'], + user=user, + jid=message['from'], + nickname=nick_from) replaced = True except CorrectionError: log.debug('Unable to correct a message', exc_info=True) if not replaced: - tab.add_message(body, time=None, nickname=nick_from, - forced_user=user, - identifier=message['id'], - jid=message['from'], - typ=1) + tab.add_message( + body, + time=None, + nickname=nick_from, + forced_user=user, + identifier=message['id'], + jid=message['from'], + typ=1) if tab.remote_wants_chatstates is None: if message['chat_state']: @@ -767,7 +853,8 @@ class HandlerCore: def _on_chatstate(self, message, state): if message['type'] == 'chat': if not self._on_chatstate_normal_conversation(message, state): - tab = self.core.get_tab_by_name(message['from'].full, tabs.PrivateTab) + tab = self.core.get_tab_by_name(message['from'].full, + tabs.PrivateTab) if not tab: return self._on_chatstate_private_conversation(message, state) @@ -862,10 +949,10 @@ class HandlerCore: contact = roster.get_and_set(jid) roster.update_contact_groups(contact) contact.pending_in = True - self.core.information('%s wants to subscribe to your presence, use ' - '/accept <jid> or /deny <jid> in the roster ' - 'tab to accept or reject the query.' % jid, - 'Roster') + self.core.information( + '%s wants to subscribe to your presence, use ' + '/accept <jid> or /deny <jid> in the roster ' + 'tab to accept or reject the query.' % jid, 'Roster') self.core.get_tab_by_number(0).state = 'highlight' roster.modified() if isinstance(self.core.current_tab(), tabs.RosterInfoTab): @@ -876,7 +963,8 @@ class HandlerCore: jid = presence['from'].bare contact = roster[jid] if contact.subscription not in ('both', 'from'): - self.core.information('%s accepted your contact proposal' % jid, 'Roster') + self.core.information('%s accepted your contact proposal' % jid, + 'Roster') if contact.pending_out: contact.pending_out = False @@ -892,7 +980,8 @@ class HandlerCore: if not contact: return roster.modified() - self.core.information('%s does not want to receive your status anymore.' % jid, 'Roster') + self.core.information( + '%s does not want to receive your status anymore.' % jid, 'Roster') self.core.get_tab_by_number(0).state = 'highlight' if isinstance(self.core.current_tab(), tabs.RosterInfoTab): self.core.refresh_window() @@ -905,10 +994,13 @@ class HandlerCore: return roster.modified() if contact.pending_out: - self.core.information('%s rejected your contact proposal' % jid, 'Roster') + self.core.information('%s rejected your contact proposal' % jid, + 'Roster') contact.pending_out = False else: - self.core.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster') + self.core.information( + '%s does not want you to receive his/her/its status anymore.' % + jid, 'Roster') self.core.get_tab_by_number(0).state = 'highlight' if isinstance(self.core.current_tab(), tabs.RosterInfoTab): self.core.refresh_window() @@ -916,7 +1008,8 @@ class HandlerCore: ### Presence-related handlers ### def on_presence(self, presence): - if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x') is not None: + if presence.match('presence/muc') or presence.xml.find( + '{http://jabber.org/protocol/muc#user}x') is not None: return jid = presence['from'] contact = roster[jid.bare] @@ -930,10 +1023,12 @@ class HandlerCore: return roster.modified() contact.error = None - self.core.events.trigger('normal_presence', presence, contact[jid.full]) + self.core.events.trigger('normal_presence', presence, + contact[jid.full]) tab = self.core.get_conversation_by_jid(jid, create=False) if tab: - tab.update_status(Status(show=presence['show'], message=presence['status'])) + tab.update_status( + Status(show=presence['show'], message=presence['status'])) if isinstance(self.core.current_tab(), tabs.RosterInfoTab): self.core.refresh_window() elif self.core.current_tab() == tab: @@ -956,7 +1051,8 @@ class HandlerCore: """ A JID got offline """ - if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x') is not None: + if presence.match('presence/muc') or presence.xml.find( + '{http://jabber.org/protocol/muc#user}x') is not None: return jid = presence['from'] if not logger.log_roster_change(jid.bare, 'got offline'): @@ -970,9 +1066,12 @@ class HandlerCore: if contact.name: name = contact.name if jid.resource: - self.core.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % name) - self.core.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % name) - self.core.information('\x193}%s \x195}is \x191}offline' % name, 'Roster') + self.core.add_information_message_to_conversation_tab( + jid.full, '\x195}%s is \x191}offline' % name) + self.core.add_information_message_to_conversation_tab( + jid.bare, '\x195}%s is \x191}offline' % name) + self.core.information('\x193}%s \x195}is \x191}offline' % name, + 'Roster') roster.modified() if isinstance(self.core.current_tab(), tabs.RosterInfoTab): self.core.refresh_window() @@ -981,7 +1080,8 @@ class HandlerCore: """ A JID got online """ - if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x') is not None: + if presence.match('presence/muc') or presence.xml.find( + '{http://jabber.org/protocol/muc#user}x') is not None: return jid = presence['from'] contact = roster[jid.bare] @@ -996,17 +1096,22 @@ class HandlerCore: 'priority': presence.get_priority() or 0, 'status': presence['status'], 'show': presence['show'], - }) + }) self.core.events.trigger('normal_presence', presence, resource) name = contact.name if contact.name else jid.bare - self.core.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % name) + self.core.add_information_message_to_conversation_tab( + jid.full, '\x195}%s is \x194}online' % name) if time.time() - self.core.connection_time > 10: # We do not display messages if we recently logged in if presence['status']: - self.core.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (name, presence['status']), "Roster") + self.core.information( + "\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % + (name, presence['status']), "Roster") else: - self.core.information("\x193}%s \x195}is \x194}online\x195}" % name, "Roster") - self.core.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % name) + self.core.information( + "\x193}%s \x195}is \x194}online\x195}" % name, "Roster") + self.core.add_information_message_to_conversation_tab( + jid.bare, '\x195}%s is \x194}online' % name) if isinstance(self.core.current_tab(), tabs.RosterInfoTab): self.core.refresh_window() @@ -1022,14 +1127,14 @@ class HandlerCore: self.core.events.trigger('muc_presence', presence, tab) tab.handle_presence(presence) - ### Connection-related handlers ### def on_failed_connection(self, error): """ We cannot contact the remote server """ - self.core.information("Connection to remote server failed: %s" % (error,), 'Error') + self.core.information("Connection to remote server failed: %s" % + (error, ), 'Error') @asyncio.coroutine def on_disconnected(self, event): @@ -1046,10 +1151,12 @@ class HandlerCore: tab.disconnect() msg_typ = 'Error' if not self.core.legitimate_disconnect else 'Info' self.core.information("Disconnected from server.", msg_typ) - if self.core.legitimate_disconnect or not config.get('auto_reconnect', True): + if self.core.legitimate_disconnect or not config.get( + 'auto_reconnect', True): return - if (self.core.last_stream_error and - self.core.last_stream_error[1]['condition'] in ('conflict', 'host-unknown')): + if (self.core.last_stream_error + and self.core.last_stream_error[1]['condition'] in ( + 'conflict', 'host-unknown')): return yield from asyncio.sleep(1) self.core.information("Auto-reconnecting.", 'Info') @@ -1076,8 +1183,8 @@ class HandlerCore: """ Authentication failed (no mech) """ - self.core.information("Authentication failed, no login method available.", - 'Error') + self.core.information( + "Authentication failed, no login method available.", 'Error') self.core.legitimate_disconnect = True def on_connected(self, event): @@ -1092,10 +1199,11 @@ class HandlerCore: Called when we are connected and authenticated """ self.core.connection_time = time.time() - if not self.core.plugins_autoloaded: # Do not reload plugins on reconnection + if not self.core.plugins_autoloaded: # Do not reload plugins on reconnection self.core.autoload_plugins() self.core.information("Authentication success.", 'Info') - self.core.information("Your JID is %s" % self.core.xmpp.boundjid.full, 'Info') + self.core.information("Your JID is %s" % self.core.xmpp.boundjid.full, + 'Info') if not self.core.xmpp.anon: # request the roster self.core.xmpp.get_roster() @@ -1112,7 +1220,8 @@ class HandlerCore: self.core.join_initial_rooms(self.core.bookmarks) if config.get('enable_user_nick'): - self.core.xmpp.plugin['xep_0172'].publish_nick(nick=self.core.own_nick, callback=dumb_callback) + self.core.xmpp.plugin['xep_0172'].publish_nick( + nick=self.core.own_nick, callback=dumb_callback) asyncio.ensure_future(self.core.xmpp.plugin['xep_0115'].update_caps()) # Start the ping's plugin regular event self.core.xmpp.set_keepalive_values() @@ -1126,9 +1235,14 @@ class HandlerCore: """ room_from = message['from'] tab = self.core.get_tab_by_name(room_from, tabs.MucTab) - status_codes = {s.attrib['code'] for s in message.xml.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))} + status_codes = { + s.attrib['code'] + for s in message.xml.findall('{%s}x/{%s}status' % ( + tabs.NS_MUC_USER, tabs.NS_MUC_USER)) + } if '101' in status_codes: - self.core.information('Your affiliation in the room %s changed' % room_from, 'Info') + self.core.information( + 'Your affiliation in the room %s changed' % room_from, 'Info') elif tab and status_codes: show_unavailable = '102' in status_codes hide_unavailable = '103' in status_codes @@ -1141,38 +1255,70 @@ class HandlerCore: modif = False if show_unavailable or hide_unavailable or non_priv or logging_off\ or non_anon or semi_anon or full_anon: - tab.add_message('\x19%(info_col)s}Info: A configuration change not privacy-related occured.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + tab.add_message( + '\x19%(info_col)s}Info: A configuration change not privacy-related occured.' + % { + 'info_col': dump_tuple( + get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) modif = True if show_unavailable: - tab.add_message('\x19%(info_col)s}Info: The unavailable members are now shown.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + tab.add_message( + '\x19%(info_col)s}Info: The unavailable members are now shown.' + % { + 'info_col': dump_tuple( + get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) elif hide_unavailable: - tab.add_message('\x19%(info_col)s}Info: The unavailable members are now hidden.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + tab.add_message( + '\x19%(info_col)s}Info: The unavailable members are now hidden.' + % { + 'info_col': dump_tuple( + get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) if non_anon: - tab.add_message('\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + tab.add_message( + '\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' + % { + 'info_col': dump_tuple( + get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) elif semi_anon: - tab.add_message('\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + tab.add_message( + '\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' + % { + 'info_col': dump_tuple( + get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) elif full_anon: - tab.add_message('\x19%(info_col)s}Info: The room is now fully anonymous.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + tab.add_message( + '\x19%(info_col)s}Info: The room is now fully anonymous.' % + { + 'info_col': dump_tuple( + get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) if logging_on: - tab.add_message('\x191}Warning: \x19%(info_col)s}This room is publicly logged' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + tab.add_message( + '\x191}Warning: \x19%(info_col)s}This room is publicly logged' + % { + 'info_col': dump_tuple( + get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) elif logging_off: - tab.add_message('\x19%(info_col)s}Info: This room is not logged anymore.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + tab.add_message( + '\x19%(info_col)s}Info: This room is not logged anymore.' % + { + 'info_col': dump_tuple( + get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) if modif: self.core.refresh_window() @@ -1203,22 +1349,30 @@ class HandlerCore: after = '' if user: user_col = dump_tuple(user.color) - user_string = '\x19%s}%s\x19%s}%s' % (user_col, nick_from, fmt['info_col'], after) + user_string = '\x19%s}%s\x19%s}%s' % (user_col, nick_from, + fmt['info_col'], + after) else: - user_string = '\x19%s}%s%s' % (fmt['info_col'], nick_from, after) + user_string = '\x19%s}%s%s' % (fmt['info_col'], nick_from, + after) fmt['user'] = user_string if nick_from: - tab.add_message("%(user)s set the subject to: \x19%(text_col)s}%(subject)s" % fmt, - time=None, - typ=2) + tab.add_message( + "%(user)s set the subject to: \x19%(text_col)s}%(subject)s" + % fmt, + time=None, + typ=2) else: - tab.add_message("\x19%(info_col)s}The subject is: \x19%(text_col)s}%(subject)s" % fmt, - time=None, - typ=2) + tab.add_message( + "\x19%(info_col)s}The subject is: \x19%(text_col)s}%(subject)s" + % fmt, + time=None, + typ=2) tab.topic = subject tab.topic_from = nick_from - if self.core.get_tab_by_name(room_from, tabs.MucTab) is self.core.current_tab(): + if self.core.get_tab_by_name(room_from, + tabs.MucTab) is self.core.current_tab(): self.core.refresh_window() def on_receipt(self, message): @@ -1231,7 +1385,8 @@ class HandlerCore: return conversation = self.core.get_tab_by_name(jid.full, tabs.OneToOneTab) - conversation = conversation or self.core.get_tab_by_name(jid.bare, tabs.OneToOneTab) + conversation = conversation or self.core.get_tab_by_name( + jid.bare, tabs.OneToOneTab) if not conversation: log.error("Received ack from non-existing chat tab: %s", jid) return @@ -1272,15 +1427,21 @@ class HandlerCore: if self.core.xml_tab: if PYGMENTS: xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER) - poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True).rstrip('\x19o').strip() + poezio_colored = xhtml.xhtml_to_poezio_colors( + xhtml_text, force=True).rstrip('\x19o').strip() else: poezio_colored = '%s' % stanza - self.core.add_message_to_text_buffer(self.core.xml_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_OUT) + self.core.add_message_to_text_buffer( + self.core.xml_buffer, + poezio_colored, + nickname=get_theme().CHAR_XML_OUT) try: - if self.core.xml_tab.match_stanza(ElementBase(ET.fromstring(stanza))): - self.core.add_message_to_text_buffer(self.core.xml_tab.filtered_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_OUT) + if self.core.xml_tab.match_stanza( + ElementBase(ET.fromstring(stanza))): + self.core.add_message_to_text_buffer( + self.core.xml_tab.filtered_buffer, + poezio_colored, + nickname=get_theme().CHAR_XML_OUT) except: log.debug('', exc_info=True) @@ -1295,15 +1456,20 @@ class HandlerCore: if self.core.xml_tab: if PYGMENTS: xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER) - poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True).rstrip('\x19o').strip() + poezio_colored = xhtml.xhtml_to_poezio_colors( + xhtml_text, force=True).rstrip('\x19o').strip() else: poezio_colored = '%s' % stanza - self.core.add_message_to_text_buffer(self.core.xml_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_IN) + self.core.add_message_to_text_buffer( + self.core.xml_buffer, + poezio_colored, + nickname=get_theme().CHAR_XML_IN) try: if self.core.xml_tab.match_stanza(stanza): - self.core.add_message_to_text_buffer(self.core.xml_tab.filtered_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_IN) + self.core.add_message_to_text_buffer( + self.core.xml_tab.filtered_buffer, + poezio_colored, + nickname=get_theme().CHAR_XML_IN) except: log.debug('', exc_info=True) if isinstance(self.core.current_tab(), tabs.XMLTab): @@ -1311,33 +1477,34 @@ class HandlerCore: self.core.doupdate() def ssl_invalid_chain(self, tb): - self.core.information('The certificate sent by the server is invalid.', 'Error') + self.core.information('The certificate sent by the server is invalid.', + 'Error') self.core.disconnect() def _ssl_pop_tab(self, old_cert, new_cert): def cb(result): if result: self.core.information( - 'New certificate accepted:\nnew: %s\nold: %s' % ( - old_cert, new_cert), - 'Info') + 'New certificate accepted:\nnew: %s\nold: %s' % + (old_cert, new_cert), 'Info') log.debug('Setting certificate to %s', new_cert) if not config.silent_set('certificate', new_cert): - self.core.information( - 'Unable to write in the config file', - 'Error') + self.core.information('Unable to write in the config file', + 'Error') else: - self.core.information('You refused to validate the certificate.' - ' You are now disconnected.', 'Info') + self.core.information( + 'You refused to validate the certificate.' + ' You are now disconnected.', 'Info') self.core.disconnect() - confirm_tab = tabs.ConfirmTab(self.core, - 'Certificate check required', - CERT_WARNING_TEXT % (self.core.xmpp.boundjid.domain, old_cert, new_cert), - 'You need to accept or reject the certificate', - cb, - critical=True) - + confirm_tab = tabs.ConfirmTab( + self.core, + 'Certificate check required', + CERT_WARNING_TEXT % (self.core.xmpp.boundjid.domain, old_cert, + new_cert), + 'You need to accept or reject the certificate', + cb, + critical=True) self.core.add_tab(confirm_tab, True) self.core.doupdate() @@ -1356,21 +1523,28 @@ class HandlerCore: cert = config.get('certificate') # update the cert representation when it uses the old one if cert and ':' not in cert: - cert = ':'.join(i + j for i, j in zip(cert[::2], cert[1::2])).upper() + cert = ':'.join( + i + j for i, j in zip(cert[::2], cert[1::2])).upper() config.set_and_save('certificate', cert) der = ssl.PEM_cert_to_DER_cert(pem) - asn1 = pyasn1.codec.der.decoder.decode(der, asn1Spec=pyasn1_modules.rfc2459.Certificate())[0] - spki = asn1.getComponentByName("tbsCertificate").getComponentByName("subjectPublicKeyInfo") - spki_digest = sha256(pyasn1.codec.der.encoder.encode(spki)).hexdigest().upper() - spki_found_cert = ':'.join(i + j for i, j in zip(spki_digest[::2], spki_digest[1::2])) + asn1 = pyasn1.codec.der.decoder.decode( + der, asn1Spec=pyasn1_modules.rfc2459.Certificate())[0] + spki = asn1.getComponentByName("tbsCertificate").getComponentByName( + "subjectPublicKeyInfo") + spki_digest = sha256( + pyasn1.codec.der.encoder.encode(spki)).hexdigest().upper() + spki_found_cert = ':'.join( + i + j for i, j in zip(spki_digest[::2], spki_digest[1::2])) sha2_digest = sha512(der).hexdigest().upper() - sha2_found_cert = ':'.join(i + j for i, j in zip(sha2_digest[::2], sha2_digest[1::2])) + sha2_found_cert = ':'.join( + i + j for i, j in zip(sha2_digest[::2], sha2_digest[1::2])) if cert: if sha2_found_cert == cert: - log.debug('Current hash is cert hash, moving to SPKI hash (%s)', - spki_found_cert) + log.debug( + 'Current hash is cert hash, moving to SPKI hash (%s)', + spki_found_cert) config.set_and_save('certificate', spki_found_cert) return elif spki_found_cert == cert: @@ -1380,10 +1554,12 @@ class HandlerCore: else: log.debug('First time. Setting certificate to %s', spki_found_cert) if not config.silent_set('certificate', spki_found_cert): - self.core.information('Unable to write in the config file', 'Error') + self.core.information('Unable to write in the config file', + 'Error') def http_confirm(self, stanza): confirm = stanza['confirm'] + def cb(result): if result: reply = stanza.reply() @@ -1396,13 +1572,15 @@ class HandlerCore: reply.append(stanza['confirm']) reply.send() - c_id, c_url, c_method = confirm['id'], confirm['url'], confirm['method'] - confirm_tab = tabs.ConfirmTab(self.core, - 'HTTP Verification', - HTTP_VERIF_TEXT % (c_method, c_url, c_id, stanza['from'].full), - 'An HTTP verification was requested', - cb, - critical=False) + c_id, c_url, c_method = confirm['id'], confirm['url'], confirm[ + 'method'] + confirm_tab = tabs.ConfirmTab( + self.core, + 'HTTP Verification', + HTTP_VERIF_TEXT % (c_method, c_url, c_id, stanza['from'].full), + 'An HTTP verification was requested', + cb, + critical=False) self.core.add_tab(confirm_tab, False) self.core.refresh_window() self.core.doupdate() @@ -1411,19 +1589,24 @@ class HandlerCore: def next_adhoc_step(self, iq, adhoc_session): status = iq['command']['status'] - xform = iq.xml.find('{http://jabber.org/protocol/commands}command/{jabber:x:data}x') + xform = iq.xml.find( + '{http://jabber.org/protocol/commands}command/{jabber:x:data}x') if xform is not None: form = self.core.xmpp.plugin['xep_0004'].build_form(xform) else: form = None if status == 'error': - return self.core.information("An error occured while executing the command") + return self.core.information( + "An error occured while executing the command") if status == 'executing': if not form: - self.core.information("Adhoc command step does not contain a data-form. Aborting the execution.", "Error") - return self.core.xmpp.plugin['xep_0050'].cancel_command(adhoc_session) + self.core.information( + "Adhoc command step does not contain a data-form. Aborting the execution.", + "Error") + return self.core.xmpp.plugin['xep_0050'].cancel_command( + adhoc_session) on_validate = self._validate_adhoc_step on_cancel = self._cancel_adhoc_command if status == 'completed': @@ -1435,18 +1618,20 @@ class HandlerCore: if form: for note in iq['command']['notes']: form.add_field(type='fixed', label=note[1]) - self.core.open_new_form(form, on_cancel, on_validate, - session=adhoc_session) - else: # otherwise, just display an information - # message + self.core.open_new_form( + form, on_cancel, on_validate, session=adhoc_session) + else: # otherwise, just display an information + # message notes = '\n'.join([note[1] for note in iq['command']['notes']]) - self.core.information("Adhoc command %s: %s" % (status, notes), "Info") + self.core.information("Adhoc command %s: %s" % (status, notes), + "Info") def adhoc_error(self, iq, adhoc_session): self.core.xmpp.plugin['xep_0050'].terminate_command(adhoc_session) error_message = self.core.get_error_message(iq) - self.core.information("An error occured while executing the command: %s" % (error_message), - 'Error') + self.core.information( + "An error occured while executing the command: %s" % + (error_message), 'Error') def _cancel_adhoc_command(self, form, session): self.core.xmpp.plugin['xep_0050'].cancel_command(session) @@ -1461,6 +1646,7 @@ class HandlerCore: self.core.xmpp.plugin['xep_0050'].terminate_command(session) self.core.close_tab() + def _composing_tab_state(tab, state): """ Set a tab state to or from the "composing" state @@ -1473,7 +1659,7 @@ def _composing_tab_state(tab, state): elif isinstance(tab, tabs.ConversationTab): values = ('true', 'direct', 'conversation') else: - return # should not happen + return # should not happen show = config.get('show_composing_tabs') show = show in values @@ -1486,4 +1672,3 @@ def _composing_tab_state(tab, state): tab.state = 'composing' elif tab.state == 'composing' and state != 'composing': tab.restore_state() - diff --git a/poezio/core/structs.py b/poezio/core/structs.py index 7d568f04..72c9628a 100644 --- a/poezio/core/structs.py +++ b/poezio/core/structs.py @@ -2,8 +2,10 @@ Module defining structures useful to the core class and related methods """ -__all__ = ['ERROR_AND_STATUS_CODES', 'DEPRECATED_ERRORS', 'POSSIBLE_SHOW', - 'Status', 'Command', 'Completion'] +__all__ = [ + 'ERROR_AND_STATUS_CODES', 'DEPRECATED_ERRORS', 'POSSIBLE_SHOW', 'Status', + 'Command', 'Completion' +] # http://xmpp.org/extensions/xep-0045.html#errorstatus ERROR_AND_STATUS_CODES = { @@ -15,7 +17,7 @@ ERROR_AND_STATUS_CODES = { '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 = { @@ -48,14 +50,18 @@ POSSIBLE_SHOW = { 'xa': 'xa' } + class Status: __slots__ = ('show', 'message') + def __init__(self, show, message): self.show = show self.message = message + class Command: __slots__ = ('func', 'desc', 'comp', 'short_desc', 'usage') + def __init__(self, func, desc, comp, short_desc, usage): self.func = func self.desc = desc @@ -63,11 +69,13 @@ class Command: self.short_desc = short_desc self.usage = usage + class Completion: """ A completion result essentially currying the input completion call. """ __slots__ = ['func', 'args', 'kwargs', 'comp_list'] + def __init__(self, func, comp_list, *args, **kwargs): self.func = func self.comp_list = comp_list diff --git a/poezio/daemon.py b/poezio/daemon.py index 6de0ec3a..c8225a07 100755 --- a/poezio/daemon.py +++ b/poezio/daemon.py @@ -5,7 +5,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ This file is a standalone program that reads commands on stdin and executes them (each line should be a command). @@ -29,6 +28,7 @@ from subprocess import DEVNULL log = logging.getLogger(__name__) + class Executor(threading.Thread): """ Just a class to execute commands in a thread. This way, the execution @@ -37,6 +37,7 @@ class Executor(threading.Thread): WARNING: Be careful to properly escape what is untrusted by using shlex.quote for example. """ + def __init__(self, command, remote=False): threading.Thread.__init__(self) self.command = command @@ -58,7 +59,10 @@ class Executor(threading.Thread): try: stdout = open(self.filename, self.redirection_mode) except OSError: - log.error('Could not open redirection file: %s', self.filename, exc_info=True) + log.error( + 'Could not open redirection file: %s', + self.filename, + exc_info=True) return try: subprocess.call(self.command, stdout=stdout, stderr=DEVNULL) @@ -69,6 +73,7 @@ class Executor(threading.Thread): else: log.error('Could not execute %s:', self.command, exc_info=True) + def main(): while True: line = sys.stdin.readline() @@ -78,5 +83,6 @@ def main(): e = Executor(command, remote=True) e.start() + if __name__ == '__main__': main() diff --git a/poezio/decorators.py b/poezio/decorators.py index e824a8f1..808c3ce6 100644 --- a/poezio/decorators.py +++ b/poezio/decorators.py @@ -4,6 +4,7 @@ Module containing various decorators from poezio import common + class RefreshWrapper(object): def __init__(self): self.core = None @@ -13,49 +14,60 @@ class RefreshWrapper(object): Decorator to refresh the UI if the wrapped function returns True """ + def wrap(*args, **kwargs): ret = func(*args, **kwargs) if self.core and ret: self.core.refresh_window() return ret + return wrap def always(self, func): """ Decorator that refreshs the UI no matter what after the function """ + def wrap(*args, **kwargs): ret = func(*args, **kwargs) if self.core: self.core.refresh_window() return ret + return wrap def update(self, func): """ Decorator that only updates the screen """ + def wrap(*args, **kwargs): ret = func(*args, **kwargs) if self.core: self.core.doupdate() return ret + 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 @@ -63,14 +75,17 @@ class CommandArgParser(object): """ Call the function without any argument """ + def wrap(self, args=None, *a, **kw): return func(self, *a, **kw) + return wrap @staticmethod - def quoted(mandatory, optional=0, defaults=None, + def quoted(mandatory, + optional=0, + defaults=None, ignore_trailing_arguments=False): - """The function receives a list with a number of arguments that is between the numbers `mandatory` and `optional`. @@ -115,6 +130,7 @@ class CommandArgParser(object): """ if defaults is None: defaults = [] + def first(func): def second(self, args, *a, **kw): default_args = defaults @@ -138,7 +154,10 @@ class CommandArgParser(object): 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/poezio/events.py b/poezio/events.py index 97d77626..15a308b4 100644 --- a/poezio/events.py +++ b/poezio/events.py @@ -3,13 +3,13 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Defines the EventHandler class. The list of available events is here: http://poezio.eu/doc/en/plugins.html#_poezio_events """ + class EventHandler(object): """ A class keeping a list of possible events that are triggered @@ -17,6 +17,7 @@ class EventHandler(object): associated with an event name, and whenever that event is triggered, the callback is called. """ + def __init__(self): self.events = { 'highlight': [], @@ -43,7 +44,7 @@ class EventHandler(object): 'send_normal_presence': [], 'ignored_private': [], 'tab_change': [], - } + } def add_event_handler(self, name, callback, position=0): """ diff --git a/poezio/fifo.py b/poezio/fifo.py index 863ef228..f2615847 100644 --- a/poezio/fifo.py +++ b/poezio/fifo.py @@ -11,6 +11,7 @@ log = logging.getLogger(__name__) import os import threading + class OpenTrick(threading.Thread): """ A threaded trick to make the open for writing succeed. @@ -22,6 +23,7 @@ class OpenTrick(threading.Thread): (we never read anything from it, obviously) """ + def __init__(self, path): threading.Thread.__init__(self) self.path = path @@ -37,6 +39,7 @@ class Fifo(object): Mode is either 'r' or 'w', just like the mode for the open() function. """ + def __init__(self, path, mode): self.trick = None if not os.path.exists(path): @@ -67,5 +70,5 @@ class Fifo(object): if self.trick: self.trick.fd.close() except: - log.error('Unable to close descriptors for the fifo', - exc_info=True) + log.error( + 'Unable to close descriptors for the fifo', exc_info=True) diff --git a/poezio/fixes.py b/poezio/fixes.py index 1eadac12..77688bf8 100644 --- a/poezio/fixes.py +++ b/poezio/fixes.py @@ -12,6 +12,7 @@ import logging log = logging.getLogger(__name__) + def has_identity(xmpp, jid, identity, on_true=None, on_false=None): def _cb(iq): ident = lambda x: x[0] @@ -20,8 +21,10 @@ def has_identity(xmpp, jid, identity, on_true=None, on_false=None): on_true() if not res and on_false is not None: on_false() + xmpp.plugin['xep_0030'].get_info(jid=jid, callback=_cb) + def get_version(xmpp, jid, callback=None, **kwargs): def handle_result(res): if res and res['type'] != 'error': @@ -31,6 +34,7 @@ def get_version(xmpp, jid, callback=None, **kwargs): if callback: callback(ret) return ret + iq = xmpp.make_iq_get(ito=jid) iq['query'] = 'jabber:iq:version' result = iq.send(callback=handle_result if callback else None) @@ -42,7 +46,8 @@ def get_room_form(xmpp, room, callback): def _cb(result): if result["type"] == "error": return callback(None) - xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') + xform = result.xml.find( + '{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') if xform is None: return callback(None) form = xmpp.plugin['xep_0004'].build_form(xform) @@ -53,6 +58,7 @@ def get_room_form(xmpp, room, callback): iq.append(query) iq.send(callback=_cb) + def _filter_add_receipt_request(self, stanza): """ Auto add receipt requests to outgoing messages, if: @@ -94,4 +100,3 @@ def _filter_add_receipt_request(self, stanza): stanza['request_receipt'] = True return stanza - diff --git a/poezio/keyboard.py b/poezio/keyboard.py index ccf9e752..fe0dde08 100755 --- a/poezio/keyboard.py +++ b/poezio/keyboard.py @@ -5,7 +5,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Functions to interact with the keyboard Mainly, read keys entered and return a string (most @@ -27,6 +26,7 @@ log = logging.getLogger(__name__) # processing of keys) continuation_keys_callback = None + def get_next_byte(s): """ Read the next byte of the utf-8 char @@ -40,7 +40,8 @@ def get_next_byte(s): return (None, None) if len(c) >= 4: return (None, c) - return (ord(c), c.encode('latin-1')) # returns a number and a bytes object + return (ord(c), c.encode('latin-1')) # returns a number and a bytes object + def get_char_list(s): ret_list = [] @@ -50,7 +51,7 @@ def get_char_list(s): except curses.error: # No input, this means a timeout occurs. return ret_list - except ValueError: # invalid input + except ValueError: # invalid input log.debug('Invalid character entered.') return ret_list # Set to non-blocking. We try to read more bytes. If there are no @@ -68,10 +69,11 @@ def get_char_list(s): 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() + part += s.get_wch() + s.get_wch() + s.get_wch( + ) + s.get_wch() except curses.error: pass - except ValueError: # invalid input + except ValueError: # invalid input log.debug('Invalid character entered.') else: key = 'M-%s' % part @@ -93,6 +95,7 @@ def get_char_list(s): key = '^M' ret_list.append(key) + class Keyboard(object): def __init__(self): self.escape = False @@ -132,6 +135,7 @@ class Keyboard(object): self.escape = False return ret_list + if __name__ == '__main__': import sys keyboard = Keyboard() diff --git a/poezio/logger.py b/poezio/logger.py index e4ce4c5a..7302df2a 100644 --- a/poezio/logger.py +++ b/poezio/logger.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ The logger module that handles logging of the poezio conversations and roster changes @@ -34,23 +33,30 @@ INFO_LOG_RE = re.compile(r'^MI (\d{4})(\d{2})(\d{2})T' r'(\d{2}):(\d{2}):(\d{2})Z ' r'(\d+) (.*)$') + class LogItem: - def __init__(self, year, month, day, hour, minute, second, nb_lines, message): - self.time = datetime(int(year), int(month), int(day), int(hour), - int(minute), int(second)) + def __init__(self, year, month, day, hour, minute, second, nb_lines, + message): + self.time = datetime( + int(year), + int(month), int(day), int(hour), int(minute), int(second)) self.nb_lines = int(nb_lines) self.text = message + class LogInfo(LogItem): def __init__(self, *args): LogItem.__init__(self, *args) + class LogMessage(LogItem): - def __init__(self, year, month, day, hour, minute, seconds, nb_lines, nick, message): + def __init__(self, year, month, day, hour, minute, seconds, nb_lines, nick, + message): LogItem.__init__(self, year, month, day, hour, minute, seconds, nb_lines, message) self.nick = nick + def parse_log_line(msg): match = re.match(MESSAGE_LOG_RE, msg) if match: @@ -61,11 +67,13 @@ def parse_log_line(msg): log.debug('Error while parsing "%s"', msg) return None + class Logger(object): """ Appends things to files. Error/information/warning logs and also log the conversations to logfiles """ + def __init__(self): self._roster_logfile = None # a dict of 'groupchatname': file-object (opened) @@ -76,7 +84,7 @@ class Logger(object): if opened_file: try: opened_file.close() - except: # Can't close? too bad + except: # Can't close? too bad pass def close(self, jid): @@ -106,7 +114,7 @@ class Logger(object): try: makedirs(log_dir) except OSError as e: - if e.errno != 17: # file exists + if e.errno != 17: # file exists log.error('Unable to create the log dir', exc_info=True) except: log.error('Unable to create the log dir', exc_info=True) @@ -118,9 +126,10 @@ class Logger(object): self._fds[room] = fd return fd except IOError: - log.error('Unable to open the log file (%s)', - os.path.join(log_dir, room), - exc_info=True) + log.error( + 'Unable to open the log file (%s)', + os.path.join(log_dir, room), + exc_info=True) def get_logs(self, jid, nb=10): """ @@ -143,14 +152,16 @@ class Logger(object): try: fd = open(os.path.join(log_dir, jid), 'rb') except FileNotFoundError: - log.info('Non-existing log file (%s)', - os.path.join(log_dir, jid), - exc_info=True) + 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) + log.error( + 'Unable to open the log file (%s)', + os.path.join(log_dir, jid), + exc_info=True) return if not fd: return @@ -161,22 +172,23 @@ class Logger(object): with fd: try: m = mmap.mmap(fd.fileno(), 0, prot=mmap.PROT_READ) - except Exception: # file probably empty - log.error('Unable to mmap the log file for (%s)', - os.path.join(log_dir, jid), - exc_info=True) + except Exception: # file probably empty + log.error( + 'Unable to mmap the log file for (%s)', + os.path.join(log_dir, jid), + exc_info=True) return - pos = m.rfind(b"\nM") # start of messages begin with MI or MR, - # after a \n + pos = m.rfind(b"\nM") # start of messages begin with MI or MR, + # after a \n # number of message found so far count = 0 - while pos != -1 and count < nb-1: + while pos != -1 and count < nb - 1: count += 1 pos = m.rfind(b"\nM", 0, pos) - if pos == -1: # If we don't have enough lines in the file - pos = 1 # 1, because we do -1 just on the next line - # to get 0 (start of the file) - lines = m[pos-1:].decode(errors='replace').splitlines() + if pos == -1: # If we don't have enough lines in the file + pos = 1 # 1, because we do -1 just on the next line + # to get 0 (start of the file) + lines = m[pos - 1:].decode(errors='replace').splitlines() messages = [] color = '\x19%s}' % dump_tuple(get_theme().COLOR_LOG_MSG) @@ -184,7 +196,7 @@ class Logger(object): # now convert that data into actual Message objects idx = 0 while idx < len(lines): - if lines[idx].startswith(' '): # should not happen ; skip + if lines[idx].startswith(' '): # should not happen ; skip idx += 1 log.debug('fail?') continue @@ -193,9 +205,11 @@ class Logger(object): if not isinstance(log_item, LogItem): log.debug('wrong log format? %s', log_item) continue - message = {'lines': [], - 'history': True, - 'time': common.get_local_time(log_item.time)} + message = { + 'lines': [], + 'history': True, + 'time': common.get_local_time(log_item.time) + } size = log_item.nb_lines if isinstance(log_item, LogInfo): message['lines'].append(color + log_item.text) @@ -237,7 +251,8 @@ class Logger(object): if date is None: str_time = common.get_utc_time().strftime('%Y%m%dT%H:%M:%SZ') else: - str_time = common.get_utc_time(date).strftime('%Y%m%dT%H:%M:%SZ') + str_time = common.get_utc_time(date).strftime( + '%Y%m%dT%H:%M:%SZ') if typ == 1: prefix = 'MR' else: @@ -248,23 +263,27 @@ class Logger(object): if nick: nick = '<' + nick + '>' - fd.write('%s %s %s %s %s\n' % (prefix, str_time, nb_lines, nick, first_line)) + fd.write('%s %s %s %s %s\n' % (prefix, str_time, nb_lines, + nick, first_line)) else: - fd.write('%s %s %s %s\n' % (prefix, str_time, nb_lines, first_line)) + fd.write('%s %s %s %s\n' % (prefix, str_time, nb_lines, + first_line)) for line in lines: fd.write(' %s\n' % line) except: - log.error('Unable to write in the log file (%s)', - os.path.join(log_dir, jid), - exc_info=True) + log.error( + 'Unable to write in the log file (%s)', + os.path.join(log_dir, jid), + exc_info=True) return False else: try: - fd.flush() # TODO do something better here? + fd.flush() # TODO do something better here? except OSError: - log.error('Unable to flush the log file (%s)', - os.path.join(log_dir, jid), - exc_info=True) + log.error( + 'Unable to flush the log file (%s)', + os.path.join(log_dir, jid), + exc_info=True) return False return True @@ -277,11 +296,13 @@ class Logger(object): self._check_and_create_log_dir('', open_fd=False) if not self._roster_logfile: try: - self._roster_logfile = open(os.path.join(log_dir, 'roster.log'), 'a') + self._roster_logfile = open( + os.path.join(log_dir, 'roster.log'), 'a') except IOError: - log.error('Unable to create the log file (%s)', - os.path.join(log_dir, 'roster.log'), - exc_info=True) + log.error( + 'Unable to create the log file (%s)', + os.path.join(log_dir, 'roster.log'), + exc_info=True) return False try: str_time = common.get_utc_time().strftime('%Y%m%dT%H:%M:%SZ') @@ -289,20 +310,24 @@ class Logger(object): lines = message.split('\n') first_line = lines.pop(0) nb_lines = str(len(lines)).zfill(3) - self._roster_logfile.write('MI %s %s %s %s\n' % (str_time, nb_lines, jid, first_line)) + self._roster_logfile.write('MI %s %s %s %s\n' % + (str_time, nb_lines, jid, first_line)) for line in lines: self._roster_logfile.write(' %s\n' % line) self._roster_logfile.flush() except: - log.error('Unable to write in the log file (%s)', - os.path.join(log_dir, 'roster.log'), - exc_info=True) + log.error( + 'Unable to write in the log file (%s)', + os.path.join(log_dir, 'roster.log'), + exc_info=True) return False return True + def create_logger(): "Create the global logger object" global logger logger = Logger() + logger = None diff --git a/poezio/multiuserchat.py b/poezio/multiuserchat.py index 23e0f7eb..64b84069 100644 --- a/poezio/multiuserchat.py +++ b/poezio/multiuserchat.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Implementation of the XEP-0045: Multi-User Chat. Add some facilities that are not available on the XEP_0045 @@ -41,15 +40,17 @@ def destroy_room(xmpp, room, reason='', altroom=''): destroy.append(xreason) query.append(destroy) 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') + iq.send(callback=callback) return True + def send_private_message(xmpp, jid, line): """ Send a private message @@ -57,6 +58,7 @@ def send_private_message(xmpp, jid, line): jid = safeJID(jid) xmpp.send_message(mto=jid, mbody=line, mtype='chat') + def send_groupchat_message(xmpp, jid, line): """ Send a message to the groupchat @@ -64,18 +66,20 @@ def send_groupchat_message(xmpp, jid, line): jid = safeJID(jid) xmpp.send_message(mto=jid, mbody=line, mtype='groupchat') + def change_show(xmpp, jid, own_nick, show, status): """ Change our 'Show' """ jid = safeJID(jid) pres = xmpp.make_presence(pto='%s/%s' % (jid, own_nick)) - if show: # if show is None, don't put a <show /> tag. It means "available" + if show: # if show is None, don't put a <show /> tag. It means "available" pres['type'] = show if status: pres['status'] = status pres.send() + def change_subject(xmpp, jid, subject): """ Change the room subject @@ -86,18 +90,28 @@ def change_subject(xmpp, jid, subject): msg['subject'] = subject msg.send() + def change_nick(core, jid, nick, status=None, show=None): """ Change our own nick in a room """ xmpp = core.xmpp - presence = xmpp.make_presence(pshow=show, pstatus=status, pto=safeJID('%s/%s' % (jid, nick))) + presence = xmpp.make_presence( + pshow=show, pstatus=status, pto=safeJID('%s/%s' % (jid, nick))) core.events.trigger('changing_nick', presence) presence.send() -def join_groupchat(core, jid, nick, passwd='', status=None, show=None, seconds=None): + +def join_groupchat(core, + jid, + nick, + passwd='', + status=None, + show=None, + seconds=None): xmpp = core.xmpp - stanza = xmpp.make_presence(pto='%s/%s' % (jid, nick), pstatus=status, pshow=show) + stanza = xmpp.make_presence( + pto='%s/%s' % (jid, nick), pstatus=status, pshow=show) x = ET.Element('{http://jabber.org/protocol/muc}x') if passwd: passelement = ET.Element('password') @@ -114,6 +128,7 @@ def join_groupchat(core, jid, nick, passwd='', status=None, show=None, seconds=N xmpp.plugin['xep_0045'].rooms[jid] = {} xmpp.plugin['xep_0045'].our_nicks[jid] = to.resource + def leave_groupchat(xmpp, jid, own_nick, msg): """ Leave the groupchat @@ -122,8 +137,11 @@ def leave_groupchat(xmpp, jid, own_nick, msg): try: xmpp.plugin['xep_0045'].leave_muc(jid, own_nick, msg) except KeyError: - log.debug("muc.leave_groupchat: could not leave the room %s", - jid, exc_info=True) + log.debug( + "muc.leave_groupchat: could not leave the room %s", + jid, + exc_info=True) + def set_user_role(xmpp, jid, nick, reason, role, callback=None): """ @@ -133,7 +151,7 @@ def set_user_role(xmpp, jid, nick, reason, role, callback=None): jid = safeJID(jid) iq = xmpp.make_iq_set() query = ET.Element('{%s}query' % NS_MUC_ADMIN) - item = ET.Element('{%s}item' % NS_MUC_ADMIN, {'nick':nick, 'role':role}) + item = ET.Element('{%s}item' % NS_MUC_ADMIN, {'nick': nick, 'role': role}) if reason: reason_el = ET.Element('{%s}reason' % NS_MUC_ADMIN) reason_el.text = reason @@ -148,19 +166,33 @@ def set_user_role(xmpp, jid, nick, reason, role, callback=None): except (IqError, IqTimeout) as e: return e.iq -def set_user_affiliation(xmpp, muc_jid, affiliation, nick=None, jid=None, reason=None, callback=None): + +def set_user_affiliation(xmpp, + muc_jid, + affiliation, + nick=None, + jid=None, + reason=None, + callback=None): """ (try to) Set the affiliation of a MUC user """ muc_jid = safeJID(muc_jid) query = ET.Element('{http://jabber.org/protocol/muc#admin}query') if nick: - item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick}) + item = ET.Element('{http://jabber.org/protocol/muc#admin}item', { + 'affiliation': affiliation, + 'nick': nick + }) else: - item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':str(jid)}) + item = ET.Element('{http://jabber.org/protocol/muc#admin}item', { + 'affiliation': affiliation, + 'jid': str(jid) + }) if reason: - reason_item = ET.Element('{http://jabber.org/protocol/muc#admin}reason') + reason_item = ET.Element( + '{http://jabber.org/protocol/muc#admin}reason') reason_item.text = reason item.append(reason_item) @@ -170,11 +202,13 @@ def set_user_affiliation(xmpp, muc_jid, affiliation, nick=None, jid=None, reason if callback: return iq.send(callback=callback) try: - return xmpp.plugin['xep_0045'].set_affiliation(str(muc_jid), str(jid) if jid else None, nick, affiliation) + return xmpp.plugin['xep_0045'].set_affiliation( + str(muc_jid), str(jid) if jid else None, nick, affiliation) except: log.debug('Error setting the affiliation: %s', exc_info=True) return False + def cancel_config(xmpp, room): query = ET.Element('{http://jabber.org/protocol/muc#owner}query') x = ET.Element('{jabber:x:data}x', type='cancel') @@ -183,6 +217,7 @@ def cancel_config(xmpp, room): iq['to'] = room iq.send() + def configure_room(xmpp, room, form): if form is None: return @@ -193,4 +228,3 @@ def configure_room(xmpp, room, form): query.append(form.xml) iq.append(query) iq.send() - diff --git a/poezio/pep.py b/poezio/pep.py index 0f7a1ced..a211b09b 100644 --- a/poezio/pep.py +++ b/poezio/pep.py @@ -4,218 +4,202 @@ extracted directly from the XEP """ 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' } - - - 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', - }, - 'drinking': { - 'category': 'Drinking', - - 'having_a_beer': 'Having a beer', - 'having_coffee': 'Having coffee', - 'having_tea': 'Having tea', - 'other': 'Other', - }, - 'eating': { - 'category':'Eating', - - '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', - }, - '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', - }, - 'having_appointment': { - 'category': 'Having appointment', - - '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', - }, - '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', - }, - 'talking': { - 'category': 'Talking', - - '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', - }, - 'undefined': { - 'category': 'Undefined', - - 'other': 'Other', - }, - 'working': { - 'category': 'Working', - - 'coding': 'Coding', - 'in_a_meeting': 'In a meeting', - 'writing': 'Writing', - 'studying': 'Studying', - 'other': 'Other', - } - } - + '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', + }, + 'drinking': { + 'category': 'Drinking', + 'having_a_beer': 'Having a beer', + 'having_coffee': 'Having coffee', + 'having_tea': 'Having tea', + 'other': 'Other', + }, + 'eating': { + 'category': 'Eating', + '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', + }, + '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', + }, + 'having_appointment': { + 'category': 'Having appointment', + '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', + }, + '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', + }, + 'talking': { + 'category': 'Talking', + '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', + }, + 'undefined': { + 'category': 'Undefined', + 'other': 'Other', + }, + 'working': { + 'category': 'Working', + 'coding': 'Coding', + 'in_a_meeting': 'In a meeting', + 'writing': 'Writing', + 'studying': 'Studying', + 'other': 'Other', + } +} diff --git a/poezio/plugin.py b/poezio/plugin.py index c8bffc95..359f1148 100644 --- a/poezio/plugin.py +++ b/poezio/plugin.py @@ -13,12 +13,14 @@ import traceback import logging log = logging.getLogger(__name__) + class PluginConfig(config.Config): """ Plugin configuration object. They are accessible inside the plugin with self.config and behave like the core Config object. """ + def __init__(self, filename, module_name, default=None): config.Config.__init__(self, filename, default=default) self.module_name = module_name @@ -81,8 +83,10 @@ class SafetyMetaclass(type): raise elif SafetyMetaclass.core: log.error('Error in a plugin', exc_info=True) - SafetyMetaclass.core.information(traceback.format_exc(), 'Error') + SafetyMetaclass.core.information(traceback.format_exc(), + 'Error') return None + return helper def __new__(meta, name, bases, class_dict): @@ -92,10 +96,12 @@ class SafetyMetaclass(type): class_dict[k] = SafetyMetaclass.safe_func(v) return type.__new__(meta, name, bases, class_dict) + class PluginWrap(object): """ A wrapper to implicitly pass the module name to PluginAPI """ + def __init__(self, api, module): self.api = api self.module = module @@ -105,6 +111,7 @@ class PluginWrap(object): module = object.__getattribute__(self, 'module') return partial(getattr(api, name), module) + class PluginAPI(object): """ The public API exposed to the plugins. @@ -368,6 +375,7 @@ class PluginAPI(object): """ self.core.xmpp.del_event_handler(event_name, handler) + class BasePlugin(object, metaclass=SafetyMetaclass): """ Class that all plugins derive from. @@ -379,10 +387,10 @@ class BasePlugin(object, metaclass=SafetyMetaclass): 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') + conf = os.path.join(plugins_conf_dir, self.__module__ + '.cfg') try: - self.config = PluginConfig(conf, self.__module__, - default=self.default_config) + 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__) @@ -419,13 +427,24 @@ class BasePlugin(object, metaclass=SafetyMetaclass): def unload(self): self.cleanup() - def add_command(self, name, handler, help, completion=None, short='', usage=''): + def add_command(self, + name, + handler, + help, + completion=None, + short='', + usage=''): """ Add a global command. You cannot overwrite the existing commands. """ - return self.api.add_command(name, handler, help, - completion=completion, short=short, usage=usage) + return self.api.add_command( + name, + handler, + help, + completion=completion, + short=short, + usage=usage) def del_command(self, name): """ @@ -458,12 +477,25 @@ class BasePlugin(object, metaclass=SafetyMetaclass): """ return self.api.del_tab_key(tab_type, key) - def add_tab_command(self, tab_type, name, handler, help, completion=None, short='', usage=''): + def add_tab_command(self, + tab_type, + name, + handler, + help, + completion=None, + short='', + usage=''): """ Add a command only for a type of tab. """ - return self.api.add_tab_command(tab_type, name, handler, help, - completion=completion, short=short, usage=usage) + return self.api.add_tab_command( + tab_type, + name, + handler, + help, + completion=completion, + short=short, + usage=usage) def del_tab_command(self, tab_type, name): """ diff --git a/poezio/plugin_manager.py b/poezio/plugin_manager.py index 8499cf51..cdc5f0d0 100644 --- a/poezio/plugin_manager.py +++ b/poezio/plugin_manager.py @@ -16,12 +16,14 @@ from poezio.config import config log = logging.getLogger(__name__) + class PluginManager(object): """ Plugin Manager Contains all the references to the plugins And keeps track of everything the plugin has done through the API. """ + def __init__(self, core): self.core = core # module name -> module object @@ -65,7 +67,8 @@ class PluginManager(object): module = None loader = self.finder.find_module(name, self.load_path) if not loader: - self.core.information('Could not find plugin: %s' % name, 'Error') + self.core.information('Could not find plugin: %s' % name, + 'Error') return module = loader.load_module() except Exception as e: @@ -90,8 +93,7 @@ class PluginManager(object): 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), - 'Error') + (name, e), 'Error') self.unload(name, notify=False) else: if notify: @@ -106,8 +108,8 @@ class PluginManager(object): del self.core.key_func[key] for tab in list(self.tab_commands[name].keys()): for command in self.tab_commands[name][tab][:]: - self.del_tab_command(name, getattr(tabs, tab), - command[0]) + self.del_tab_command(name, + getattr(tabs, tab), command[0]) del self.tab_commands[name][tab] for tab in list(self.tab_keys[name].keys()): for key in self.tab_keys[name][tab][:]: @@ -128,16 +130,21 @@ class PluginManager(object): 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), - 'Error') + (name, e), 'Error') - def add_command(self, module_name, name, handler, help, - completion=None, short='', usage=''): + def add_command(self, + module_name, + name, + handler, + help, + completion=None, + short='', + usage=''): """ 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] = Command(handler, help, completion, short, usage) @@ -152,8 +159,15 @@ class PluginManager(object): if name in self.core.commands: del self.core.commands[name] - def add_tab_command(self, module_name, tab_type, name, handler, help, - completion=None, short='', usage=''): + def add_tab_command(self, + module_name, + tab_type, + name, + handler, + help, + completion=None, + short='', + usage=''): """ Add a command only for a type of Tab. """ @@ -164,8 +178,8 @@ class PluginManager(object): if t not in commands: commands[t] = [] commands[t].append((name, handler, help, completion)) - tab_type.plugin_commands[name] = Command(handler, help, - completion, short, usage) + tab_type.plugin_commands[name] = Command(handler, help, completion, + short, usage) for tab in self.core.tabs: if isinstance(tab, tab_type): tab.update_commands() @@ -224,7 +238,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 @@ -273,20 +287,31 @@ class PluginManager(object): names |= add except OSError: pass - plugins_files = [name[:-3] for name in names if name.endswith('.py') - and name != '__init__.py' and not name.startswith('.')] + plugins_files = [ + name[:-3] for name in names + if name.endswith('.py') and name != '__init__.py' + and not name.startswith('.') + ] plugins_files.sort() position = the_input.get_argument_position(quoted=False) - return Completion(the_input.new_completion, plugins_files, position, '', - quotify=False) + return Completion( + the_input.new_completion, + plugins_files, + position, + '', + quotify=False) def completion_unload(self, the_input): """ completion function that completes the name of loaded plugins """ position = the_input.get_argument_position(quoted=False) - return Completion(the_input.new_completion, sorted(self.plugins.keys()), position, - '', quotify=False) + return Completion( + the_input.new_completion, + sorted(self.plugins.keys()), + position, + '', + quotify=False) def on_plugins_dir_change(self, new_value): self.plugins_dir = new_value @@ -319,8 +344,10 @@ class PluginManager(object): try: os.makedirs(self.plugins_conf_dir) except OSError: - log.error('Unable to create the plugin conf dir: %s', - self.plugins_conf_dir, exc_info=True) + log.error( + 'Unable to create the plugin conf dir: %s', + self.plugins_conf_dir, + exc_info=True) return False return True @@ -346,8 +373,10 @@ class PluginManager(object): try: os.makedirs(self.plugins_dir, exist_ok=True) except OSError: - log.error('Unable to create the plugins dir: %s', - self.plugins_dir, exc_info=True) + log.error( + 'Unable to create the plugins dir: %s', + self.plugins_dir, + exc_info=True) return False return True @@ -358,8 +387,8 @@ class PluginManager(object): self.load_path = [] - default_plugin_path = path.join(path.dirname(path.dirname(__file__)), - 'plugins') + default_plugin_path = path.join( + path.dirname(path.dirname(__file__)), 'plugins') if os.access(default_plugin_path, os.R_OK | os.X_OK): self.load_path.insert(0, default_plugin_path) @@ -374,4 +403,3 @@ class PluginManager(object): else: if poezio_plugins.__path__: self.load_path.append(list(poezio_plugins.__path__)[0]) - diff --git a/poezio/poezio.py b/poezio/poezio.py index f841b672..51e10d1b 100644 --- a/poezio/poezio.py +++ b/poezio/poezio.py @@ -4,8 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - - """ Starting point of poezio. Launches both the Connection and Gui """ @@ -16,6 +14,7 @@ import signal sys.path.append(os.path.dirname(os.path.abspath(__file__))) + def test_curses(): """ Check if the system ncurses linked with python has unicode capabilities. @@ -74,9 +73,9 @@ def main(): from poezio import core - signal.signal(signal.SIGINT, signal.SIG_IGN) # ignore ctrl-c + signal.signal(signal.SIGINT, signal.SIG_IGN) # ignore ctrl-c cocore = core.Core() - signal.signal(signal.SIGUSR1, cocore.sigusr_handler) # reload the config + signal.signal(signal.SIGUSR1, cocore.sigusr_handler) # reload the config signal.signal(signal.SIGHUP, cocore.exit_from_signal) signal.signal(signal.SIGTERM, cocore.exit_from_signal) if options.debug: @@ -84,6 +83,7 @@ def main(): cocore.start() from slixmpp.exceptions import IqError, IqTimeout + def swallow_iqerrors(loop, context): """Do not log unhandled iq errors and timeouts""" if not isinstance(context['exception'], (IqError, IqTimeout)): diff --git a/poezio/poezio_shlex.py b/poezio/poezio_shlex.py index 7072d10d..ba8d6d56 100644 --- a/poezio/poezio_shlex.py +++ b/poezio/poezio_shlex.py @@ -20,10 +20,12 @@ from io import StringIO __all__ = ["shlex", "split", "quote"] + class shlex(object): """ A custom version of the shlex in the stdlib to yield more information """ + def __init__(self, instream=None, infile=None, posix=True): if isinstance(instream, str): instream = StringIO(instream) @@ -73,9 +75,9 @@ class shlex(object): self.lineno = 1 if self.debug: if newfile is not None: - print('shlex: pushing to file %s' % (self.infile,)) + print('shlex: pushing to file %s' % (self.infile, )) else: - print('shlex: pushing to stream %s' % (self.instream,)) + print('shlex: pushing to stream %s' % (self.instream, )) def pop_source(self): "Pop the input source stack." @@ -111,7 +113,7 @@ class shlex(object): print("shlex: in state", repr(self.state), \ "I see character:", repr(nextchar)) if self.state == '\0': - self.token = '' # past end of file + self.token = '' # past end of file token_end = self.instream.tell() break elif self.state == ' ': @@ -124,7 +126,7 @@ class shlex(object): print("shlex: I see whitespace in whitespace state") if self.token or (self.posix and quoted): token_end = self.instream.tell() - 1 - break # emit current token + break # emit current token else: continue elif nextchar in self.wordchars: @@ -143,12 +145,12 @@ class shlex(object): self.token = nextchar if self.token or (self.posix and quoted): token_end = self.instream.tell() - 1 - break # emit current token + break # emit current token else: continue elif self.state == self.quotes: quoted = True - if not nextchar: # end of file + if not nextchar: # end of file if self.debug >= 2: print("shlex: I see EOF in quotes state") # XXX what error should be raised here? @@ -169,7 +171,7 @@ class shlex(object): else: self.token = self.token + nextchar elif self.state == self.escape: - if not nextchar: # end of file + if not nextchar: # end of file if self.debug >= 2: print("shlex: I see EOF in escape state") # XXX what error should be raised here? @@ -182,7 +184,7 @@ class shlex(object): self.state = escapedstate elif self.state == 'a': if not nextchar: - self.state = '\0' # end of file + self.state = '\0' # end of file token_end = self.instream.tell() break elif nextchar in self.whitespace: @@ -191,7 +193,7 @@ class shlex(object): self.state = ' ' if self.token or (self.posix and quoted): token_end = self.instream.tell() - 1 - break # emit current token + break # emit current token else: continue elif nextchar in self.wordchars or nextchar == self.quotes \ @@ -204,7 +206,7 @@ class shlex(object): self.state = ' ' if self.token: token_end = self.instream.tell() - break # emit current token + break # emit current token else: continue result = self.token @@ -244,6 +246,7 @@ class shlex(object): raise StopIteration return token + def split(s, comments=False, posix=True): lex = shlex(s, posix=posix) lex.whitespace_split = True @@ -254,6 +257,7 @@ def split(s, comments=False, posix=True): _find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search + def quote(s): """Return a shell-escaped version of the string *s*.""" if not s: diff --git a/poezio/poopt.py b/poezio/poopt.py index 51474222..b9243686 100644 --- a/poezio/poopt.py +++ b/poezio/poopt.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - '''This is a template module just for instruction. And poopt.''' from typing import List, Tuple @@ -24,6 +23,7 @@ libc = ffi.dlopen(None) # ctypedef Py_UCS4 wchar_t # int wcwidth(wchar_t c) + # Just checking if the return value is -1. In some (all?) implementations, # wcwidth("😆") returns -1 while it should return 1. In these cases, we # return 1 instead because this is by far the most probable real value. @@ -40,6 +40,7 @@ def xwcwidth(c: str) -> int: return 1 return res + # cut_text: takes a string and returns a tuple of int. # # Each two int tuple is a line, represented by the ending position it @@ -141,9 +142,10 @@ def cut_text(string: str, width: int) -> List[Tuple[int, int]]: # char's columns to the line's columns columns += cols # We are at the end of the string, append the last line, not finished - retlist.append((start_pos, spos+1)) + retlist.append((start_pos, spos + 1)) return retlist + # wcswidth: An emulation of the POSIX wcswidth(3) function using xwcwidth. def wcswidth(string: str) -> int: '''wcswidth(s) @@ -155,6 +157,7 @@ def wcswidth(string: str) -> int: columns += xwcwidth(wc) return columns + # cut_by_columns: takes a python string and a number of columns, returns a # python string truncated to take at most that many columns # For example cut_by_columns(n, "エメルカ") will return: diff --git a/poezio/roster.py b/poezio/roster.py index 96dfd396..9516d27d 100644 --- a/poezio/roster.py +++ b/poezio/roster.py @@ -4,8 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - - """ Defines the Roster and RosterGroup classes """ @@ -21,6 +19,7 @@ from datetime import datetime from poezio.common import safeJID from slixmpp.exceptions import IqError, IqTimeout + class Roster(object): """ The proxy class to get the roster from slixmpp. @@ -28,6 +27,7 @@ class Roster(object): """ DEFAULT_FILTER = (lambda x, y: None, None) + def __init__(self): """ node: the RosterSingle from slixmpp @@ -37,8 +37,8 @@ class Roster(object): # A tuple(function, *args) function to filter contacts # on search, for example self.contact_filter = self.DEFAULT_FILTER - self.folded_groups = set(config.get('folded_roster_groups', - section='var').split(':')) + self.folded_groups = set( + config.get('folded_roster_groups', section='var').split(':')) self.groups = {} self.contacts = {} self.length = 0 @@ -121,9 +121,8 @@ class Roster(object): def get_groups(self, sort=''): """Return a list of the RosterGroups""" group_list = sorted( - (group for group in self.groups.values() if group), - key=lambda x: x.name.lower() if x.name else '' - ) + (group for group in self.groups.values() if group), + key=lambda x: x.name.lower() if x.name else '') for sorting in sort.split(':'): if sorting == 'reverse': @@ -137,7 +136,8 @@ class Roster(object): """Return a group or create it if not present""" if name in self.groups: return self.groups[name] - self.groups[name] = RosterGroup(name, folded=name in self.folded_groups) + self.groups[name] = RosterGroup( + name, folded=name in self.folded_groups) def add(self, jid): """Subscribe to a jid""" @@ -216,7 +216,8 @@ class Roster(object): for group in contact.groups: if group not in self.groups: - self.groups[group] = RosterGroup(group, folded=group in self.folded_groups) + self.groups[group] = RosterGroup( + group, folded=group in self.folded_groups) self.groups[group].add(contact) def __len__(self): @@ -230,10 +231,10 @@ class Roster(object): def __repr__(self): ret = '== Roster:\nContacts:\n' for contact in self.contacts.values(): - ret += '%s\n' % (contact,) + ret += '%s\n' % (contact, ) ret += 'Groups\n' for group in self.groups: - ret += '%s\n' % (group,) + ret += '%s\n' % (group, ) return ret + '\n' def export(self, path): @@ -242,7 +243,10 @@ class Roster(object): return False try: f = open(path, 'w+', encoding='utf-8') - f.writelines([str(i) + "\n" for i in self.contacts if self[i] and (self[i].subscription == "both" or self[i].ask)]) + f.writelines([ + str(i) + "\n" for i in self.contacts + if self[i] and (self[i].subscription == "both" or self[i].ask) + ]) f.close() return True except OSError: @@ -263,12 +267,13 @@ class RosterGroup(object): It can be Friends/Family etc, but also can be Online/Offline or whatever """ + def __init__(self, name, contacts=None, folded=False): if not contacts: contacts = [] self.contacts = set(contacts) self.name = name if name is not None else '' - self.folded = folded # if the group content is to be shown + self.folded = folded # if the group content is to be shown def __iter__(self): """Iterate over the contacts""" @@ -301,9 +306,9 @@ class RosterGroup(object): if contact_filter is Roster.DEFAULT_FILTER or contact_filter is None: contact_list = self.contacts.copy() else: - contact_list = [contact - for contact in self.contacts.copy() - if contact_filter[0](contact, contact_filter[1]) + contact_list = [ + contact for contact in self.contacts.copy() + if contact_filter[0](contact, contact_filter[1]) ] contact_list = sorted(contact_list, key=SORTING_METHODS['name']) @@ -329,10 +334,12 @@ class RosterGroup(object): """Return the number of connected contacts""" return len([1 for contact in self.contacts if len(contact)]) + def create_roster(): "Create the global roster object" global roster roster = Roster() + # Shared roster object roster = None diff --git a/poezio/roster_sorting.py b/poezio/roster_sorting.py index c57f0dce..9f156e0f 100644 --- a/poezio/roster_sorting.py +++ b/poezio/roster_sorting.py @@ -5,17 +5,21 @@ Defines the roster sorting methods used in roster.py ########################### Contacts sorting ############################ -PRESENCE_PRIORITY = {'unavailable': 5, - 'xa': 4, - 'away': 3, - 'dnd': 2, - '': 1, - 'available': 1} +PRESENCE_PRIORITY = { + 'unavailable': 5, + 'xa': 4, + 'away': 3, + 'dnd': 2, + '': 1, + 'available': 1 +} + def sort_jid(contact): """Sort by contact JID""" return contact.bare_jid + def sort_show(contact): """Sort by show (from high availability to low)""" res = contact.get_highest_priority_resource() @@ -26,23 +30,28 @@ def sort_show(contact): return 0 return PRESENCE_PRIORITY[show] + def sort_resource_nb(contact): """Sort by number of connected resources""" - return - len(contact) + return -len(contact) + def sort_name(contact): """Sort by name (case insensitive)""" return contact.name.lower() or contact.bare_jid + def sort_sname(contact): """Sort by name (case sensitive)""" return contact.name or contact.bare_jid + def sort_online(contact): """Sort by connected/disconnected""" result = sort_show(contact) return 0 if result < 5 else 1 + SORTING_METHODS = { 'jid': sort_jid, 'sname': sort_sname, @@ -52,39 +61,44 @@ SORTING_METHODS = { 'online': sort_online, } - ######################## Roster Groups sorting ########################## + def sort_group_name(group): """Sort by name (case insensitive)""" return group.name.lower() + def sort_group_sname(group): """Sort by name (case-sensitive)""" return group.name + def sort_group_folded(group): """Sort by folded/unfolded""" return group.folded + def sort_group_connected(group): """Sort by number of connected contacts""" - return - group.get_nb_connected_contacts() + return -group.get_nb_connected_contacts() + def sort_group_size(group): """Sort by group size""" - return - len(group) + return -len(group) + def sort_group_none(group): """Put the none group at the end, if any""" return 0 if group.name != 'none' else 1 + GROUP_SORTING_METHODS = { - 'name': sort_group_name, - 'fold': sort_group_folded, - 'connected': sort_group_connected, - 'size': sort_group_size, - 'none': sort_group_none, - 'sname': sort_group_sname, + 'name': sort_group_name, + 'fold': sort_group_folded, + 'connected': sort_group_connected, + 'size': sort_group_size, + 'none': sort_group_none, + 'sname': sort_group_sname, } - diff --git a/poezio/size_manager.py b/poezio/size_manager.py index 4dfa6f76..d5c7e719 100644 --- a/poezio/size_manager.py +++ b/poezio/size_manager.py @@ -12,8 +12,8 @@ THRESHOLD_HEIGHT_DEGRADE = 10 FULL_WIDTH_DEGRADE = 66 FULL_HEIGHT_DEGRADE = 10 -class SizeManager(object): +class SizeManager(object): def __init__(self, core): self._core = core @@ -36,4 +36,3 @@ class SizeManager(object): def core_degrade_y(self): y, x = self._core.stdscr.getmaxyx() return y < FULL_HEIGHT_DEGRADE - diff --git a/poezio/tabs/__init__.py b/poezio/tabs/__init__.py index 01f65aa3..81e9f115 100644 --- a/poezio/tabs/__init__.py +++ b/poezio/tabs/__init__.py @@ -13,8 +13,10 @@ from poezio.tabs.adhoc_commands_list import AdhocCommandsListTab from poezio.tabs.data_forms import DataFormsTab from poezio.tabs.bookmarkstab import BookmarksTab -__all__ = ['Tab', 'ChatTab', 'GapTab', 'OneToOneTab', 'STATE_PRIORITY', - 'SHOW_NAME', 'RosterInfoTab', 'MucTab', 'NS_MUC_USER', 'PrivateTab', - 'ConfirmTab', 'ConversationTab', 'StaticConversationTab', - 'DynamicConversationTab', 'XMLTab', 'ListTab', 'MucListTab', - 'AdhocCommandsListTab', 'DataFormsTab', 'BookmarksTab'] +__all__ = [ + 'Tab', 'ChatTab', 'GapTab', 'OneToOneTab', 'STATE_PRIORITY', 'SHOW_NAME', + 'RosterInfoTab', 'MucTab', 'NS_MUC_USER', 'PrivateTab', 'ConfirmTab', + 'ConversationTab', 'StaticConversationTab', 'DynamicConversationTab', + 'XMLTab', 'ListTab', 'MucListTab', 'AdhocCommandsListTab', 'DataFormsTab', + 'BookmarksTab' +] diff --git a/poezio/tabs/adhoc_commands_list.py b/poezio/tabs/adhoc_commands_list.py index 6db654c9..a1b186be 100644 --- a/poezio/tabs/adhoc_commands_list.py +++ b/poezio/tabs/adhoc_commands_list.py @@ -11,42 +11,51 @@ from poezio.tabs import ListTab from slixmpp.plugins.xep_0030.stanza.items import DiscoItem + class AdhocCommandsListTab(ListTab): plugin_commands = {} plugin_keys = {} def __init__(self, core, jid): - ListTab.__init__(self, core, jid.full, - "“Enter”: execute selected command.", - 'Ad-hoc commands of JID %s (Loading)' % jid, - (('Node', 0), ('Description', 1))) + ListTab.__init__( + self, core, jid.full, "“Enter”: execute selected command.", + 'Ad-hoc commands of JID %s (Loading)' % jid, (('Node', 0), + ('Description', 1))) self.key_func['^M'] = self.execute_selected_command def execute_selected_command(self): if not self.listview or not self.listview.get_selected_row(): return node, name, jid = self.listview.get_selected_row() - session = {'next': self.core.handler.next_adhoc_step, - 'error': self.core.handler.adhoc_error} + session = { + 'next': self.core.handler.next_adhoc_step, + 'error': self.core.handler.adhoc_error + } self.core.xmpp.plugin['xep_0050'].start_command(jid, node, session) def get_columns_sizes(self): - return {'Node': int(self.width * 3 / 8), - 'Description': int(self.width * 5 / 8)} + return { + 'Node': int(self.width * 3 / 8), + 'Description': int(self.width * 5 / 8) + } def on_list_received(self, iq): """ Fill the listview with the value from the received iq """ if iq['type'] == 'error': - self.set_error(iq['error']['type'], iq['error']['code'], iq['error']['text']) + self.set_error(iq['error']['type'], iq['error']['code'], + iq['error']['text']) return + def get_items(): substanza = iq['disco_items'] for item in substanza['substanzas']: if isinstance(item, DiscoItem): yield item - items = [(item['node'], item['name'] or '', item['jid']) for item in get_items()] + + 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 if self.core.current_tab() is self: diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index 0570be27..29404036 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -35,60 +35,61 @@ from poezio.decorators import command_args_parser # getters for tab colors (lambdas, so that they are dynamic) STATE_COLORS = { - 'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED, - 'scrolled': lambda: get_theme().COLOR_TAB_SCROLLED, - 'nonempty': lambda: get_theme().COLOR_TAB_NONEMPTY, - 'joined': lambda: get_theme().COLOR_TAB_JOINED, - 'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE, - 'composing': lambda: get_theme().COLOR_TAB_COMPOSING, - 'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT, - 'private': lambda: get_theme().COLOR_TAB_PRIVATE, - 'normal': lambda: get_theme().COLOR_TAB_NORMAL, - 'current': lambda: get_theme().COLOR_TAB_CURRENT, - 'attention': lambda: get_theme().COLOR_TAB_ATTENTION, - } + 'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED, + 'scrolled': lambda: get_theme().COLOR_TAB_SCROLLED, + 'nonempty': lambda: get_theme().COLOR_TAB_NONEMPTY, + 'joined': lambda: get_theme().COLOR_TAB_JOINED, + 'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE, + 'composing': lambda: get_theme().COLOR_TAB_COMPOSING, + 'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT, + 'private': lambda: get_theme().COLOR_TAB_PRIVATE, + 'normal': lambda: get_theme().COLOR_TAB_NORMAL, + 'current': lambda: get_theme().COLOR_TAB_CURRENT, + 'attention': lambda: get_theme().COLOR_TAB_ATTENTION, +} VERTICAL_STATE_COLORS = { - 'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED, - 'scrolled': lambda: get_theme().COLOR_VERTICAL_TAB_SCROLLED, - 'nonempty': lambda: get_theme().COLOR_VERTICAL_TAB_NONEMPTY, - 'joined': lambda: get_theme().COLOR_VERTICAL_TAB_JOINED, - 'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE, - 'composing': lambda: get_theme().COLOR_VERTICAL_TAB_COMPOSING, - 'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT, - 'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE, - 'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL, - 'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT, - 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION, - } - + 'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED, + 'scrolled': lambda: get_theme().COLOR_VERTICAL_TAB_SCROLLED, + 'nonempty': lambda: get_theme().COLOR_VERTICAL_TAB_NONEMPTY, + 'joined': lambda: get_theme().COLOR_VERTICAL_TAB_JOINED, + 'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE, + 'composing': lambda: get_theme().COLOR_VERTICAL_TAB_COMPOSING, + 'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT, + 'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE, + 'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL, + 'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT, + 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION, +} # priority of the different tab states when using Alt+e # higher means more priority, < 0 means not selectable STATE_PRIORITY = { - 'normal': -1, - 'current': -1, - 'disconnected': 0, - 'nonempty': 0.1, - 'scrolled': 0.5, - 'joined': 0.8, - 'composing': 0.9, - 'message': 1, - 'highlight': 2, - 'private': 2, - 'attention': 3 - } + 'normal': -1, + 'current': -1, + 'disconnected': 0, + 'nonempty': 0.1, + 'scrolled': 0.5, + 'joined': 0.8, + 'composing': 0.9, + 'message': 1, + 'highlight': 2, + 'private': 2, + 'attention': 3 +} SHOW_NAME = { - 'dnd': 'busy', - 'away': 'away', - 'xa': 'not available', - 'chat': 'chatty', - '': 'available' - } + 'dnd': 'busy', + 'away': 'away', + 'xa': 'not available', + 'chat': 'chatty', + '': 'available' +} + class Tab(object): plugin_commands = {} plugin_keys = {} + def __init__(self, core): self.core = core if not hasattr(self, 'name'): @@ -99,10 +100,9 @@ class Tab(object): self._prev_state = None self.need_resize = False - self.key_func = {} # each tab should add their keys in there - # and use them in on_input - self.commands = {} # and their own commands - + self.key_func = {} # each tab should add their keys in there + # and use them in on_input + self.commands = {} # and their own commands @property def size(self): @@ -148,9 +148,13 @@ class Tab(object): elif STATE_PRIORITY[value] < STATE_PRIORITY[self._state] and \ value not in ('current', 'disconnected') and \ not (self._state == 'scrolled' and value == 'disconnected'): - log.debug("Did not set state because of lower priority, asked: %s, kept: %s", value, self._state) - elif self._state == 'disconnected' and value not in ('joined', 'current'): - log.debug('Did not set state because disconnected tabs remain visible') + log.debug( + "Did not set state because of lower priority, asked: %s, kept: %s", + value, self._state) + elif self._state == 'disconnected' and value not in ('joined', + 'current'): + log.debug( + 'Did not set state because disconnected tabs remain visible') else: self._state = value if self._state == 'current': @@ -194,11 +198,22 @@ class Tab(object): shortdesc = command.get('shortdesc', '') completion = command.get('completion') usage = command.get('usage', '') - self.register_command(name, func, desc=desc, shortdesc=shortdesc, - completion=completion, usage=usage) - - - def register_command(self, name, func, *, desc='', shortdesc='', completion=None, usage=''): + self.register_command( + name, + func, + desc=desc, + shortdesc=shortdesc, + completion=completion, + usage=usage) + + def register_command(self, + name, + func, + *, + desc='', + shortdesc='', + completion=None, + usage=''): """ Add a command """ @@ -234,7 +249,8 @@ class Tab(object): whitespace = the_input.text.find(' ') if whitespace == -1: whitespace = len(the_input.text) - the_input.text = the_input.text[:whitespace-1] + the_input.text[whitespace:] + the_input.text = the_input.text[:whitespace - + 1] + the_input.text[whitespace:] the_input.new_completion(words, 0) hit_copy = set(the_input.hit_list) if len(hit_copy) == 1: @@ -249,10 +265,10 @@ class Tab(object): command = self.commands[command_name] elif command_name in self.core.commands: command = self.core.commands[command_name] - else: # Unknown command, cannot complete + else: # Unknown command, cannot complete return False if command.comp is None: - return False # There's no completion function + return False # There's no completion function else: comp = command.comp(the_input) if comp: @@ -269,11 +285,11 @@ class Tab(object): if txt.startswith('/') and not txt.startswith('//') and\ not txt.startswith('/me '): command = txt.strip().split()[0][1:] - arg = txt[2+len(command):] # jump the '/' and the ' ' + arg = txt[2 + len(command):] # jump the '/' and the ' ' func = None - if command in self.commands: # check tab-specific commands + if command in self.commands: # check tab-specific commands func = self.commands[command].func - elif command in self.core.commands: # check global commands + elif command in self.core.commands: # check global commands func = self.core.commands[command].func else: low = command.lower() @@ -286,9 +302,8 @@ class Tab(object): error_handled = self.missing_command_callback(low) if not error_handled: self.core.information("Unknown command (%s)" % - (command), - 'Error') - if command in ('correct', 'say'): # hack + (command), 'Error') + if command in ('correct', 'say'): # hack arg = xhtml.convert_simple_to_full_colors(arg) else: arg = xhtml.clean_text_simple(arg) @@ -422,8 +437,8 @@ class Tab(object): def __del__(self): log.debug('------ Closing tab %s', self.__class__.__name__) -class GapTab(Tab): +class GapTab(Tab): def __bool__(self): return False @@ -435,7 +450,9 @@ class GapTab(Tab): return '' def refresh(self): - log.debug('WARNING: refresh() called on a gap tab, this should not happen') + log.debug( + 'WARNING: refresh() called on a gap tab, this should not happen') + class ChatTab(Tab): """ @@ -447,6 +464,7 @@ class ChatTab(Tab): plugin_commands = {} plugin_keys = {} message_type = 'chat' + def __init__(self, core, jid=''): Tab.__init__(self, core) self.name = jid @@ -454,7 +472,7 @@ class ChatTab(Tab): self._remote_wants_chatstates = False self.directed_presence = None self._text_buffer = TextBuffer() - self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive" + self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive" # We keep a reference of the event that will set our chatstate to "paused", so that # we can delete it or change it if we need to self.timed_event_paused = None @@ -464,18 +482,24 @@ class ChatTab(Tab): self.key_func['M-h'] = self.scroll_separator 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.') - self.register_command('xhtml', self.command_xhtml, - usage='<custom xhtml>', - shortdesc='Send custom XHTML.') - self.register_command('clear', self.command_clear, - 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.', - completion=self.completion_correct) + self.register_command( + 'say', + self.command_say, + usage='<message>', + shortdesc='Send the message.') + self.register_command( + 'xhtml', + self.command_xhtml, + usage='<custom xhtml>', + shortdesc='Send custom XHTML.') + self.register_command( + 'clear', self.command_clear, 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.', + completion=self.completion_correct) self.chat_state = None self.update_commands() self.update_keys() @@ -508,22 +532,39 @@ class ChatTab(Tab): if not logger.log_message(name, nickname, txt, date=time, typ=typ): 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, - typ=1, highlight=False): + def add_message(self, + txt, + time=None, + nickname=None, + forced_user=None, + nick_color=None, + identifier=None, + jid=None, + history=None, + typ=1, + highlight=False): self.log_message(txt, nickname, time=time, typ=typ) - self._text_buffer.add_message(txt, time=time, - nickname=nickname, - highlight=highlight, - nick_color=nick_color, - history=history, - user=forced_user, - identifier=identifier, - jid=jid) - - def modify_message(self, txt, old_id, new_id, user=None, jid=None, nickname=None): + self._text_buffer.add_message( + txt, + time=time, + nickname=nickname, + highlight=highlight, + nick_color=nick_color, + history=history, + user=forced_user, + identifier=identifier, + jid=jid) + + def modify_message(self, + txt, + old_id, + new_id, + user=None, + jid=None, + nickname=None): self.log_message(txt, nickname, typ=1) - message = self._text_buffer.modify_message(txt, old_id, new_id, time=time, user=user, jid=jid) + message = self._text_buffer.modify_message( + txt, old_id, new_id, time=time, user=user, jid=jid) if message: self.text_win.modify_message(old_id, message) self.core.refresh_window() @@ -535,7 +576,7 @@ class ChatTab(Tab): Complete the input with words recently said """ # build the list of the recent words - char_we_dont_want = string.punctuation+' ’„“”…«»' + char_we_dont_want = string.punctuation + ' ’„“”…«»' words = list() for msg in self._text_buffer.messages[:-40:-1]: if not msg: @@ -571,7 +612,8 @@ class ChatTab(Tab): if not arg: return try: - body = xhtml.clean_text(xhtml.xhtml_to_poezio_colors(arg, force=True)) + body = xhtml.clean_text( + xhtml.xhtml_to_poezio_colors(arg, force=True)) ET.fromstring(arg) except: self.core.information('Could not send custom xhtml', 'Error') @@ -604,7 +646,8 @@ class ChatTab(Tab): Send an empty chatstate message """ if self.check_send_chat_state(): - if state in ('active', 'inactive', 'gone') and self.inactive and not always_send: + if state in ('active', 'inactive', + 'gone') and self.inactive and not always_send: return if (config.get_by_tabname('send_chat_states', self.general_jid) and self.remote_wants_chatstates is not False): @@ -642,7 +685,8 @@ class ChatTab(Tab): # First, cancel the delay if it already exists, before rescheduling # it at a new date self.cancel_paused_delay() - new_event = timed_events.DelayedEvent(4, self.send_chat_state, 'paused') + new_event = timed_events.DelayedEvent(4, self.send_chat_state, + 'paused') self.core.add_timed_event(new_event) self.timed_event_paused = new_event @@ -671,7 +715,10 @@ class ChatTab(Tab): def completion_correct(self, the_input): if self.last_sent_message and the_input.get_argument_position() == 1: - return Completion(the_input.auto_completion, [self.last_sent_message['body']], '', quotify=False) + return Completion( + the_input.auto_completion, [self.last_sent_message['body']], + '', + quotify=False) @property def inactive(self): @@ -703,23 +750,23 @@ class ChatTab(Tab): return self.text_win.scroll_down(1) def on_scroll_up(self): - return self.text_win.scroll_up(self.text_win.height-1) + return self.text_win.scroll_up(self.text_win.height - 1) def on_scroll_down(self): - return self.text_win.scroll_down(self.text_win.height-1) + return self.text_win.scroll_down(self.text_win.height - 1) def on_half_scroll_up(self): - return self.text_win.scroll_up((self.text_win.height-1) // 2) + return self.text_win.scroll_up((self.text_win.height - 1) // 2) def on_half_scroll_down(self): - return self.text_win.scroll_down((self.text_win.height-1) // 2) + return self.text_win.scroll_down((self.text_win.height - 1) // 2) @refresh_wrapper.always def scroll_separator(self): self.text_win.scroll_to_separator() -class OneToOneTab(ChatTab): +class OneToOneTab(ChatTab): def __init__(self, core, jid=''): ChatTab.__init__(self, core, jid) @@ -735,18 +782,18 @@ class OneToOneTab(ChatTab): self.remote_supports_attention = True self.remote_supports_receipts = True self.check_features() - self.register_command('unquery', self.command_unquery, - shortdesc='Close the tab.') - self.register_command('close', self.command_unquery, - shortdesc='Close the tab.') + self.register_command( + 'unquery', self.command_unquery, shortdesc='Close the tab.') + self.register_command( + 'close', self.command_unquery, shortdesc='Close the tab.') def remote_user_color(self): return dump_tuple(get_theme().COLOR_REMOTE_USER) def update_status(self, status): old_status = self.__status - if not (old_status.show != status.show or - old_status.message != status.message): + if not (old_status.show != status.show + or old_status.message != status.message): return self.__status = status hide_status_change = config.get_by_tabname('hide_status_change', @@ -820,16 +867,20 @@ class OneToOneTab(ChatTab): 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._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: self.core.xmpp.plugin['xep_0030'].get_info( - jid=self.get_dest_jid(), timeout=5, - callback=self.features_checked) + jid=self.get_dest_jid(), + timeout=5, + callback=self.features_checked) @command_args_parser.raw def command_attention(self, message): @@ -872,12 +923,14 @@ class OneToOneTab(ChatTab): "Check for the 'attention' features" 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.') + 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.') else: self.remote_supports_attention = False return self.remote_supports_attention @@ -888,10 +941,12 @@ class OneToOneTab(ChatTab): if 'correct' in self.commands: del self.commands['correct'] elif 'correct' not in self.commands: - self.register_command('correct', self.command_correct, - desc='Fix the last message with whatever you want.', - shortdesc='Correct the last message.', - completion=self.completion_correct) + self.register_command( + 'correct', + self.command_correct, + desc='Fix the last message with whatever you want.', + shortdesc='Correct the last message.', + completion=self.completion_correct) return 'correct' in self.commands def _feature_receipts(self, features): @@ -905,8 +960,7 @@ class OneToOneTab(ChatTab): def features_checked(self, iq): "Features check callback" features = iq['disco_info'].get_features() or [] - before = ('correct' in self.commands, - self.remote_supports_attention, + before = ('correct' in self.commands, self.remote_supports_attention, self.remote_supports_receipts) correct = self._feature_correct(features) attention = self._feature_attention(features) @@ -918,7 +972,7 @@ class OneToOneTab(ChatTab): self.__initial_disco = True if not (correct or attention or receipts): - return # don’t display anything + return # don’t display anything ok = get_theme().CHAR_OK nope = get_theme().CHAR_EMPTY @@ -933,4 +987,3 @@ class OneToOneTab(ChatTab): msg = msg % (color, correct, attention, receipts) self.add_message(msg, typ=0) self.core.refresh_window() - diff --git a/poezio/tabs/bookmarkstab.py b/poezio/tabs/bookmarkstab.py index 452edd21..498d2f00 100644 --- a/poezio/tabs/bookmarkstab.py +++ b/poezio/tabs/bookmarkstab.py @@ -17,19 +17,17 @@ class BookmarksTab(Tab): a 4 widgets to set the jid/password/autojoin/storage method """ plugin_commands = {} + def __init__(self, core, bookmarks: BookmarkList): Tab.__init__(self, core) 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.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' @@ -46,7 +44,8 @@ class BookmarksTab(Tab): self.update_commands() def add_bookmark(self): - new_bookmark = Bookmark(safeJID('room@example.tld/nick'), method='local') + new_bookmark = Bookmark( + safeJID('room@example.tld/nick'), method='local') self.new_bookmarks.append(new_bookmark) self.bookmarks_win.add_bookmark(new_bookmark) @@ -70,14 +69,16 @@ class BookmarksTab(Tab): def on_save(self): self.bookmarks_win.save() if find_duplicates(self.new_bookmarks): - self.core.information('Duplicate bookmarks in list (saving aborted)', 'Error') + 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') + self.core.information('Invalid JID for bookmark: %s/%s' % + (bm.jid, bm.nick), 'Error') return for bm in self.removed_bookmarks: @@ -89,6 +90,7 @@ class BookmarksTab(Tab): self.core.information('Bookmarks saved.', 'Info') else: self.core.information('Remote bookmarks not saved.', 'Error') + self.bookmarks.save(self.core.xmpp, callback=send_cb) self.core.close_tab(self) return True @@ -105,19 +107,23 @@ class BookmarksTab(Tab): 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 - }) + '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.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) + self.height - 2 - tab_height - info_height, 0) def on_info_win_size_changed(self): if self.core.information_win_size >= self.height - 3: @@ -125,9 +131,9 @@ class BookmarksTab(Tab): 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.width, 1, 0) self.info_header.resize(1, self.width, - self.height - 2 - tab_height - info_height, 0) + self.height - 2 - tab_height - info_height, 0) def refresh(self): if self.need_resize: @@ -147,4 +153,3 @@ def find_duplicates(bm_list): return True jids.add(bookmark.jid) return False - diff --git a/poezio/tabs/confirmtab.py b/poezio/tabs/confirmtab.py index 39f630cb..28c26122 100644 --- a/poezio/tabs/confirmtab.py +++ b/poezio/tabs/confirmtab.py @@ -13,7 +13,13 @@ class ConfirmTab(Tab): plugin_commands = {} plugin_keys = {} - def __init__(self, core, name, text, short_message, callback, critical=False): + def __init__(self, + core, + name, + text, + short_message, + callback, + critical=False): """Parameters: name: The name of the tab text: the text shown in the tab @@ -26,7 +32,8 @@ class ConfirmTab(Tab): Tab.__init__(self, core) self.state = 'highlight' self.name = name - self.default_help_message = windows.HelpText("Choose with arrow keys and press enter") + self.default_help_message = windows.HelpText( + "Choose with arrow keys and press enter") self.input = self.default_help_message self.infowin_top = windows.ConfirmStatusWin(short_message, critical) self.infowin_bottom = windows.ConfirmStatusWin(short_message, critical) @@ -76,10 +83,12 @@ class ConfirmTab(Tab): tab_win_height = Tab.tab_win_height() self.infowin_top.resize(1, self.width, 0, 0) - self.infowin_bottom.resize(1, self.width, self.height - 2 - info_win_height - tab_win_height, 0) + self.infowin_bottom.resize( + 1, self.width, self.height - 2 - info_win_height - tab_win_height, + 0) self.dialog.resize(self.height - 3 - info_win_height - tab_win_height, self.width, 1, 0) - self.input.resize(1, self.width, self.height-1, 0) + self.input.resize(1, self.width, self.height - 1, 0) def close(self, arg=None): self.done = True @@ -95,8 +104,10 @@ class ConfirmTab(Tab): return self.key_func[key]() def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: + if self.core.information_win_size >= self.height - 3: return - self.dialog.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0) - self.infowin_bottom.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) - + self.dialog.resize(self.height - 3 - self.core.information_win_size - + Tab.tab_win_height(), self.width, 1, 0) + self.infowin_bottom.resize( + 1, self.width, self.height - 2 - self.core.information_win_size - + Tab.tab_win_height(), 0) diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py index cc6d716d..e8fcd720 100644 --- a/poezio/tabs/conversationtab.py +++ b/poezio/tabs/conversationtab.py @@ -30,6 +30,7 @@ from poezio.text_buffer import CorrectionError from poezio.theming import get_theme, dump_tuple from poezio.decorators import command_args_parser + class ConversationTab(OneToOneTab): """ The tab containg a normal conversation (not from a MUC) @@ -39,12 +40,13 @@ class ConversationTab(OneToOneTab): plugin_keys = {} additional_information = {} message_type = 'chat' + def __init__(self, core, jid): OneToOneTab.__init__(self, core, jid) self.nick = None self.nick_sent = False self.state = 'normal' - self.name = jid # a conversation tab is linked to one specific full jid OR bare jid + self.name = jid # a conversation tab is linked to one specific full jid OR bare jid self.text_win = windows.TextWin() self._text_buffer.add_window(self.text_win) self.upper_bar = windows.ConversationStatusMessageWin() @@ -52,21 +54,30 @@ class ConversationTab(OneToOneTab): # keys self.key_func['^I'] = self.completion # commands - 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.') - self.register_command('info', self.command_info, - 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.', - completion=self.core.completion.last_activity) - self.register_command('add', self.command_add, - desc='Add the current JID to your roster, ask them to' - ' allow you to see his presence, and allow them to' - ' see your presence.', - shortdesc='Add a user to your roster.') + 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.') + self.register_command( + 'info', + self.command_info, + 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.', + completion=self.core.completion.last_activity) + self.register_command( + 'add', + self.command_add, + desc='Add the current JID to your roster, ask them to' + ' allow you to see his presence, and allow them to' + ' see your presence.', + shortdesc='Add a user to your roster.') self.update_commands() self.update_keys() @@ -114,8 +125,12 @@ class ConversationTab(OneToOneTab): msg['replace']['id'] = self.last_sent_message['id'] if config.get_by_tabname('group_corrections', self.name): try: - self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'], jid=self.core.xmpp.boundjid, - nickname=self.core.own_nick) + self.modify_message( + msg['body'], + self.last_sent_message['id'], + msg['id'], + jid=self.core.xmpp.boundjid, + nickname=self.core.own_nick) replaced = True except CorrectionError: log.error('Unable to correct a message', exc_info=True) @@ -125,8 +140,8 @@ class ConversationTab(OneToOneTab): msg.enable('html') msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) msg['body'] = xhtml.clean_text(msg['body']) - if (config.get_by_tabname('send_chat_states', self.general_jid) and - self.remote_wants_chatstates is not False): + if (config.get_by_tabname('send_chat_states', self.general_jid) + and self.remote_wants_chatstates is not False): needed = 'inactive' if self.inactive else 'active' msg['chat_state'] = needed if attention and self.remote_supports_attention: @@ -138,12 +153,13 @@ class ConversationTab(OneToOneTab): self.input.refresh() return if not replaced: - self.add_message(msg['body'], - nickname=self.core.own_nick, - nick_color=get_theme().COLOR_OWN_NICK, - identifier=msg['id'], - jid=self.core.xmpp.boundjid, - typ=1) + self.add_message( + msg['body'], + nickname=self.core.own_nick, + nick_color=get_theme().COLOR_OWN_NICK, + identifier=msg['id'], + jid=self.core.xmpp.boundjid, + typ=1) self.last_sent_message = msg if self.remote_supports_receipts: @@ -164,9 +180,12 @@ class ConversationTab(OneToOneTab): def callback(iq): if iq['type'] != 'result': if iq['error']['type'] == 'auth': - self.core.information('You are not allowed to see the activity of this contact.', 'Error') + self.core.information( + 'You are not allowed to see the activity of this contact.', + 'Error') else: - self.core.information('Error retrieving the activity', 'Error') + self.core.information('Error retrieving the activity', + 'Error') return seconds = iq['last_activity']['seconds'] status = iq['last_activity']['status'] @@ -174,19 +193,21 @@ class ConversationTab(OneToOneTab): msg = '\x19%s}The last activity of %s was %s ago%s' if not safeJID(from_).user: msg = '\x19%s}The uptime of %s is %s.' % ( - dump_tuple(get_theme().COLOR_INFORMATION_TEXT), - from_, - common.parse_secs_to_str(seconds)) + dump_tuple(get_theme().COLOR_INFORMATION_TEXT), from_, + common.parse_secs_to_str(seconds)) else: msg = '\x19%s}The last activity of %s was %s ago%s' % ( dump_tuple(get_theme().COLOR_INFORMATION_TEXT), from_, common.parse_secs_to_str(seconds), - (' and his/her last status was %s' % status) if status else '',) + (' and his/her last status was %s' % status) + if status else '', + ) self.add_message(msg) self.core.refresh_window() - self.core.xmpp.plugin['xep_0012'].get_last_activity(self.get_dest_jid(), callback=callback) + self.core.xmpp.plugin['xep_0012'].get_last_activity( + self.get_dest_jid(), callback=callback) @refresh_wrapper.conditional @command_args_parser.ignored @@ -201,12 +222,20 @@ class ConversationTab(OneToOneTab): else: resource = None if resource: - 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.presence or 'available', 'status': status, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}) + 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.presence or 'available', + 'status': status, + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + }) return True else: - self._text_buffer.add_message("\x19%(info_col)s}No information available\x19o" % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}) + self._text_buffer.add_message( + "\x19%(info_col)s}No information available\x19o" % { + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + }) return True @command_args_parser.quoted(0, 1) @@ -214,14 +243,18 @@ class ConversationTab(OneToOneTab): """ /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') + 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') self.core.information(version, 'Info') + if args: return self.core.command.version(args[0]) jid = safeJID(self.name) @@ -229,8 +262,7 @@ class ConversationTab(OneToOneTab): if jid in roster: resource = roster[jid].get_highest_priority_resource() jid = resource.jid if resource else jid - fixes.get_version(self.core.xmpp, jid, - callback=callback) + fixes.get_version(self.core.xmpp, jid, callback=callback) @command_args_parser.ignored def command_add(self): @@ -258,16 +290,15 @@ class ConversationTab(OneToOneTab): tab_win_height = Tab.tab_win_height() bar_height = 1 - self.text_win.resize(self.height - 2 - bar_height - info_win_height - - tab_win_height, - self.width, bar_height, 0) + self.text_win.resize( + self.height - 2 - bar_height - info_win_height - tab_win_height, + self.width, bar_height, 0) self.text_win.rebuild_everything(self._text_buffer) if display_bar: self.upper_bar.resize(1, self.width, 0, 0) - self.get_info_header().resize(1, self.width, - self.height - 2 - info_win_height - - tab_win_height, - 0) + self.get_info_header().resize( + 1, self.width, self.height - 2 - info_win_height - tab_win_height, + 0) self.input.resize(1, self.width, self.height - 1, 0) def refresh(self): @@ -279,8 +310,11 @@ class ConversationTab(OneToOneTab): self.text_win.refresh() if display_bar: - self.upper_bar.refresh(self.get_dest_jid(), roster[self.get_dest_jid()]) - self.get_info_header().refresh(self.get_dest_jid(), roster[self.get_dest_jid()], self.text_win, self.chatstate, ConversationTab.additional_information) + self.upper_bar.refresh(self.get_dest_jid(), + roster[self.get_dest_jid()]) + self.get_info_header().refresh( + self.get_dest_jid(), roster[self.get_dest_jid()], self.text_win, + self.chatstate, ConversationTab.additional_information) if display_info_win: self.info_win.refresh() @@ -288,8 +322,9 @@ class ConversationTab(OneToOneTab): self.input.refresh() def refresh_info_header(self): - self.get_info_header().refresh(self.get_dest_jid(), roster[self.get_dest_jid()], - self.text_win, self.chatstate, ConversationTab.additional_information) + self.get_info_header().refresh( + self.get_dest_jid(), roster[self.get_dest_jid()], self.text_win, + self.chatstate, ConversationTab.additional_information) self.input.refresh() def get_nick(self): @@ -307,7 +342,9 @@ class ConversationTab(OneToOneTab): self.key_func[key]() return False self.input.do_command(key, raw=raw) - empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) + 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) return False @@ -347,15 +384,18 @@ class ConversationTab(OneToOneTab): curses.curs_set(1) if (config.get_by_tabname('send_chat_states', self.general_jid) and (not self.input.get_text() - or not self.input.get_text().startswith('//'))): + or not self.input.get_text().startswith('//'))): if resource: self.send_chat_state('active') def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: + if self.core.information_win_size >= self.height - 3: return - self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0) - self.get_info_header().resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) + self.text_win.resize(self.height - 3 - self.core.information_win_size - + Tab.tab_win_height(), self.width, 1, 0) + self.get_info_header().resize( + 1, self.width, self.height - 2 - self.core.information_win_size - + Tab.tab_win_height(), 0) def get_text_window(self): return self.text_win @@ -375,12 +415,14 @@ class ConversationTab(OneToOneTab): res.append((0, contact.name)) return res + class DynamicConversationTab(ConversationTab): """ A conversation tab associated with one bare JID that can be “locked” to a full jid, and unlocked, as described in the XEP-0296. Only one DynamicConversationTab can be opened for a given jid. """ + def __init__(self, core, jid, resource=None): self.locked_resource = None self.name = safeJID(jid).bare @@ -388,8 +430,10 @@ class DynamicConversationTab(ConversationTab): self.lock(resource) ConversationTab.__init__(self, core, jid) self.info_header = windows.DynamicConversationInfoWin() - self.register_command('unlock', self.unlock_command, - shortdesc='Unlock the conversation from a particular resource.') + self.register_command( + 'unlock', + self.unlock_command, + shortdesc='Unlock the conversation from a particular resource.') self.resize() def get_info_header(self): @@ -399,7 +443,7 @@ class DynamicConversationTab(ConversationTab): """ Lock the tab to the resource. """ - assert(resource) + assert (resource) if resource != self.locked_resource: self.locked_resource = resource info = '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT) @@ -407,10 +451,11 @@ class DynamicConversationTab(ConversationTab): message = ('%(info)sConversation locked to ' '%(jid_c)s%(jid)s/%(resource)s%(info)s.') % { - 'info': info, - 'jid_c': jid_c, - 'jid': self.name, - 'resource': resource} + 'info': info, + 'jid_c': jid_c, + 'jid': self.name, + 'resource': resource + } self.add_message(message, typ=0) self.check_features() @@ -432,9 +477,10 @@ class DynamicConversationTab(ConversationTab): if from_: message = ('%(info)sConversation unlocked (received activity' ' from %(jid_c)s%(jid)s%(info)s).') % { - 'info': info, - 'jid_c': jid_c, - 'jid': from_} + 'info': info, + 'jid_c': jid_c, + 'jid': from_ + } self.add_message(message, typ=0) else: message = '%sConversation unlocked.' % info @@ -466,8 +512,8 @@ class DynamicConversationTab(ConversationTab): else: displayed_jid = self.name self.get_info_header().refresh(displayed_jid, roster[self.name], - self.text_win, self.chatstate, - ConversationTab.additional_information) + self.text_win, self.chatstate, + ConversationTab.additional_information) if display_info_win: self.info_win.refresh() @@ -483,16 +529,19 @@ class DynamicConversationTab(ConversationTab): else: displayed_jid = self.name self.get_info_header().refresh(displayed_jid, roster[self.name], - self.text_win, self.chatstate, ConversationTab.additional_information) + self.text_win, self.chatstate, + ConversationTab.additional_information) self.input.refresh() + class StaticConversationTab(ConversationTab): """ A conversation tab associated with one Full JID. It cannot be locked to an different resource or unlocked. """ + def __init__(self, core, jid): - assert(safeJID(jid).resource) + assert (safeJID(jid).resource) ConversationTab.__init__(self, core, jid) self.info_header = windows.ConversationInfoWin() self.resize() diff --git a/poezio/tabs/data_forms.py b/poezio/tabs/data_forms.py index c73f4922..d216d4ca 100644 --- a/poezio/tabs/data_forms.py +++ b/poezio/tabs/data_forms.py @@ -8,12 +8,14 @@ log = logging.getLogger(__name__) from poezio import windows from poezio.tabs import Tab + class DataFormsTab(Tab): """ A tab contaning various window type, displaying a form that the user needs to fill. """ plugin_commands = {} + def __init__(self, core, form, on_cancel, on_send, kwargs): Tab.__init__(self, core) self._form = form @@ -24,7 +26,8 @@ class DataFormsTab(Tab): for field in self._form: self.fields.append(field) self.topic_win = windows.Topic() - self.form_win = windows.FormWin(form, self.height-4, self.width, 1, 0) + self.form_win = windows.FormWin(form, self.height - 4, self.width, 1, + 0) self.help_win = windows.HelpText("Ctrl+Y: send form, Ctrl+G: cancel") self.help_win_dyn = windows.HelpText() self.key_func['KEY_UP'] = self.form_win.go_to_previous_input @@ -72,4 +75,3 @@ class DataFormsTab(Tab): self.help_win.refresh() self.help_win_dyn.refresh(self.form_win.get_help_message()) self.form_win.refresh() - diff --git a/poezio/tabs/listtab.py b/poezio/tabs/listtab.py index ed40241b..84dcc38b 100644 --- a/poezio/tabs/listtab.py +++ b/poezio/tabs/listtab.py @@ -49,8 +49,7 @@ class ListTab(Tab): self.key_func['KEY_LEFT'] = self.list_header.sel_column_left 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.') + self.register_command('close', self.close, shortdesc='Close this tab.') self.resize() self.update_keys() self.update_commands() @@ -64,7 +63,6 @@ class ListTab(Tab): """ raise NotImplementedError - def refresh(self): if self.need_resize: self.resize() @@ -90,26 +88,27 @@ class ListTab(Tab): info_win_height = self.core.information_win_size tab_win_height = Tab.tab_win_height() - self.info_header.resize(1, self.width, - self.height - 2 - info_win_height - - tab_win_height, - 0) + self.info_header.resize( + 1, self.width, self.height - 2 - info_win_height - tab_win_height, + 0) column_size = self.get_columns_sizes() self.list_header.resize_columns(column_size) self.list_header.resize(1, self.width, 0, 0) self.listview.resize_columns(column_size) - self.listview.resize(self.height - 3 - info_win_height - tab_win_height, - self.width, 1, 0) - self.input.resize(1, self.width, self.height-1, 0) + self.listview.resize( + self.height - 3 - info_win_height - tab_win_height, self.width, 1, + 0) + self.input.resize(1, self.width, self.height - 1, 0) def on_slash(self): """ '/' is pressed, activate the input """ 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) - self.input.do_command("/") # we add the slash + self.input = windows.CommandInput("", self.reset_help_message, + self.execute_slash_command) + self.input.resize(1, self.width, self.height - 1, 0) + self.input.do_command("/") # we add the slash def close(self, arg=None): self.core.close_tab(self) @@ -118,7 +117,11 @@ 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() @@ -126,14 +129,12 @@ class ListTab(Tab): def sort_by(self): if self.list_header.get_order(): self.listview.sort_by_column( - col_name=self.list_header.get_sel_column(), - asc=False) + col_name=self.list_header.get_sel_column(), asc=False) self.list_header.set_order(False) self.list_header.refresh() else: self.listview.sort_by_column( - col_name=self.list_header.get_sel_column(), - asc=True) + col_name=self.list_header.get_sel_column(), asc=True) self.list_header.set_order(True) self.list_header.refresh() self.core.doupdate() @@ -144,7 +145,7 @@ class ListTab(Tab): return True curses.curs_set(0) self.input = self.default_help_message - self.input.resize(1, self.width, self.height-1, 0) + self.input.resize(1, self.width, self.height - 1, 0) return True def execute_slash_command(self, txt): @@ -167,10 +168,13 @@ class ListTab(Tab): return self.key_func[key]() def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: + if self.core.information_win_size >= self.height - 3: return - self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) - self.listview.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0) + self.info_header.resize( + 1, self.width, self.height - 2 - self.core.information_win_size - + Tab.tab_win_height(), 0) + self.listview.resize(self.height - 3 - self.core.information_win_size - + Tab.tab_win_height(), self.width, 1, 0) def on_lose_focus(self): self.state = 'normal' @@ -197,5 +201,3 @@ class ListTab(Tab): def matching_names(self): return [(2, self.name)] - - diff --git a/poezio/tabs/muclisttab.py b/poezio/tabs/muclisttab.py index f14f4172..005f3fe1 100644 --- a/poezio/tabs/muclisttab.py +++ b/poezio/tabs/muclisttab.py @@ -11,6 +11,7 @@ from poezio.tabs import ListTab from slixmpp.plugins.xep_0030.stanza.items import DiscoItem + class MucListTab(ListTab): """ A tab listing rooms from a specific server, displaying various information, @@ -20,8 +21,7 @@ class MucListTab(ListTab): plugin_keys = {} def __init__(self, core, server): - ListTab.__init__(self, core, server.full, - "“j”: join room.", + ListTab.__init__(self, core, server.full, "“j”: join room.", 'Chatroom list on server %s (Loading)' % server, (('node-part', 0), ('name', 2), ('users', 3))) self.key_func['j'] = self.join_selected @@ -29,10 +29,12 @@ class MucListTab(ListTab): self.key_func['^M'] = self.join_selected def get_columns_sizes(self): - return {'node-part': int(self.width* 2 / 8), - 'name': int(self.width * 5 / 8), - 'users': self.width - int(self.width * 2 / 8) - - int(self.width * 5 / 8)} + return { + 'node-part': int(self.width * 2 / 8), + 'name': int(self.width * 5 / 8), + 'users': + self.width - int(self.width * 2 / 8) - int(self.width * 5 / 8) + } def join_selected_no_focus(self): return @@ -43,16 +45,18 @@ class MucListTab(ListTab): Used with command_list """ if iq['type'] == 'error': - self.set_error(iq['error']['type'], iq['error']['code'], iq['error']['text']) + self.set_error(iq['error']['type'], iq['error']['code'], + iq['error']['text']) return + def get_items(): substanza = iq['disco_items'] for item in substanza['substanzas']: if isinstance(item, DiscoItem): yield (item['jid'], item['node'], item['name']) - items = [(item[0].split('@')[0], - item[0], - item[2] or '', '') for item in get_items()] + + items = [(item[0].split('@')[0], 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 if self.core.current_tab() is self: @@ -67,4 +71,3 @@ class MucListTab(ListTab): if not row: return self.core.command.join(row[1]) - diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index 4438e015..94b521ba 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -34,7 +34,6 @@ from poezio.theming import get_theme, dump_tuple from poezio.user import User from poezio.core.structs import Completion, Status - NS_MUC_USER = 'http://jabber.org/protocol/muc#user' STATUS_XPATH = '{%s}x/{%s}status' % (NS_MUC_USER, NS_MUC_USER) @@ -129,11 +128,14 @@ class MucTab(ChatTab): seconds = delta.seconds + delta.days * 24 * 3600 else: seconds = 0 - muc.join_groupchat(self.core, self.name, self.own_nick, - self.password, - status=status.message, - show=status.show, - seconds=seconds) + muc.join_groupchat( + self.core, + self.name, + self.own_nick, + self.password, + status=status.message, + show=status.show, + seconds=seconds) def leave_room(self, message): if self.joined: @@ -152,8 +154,10 @@ class MucTab(ChatTab): 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})' ' left the room' ' (\x19o%(reason)s\x19%(info_col)s})') % { - 'info_col': info_col, 'reason': message, - 'spec': char_quit, 'color': color, + 'info_col': info_col, + 'reason': message, + 'spec': char_quit, + 'color': color, 'color_spec': spec_col, 'nick': self.own_nick, } @@ -162,60 +166,77 @@ class MucTab(ChatTab): 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})' ' left the room') % { 'info_col': info_col, - 'spec': char_quit, 'color': color, + 'spec': char_quit, + 'color': color, 'color_spec': spec_col, 'nick': self.own_nick, } self.add_message(msg, typ=2) self.disconnect() - muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, message) + muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, + message) self.core.disable_private_tabs(self.name, reason=msg) else: - muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, message) + muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, + message) def change_affiliation(self, nick_or_jid, affiliation, reason=''): """ Change the affiliation of a nick or JID """ + def callback(iq): if iq['type'] == 'error': - self.core.information("Could not set affiliation '%s' for '%s'." % ( - affiliation, nick_or_jid), "Warning") + self.core.information( + "Could not set affiliation '%s' for '%s'." % + (affiliation, nick_or_jid), "Warning") + if not self.joined: 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), + return self.core.information('The affiliation must be one of ' + + ', '.join(valid_affiliations), 'Error') if nick_or_jid in [user.nick for user in self.users]: - res = muc.set_user_affiliation(self.core.xmpp, self.name, - affiliation, nick=nick_or_jid, - callback=callback, reason=reason) + res = muc.set_user_affiliation( + self.core.xmpp, + self.name, + affiliation, + nick=nick_or_jid, + callback=callback, + reason=reason) else: - res = muc.set_user_affiliation(self.core.xmpp, self.name, - affiliation, jid=safeJID(nick_or_jid), - callback=callback, reason=reason) + res = muc.set_user_affiliation( + self.core.xmpp, + self.name, + affiliation, + jid=safeJID(nick_or_jid), + callback=callback, + reason=reason) def change_role(self, nick, role, reason=''): """ Change the role of a nick """ + def callback(iq): if iq['type'] == 'error': - self.core.information("Could not set role '%s' for '%s'." % ( - role, nick), "Warning") + self.core.information("Could not set role '%s' for '%s'." % + (role, nick), "Warning") + 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') + return self.core.information( + 'The role must be one of ' + ', '.join(valid_roles), 'Error') if not safeJID(self.name + '/' + nick): return self.core.information('Invalid nick', 'Info') - muc.set_user_role(self.core.xmpp, self.name, nick, reason, role, - callback=callback) + muc.set_user_role( + self.core.xmpp, self.name, nick, reason, role, callback=callback) @refresh_wrapper.conditional def print_info(self, nick): @@ -228,26 +249,24 @@ class MucTab(ChatTab): inf = '\x19' + dump_tuple(theme.COLOR_INFORMATION_TEXT) + '}' if user.jid: user_jid = '%s (\x19%s}%s\x19o%s)' % ( - inf, - dump_tuple(theme.COLOR_MUC_JID), - user.jid, - inf) + inf, dump_tuple(theme.COLOR_MUC_JID), user.jid, inf) else: user_jid = '' info = ('\x19%(user_col)s}%(nick)s\x19o%(jid)s%(info)s: show: ' '\x19%(show_col)s}%(show)s\x19o%(info)s, affiliation: ' '\x19%(role_col)s}%(affiliation)s\x19o%(info)s, role: ' '\x19%(role_col)s}%(role)s\x19o%(status)s') % { - 'user_col': dump_tuple(user.color), - 'nick': nick, - 'jid': user_jid, - 'info': inf, - 'show_col': dump_tuple(theme.color_show(user.show)), - 'show': user.show or 'Available', - 'role_col': dump_tuple(theme.color_role(user.role)), - 'affiliation': user.affiliation or 'None', - 'role': user.role or 'None', - 'status': '\n%s' % user.status if user.status else ''} + 'user_col': dump_tuple(user.color), + 'nick': nick, + 'jid': user_jid, + 'info': inf, + 'show_col': dump_tuple(theme.color_show(user.show)), + 'show': user.show or 'Available', + 'role_col': dump_tuple(theme.color_role(user.role)), + 'affiliation': user.affiliation or 'None', + 'role': user.role or 'None', + 'status': '\n%s' % user.status if user.status else '' + } self.add_message(info, typ=0) return True @@ -266,21 +285,24 @@ class MucTab(ChatTab): user = self.get_user_by_name(self.topic_from) if user: user_text = dump_tuple(user.color) - user_string = '\x19%s}(set by \x19%s}%s\x19%s})' % ( - info_text, user_text, user.nick, info_text) + user_string = '\x19%s}(set by \x19%s}%s\x19%s})' % (info_text, + user_text, + user.nick, + info_text) else: user_string = self.topic_from else: user_string = '' self._text_buffer.add_message( - "\x19%s}The subject of the room is: \x19%s}%s %s" % - (info_text, norm_text, self.topic, user_string)) + "\x19%s}The subject of the room is: \x19%s}%s %s" % + (info_text, norm_text, self.topic, user_string)) @refresh_wrapper.always def recolor(self, random_colors=False): """Recolor the current MUC users""" - deterministic = config.get_by_tabname('deterministic_nick_colors', self.name) + deterministic = config.get_by_tabname('deterministic_nick_colors', + self.name) if deterministic: for user in self.users: if user is self.own_user: @@ -321,14 +343,16 @@ class MucTab(ChatTab): return False if color == 'unset': if config.remove_and_save(nick, 'muc_colors'): - self.core.information('Color for nick %s unset' % (nick), 'Info') + self.core.information('Color for nick %s unset' % (nick), + 'Info') 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) + 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 @@ -347,9 +371,9 @@ class MucTab(ChatTab): return False self.input.do_command(key, raw=raw) empty_after = self.input.get_text() == '' - empty_after = empty_after or (self.input.get_text().startswith('/') - and not - self.input.get_text().startswith('//')) + empty_after = empty_after or ( + self.input.get_text().startswith('/') + and not self.input.get_text().startswith('//')) self.send_composing_chat_state(empty_after) return False @@ -381,7 +405,8 @@ class MucTab(ChatTab): and not config.get('show_useless_separator')): self.text_win.remove_line_separator() curses.curs_set(1) - if self.joined and config.get_by_tabname('send_chat_states', + if self.joined and config.get_by_tabname( + 'send_chat_states', self.general_jid) and not self.input.get_text(): self.send_chat_state('active') @@ -416,7 +441,8 @@ class MucTab(ChatTab): """ Batch-process all the initial presences """ - deterministic = config.get_by_tabname('deterministic_nick_colors', self.name) + deterministic = config.get_by_tabname('deterministic_nick_colors', + self.name) for stanza in self.presence_buffer: try: @@ -437,10 +463,11 @@ class MucTab(ChatTab): """ Presence received while we are not in the room (before code=110) """ - from_nick, from_room, affiliation, show, status, role, jid, typ = dissect_presence(presence) + from_nick, from_room, affiliation, show, status, role, jid, typ = dissect_presence( + presence) user_color = self.search_for_color(from_nick) - new_user = User(from_nick, affiliation, show, - status, role, jid, deterministic, user_color) + new_user = User(from_nick, affiliation, show, status, role, jid, + deterministic, user_color) self.users.append(new_user) self.core.events.trigger('muc_join', presence, self) if own: @@ -475,44 +502,43 @@ class MucTab(ChatTab): info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) warn_col = dump_tuple(get_theme().COLOR_WARNING_TEXT) spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR) - enable_message = ( - '\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You ' - '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined' - ' the room') % { - 'nick': from_nick, - 'spec': get_theme().CHAR_JOIN, - 'color_spec': spec_col, - 'nick_col': color, - 'info_col': info_col, - } + enable_message = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You ' + '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined' + ' the room') % { + 'nick': from_nick, + 'spec': get_theme().CHAR_JOIN, + 'color_spec': spec_col, + 'nick_col': color, + 'info_col': info_col, + } self.add_message(enable_message, typ=2) self.core.enable_private_tabs(self.name, enable_message) if '201' in status_codes: self.add_message( - '\x19%(info_col)s}Info: The room ' - 'has been created' % - {'info_col': info_col}, + '\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' % - {'info_col': info_col, - 'warn_col': warn_col}, + '\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.' % - {'info_col': info_col, - 'warn_col': warn_col}, + '\x19%(warn_col)s}Warning:\x19%(info_col)s}' + ' This room is not anonymous.' % + {'info_col': info_col, + 'warn_col': warn_col}, typ=0) def handle_presence_joined(self, presence, status_codes): """ Handle new presences when we are already in the room """ - from_nick, from_room, affiliation, show, status, role, jid, typ = dissect_presence(presence) + from_nick, from_room, affiliation, show, status, role, jid, typ = dissect_presence( + presence) change_nick = '303' in status_codes kick = '307' in status_codes and typ == 'unavailable' ban = '301' in status_codes and typ == 'unavailable' @@ -523,8 +549,8 @@ class MucTab(ChatTab): if not user and typ != "unavailable": user_color = self.search_for_color(from_nick) self.core.events.trigger('muc_join', presence, self) - self.on_user_join(from_nick, affiliation, show, status, role, - jid, user_color) + self.on_user_join(from_nick, affiliation, show, status, role, jid, + user_color) elif user is None: log.error('BUG: User %s in %s is None', from_nick, self.name) return @@ -533,13 +559,13 @@ class MucTab(ChatTab): self.on_user_nick_change(presence, user, from_nick, from_room) elif ban: self.core.events.trigger('muc_ban', presence, self) - self.core.on_user_left_private_conversation(from_room, - user, status) + self.core.on_user_left_private_conversation( + from_room, user, status) self.on_user_banned(presence, user, from_nick) elif kick: self.core.events.trigger('muc_kick', presence, self) - self.core.on_user_left_private_conversation(from_room, - user, status) + self.core.on_user_left_private_conversation( + from_room, user, status) self.on_user_kicked(presence, user, from_nick) elif shutdown: self.core.events.trigger('muc_shutdown', presence, self) @@ -549,38 +575,40 @@ class MucTab(ChatTab): self.on_non_member_kicked() # user quit elif typ == 'unavailable': - self.on_user_leave_groupchat(user, jid, status, - from_nick, from_room) + self.on_user_leave_groupchat(user, jid, status, from_nick, + from_room) # status change else: - self.on_user_change_status(user, from_nick, from_room, - affiliation, role, show, status) + self.on_user_change_status(user, from_nick, from_room, affiliation, + role, show, status) 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.' % { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, + '\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() 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.' % { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, + '\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, color): + 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, deterministic, color) + deterministic = config.get_by_tabname('deterministic_nick_colors', + self.name) + user = User(from_nick, affiliation, show, status, role, jid, + deterministic, color) bisect.insort_left(self.users, user) hide_exit_join = config.get_by_tabname('hide_exit_join', self.general_jid) @@ -596,27 +624,30 @@ class MucTab(ChatTab): if not jid.full: msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s' '\x19%(info_col)s} joined the room') % { - 'nick': from_nick, 'spec': char_join, - 'color': color, - 'info_col': info_col, - 'color_spec': spec_col, - } + '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 room') % { - 'spec': char_join, 'nick': from_nick, - 'color':color, 'jid':jid.full, - 'info_col': info_col, - 'jid_color': dump_tuple(get_theme().COLOR_MUC_JID), - 'color_spec': spec_col, - } + 'spec': char_join, + 'nick': from_nick, + 'color': color, + 'jid': jid.full, + 'info_col': info_col, + 'jid_color': dump_tuple(get_theme().COLOR_MUC_JID), + 'color_spec': spec_col, + } self.add_message(msg, typ=2) self.core.on_user_rejoined_private_conversation(self.name, from_nick) def on_user_nick_change(self, presence, user, from_nick, from_room): - new_nick = presence.xml.find('{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER) - ).attrib['nick'] + new_nick = presence.xml.find('{%s}x/{%s}item' % + (NS_MUC_USER, NS_MUC_USER)).attrib['nick'] if user.nick == self.own_nick: self.own_nick = new_nick # also change our nick in all private discussions of this room @@ -624,8 +655,8 @@ class MucTab(ChatTab): else: color = config.get_by_tabname(new_nick, 'muc_colors') if color != '': - deterministic = config.get_by_tabname('deterministic_nick_colors', - self.name) + deterministic = config.get_by_tabname( + 'deterministic_nick_colors', self.name) user.change_color(color, deterministic) user.change_nick(new_nick) self.users.remove(user) @@ -637,11 +668,15 @@ 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' % { - 'old':from_nick, 'new':new_nick, - 'color':color, 'info_col': info_col}, - typ=2) + 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) # rename the private tabs if needed self.core.rename_private_tabs(self.name, from_nick, user) @@ -662,16 +697,20 @@ class MucTab(ChatTab): info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) char_kick = get_theme().CHAR_KICK - if from_nick == self.own_nick: # we are banned + 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') % { - 'spec': char_kick, 'by': by, - 'info_col': info_col} + 'spec': char_kick, + 'by': by, + 'info_col': info_col + } else: kick_msg = ('\x191}%(spec)s \x193}You\x19' '%(info_col)s} have been banned.') % { - 'spec': char_kick, 'info_col': info_col} + 'spec': char_kick, + 'info_col': info_col + } self.core.disable_private_tabs(self.name, reason=kick_msg) self.disconnect() self.refresh_tab_win() @@ -684,11 +723,9 @@ class MucTab(ChatTab): if delay <= 0: muc.join_groupchat(self.core, self.name, self.own_nick) else: - self.core.add_timed_event(timed_events.DelayedEvent( - delay, - muc.join_groupchat, - self.core, - self.name, + self.core.add_timed_event( + timed_events.DelayedEvent(delay, muc.join_groupchat, + self.core, self.name, self.own_nick)) else: @@ -702,18 +739,26 @@ class MucTab(ChatTab): 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} + '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') % { - 'spec': char_kick, 'nick': from_nick, - 'color': color, 'info_col': info_col} + '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}') % { - 'reason': reason.text, 'info_col': info_col} + 'reason': reason.text, + 'info_col': info_col + } self.add_message(kick_msg, typ=2) def on_user_kicked(self, presence, user, from_nick): @@ -730,18 +775,21 @@ class MucTab(ChatTab): char_kick = get_theme().CHAR_KICK if actor_elem is not None: by = actor_elem.get('nick') or actor_elem.get('jid') - if from_nick == self.own_nick: # we are kicked + 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') % { - 'spec': char_kick, 'by': by, - 'info_col': info_col} + 'spec': char_kick, + 'by': by, + 'info_col': info_col + } else: kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}' ' have been kicked.') % { - 'spec': char_kick, - 'info_col': info_col} + 'spec': char_kick, + 'info_col': info_col + } self.core.disable_private_tabs(self.name, reason=kick_msg) self.disconnect() self.refresh_tab_win() @@ -755,12 +803,10 @@ class MucTab(ChatTab): if delay <= 0: muc.join_groupchat(self.core, self.name, self.own_nick) else: - self.core.add_timed_event(timed_events.DelayedEvent( - delay, - muc.join_groupchat, - self.core, - self.name, - self.own_nick)) + self.core.add_timed_event( + timed_events.DelayedEvent(delay, muc.join_groupchat, + self.core, self.name, + self.own_nick)) else: if config.get_by_tabname('display_user_color_in_join_part', self.general_jid): @@ -771,17 +817,26 @@ class MucTab(ChatTab): 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} + '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') % { - 'spec': char_kick, 'nick': from_nick, - 'color':color, 'info_col': info_col} + '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') % { - 'reason': reason.text, 'info_col': info_col} + 'reason': reason.text, + 'info_col': info_col + } self.add_message(kick_msg, typ=2) def on_user_leave_groupchat(self, user, jid, status, from_nick, from_room): @@ -812,35 +867,39 @@ class MucTab(ChatTab): leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}' '%(nick)s\x19%(info_col)s} has left the ' 'room') % { - 'nick':from_nick, 'color':color, - 'spec':get_theme().CHAR_QUIT, - 'info_col': info_col, - 'color_spec': spec_col} + '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 ' 'room') % { - 'spec':get_theme().CHAR_QUIT, - 'nick':from_nick, 'color':color, - 'jid':jid.full, 'info_col': info_col, - 'color_spec': spec_col, - 'jid_col': jid_col} + 'spec': get_theme().CHAR_QUIT, + 'nick': from_nick, + 'color': color, + 'jid': jid.full, + 'info_col': info_col, + 'color_spec': spec_col, + 'jid_col': jid_col + } if status: leave_msg += ' (\x19o%s\x19%s})' % (status, info_col) self.add_message(leave_msg, typ=2) - self.core.on_user_left_private_conversation(from_room, user, - status) + self.core.on_user_left_private_conversation(from_room, user, status) - def on_user_change_status( - self, user, from_nick, from_room, affiliation, role, show, status): + def on_user_change_status(self, user, from_nick, from_room, affiliation, + role, show, status): """ When an user changes her status """ # build the message - display_message = False # flag to know if something significant enough - # to be displayed has changed + display_message = False # flag to know if something significant enough + # to be displayed has changed if config.get_by_tabname('display_user_color_in_join_part', self.general_jid): color = dump_tuple(user.color) @@ -848,12 +907,15 @@ class MucTab(ChatTab): 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} + '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)} + 'nick': from_nick, + 'color': color, + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + } if affiliation != user.affiliation: msg += 'affiliation: %s, ' % affiliation display_message = True @@ -873,7 +935,7 @@ class MucTab(ChatTab): display_message = True if not display_message: return - msg = msg[:-2] # remove the last ", " + msg = msg[:-2] # remove the last ", " hide_status_change = config.get_by_tabname('hide_status_change', self.general_jid) if hide_status_change < -1: @@ -891,8 +953,8 @@ class MucTab(ChatTab): role != user.role): # display the message in the room self._text_buffer.add_message(msg) - self.core.on_user_changed_status_in_private('%s/%s' % - (from_room, from_nick), + self.core.on_user_changed_status_in_private('%s/%s' % (from_room, + from_nick), Status(show, status)) self.users.remove(user) # finally, effectively change the user status @@ -922,7 +984,7 @@ class MucTab(ChatTab): Log the messages in the archives, if it needs to be """ - if time is None and self.joined: # don't log the history messages + 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') @@ -965,32 +1027,44 @@ class MucTab(ChatTab): args['user'] = kwargs['forced_user'] if (not time and nickname and nickname != self.own_nick - and self.state != 'current'): - if (self.state != 'highlight' and - config.get_by_tabname('notify_messages', self.name)): + and self.state != 'current'): + if (self.state != 'highlight' + and config.get_by_tabname('notify_messages', self.name)): self.state = 'message' if time and not txt.startswith('/me'): txt = '\x19%(info_col)s}%(txt)s' % { - 'txt': txt, - 'info_col': dump_tuple(get_theme().COLOR_LOG_MSG)} + 'txt': txt, + 'info_col': dump_tuple(get_theme().COLOR_LOG_MSG) + } elif not nickname: txt = '\x19%(info_col)s}%(txt)s' % { - 'txt': txt, - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)} - elif not kwargs.get('highlight'): # TODO + 'txt': txt, + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + } + elif not kwargs.get('highlight'): # TODO args['highlight'] = self.do_highlight(txt, time, nickname) time = time or datetime.now() self._text_buffer.add_message(txt, time, nickname, **args) return args.get('highlight', False) - def modify_message(self, txt, old_id, new_id, - time=None, nickname=None, user=None, jid=None): + def modify_message(self, + txt, + old_id, + new_id, + time=None, + nickname=None, + user=None, + jid=None): self.log_message(txt, nickname, time=time, typ=1) highlight = self.do_highlight(txt, time, nickname, corrected=True) - message = self._text_buffer.modify_message(txt, old_id, new_id, - highlight=highlight, - time=time, user=user, - jid=jid) + message = self._text_buffer.modify_message( + txt, + old_id, + new_id, + highlight=highlight, + time=time, + user=user, + jid=jid) if message: self.text_win.modify_message(old_id, message) return highlight @@ -1000,11 +1074,13 @@ class MucTab(ChatTab): return [(1, safeJID(self.name).user), (3, self.name)] def enable_self_ping_event(self): - delay = config.get_by_tabname("self_ping_delay", self.general_jid, default=0) - if delay <= 0: # use 0 or some negative value to disable it + delay = config.get_by_tabname( + "self_ping_delay", self.general_jid, default=0) + if delay <= 0: # use 0 or some negative value to disable it return self.disable_self_ping_event() - self.self_ping_event = timed_events.DelayedEvent(delay, self.send_self_ping) + self.self_ping_event = timed_events.DelayedEvent( + delay, self.send_self_ping) self.core.add_timed_event(self.self_ping_event) def disable_self_ping_event(self): @@ -1014,16 +1090,17 @@ class MucTab(ChatTab): def send_self_ping(self): to = self.name + "/" + self.own_nick - self.core.xmpp.plugin['xep_0199'].send_ping(jid=to, - callback=self.on_self_ping_result, - timeout_callback=self.on_self_ping_failed, - timeout=60) + self.core.xmpp.plugin['xep_0199'].send_ping( + jid=to, + callback=self.on_self_ping_result, + timeout_callback=self.on_self_ping_failed, + timeout=60) def on_self_ping_result(self, iq): if iq["type"] == "error" and iq["error"]["condition"] != "feature-not-implemented": self.command_cycle(iq["error"]["text"] or "not in this room") self.core.refresh_window() - else: # Re-send a self-ping in a few seconds + else: # Re-send a self-ping in a few seconds self.enable_self_ping_event() def search_for_color(self, nick): @@ -1035,7 +1112,8 @@ class MucTab(ChatTab): color = config.get_by_tabname(nick, 'muc_colors') if color != '': return color - nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name) + nick_color_aliases = config.get_by_tabname('nick_color_aliases', + self.name) if nick_color_aliases: nick_alias = re.sub('^_*(.*?)_*$', '\\1', nick) color = config.get_by_tabname(nick_alias, 'muc_colors') @@ -1088,26 +1166,23 @@ class MucTab(ChatTab): tab_win_height = Tab.tab_win_height() info_win_height = self.core.information_win_size - - self.user_win.resize(self.height - 3 - info_win_height - - tab_win_height, - self.width - (self.width // 10) * 9 - 1, - 1, - (self.width // 10) * 9 + 1) - self.v_separator.resize(self.height - 3 - info_win_height - tab_win_height, - 1, 1, 9 * (self.width // 10)) + self.user_win.resize( + self.height - 3 - info_win_height - tab_win_height, self.width - + (self.width // 10) * 9 - 1, 1, (self.width // 10) * 9 + 1) + self.v_separator.resize( + self.height - 3 - info_win_height - tab_win_height, 1, 1, + 9 * (self.width // 10)) self.topic_win.resize(1, self.width, 0, 0) - self.text_win.resize(self.height - 3 - info_win_height - - tab_win_height, - text_width, 1, 0) + self.text_win.resize( + self.height - 3 - info_win_height - tab_win_height, text_width, 1, + 0) self.text_win.rebuild_everything(self._text_buffer) - self.info_header.resize(1, self.width, - self.height - 2 - info_win_height - - tab_win_height, - 0) - self.input.resize(1, self.width, self.height-1, 0) + self.info_header.resize( + 1, self.width, self.height - 2 - info_win_height - tab_win_height, + 0) + self.input.resize(1, self.width, self.height - 1, 0) def refresh(self): if self.need_resize: @@ -1131,40 +1206,40 @@ class MucTab(ChatTab): self.input.refresh() def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: + if self.core.information_win_size >= self.height - 3: return if config.get("hide_user_list"): text_width = self.width else: - text_width = (self.width//10)*9 - self.user_win.resize(self.height - 3 - self.core.information_win_size - - Tab.tab_win_height(), - self.width - (self.width // 10) * 9 - 1, - 1, + text_width = (self.width // 10) * 9 + self.user_win.resize(self.height - 3 - self.core.information_win_size - + Tab.tab_win_height(), + self.width - (self.width // 10) * 9 - 1, 1, (self.width // 10) * 9 + 1) - self.v_separator.resize(self.height - 3 - self.core.information_win_size - Tab.tab_win_height(), - 1, 1, 9 * (self.width // 10)) - self.text_win.resize(self.height - 3 - self.core.information_win_size - - Tab.tab_win_height(), - text_width, 1, 0) - self.info_header.resize(1, self.width, - self.height-2-self.core.information_win_size - - Tab.tab_win_height(), - 0) + self.v_separator.resize( + self.height - 3 - self.core.information_win_size - + Tab.tab_win_height(), 1, 1, 9 * (self.width // 10)) + self.text_win.resize(self.height - 3 - self.core.information_win_size - + Tab.tab_win_height(), text_width, 1, 0) + self.info_header.resize( + 1, self.width, self.height - 2 - self.core.information_win_size - + Tab.tab_win_height(), 0) + def do_highlight(self, txt, time, nickname, corrected=False): """ Set the tab color and returns the nick color """ highlighted = False - if (not time or corrected) and nickname and nickname != self.own_nick and self.joined: + if (not time or corrected + ) and nickname and nickname != self.own_nick and self.joined: if re.search(r'\b' + self.own_nick.lower() + r'\b', txt.lower()): if self.state != 'current': self.state = 'highlight' highlighted = True else: - highlight_words = config.get_by_tabname('highlight_on', - self.general_jid) + highlight_words = config.get_by_tabname( + 'highlight_on', self.general_jid) highlight_words = highlight_words.split(':') for word in highlight_words: if word and word.lower() in txt.lower(): @@ -1205,11 +1280,11 @@ class MucTab(ChatTab): """ /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) @@ -1254,17 +1329,18 @@ class MucTab(ChatTab): """ /version <jid or nick> """ + def callback(res): if not res: return self.core.information('Could not get the software ' - 'version from %s' % (jid,), + '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') + jid, 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 args is None: return self.core.command.help('version') nick = args[0] @@ -1273,8 +1349,7 @@ class MucTab(ChatTab): jid = safeJID(jid + '/' + nick) else: jid = safeJID(nick) - fixes.get_version(self.core.xmpp, jid, - callback=callback) + fixes.get_version(self.core.xmpp, jid, callback=callback) @command_args_parser.quoted(1) def command_nick(self, args): @@ -1290,8 +1365,7 @@ class MucTab(ChatTab): current_status = self.core.get_status() if not safeJID(self.name + '/' + nick): return self.core.information('Invalid nick', 'Info') - muc.change_nick(self.core, self.name, nick, - current_status.message, + muc.change_nick(self.core, self.name, nick, current_status.message, current_status.show) @command_args_parser.quoted(0, 1, ['']) @@ -1323,7 +1397,7 @@ class MucTab(ChatTab): /query <nick> [message] """ if args is None: - return self.core.command.help('query') + return self.core.command.help('query') nick = args[0] r = None for user in self.users: @@ -1332,7 +1406,7 @@ class MucTab(ChatTab): if r and len(args) == 2: msg = args[1] self.core.current_tab().command_say( - xhtml.convert_simple_to_full_colors(msg)) + xhtml.convert_simple_to_full_colors(msg)) if not r: self.core.information("Cannot find user: %s" % nick, 'Error') @@ -1355,11 +1429,11 @@ class MucTab(ChatTab): return aff = { - 'owner': get_theme().CHAR_AFFILIATION_OWNER, - 'admin': get_theme().CHAR_AFFILIATION_ADMIN, - 'member': get_theme().CHAR_AFFILIATION_MEMBER, - 'none': get_theme().CHAR_AFFILIATION_NONE, - } + 'owner': get_theme().CHAR_AFFILIATION_OWNER, + 'admin': get_theme().CHAR_AFFILIATION_ADMIN, + 'member': get_theme().CHAR_AFFILIATION_MEMBER, + 'none': get_theme().CHAR_AFFILIATION_NONE, + } colors = {} colors["visitor"] = dump_tuple(get_theme().COLOR_USER_VISITOR) @@ -1372,8 +1446,9 @@ class MucTab(ChatTab): affiliation = aff.get(user.affiliation, get_theme().CHAR_AFFILIATION_NONE) color = colors.get(user.role, color_other) - buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % ( - color, affiliation, dump_tuple(user.color), user.nick)) + buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % + (color, affiliation, dump_tuple(user.color), + user.nick)) buff.append('\n') message = ' '.join(buff) @@ -1414,6 +1489,7 @@ class MucTab(ChatTab): Changes the role of an user roles can be: none, visitor, participant, moderator """ + def callback(iq): if iq['type'] == 'error': self.core.room_error(iq, self.name) @@ -1431,6 +1507,7 @@ class MucTab(ChatTab): Changes the affiliation of an user affiliations can be: outcast, none, member, admin, owner """ + def callback(iq): if iq['type'] == 'error': self.core.room_error(iq, self.name) @@ -1542,9 +1619,8 @@ class MucTab(ChatTab): after = config.get('after_completion') + ' ' input_pos = self.input.pos if ' ' not in self.input.get_text()[:input_pos] or ( - self.input.last_completion and - self.input.get_text()[:input_pos] == - self.input.last_completion + after): + self.input.last_completion and self.input.get_text() + [:input_pos] == self.input.last_completion + after): add_after = after else: if not config.get('add_space_after_completion'): @@ -1553,9 +1629,9 @@ class MucTab(ChatTab): add_after = ' ' self.input.auto_completion(word_list, add_after, quotify=False) empty_after = self.input.get_text() == '' - empty_after = empty_after or (self.input.get_text().startswith('/') - and not - self.input.get_text().startswith('//')) + empty_after = empty_after or ( + self.input.get_text().startswith('/') + and not self.input.get_text().startswith('//')) self.send_composing_chat_state(empty_after) def completion_version(self, the_input): @@ -1584,15 +1660,18 @@ class MucTab(ChatTab): def completion_nick(self, the_input): """Completion for /nick""" - nicks = [os.environ.get('USER'), - config.get('default_nick'), - self.core.get_bookmark_nickname(self.name)] + nicks = [ + os.environ.get('USER'), + config.get('default_nick'), + self.core.get_bookmark_nickname(self.name) + ] nicks = [i for i in nicks if i] return Completion(the_input.auto_completion, nicks, '', quotify=False) def completion_recolor(self, the_input): if the_input.get_argument_position() == 1: - return Completion(the_input.new_completion, ['random'], 1, '', quotify=False) + return Completion( + the_input.new_completion, ['random'], 1, '', quotify=False) return True def completion_color(self, the_input): @@ -1602,13 +1681,15 @@ class MucTab(ChatTab): userlist = [user.nick for user in self.users] if self.own_nick in userlist: userlist.remove(self.own_nick) - return Completion(the_input.new_completion, userlist, 1, '', quotify=True) + return Completion( + 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 Completion(the_input.new_completion, colors, 2, '', quotify=False) + return Completion( + the_input.new_completion, colors, 2, '', quotify=False) def completion_ignore(self, the_input): """Completion for /ignore""" @@ -1625,11 +1706,12 @@ class MucTab(ChatTab): userlist = [user.nick for user in self.users] if self.own_nick in userlist: userlist.remove(self.own_nick) - return Completion(the_input.new_completion, userlist, 1, '', quotify=True) + return Completion( + the_input.new_completion, userlist, 1, '', quotify=True) elif n == 2: possible_roles = ['none', 'visitor', 'participant', 'moderator'] - return Completion(the_input.new_completion, possible_roles, 2, '', - quotify=True) + return Completion( + the_input.new_completion, possible_roles, 2, '', quotify=True) def completion_affiliation(self, the_input): """Completion for /affiliation""" @@ -1642,22 +1724,30 @@ class MucTab(ChatTab): if self.core.xmpp.boundjid.bare in jidlist: jidlist.remove(self.core.xmpp.boundjid.bare) userlist.extend(jidlist) - return Completion(the_input.new_completion, userlist, 1, '', quotify=True) + return Completion( + the_input.new_completion, userlist, 1, '', quotify=True) elif n == 2: - possible_affiliations = ['none', 'member', 'admin', - 'owner', 'outcast'] - return Completion(the_input.new_completion, possible_affiliations, 2, '', - quotify=True) + possible_affiliations = [ + 'none', 'member', 'admin', 'owner', 'outcast' + ] + return Completion( + the_input.new_completion, + possible_affiliations, + 2, + '', + quotify=True) def completion_invite(self, the_input): """Completion for /invite""" n = the_input.get_argument_position(quoted=True) if n == 1: - return Completion(the_input.new_completion, roster.jids(), 1, quotify=True) + return Completion( + the_input.new_completion, roster.jids(), 1, quotify=True) def completion_topic(self, the_input): if the_input.get_argument_position() == 1: - return Completion(the_input.auto_completion, [self.topic], '', quotify=False) + return Completion( + the_input.auto_completion, [self.topic], '', quotify=False) def completion_quoted(self, the_input): """Nick completion, but with quotes""" @@ -1668,13 +1758,15 @@ class MucTab(ChatTab): if user.nick != self.own_nick: word_list.append(user.nick) - return Completion(the_input.new_completion, word_list, 1, quotify=True) + return Completion( + the_input.new_completion, word_list, 1, quotify=True) def completion_unignore(self, the_input): if the_input.get_argument_position() == 1: users = [user.nick for user in self.ignores] return Completion(the_input.auto_completion, users, quotify=False) + ########################## REGISTER STUFF ############################## def register_keys(self): @@ -1687,183 +1779,261 @@ class MucTab(ChatTab): def register_commands(self): "Register tab-specific commands" - self.register_commands_batch([ - { - 'name': 'ignore', - 'func': self.command_ignore, - 'usage': '<nickname>', - 'desc': 'Ignore a specified nickname.', - 'shortdesc': 'Ignore someone', - 'completion': self.completion_unignore - }, - { - 'name': 'unignore', - 'func': self.command_unignore, - 'usage': '<nickname>', - 'desc': 'Remove the specified nickname from the ignore list.', - 'shortdesc': 'Unignore someone.', - 'completion': self.completion_unignore - }, - { - 'name': 'kick', - 'func': self.command_kick, - '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 - }, - { - 'name': 'ban', - 'func': self.command_ban, - '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 - }, - { - 'name': 'role', - 'func': 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.', - 'completion': self.completion_role - }, - { - 'name': 'affiliation', - 'func': 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.', - 'completion': self.completion_affiliation - }, - { - 'name': 'topic', - 'func': self.command_topic, - 'usage': '<subject>', - 'desc': 'Change the subject of the room.', - 'shortdesc': 'Change the subject.', - 'completion': self.completion_topic - }, - { - 'name': 'subject', - 'func': self.command_topic, - 'usage': '<subject>', - 'desc': 'Change the subject of the room.', - 'shortdesc': 'Change the subject.', - 'completion': self.completion_topic - }, - { - 'name': 'query', - 'func': 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 a user.', - 'completion': self.completion_quoted - }, - { - 'name': 'part', - 'func': self.command_part, - 'usage': '[message]', - 'desc': ('Disconnect from a room. You can' - ' specify an optional message.'), - 'shortdesc': 'Leave the room.' - }, - { - 'name': 'close', - 'func': 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.' - }, - { - 'name': 'nick', - 'func': self.command_nick, - 'usage': '<nickname>', - 'desc': 'Change your nickname in the current room.', - 'shortdesc': 'Change your nickname.', - 'completion': self.completion_nick - }, - { - 'name':'recolor', - 'func': 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.', - 'completion': self.completion_recolor - }, - { - 'name': 'color', - 'func': 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_recolor - }, - { - 'name': 'cycle', - 'func': self.command_cycle, - 'usage': '[message]', - 'desc': 'Leave the current room and rejoin it immediately.', - 'shortdesc': 'Leave and re-join the room.' - }, - { - 'name': 'info', - 'func': 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.', - 'completion': self.completion_info - }, - { - 'name': 'configure', - 'func': self.command_configure, - 'desc': 'Configure the current room, through a form.', - 'shortdesc': 'Configure the room.' - }, - { - 'name': 'version', - 'func': 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.', - 'completion': self.completion_version - }, - { - 'name': 'names', - 'func': self.command_names, - 'desc': 'Get the users in the room with their roles.', - 'shortdesc': 'List the users.' - }, - { - 'name': 'invite', - 'func': self.command_invite, - 'desc': 'Invite a contact to this room', - 'usage': '<jid> [reason]', - 'shortdesc': 'Invite a contact to this room', - 'completion': self.completion_invite - } - ]) + self.register_commands_batch([{ + 'name': 'ignore', + 'func': self.command_ignore, + 'usage': '<nickname>', + 'desc': 'Ignore a specified nickname.', + 'shortdesc': 'Ignore someone', + 'completion': self.completion_unignore + }, { + 'name': + 'unignore', + 'func': + self.command_unignore, + 'usage': + '<nickname>', + 'desc': + 'Remove the specified nickname from the ignore list.', + 'shortdesc': + 'Unignore someone.', + 'completion': + self.completion_unignore + }, { + 'name': + 'kick', + 'func': + self.command_kick, + '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 + }, { + 'name': + 'ban', + 'func': + self.command_ban, + '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 + }, { + 'name': + 'role', + 'func': + 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.', + 'completion': + self.completion_role + }, { + 'name': + 'affiliation', + 'func': + 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.', + 'completion': + self.completion_affiliation + }, { + 'name': + 'topic', + 'func': + self.command_topic, + 'usage': + '<subject>', + 'desc': + 'Change the subject of the room.', + 'shortdesc': + 'Change the subject.', + 'completion': + self.completion_topic + }, { + 'name': + 'subject', + 'func': + self.command_topic, + 'usage': + '<subject>', + 'desc': + 'Change the subject of the room.', + 'shortdesc': + 'Change the subject.', + 'completion': + self.completion_topic + }, { + 'name': + 'query', + 'func': + 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 a user.', + 'completion': + self.completion_quoted + }, { + 'name': + 'part', + 'func': + self.command_part, + 'usage': + '[message]', + 'desc': ('Disconnect from a room. You can' + ' specify an optional message.'), + 'shortdesc': + 'Leave the room.' + }, { + 'name': + 'close', + 'func': + 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.' + }, { + 'name': + 'nick', + 'func': + self.command_nick, + 'usage': + '<nickname>', + 'desc': + 'Change your nickname in the current room.', + 'shortdesc': + 'Change your nickname.', + 'completion': + self.completion_nick + }, { + 'name': + 'recolor', + 'func': + 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.', + 'completion': + self.completion_recolor + }, { + 'name': + 'color', + 'func': + 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_recolor + }, { + 'name': + 'cycle', + 'func': + self.command_cycle, + 'usage': + '[message]', + 'desc': + 'Leave the current room and rejoin it immediately.', + 'shortdesc': + 'Leave and re-join the room.' + }, { + 'name': + 'info', + 'func': + 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.', + 'completion': + self.completion_info + }, { + 'name': + 'configure', + 'func': + self.command_configure, + 'desc': + 'Configure the current room, through a form.', + 'shortdesc': + 'Configure the room.' + }, { + 'name': + 'version', + 'func': + 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.', + 'completion': + self.completion_version + }, { + 'name': + 'names', + 'func': + self.command_names, + 'desc': + 'Get the users in the room with their roles.', + 'shortdesc': + 'List the users.' + }, { + 'name': + 'invite', + 'func': + self.command_invite, + 'desc': + 'Invite a contact to this room', + 'usage': + '<jid> [reason]', + 'shortdesc': + 'Invite a contact to this room', + 'completion': + self.completion_invite + }]) + + +class PresenceError(Exception): + pass -class PresenceError(Exception): pass def dissect_presence(presence): """ diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py index ec3ed74a..735522e1 100644 --- a/poezio/tabs/privatetab.py +++ b/poezio/tabs/privatetab.py @@ -27,6 +27,7 @@ from poezio.logger import logger from poezio.theming import get_theme, dump_tuple from poezio.decorators import command_args_parser + class PrivateTab(OneToOneTab): """ The tab containg a private conversation (someone from a MUC) @@ -35,6 +36,7 @@ class PrivateTab(OneToOneTab): plugin_commands = {} additional_information = {} plugin_keys = {} + def __init__(self, core, name, nick): OneToOneTab.__init__(self, core, name) self.own_nick = nick @@ -46,12 +48,18 @@ class PrivateTab(OneToOneTab): # keys 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.') - 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.') + 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.') + 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.') self.resize() self.parent_muc = self.core.get_tab_by_name(safeJID(name).bare, MucTab) self.on = True @@ -61,7 +69,7 @@ class PrivateTab(OneToOneTab): def remote_user_color(self): user = self.parent_muc.get_user_by_name(safeJID(self.name).resource) if user: - return dump_tuple(user.color); + return dump_tuple(user.color) return super().remote_user_color() @property @@ -87,14 +95,16 @@ class PrivateTab(OneToOneTab): del PrivateTab.additional_information[plugin_name] def load_logs(self, log_nb): - logs = logger.get_logs(safeJID(self.name).full.replace('/', '\\'), 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): + if not logger.log_message( + self.name, nickname, txt, date=time, typ=typ): self.core.information('Unable to write in the log file', 'Error') def on_close(self): @@ -120,7 +130,9 @@ class PrivateTab(OneToOneTab): else: add_after = '' self.input.auto_completion(word_list, add_after, quotify=False) - empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) + 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 @@ -145,8 +157,13 @@ class PrivateTab(OneToOneTab): msg['replace']['id'] = self.last_sent_message['id'] if config.get_by_tabname('group_corrections', self.name): try: - self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'], - user=user, jid=self.core.xmpp.boundjid, nickname=self.own_nick) + self.modify_message( + msg['body'], + self.last_sent_message['id'], + msg['id'], + user=user, + jid=self.core.xmpp.boundjid, + nickname=self.own_nick) replaced = True except: log.error('Unable to correct a message', exc_info=True) @@ -157,8 +174,8 @@ class PrivateTab(OneToOneTab): msg.enable('html') msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) msg['body'] = xhtml.clean_text(msg['body']) - if (config.get_by_tabname('send_chat_states', self.general_jid) and - self.remote_wants_chatstates is not False): + if (config.get_by_tabname('send_chat_states', self.general_jid) + and self.remote_wants_chatstates is not False): needed = 'inactive' if self.inactive else 'active' msg['chat_state'] = needed if attention and self.remote_supports_attention: @@ -170,13 +187,14 @@ class PrivateTab(OneToOneTab): self.input.refresh() return if not replaced: - self.add_message(msg['body'], - nickname=self.own_nick or self.core.own_nick, - forced_user=user, - nick_color=get_theme().COLOR_OWN_NICK, - identifier=msg['id'], - jid=self.core.xmpp.boundjid, - typ=1) + self.add_message( + msg['body'], + nickname=self.own_nick or self.core.own_nick, + forced_user=user, + nick_color=get_theme().COLOR_OWN_NICK, + identifier=msg['id'], + jid=self.core.xmpp.boundjid, + typ=1) self.last_sent_message = msg if self.remote_supports_receipts: @@ -191,19 +209,22 @@ class PrivateTab(OneToOneTab): """ /version """ + 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') + 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') self.core.information(version, 'Info') + if args: return self.core.command.version(args[0]) jid = safeJID(self.name) - fixes.get_version(self.core.xmpp, jid, - callback=callback) + fixes.get_version(self.core.xmpp, jid, callback=callback) @command_args_parser.quoted(0, 1) def command_info(self, arg): @@ -226,14 +247,14 @@ class PrivateTab(OneToOneTab): info_win_height = self.core.information_win_size tab_win_height = Tab.tab_win_height() - self.text_win.resize(self.height - 2 - info_win_height - tab_win_height, - self.width, 0, 0) + self.text_win.resize( + self.height - 2 - info_win_height - tab_win_height, self.width, 0, + 0) self.text_win.rebuild_everything(self._text_buffer) - self.info_header.resize(1, self.width, - self.height - 2 - info_win_height - - tab_win_height, - 0) - self.input.resize(1, self.width, self.height-1, 0) + self.info_header.resize( + 1, self.width, self.height - 2 - info_win_height - tab_win_height, + 0) + self.input.resize(1, self.width, self.height - 1, 0) def refresh(self): if self.need_resize: @@ -251,7 +272,8 @@ class PrivateTab(OneToOneTab): self.input.refresh() def refresh_info_header(self): - self.info_header.refresh(self.name, self.text_win, self.chatstate, PrivateTab.additional_information) + self.info_header.refresh(self.name, self.text_win, self.chatstate, + PrivateTab.additional_information) self.input.refresh() def get_nick(self): @@ -264,7 +286,9 @@ class PrivateTab(OneToOneTab): self.input.do_command(key, raw=raw) if not self.on: return False - empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) + empty_after = self.input.get_text() == '' or ( + self.input.get_text().startswith('/') + and not self.input.get_text().startswith('//')) tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) if tab and tab.joined: self.send_composing_chat_state(empty_after) @@ -279,8 +303,8 @@ class PrivateTab(OneToOneTab): self.text_win.remove_line_separator() self.text_win.add_line_separator(self._text_buffer) tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) - if tab and tab.joined and config.get_by_tabname('send_chat_states', - self.general_jid) and self.on: + if tab and tab.joined and config.get_by_tabname( + 'send_chat_states', self.general_jid) and self.on: self.send_chat_state('inactive') self.check_scrolled() @@ -288,15 +312,20 @@ class PrivateTab(OneToOneTab): self.state = 'current' curses.curs_set(1) tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) - if tab and tab.joined and config.get_by_tabname('send_chat_states', - self.general_jid,) and not self.input.get_text() and self.on: + if tab and tab.joined and config.get_by_tabname( + 'send_chat_states', + self.general_jid, + ) and not self.input.get_text() and self.on: self.send_chat_state('active') def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: + if self.core.information_win_size >= self.height - 3: return - self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0) - self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) + self.text_win.resize(self.height - 2 - self.core.information_win_size - + Tab.tab_win_height(), self.width, 0, 0) + self.info_header.resize( + 1, self.width, self.height - 2 - self.core.information_win_size - + Tab.tab_win_height(), 0) def get_text_window(self): return self.text_win @@ -307,13 +336,16 @@ class PrivateTab(OneToOneTab): The user changed her nick in the corresponding muc: update the tab’s name and display a message. """ - self.add_message('\x19%(nick_col)s}%(old)s\x19%(info_col)s} is now ' - 'known as \x19%(nick_col)s}%(new)s' % { - 'old':old_nick, 'new': user.nick, - 'nick_col': dump_tuple(user.color), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - new_jid = safeJID(self.name).bare+'/'+user.nick + self.add_message( + '\x19%(nick_col)s}%(old)s\x19%(info_col)s} is now ' + 'known as \x19%(nick_col)s}%(new)s' % { + 'old': old_nick, + 'new': user.nick, + 'nick_col': dump_tuple(user.color), + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) + new_jid = safeJID(self.name).bare + '/' + user.nick self.name = new_jid return self.core.current_tab() is self @@ -323,28 +355,36 @@ class PrivateTab(OneToOneTab): The user left the associated MUC """ self.deactivate() - if config.get_by_tabname('display_user_color_in_join_part', self.general_jid): + if config.get_by_tabname('display_user_color_in_join_part', + self.general_jid): color = dump_tuple(user.color) else: color = dump_tuple(get_theme().COLOR_REMOTE_USER) if not status_message: - self.add_message('\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' - '%(nick)s\x19%(info_col)s} has left the room' % { - 'nick': user.nick, 'spec': get_theme().CHAR_QUIT, - 'nick_col': color, - 'quit_col': dump_tuple(get_theme().COLOR_QUIT_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + self.add_message( + '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' + '%(nick)s\x19%(info_col)s} has left the room' % { + 'nick': user.nick, + 'spec': get_theme().CHAR_QUIT, + 'nick_col': color, + 'quit_col': dump_tuple(get_theme().COLOR_QUIT_CHAR), + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) else: - self.add_message('\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' - '%(nick)s\x19%(info_col)s} has left the room' - ' (%(status)s)' % { 'status': status_message, - 'nick': user.nick, 'spec': get_theme().CHAR_QUIT, - 'nick_col': color, - 'quit_col': dump_tuple(get_theme().COLOR_QUIT_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + self.add_message( + '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' + '%(nick)s\x19%(info_col)s} has left the room' + ' (%(status)s)' % { + 'status': status_message, + 'nick': user.nick, + 'spec': get_theme().CHAR_QUIT, + 'nick_col': color, + 'quit_col': dump_tuple(get_theme().COLOR_QUIT_CHAR), + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) return self.core.current_tab() is self @refresh_wrapper.conditional @@ -361,12 +401,16 @@ class PrivateTab(OneToOneTab): user = tab.get_user_by_name(nick) if user: color = dump_tuple(user.color) - self.add_message('\x19%(join_col)s}%(spec)s \x19%(color)s}%(nick)s\x19' - '%(info_col)s} joined the room' % {'nick':nick, - 'color': color, 'spec':get_theme().CHAR_JOIN, - 'join_col': dump_tuple(get_theme().COLOR_JOIN_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) + self.add_message( + '\x19%(join_col)s}%(spec)s \x19%(color)s}%(nick)s\x19' + '%(info_col)s} joined the room' % { + 'nick': nick, + 'color': color, + 'spec': get_theme().CHAR_JOIN, + 'join_col': dump_tuple(get_theme().COLOR_JOIN_CHAR), + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + }, + typ=2) return self.core.current_tab() is self def activate(self, reason=None): @@ -386,7 +430,10 @@ class PrivateTab(OneToOneTab): def add_error(self, error_message): error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK), error_message) - self.add_message(error, highlight=True, nickname='Error', - nick_color=get_theme().COLOR_ERROR_MSG, typ=2) + self.add_message( + error, + highlight=True, + nickname='Error', + nick_color=get_theme().COLOR_ERROR_MSG, + typ=2) self.core.refresh_window() - diff --git a/poezio/tabs/rostertab.py b/poezio/tabs/rostertab.py index 731120be..c079d550 100644 --- a/poezio/tabs/rostertab.py +++ b/poezio/tabs/rostertab.py @@ -36,6 +36,7 @@ class RosterInfoTab(Tab): """ plugin_commands = {} plugin_keys = {} + def __init__(self, core): Tab.__init__(self, core) self.name = "Roster" @@ -45,7 +46,8 @@ class RosterInfoTab(Tab): self.roster_win = windows.RosterWin() self.contact_info_win = windows.ContactInfoWin() self.avatar_win = windows.ImageWin() - self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show") + self.default_help_message = windows.HelpText( + "Enter commands with “/”. “o”: toggle offline show") self.input = self.default_help_message self.state = 'normal' self.key_func['^I'] = self.completion @@ -68,82 +70,110 @@ class RosterInfoTab(Tab): 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 a 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 a 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 them to' - ' allow you to see his presence, and allow them to' - ' see your presence.', - shortdesc='Add a 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>]|<group>', - desc='Add the given JID or selected line to the given group.', - shortdesc='Add a 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 a 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 a 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 a 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.') - self.register_command('disconnect', self.command_disconnect, - desc='Disconnect from the remote server.', - shortdesc='Disconnect from the server.') - self.register_command('clear', self.command_clear, - shortdesc='Clear the info buffer.') - self.register_command('last_activity', self.command_last_activity, + 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 a 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 a user your presence.', + completion=self.completion_deny) + self.register_command( + 'add', + self.command_add, usage='<jid>', - desc='Informs you of the last activity of a JID.', - shortdesc='Get the activity of someone.', - completion=self.core.completion.last_activity) + desc='Add the specified JID to your roster, ask them to' + ' allow you to see his presence, and allow them to' + ' see your presence.', + shortdesc='Add a 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>]|<group>', + desc='Add the given JID or selected line to the given group.', + shortdesc='Add a 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 a 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 a 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 a 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.') + self.register_command( + 'disconnect', + self.command_disconnect, + desc='Disconnect from the remote server.', + shortdesc='Disconnect from the server.') + self.register_command( + 'clear', self.command_clear, 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.', + completion=self.core.completion.last_activity) self.resize() self.update_commands() @@ -151,54 +181,72 @@ class RosterInfoTab(Tab): def check_blocking(self, 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.', - completion=self.completion_block) - self.register_command('unblock', self.command_unblock, - 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.') - self.core.xmpp.del_event_handler('session_start', self.check_blocking) - self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message) + self.register_command( + 'block', + self.command_block, + 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.', + completion=self.completion_unblock) + self.register_command( + 'list_blocks', + self.command_list_blocks, + 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) + 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) @property def selected_row(self): @@ -209,10 +257,11 @@ class RosterInfoTab(Tab): """ /certs """ + def cb(iq): if iq['type'] == 'error': - self.core.information('Unable to retrieve the certificate list.', - 'Error') + self.core.information( + 'Unable to retrieve the certificate list.', 'Error') return certs = [] for item in iq['sasl_certs']['items']: @@ -222,7 +271,8 @@ class RosterInfoTab(Tab): 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)) + 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) @@ -234,9 +284,11 @@ class RosterInfoTab(Tab): """ 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') + self.core.information('Unable to add the certificate.', + 'Error') else: self.core.information('Certificate added.', 'Info') @@ -245,9 +297,11 @@ class RosterInfoTab(Tab): try: with open(args[1]) as fd: crt = fd.read() - crt = crt.replace(ssl.PEM_FOOTER, '').replace(ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '') + 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') + self.core.information('Unable to read the certificate: %s' % e, + 'Error') return if len(args) > 2: @@ -263,8 +317,8 @@ class RosterInfoTab(Tab): else: management = True - self.core.xmpp.plugin['xep_0257'].add_cert(name, crt, callback=cb, - allow_management=management) + self.core.xmpp.plugin['xep_0257'].add_cert( + name, crt, callback=cb, allow_management=management) def completion_cert_add(self, the_input): """ @@ -286,9 +340,11 @@ class RosterInfoTab(Tab): """ 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') + self.core.information('Unable to disable the certificate.', + 'Error') else: self.core.information('Certificate disabled.', 'Info') @@ -303,9 +359,11 @@ class RosterInfoTab(Tab): """ 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') + self.core.information('Unable to revoke the certificate.', + 'Error') else: self.core.information('Certificate revoked.', 'Info') @@ -313,7 +371,6 @@ class RosterInfoTab(Tab): self.core.xmpp.plugin['xep_0257'].revoke_cert(name, callback=cb) - @command_args_parser.quoted(2) def command_cert_fetch(self, args): """ @@ -321,6 +378,7 @@ class RosterInfoTab(Tab): """ 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.', @@ -364,11 +422,12 @@ class RosterInfoTab(Tab): """ tab = self.core.get_conversation_by_jid(message['from'], False) if not tab: - log.debug('Received message from nonexistent tab: %s', message['from']) + log.debug('Received message from nonexistent tab: %s', + message['from']) message = '\x19%(info_col)s}Cannot send message to %(jid)s: contact blocked' % { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), - 'jid': message['from'], - } + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), + 'jid': message['from'], + } tab.add_message(message) @command_args_parser.quoted(0, 1) @@ -386,7 +445,8 @@ class RosterInfoTab(Tab): def callback(iq): if iq['type'] == 'error': - return self.core.information('Could not block %s.' % jid, 'Error') + return self.core.information('Could not block %s.' % jid, + 'Error') elif iq['type'] == 'result': return self.core.information('Blocked %s.' % jid, 'Info') @@ -398,16 +458,19 @@ class RosterInfoTab(Tab): """ if the_input.get_argument_position() == 1: jids = roster.jids() - return Completion(the_input.new_completion, jids, 1, '', quotify=False) + return Completion( + the_input.new_completion, jids, 1, '', quotify=False) @command_args_parser.quoted(0, 1) def command_unblock(self, args): """ /unblock [jid] """ + def callback(iq): if iq['type'] == 'error': - return self.core.information('Could not unblock the contact.', 'Error') + return self.core.information('Could not unblock the contact.', + 'Error') elif iq['type'] == 'result': return self.core.information('Contact unblocked.', 'Info') @@ -424,6 +487,7 @@ class RosterInfoTab(Tab): """ Completion for /unblock """ + def on_result(iq): if iq['type'] == 'error': return @@ -439,9 +503,11 @@ class RosterInfoTab(Tab): """ /list_blocks """ + def callback(iq): if iq['type'] == 'error': - return self.core.information('Could not retrieve the blocklist.', 'Error') + return self.core.information( + 'Could not retrieve the blocklist.', 'Error') s = 'List of blocked JIDs:\n' items = (str(item) for item in iq['blocklist']['items']) jids = '\n'.join(items) @@ -508,25 +574,23 @@ class RosterInfoTab(Tab): info_width = self.width - roster_width - 1 if display_info: - self.v_separator.resize(self.height - 1 - tab_win_height, - 1, 0, roster_width) - self.information_win.resize(self.height - 1 - tab_win_height - - contact_win_h, - info_width, 0, roster_width + 1, - self.core.information_buffer) + self.v_separator.resize(self.height - 1 - tab_win_height, 1, 0, + roster_width) + self.information_win.resize( + self.height - 1 - tab_win_height - contact_win_h, info_width, + 0, roster_width + 1, self.core.information_buffer) if display_contact_win: y = self.height - tab_win_height - contact_win_h - 1 avatar_width = contact_win_h * 2 self.contact_info_win.resize(contact_win_h, - info_width - avatar_width, - y, roster_width + 1) - self.avatar_win.resize(contact_win_h, - avatar_width, - y, self.width - avatar_width) + info_width - avatar_width, y, + roster_width + 1) + self.avatar_win.resize(contact_win_h, avatar_width, y, + self.width - avatar_width) self.roster_win.resize(self.height - 1 - Tab.tab_win_height(), roster_width, 0, 0) - self.input.resize(1, self.width, self.height-1, 0) - self.default_help_message.resize(1, self.width, self.height-1, 0) + self.input.resize(1, self.width, self.height - 1, 0) + self.default_help_message.resize(1, self.width, self.height - 1, 0) def completion(self): # Check if we are entering a command (with the '/' key) @@ -544,9 +608,10 @@ class RosterInfoTab(Tab): args = shell_split(text) n = the_input.get_argument_position() if n == complete_number: - if args[n-1] == '' or len(args) < n+1: + if args[n - 1] == '' or len(args) < n + 1: home = os.getenv('HOME') or '/' - return Completion(the_input.new_completion, [home, '/tmp'], n, quotify=True) + return Completion( + the_input.new_completion, [home, '/tmp'], n, quotify=True) path_ = args[n] if path.isdir(path_): dir_ = path_ @@ -569,7 +634,8 @@ class RosterInfoTab(Tab): if not name.startswith('.'): end_list.append(value) - return Completion(the_input.new_completion, end_list, n, quotify=True) + return Completion( + the_input.new_completion, end_list, n, quotify=True) @command_args_parser.ignored def command_clear(self): @@ -578,7 +644,8 @@ class RosterInfoTab(Tab): """ self.core.information_buffer.messages = [] self.information_win.rebuild_everything(self.core.information_buffer) - self.core.information_win.rebuild_everything(self.core.information_buffer) + self.core.information_win.rebuild_everything( + self.core.information_buffer) self.refresh() @command_args_parser.quoted(1) @@ -586,14 +653,18 @@ class RosterInfoTab(Tab): """ /password <password> """ + def callback(iq): if iq['type'] == 'result': self.core.information('Password updated', 'Account') if config.get('password'): config.silent_set('password', args[0]) else: - self.core.information('Unable to change the password', 'Account') - self.core.xmpp.plugin['xep_0077'].change_password(args[0], callback=callback) + self.core.information('Unable to change the password', + 'Account') + + self.core.xmpp.plugin['xep_0077'].change_password( + args[0], callback=callback) @command_args_parser.quoted(0, 1) def command_deny(self, args): @@ -631,7 +702,8 @@ class RosterInfoTab(Tab): return jid = safeJID(safeJID(args[0]).bare) if not str(jid): - self.core.information('The provided JID (%s) is not valid' % (args[0],), 'Error') + self.core.information('The provided JID (%s) is not valid' % + (args[0], ), 'Error') return if jid in roster and roster[jid].subscription in ('to', 'both'): return self.core.information('Already subscribed.', 'Roster') @@ -644,10 +716,12 @@ class RosterInfoTab(Tab): """ Set a name for the specified JID in your roster """ + def callback(iq): if not iq: self.core.information('The name could not be set.', 'Error') log.debug('Error in /name:\n%s', iq) + if args is None: return self.core.command.help('name') jid = safeJID(args[0]).bare @@ -662,8 +736,12 @@ class RosterInfoTab(Tab): if 'none' in groups: groups.remove('none') subscription = contact.subscription - self.core.xmpp.update_roster(jid, name=name, groups=groups, - subscription=subscription, callback=callback) + self.core.xmpp.update_roster( + jid, + name=name, + groups=groups, + subscription=subscription, + callback=callback) @command_args_parser.quoted(1, 1) def command_groupadd(self, args): @@ -712,8 +790,12 @@ class RosterInfoTab(Tab): self.core.information('The group could not be set.', 'Error') log.debug('Error in groupadd:\n%s', iq) - self.core.xmpp.update_roster(jid, name=name, groups=new_groups, - subscription=subscription, callback=callback) + self.core.xmpp.update_roster( + jid, + name=name, + groups=new_groups, + subscription=subscription, + callback=callback) @command_args_parser.quoted(3) def command_groupmove(self, args): @@ -767,8 +849,12 @@ class RosterInfoTab(Tab): self.core.information('The group could not be set', 'Error') log.debug('Error in groupmove:\n%s', iq) - self.core.xmpp.update_roster(jid, name=name, groups=new_groups, - subscription=subscription, callback=callback) + 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): @@ -808,8 +894,12 @@ class RosterInfoTab(Tab): self.core.information('The group could not be set') log.debug('Error in groupremove:\n%s', iq) - self.core.xmpp.update_roster(jid, name=name, groups=new_groups, - subscription=subscription, callback=callback) + 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): @@ -842,7 +932,8 @@ class RosterInfoTab(Tab): else: filepath = path.join(getenv('HOME'), 'poezio_contacts') if not path.isfile(filepath): - self.core.information('The file %s does not exist' % filepath, 'Error') + self.core.information('The file %s does not exist' % filepath, + 'Error') return try: handle = open(filepath, 'r', encoding='utf-8') @@ -877,7 +968,8 @@ class RosterInfoTab(Tab): if roster.export(filepath): self.core.information('Contacts exported to %s' % filepath, 'Info') else: - self.core.information('Failed to export contacts to %s' % filepath, 'Info') + self.core.information('Failed to export contacts to %s' % filepath, + 'Info') def completion_remove(self, the_input): """ @@ -898,10 +990,13 @@ class RosterInfoTab(Tab): n = the_input.get_argument_position() if n == 1: jids = sorted(jid for jid in roster.jids()) - return Completion(the_input.new_completion, jids, n, '', quotify=True) + return Completion( + the_input.new_completion, jids, n, '', quotify=True) elif n == 2: - groups = sorted(group for group in roster.groups if group != 'none') - return Completion(the_input.new_completion, groups, n, '', quotify=True) + groups = sorted( + group for group in roster.groups if group != 'none') + return Completion( + the_input.new_completion, groups, n, '', quotify=True) return False def completion_groupmove(self, the_input): @@ -909,7 +1004,8 @@ class RosterInfoTab(Tab): n = the_input.get_argument_position() if n == 1: jids = sorted(jid for jid in roster.jids()) - return Completion(the_input.new_completion, jids, n, '', quotify=True) + return Completion( + the_input.new_completion, jids, n, '', quotify=True) elif n == 2: contact = roster[args[1]] if not contact: @@ -917,10 +1013,12 @@ class RosterInfoTab(Tab): groups = list(contact.groups) if 'none' in groups: groups.remove('none') - return Completion(the_input.new_completion, groups, n, '', quotify=True) + return Completion( + the_input.new_completion, groups, n, '', quotify=True) elif n == 3: groups = sorted(group for group in roster.groups) - return Completion(the_input.new_completion, groups, n, '', quotify=True) + return Completion( + the_input.new_completion, groups, n, '', quotify=True) return False def completion_groupremove(self, the_input): @@ -928,7 +1026,8 @@ class RosterInfoTab(Tab): n = the_input.get_argument_position() if n == 1: jids = sorted(jid for jid in roster.jids()) - return Completion(the_input.new_completion, jids, n, '', quotify=True) + return Completion( + the_input.new_completion, jids, n, '', quotify=True) elif n == 2: contact = roster[args[1]] if contact is None: @@ -938,7 +1037,8 @@ class RosterInfoTab(Tab): groups.remove('none') except ValueError: pass - return Completion(the_input.new_completion, groups, n, '', quotify=True) + return Completion( + the_input.new_completion, groups, n, '', quotify=True) return False def completion_deny(self, the_input): @@ -946,8 +1046,9 @@ class RosterInfoTab(Tab): Complete the first argument from the list of the contact with ask=='subscribe' """ - jids = sorted(str(contact.bare_jid) for contact in roster.contacts.values() - if contact.pending_in) + jids = sorted( + str(contact.bare_jid) for contact in roster.contacts.values() + if contact.pending_in) return Completion(the_input.new_completion, jids, 1, '', quotify=False) @command_args_parser.quoted(0, 1) @@ -976,8 +1077,10 @@ class RosterInfoTab(Tab): roster.modified() self.core.xmpp.send_presence(pto=jid, ptype='subscribed') self.core.xmpp.client_roster.send_last_presence() - if contact.subscription in ('from', 'none') and not contact.pending_out: - self.core.xmpp.send_presence(pto=jid, ptype='subscribe', pnick=self.core.own_nick) + if contact.subscription in ('from', + 'none') and not contact.pending_out: + self.core.xmpp.send_presence( + pto=jid, ptype='subscribe', pnick=self.core.own_nick) self.core.information('%s is now authorized' % jid, 'Roster') @@ -1027,7 +1130,8 @@ 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): @@ -1035,9 +1139,10 @@ class RosterInfoTab(Tab): '/' is pressed, we enter "input mode" """ 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) - self.input.do_command("/") # we add the slash + self.input = windows.CommandInput("", self.reset_help_message, + self.execute_slash_command) + self.input.resize(1, self.width, self.height - 1, 0) + self.input.do_command("/") # we add the slash def reset_help_message(self, _=None): self.input = self.default_help_message @@ -1065,13 +1170,15 @@ class RosterInfoTab(Tab): @refresh_wrapper.conditional def move_cursor_down(self): - if isinstance(self.input, windows.Input) and not self.input.history_disabled: + if isinstance(self.input, + windows.Input) and not self.input.history_disabled: return return self.roster_win.move_cursor_down() @refresh_wrapper.conditional def move_cursor_up(self): - if isinstance(self.input, windows.Input) and not self.input.history_disabled: + if isinstance(self.input, + windows.Input) and not self.input.history_disabled: return return self.roster_win.move_cursor_up() @@ -1156,9 +1263,11 @@ class RosterInfoTab(Tab): cont = selected_row res = selected_row.get_highest_priority_resource() acc = [] - acc.append('Contact: %s (%s)' % (cont.bare_jid, res.presence if res else 'unavailable')) + acc.append('Contact: %s (%s)' % (cont.bare_jid, res.presence + if res else 'unavailable')) if res: - acc.append('%s connected resource%s' % (len(cont), '' if len(cont) == 1 else 's')) + acc.append('%s connected resource%s' % + (len(cont), '' if len(cont) == 1 else 's')) acc.append('Current status: %s' % res.status) if cont.tune: acc.append('Tune: %s' % common.format_tune_string(cont.tune)) @@ -1167,21 +1276,20 @@ class RosterInfoTab(Tab): if cont.activity: acc.append('Activity: %s' % cont.activity) if cont.gaming: - acc.append('Game: %s' % (common.format_gaming_string(cont.gaming))) + acc.append('Game: %s' % + (common.format_gaming_string(cont.gaming))) msg = '\n'.join(acc) elif isinstance(selected_row, Resource): res = selected_row msg = 'Resource: %s (%s)\nCurrent status: %s\nPriority: %s' % ( - res.jid, - res.presence, - res.status, - res.priority) + res.jid, res.presence, res.status, res.priority) elif isinstance(selected_row, RosterGroup): rg = selected_row msg = 'Group: %s [%s/%s] contacts online' % ( - rg.name, - rg.get_nb_connected_contacts(), - len(rg),) + rg.name, + rg.get_nb_connected_contacts(), + len(rg), + ) else: msg = None if msg: @@ -1210,8 +1318,10 @@ class RosterInfoTab(Tab): in it. """ 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) + 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) self.input.disable_history() roster.modified() self.refresh() @@ -1220,8 +1330,10 @@ class RosterInfoTab(Tab): @refresh_wrapper.always def start_search_slow(self): 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) + 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) self.input.disable_history() return True @@ -1248,6 +1360,7 @@ class RosterInfoTab(Tab): def on_close(self): return + def diffmatch(search, string): """ Use difflib and a loop to check if search_pattern can @@ -1259,10 +1372,12 @@ def diffmatch(search, string): l = len(search) ratio = 0.7 for i in range(len(string) - l + 1): - if difflib.SequenceMatcher(None, search, string[i:i+l]).ratio() >= ratio: + if difflib.SequenceMatcher(None, search, + string[i:i + l]).ratio() >= ratio: return True return False + def jid_and_name_match(contact, txt): """ Match jid with text precisely @@ -1276,13 +1391,14 @@ def jid_and_name_match(contact, txt): return True return False + def jid_and_name_match_slow(contact, txt): """ A function used to know if a contact in the roster should be shown in the roster """ if not txt: - return True # Everything matches when search is empty + return True # Everything matches when search is empty user = safeJID(contact.bare_jid).bare if diffmatch(txt, user): return True diff --git a/poezio/tabs/xmltab.py b/poezio/tabs/xmltab.py index 65491bf4..223b6667 100644 --- a/poezio/tabs/xmltab.py +++ b/poezio/tabs/xmltab.py @@ -25,7 +25,6 @@ from poezio.common import safeJID class MatchJID(object): - def __init__(self, jid, dest=''): self.jid = jid self.dest = dest @@ -46,13 +45,15 @@ class MatchJID(object): def __repr__(self): return '%s%s%s' % (self.dest, ': ' if self.dest else '', self.jid) + MATCHERS_MAPPINGS = { - MatchJID: ('JID', repr), - matcher.MatcherId: ('ID', lambda obj: obj._criteria), - matcher.MatchXMLMask: ('XMLMask', lambda obj: tostring(obj._criteria)), - matcher.MatchXPath: ('XPath', lambda obj: obj._criteria) + MatchJID: ('JID', repr), + 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, core): Tab.__init__(self, core) @@ -68,41 +69,57 @@ class XMLTab(Tab): 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.") - self.register_command('clear', self.command_clear, - shortdesc='Clear the current buffer.') - self.register_command('filter_reset', self.command_filter_reset, - 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.') - self.register_command('filter_xpath', self.command_filter_xpath, - usage='<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.') - 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.') + self.register_command('close', self.close, shortdesc="Close this tab.") + self.register_command( + 'clear', self.command_clear, shortdesc='Clear the current buffer.') + self.register_command( + 'filter_reset', + self.command_filter_reset, + 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.') + self.register_command( + 'filter_xpath', + self.command_filter_xpath, + usage='<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.') + 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.') self.input = self.default_help_message self.key_func['^T'] = self.close self.key_func['^I'] = self.completion @@ -120,8 +137,10 @@ class XMLTab(Tab): 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) + 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) @@ -138,7 +157,8 @@ class XMLTab(Tab): new_messages = [] for msg in messages: try: - if msg.txt.strip() and self.match_stanza(ElementBase(ET.fromstring(clean_text(msg.txt)))): + if msg.txt.strip() and self.match_stanza( + ElementBase(ET.fromstring(clean_text(msg.txt)))): new_messages.append(msg) except ET.ParseError: log.debug('Malformed XML : %s', msg.txt, exc_info=True) @@ -212,7 +232,9 @@ class XMLTab(Tab): def command_filter_xpath(self, xpath): """/filter_xpath <xpath>""" try: - self.update_filters(matcher.MatchXPath(xpath.replace('%n', self.core.xmpp.default_ns))) + self.update_filters( + matcher.MatchXPath( + xpath.replace('%n', self.core.xmpp.default_ns))) self.refresh() except: self.core.information('Invalid XML Path', 'Error') @@ -239,22 +261,25 @@ class XMLTab(Tab): 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)) + 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) except Exception as e: - self.core.information('Could not write the XML dump: %s' % e, 'Error') + self.core.information('Could not write the XML dump: %s' % e, + 'Error') def on_slash(self): """ '/' is pressed, activate the input """ 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) - self.input.do_command("/") # we add the slash + self.input = windows.CommandInput("", self.reset_help_message, + self.execute_slash_command) + self.input.resize(1, self.width, self.height - 1, 0) + self.input.do_command("/") # we add the slash @refresh_wrapper.always def reset_help_message(self, _=None): @@ -266,10 +291,10 @@ class XMLTab(Tab): return True def on_scroll_up(self): - return self.text_win.scroll_up(self.text_win.height-1) + return self.text_win.scroll_up(self.text_win.height - 1) def on_scroll_down(self): - return self.text_win.scroll_down(self.text_win.height-1) + return self.text_win.scroll_down(self.text_win.height - 1) @command_args_parser.ignored def command_clear(self): @@ -314,11 +339,9 @@ class XMLTab(Tab): self.text_win.resize(self.height - info_win_size - tab_win_height - 2, self.width, 0, 0) self.text_win.rebuild_everything(self.core_buffer) - self.info_header.resize(1, self.width, - self.height - 2 - info_win_size - - tab_win_height, - 0) - self.input.resize(1, self.width, self.height-1, 0) + self.info_header.resize( + 1, self.width, self.height - 2 - info_win_size - tab_win_height, 0) + self.input.resize(1, self.width, self.height - 1, 0) def refresh(self): if self.need_resize: @@ -350,9 +373,10 @@ class XMLTab(Tab): self.core.xml_tab = False def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: + if self.core.information_win_size >= self.height - 3: return - self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0) - self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) - - + self.text_win.resize(self.height - 2 - self.core.information_win_size - + Tab.tab_win_height(), self.width, 0, 0) + self.info_header.resize( + 1, self.width, self.height - 2 - self.core.information_win_size - + Tab.tab_win_height(), 0) diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index 13eca399..3e6732cf 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -15,14 +15,26 @@ from datetime import datetime from poezio.config import config from poezio.theming import get_theme, dump_tuple + class Message: __slots__ = ('txt', 'nick_color', 'time', 'str_time', 'nickname', 'user', 'identifier', 'highlight', 'me', 'old_message', 'revisions', 'jid', 'ack') - def __init__(self, txt, time, nickname, nick_color, history, user, - identifier, str_time=None, highlight=False, - old_message=None, revisions=0, jid=None, ack=0): + def __init__(self, + txt, + time, + nickname, + nick_color, + history, + user, + identifier, + str_time=None, + highlight=False, + old_message=None, + revisions=0, + jid=None, + ack=0): """ Create a new Message object with parameters, check for /me messages, and delayed messages @@ -35,8 +47,9 @@ class Message: else: me = False if history: - txt = txt.replace('\x19o', '\x19o\x19%s}' % - dump_tuple(get_theme().COLOR_LOG_MSG)) + txt = txt.replace( + '\x19o', + '\x19o\x19%s}' % dump_tuple(get_theme().COLOR_LOG_MSG)) str_time = time.strftime("%Y-%m-%d %H:%M:%S") else: if str_time is None: @@ -87,17 +100,21 @@ class Message: rev -= 1 return ''.join(acc) + class CorrectionError(Exception): pass + class AckError(Exception): pass + class TextBuffer(object): """ This class just keep trace of messages, in a list with various information and attributes. """ + def __init__(self, messages_nb_limit=None): if messages_nb_limit is None: @@ -117,15 +134,33 @@ class TextBuffer(object): def last_message(self): return self.messages[-1] if self.messages else None - 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=0): + 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=0): """ Create a message and add it to the text buffer """ - msg = Message(txt, time, nickname, nick_color, history, user, - identifier, str_time=str_time, highlight=highlight, - jid=jid, ack=ack) + msg = Message( + txt, + time, + nickname, + nick_color, + history, + user, + identifier, + str_time=str_time, + highlight=highlight, + jid=jid, + ack=ack) self.messages.append(msg) while len(self.messages) > self._messages_nb_limit: @@ -134,12 +169,14 @@ class TextBuffer(object): ret_val = 0 show_timestamps = config.get('show_timestamps') nick_size = config.get('max_nick_length') - for window in self._windows: # make the associated windows - # build the lines from the new message - nb = window.build_new_message(msg, history=history, - highlight=highlight, - timestamp=show_timestamps, - nick_size=nick_size) + for window in self._windows: # make the associated windows + # build the lines from the new message + nb = window.build_new_message( + msg, + history=history, + highlight=highlight, + timestamp=show_timestamps, + nick_size=nick_size) if ret_val == 0: ret_val = nb if window.pos != 0: @@ -151,7 +188,7 @@ class TextBuffer(object): """ Find a message in the text buffer from its message id """ - for i in range(len(self.messages) -1, -1, -1): + for i in range(len(self.messages) - 1, -1, -1): msg = self.messages[i] if msg.identifier == old_id: return i @@ -175,16 +212,23 @@ class TextBuffer(object): 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)) + raise AckError( + 'Wrong JID for message id %s (was %s, expected %s)' % + (old_id, msg.jid, jid)) msg.ack = value if append: msg.txt += append return msg - def modify_message(self, txt, old_id, new_id, highlight=False, - time=None, user=None, jid=None): + def modify_message(self, + txt, + old_id, + new_id, + highlight=False, + time=None, + user=None, + jid=None): """ Correct a message in a text buffer. """ @@ -192,30 +236,39 @@ class TextBuffer(object): i = self._find_message(old_id) if i == -1: - log.debug('Message %s not found in text_buffer, abort replacement.', - old_id) + log.debug( + 'Message %s not found in text_buffer, abort replacement.', + old_id) raise CorrectionError("nothing to replace") msg = self.messages[i] if msg.user and msg.user is not user: raise CorrectionError("Different users") - elif len(msg.str_time) > 8: # ugly + elif len(msg.str_time) > 8: # ugly raise CorrectionError("Delayed message") elif not msg.user and (msg.jid is None or jid is None): raise CorrectionError('Could not check the ' 'identity of the sender') elif not msg.user and msg.jid != jid: raise CorrectionError('Messages %s and %s have not been ' - 'sent by the same fullJID' % - (old_id, new_id)) + 'sent by the same fullJID' % (old_id, + new_id)) if not time: time = msg.time - message = Message(txt, time, msg.nickname, msg.nick_color, None, - msg.user, new_id, highlight=highlight, - old_message=msg, revisions=msg.revisions + 1, - jid=jid) + message = Message( + txt, + time, + msg.nickname, + msg.nick_color, + None, + msg.user, + new_id, + highlight=highlight, + old_message=msg, + revisions=msg.revisions + 1, + jid=jid) self.messages[i] = message log.debug('Replacing message %s with %s.', old_id, new_id) return message diff --git a/poezio/theming.py b/poezio/theming.py index d007e463..a3721f08 100755 --- a/poezio/theming.py +++ b/poezio/theming.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Define the variables (colors and some other stuff) that are used when drawing the interface. @@ -80,6 +79,7 @@ from os import path from importlib import machinery finder = machinery.PathFinder() + class Theme(object): """ The theme class, from which all themes should inherit. @@ -90,6 +90,7 @@ class Theme(object): needs. Create a new theme and share it if you think it can be useful for others. """ + @classmethod def color_role(cls, role): role_mapping = { @@ -104,34 +105,34 @@ class Theme(object): @classmethod def char_affiliation(cls, affiliation): affiliation_mapping = { - 'owner': cls.CHAR_AFFILIATION_OWNER, - 'admin': cls.CHAR_AFFILIATION_ADMIN, - 'member': cls.CHAR_AFFILIATION_MEMBER, - 'none': cls.CHAR_AFFILIATION_NONE + 'owner': cls.CHAR_AFFILIATION_OWNER, + 'admin': cls.CHAR_AFFILIATION_ADMIN, + 'member': cls.CHAR_AFFILIATION_MEMBER, + 'none': cls.CHAR_AFFILIATION_NONE } return affiliation_mapping.get(affiliation, cls.CHAR_AFFILIATION_NONE) @classmethod def color_show(cls, show): show_mapping = { - 'xa': cls.COLOR_STATUS_XA, - 'none': cls.COLOR_STATUS_NONE, - 'dnd': cls.COLOR_STATUS_DND, - 'away': cls.COLOR_STATUS_AWAY, - 'chat': cls.COLOR_STATUS_CHAT, - '': cls.COLOR_STATUS_ONLINE, - 'available': cls.COLOR_STATUS_ONLINE, - 'unavailable': cls.COLOR_STATUS_UNAVAILABLE, + 'xa': cls.COLOR_STATUS_XA, + 'none': cls.COLOR_STATUS_NONE, + 'dnd': cls.COLOR_STATUS_DND, + 'away': cls.COLOR_STATUS_AWAY, + 'chat': cls.COLOR_STATUS_CHAT, + '': cls.COLOR_STATUS_ONLINE, + 'available': cls.COLOR_STATUS_ONLINE, + 'unavailable': cls.COLOR_STATUS_UNAVAILABLE, } return show_mapping.get(show, cls.COLOR_STATUS_NONE) @classmethod def char_subscription(cls, sub, keep='incomplete'): sub_mapping = { - 'from': cls.CHAR_ROSTER_FROM, - 'both': cls.CHAR_ROSTER_BOTH, - 'none': cls.CHAR_ROSTER_NONE, - 'to': cls.CHAR_ROSTER_TO, + 'from': cls.CHAR_ROSTER_FROM, + 'both': cls.CHAR_ROSTER_BOTH, + 'none': cls.CHAR_ROSTER_NONE, + 'to': cls.CHAR_ROSTER_TO, } if keep == 'incomplete' and sub == 'both': return '' @@ -141,7 +142,7 @@ class Theme(object): # Message text color COLOR_NORMAL_TEXT = (-1, -1) - COLOR_INFORMATION_TEXT = (5, -1) # TODO + COLOR_INFORMATION_TEXT = (5, -1) # TODO COLOR_WARNING_TEXT = (1, -1) # Color of the commands in the help message @@ -181,7 +182,6 @@ class Theme(object): CHAR_AFFILIATION_MEMBER = '+' CHAR_AFFILIATION_NONE = '-' - # XML Tab CHAR_XML_IN = 'IN ' CHAR_XML_OUT = 'OUT' @@ -238,42 +238,132 @@ class Theme(object): # Setting more colors makes it harder to have two nicks with the same color, # avoiding confusions. LIST_COLOR_NICKNAMES = [ - (1, -1), (2, -1), (3, -1), (4, -1), (5, -1), (6, -1), (9, -1), - (10, -1), (11, -1), (12, -1), (13, -1), (14, -1), (19, -1), - (20, -1), (21, -1), (22, -1), (23, -1), (24, -1), (25, -1), - (26, -1), (27, -1), (28, -1), (29, -1), (30, -1), (31, -1), - (32, -1), (33, -1), (34, -1), (35, -1), (36, -1), (37, -1), - (38, -1), (39, -1), (40, -1), (41, -1), (42, -1), (43, -1), - (44, -1), (45, -1), (46, -1), (47, -1), (48, -1), (49, -1), - (50, -1), (51, -1), (54, -1), (55, -1), (56, -1), (57, -1), - (58, -1), (60, -1), (61, -1), (62, -1), (63, -1), (64, -1), - (65, -1), (66, -1), (67, -1), (68, -1), (69, -1), (70, -1), - (71, -1), (72, -1), (73, -1), (74, -1), (75, -1), (76, -1), - (77, -1), (78, -1), (79, -1), (80, -1), (81, -1), (82, -1), - (83, -1), (84, -1), (85, -1), (86, -1), (87, -1), (88, -1), - (89, -1), (90, -1), (91, -1), (92, -1), (93, -1), (94, -1), - (95, -1), (96, -1), (97, -1), (98, -1), (99, -1), (100, -1), - (101, -1), (103, -1), (104, -1), (105, -1), (106, -1), (107, -1), - (108, -1), (109, -1), (110, -1), (111, -1), (112, -1), (113, -1), - (114, -1), (115, -1), (116, -1), (117, -1), (118, -1), (119, -1), - (120, -1), (121, -1), (122, -1), (123, -1), (124, -1), (125, -1), - (126, -1), (127, -1), (128, -1), (129, -1), (130, -1), (131, -1), - (132, -1), (133, -1), (134, -1), (135, -1), (136, -1), (137, -1), - (138, -1), (139, -1), (140, -1), (141, -1), (142, -1), (143, -1), - (144, -1), (145, -1), (146, -1), (147, -1), (148, -1), (149, -1), - (150, -1), (151, -1), (152, -1), (153, -1), (154, -1), (155, -1), - (156, -1), (157, -1), (158, -1), (159, -1), (160, -1), (161, -1), - (162, -1), (163, -1), (164, -1), (165, -1), (166, -1), (167, -1), - (168, -1), (169, -1), (170, -1), (171, -1), (172, -1), (173, -1), - (174, -1), (175, -1), (176, -1), (177, -1), (178, -1), (179, -1), - (180, -1), (181, -1), (182, -1), (183, -1), (184, -1), (185, -1), - (186, -1), (187, -1), (188, -1), (189, -1), (190, -1), (191, -1), - (192, -1), (193, -1), (196, -1), (197, -1), (198, -1), (199, -1), - (200, -1), (201, -1), (202, -1), (203, -1), (204, -1), (205, -1), - (206, -1), (207, -1), (208, -1), (209, -1), (210, -1), (211, -1), - (212, -1), (213, -1), (214, -1), (215, -1), (216, -1), (217, -1), - (218, -1), (219, -1), (220, -1), (221, -1), (222, -1), (223, -1), - (224, -1), (225, -1), (226, -1), (227, -1)] + (1, -1), (2, -1), (3, -1), (4, -1), (5, -1), (6, -1), (9, -1), + (10, -1), (11, -1), (12, -1), (13, -1), (14, -1), (19, -1), (20, -1), + (21, -1), (22, -1), (23, -1), (24, -1), (25, -1), (26, -1), (27, -1), + (28, -1), (29, -1), (30, -1), (31, -1), (32, -1), (33, -1), (34, -1), + (35, -1), (36, -1), (37, -1), (38, -1), (39, -1), (40, -1), (41, -1), + (42, -1), (43, -1), (44, -1), (45, -1), (46, -1), (47, -1), (48, -1), + (49, -1), (50, -1), (51, -1), (54, -1), (55, -1), (56, -1), (57, -1), + (58, -1), (60, -1), (61, -1), (62, -1), (63, -1), (64, -1), (65, -1), + (66, -1), (67, -1), (68, -1), (69, -1), (70, -1), (71, -1), (72, -1), + (73, -1), (74, -1), (75, -1), (76, -1), (77, -1), (78, -1), (79, -1), + (80, -1), (81, -1), (82, -1), (83, -1), (84, -1), (85, -1), (86, -1), + (87, -1), (88, -1), (89, -1), (90, -1), (91, -1), (92, -1), (93, -1), + (94, -1), (95, -1), (96, -1), (97, -1), (98, -1), (99, -1), (100, -1), + (101, -1), (103, -1), (104, -1), (105, -1), (106, -1), (107, -1), (108, + -1), + (109, -1), (110, -1), (111, -1), (112, -1), (113, -1), (114, -1), (115, + -1), + (116, -1), (117, -1), (118, -1), (119, -1), (120, -1), (121, -1), (122, + -1), + (123, -1), (124, -1), (125, -1), (126, -1), (127, -1), (128, -1), (129, + -1), + (130, + -1), (131, + -1), (132, + -1), (133, + -1), (134, + -1), (135, + -1), (136, + -1), (137, + -1), (138, + -1), (139, + -1), (140, + -1), (141, + -1), + (142, + -1), (143, + -1), (144, + -1), (145, + -1), (146, + -1), (147, + -1), (148, + -1), (149, + -1), (150, + -1), (151, + -1), (152, + -1), (153, + -1), + (154, + -1), (155, + -1), (156, + -1), (157, + -1), (158, + -1), (159, + -1), (160, + -1), (161, + -1), (162, + -1), (163, + -1), (164, + -1), (165, + -1), + (166, + -1), (167, + -1), (168, + -1), (169, + -1), (170, + -1), (171, + -1), (172, + -1), (173, + -1), (174, + -1), (175, + -1), (176, + -1), (177, + -1), + (178, + -1), (179, + -1), (180, + -1), (181, + -1), (182, + -1), (183, + -1), (184, + -1), (185, + -1), (186, + -1), (187, + -1), (188, + -1), (189, + -1), + (190, + -1), (191, + -1), (192, + -1), (193, + -1), (196, + -1), (197, + -1), (198, + -1), (199, + -1), (200, + -1), (201, + -1), (202, + -1), (203, + -1), + (204, + -1), (205, + -1), (206, + -1), (207, + -1), (208, + -1), (209, + -1), (210, + -1), (211, + -1), (212, + -1), (213, + -1), (214, + -1), (215, + -1), + (216, + -1), (217, + -1), (218, + -1), (219, + -1), (220, + -1), (221, + -1), (222, + -1), (223, + -1), (224, + -1), (225, + -1), (226, + -1), (227, + -1) + ] # This is your own nickname COLOR_OWN_NICK = (254, -1) @@ -346,19 +436,20 @@ class Theme(object): # Info messages color (the part before the ">") INFO_COLORS = { - 'info': (5, -1), - 'error': (16, 1), - 'warning': (1, -1), - 'roster': (2, -1), - 'help': (10, -1), - 'headline': (11, -1, 'b'), - 'tune': (6, -1), - 'gaming': (6, -1), - 'mood': (2, -1), - 'activity': (3, -1), - 'default': (7, -1), + 'info': (5, -1), + 'error': (16, 1), + 'warning': (1, -1), + 'roster': (2, -1), + 'help': (10, -1), + 'headline': (11, -1, 'b'), + 'tune': (6, -1), + 'gaming': (6, -1), + 'mood': (2, -1), + 'activity': (3, -1), + 'default': (7, -1), } + # This is the default theme object, used if no theme is defined in the conf theme = Theme() @@ -369,37 +460,36 @@ theme = Theme() curses_colors_dict = {} table_256_to_16 = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 0, 4, 4, 4, 12, 12, 2, 6, 4, 4, 12, 12, 2, 2, 6, 4, - 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, - 10, 10, 10, 14, 1, 5, 4, 4, 12, 12, 3, 8, 4, 4, 12, 12, - 2, 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, - 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 5, 4, 12, 12, 1, 1, - 5, 4, 12, 12, 3, 3, 8, 4, 12, 12, 2, 2, 2, 6, 12, 12, - 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 1, 5, - 12, 12, 1, 1, 1, 5, 12, 12, 1, 1, 1, 5, 12, 12, 3, 3, - 3, 7, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, - 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, - 13, 12, 9, 9, 9, 9, 13, 12, 11, 11, 11, 11, 7, 12, 10, 10, - 10, 10, 10, 14, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, - 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, - 9, 13, 11, 11, 11, 11, 11, 15, 0, 0, 0, 0, 0, 0, 8, 8, - 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 4, 4, 4, 12, 12, + 2, 6, 4, 4, 12, 12, 2, 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, + 14, 12, 10, 10, 10, 10, 10, 14, 1, 5, 4, 4, 12, 12, 3, 8, 4, 4, 12, 12, 2, + 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, + 10, 10, 14, 1, 1, 5, 4, 12, 12, 1, 1, 5, 4, 12, 12, 3, 3, 8, 4, 12, 12, 2, + 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 1, + 5, 12, 12, 1, 1, 1, 5, 12, 12, 1, 1, 1, 5, 12, 12, 3, 3, 3, 7, 12, 12, 10, + 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, + 13, 12, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, 13, 12, 11, 11, 11, 11, 7, 12, 10, + 10, 10, 10, 10, 14, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, + 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 11, 11, 11, 11, 11, 15, 0, 0, 0, + 0, 0, 0, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15 ] load_path = [] + def color_256_to_16(color): if color == -1: return color return table_256_to_16[color] + def dump_tuple(tup): """ Dump a tuple to a string of fg,bg,attr (optional) """ return ','.join(str(i) for i in tup) + def read_tuple(_str): """ Read a tuple dumped with dump_tumple @@ -408,6 +498,7 @@ def read_tuple(_str): char = attrs[2] if len(attrs) > 2 else '\0' return (int(attrs[0]), int(attrs[1])), char + @functools.lru_cache(maxsize=128) def to_curses_attr(color_tuple): """ @@ -451,31 +542,30 @@ def to_curses_attr(color_tuple): curses_pair = curses_pair | curses.A_BLINK return curses_pair + def get_theme(): """ Returns the current theme """ return theme + def update_themes_dir(option=None, value=None): global load_path load_path = [] # import from the git sources default_dir = path.join( - path.dirname(path.dirname(__file__)), - 'data/themes') + path.dirname(path.dirname(__file__)), 'data/themes') if path.exists(default_dir): load_path.append(default_dir) # import from the user-defined prefs themes_dir = path.expanduser( - value or - config.get('themes_dir') or - path.join(os.environ.get('XDG_DATA_HOME') or - path.join(os.environ.get('HOME'), '.local', 'share'), - 'poezio', 'themes') - ) + value or config.get('themes_dir') or path.join( + os.environ.get('XDG_DATA_HOME') + or path.join(os.environ.get('HOME'), '.local', 'share'), 'poezio', + 'themes')) try: os.makedirs(themes_dir) except OSError as e: @@ -497,6 +587,7 @@ def update_themes_dir(option=None, value=None): log.debug('Theme load path: %s', load_path) + def reload_theme(): theme_name = config.get('theme') global theme @@ -522,6 +613,7 @@ def reload_theme(): else: return 'No theme present in the theme file' + if __name__ == '__main__': # Display some nice text with nice colors s = curses.initscr() @@ -539,4 +631,3 @@ if __name__ == '__main__': finally: curses.endwin() print() - diff --git a/poezio/timed_events.py b/poezio/timed_events.py index 8ce94407..f203bf19 100644 --- a/poezio/timed_events.py +++ b/poezio/timed_events.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Timed events are the standard way to schedule events for later in poezio. @@ -15,11 +14,13 @@ Once created, they must be added to the list of checked events with import datetime + class DelayedEvent(object): """ A TimedEvent, but with the date calculated from now + a delay in seconds. Use it if you want an event to happen in, e.g. 6 seconds. """ + def __init__(self, delay, callback, *args): """ Create a new DelayedEvent. @@ -34,12 +35,14 @@ class DelayedEvent(object): # An asyncio handler, as returned by call_later() or call_at() self.handler = None + class TimedEvent(DelayedEvent): """ An event with a callback that is called when the specified time is passed. The callback and its arguments should be passed as the lasts arguments. """ + def __init__(self, date, callback, *args): """ Create a new timed event. diff --git a/poezio/user.py b/poezio/user.py index 0cf32f79..68d52493 100644 --- a/poezio/user.py +++ b/poezio/user.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Define the user class. An user is a MUC participant, not a roster contact (see contact.py) @@ -20,22 +19,26 @@ from poezio.theming import get_theme import logging log = logging.getLogger(__name__) -ROLE_DICT = { - '':0, - 'none':0, - 'visitor':1, - 'participant':2, - 'moderator':3 - } +ROLE_DICT = {'': 0, 'none': 0, 'visitor': 1, 'participant': 2, 'moderator': 3} + class User(object): """ keep trace of an user in a Room """ - __slots__ = ('last_talked', 'jid', 'chatstate', 'affiliation', 'show', 'status', 'role', 'nick', 'color') - - def __init__(self, nick, affiliation, show, status, role, jid, deterministic=True, color=''): - self.last_talked = datetime(1, 1, 1) # The oldest possible time + __slots__ = ('last_talked', 'jid', 'chatstate', 'affiliation', 'show', + 'status', 'role', 'nick', 'color') + + 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) if color != '': diff --git a/poezio/windows/__init__.py b/poezio/windows/__init__.py index 56657433..68250906 100644 --- a/poezio/windows/__init__.py +++ b/poezio/windows/__init__.py @@ -20,11 +20,13 @@ from poezio.windows.roster_win import RosterWin, ContactInfoWin from poezio.windows.text_win import TextWin, XMLTextWin from poezio.windows.image import ImageWin -__all__ = ['Win', 'FormWin', 'BookmarksWin', 'Dialog', 'GlobalInfoBar', - 'VerticalGlobalInfoBar', 'InfoWin', 'PrivateInfoWin', 'XMLInfoWin', - 'MucListInfoWin', 'ConversationInfoWin', 'MucInfoWin', - 'DynamicConversationInfoWin', 'ConversationStatusMessageWin', - 'BookmarksInfoWin', 'ConfirmStatusWin', 'HelpText', 'Input', - 'HistoryInput', 'MessageInput', 'CommandInput', 'ListWin', - 'ColumnHeaderWin', 'VerticalSeparator', 'UserList', 'Topic', - 'RosterWin', 'ContactInfoWin', 'TextWin', 'XMLTextWin', 'ImageWin'] +__all__ = [ + 'Win', 'FormWin', 'BookmarksWin', 'Dialog', 'GlobalInfoBar', + 'VerticalGlobalInfoBar', 'InfoWin', 'PrivateInfoWin', 'XMLInfoWin', + 'MucListInfoWin', 'ConversationInfoWin', 'MucInfoWin', + 'DynamicConversationInfoWin', 'ConversationStatusMessageWin', + 'BookmarksInfoWin', 'ConfirmStatusWin', 'HelpText', 'Input', + 'HistoryInput', 'MessageInput', 'CommandInput', 'ListWin', + 'ColumnHeaderWin', 'VerticalSeparator', 'UserList', 'Topic', 'RosterWin', + 'ContactInfoWin', 'TextWin', 'XMLTextWin', 'ImageWin' +] diff --git a/poezio/windows/base_wins.py b/poezio/windows/base_wins.py index 8813d8f7..f3970ff5 100644 --- a/poezio/windows/base_wins.py +++ b/poezio/windows/base_wins.py @@ -22,6 +22,7 @@ FORMAT_CHAR = '\x19' # I guess. But maybe we can find better chars that are even less risky. format_chars = '\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18' + class DummyWin(object): def __getattribute__(self, name): if name != '__bool__': @@ -103,7 +104,7 @@ class Win(object): has_italic = hasattr(curses, 'A_ITALIC') while next_attr_char != -1 and text: if next_attr_char + 1 < len(text): - attr_char = text[next_attr_char+1].lower() + attr_char = text[next_attr_char + 1].lower() else: attr_char = str() if next_attr_char != 0: @@ -116,8 +117,10 @@ class Win(object): self._win.attron(curses.A_BOLD) elif attr_char == 'i' and has_italic: self._win.attron(curses.A_ITALIC) - if (attr_char in string.digits or attr_char == '-') and attr_char != '': - color_str = text[next_attr_char+1:text.find('}', next_attr_char)] + if (attr_char in string.digits + or attr_char == '-') and attr_char != '': + color_str = text[next_attr_char + 1:text.find( + '}', next_attr_char)] if ',' in color_str: tup, char = read_tuple(color_str) self._win.attron(to_curses_attr(tup)) @@ -136,9 +139,9 @@ class Win(object): self._win.attroff(curses.A_BOLD) elif color_str: self._win.attron(to_curses_attr((int(color_str), -1))) - text = text[next_attr_char+len(color_str)+2:] + text = text[next_attr_char + len(color_str) + 2:] else: - text = text[next_attr_char+2:] + text = text[next_attr_char + 2:] next_attr_char = text.find(FORMAT_CHAR) self.addstr(text) @@ -149,6 +152,6 @@ class Win(object): (y, x) = self._win.getyx() size = self.width - x if color: - self.addnstr(' '*size, size, to_curses_attr(color)) + self.addnstr(' ' * size, size, to_curses_attr(color)) else: - self.addnstr(' '*size, size) + self.addnstr(' ' * size, size) diff --git a/poezio/windows/bookmark_forms.py b/poezio/windows/bookmark_forms.py index 5fbc3858..5f5d581a 100644 --- a/poezio/windows/bookmark_forms.py +++ b/poezio/windows/bookmark_forms.py @@ -10,6 +10,7 @@ from poezio.windows.data_forms import FieldInput, FieldInputMixin from poezio.theming import to_curses_attr, get_theme from poezio.common import safeJID + class BookmarkJIDInput(FieldInput, Input): def __init__(self, field): FieldInput.__init__(self, field) @@ -29,6 +30,7 @@ class BookmarkJIDInput(FieldInput, Input): def get_help_message(self): return 'Edit the text' + class BookmarkMethodInput(FieldInputMixin): def __init__(self, field): FieldInput.__init__(self, field) @@ -42,7 +44,7 @@ class BookmarkMethodInput(FieldInputMixin): if self.val_pos > 0: self.val_pos -= 1 elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: + if self.val_pos < len(self.options) - 1: self.val_pos += 1 else: return @@ -51,14 +53,14 @@ class BookmarkMethodInput(FieldInputMixin): def refresh(self): self._win.erase() self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) + 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.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.addstr(0, self.width // 2 - len(option) // 2, option) self._win.attroff(to_curses_attr(self.color)) self._refresh() @@ -68,6 +70,7 @@ class BookmarkMethodInput(FieldInputMixin): def get_help_message(self): return '←, →: Select a value amongst the others' + class BookmarkPasswordInput(FieldInput, Input): def __init__(self, field): FieldInput.__init__(self, field) @@ -80,11 +83,12 @@ class BookmarkPasswordInput(FieldInput, Input): 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])) + 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)) + 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)) @@ -96,6 +100,7 @@ class BookmarkPasswordInput(FieldInput, Input): def get_help_message(self): return 'Edit the secret text' + class BookmarkAutojoinWin(FieldInputMixin): def __init__(self, field): FieldInput.__init__(self, field) @@ -138,10 +143,9 @@ class BookmarksWin(Win): 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))) + self.lines.append( + (BookmarkJIDInput(bookmark), BookmarkPasswordInput(bookmark), + BookmarkAutojoinWin(bookmark), BookmarkMethodInput(bookmark))) @property def current_input(self): @@ -151,20 +155,23 @@ class BookmarksWin(Win): 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.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.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: + if self.current_input - self.scroll_pos > self.height - 1: self.scroll_pos = self.current_input - self.height + 1 self.refresh() @@ -185,7 +192,7 @@ class BookmarksWin(Win): self._win = base_wins.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: + while self.current_input - self.scroll_pos > self.height - 1: self.scroll_pos += 1 def go_to_next_line_input(self): @@ -193,39 +200,51 @@ class BookmarksWin(Win): 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) + 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: + 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) + 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.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) + 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.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) + self.lines[ + self.current_input][self.current_horizontal_input].set_color( + get_theme().COLOR_SELECTED_ROW) def go_to_next_page(self): if not self.lines: @@ -234,7 +253,9 @@ class BookmarksWin(Win): 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) + self.lines[ + self.current_input][self.current_horizontal_input].set_color( + get_theme().COLOR_NORMAL_TEXT) inc = min(self.height, len(self.lines) - self.current_input - 1) if self.current_input + inc - self.scroll_pos > self.height - 1: @@ -243,7 +264,9 @@ class BookmarksWin(Win): self.refresh() else: self.current_input += inc - 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].set_color( + get_theme().COLOR_SELECTED_ROW) return True def go_to_previous_page(self): @@ -253,7 +276,9 @@ class BookmarksWin(Win): if self.current_input == 0: return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) + self.lines[ + self.current_input][self.current_horizontal_input].set_color( + get_theme().COLOR_NORMAL_TEXT) dec = min(self.height, self.current_input) self.current_input -= dec @@ -262,7 +287,9 @@ class BookmarksWin(Win): 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) + self.lines[ + self.current_input][self.current_horizontal_input].set_color( + get_theme().COLOR_SELECTED_ROW) return True def go_to_previous_horizontal_input(self): @@ -270,24 +297,31 @@ class BookmarksWin(Win): 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.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) + 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) + 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 + 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) + 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): @@ -298,9 +332,12 @@ class BookmarksWin(Win): 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 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: @@ -308,10 +345,10 @@ class BookmarksWin(Win): def refresh_current_input(self): if self.lines: - self.lines[self.current_input][self.current_horizontal_input].refresh() + 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/poezio/windows/confirm.py b/poezio/windows/confirm.py index 591ff89a..f2794221 100644 --- a/poezio/windows/confirm.py +++ b/poezio/windows/confirm.py @@ -2,9 +2,11 @@ from poezio.windows.base_wins import Win from poezio.theming import get_theme, to_curses_attr + class Dialog(Win): str_accept = " Accept " str_refuse = " Reject " + def __init__(self, helper_text, critical=False): self.text = helper_text self.accept = False @@ -35,4 +37,3 @@ class Dialog(Win): def toggle_choice(self): self.accept = not self.accept - diff --git a/poezio/windows/data_forms.py b/poezio/windows/data_forms.py index a14418d7..c600273e 100644 --- a/poezio/windows/data_forms.py +++ b/poezio/windows/data_forms.py @@ -12,12 +12,14 @@ from poezio.windows.inputs import Input from poezio.theming import to_curses_attr, get_theme + class FieldInput(object): """ All input types in a data form should inherit this class, in addition with windows.Input or any relevant class from the 'windows' library. """ + def __init__(self, field): self._field = field self.color = get_theme().COLOR_NORMAL_TEXT @@ -25,7 +27,6 @@ class FieldInput(object): def update_field_value(self, value): raise NotImplementedError - def is_dummy(self): return False @@ -42,8 +43,10 @@ class FieldInput(object): """ return '' + class FieldInputMixin(FieldInput, Win): "Mix both FieldInput and Win" + def __init__(self, field): FieldInput.__init__(self, field) Win.__init__(self) @@ -81,6 +84,7 @@ class DummyInput(FieldInputMixin): """ Used for fields that do not require any input ('fixed') """ + def __init__(self, field): FieldInputMixin.__init__(self, field) @@ -93,6 +97,7 @@ class DummyInput(FieldInputMixin): def is_dummy(self): return True + class BooleanWin(FieldInputMixin): def __init__(self, field): FieldInputMixin.__init__(self, field) @@ -108,8 +113,8 @@ class BooleanWin(FieldInputMixin): def refresh(self): self._win.erase() self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*(8), self.width) - self.addstr(0, 2, "%s"%self.value) + self.addnstr(0, 0, ' ' * (8), self.width) + self.addstr(0, 2, "%s" % self.value) self.addstr(0, 8, '→') self.addstr(0, 0, '←') if self.last_key == 'KEY_RIGHT': @@ -126,6 +131,7 @@ class BooleanWin(FieldInputMixin): def get_help_message(self): return '← and →: change the value between True and False' + class TextMultiWin(FieldInputMixin): def __init__(self, field): FieldInputMixin.__init__(self, field) @@ -149,20 +155,22 @@ class TextMultiWin(FieldInputMixin): if self.val_pos > 0: self.val_pos -= 1 elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: + if self.val_pos < len(self.options) - 1: self.val_pos += 1 elif key == '^M': self.edition_input = Input() self.edition_input.color = self.color - self.edition_input.resize(self.height, self.width, self.y, self.x) + self.edition_input.resize(self.height, self.width, self.y, + self.x) self.edition_input.text = self.options[self.val_pos] self.edition_input.key_end() else: if key == '^M': self.options[self.val_pos] = self.edition_input.get_text() - if not self.options[self.val_pos] and self.val_pos != len(self.options) -1: + if not self.options[self.val_pos] and self.val_pos != len( + self.options) - 1: del self.options[self.val_pos] - if self.val_pos == len(self.options) -1: + if self.val_pos == len(self.options) - 1: self.val_pos -= 1 self.edition_input = None if not self.options or self.options[-1] != '': @@ -175,13 +183,13 @@ class TextMultiWin(FieldInputMixin): if not self.edition_input: self._win.erase() self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) + self.addnstr(0, 0, ' ' * self.width, self.width) option = self.options[self.val_pos] - self.addstr(0, self.width//2-len(option)//2, option) + self.addstr(0, self.width // 2 - len(option) // 2, option) 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.val_pos < len(self.options) - 1: + self.addstr(0, self.width - 1, '→') self._win.attroff(to_curses_attr(self.color)) self._refresh() else: @@ -194,7 +202,7 @@ class TextMultiWin(FieldInputMixin): def get_help_message(self): if not self.edition_input: help_msg = '← and →: browse the available entries. ' - if self.val_pos == len(self.options)-1: + if self.val_pos == len(self.options) - 1: help_msg += 'Enter: add an entry' else: help_msg += 'Enter: edit this entry' @@ -202,6 +210,7 @@ class TextMultiWin(FieldInputMixin): help_msg = 'Enter: finish editing this entry.' return help_msg + class ListMultiWin(FieldInputMixin): def __init__(self, field): FieldInputMixin.__init__(self, field) @@ -215,7 +224,7 @@ class ListMultiWin(FieldInputMixin): if self.val_pos > 0: self.val_pos -= 1 elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: + if self.val_pos < len(self.options) - 1: self.val_pos += 1 elif key == ' ': self.options[self.val_pos][1] = not self.options[self.val_pos][1] @@ -226,14 +235,15 @@ class ListMultiWin(FieldInputMixin): def refresh(self): self._win.erase() self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) + 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.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[0]['label']) + self.addstr(0, self.width // 2 - len(option) // 2, + option[0]['label']) self.addstr(0, 2, '✔' if option[1] else '☐') self._win.attroff(to_curses_attr(self.color)) self._refresh() @@ -241,12 +251,15 @@ class ListMultiWin(FieldInputMixin): def reply(self): self._field['label'] = '' self._field.delOptions() - values = [option[0]['value'] for option in self.options if option[1] is True] + values = [ + option[0]['value'] for option in self.options if option[1] is True + ] self._field.set_answer(values) def get_help_message(self): return '←, →: Switch between the value. Space: select or unselect a value' + class ListSingleWin(FieldInputMixin): def __init__(self, field): FieldInputMixin.__init__(self, field) @@ -263,7 +276,7 @@ class ListSingleWin(FieldInputMixin): if self.val_pos > 0: self.val_pos -= 1 elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: + if self.val_pos < len(self.options) - 1: self.val_pos += 1 else: return @@ -272,14 +285,14 @@ class ListSingleWin(FieldInputMixin): def refresh(self): self._win.erase() self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) + 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.val_pos < len(self.options) - 1: + self.addstr(0, self.width - 1, '→') if self.options: option = self.options[self.val_pos]['label'] - self.addstr(0, self.width//2-len(option)//2, option) + self.addstr(0, self.width // 2 - len(option) // 2, option) self._win.attroff(to_curses_attr(self.color)) self._refresh() @@ -291,6 +304,7 @@ class ListSingleWin(FieldInputMixin): def get_help_message(self): return '←, →: Select a value amongst the others' + class TextSingleWin(FieldInputMixin, Input): def __init__(self, field): FieldInputMixin.__init__(self, field) @@ -307,6 +321,7 @@ class TextSingleWin(FieldInputMixin, Input): def get_help_message(self): return 'Edit the text' + class TextPrivateWin(TextSingleWin): def __init__(self, field): TextSingleWin.__init__(self, field) @@ -315,11 +330,12 @@ class TextPrivateWin(TextSingleWin): 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])) + 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)) + 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)) @@ -328,6 +344,7 @@ class TextPrivateWin(TextSingleWin): def get_help_message(self): return 'Edit the secret text' + class FormWin(object): """ A window, with some subwins (the various inputs). @@ -335,22 +352,24 @@ class FormWin(object): On resize, move and resize all the subwin and define how the text will be written On refresh, write all the text, and refresh all the subwins """ - input_classes = {'boolean': BooleanWin, - 'fixed': DummyInput, - 'jid-multi': TextMultiWin, - 'jid-single': TextSingleWin, - 'list-multi': ListMultiWin, - 'list-single': ListSingleWin, - 'text-multi': TextMultiWin, - 'text-private': TextPrivateWin, - 'text-single': TextSingleWin, - } + input_classes = { + 'boolean': BooleanWin, + 'fixed': DummyInput, + 'jid-multi': TextMultiWin, + 'jid-single': TextSingleWin, + 'list-multi': ListMultiWin, + 'list-single': ListSingleWin, + 'text-multi': TextMultiWin, + 'text-private': TextPrivateWin, + 'text-single': TextSingleWin, + } + def __init__(self, form, height, width, y, x): self._form = form self._win = base_wins.TAB_WIN.derwin(height, width, y, x) self.scroll_pos = 0 self.current_input = 0 - self.inputs = [] # dict list + self.inputs = [] # dict list for (name, field) in self._form.getFields().items(): if field['type'] == 'hidden': continue @@ -363,9 +382,11 @@ class FormWin(object): if field['type'] == 'fixed': label = field.get_value() inp = input_class(field) - self.inputs.append({'label':ColoredLabel(label), - 'description': desc, - 'input':inp}) + self.inputs.append({ + 'label': ColoredLabel(label), + 'description': desc, + 'input': inp + }) def resize(self, height, width, y, x): self.height = height @@ -373,7 +394,7 @@ class FormWin(object): self._win = base_wins.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: + while self.current_input - self.scroll_pos > self.height - 1: self.scroll_pos += 1 def reply(self): @@ -394,13 +415,17 @@ class FormWin(object): return if self.current_input == len(self.inputs) - 1: return - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_NORMAL_TEXT) + self.inputs[self.current_input]['input'].set_color( + get_theme().COLOR_NORMAL_TEXT) + self.inputs[self.current_input]['label'].set_color( + get_theme().COLOR_NORMAL_TEXT) self.current_input += 1 jump = 0 - while self.current_input+jump != len(self.inputs) - 1 and self.inputs[self.current_input+jump]['input'].is_dummy(): + while self.current_input + jump != len( + self.inputs) - 1 and self.inputs[self.current_input + + jump]['input'].is_dummy(): jump += 1 - if self.inputs[self.current_input+jump]['input'].is_dummy(): + if self.inputs[self.current_input + jump]['input'].is_dummy(): return self.current_input += jump # If moving made the current input out of the visible screen, we @@ -408,24 +433,31 @@ class FormWin(object): # call refresh() if this is not the case, because # refresh_current_input() is always called anyway, so this is not # needed - if self.current_input - self.scroll_pos > self.height-1: + if self.current_input - self.scroll_pos > self.height - 1: self.scroll_pos += 1 self.refresh() - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) + self.inputs[self.current_input]['input'].set_color( + get_theme().COLOR_SELECTED_ROW) + self.inputs[self.current_input]['label'].set_color( + get_theme().COLOR_SELECTED_ROW) def go_to_previous_input(self): if not self.inputs: return if self.current_input == 0: return - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_NORMAL_TEXT) + self.inputs[self.current_input]['input'].set_color( + get_theme().COLOR_NORMAL_TEXT) + self.inputs[self.current_input]['label'].set_color( + get_theme().COLOR_NORMAL_TEXT) self.current_input -= 1 jump = 0 - while self.current_input-jump > 0 and self.inputs[self.current_input+jump]['input'].is_dummy(): + while self.current_input - jump > 0 and self.inputs[self.current_input + + + jump]['input'].is_dummy( + ): jump += 1 - if self.inputs[self.current_input+jump]['input'].is_dummy(): + if self.inputs[self.current_input + jump]['input'].is_dummy(): return # Adjust the scroll position if the current_input would be outside # of the visible area @@ -433,8 +465,10 @@ class FormWin(object): self.scroll_pos = self.current_input self.refresh() self.current_input -= jump - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) + self.inputs[self.current_input]['input'].set_color( + get_theme().COLOR_SELECTED_ROW) + self.inputs[self.current_input]['label'].set_color( + get_theme().COLOR_SELECTED_ROW) def on_input(self, key, raw=False): if not self.inputs: @@ -448,8 +482,9 @@ class FormWin(object): for name, field in self._form.getFields().items(): if field['type'] == 'hidden': continue - self.inputs[i]['label'].resize(1, self.width//2, y + 1, 0) - self.inputs[i]['input'].resize(1, self.width//2, y+1, self.width//2) + self.inputs[i]['label'].resize(1, self.width // 2, y + 1, 0) + self.inputs[i]['input'].resize(1, self.width // 2, y + 1, + self.width // 2) # TODO: display the field description y += 1 i += 1 @@ -462,17 +497,19 @@ class FormWin(object): inp['label'].refresh() inp['input'].refresh() inp['label'].refresh() - if self.inputs and self.current_input < self.height-1: - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) + if self.inputs and self.current_input < self.height - 1: + self.inputs[self.current_input]['input'].set_color( + get_theme().COLOR_SELECTED_ROW) self.inputs[self.current_input]['input'].refresh() - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) + self.inputs[self.current_input]['label'].set_color( + get_theme().COLOR_SELECTED_ROW) self.inputs[self.current_input]['label'].refresh() def refresh_current_input(self): self.inputs[self.current_input]['input'].refresh() def get_help_message(self): - if self.inputs and self.current_input < self.height-1 and self.inputs[self.current_input]['input']: + if self.inputs and self.current_input < self.height - 1 and self.inputs[self. + current_input]['input']: return self.inputs[self.current_input]['input'].get_help_message() return '' - diff --git a/poezio/windows/funcs.py b/poezio/windows/funcs.py index ea2941c8..3648bac3 100644 --- a/poezio/windows/funcs.py +++ b/poezio/windows/funcs.py @@ -7,6 +7,7 @@ DIGITS = string.digits + '-' from poezio.windows.base_wins import FORMAT_CHAR, format_chars + def find_first_format_char(text, chars=None): if chars is None: chars = format_chars @@ -19,13 +20,15 @@ def find_first_format_char(text, chars=None): pos = p return pos + def truncate_nick(nick, size=10): if size < 1: size = 1 if nick and len(nick) > size: - return nick[:size]+'…' + return nick[:size] + '…' return nick + def parse_attrs(text, previous=None): next_attr_char = text.find(FORMAT_CHAR) if previous: @@ -34,7 +37,7 @@ def parse_attrs(text, previous=None): attrs = [] while next_attr_char != -1 and text: if next_attr_char + 1 < len(text): - attr_char = text[next_attr_char+1].lower() + attr_char = text[next_attr_char + 1].lower() else: attr_char = '\0' if attr_char == 'o': @@ -46,12 +49,11 @@ def parse_attrs(text, previous=None): elif attr_char == 'i': attrs.append('i') if attr_char in DIGITS and attr_char: - color_str = text[next_attr_char+1:text.find('}', next_attr_char)] + color_str = text[next_attr_char + 1:text.find('}', next_attr_char)] if color_str: attrs.append(color_str + '}') - text = text[next_attr_char+len(color_str)+2:] + text = text[next_attr_char + len(color_str) + 2:] else: - text = text[next_attr_char+2:] + text = text[next_attr_char + 2:] next_attr_char = text.find(FORMAT_CHAR) return attrs - diff --git a/poezio/windows/image.py b/poezio/windows/image.py index 57bbec71..09a11d6f 100644 --- a/poezio/windows/image.py +++ b/poezio/windows/image.py @@ -15,10 +15,12 @@ from poezio.windows.base_wins import Win from poezio.theming import to_curses_attr from poezio.xhtml import _parse_css_color + class ImageWin(Win): """ A window which contains either an image or a border. """ + def __init__(self): self._image = None Win.__init__(self) @@ -41,10 +43,10 @@ class ImageWin(Win): self._refresh() def _display_border(self): - self._win.border(curses.ACS_VLINE, curses.ACS_VLINE, - curses.ACS_HLINE, curses.ACS_HLINE, - curses.ACS_ULCORNER, curses.ACS_URCORNER, - curses.ACS_LLCORNER, curses.ACS_LRCORNER) + self._win.border(curses.ACS_VLINE, curses.ACS_VLINE, curses.ACS_HLINE, + curses.ACS_HLINE, curses.ACS_ULCORNER, + curses.ACS_URCORNER, curses.ACS_LLCORNER, + curses.ACS_LRCORNER) @staticmethod def _compute_size(image_size, width: int, height: int): @@ -69,13 +71,13 @@ class ImageWin(Win): start_y = (original_height - height // 2) // 2 start_x = (original_width - width) // 2 for y in range(height // 2): - two_lines = data[(2 * y) * width * 3: (2 * y + 2) * width * 3] + two_lines = data[(2 * y) * width * 3:(2 * y + 2) * width * 3] line1 = two_lines[:width * 3] line2 = two_lines[width * 3:] self.move(start_y + y, start_x) for x in range(width): - r, g, b = line1[x * 3: (x + 1) * 3] + r, g, b = line1[x * 3:(x + 1) * 3] top_color = _parse_css_color('#%02x%02x%02x' % (r, g, b)) - r, g, b = line2[x * 3: (x + 1) * 3] + r, g, b = line2[x * 3:(x + 1) * 3] bot_color = _parse_css_color('#%02x%02x%02x' % (r, g, b)) self.addstr('▄', to_curses_attr((bot_color, top_color))) diff --git a/poezio/windows/info_bar.py b/poezio/windows/info_bar.py index cac56db9..950813f1 100644 --- a/poezio/windows/info_bar.py +++ b/poezio/windows/info_bar.py @@ -10,11 +10,11 @@ log = logging.getLogger(__name__) import curses - from poezio.config import config from poezio.windows.base_wins import Win from poezio.theming import get_theme, to_curses_attr + class GlobalInfoBar(Win): def __init__(self, core): Win.__init__(self) @@ -23,7 +23,8 @@ class GlobalInfoBar(Win): def refresh(self): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() - self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(0, 0, "[", + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) create_gaps = config.get('create_gaps') show_names = config.get('show_tab_names') @@ -48,20 +49,24 @@ class GlobalInfoBar(Win): self.addstr(' ', to_curses_attr(color)) if show_names: if use_nicks: - self.addstr("%s" % str(tab.get_nick()), to_curses_attr(color)) + self.addstr("%s" % str(tab.get_nick()), + to_curses_attr(color)) else: self.addstr("%s" % tab.name, to_curses_attr(color)) - self.addstr("|", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - except: # end of line + self.addstr("|", + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + except: # end of line break (y, x) = self._win.getyx() - self.addstr(y, x-1, '] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(y, x - 1, '] ', + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) (y, x) = self._win.getyx() remaining_size = self.width - x - self.addnstr(' '*remaining_size, remaining_size, + self.addnstr(' ' * remaining_size, remaining_size, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self._refresh() + class VerticalGlobalInfoBar(Win): def __init__(self, core, scr): Win.__init__(self) @@ -79,30 +84,32 @@ class VerticalGlobalInfoBar(Win): use_nicks = config.get('use_tab_nicks') if nb_tabs >= height: for y, tab in enumerate(sorted_tabs): - if tab.vertical_color == get_theme().COLOR_VERTICAL_TAB_CURRENT: + if tab.vertical_color == get_theme( + ).COLOR_VERTICAL_TAB_CURRENT: pos = y break # center the current tab as much as possible - if pos < height//2: + if pos < height // 2: sorted_tabs = sorted_tabs[:height] - elif nb_tabs - pos <= height//2: + elif nb_tabs - pos <= height // 2: sorted_tabs = sorted_tabs[-height:] else: - sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2] + 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 asc_sort: y = height - y - 1 self.addstr(y, 0, "%2d" % tab.nb, - to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER)) + to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER)) self.addstr('.') if use_nicks: - self.addnstr("%s" % tab.get_nick(), width - 4, to_curses_attr(color)) + self.addnstr("%s" % tab.get_nick(), width - 4, + to_curses_attr(color)) else: self.addnstr("%s" % tab.name, width - 4, to_curses_attr(color)) separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR) self._win.attron(separator) - self._win.vline(0, width-1, curses.ACS_VLINE, height) + self._win.vline(0, width - 1, curses.ACS_VLINE, height) self._win.attroff(separator) self._refresh() diff --git a/poezio/windows/info_wins.py b/poezio/windows/info_wins.py index 1afb34b0..2d4d1e01 100644 --- a/poezio/windows/info_wins.py +++ b/poezio/windows/info_wins.py @@ -13,11 +13,13 @@ from poezio.windows.base_wins import Win from poezio.windows.funcs import truncate_nick from poezio.theming import get_theme, to_curses_attr + class InfoWin(Win): """ Base class for all the *InfoWin, used in various tabs. For example MucInfoWin, etc. Provides some useful methods. """ + def __init__(self): Win.__init__(self) @@ -29,12 +31,15 @@ class InfoWin(Win): """ if window.pos > 0: plus = ' -MORE(%s)-' % window.pos - self.addstr(plus, to_curses_attr(get_theme().COLOR_SCROLLABLE_NUMBER)) + self.addstr(plus, + to_curses_attr(get_theme().COLOR_SCROLLABLE_NUMBER)) + class XMLInfoWin(InfoWin): """ Info about the latest xml filter used and the state of the buffer. """ + def __init__(self): InfoWin.__init__(self) @@ -51,11 +56,13 @@ class XMLInfoWin(InfoWin): self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() + class PrivateInfoWin(InfoWin): """ The line above the information window, displaying information about the MUC user we are talking to """ + def __init__(self): InfoWin.__init__(self) @@ -75,7 +82,8 @@ class PrivateInfoWin(InfoWin): value returned by the callbacks. """ for key in information: - self.addstr(information[key](jid), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(information[key](jid), + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_room_name(self, name): jid = safeJID(name) @@ -86,13 +94,16 @@ class PrivateInfoWin(InfoWin): def write_chatstate(self, state): if state: - self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(' %s' % (state, ), + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + class MucListInfoWin(InfoWin): """ The live above the information window, displaying informatios about the muc server being listed """ + def __init__(self, message=''): InfoWin.__init__(self) self.message = message @@ -101,14 +112,17 @@ class MucListInfoWin(InfoWin): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() if name: - self.addstr(name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(name, + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) else: - self.addstr(self.message, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(self.message, + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) if window: self.print_scroll_position(window) self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() + class ConversationInfoWin(InfoWin): """ The line above the information window, displaying information @@ -154,7 +168,7 @@ class ConversationInfoWin(InfoWin): """ for key in information: self.addstr(information[key](jid), - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_resource_information(self, resource): """ @@ -171,7 +185,8 @@ class ConversationInfoWin(InfoWin): self.addstr(presence, to_curses_attr(color)) if resource and resource.status: shortened = resource.status[:20] + (resource.status[:20] and '…') - self.addstr(' %s' % shortened, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(' %s' % shortened, + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_contact_information(self, contact): @@ -179,23 +194,28 @@ class ConversationInfoWin(InfoWin): Write the information about the contact """ if not contact: - self.addstr("(contact not in roster)", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr("(contact not in roster)", + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) return display_name = contact.name if display_name: - self.addstr('%s '%(display_name), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr('%s ' % (display_name), + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_contact_jid(self, jid): """ Just write the jid that we are talking to """ self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(jid.full, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) + self.addstr(jid.full, + to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_chatstate(self, state): if state: - self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(' %s' % (state, ), + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + class DynamicConversationInfoWin(ConversationInfoWin): def write_contact_jid(self, jid): @@ -203,18 +223,23 @@ class DynamicConversationInfoWin(ConversationInfoWin): Just displays the resource in an other color """ log.debug("write_contact_jid DynamicConversationInfoWin, jid: %s", - jid.resource) + jid.resource) self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(jid.bare, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) + self.addstr(jid.bare, + to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) if jid.resource: - self.addstr("/%s" % (jid.resource,), to_curses_attr(get_theme().COLOR_CONVERSATION_RESOURCE)) + self.addstr("/%s" % (jid.resource, ), + to_curses_attr( + get_theme().COLOR_CONVERSATION_RESOURCE)) self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + class MucInfoWin(InfoWin): """ The line just above the information window, displaying information about the MUC we are viewing """ + def __init__(self): InfoWin.__init__(self) @@ -233,12 +258,15 @@ class MucInfoWin(InfoWin): def write_room_name(self, room): self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(room.name, to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) + self.addstr(room.name, + to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_participants_number(self, room): self.addstr('{', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(str(len(room.users)), to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) + self.addstr( + str(len(room.users)), + to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) self.addstr('} ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_disconnected(self, room): @@ -246,7 +274,8 @@ class MucInfoWin(InfoWin): Shows a message if the room is not joined """ if not room.joined: - self.addstr(' -!- Not connected ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(' -!- Not connected ', + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_own_nick(self, room): """ @@ -255,7 +284,9 @@ class MucInfoWin(InfoWin): nick = room.own_nick if not nick: return - self.addstr(truncate_nick(nick, 13), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr( + truncate_nick(nick, 13), + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_role(self, room, user): """ @@ -269,10 +300,12 @@ class MucInfoWin(InfoWin): txt += user.role + ')' self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + class ConversationStatusMessageWin(InfoWin): """ The upper bar displaying the status message of the contact """ + def __init__(self): InfoWin.__init__(self) @@ -293,7 +326,9 @@ class ConversationStatusMessageWin(InfoWin): self._refresh() def write_status_message(self, resource): - self.addstr(resource.status, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(resource.status, + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + class BookmarksInfoWin(InfoWin): def __init__(self): @@ -307,7 +342,9 @@ class BookmarksInfoWin(InfoWin): self._refresh() def write_remote_status(self, preferred): - self.addstr('Remote storage: %s' % preferred, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr('Remote storage: %s' % preferred, + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + class ConfirmStatusWin(Win): def __init__(self, text, critical=False): @@ -326,4 +363,3 @@ class ConfirmStatusWin(Win): self.addstr(self.text, c_color) self.finish_line(color) self._refresh() - diff --git a/poezio/windows/input_placeholders.py b/poezio/windows/input_placeholders.py index 3ac478fd..47cfac31 100644 --- a/poezio/windows/input_placeholders.py +++ b/poezio/windows/input_placeholders.py @@ -6,7 +6,6 @@ but which are not inputs. import logging log = logging.getLogger(__name__) - from poezio.windows.base_wins import Win from poezio.theming import get_theme, to_curses_attr @@ -17,6 +16,7 @@ class HelpText(Win): Usually used to replace an Input when the tab is in command mode. """ + def __init__(self, text=''): Win.__init__(self) self.txt = text @@ -26,7 +26,8 @@ class HelpText(Win): if txt: self.txt = txt self._win.erase() - self.addstr(0, 0, self.txt[:self.width-1], to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(0, 0, self.txt[:self.width - 1], + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() diff --git a/poezio/windows/inputs.py b/poezio/windows/inputs.py index faf0125d..0ccd179e 100644 --- a/poezio/windows/inputs.py +++ b/poezio/windows/inputs.py @@ -18,6 +18,7 @@ from poezio.theming import to_curses_attr DEFAULT_ON_INPUT = lambda x: None + class Input(Win): """ The simplest Input possible, provides just a way to edit a single line @@ -31,7 +32,8 @@ class Input(Win): in a very flexible way. """ text_attributes = 'bou1234567t' - clipboard = '' # A common clipboard for all the inputs, this makes + clipboard = '' # A common clipboard for all the inputs, this makes + # it easy cut and paste text between various input def __init__(self): self.key_func = { @@ -59,16 +61,16 @@ class Input(Win): '^?': self.key_backspace, "M-^?": self.delete_word, # '^J': self.add_line_break, - } + } Win.__init__(self) self.text = '' - self.pos = 0 # The position of the “cursor” in the text - # (not only in the view) + self.pos = 0 # The position of the “cursor” in the text + # (not only in the view) self.view_pos = 0 # The position (in the text) of the - # first character displayed on the - # screen - self.on_input = DEFAULT_ON_INPUT # callback called on any key pressed - self.color = None # use this color on addstr + # first character displayed on the + # screen + self.on_input = DEFAULT_ON_INPUT # callback called on any key pressed + self.color = None # use this color on addstr def on_delete(self): """ @@ -101,10 +103,10 @@ class Input(Win): """ if self.pos == 0: return True - separators = string.punctuation+' ' - while self.pos > 0 and self.text[self.pos-1] in separators: + separators = string.punctuation + ' ' + while self.pos > 0 and self.text[self.pos - 1] in separators: self.key_left() - while self.pos > 0 and self.text[self.pos-1] not in separators: + while self.pos > 0 and self.text[self.pos - 1] not in separators: self.key_left() return True @@ -114,10 +116,11 @@ class Input(Win): """ if self.is_cursor_at_end(): return True - separators = string.punctuation+' ' + separators = string.punctuation + ' ' while not self.is_cursor_at_end() and self.text[self.pos] in separators: self.key_right() - while not self.is_cursor_at_end() and self.text[self.pos] not in separators: + while not self.is_cursor_at_end() and self.text[self. + pos] not in separators: self.key_right() return True @@ -125,10 +128,10 @@ class Input(Win): """ Delete the word just before the cursor """ - separators = string.punctuation+' ' - while self.pos > 0 and self.text[self.pos-1] in separators: + separators = string.punctuation + ' ' + while self.pos > 0 and self.text[self.pos - 1] in separators: self.key_backspace() - while self.pos > 0 and self.text[self.pos-1] not in separators: + while self.pos > 0 and self.text[self.pos - 1] not in separators: self.key_backspace() return True @@ -136,10 +139,11 @@ class Input(Win): """ Delete the word just after the cursor """ - separators = string.punctuation+' ' + separators = string.punctuation + ' ' while not self.is_cursor_at_end() and self.text[self.pos] in separators: self.key_dc() - while not self.is_cursor_at_end() and self.text[self.pos] not in separators: + while not self.is_cursor_at_end() and self.text[self. + pos] not in separators: self.key_dc() return True @@ -182,8 +186,8 @@ class Input(Win): """ self.reset_completion() if self.is_cursor_at_end(): - return True # end of line, nothing to delete - self.text = self.text[:self.pos]+self.text[self.pos+1:] + return True # end of line, nothing to delete + self.text = self.text[:self.pos] + self.text[self.pos + 1:] self.rewrite_text() return True @@ -257,7 +261,12 @@ class Input(Win): self.normal_completion(word_list, add_after) return True - def new_completion(self, word_list, argument_position=-1, add_after='', quotify=True, override=False): + def new_completion(self, + word_list, + argument_position=-1, + add_after='', + quotify=True, + override=False): """ Complete the argument at position ``argument_postion`` in the input. If ``quotify`` is ``True``, then the completion will operate on block of words @@ -277,11 +286,17 @@ class Input(Win): if argument_position == 0: self._new_completion_first(word_list) else: - self._new_completion_args(word_list, argument_position, add_after, quotify, override) + self._new_completion_args(word_list, argument_position, add_after, + quotify, override) self.rewrite_text() return True - def _new_completion_args(self, word_list, argument_position=-1, add_after='', quoted=True, override=False): + def _new_completion_args(self, + word_list, + argument_position=-1, + add_after='', + quoted=True, + override=False): """ Case for completing arguments with position ≠ 0 """ @@ -319,12 +334,12 @@ class Input(Win): if argument_position >= len(words): if quoted and ' ' in self.hit_list[0]: - words.append('"'+self.hit_list[0]+'"') + words.append('"' + self.hit_list[0] + '"') else: words.append(self.hit_list[0]) else: if quoted and ' ' in self.hit_list[0]: - words[argument_position] = '"'+self.hit_list[0]+'"' + words[argument_position] = '"' + self.hit_list[0] + '"' else: words[argument_position] = self.hit_list[0] @@ -370,7 +385,7 @@ class Input(Win): command_stop = self.text.find(' ') if command_stop == -1 or self.pos <= command_stop: return 0 - text = self.text[command_stop+1:] + text = self.text[command_stop + 1:] pos = self.pos - len(self.text) + len(text) - 1 val = common.find_argument(pos, text, quoted=quoted) + 1 return val @@ -387,19 +402,22 @@ class Input(Win): Normal completion """ pos = self.pos - if pos < len(self.text) and after.endswith(' ') and self.text[pos] == ' ': - after = after[:-1] # remove the last space if we are already on a space + if pos < len( + self.text) and after.endswith(' ') and self.text[pos] == ' ': + after = after[: + -1] # remove the last space if we are already on a space if not self.last_completion: space_before_cursor = self.text.rfind(' ', 0, pos) if space_before_cursor != -1: - begin = self.text[space_before_cursor+1:pos] + begin = self.text[space_before_cursor + 1:pos] else: begin = self.text[:pos] - hit_list = [] # list of matching hits + hit_list = [] # list of matching hits for word in word_list: if word.lower().startswith(begin.lower()): hit_list.append(word) - elif word.startswith('"') and word.lower()[1:].startswith(begin.lower()): + elif word.startswith('"') and word.lower()[1:].startswith( + begin.lower()): hit_list.append(word) if len(hit_list) == 0: return @@ -408,11 +426,11 @@ class Input(Win): else: begin = self.last_completion end = len(begin) + len(after) - self.hit_list.append(self.hit_list.pop(0)) # rotate list + self.hit_list.append(self.hit_list.pop(0)) # rotate list - self.text = self.text[:pos-end] + self.text[pos:] + self.text = self.text[:pos - end] + self.text[pos:] pos -= end - hit = self.hit_list[0] # take the first hit + hit = self.hit_list[0] # take the first hit self.text = self.text[:pos] + hit + after + self.text[pos:] for _ in range(end): try: @@ -432,11 +450,11 @@ class Input(Win): self.on_input(self.get_text()) return res if not raw and (not key or len(key) > 1): - return False # ignore non-handled keyboard shortcuts + return False # ignore non-handled keyboard shortcuts if reset: self.reset_completion() # Insert the char at the cursor position - self.text = self.text[:self.pos]+key+self.text[self.pos:] + self.text = self.text[:self.pos] + key + self.text[self.pos:] self.pos += len(key) if reset: self.rewrite_text() @@ -472,11 +490,11 @@ class Input(Win): if text[format_char] == '\n': attr_char = '|' else: - attr_char = self.text_attributes[ - format_chars.index(text[format_char])] + attr_char = self.text_attributes[format_chars.index( + text[format_char])] self.addstr(text[:format_char]) self.addstr(attr_char, curses.A_REVERSE) - text = text[format_char+1:] + text = text[format_char + 1:] if attr_char == 'o': self._win.attrset(0) elif attr_char == 'u': @@ -503,7 +521,9 @@ class Input(Win): self._win.erase() if self.color: self._win.attron(to_curses_attr(self.color)) - displayed_text = text[self.view_pos:self.view_pos+self.width-1].replace('\t', '\x18') + displayed_text = text[self.view_pos: + self.view_pos + self.width - 1].replace( + '\t', '\x18') self._win.attrset(0) self._addstr_colored_lite(displayed_text) # Fill the rest of the line with the input color @@ -512,7 +532,8 @@ class Input(Win): size = self.width - x self.addnstr(' ' * size, size, to_curses_attr(self.color)) self.addstr(0, - poopt.wcswidth(displayed_text[:self.pos-self.view_pos]), '') + poopt.wcswidth(displayed_text[:self.pos - self.view_pos]), + '') if self.color: self._win.attroff(to_curses_attr(self.color)) curses.curs_set(1) @@ -557,6 +578,7 @@ class Input(Win): self.clear_text() return txt + class HistoryInput(Input): """ An input with colors and stuff, plus an history @@ -641,13 +663,14 @@ class HistoryInput(Input): self.key_end() return True + class MessageInput(HistoryInput): """ The input featuring history and that is being used in Conversation, Muc and Private tabs Also letting the user enter colors or other text markups """ - history = list() # The history is common to all MessageInput + history = list() # The history is common to all MessageInput def __init__(self): HistoryInput.__init__(self) @@ -662,11 +685,13 @@ class MessageInput(HistoryInput): """ Read one more char (c), add the corresponding char from formats_char to the text string """ + def cb(attr_char): if attr_char in self.text_attributes: char = format_chars[self.text_attributes.index(attr_char)] self.do_command(char, False) self.rewrite_text() + keyboard.continuation_keys_callback = cb def key_enter(self): @@ -682,6 +707,7 @@ class MessageInput(HistoryInput): self.clear_text() return txt + class CommandInput(HistoryInput): """ An input with an help message in the left, with three given callbacks: @@ -769,4 +795,3 @@ class CommandInput(HistoryInput): # add the message to history, but avoid duplicates self.history.insert(0, txt) self.histo_pos = -1 - diff --git a/poezio/windows/list.py b/poezio/windows/list.py index c0dc44cf..1f51e88f 100644 --- a/poezio/windows/list.py +++ b/poezio/windows/list.py @@ -16,12 +16,13 @@ class ListWin(Win): A list (with no depth, so not for the roster) that can be scrolled up and down, with one selected line at a time """ + def __init__(self, columns, with_headers=True): Win.__init__(self) - self._columns = columns # a dict {'column_name': tuple_index} - self._columns_sizes = {} # a dict {'column_name': size} - self.sorted_by = (None, None) # for example: ('name', '↑') - self.lines = [] # a list of dicts + self._columns = columns # a dict {'column_name': tuple_index} + self._columns_sizes = {} # a dict {'column_name': size} + self.sorted_by = (None, None) # for example: ('name', '↑') + self.lines = [] # a list of dicts self._selected_row = 0 self._starting_pos = 0 # The column number from which we start the refresh @@ -55,8 +56,8 @@ class ListWin(Win): elif asc: self.lines.sort(key=lambda x: x[self._columns[col_name]]) else: - self.lines.sort(key=lambda x: x[self._columns[col_name]], - reverse=True) + self.lines.sort( + key=lambda x: x[self._columns[col_name]], reverse=True) self.refresh() curses.doupdate() @@ -87,7 +88,7 @@ class ListWin(Win): def refresh(self): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() - lines = self.lines[self._starting_pos:self._starting_pos+self.height] + lines = self.lines[self._starting_pos:self._starting_pos + self.height] for y, line in enumerate(lines): x = 0 for col in self._columns.items(): @@ -96,12 +97,13 @@ class ListWin(Win): except KeyError: txt = '' size = self._columns_sizes[col[0]] - txt += ' ' * (size-len(txt)) + txt += ' ' * (size - len(txt)) if not txt: continue if line is self.lines[self._selected_row]: self.addstr(y, x, txt[:size], - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr( + get_theme().COLOR_INFORMATION_BAR)) else: self.addstr(y, x, txt[:size]) x += size @@ -138,7 +140,7 @@ class ListWin(Win): return self._selected_row += self.height if self._selected_row > len(self.lines) - 1: - self._selected_row = len(self.lines) -1 + self._selected_row = len(self.lines) - 1 while self._selected_row >= self._starting_pos + self.height: self._starting_pos += self.height // 2 if self._starting_pos < 0: @@ -155,10 +157,12 @@ class ListWin(Win): self._starting_pos -= self.height // 2 return True + class ColumnHeaderWin(Win): """ A class displaying the column's names """ + def __init__(self, columns): Win.__init__(self) self._columns = columns @@ -186,11 +190,14 @@ class ColumnHeaderWin(Win): txt += get_theme().CHAR_COLUMN_DESC #⇓⇑↑↓⇧⇩▲▼ size = self._columns_sizes[col] - txt += ' ' * (size-len(txt)) + txt += ' ' * (size - len(txt)) if col in self._column_sel: - self.addstr(0, x, txt, to_curses_attr(get_theme().COLOR_COLUMN_HEADER_SEL)) + self.addstr(0, x, txt, + to_curses_attr( + get_theme().COLOR_COLUMN_HEADER_SEL)) else: - self.addstr(0, x, txt, to_curses_attr(get_theme().COLOR_COLUMN_HEADER)) + self.addstr(0, x, txt, + to_curses_attr(get_theme().COLOR_COLUMN_HEADER)) x += size self._refresh() @@ -214,7 +221,7 @@ class ColumnHeaderWin(Win): if self._column_sel in self._columns: index = self._columns.index(self._column_sel) if index > 1: - index = index -1 + index = index - 1 else: index = 0 else: @@ -225,12 +232,11 @@ class ColumnHeaderWin(Win): def sel_column_right(self): if self._column_sel in self._columns: index = self._columns.index(self._column_sel) - if index < len(self._columns)-2: - index = index +1 + if index < len(self._columns) - 2: + index = index + 1 else: - index = len(self._columns) -1 + index = len(self._columns) - 1 else: index = len(self._columns) - 1 self._column_sel = self._columns[index] self.refresh() - diff --git a/poezio/windows/misc.py b/poezio/windows/misc.py index d3d6783e..71027b7c 100644 --- a/poezio/windows/misc.py +++ b/poezio/windows/misc.py @@ -10,11 +10,13 @@ import curses from poezio.windows.base_wins import Win from poezio.theming import get_theme, to_curses_attr + class VerticalSeparator(Win): """ Just a one-column window, with just a line in it, that is refreshed only on resize, but never on refresh, for efficiency """ + def rewrite_line(self): self._win.vline(0, 0, curses.ACS_VLINE, self.height, to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) @@ -54,4 +56,3 @@ class SimpleTextWin(Win): for y, line in enumerate(self.built_lines): self.addstr_colored(line, y, 0) self._refresh() - diff --git a/poezio/windows/muc.py b/poezio/windows/muc.py index 553a2cfa..4251c279 100644 --- a/poezio/windows/muc.py +++ b/poezio/windows/muc.py @@ -13,12 +13,15 @@ from poezio import poopt from poezio.config import config from poezio.theming import to_curses_attr, get_theme + def userlist_to_cache(userlist): result = [] for user in userlist: - result.append((user.nick, user.status, user.chatstate, user.affiliation, user.role)) + result.append((user.nick, user.status, user.chatstate, + user.affiliation, user.role)) return result + class UserList(Win): def __init__(self): Win.__init__(self) @@ -26,23 +29,23 @@ class UserList(Win): self.cache = [] def scroll_up(self): - self.pos += self.height-1 + self.pos += self.height - 1 return True def scroll_down(self): pos = self.pos - self.pos -= self.height-1 + self.pos -= self.height - 1 if self.pos < 0: self.pos = 0 return self.pos != pos def draw_plus(self, y): - self.addstr(y, self.width-2, '++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) - + self.addstr(y, self.width - 2, '++', + to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) def refresh_if_changed(self, users): old = self.cache - new = userlist_to_cache(users[self.pos:self.pos+self.height]) + new = userlist_to_cache(users[self.pos:self.pos + self.height]) if len(old) != len(new): self.cache = new self.refresh(users) @@ -56,7 +59,7 @@ class UserList(Win): def refresh(self, users): log.debug('Refresh: %s', self.__class__.__name__) if config.get('hide_user_list'): - return # do not refresh if this win is hidden. + return # do not refresh if this win is hidden. if len(users) < self.height: self.pos = 0 elif self.pos >= len(users) - self.height and self.pos != 0: @@ -69,12 +72,12 @@ class UserList(Win): else: y = 0 - for user in users[self.pos:self.pos+self.height]: + for user in users[self.pos:self.pos + self.height]: self.draw_role_affiliation(y, user) self.draw_status_chatstate(y, user) self.addstr(y, 2, - poopt.cut_by_columns(user.nick, self.width - 2), - to_curses_attr(user.color)) + poopt.cut_by_columns(user.nick, self.width - 2), + to_curses_attr(user.color)) if asc_sort: y -= 1 else: @@ -84,14 +87,14 @@ class UserList(Win): # draw indicators of position in the list if self.pos > 0: if asc_sort: - self.draw_plus(self.height-1) + self.draw_plus(self.height - 1) else: self.draw_plus(0) if self.pos + self.height < len(users): if asc_sort: self.draw_plus(0) else: - self.draw_plus(self.height-1) + self.draw_plus(self.height - 1) self._refresh() def draw_role_affiliation(self, y, user): @@ -119,6 +122,7 @@ class UserList(Win): self._win.vline(0, 0, curses.ACS_VLINE, self.height) self._win.attroff(separator) + class Topic(Win): def __init__(self): Win.__init__(self) @@ -128,17 +132,16 @@ class Topic(Win): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() if topic: - msg = topic[:self.width-1] + msg = topic[:self.width - 1] else: - msg = self._message[:self.width-1] + msg = self._message[:self.width - 1] self.addstr(0, 0, msg, to_curses_attr(get_theme().COLOR_TOPIC_BAR)) _, x = self._win.getyx() remaining_size = self.width - x if remaining_size: - self.addnstr(' '*remaining_size, remaining_size, + self.addnstr(' ' * remaining_size, remaining_size, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self._refresh() def set_message(self, message): self._message = message - diff --git a/poezio/windows/roster_win.py b/poezio/windows/roster_win.py index 73191e84..7b1e5ba7 100644 --- a/poezio/windows/roster_win.py +++ b/poezio/windows/roster_win.py @@ -17,11 +17,10 @@ from poezio.theming import get_theme, to_curses_attr class RosterWin(Win): - def __init__(self): Win.__init__(self) - self.pos = 0 # cursor position in the contact list - self.start_pos = 1 # position of the start of the display + self.pos = 0 # cursor position in the contact list + self.start_pos = 1 # position of the start of the display self.selected_row = None self.roster_cache = [] @@ -34,15 +33,15 @@ class RosterWin(Win): Return True if we scrolled, False otherwise """ pos = self.pos - if self.pos < self.roster_len-number: + if self.pos < self.roster_len - number: self.pos += number else: self.pos = self.roster_len - 1 - if self.pos >= self.start_pos-1 + self.height-1: + if self.pos >= self.start_pos - 1 + self.height - 1: if number == 1: self.scroll_down(8) else: - self.scroll_down(self.pos-self.start_pos - self.height // 2) + self.scroll_down(self.pos - self.start_pos - self.height // 2) self.update_pos() return pos != self.pos @@ -51,7 +50,7 @@ class RosterWin(Win): Return True if we scrolled, False otherwise """ pos = self.pos - if self.pos-number >= 0: + if self.pos - number >= 0: self.pos -= number else: self.pos = 0 @@ -59,7 +58,7 @@ class RosterWin(Win): if number == 1: self.scroll_up(8) else: - self.scroll_up(self.start_pos-self.pos + self.height // 2) + self.scroll_up(self.start_pos - self.pos + self.height // 2) self.update_pos() return pos != self.pos @@ -71,10 +70,10 @@ class RosterWin(Win): def scroll_down(self, number=8): pos = self.start_pos - if self.start_pos + number <= self.roster_len-1: + if self.start_pos + number <= self.roster_len - 1: self.start_pos += number else: - self.start_pos = self.roster_len-1 + self.start_pos = self.roster_len - 1 return self.start_pos != pos def scroll_up(self, number=8): @@ -106,21 +105,23 @@ class RosterWin(Win): # build the cache for group in roster.get_groups(group_sort): contacts_filtered = group.get_contacts() - if (not show_offline and group.get_nb_connected_contacts() == 0) or not contacts_filtered: - continue # Ignore empty groups + if (not show_offline and group.get_nb_connected_contacts() == 0 + ) or not contacts_filtered: + continue # Ignore empty groups self.roster_cache.append(group) if group.folded: - continue # ignore folded groups + continue # ignore folded groups for contact in group.get_contacts(sort=sort): if not show_offline and len(contact) == 0: - continue # ignore offline contacts + continue # ignore offline contacts self.roster_cache.append(contact) if not contact.folded(group.name): for resource in contact.get_resources(): self.roster_cache.append(resource) roster.last_built = datetime.now() if self.selected_row in self.roster_cache: - if self.pos < self.roster_len and self.roster_cache[self.pos] != self.selected_row: + if self.pos < self.roster_len and self.roster_cache[self. + pos] != self.selected_row: self.pos = self.roster_cache.index(self.selected_row) def refresh(self, roster): @@ -131,7 +132,8 @@ class RosterWin(Win): log.debug('Refresh: %s', self.__class__.__name__) self.build_roster_cache(roster) # make sure we are within bounds - self.move_cursor_up((self.roster_len + self.pos) if self.pos >= self.roster_len else 0) + self.move_cursor_up((self.roster_len + self.pos) + if self.pos >= self.roster_len else 0) if not self.roster_cache: self.selected_row = None self._win.erase() @@ -140,10 +142,12 @@ class RosterWin(Win): y = 1 group = "none" # scroll down if needed - if self.start_pos+self.height <= self.pos+2: - self.scroll_down(self.pos - self.start_pos - self.height + (self.height//2)) + if self.start_pos + self.height <= self.pos + 2: + self.scroll_down(self.pos - self.start_pos - self.height + + (self.height // 2)) # draw the roster from the cache - roster_view = self.roster_cache[self.start_pos-1:self.start_pos+self.height] + roster_view = self.roster_cache[self.start_pos - 1: + self.start_pos + self.height] options = { 'show_roster_sub': config.get('show_roster_subscriptions'), @@ -153,7 +157,7 @@ class RosterWin(Win): for item in roster_view: draw_selected = False - if y -2 + self.start_pos == self.pos: + if y - 2 + self.start_pos == self.pos: draw_selected = True self.selected_row = item @@ -161,7 +165,8 @@ 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, **options) + self.draw_contact_line(y, item, draw_selected, group, + **options) elif isinstance(item, Resource): self.draw_resource_line(y, item, draw_selected) @@ -169,26 +174,25 @@ class RosterWin(Win): if self.start_pos > 1: self.draw_plus(1) - if self.start_pos + self.height-2 < self.roster_len: - self.draw_plus(self.height-1) + if self.start_pos + self.height - 2 < self.roster_len: + self.draw_plus(self.height - 1) self._refresh() - def draw_plus(self, y): """ Draw the indicator that shows that the list is longer than what is displayed """ - self.addstr(y, self.width-5, '++++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) + self.addstr(y, self.width - 5, '++++', + to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) def draw_roster_information(self, roster): """ The header at the top """ - self.addstr('Roster: %s/%s contacts' % ( - roster.get_nb_connected_contacts(), - len(roster)), - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr('Roster: %s/%s contacts' % + (roster.get_nb_connected_contacts(), len(roster)), + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self.finish_line(get_theme().COLOR_INFORMATION_BAR) def draw_group(self, y, group, colored): @@ -202,7 +206,8 @@ class RosterWin(Win): else: self.addstr(y, 0, '[-] ') contacts = " (%s/%s)" % (group.get_nb_connected_contacts(), len(group)) - self.addstr(y, 4, self.truncate_name(group.name, len(contacts)+4) + contacts) + self.addstr( + y, 4, self.truncate_name(group.name, len(contacts) + 4) + contacts) if colored: self._win.attroff(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) self.finish_line() @@ -212,8 +217,14 @@ class RosterWin(Win): return name return name[:self.width - added - 1] + '…' - def draw_contact_line(self, y, contact, colored, group, show_roster_sub=False, - show_s2s_errors=True, show_roster_jids=False): + 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 information about one contact. This is basically the highest priority resource's information @@ -255,37 +266,51 @@ class RosterWin(Win): added += len(get_theme().CHAR_ROSTER_ACTIVITY) if contact.gaming: added += len(get_theme().CHAR_ROSTER_GAMING) - if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): - added += len(theme.char_subscription(contact.subscription, keep=show_roster_sub)) + if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', + 'none'): + added += len( + theme.char_subscription( + contact.subscription, keep=show_roster_sub)) 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) else: - display_name = '%s' % (contact.bare_jid,) + display_name = '%s' % (contact.bare_jid, ) display_name = self.truncate_name(display_name, added) + nb if colored: - self.addstr(display_name, to_curses_attr(get_theme().COLOR_SELECTED_ROW)) + self.addstr(display_name, + to_curses_attr(get_theme().COLOR_SELECTED_ROW)) else: self.addstr(display_name) - if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): - self.addstr(theme.char_subscription(contact.subscription, keep=show_roster_sub), to_curses_attr(theme.COLOR_ROSTER_SUBSCRIPTION)) + if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', + 'none'): + 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)) + self.addstr(get_theme().CHAR_ROSTER_ASKED, + to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) if show_s2s_errors and contact.error: - self.addstr(get_theme().CHAR_ROSTER_ERROR, to_curses_attr(get_theme().COLOR_ROSTER_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)) + self.addstr(get_theme().CHAR_ROSTER_TUNE, + to_curses_attr(get_theme().COLOR_ROSTER_TUNE)) if contact.activity: - self.addstr(get_theme().CHAR_ROSTER_ACTIVITY, to_curses_attr(get_theme().COLOR_ROSTER_ACTIVITY)) + self.addstr(get_theme().CHAR_ROSTER_ACTIVITY, + to_curses_attr(get_theme().COLOR_ROSTER_ACTIVITY)) if contact.mood: - self.addstr(get_theme().CHAR_ROSTER_MOOD, to_curses_attr(get_theme().COLOR_ROSTER_MOOD)) + self.addstr(get_theme().CHAR_ROSTER_MOOD, + to_curses_attr(get_theme().COLOR_ROSTER_MOOD)) if contact.gaming: - self.addstr(get_theme().CHAR_ROSTER_GAMING, to_curses_attr(get_theme().COLOR_ROSTER_GAMING)) + self.addstr(get_theme().CHAR_ROSTER_GAMING, + to_curses_attr(get_theme().COLOR_ROSTER_GAMING)) self.finish_line() def draw_resource_line(self, y, resource, colored): @@ -295,7 +320,9 @@ class RosterWin(Win): color = get_theme().color_show(resource.presence) self.addstr(y, 4, get_theme().CHAR_STATUS, to_curses_attr(color)) if colored: - self.addstr(y, 8, self.truncate_name(str(resource.jid), 6), to_curses_attr(get_theme().COLOR_SELECTED_ROW)) + self.addstr(y, 8, + self.truncate_name(str(resource.jid), 6), + to_curses_attr(get_theme().COLOR_SELECTED_ROW)) else: self.addstr(y, 8, self.truncate_name(str(resource.jid), 6)) self.finish_line() @@ -308,6 +335,7 @@ class RosterWin(Win): return self.roster_cache[self.pos] return None + class ContactInfoWin(Win): def draw_contact_info(self, contact): """ @@ -319,23 +347,27 @@ class ContactInfoWin(Win): elif resource: jid = resource.jid else: - jid = 'example@example.com' # should never happen + jid = 'example@example.com' # should never happen if resource: presence = resource.presence else: presence = 'unavailable' i = 0 - self.addstr(0, 0, '%s (%s)'%(jid, presence,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(0, 0, '%s (%s)' % ( + jid, + presence, + ), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self.finish_line(get_theme().COLOR_INFORMATION_BAR) i += 1 - self.addstr(i, 0, 'Subscription: %s' % (contact.subscription,)) + self.addstr(i, 0, 'Subscription: %s' % (contact.subscription, )) self.finish_line() i += 1 if contact.ask: if contact.ask == 'asked': - self.addstr(i, 0, 'Ask: %s' % (contact.ask,), to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) + self.addstr(i, 0, 'Ask: %s' % (contact.ask, ), + to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) else: - self.addstr(i, 0, 'Ask: %s' % (contact.ask,)) + self.addstr(i, 0, 'Ask: %s' % (contact.ask, )) self.finish_line() i += 1 if resource: @@ -344,27 +376,34 @@ class ContactInfoWin(Win): i += 1 if contact.error: - self.addstr(i, 0, 'Error: %s' % contact.error, to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) + self.addstr(i, 0, 'Error: %s' % contact.error, + to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) self.finish_line() i += 1 if contact.tune: - self.addstr(i, 0, 'Tune: %s' % common.format_tune_string(contact.tune), to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) + self.addstr(i, 0, + 'Tune: %s' % common.format_tune_string(contact.tune), + to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) self.finish_line() i += 1 if contact.mood: - self.addstr(i, 0, 'Mood: %s' % contact.mood, to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) + self.addstr(i, 0, 'Mood: %s' % contact.mood, + to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) self.finish_line() i += 1 if contact.activity: - self.addstr(i, 0, 'Activity: %s' % contact.activity, to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) + self.addstr(i, 0, 'Activity: %s' % contact.activity, + to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) self.finish_line() i += 1 if contact.gaming: - self.addstr(i, 0, 'Game: %s' % common.format_gaming_string(contact.gaming), to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) + self.addstr( + i, 0, 'Game: %s' % common.format_gaming_string(contact.gaming), + to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) self.finish_line() i += 1 @@ -372,7 +411,8 @@ class ContactInfoWin(Win): """ draw the group information """ - self.addstr(0, 0, group.name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(0, 0, group.name, + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self.finish_line(get_theme().COLOR_INFORMATION_BAR) def refresh(self, selected_row): diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py index 4b16043d..1521ee34 100644 --- a/poezio/windows/text_win.py +++ b/poezio/windows/text_win.py @@ -21,6 +21,7 @@ from poezio.theming import to_curses_attr, get_theme, dump_tuple # text_end are the position delimiting the text in this line. class Line: __slots__ = ('msg', 'start_pos', 'end_pos', 'prepend') + def __init__(self, msg, start_pos, end_pos, prepend): self.msg = msg self.start_pos = start_pos @@ -35,7 +36,7 @@ class BaseTextWin(Win): Win.__init__(self) self.lines_nb_limit = lines_nb_limit self.pos = 0 - self.built_lines = [] # Each new message is built and kept here. + self.built_lines = [] # Each new message is built and kept here. # on resize, we rebuild all the messages self.lock = False @@ -73,13 +74,20 @@ class BaseTextWin(Win): self.pos = 0 return self.pos != pos - def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False, nick_size=10): + 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) + lines = self.build_message( + message, timestamp=timestamp, nick_size=nick_size) if self.lock: self.lock_buffer.extend(lines) else: @@ -143,16 +151,22 @@ class BaseTextWin(Win): 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) + 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))) + 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) @@ -179,8 +193,8 @@ class TextWin(BaseTextWin): highlights, scroll to the end of the buffer. """ log.debug('Going to the next highlight…') - if (not self.highlights or self.hl_pos != self.hl_pos or - self.hl_pos >= len(self.highlights) - 1): + if (not self.highlights or self.hl_pos != self.hl_pos + or self.hl_pos >= len(self.highlights) - 1): self.hl_pos = float('nan') self.pos = 0 return @@ -196,7 +210,7 @@ class TextWin(BaseTextWin): try: pos = self.built_lines.index(hl) except ValueError: - self.highlights = self.highlights[self.hl_pos+1:] + self.highlights = self.highlights[self.hl_pos + 1:] if not self.highlights: self.hl_pos = float('nan') self.pos = 0 @@ -230,7 +244,7 @@ class TextWin(BaseTextWin): try: pos = self.built_lines.index(hl) except ValueError: - self.highlights = self.highlights[self.hl_pos+1:] + self.highlights = self.highlights[self.hl_pos + 1:] if not self.highlights: self.hl_pos = float('nan') self.pos = 0 @@ -247,7 +261,8 @@ class TextWin(BaseTextWin): present, scroll at the top of the window """ if None in self.built_lines: - self.pos = len(self.built_lines) - self.built_lines.index(None) - self.height + 1 + self.pos = len(self.built_lines) - self.built_lines.index( + None) - self.height + 1 if self.pos < 0: self.pos = 0 else: @@ -257,7 +272,8 @@ class TextWin(BaseTextWin): # Make “next highlight” work afterwards. This makes it easy to # review all the highlights since the separator was placed, in # the correct order. - self.hl_pos = len(self.highlights) - self.nb_of_highlights_after_separator - 1 + self.hl_pos = len( + self.highlights) - self.nb_of_highlights_after_separator - 1 log.debug("self.hl_pos = %s", self.hl_pos) def remove_line_separator(self): @@ -282,13 +298,20 @@ class TextWin(BaseTextWin): 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, nick_size=10): + 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) + lines = self.build_message( + message, timestamp=timestamp, nick_size=nick_size) if self.lock: self.lock_buffer.extend(lines) else: @@ -316,8 +339,8 @@ class TextWin(BaseTextWin): if not txt: return [] if len(message.str_time) > 8: - default_color = (FORMAT_CHAR + dump_tuple(get_theme().COLOR_LOG_MSG) - + '}') + default_color = ( + FORMAT_CHAR + dump_tuple(get_theme().COLOR_LOG_MSG) + '}') else: default_color = None ret = [] @@ -329,11 +352,11 @@ class TextWin(BaseTextWin): else: offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1 if nick: - offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length + offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length if message.revisions > 0: offset += ceil(log10(message.revisions + 1)) if message.me: - offset += 1 # '* ' before and ' ' after + offset += 1 # '* ' before and ' ' after if timestamp: if message.str_time: offset += 1 + len(message.str_time) @@ -341,11 +364,15 @@ class TextWin(BaseTextWin): offset += 1 if get_theme().CHAR_TIME_RIGHT and message.str_time: offset += 1 - lines = poopt.cut_text(txt, self.width-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) + 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) @@ -364,7 +391,7 @@ class TextWin(BaseTextWin): if self.pos == 0: lines = self.built_lines[-self.height:] else: - lines = self.built_lines[-self.height-self.pos:-self.pos] + 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) @@ -374,14 +401,17 @@ class TextWin(BaseTextWin): if line: msg = line.msg if line.start_pos == 0: - offset = self.write_pre_msg(msg, with_timestamps, nick_size) + offset = self.write_pre_msg(msg, with_timestamps, + nick_size) elif y == 0: - offset = self.compute_offset(msg, with_timestamps, nick_size) - self.write_text(y, offset, line.prepend - + line.msg.txt[line.start_pos:line.end_pos]) + offset = self.compute_offset(msg, with_timestamps, + nick_size) + self.write_text( + y, offset, + line.prepend + line.msg.txt[line.start_pos:line.end_pos]) else: self.write_line_separator(y) - if y != self.height-1: + if y != self.height - 1: self.addstr('\n') self._win.attrset(0) self._refresh() @@ -391,7 +421,7 @@ class TextWin(BaseTextWin): if with_timestamps and msg.str_time: offset += poopt.wcswidth(msg.str_time) + 1 - if not msg.nickname: # not a message, nothing to do afterwards + if not msg.nickname: # not a message, nothing to do afterwards return offset nick = truncate_nick(msg.nickname, nick_size) @@ -410,13 +440,12 @@ class TextWin(BaseTextWin): offset += self.write_revisions(msg) return offset - def write_pre_msg(self, msg, with_timestamps, nick_size): offset = 0 if with_timestamps: offset += self.write_time(msg.str_time) - if not msg.nickname: # not a message, nothing to do afterwards + if not msg.nickname: # not a message, nothing to do afterwards return offset nick = truncate_nick(msg.nickname, nick_size) @@ -448,7 +477,8 @@ class TextWin(BaseTextWin): def write_revisions(self, msg): if msg.revisions: - self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE)) + self._win.attron( + to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE)) self.addstr('%d' % msg.revisions) self._win.attrset(0) return ceil(log10(msg.revisions + 1)) @@ -457,8 +487,7 @@ class TextWin(BaseTextWin): def write_line_separator(self, y): char = get_theme().CHAR_NEW_TEXT_SEPARATOR self.addnstr(y, 0, - char * (self.width // len(char) - 1), - self.width, + char * (self.width // len(char) - 1), self.width, to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR)) def write_ack(self): @@ -505,23 +534,26 @@ class TextWin(BaseTextWin): """ with_timestamps = config.get('show_timestamps') nick_size = config.get('max_nick_length') - for i in range(len(self.built_lines)-1, -1, -1): + 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 while index >= 0 and self.built_lines[index] and self.built_lines[index].msg.identifier == old_id: self.built_lines.pop(index) index -= 1 index += 1 - lines = self.build_message(message, timestamp=with_timestamps, nick_size=nick_size) + lines = self.build_message( + message, timestamp=with_timestamps, nick_size=nick_size) for line in lines: self.built_lines.insert(index, line) index += 1 break def __del__(self): - log.debug('** TextWin: deleting %s built lines', (len(self.built_lines))) + log.debug('** TextWin: deleting %s built lines', + (len(self.built_lines))) del self.built_lines + class XMLTextWin(BaseTextWin): def __init__(self): BaseTextWin.__init__(self) @@ -534,7 +566,7 @@ class XMLTextWin(BaseTextWin): if self.pos == 0: lines = self.built_lines[-self.height:] else: - lines = self.built_lines[-self.height-self.pos:-self.pos] + lines = self.built_lines[-self.height - self.pos:-self.pos] self._win.move(0, 0) self._win.erase() for y, line in enumerate(lines): @@ -548,7 +580,7 @@ class XMLTextWin(BaseTextWin): self.write_time(msg.str_time) self.write_prefix(msg.nickname, color) self.addstr(' ') - if y != self.height-1: + if y != self.height - 1: self.addstr('\n') self._win.attrset(0) for y, line in enumerate(lines): @@ -563,9 +595,10 @@ class XMLTextWin(BaseTextWin): # 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.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() @@ -577,18 +610,22 @@ class XMLTextWin(BaseTextWin): nick = truncate_nick(message.nickname, nick_size) offset = 0 if nick: - offset += poopt.wcswidth(nick) + 1 # + nick + ' ' length + 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) + 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) + 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) @@ -604,4 +641,3 @@ class XMLTextWin(BaseTextWin): self._win.attron(to_curses_attr(color)) self.addstr(truncate_nick(nickname)) self._win.attroff(to_curses_attr(color)) - diff --git a/poezio/xhtml.py b/poezio/xhtml.py index 2f73cd3d..ee98f23d 100644 --- a/poezio/xhtml.py +++ b/poezio/xhtml.py @@ -4,7 +4,6 @@ # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. - """ Various methods to convert shell colors to poezio colors, @@ -26,7 +25,7 @@ from xml.sax import saxutils from slixmpp.xmlstream import ET from poezio.config import config -digits = '0123456789' # never trust the modules +digits = '0123456789' # never trust the modules XHTML_NS = 'http://www.w3.org/1999/xhtml' @@ -190,8 +189,11 @@ poezio_format_trim = re.compile(r'(\x19\d+}|\x19\d|\x19[buaio]|\x19o)+\x19o') xhtml_simple_attr_re = re.compile(r'\x19\d') -def get_body_from_message_stanza(message, use_xhtml=False, - tmp_dir=None, extract_images=False): + +def get_body_from_message_stanza(message, + use_xhtml=False, + tmp_dir=None, + extract_images=False): """ Returns a string with xhtml markups converted to poezio colors if there's an xhtml_im element, or @@ -205,11 +207,12 @@ def get_body_from_message_stanza(message, use_xhtml=False, xhtml_body = xhtml.find('{http://www.w3.org/1999/xhtml}body') if xhtml_body is None: return message['body'] - content = xhtml_to_poezio_colors(xhtml_body, tmp_dir=tmp_dir, - extract_images=extract_images) + content = xhtml_to_poezio_colors( + xhtml_body, tmp_dir=tmp_dir, extract_images=extract_images) content = content if content else message['body'] return content or " " + def ncurses_color_to_html(color): """ Takes an int between 0 and 256 and returns @@ -219,8 +222,8 @@ def ncurses_color_to_html(color): if color <= 15: try: (r, g, b) = curses.color_content(color) - except: # fallback in faulty terminals (e.g. xterm) - (r, g, b) = curses.color_content(color%8) + except: # fallback in faulty terminals (e.g. xterm) + (r, g, b) = curses.color_content(color % 8) r = r / 1000 * 6 - 0.01 g = g / 1000 * 6 - 0.01 b = b / 1000 * 6 - 0.01 @@ -234,7 +237,9 @@ def ncurses_color_to_html(color): else: color -= 232 r = g = b = color / 24 * 6 - return '#%02X%02X%02X' % (int(r*256/6), int(g*256/6), int(b*256/6)) + return '#%02X%02X%02X' % (int(r * 256 / 6), int(g * 256 / 6), + int(b * 256 / 6)) + def _parse_css_color(name): if name[0] == '#': @@ -257,11 +262,12 @@ def _parse_css_color(name): if r == g == b: return int(232 + 1.54 * r) mult = 0.3984 - return 6*6*int(mult*r) + 6*int(mult*g) + int(mult*b) + 16 + return 6 * 6 * int(mult * r) + 6 * int(mult * g) + int(mult * b) + 16 if name in colors: return colors[name] return -1 + def _parse_css(css): shell = '' rules = css.split(';') @@ -272,7 +278,7 @@ def _parse_css(css): key = key.strip() value = value.strip() if key == 'background-color': - pass#shell += '\x191' + pass #shell += '\x191' elif key == 'color': color = _parse_css_color(value) if color != -1: @@ -292,13 +298,17 @@ def _parse_css(css): shell += '\x19a' return shell + def _trim(string): return re.sub(whitespace_re, ' ', string) + def get_hash(data: bytes) -> str: # Currently using SHA-256, this might change in the future. # base64 gives shorter hashes than hex, so use that. - return b64encode(hashlib.sha256(data).digest()).rstrip(b'=').replace(b'/', b'-').decode() + return b64encode(hashlib.sha256(data).digest()).rstrip(b'=').replace( + b'/', b'-').decode() + class XHTMLHandler(sax.ContentHandler): def __init__(self, force_ns=False, tmp_dir=None, extract_images=False): @@ -317,7 +327,8 @@ class XHTMLHandler(sax.ContentHandler): @property def result(self): - sanitized = re.sub(poezio_color_double, r'\1', ''.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): @@ -336,7 +347,10 @@ class XHTMLHandler(sax.ContentHandler): return builder = self.builder - attrs = {name: value for ((ns, name), value) in attrs.items() if ns is None} + attrs = { + name: value + for ((ns, name), value) in attrs.items() if ns is None + } self.attrs.append(attrs) if 'style' in attrs and self.enable_css_parsing: @@ -357,7 +371,9 @@ class XHTMLHandler(sax.ContentHandler): self.append_formatting('\x19i') elif name == 'img': if re.match(xhtml_data_re, attrs['src']) and self.extract_images: - type_, data = [i for i in re.split(xhtml_data_re, attrs['src']) if i] + type_, data = [ + i for i in re.split(xhtml_data_re, attrs['src']) if i + ] bin_data = b64decode(unquote(data)) filename = get_hash(bin_data) + '.' + type_ filepath = path.join(self.tmp_dir, filename) @@ -408,8 +424,10 @@ class XHTMLHandler(sax.ContentHandler): if name == 'a': self.pop_formatting() # do not display the link twice - text_elements = [x for x in self.builder[self.a_start:] - if not x.startswith('\x19')] + text_elements = [ + x for x in self.builder[self.a_start:] + if not x.startswith('\x19') + ] link_text = ''.join(text_elements).strip() if 'href' in attrs and attrs['href'] != link_text: builder.append(' (%s)' % _trim(attrs['href'])) @@ -429,20 +447,23 @@ class XHTMLHandler(sax.ContentHandler): if 'title' in attrs: builder.append(' [' + attrs['title'] + ']') -def xhtml_to_poezio_colors(xml, force=False, tmp_dir=None, extract_images=None): + +def xhtml_to_poezio_colors(xml, force=False, tmp_dir=None, + extract_images=None): if isinstance(xml, str): xml = xml.encode('utf8') elif not isinstance(xml, bytes): xml = ET.tostring(xml) - handler = XHTMLHandler(force_ns=force, tmp_dir=tmp_dir, - extract_images=extract_images) + handler = XHTMLHandler( + force_ns=force, tmp_dir=tmp_dir, extract_images=extract_images) parser = sax.make_parser() parser.setFeature(sax.handler.feature_namespaces, True) parser.setContentHandler(handler) parser.parse(BytesIO(xml)) return handler.result + def clean_text(s): """ Remove all xhtml-im attributes (\x19etc) from the string with the @@ -451,6 +472,7 @@ def clean_text(s): s = re.sub(xhtml_attr_re, "", s) return s + def clean_text_simple(string): """ Remove all \x19 from the string formatted with simple colors: @@ -458,10 +480,11 @@ def clean_text_simple(string): """ pos = string.find('\x19') while pos != -1: - string = string[:pos] + string[pos+2:] + string = string[:pos] + string[pos + 2:] pos = string.find('\x19') return string + def convert_simple_to_full_colors(text): """ takes a \x19n formatted string and returns @@ -469,15 +492,28 @@ def convert_simple_to_full_colors(text): """ # TODO, have a single list of this. This is some sort of # dusplicate from windows.format_chars - mapping = str.maketrans({'\x0E': '\x19b', '\x0F': '\x19o', '\x10': '\x19u', - '\x11': '\x191', '\x12': '\x192', '\x13': '\x193', - '\x14': '\x194', '\x15': '\x195', '\x16': '\x196', - '\x17': '\x197', '\x18': '\x198', '\x19': '\x199'}) + mapping = str.maketrans({ + '\x0E': '\x19b', + '\x0F': '\x19o', + '\x10': '\x19u', + '\x11': '\x191', + '\x12': '\x192', + '\x13': '\x193', + '\x14': '\x194', + '\x15': '\x195', + '\x16': '\x196', + '\x17': '\x197', + '\x18': '\x198', + '\x19': '\x199' + }) text = text.translate(mapping) + def add_curly_bracket(match): return match.group(0) + '}' + return re.sub(xhtml_simple_attr_re, add_curly_bracket, text) + number_to_color_names = { 1: 'red', 2: 'green', @@ -488,9 +524,11 @@ number_to_color_names = { 7: 'white' } + def format_inline_css(_dict): return ''.join(('%s: %s;' % (key, value) for key, value in _dict.items())) + def poezio_colors_to_html(string): """ Convert poezio colors to html @@ -514,11 +552,12 @@ def poezio_colors_to_html(string): build.append('</span>') while next_attr_char != -1: - attr_char = string[next_attr_char+1].lower() + attr_char = string[next_attr_char + 1].lower() if next_attr_char != 0 and string[:next_attr_char]: if current_attrs and not tag_open: - build.append('<span style="%s">' % format_inline_css(current_attrs)) + build.append( + '<span style="%s">' % format_inline_css(current_attrs)) tag_open = True build.append(saxutils.escape(string[:next_attr_char])) @@ -535,15 +574,17 @@ def poezio_colors_to_html(string): check_property('font-style', 'italic') if attr_char in digits: - number_str = string[next_attr_char+1:string.find('}', next_attr_char)] + number_str = string[next_attr_char + 1:string.find( + '}', next_attr_char)] number = int(number_str) if number in number_to_color_names: - check_property('color', number_to_color_names.get(number, 'black')) + check_property('color', + number_to_color_names.get(number, 'black')) else: check_property('color', ncurses_color_to_html(number)) - string = string[next_attr_char+len(number_str)+2:] + string = string[next_attr_char + len(number_str) + 2:] else: - string = string[next_attr_char+2:] + string = string[next_attr_char + 2:] next_attr_char = string.find('\x19') if current_attrs and not tag_open and string: |