diff options
Diffstat (limited to 'src')
38 files changed, 1097 insertions, 1125 deletions
diff --git a/src/bookmark.py b/src/bookmark.py index 672fb4a5..15a28c9d 100644 --- a/src/bookmark.py +++ b/src/bookmark.py @@ -8,10 +8,12 @@ bookmark storage. It can also parse xml Elements. This module also defines several functions for retrieving and updating bookmarks, both local and remote. """ + +import functools import logging from sys import version_info -from sleekxmpp.plugins.xep_0048 import Bookmarks, Conference +from slixmpp.plugins.xep_0048 import Bookmarks, Conference from common import safeJID from config import config @@ -26,7 +28,7 @@ def xml_iter(xml, tag=''): preferred = config.get('use_bookmarks_method').lower() if preferred not in ('pep', 'privatexml'): preferred = 'privatexml' -not_preferred = 'privatexml' if preferred == 'pep' else 'privatexml' +not_preferred = 'privatexml' if preferred == 'pep' else 'pep' methods = ('local', preferred, not_preferred) @@ -131,21 +133,18 @@ def save_privatexml(xmpp): xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('privatexml'), method='xep_0049') -def save_remote(xmpp, method=preferred): +def save_remote(xmpp, callback, method=preferred): """Save the remote bookmarks.""" method = 'privatexml' if method != 'pep' else 'pep' - try: - if method is 'privatexml': - xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('privatexml'), - method='xep_0049') - else: - xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('pep'), - method='xep_0223') - except Exception: - log.error("Could not save the bookmarks:", exc_info=True) - return False - return True + if method is 'privatexml': + xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('privatexml'), + method='xep_0049', + callback=callback) + else: + xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage('pep'), + method='xep_0223', + callback=callback) def save_local(): """Save the local bookmarks.""" @@ -155,62 +154,81 @@ def save_local(): def save(xmpp, core=None): """Save all the bookmarks.""" save_local() - if config.get('use_remote_bookmarks'): - preferred = config.get('use_bookmarks_method') - if not save_remote(xmpp, method=preferred) and core: + def _cb(core, iq): + if iq["type"] == "error": core.information('Could not save bookmarks.', 'Error') - return False elif core: core.information('Bookmarks saved', 'Info') - return True + if config.get('use_remote_bookmarks'): + preferred = config.get('use_bookmarks_method') + cb = functools.partial(_cb, core) + save_remote(xmpp, cb, method=preferred) -def get_pep(xmpp): +def get_pep(xmpp, available_methods, callback): """Add the remotely stored bookmarks via pep to the list.""" - try: - iq = xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0223', block=True) - except: - return False - for conf in xml_iter(iq.xml, '{storage:bookmarks}conference'): - b = Bookmark.parse_from_element(conf, method='pep') - if not get_by_jid(b.jid): - bookmarks.append(b) - return True - -def get_privatexml(xmpp): - """Add the remotely stored bookmarks via privatexml to the list.""" - try: - iq = xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0049', block=True) - except: - return False - for conf in xml_iter(iq.xml, '{storage:bookmarks}conference'): - b = Bookmark.parse_from_element(conf, method='privatexml') - if not get_by_jid(b.jid): - bookmarks.append(b) - return True + def _cb(iq): + if iq["type"] == "error": + available_methods["pep"] = False + else: + available_methods["pep"] = True + for conf in xml_iter(iq.xml, '{storage:bookmarks}conference'): + b = Bookmark.parse_from_element(conf, method='pep') + if not get_by_jid(b.jid): + bookmarks.append(b) + if callback: + callback() + + xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0223', callback=_cb) + +def get_privatexml(xmpp, available_methods, callback): + """Add the remotely stored bookmarks via privatexml to the list. + If both is True, we want to have the result of both methods (privatexml and pep) before calling pep""" + def _cb(iq): + if iq["type"] == "error": + available_methods["privatexml"] = False + else: + available_methods["privatexml"] = True + for conf in xml_iter(iq.xml, '{storage:bookmarks}conference'): + b = Bookmark.parse_from_element(conf, method='privatexml') + if not get_by_jid(b.jid): + bookmarks.append(b) + if callback: + callback() + + xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0049', callback=_cb) -def get_remote(xmpp): +def get_remote(xmpp, callback): """Add the remotely stored bookmarks to the list.""" if xmpp.anon: return method = config.get('use_bookmarks_method') if not method: - pep, privatexml = True, True + available_methods = {} + def _save_and_call_callback(): + # If both methods returned a result, we can now call the given callback + if callback and "privatexml" in available_methods and "pep" in available_methods: + save_bookmarks_method(available_methods) + if callback: + callback() for method in methods[1:]: if method == 'pep': - pep = get_pep(xmpp) + get_pep(xmpp, available_methods, _save_and_call_callback) else: - privatexml = get_privatexml(xmpp) - if pep and not privatexml: - config.set_and_save('use_bookmarks_method', 'pep') - elif privatexml and not pep: - config.set_and_save('use_bookmarks_method', 'privatexml') - elif not pep and not privatexml: - config.set_and_save('use_bookmarks_method', '') + get_privatexml(xmpp, available_methods, _save_and_call_callback) else: if method == 'pep': - get_pep(xmpp) + get_pep(xmpp, {}, callback) else: - get_privatexml(xmpp) + get_privatexml(xmpp, {}, callback) + +def save_bookmarks_method(available_methods): + pep, privatexml = available_methods["pep"], available_methods["privatexml"] + if pep and not privatexml: + config.set_and_save('use_bookmarks_method', 'pep') + elif privatexml and not pep: + config.set_and_save('use_bookmarks_method', 'privatexml') + elif not pep and not privatexml: + config.set_and_save('use_bookmarks_method', '') def get_local(): """Add the locally stored bookmarks to the list.""" diff --git a/src/common.py b/src/common.py index 8cb02d4c..a62c83f1 100644 --- a/src/common.py +++ b/src/common.py @@ -11,7 +11,7 @@ Various useful functions. from sys import version_info from datetime import datetime, timedelta -from sleekxmpp import JID, InvalidJID +from slixmpp import JID, InvalidJID import base64 import os @@ -261,7 +261,7 @@ def find_delayed_tag(message): """ Check if a message is delayed or not. - :param sleekxmpp.Message message: The message to check. + :param slixmpp.Message message: The message to check. :return: A tuple containing (True, the datetime) or (False, None) :rtype: :py:class:`tuple` """ @@ -471,7 +471,7 @@ def format_gaming_string(infos): def safeJID(*args, **kwargs): """ - Construct a :py:class:`sleekxmpp.JID` object from a string. + Construct a :py:class:`slixmpp.JID` object from a string. Used to avoid tracebacks during is stringprep fails (fall back to a JID with an empty string). diff --git a/src/config.py b/src/config.py index 1c191eb1..1f0771ca 100644 --- a/src/config.py +++ b/src/config.py @@ -591,7 +591,7 @@ def setup_logging(): log = logging.getLogger(__name__) def post_logging_setup(): - # common imports sleekxmpp, which creates then its loggers, so + # common imports slixmpp, which creates then its loggers, so # it needs to be after logger configuration from common import safeJID as JID global safeJID diff --git a/src/connection.py b/src/connection.py index dc35dd94..1bbe632d 100644 --- a/src/connection.py +++ b/src/connection.py @@ -14,15 +14,15 @@ log = logging.getLogger(__name__) import getpass -import sleekxmpp -from sleekxmpp.plugins.xep_0184 import XEP_0184 +import slixmpp +from slixmpp.plugins.xep_0184 import XEP_0184 import common import fixes from common import safeJID from config import config, options -class Connection(sleekxmpp.ClientXMPP): +class Connection(slixmpp.ClientXMPP): """ Receives everything from Jabber and emits the appropriate signals @@ -47,7 +47,7 @@ class Connection(sleekxmpp.ClientXMPP): password = None jid = safeJID(jid) # TODO: use the system language - sleekxmpp.ClientXMPP.__init__(self, jid, password, + slixmpp.ClientXMPP.__init__(self, jid, password, lang=config.get('lang')) force_encryption = config.get('force_encryption') @@ -95,7 +95,6 @@ class Connection(sleekxmpp.ClientXMPP): self.register_plugin('xep_0191') self.register_plugin('xep_0199') - self.set_keepalive_values() if config.get('enable_user_tune'): self.register_plugin('xep_0118') @@ -131,14 +130,18 @@ class Connection(sleekxmpp.ClientXMPP): self.register_plugin('xep_0280') self.register_plugin('xep_0297') self.register_plugin('xep_0308') + self.init_plugins() def set_keepalive_values(self, option=None, value=None): """ - Called at startup, or triggered when one of + Called after the XMPP session has been started, or triggered when one of "connection_timeout_delay" and "connection_check_interval" options - is changed. - Unload and reload the ping plugin, with the new values. + is changed. Unload and reload the ping plugin, with the new values. """ + if not self.is_connected(): + # Happens when we change the value with /set while we are not + # connected. Do nothing in that case + return ping_interval = config.get('connection_check_interval') timeout_delay = config.get('connection_timeout_delay') if timeout_delay <= 0: @@ -156,34 +159,27 @@ class Connection(sleekxmpp.ClientXMPP): def start(self): """ Connect and process events. - - TODO: try multiple servers with anon auth. """ custom_host = config.get('custom_host') custom_port = config.get('custom_port', 5222) if custom_port == -1: custom_port = 5222 if custom_host: - res = self.connect((custom_host, custom_port), reattempt=True) + self.connect((custom_host, custom_port)) elif custom_port != 5222 and custom_port != -1: - res = self.connect((self.boundjid.host, custom_port), - reattempt=True) + self.connect((self.boundjid.host, custom_port)) else: - res = self.connect(reattempt=True) - if not res: - return False - self.process(threaded=True) - return True + self.connect() - def send_raw(self, data, now=False, reconnect=None): + def send_raw(self, data): """ Overrides XMLStream.send_raw, with an event added """ if self.core: self.core.outgoing_stanza(data) - sleekxmpp.ClientXMPP.send_raw(self, data, now, reconnect) + slixmpp.ClientXMPP.send_raw(self, data) -class MatchAll(sleekxmpp.xmlstream.matcher.base.MatcherBase): +class MatchAll(slixmpp.xmlstream.matcher.base.MatcherBase): """ Callback to retrieve all the stanzas for the XML tab """ diff --git a/src/contact.py b/src/contact.py index 908b609e..c670e5bc 100644 --- a/src/contact.py +++ b/src/contact.py @@ -62,7 +62,7 @@ class Contact(object): """ def __init__(self, item): """ - item: a SleekXMPP RosterItem pointing to that contact + item: a slixmpp RosterItem pointing to that contact """ self.__item = item self.folded_states = defaultdict(lambda: True) diff --git a/src/core/commands.py b/src/core/commands.py index d212de9b..4a8f7f19 100644 --- a/src/core/commands.py +++ b/src/core/commands.py @@ -6,15 +6,16 @@ import logging log = logging.getLogger(__name__) +import functools import os import sys from datetime import datetime from gettext import gettext as _ from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.stanzabase import StanzaBase -from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.xmlstream.matcher import StanzaPath +from slixmpp.xmlstream.stanzabase import StanzaBase +from slixmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.matcher import StanzaPath import bookmark import common @@ -276,7 +277,6 @@ def command_list(self, arg): self.add_tab(list_tab, True) cb = list_tab.on_muc_list_item_received self.xmpp.plugin['xep_0030'].get_items(jid=server, - block=False, callback=cb) def command_version(self, arg): @@ -439,7 +439,7 @@ def command_bookmark_local(self, arg=''): new_bookmarks.extend(bookmark.bookmarks) bookmark.bookmarks = new_bookmarks bookmark.save_local() - bookmark.save_remote(self.xmpp) + bookmark.save_remote(self.xmpp, None) self.information('Bookmarks added and saved.', 'Info') return else: @@ -507,12 +507,13 @@ def command_bookmark(self, arg=''): new_bookmarks.append(b) new_bookmarks.extend(bookmark.bookmarks) bookmark.bookmarks = new_bookmarks - - if bookmark.save_remote(self.xmpp): - bookmark.save_local() - self.information("Bookmarks added.", "Info") - else: - self.information("Could not add the bookmarks.", "Info") + def _cb(self, iq): + if iq["type"] != "error": + bookmark.save_local() + self.information("Bookmarks added.", "Info") + else: + self.information("Could not add the bookmarks.", "Info") + bookmark.save_remote(self.xmpp, functools.partial(_cb, self)) return else: info = safeJID(args[0]) @@ -541,14 +542,16 @@ def command_bookmark(self, arg=''): if password: bm.password = password bm.autojoin = autojoin - if bookmark.save_remote(self.xmpp): - self.information('Bookmark added.', 'Info') + def _cb(self, iq): + if iq["type"] != "error": + self.information('Bookmark added.', 'Info') + else: + self.information("Could not add the bookmarks.", "Info") + bookmark.save_remote(self.xmpp, functools.partial(_cb, self)) remote = [] for each in bookmark.bookmarks: if each.method in ('pep', 'privatexml'): remote.append(each) - self.information(_('Your remote bookmarks are now: %s') % remote, - _('Info')) def command_bookmarks(self, arg=''): """/bookmarks""" @@ -718,7 +721,6 @@ def command_last_activity(self, arg): if jid == '': return self.command_help('last_activity') self.xmpp.plugin['xep_0012'].get_last_activity(jid, - block=False, callback=callback) def command_mood(self, arg): @@ -727,7 +729,8 @@ def command_mood(self, arg): """ args = common.shell_split(arg) if not args: - return self.xmpp.plugin['xep_0107'].stop(block=False) + self.xmpp.plugin['xep_0107'].stop() + return mood = args[0] if mood not in pep.MOODS: return self.information(_('%s is not a correct value for a mood.') @@ -737,10 +740,8 @@ def command_mood(self, arg): text = args[1] else: text = None - self.xmpp.plugin['xep_0107'].publish_mood(mood, - text, - callback=dumb_callback, - block=False) + self.xmpp.plugin['xep_0107'].publish_mood(mood, text, + callback=dumb_callback) def command_activity(self, arg): """ @@ -749,7 +750,8 @@ def command_activity(self, arg): args = common.shell_split(arg) length = len(args) if not length: - return self.xmpp.plugin['xep_0108'].stop(block=False) + self.xmpp.plugin['xep_0108'].stop() + return general = args[0] if general not in pep.ACTIVITIES: return self.information(_('%s is not a correct value for an activity') @@ -769,11 +771,8 @@ def command_activity(self, arg): return self.information(_('%s is not a correct value ' 'for an activity') % specific, _('Error')) - self.xmpp.plugin['xep_0108'].publish_activity(general, - specific, - text, - callback=dumb_callback, - block=False) + self.xmpp.plugin['xep_0108'].publish_activity(general, specific, text, + callback=dumb_callback) def command_gaming(self, arg): """ @@ -781,7 +780,8 @@ def command_gaming(self, arg): """ args = common.shell_split(arg) if not args: - return self.xmpp.plugin['xep_0196'].stop(block=False) + self.xmpp.plugin['xep_0196'].stop() + return name = args[0] if len(args) > 1: address = args[1] @@ -789,8 +789,7 @@ def command_gaming(self, arg): address = None return self.xmpp.plugin['xep_0196'].publish_gaming(name=name, server_address=address, - callback=dumb_callback, - block=False) + callback=dumb_callback) def command_invite(self, arg): """/invite <to> <room> [reason]""" @@ -834,22 +833,23 @@ def command_quit(self, arg=''): """ /quit """ + if not self.xmpp.is_connected(): + self.exit() + return if len(arg.strip()) != 0: msg = arg else: msg = None if config.get('enable_user_mood'): - self.xmpp.plugin['xep_0107'].stop(block=False) + self.xmpp.plugin['xep_0107'].stop() if config.get('enable_user_activity'): - self.xmpp.plugin['xep_0108'].stop(block=False) + self.xmpp.plugin['xep_0108'].stop() if config.get('enable_user_gaming'): - self.xmpp.plugin['xep_0196'].stop(block=False) + self.xmpp.plugin['xep_0196'].stop() self.save_config() self.plugin_manager.disable_plugins() self.disconnect(msg) - self.running = False - self.reset_curses() - sys.exit() + self.xmpp.add_event_handler("disconnected", self.exit, disposable=True) def command_destroy_room(self, arg=''): """ @@ -972,15 +972,13 @@ def command_adhoc(self, arg): if len(arg) > 1: return self.command_help('ad-hoc') elif arg: - jid = safeJID(arg[0]).server + jid = safeJID(arg[0]) else: return self.information('Please provide a jid', 'Error') list_tab = tabs.AdhocCommandsListTab(jid) self.add_tab(list_tab, True) cb = list_tab.on_list_received - self.xmpp.plugin['xep_0050'].get_commands(jid=jid, - local=False, - block=False, + self.xmpp.plugin['xep_0050'].get_commands(jid=jid, local=False, callback=cb) def command_self(self, arg=None): diff --git a/src/core/completions.py b/src/core/completions.py index 9549c13f..7d95321b 100644 --- a/src/core/completions.py +++ b/src/core/completions.py @@ -106,22 +106,7 @@ def completion_join(self, the_input): if the_input.last_completion: return the_input.new_completion([], 1, quotify=True) - if jid.server and not jid.user: - # no room was given: complete the node - try: - response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1) - except: - log.error('/join completion: Unable to get the list of rooms for %s', - jid.server, - exc_info=True) - response = None - if response: - items = response['disco_items'].get_items() - else: - return True - items = sorted('%s/%s' % (tup[0], jid.resource) for tup in items) - return the_input.new_completion(items, 1, quotify=True, override=True) - elif jid.user: + if jid.user: # we are writing the server: complete the server serv_list = [] for tab in self.get_tabs(tabs.MucTab): diff --git a/src/core/core.py b/src/core/core.py index 52199206..4daeed6c 100644 --- a/src/core/core.py +++ b/src/core/core.py @@ -9,7 +9,9 @@ import logging log = logging.getLogger(__name__) +import asyncio import collections +import shutil import curses import os import pipes @@ -19,7 +21,7 @@ from threading import Event from datetime import datetime from gettext import gettext as _ -from sleekxmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.handler import Callback import bookmark import connection @@ -37,14 +39,13 @@ from config import config, firstrun from contact import Contact, Resource from daemon import Executor from fifo import Fifo -from keyboard import Keyboard from logger import logger from plugin_manager import PluginManager from roster import roster from size_manager import SizeManager from text_buffer import TextBuffer from theming import get_theme -from windows import g_lock +import keyboard from . import completions from . import commands @@ -71,7 +72,7 @@ class Core(object): self.running = True self.xmpp = singleton.Singleton(connection.Connection) self.xmpp.core = self - self.keyboard = Keyboard() + self.keyboard = keyboard.Keyboard() roster.set_node(self.xmpp.client_roster) decorators.refresh_wrapper.core = self self.paused = False @@ -108,6 +109,13 @@ class Core(object): self.size = SizeManager(self, windows.Win) + # Set to True whenever we consider that we have been disconnected + # from the server because of a legitimate reason (bad credentials, + # or explicit disconnect from the user for example), in that case we + # should not try to auto-reconnect, even if auto_reconnect is true + # in the user config. + self.legitimate_disconnect = False + # global commands, available from all tabs # a command is tuple of the form: # (the function executing the command. Takes a string as argument, @@ -123,6 +131,11 @@ class Core(object): del self.commands['status'] del self.commands['show'] + # A list of integers. For example if the user presses Alt+j, 2, 1, + # we will insert 2, then 1 in that list, and we will finally build + # the number 21 and use it with command_win, before clearing the + # list. + self.room_number_jump = [] self.key_func = KeyDict() # Key bindings associated with handlers # and pseudo-keys used to map actions below. @@ -188,9 +201,12 @@ class Core(object): self.key_func.update(key_func) # Add handlers + self.xmpp.add_event_handler('connecting', self.on_connecting) self.xmpp.add_event_handler('connected', self.on_connected) + self.xmpp.add_event_handler('connection_failed', self.on_failed_connection) self.xmpp.add_event_handler('disconnected', self.on_disconnected) - self.xmpp.add_event_handler('failed_auth', self.on_failed_auth) + self.xmpp.add_event_handler('stream_error', self.on_stream_error) + self.xmpp.add_event_handler('failed_all_auth', self.on_failed_all_auth) self.xmpp.add_event_handler('no_auth', self.on_no_auth) self.xmpp.add_event_handler("session_start", self.on_session_start) self.xmpp.add_event_handler("session_start", @@ -259,8 +275,6 @@ class Core(object): self.initial_joins = [] - self.timed_events = set() - self.connected_events = {} self.pending_invites = {} @@ -296,6 +310,8 @@ class Core(object): theming.update_themes_dir) self.add_configuration_handler("theme", self.on_theme_config_change) + self.add_configuration_handler("password", + self.on_password_change) self.add_configuration_handler("", self.on_any_config_change) @@ -374,6 +390,12 @@ class Core(object): self.information(error_msg, 'Warning') self.refresh_window() + def on_password_change(self, option, value): + """ + Set the new password in the slixmpp.ClientXMPP object + """ + self.xmpp.password = value + def sigusr_handler(self, num, stack): """ Handle SIGUSR1 (10) @@ -422,19 +444,14 @@ class Core(object): log.error("%s received. Exiting…", signals[sig]) if config.get('enable_user_mood'): - self.xmpp.plugin['xep_0107'].stop(block=False) + self.xmpp.plugin['xep_0107'].stop() if config.get('enable_user_activity'): - self.xmpp.plugin['xep_0108'].stop(block=False) + self.xmpp.plugin['xep_0108'].stop() if config.get('enable_user_gaming'): - self.xmpp.plugin['xep_0196'].stop(block=False) + self.xmpp.plugin['xep_0196'].stop() self.plugin_manager.disable_plugins() - self.disconnect('') - self.running = False - try: - self.reset_curses() - except: # too bad - pass - sys.exit() + self.disconnect('%s received' % signals.get(sig)) + self.xmpp.add_event_handler("disconnected", self.exit, disposable=True) def autoload_plugins(self): """ @@ -469,6 +486,11 @@ class Core(object): ' 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,)) + asyncio.get_event_loop().stop() def on_exception(self, typ, value, trace): """ @@ -481,7 +503,28 @@ class Core(object): pass sys.__excepthook__(typ, value, trace) - def main_loop(self): + def sigwinch_handler(self): + """A work-around for ncurses resize stuff, which sucks. Normally, ncurses + catches SIGWINCH itself. In its signal handler, it updates the + windows structures (for example the size, etc) and it + ungetch(KEY_RESIZE). That way, the next time we call getch() we know + that a resize occured and we can act on it. BUT poezio doesn’t call + getch() until it knows it will return something. The problem is we + can’t know that, because stdin is not affected by this KEY_RESIZE + value (it is only inserted in a ncurses internal fifo that we can’t + access). + + The (ugly) solution is to handle SIGWINCH ourself, trigger the + change of the internal windows sizes stored in ncurses module, using + sizes that we get using shutil, ungetch the KEY_RESIZE value and + then call getch to handle the resize on poezio’s side properly. + """ + size = shutil.get_terminal_size() + curses.resizeterm(size.lines, size.columns) + curses.ungetch(curses.KEY_RESIZE) + self.on_input_readable() + + def on_input_readable(self): """ main loop waiting for the user to press a key """ @@ -528,39 +571,42 @@ class Core(object): res.append(current) return res - while self.running: - self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid) - big_char_list = [replace_key_with_bound(key)\ - for key in self.read_keyboard()] - # whether to refresh after ALL keys have been handled - for char_list in separate_chars_from_bindings(big_char_list): - if self.paused: - self.current_tab().input.do_command(char_list[0]) - self.current_tab().input.prompt() - self.event.set() - continue - # Special case for M-x where x is a number - if len(char_list) == 1: - char = char_list[0] - if char.startswith('M-') and len(char) == 3: - try: - nb = int(char[2]) - except ValueError: - pass - else: - if self.current_tab().nb == nb: - self.go_to_previous_tab() - else: - self.command_win('%d' % nb) - # search for keyboard shortcut - func = self.key_func.get(char, None) - if func: - func() + 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,)) + + # whether to refresh after ALL keys have been handled + for char_list in separate_chars_from_bindings(big_char_list): + if self.paused: + self.current_tab().input.do_command(char_list[0]) + self.current_tab().input.prompt() + self.event.set() + continue + # Special case for M-x where x is a number + if len(char_list) == 1: + char = char_list[0] + if char.startswith('M-') and len(char) == 3: + try: + nb = int(char[2]) + except ValueError: + pass else: - self.do_command(replace_line_breaks(char), False) + if self.current_tab().nb == nb: + self.go_to_previous_tab() + else: + self.command_win('%d' % nb) + # search for keyboard shortcut + func = self.key_func.get(char, None) + if func: + func() else: - self.do_command(''.join(char_list), True) - self.doupdate() + self.do_command(replace_line_breaks(char), False) + else: + self.do_command(''.join(char_list), True) + if self.status.show not in ('xa', 'away'): + self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid) + self.doupdate() def save_config(self): """ @@ -703,10 +749,21 @@ class Core(object): def do_command(self, key, raw): """ Execute the action associated with a key + + Or if keyboard.continuation_keys_callback is set, call it instead. See + the comment of this variable. """ if not key: return - return self.current_tab().on_input(key, raw) + if keyboard.continuation_keys_callback is not None: + # Reset the callback to None BEFORE calling it, because this + # callback MAY set a new callback itself, and we don’t want to + # erase it in that case + cb = keyboard.continuation_keys_callback + keyboard.continuation_keys_callback = None + cb(key) + else: + self.current_tab().on_input(key, raw) def try_execute(self, line): @@ -724,22 +781,13 @@ class Core(object): def remove_timed_event(self, event): """Remove an existing timed event""" - if event and event in self.timed_events: - self.timed_events.remove(event) + event.handler.cancel() def add_timed_event(self, event): """Add a new timed event""" - self.timed_events.add(event) - - def check_timed_events(self): - """Check for the execution of timed events""" - now = datetime.now() - for event in self.timed_events: - if event.has_timed_out(now): - res = event() - if not res: - self.timed_events.remove(event) - break + event.handler = asyncio.get_event_loop().call_later(event.delay, + event.callback, + *event.args) ####################### XMPP-related actions ################################## @@ -779,12 +827,15 @@ class Core(object): Disconnect from remote server and correctly set the states of all parts of the client (for example, set the MucTabs as not joined, etc) """ + self.legitimate_disconnect = True msg = msg or '' for tab in self.get_tabs(tabs.MucTab): tab.command_part(msg) self.xmpp.disconnect() if reconnect: - self.xmpp.start() + # 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) def send_message(self, msg): """ @@ -815,8 +866,8 @@ class Core(object): self.xmpp.plugin['xep_0045'].invite(room, jid, reason=reason or '') - self.xmpp.plugin['xep_0030'].get_info(jid=jid, block=False, - 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): """ @@ -1027,17 +1078,24 @@ class Core(object): Read 2 more chars and go to the tab with the given number """ - char = self.read_keyboard()[0] - try: - nb1 = int(char) - except ValueError: - return - char = self.read_keyboard()[0] - try: - nb2 = int(char) - except ValueError: - return - self.command_win('%s%s' % (nb1, nb2)) + def read_next_digit(digit): + try: + nb = int(digit) + except ValueError: + # If it is not a number, we do nothing. If it was the first + # one, we do not wait for a second one by re-setting the + # callback + self.room_number_jump.clear() + else: + self.room_number_jump.append(digit) + if len(self.room_number_jump) == 2: + arg = "".join(self.room_number_jump) + self.room_number_jump.clear() + self.command_win(arg) + 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): "Select the roster as the current tab" @@ -1505,41 +1563,39 @@ class Core(object): """ Resize the global_information_win only once at each resize. """ - with g_lock: - if self.information_win_size > tabs.Tab.height - 6: - 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) + if self.information_win_size > tabs.Tab.height - 6: + 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) def resize_global_info_bar(self): """ Resize the GlobalInfoBar only once at each resize """ - with g_lock: - height, width = self.stdscr.getmaxyx() - if config.get('enable_vertical_tab_list'): + height, width = self.stdscr.getmaxyx() + if config.get('enable_vertical_tab_list'): - if self.size.core_degrade_x: - return - try: - height, _ = self.stdscr.getmaxyx() - 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(truncated_win) - elif not self.size.core_degrade_y: - self.tab_win.resize(1, tabs.Tab.width, - tabs.Tab.height - 2, 0) - self.left_tab_win = None + if self.size.core_degrade_x: + return + try: + height, _ = self.stdscr.getmaxyx() + 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(truncated_win) + elif not self.size.core_degrade_y: + 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, time=None, nickname=None, history=None): @@ -1564,46 +1620,38 @@ class Core(object): Called when we want to resize the screen """ # If we have the tabs list on the left, we just give a truncated - # window to each Tab class, so the draw themself in the portion - # of the screen that the can occupy, and we draw the tab list - # on the left remaining space - with g_lock: - height, width = self.stdscr.getmaxyx() + # window to each Tab class, so they draw themself in the portion of + # 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): - with g_lock: - try: - scr = self.stdscr.subwin(0, - config.get('vertical_tab_list_size')) - except: - log.error('Curses error on resize', exc_info=True) - return + try: + scr = self.stdscr.subwin(0, + config.get('vertical_tab_list_size')) + except: + log.error('Curses error on resize', exc_info=True) + return else: scr = self.stdscr tabs.Tab.resize(scr) self.resize_global_info_bar() self.resize_global_information_win() - with g_lock: - for tab in self.tabs: - if config.get('lazy_resize'): - tab.need_resize = True - else: - tab.resize() - if self.tabs: - self.full_screen_redraw() + for tab in self.tabs: + if config.get('lazy_resize'): + tab.need_resize = True + else: + tab.resize() + if self.tabs: + self.full_screen_redraw() def read_keyboard(self): """ - Get the next keyboard key pressed and returns it. - get_user_input() has a timeout: it returns None when the timeout - occurs. In that case we do not return (we loop until we get - a non-None value), but we check for timed events instead. + Get the next keyboard key pressed and returns it. It blocks until + something can be read on stdin, this function must be called only if + there is something to read. No timeout ever occurs. """ - res = self.keyboard.get_user_input(self.stdscr) - while res is None: - self.check_timed_events() - res = self.keyboard.get_user_input(self.stdscr) - return res + return self.keyboard.get_user_input(self.stdscr) def escape_next_key(self): """ @@ -1883,9 +1931,11 @@ class Core(object): on_groupchat_presence = handlers.on_groupchat_presence on_failed_connection = handlers.on_failed_connection on_disconnected = handlers.on_disconnected - on_failed_auth = handlers.on_failed_auth + on_stream_error = handlers.on_stream_error + on_failed_all_auth = handlers.on_failed_all_auth on_no_auth = handlers.on_no_auth on_connected = handlers.on_connected + on_connecting = handlers.on_connecting on_session_start = handlers.on_session_start on_status_codes = handlers.on_status_codes on_groupchat_subject = handlers.on_groupchat_subject diff --git a/src/core/handlers.py b/src/core/handlers.py index dfcb3223..50dca216 100644 --- a/src/core/handlers.py +++ b/src/core/handlers.py @@ -5,16 +5,19 @@ XMPP-related handlers for the Core class import logging log = logging.getLogger(__name__) +import asyncio import curses +import functools import ssl +import sys import time from hashlib import sha1, sha512 from gettext import gettext as _ from os import path -from sleekxmpp import InvalidJID -from sleekxmpp.stanza import Message -from sleekxmpp.xmlstream.stanzabase import StanzaBase +from slixmpp import InvalidJID +from slixmpp.stanza import Message +from slixmpp.xmlstream.stanzabase import StanzaBase import bookmark import common @@ -49,47 +52,54 @@ def on_session_start_features(self, _): self.xmpp.plugin['xep_0280'].enable() self.xmpp.add_event_handler('carbon_received', self.on_carbon_received) self.xmpp.add_event_handler('carbon_sent', self.on_carbon_sent) - features = self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.domain, callback=callback, block=False) + + self.xmpp.plugin['xep_0030'].get_info(jid=self.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.xmpp.boundjid.full + if recv['receipt']: + return self.on_receipt(recv) + self.on_normal_message(recv) + recv = message['carbon_received'] if (recv['from'].bare not in roster or - roster[recv['from'].bare].subscription == 'none'): - try: - if fixes.has_identity(self.xmpp, recv['from'].server, - identity='conference'): - log.debug('%s has category conference, ignoring carbon', - recv['from'].server) - return - except: - log.debug('Traceback when getting the identity of a server:', - exc_info=True) - recv['to'] = self.xmpp.boundjid.full - if recv['receipt']: - return self.on_receipt(recv) - self.on_normal_message(recv) + roster[recv['from'].bare].subscription == 'none'): + fixes.has_identity(self.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) def on_carbon_sent(self, message): """ 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.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'): - try: - if fixes.has_identity(self.xmpp, sent['to'].server, - identity='conference'): - log.debug('%s has category conference, ignoring carbon', - sent['to'].server) - return - except: - log.debug('Traceback when getting the identity of a server:', - exc_info=True) - sent['from'] = self.xmpp.boundjid.full - self.on_normal_message(sent) + fixes.has_identity(self.xmpp, sent['to'].server, + identity='conference', + on_true=functools.partial(ignore_message, sent), + on_false=functools.partial(send_message, sent)) + else: + send_message(sent) ### Invites ### @@ -171,7 +181,8 @@ def on_message(self, message): def on_normal_message(self, message): """ - When receiving "normal" messages (from someone in our roster) + When receiving "normal" messages (not a private message from a + muc participant) """ if message['type'] == 'error': return self.information(self.get_error_message(message, deprecated=True), 'Error') @@ -630,7 +641,7 @@ def on_chatstate_groupchat_conversation(self, message, state): Chatstate received in a MUC """ nick = message['mucnick'] - room_from = message.getMucroom() + room_from = message.get_mucroom() tab = self.get_tab_by_name(room_from, tabs.MucTab) if tab and tab.get_user_by_name(nick): self.events.trigger('muc_chatstate', message, tab) @@ -828,27 +839,40 @@ def on_groupchat_presence(self, presence): ### Connection-related handlers ### -def on_failed_connection(self): +def on_failed_connection(self, error): """ We cannot contact the remote server """ - self.information(_("Connection to remote server failed"), _('Error')) + self.information(_("Connection to remote server failed: %s" % (error,)), _('Error')) def on_disconnected(self, event): """ When we are disconnected from remote server """ + # Stop the ping plugin. It would try to send stanza on regular basis + self.xmpp.plugin['xep_0199'].disable_keepalive() roster.modified() for tab in self.get_tabs(tabs.MucTab): tab.disconnect() self.information(_("Disconnected from server."), _('Error')) + if not self.legitimate_disconnect and config.get('auto_reconnect', False): + self.information(_("Auto-reconnecting."), _('Info')) + self.xmpp.connect() -def on_failed_auth(self, event): +def on_stream_error(self, event): + """ + When we receive a stream error + """ + if event and event['text']: + self.information(_('Stream error: %s') % event['text'], _('Error')) + +def on_failed_all_auth(self, event): """ Authentication failed """ self.information(_("Authentication failed (bad credentials?)."), _('Error')) + self.legitimate_disconnect = True def on_no_auth(self, event): """ @@ -856,6 +880,7 @@ def on_no_auth(self, event): """ self.information(_("Authentication failed, no login method available."), _('Error')) + self.legitimate_disconnect = True def on_connected(self, event): """ @@ -863,6 +888,12 @@ def on_connected(self, event): """ self.information(_("Connected to server."), 'Info') +def on_connecting(self, event): + """ + Just before we try to connect to the server + """ + self.legitimate_disconnect = False + def on_session_start(self, event): """ Called when we are connected and authenticated @@ -883,32 +914,42 @@ def on_session_start(self, event): self.events.trigger('send_normal_presence', pres) pres.send() bookmark.get_local() + def _join_initial_rooms(bookmarks): + """Join all rooms given in the iterator `bookmarks`""" + for bm in bookmarks: + if bm.autojoin or config.get('open_all_bookmarks'): + tab = self.get_tab_by_name(bm.jid, tabs.MucTab) + nick = bm.nick if bm.nick else self.own_nick + if not tab: + self.open_new_room(bm.jid, nick, False) + self.initial_joins.append(bm.jid) + histo_length = config.get('muc_history_length') + if histo_length == -1: + histo_length = None + if histo_length is not None: + histo_length = str(histo_length) + # do not join rooms that do not have autojoin + # but display them anyway + if bm.autojoin: + muc.join_groupchat(self, bm.jid, nick, + passwd=bm.password, + maxhistory=histo_length, + status=self.status.message, + show=self.status.show) + def _join_remote_only(): + remote_bookmarks = (bm for bm in bookmark.bookmarks if (bm.method in ("pep", "privatexml"))) + _join_initial_rooms(remote_bookmarks) if not self.xmpp.anon and config.get('use_remote_bookmarks'): - bookmark.get_remote(self.xmpp) - for bm in bookmark.bookmarks: - if bm.autojoin or config.get('open_all_bookmarks'): - tab = self.get_tab_by_name(bm.jid, tabs.MucTab) - nick = bm.nick if bm.nick else self.own_nick - if not tab: - self.open_new_room(bm.jid, nick, False) - self.initial_joins.append(bm.jid) - histo_length = config.get('muc_history_length') - if histo_length == -1: - histo_length = None - if histo_length is not None: - histo_length = str(histo_length) - # do not join rooms that do not have autojoin - # but display them anyway - if bm.autojoin: - muc.join_groupchat(self, bm.jid, nick, - passwd=bm.password, - maxhistory=histo_length, - status=self.status.message, - show=self.status.show) + bookmark.get_remote(self.xmpp, _join_remote_only) + # join all the available bookmarks. As of yet, this is just the local + # ones + _join_initial_rooms(bookmark.bookmarks) if config.get('enable_user_nick'): - self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback, block=False) + self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback) self.xmpp.plugin['xep_0115'].update_caps() + # Start the ping's plugin regular event + self.xmpp.set_keepalive_values() ### Other handlers ### @@ -974,7 +1015,7 @@ def on_groupchat_subject(self, message): Triggered when the topic is changed. """ nick_from = message['mucnick'] - room_from = message.getMucroom() + room_from = message.get_mucroom() tab = self.get_tab_by_name(room_from, tabs.MucTab) subject = message['subject'] if subject is None or not tab: @@ -1079,7 +1120,7 @@ def incoming_stanza(self, stanza): def validate_ssl(self, pem): """ - Check the server certificate using the sleekxmpp ssl_cert event + Check the server certificate using the slixmpp ssl_cert event """ if config.get('ignore_certificate'): return @@ -1115,19 +1156,31 @@ def validate_ssl(self, pem): input.resize(1, self.current_tab().width, self.current_tab().height-1, 0) input.refresh() self.doupdate() - self.paused = True - while input.value is None: - self.event.wait() - self.current_tab().input = saved_input - self.paused = False - if input.value: - self.information('Setting new certificate: old: %s, new: %s' % (cert, sha2_found_cert), 'Info') - log.debug('Setting certificate to %s', sha2_found_cert) - if not config.silent_set('certificate', sha2_found_cert): - self.information(_('Unable to write in the config file'), 'Error') - else: - self.information('You refused to validate the certificate. You are now disconnected', 'Info') - self.xmpp.disconnect() + old_loop = asyncio.get_event_loop() + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + new_loop.add_reader(sys.stdin, self.on_input_readable) + future = asyncio.Future() + @asyncio.coroutine + def check_input(future): + while input.value is None: + yield from asyncio.sleep(0.01) + self.current_tab().input = saved_input + self.paused = False + if input.value: + self.information('Setting new certificate: old: %s, new: %s' % (cert, sha2_found_cert), 'Info') + log.debug('Setting certificate to %s', sha2_found_cert) + if not config.silent_set('certificate', sha2_found_cert): + self.information(_('Unable to write in the config file'), 'Error') + else: + self.information('You refused to validate the certificate. You are now disconnected', 'Info') + self.xmpp.disconnect() + new_loop.stop() + asyncio.set_event_loop(old_loop) + asyncio.async(check_input(future)) + new_loop.run_forever() + + else: log.debug('First time. Setting certificate to %s', sha2_found_cert) if not config.silent_set('certificate', sha2_found_cert): diff --git a/src/fixes.py b/src/fixes.py index 18c117d8..1c5da7c8 100644 --- a/src/fixes.py +++ b/src/fixes.py @@ -1,25 +1,26 @@ """ -Module used to provide fixes for sleekxmpp functions not yet fixed +Module used to provide fixes for slixmpp functions not yet fixed upstream. TODO: Check that they are fixed and remove those hacks """ -from sleekxmpp.stanza import Message -from sleekxmpp.xmlstream import ET +from slixmpp.stanza import Message +from slixmpp.xmlstream import ET import logging log = logging.getLogger(__name__) -def has_identity(xmpp, jid, identity): - try: - iq = xmpp.plugin['xep_0030'].get_info(jid=jid, block=True, timeout=1) +def has_identity(xmpp, jid, identity, on_true=None, on_false=None): + def _cb(iq): ident = lambda x: x[0] - return identity in map(ident, iq['disco_info']['identities']) - except: - log.debug('Traceback while retrieving identity', exc_info=True) - return False + res = identity in map(ident, iq['disco_info']['identities']) + if res and on_true is not 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): @@ -37,20 +38,20 @@ def get_version(xmpp, jid, callback=None, **kwargs): return handle_result(result) -def get_room_form(xmpp, room): +def get_room_form(xmpp, room, callback): + def _cb(result): + if result["type"] == "error": + callback(None) + xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') + if xform is None: + callback(None) + form = xmpp.plugin['xep_0004'].buildForm(xform) + callback(form) + iq = xmpp.make_iq_get(ito=room) query = ET.Element('{http://jabber.org/protocol/muc#owner}query') iq.append(query) - try: - result = iq.send() - except: - return False - xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') - if xform is None: - return False - form = xmpp.plugin['xep_0004'].buildForm(xform) - return form - + iq.send(callback=_cb) def _filter_add_receipt_request(self, stanza): """ diff --git a/src/keyboard.py b/src/keyboard.py index 0a1391ea..ec1e7d0a 100755 --- a/src/keyboard.py +++ b/src/keyboard.py @@ -18,6 +18,15 @@ import curses.ascii import logging log = logging.getLogger(__name__) +# A callback that will handle the next key entered by the user. For +# example if the user presses Ctrl+j, we set a callbacks, and the +# next key pressed by the user will be passed to this callback +# instead of the normal process of executing global keybard +# shortcuts or inserting text in the current output. The callback +# is always reset to None afterwards (to resume the normal +# processing of keys) +continuation_keys_callback = None + def get_next_byte(s): """ Read the next byte of the utf-8 char @@ -33,59 +42,7 @@ def get_next_byte(s): return (None, c) return (ord(c), c.encode('latin-1')) # returns a number and a bytes object -def get_char_list_old(s): - """ - Kept for compatibility for python versions without get_wchar() - (introduced in 3.3) Read one or more bytes, concatenate them to create a - unicode char. Also treat special bytes to create special chars (like - control, alt, etc), returns one or more utf-8 chars - - see http://en.wikipedia.org/wiki/UTF-8#Description - """ - ret_list = [] - # The list of all chars. For example if you paste a text, the list the chars pasted - # so that they can be handled at once. - (first, char) = get_next_byte(s) - while first is not None or char is not None: - if not isinstance(first, int): # Keyboard special, like KEY_HOME etc - return [char] - if first == 127 or first == 8: - ret_list.append("KEY_BACKSPACE") - break - s.timeout(0) # we are now getting the missing utf-8 bytes to get a whole char - if first < 127: # ASCII char on one byte - if first <= 26: # transform Ctrl+* keys - char = chr(first + 64) - ret_list.append("^"+char) - (first, char) = get_next_byte(s) - continue - if first == 27: - second = get_char_list_old(s) - if not second: # if escape was pressed, a second char - # has to be read. But it timed out. - return [] - res = 'M-%s' % (second[0],) - ret_list.append(res) - (first, char) = get_next_byte(s) - continue - if 194 <= first: - (code, c) = get_next_byte(s) # 2 bytes char - char += c - if 224 <= first: - (code, c) = get_next_byte(s) # 3 bytes char - char += c - if 240 <= first: - (code, c) = get_next_byte(s) # 4 bytes char - char += c - try: - ret_list.append(char.decode('utf-8')) # return all the concatened byte objets, decoded - except UnicodeDecodeError: - return None - # s.timeout(1) # timeout to detect a paste of many chars - (first, char) = get_next_byte(s) - return ret_list - -def get_char_list_new(s): +def get_char_list(s): ret_list = [] while True: try: @@ -96,6 +53,9 @@ def get_char_list_new(s): 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 + # more data to read, it will immediately timeout and return with the + # data we have so far s.timeout(0) if isinstance(key, int): ret_list.append(curses.keyname(key).decode()) @@ -132,7 +92,6 @@ def get_char_list_new(s): class Keyboard(object): def __init__(self): - self.get_char_list = get_char_list_new self.escape = False def escape_next_key(self): @@ -144,7 +103,7 @@ class Keyboard(object): """ self.escape = True - def get_user_input(self, s, timeout=1000): + def get_user_input(self, s): """ Returns a list of all the available characters to read (for example it may contain a whole text if there’s some lag, or the user pasted text, @@ -153,19 +112,11 @@ class Keyboard(object): blocking, we need to get out of it every now and then even if nothing was entered). """ - s.timeout(timeout) # The timeout for timed events to be checked every second - try: - ret_list = self.get_char_list(s) - except AttributeError: - # caught if screen.get_wch() does not exist. In that case we use the - # old version, so this exception is caught only once. No efficiency - # issue here. - log.debug("get_wch() missing, switching to old keyboard method") - self.get_char_list = get_char_list_old - ret_list = self.get_char_list(s) + # Disable the timeout + s.timeout(-1) + ret_list = get_char_list(s) if not ret_list: - # nothing at all was read, that’s a timed event timeout - return None + return ret_list if len(ret_list) != 1: if ret_list[-1] == '^M': ret_list.pop(-1) diff --git a/src/multiuserchat.py b/src/multiuserchat.py index ae8acb77..92d09a60 100644 --- a/src/multiuserchat.py +++ b/src/multiuserchat.py @@ -8,7 +8,7 @@ """ Implementation of the XEP-0045: Multi-User Chat. Add some facilities that are not available on the XEP_0045 -sleek plugin +slix plugin """ from gettext import gettext as _ @@ -47,7 +47,7 @@ def destroy_room(xmpp, room, reason='', altroom=''): _('Info')) else: xmpp.core.information(_('Room %s destroyed') % room, _('Info')) - iq.send(block=False, callback=callback) + iq.send(callback=callback) return True def send_private_message(xmpp, jid, line): @@ -97,7 +97,7 @@ def change_nick(core, jid, nick, status=None, show=None): def join_groupchat(core, jid, nick, passwd='', maxhistory=None, status=None, show=None, seconds=None): xmpp = core.xmpp - stanza = xmpp.makePresence(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') @@ -131,7 +131,7 @@ def set_user_role(xmpp, jid, nick, reason, role, callback=None): (role = 'none': eject user) """ jid = safeJID(jid) - iq = xmpp.makeIqSet() + 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}) if reason: @@ -142,7 +142,7 @@ def set_user_role(xmpp, jid, nick, reason, role, callback=None): iq.append(query) iq['to'] = jid if callback: - return iq.send(block=False, callback=callback) + return iq.send(callback=callback) try: return iq.send() except Exception as e: @@ -165,10 +165,10 @@ def set_user_affiliation(xmpp, muc_jid, affiliation, nick=None, jid=None, reason item.append(reason_item) query.append(item) - iq = xmpp.makeIqSet(query) + iq = xmpp.make_iq_set(query) iq['to'] = muc_jid if callback: - return iq.send(block=False, callback=callback) + return iq.send(callback=callback) try: return xmpp.plugin['xep_0045'].setAffiliation(str(muc_jid), str(jid) if jid else None, nick, affiliation) except: @@ -180,18 +180,18 @@ def cancel_config(xmpp, room): query = ET.Element('{http://jabber.org/protocol/muc#owner}query') x = ET.Element('{jabber:x:data}x', type='cancel') query.append(x) - iq = xmpp.makeIqSet(query) + iq = xmpp.make_iq_set(query) iq['to'] = room - iq.send(block=False) + iq.send() def configure_room(xmpp, room, form): if form is None: return - iq = xmpp.makeIqSet() + iq = xmpp.make_iq_set() iq['to'] = room query = ET.Element('{http://jabber.org/protocol/muc#owner}query') form = form.getXML('submit') query.append(form) iq.append(query) - iq.send(block=False) + iq.send() diff --git a/src/plugin.py b/src/plugin.py index 22a17eee..eb2a89e3 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -338,21 +338,21 @@ class PluginAPI(object): """ return self.plugin_manager.del_event_handler(module, *args, **kwargs) - def add_sleek_event_handler(self, module, event_name, handler): + def add_slix_event_handler(self, module, event_name, handler): """ - Add an event handler for a sleekxmpp event. + Add an event handler for a slixmpp event. :param str event_name: The event name. :param function handler: The handler function. - A list of the SleekXMPP events can be found here + A list of the slixmpp events can be found here http://sleekxmpp.com/event_index.html """ self.core.xmpp.add_event_handler(event_name, handler) - def del_sleek_event_handler(self, module, event_name, handler): + def del_slix_event_handler(self, module, event_name, handler): """ - Remove a handler for a SleekXMPP event + Remove a handler for a slixmpp event :param str event_name: The name of the targeted event. :param function handler: The function to remove from the handlers. diff --git a/src/plugin_manager.py b/src/plugin_manager.py index d5cb9bc1..d4cc7384 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -261,7 +261,7 @@ class PluginManager(object): def add_event_handler(self, module_name, event_name, handler, position=0): """ Add an event handler. If event_name isn’t in the event list, assume - it is a sleekxmpp event. + it is a slixmpp event. """ eh = self.event_handlers[module_name] eh.append((event_name, handler)) diff --git a/src/poezio.py b/src/poezio.py index f82f103f..9a26e135 100644 --- a/src/poezio.py +++ b/src/poezio.py @@ -57,17 +57,22 @@ def main(): if options.debug: cocore.debug = True cocore.start() + + # Warning: asyncio must always be imported after the config. Otherwise + # the asyncio logger will not follow our configuration and won't write + # the tracebacks in the correct file, etc + import asyncio + loop = asyncio.get_event_loop() + + loop.add_reader(sys.stdin, cocore.on_input_readable) + loop.add_signal_handler(signal.SIGWINCH, cocore.sigwinch_handler) + cocore.xmpp.start() + loop.run_forever() + # We reach this point only when loop.stop() is called try: - if not cocore.xmpp.start(): # Connect to remote server - cocore.on_failed_connection() - except: - cocore.running = False cocore.reset_curses() - print("Poezio could not start, maybe you tried aborting it while it was starting?\n" - "If you think it is abnormal, please run it with the -d option and report the bug.") - else: - log.error('------------------------ new poezio start ------------------------') - cocore.main_loop() # Refresh the screen, wait for user events etc + except: + pass if __name__ == '__main__': main() diff --git a/src/roster.py b/src/roster.py index d18a41c4..d2b99cef 100644 --- a/src/roster.py +++ b/src/roster.py @@ -19,18 +19,18 @@ from roster_sorting import SORTING_METHODS, GROUP_SORTING_METHODS from os import path as p from datetime import datetime from common import safeJID -from sleekxmpp.exceptions import IqError, IqTimeout +from slixmpp.exceptions import IqError, IqTimeout class Roster(object): """ - The proxy class to get the roster from SleekXMPP. + The proxy class to get the roster from slixmpp. Caches Contact and RosterGroup objects. """ def __init__(self): """ - node: the RosterSingle from SleekXMPP + node: the RosterSingle from slixmpp """ self.__node = None self.contact_filter = None # A tuple(function, *args) @@ -111,7 +111,7 @@ class Roster(object): return self.contacts[jid] def set_node(self, value): - """Set the SleekXMPP RosterSingle for our roster""" + """Set the slixmpp RosterSingle for our roster""" self.__node = value def get_groups(self, sort=''): diff --git a/src/size_manager.py b/src/size_manager.py index 7e01d5d0..1cad83fd 100644 --- a/src/size_manager.py +++ b/src/size_manager.py @@ -3,8 +3,6 @@ Size Manager: used to check size boundaries of the whole window and specific tabs """ -from windows import g_lock - THRESHOLD_WIDTH_DEGRADE = 45 THRESHOLD_HEIGHT_DEGRADE = 10 @@ -27,26 +25,22 @@ class SizeManager(object): @property def tab_degrade_x(self): - with g_lock: - _, x = self.tab_scr.getmaxyx() + _, x = self.tab_scr.getmaxyx() return x < THRESHOLD_WIDTH_DEGRADE @property def tab_degrade_y(self): - with g_lock: - y, x = self.tab_scr.getmaxyx() + y, x = self.tab_scr.getmaxyx() return y < THRESHOLD_HEIGHT_DEGRADE @property def core_degrade_x(self): - with g_lock: - y, x = self.core_scr.getmaxyx() + y, x = self.core_scr.getmaxyx() return x < FULL_WIDTH_DEGRADE @property def core_degrade_y(self): - with g_lock: - y, x = self.core_scr.getmaxyx() + y, x = self.core_scr.getmaxyx() return y < FULL_HEIGHT_DEGRADE diff --git a/src/tabs/adhoc_commands_list.py b/src/tabs/adhoc_commands_list.py index 87ee0c52..7f5abf6a 100644 --- a/src/tabs/adhoc_commands_list.py +++ b/src/tabs/adhoc_commands_list.py @@ -11,14 +11,14 @@ log = logging.getLogger(__name__) from . import ListTab -from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItem +from slixmpp.plugins.xep_0030.stanza.items import DiscoItem class AdhocCommandsListTab(ListTab): plugin_commands = {} plugin_keys = {} def __init__(self, jid): - ListTab.__init__(self, jid, + ListTab.__init__(self, jid.full, "“Enter”: execute selected command.", _('Ad-hoc commands of JID %s (Loading)') % jid, (('Node', 0), ('Description', 1))) diff --git a/src/tabs/basetabs.py b/src/tabs/basetabs.py index f684a08f..0a55640c 100644 --- a/src/tabs/basetabs.py +++ b/src/tabs/basetabs.py @@ -35,7 +35,6 @@ from decorators import refresh_wrapper from logger import logger from text_buffer import TextBuffer from theming import get_theme, dump_tuple -from windows import g_lock # getters for tab colors (lambdas, so that they are dynamic) @@ -187,9 +186,8 @@ class Tab(object): @staticmethod def resize(scr): - with g_lock: - Tab.height, Tab.width = scr.getmaxyx() - windows.Win._tab_win = scr + Tab.height, Tab.width = scr.getmaxyx() + windows.Win._tab_win = scr def missing_command_callback(self, command_name): """ @@ -447,12 +445,9 @@ class ChatTab(Tab): self.text_win = None self._text_buffer = TextBuffer() self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive" - # We keep a weakref of the event that will set our chatstate to "paused", so that + # 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 - # if that’s None, then no paused chatstate was sent recently - # if that’s a weakref returning None, then a paused chatstate was sent - # since the last input # Keeps the last sent message to complete it easily in completion_correct, and to replace it. self.last_sent_message = None self.key_func['M-v'] = self.move_separator @@ -625,17 +620,12 @@ class ChatTab(Tab): """ if not config.get_by_tabname('send_chat_states', self.general_jid): return - if self.timed_event_paused: - # check the weakref - event = self.timed_event_paused() - if event: - # the event already exists: we just update - # its date - event.change_date(datetime.now() + timedelta(seconds=4)) - return + # 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') self.core.add_timed_event(new_event) - self.timed_event_paused = weakref.ref(new_event) + self.timed_event_paused = new_event def cancel_paused_delay(self): """ @@ -643,12 +633,9 @@ class ChatTab(Tab): Called for example when the input is emptied, or when the message is sent """ - if self.timed_event_paused: - event = self.timed_event_paused() - if event: - self.core.remove_timed_event(event) - del event - self.timed_event_paused = None + if self.timed_event_paused is not None: + self.core.remove_timed_event(self.timed_event_paused) + self.timed_event_paused = None def command_correct(self, line): """ @@ -738,7 +725,7 @@ class OneToOneTab(ChatTab): "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(), block=False, timeout=5, + jid=self.get_dest_jid(), timeout=5, callback=self.features_checked) def command_attention(self, message=''): diff --git a/src/tabs/conversationtab.py b/src/tabs/conversationtab.py index 99f9fe47..52c503d7 100644 --- a/src/tabs/conversationtab.py +++ b/src/tabs/conversationtab.py @@ -188,7 +188,7 @@ class ConversationTab(OneToOneTab): self.add_message(msg) self.core.refresh_window() - self.core.xmpp.plugin['xep_0012'].get_last_activity(self.general_jid, block=False, callback=callback) + self.core.xmpp.plugin['xep_0012'].get_last_activity(self.general_jid, callback=callback) @refresh_wrapper.conditional def command_info(self, arg): diff --git a/src/tabs/muclisttab.py b/src/tabs/muclisttab.py index d7c68588..55d5c2bd 100644 --- a/src/tabs/muclisttab.py +++ b/src/tabs/muclisttab.py @@ -11,7 +11,7 @@ log = logging.getLogger(__name__) from . import ListTab -from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItem +from slixmpp.plugins.xep_0030.stanza.items import DiscoItem class MucListTab(ListTab): """ diff --git a/src/tabs/muctab.py b/src/tabs/muctab.py index fb89b0fa..547830cb 100644 --- a/src/tabs/muctab.py +++ b/src/tabs/muctab.py @@ -362,13 +362,15 @@ class MucTab(ChatTab): """ /configure """ - form = fixes.get_room_form(self.core.xmpp, self.name) - if not form: - self.core.information( + def on_form_received(form): + if not form: + self.core.information( _('Could not retrieve the configuration form'), _('Error')) - return - self.core.open_new_form(form, self.cancel_config, self.send_config) + return + self.core.open_new_form(form, self.cancel_config, self.send_config) + + form = fixes.get_room_form(self.core.xmpp, self.name, on_form_received) def cancel_config(self, form): """ diff --git a/src/tabs/rostertab.py b/src/tabs/rostertab.py index 26f429d0..878e89ed 100644 --- a/src/tabs/rostertab.py +++ b/src/tabs/rostertab.py @@ -175,7 +175,7 @@ class RosterInfoTab(Tab): jid = item.bare_jid elif isinstance(item, Resource): jid = item.jid.bare - self.core.xmpp.plugin['xep_0191'].block(jid, block=False, callback=callback) + self.core.xmpp.plugin['xep_0191'].block(jid, callback=callback) def completion_block(self, the_input): """ @@ -202,22 +202,21 @@ class RosterInfoTab(Tab): jid = item.bare_jid elif isinstance(item, Resource): jid = item.jid.bare - self.core.xmpp.plugin['xep_0191'].unblock(jid, block=False, callback=callback) + self.core.xmpp.plugin['xep_0191'].unblock(jid, callback=callback) def completion_unblock(self, the_input): """ Completion for /unblock """ + def on_result(iq): + if iq['type'] == 'error': + return + l = sorted(str(item) for item in iq['blocklist']['items']) + return the_input.new_completion(l, 1, quotify=False) + if the_input.get_argument_position(): - try: - iq = self.core.xmpp.plugin['xep_0191'].get_blocked(block=True) - except Exception as e: - iq = e.iq - finally: - if iq['type'] == 'error': - return - l = sorted(str(item) for item in iq['blocklist']['items']) - return the_input.new_completion(l, 1, quotify=False) + self.core.xmpp.plugin['xep_0191'].get_blocked(callback=on_result) + return True def command_list_blocks(self, arg=None): """ @@ -235,13 +234,16 @@ class RosterInfoTab(Tab): s = 'No blocked JIDs.' self.core.information(s, 'Info') - self.core.xmpp.plugin['xep_0191'].get_blocked(block=False, callback=callback) + self.core.xmpp.plugin['xep_0191'].get_blocked(callback=callback) def command_reconnect(self, args=None): """ /reconnect """ - self.core.disconnect(reconnect=True) + if self.core.xmpp.is_connected(): + self.core.disconnect(reconnect=True) + else: + self.core.xmpp.connect() def command_disconnect(self, args=None): """ @@ -419,8 +421,8 @@ 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, block=False) + self.core.xmpp.update_roster(jid, name=name, groups=groups, + subscription=subscription, callback=callback) def command_groupadd(self, args): """ @@ -459,8 +461,8 @@ 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, block=False) + self.core.xmpp.update_roster(jid, name=name, groups=new_groups, + subscription=subscription, callback=callback) def command_groupmove(self, arg): """ @@ -514,8 +516,8 @@ class RosterInfoTab(Tab): self.core.information('The group could not be set') log.debug('Error in groupmove:\n%s', iq) - self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription, - callback=callback, block=False) + self.core.xmpp.update_roster(jid, name=name, groups=new_groups, + subscription=subscription, callback=callback) def command_groupremove(self, args): """ @@ -554,8 +556,8 @@ 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, block=False) + self.core.xmpp.update_roster(jid, name=name, groups=new_groups, + subscription=subscription, callback=callback) def command_remove(self, args): """ diff --git a/src/tabs/xmltab.py b/src/tabs/xmltab.py index d33f4d48..083e97c5 100644 --- a/src/tabs/xmltab.py +++ b/src/tabs/xmltab.py @@ -12,8 +12,8 @@ log = logging.getLogger(__name__) import curses import os -from sleekxmpp.xmlstream import matcher -from sleekxmpp.xmlstream.handler import Callback +from slixmpp.xmlstream import matcher +from slixmpp.xmlstream.handler import Callback from . import Tab diff --git a/src/timed_events.py b/src/timed_events.py index a922ee03..6160645b 100644 --- a/src/timed_events.py +++ b/src/timed_events.py @@ -13,87 +13,47 @@ Once created, they must be added to the list of checked events with :py:func:`.PluginAPI.add_timed_event` (within a plugin). """ +import asyncio import logging log = logging.getLogger(__name__) import datetime -class TimedEvent(object): +class DelayedEvent(object): """ - An event with a callback that is called when the specified time is passed. - - Note that these events can NOT be used for very small delay or a very - precise date, since the check for events is done once per second, as - a maximum. - - The callback and its arguments should be passed as the lasts arguments. + 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, date, callback, *args): + def __init__(self, delay, callback, *args): """ - Create a new timed event. + Create a new DelayedEvent. - :param datetime.datetime date: Time at which the callback must be run. + :param int delay: The number of seconds. :param function callback: The handler that will be executed. :param \*args: Optional arguments passed to the handler. """ - self._callback = callback + self.callback = callback self.args = args self.repetive = False - self.next_call_date = date - - def __call__(self): - """ - the call should return False if this event should be remove from - the events list. - If it’s true, the date should be updated beforehand to a later date, - or else it will be called every second - """ - self._callback(*self.args) - return self.repetive + self.delay = delay + # An asyncio handler, as returned by call_later() or call_at() + self.handler = None - def has_timed_out(self, current_date): - """ - Check if the event has timed out. - - :param datetime.datetime current_date: The current date. - :returns: True if the callback should be called - :rtype: bool - """ - if self.next_call_date < current_date: - return True - else: - return False - - def change_date(self, date): - """ - Simply change the date of the event. - - :param datetime.datetime date: Next date. - """ - self.next_call_date = date - - def add_delay(self, delay): - """ - Add a delay (in seconds) to the date. - - :param int delay: The delay to add. - """ - self.next_call_date += datetime.timedelta(seconds=delay) - -class DelayedEvent(TimedEvent): +class TimedEvent(DelayedEvent): """ - 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. + 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, delay, callback, *args): + def __init__(self, date, callback, *args): """ - Create a new DelayedEvent. + Create a new timed event. - :param int delay: The number of seconds. + :param datetime.datetime date: Time at which the callback must be run. :param function callback: The handler that will be executed. :param \*args: Optional arguments passed to the handler. """ - date = datetime.datetime.now() + datetime.timedelta(seconds=delay) - TimedEvent.__init__(self, date, callback, *args) - + delta = date - datetime.datetime.now() + delay = delta.total_seconds() + DelayedEvent.__init__(self, delay, callback, *args) diff --git a/src/windows/__init__.py b/src/windows/__init__.py index adb07cbe..9e165201 100644 --- a/src/windows/__init__.py +++ b/src/windows/__init__.py @@ -2,7 +2,8 @@ Module exporting all the Windows, which are wrappers around curses wins used to display information on the screen """ -from . base_wins import Win, g_lock + +from . base_wins import Win from . data_forms import FormWin from . info_bar import GlobalInfoBar, VerticalGlobalInfoBar from . info_wins import InfoWin, XMLInfoWin, PrivateInfoWin, MucListInfoWin, \ diff --git a/src/windows/base_wins.py b/src/windows/base_wins.py index 44c62e91..574eee89 100644 --- a/src/windows/base_wins.py +++ b/src/windows/base_wins.py @@ -32,8 +32,6 @@ allowed_color_digits = ('0', '1', '2', '3', '4', '5', '6', '7') # text_end are the position delimiting the text in this line. Line = collections.namedtuple('Line', 'msg start_pos end_pos prepend') -g_lock = RLock() - LINES_NB_LIMIT = 4096 class DummyWin(object): @@ -69,8 +67,7 @@ class Win(object): """ Override if something has to be done on resize """ - with g_lock: - self._resize(height, width, y, x) + self._resize(height, width, y, x) def _refresh(self): self._win.noutrefresh() diff --git a/src/windows/data_forms.py b/src/windows/data_forms.py index 0b27291c..d6e2cc66 100644 --- a/src/windows/data_forms.py +++ b/src/windows/data_forms.py @@ -6,7 +6,7 @@ does not inherit from the Win base class), as it will create the others when needed. """ -from . import g_lock, Win +from . import Win from . inputs import Input from theming import to_curses_attr, get_theme @@ -61,12 +61,11 @@ class ColoredLabel(Win): self.refresh() def refresh(self): - with g_lock: - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addstr(0, 0, self.text) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() + self._win.erase() + self._win.attron(to_curses_attr(self.color)) + self.addstr(0, 0, self.text) + self._win.attroff(to_curses_attr(self.color)) + self._refresh() class DummyInput(FieldInput, Win): @@ -100,19 +99,18 @@ class BooleanWin(FieldInput, Win): self.refresh() def refresh(self): - with g_lock: - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*(8), self.width) - self.addstr(0, 2, "%s"%self.value) - self.addstr(0, 8, '→') - self.addstr(0, 0, '←') - if self.last_key == 'KEY_RIGHT': - self.addstr(0, 8, '') - else: - self.addstr(0, 0, '') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() + 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.addstr(0, 8, '→') + self.addstr(0, 0, '←') + if self.last_key == 'KEY_RIGHT': + self.addstr(0, 8, '') + else: + self.addstr(0, 0, '') + self._win.attroff(to_curses_attr(self.color)) + self._refresh() def reply(self): self._field['label'] = '' @@ -166,18 +164,17 @@ class TextMultiWin(FieldInput, Win): def refresh(self): if not self.edition_input: - with g_lock: - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - option = self.options[self.val_pos] - 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, '→') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() + self._win.erase() + self._win.attron(to_curses_attr(self.color)) + self.addnstr(0, 0, ' '*self.width, self.width) + option = self.options[self.val_pos] + 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, '→') + self._win.attroff(to_curses_attr(self.color)) + self._refresh() else: self.edition_input.refresh() @@ -219,20 +216,19 @@ class ListMultiWin(FieldInput, Win): self.refresh() def refresh(self): - with g_lock: - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - if self.options: - option = self.options[self.val_pos] - self.addstr(0, self.width//2-len(option)//2, option[0]['label']) - self.addstr(0, 2, '✔' if option[1] else '☐') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() + self._win.erase() + self._win.attron(to_curses_attr(self.color)) + self.addnstr(0, 0, ' '*self.width, self.width) + if self.val_pos > 0: + self.addstr(0, 0, '←') + if self.val_pos < len(self.options)-1: + self.addstr(0, self.width-1, '→') + if self.options: + option = self.options[self.val_pos] + self.addstr(0, self.width//2-len(option)//2, option[0]['label']) + self.addstr(0, 2, '✔' if option[1] else '☐') + self._win.attroff(to_curses_attr(self.color)) + self._refresh() def reply(self): self._field['label'] = '' @@ -267,19 +263,18 @@ class ListSingleWin(FieldInput, Win): self.refresh() def refresh(self): - with g_lock: - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - if self.options: - option = self.options[self.val_pos]['label'] - self.addstr(0, self.width//2-len(option)//2, option) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() + self._win.erase() + self._win.attron(to_curses_attr(self.color)) + self.addnstr(0, 0, ' '*self.width, self.width) + if self.val_pos > 0: + self.addstr(0, 0, '←') + if self.val_pos < len(self.options)-1: + self.addstr(0, self.width-1, '→') + if self.options: + option = self.options[self.val_pos]['label'] + self.addstr(0, self.width//2-len(option)//2, option) + self._win.attroff(to_curses_attr(self.color)) + self._refresh() def reply(self): self._field['label'] = '' @@ -310,19 +305,18 @@ class TextPrivateWin(TextSingleWin): TextSingleWin.__init__(self, field) def rewrite_text(self): - with g_lock: - self._win.erase() - if self.color: - self._win.attron(to_curses_attr(self.color)) - self.addstr('*'*len(self.text[self.view_pos:self.view_pos+self.width-1])) - if self.color: - (y, x) = self._win.getyx() - size = self.width-x - self.addnstr(' '*size, size, to_curses_attr(self.color)) - self.addstr(0, self.pos, '') - if self.color: - self._win.attroff(to_curses_attr(self.color)) - self._refresh() + self._win.erase() + if self.color: + self._win.attron(to_curses_attr(self.color)) + self.addstr('*'*len(self.text[self.view_pos:self.view_pos+self.width-1])) + if self.color: + (y, x) = self._win.getyx() + size = self.width-x + self.addnstr(' '*size, size, to_curses_attr(self.color)) + self.addstr(0, self.pos, '') + if self.color: + self._win.attroff(to_curses_attr(self.color)) + self._refresh() def get_help_message(self): return 'Edit the secret text' @@ -346,8 +340,7 @@ class FormWin(object): } def __init__(self, form, height, width, y, x): self._form = form - with g_lock: - self._win = Win._tab_win.derwin(height, width, y, x) + self._win = Win._tab_win.derwin(height, width, y, x) self.scroll_pos = 0 self.current_input = 0 self.inputs = [] # dict list @@ -370,8 +363,7 @@ class FormWin(object): def resize(self, height, width, y, x): self.height = height self.width = width - with g_lock: - self._win = Win._tab_win.derwin(height, width, y, x) + self._win = Win._tab_win.derwin(height, width, y, x) # Adjust the scroll position, if resizing made the window too small # for the cursor to be visible while self.current_input - self.scroll_pos > self.height-1: @@ -443,19 +435,18 @@ class FormWin(object): self.inputs[self.current_input]['input'].do_command(key) def refresh(self): - with g_lock: - self._win.erase() - y = -self.scroll_pos - i = 0 - 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) - # TODO: display the field description - y += 1 - i += 1 - self._win.refresh() + self._win.erase() + y = -self.scroll_pos + i = 0 + 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) + # TODO: display the field description + y += 1 + i += 1 + self._win.refresh() for i, inp in enumerate(self.inputs): if i < self.scroll_pos: continue diff --git a/src/windows/info_bar.py b/src/windows/info_bar.py index cea4702f..e66343c5 100644 --- a/src/windows/info_bar.py +++ b/src/windows/info_bar.py @@ -12,7 +12,7 @@ import curses from config import config -from . import Win, g_lock +from . import Win from theming import get_theme, to_curses_attr class GlobalInfoBar(Win): @@ -21,46 +21,45 @@ class GlobalInfoBar(Win): def refresh(self): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self._win.erase() + 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') - show_nums = config.get('show_tab_numbers') - use_nicks = config.get('use_tab_nicks') - # ignore any remaining gap tabs if the feature is not enabled - if create_gaps: - sorted_tabs = self.core.tabs[:] - else: - sorted_tabs = [tab for tab in self.core.tabs if tab] + create_gaps = config.get('create_gaps') + show_names = config.get('show_tab_names') + show_nums = config.get('show_tab_numbers') + use_nicks = config.get('use_tab_nicks') + # ignore any remaining gap tabs if the feature is not enabled + if create_gaps: + sorted_tabs = self.core.tabs[:] + else: + sorted_tabs = [tab for tab in self.core.tabs if tab] - for nb, tab in enumerate(sorted_tabs): - if not tab: continue - color = tab.color - if not config.get('show_inactive_tabs') and\ - color is get_theme().COLOR_TAB_NORMAL: - continue - try: - if show_nums or not show_names: - self.addstr("%s" % str(nb), to_curses_attr(color)) - if show_names: - self.addstr(' ', to_curses_attr(color)) + for nb, tab in enumerate(sorted_tabs): + if not tab: continue + color = tab.color + if not config.get('show_inactive_tabs') and\ + color is get_theme().COLOR_TAB_NORMAL: + continue + try: + if show_nums or not show_names: + self.addstr("%s" % str(nb), to_curses_attr(color)) if show_names: - if use_nicks: - 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 - break - (y, x) = self._win.getyx() - 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, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self._refresh() + self.addstr(' ', to_curses_attr(color)) + if show_names: + if use_nicks: + 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 + break + (y, x) = self._win.getyx() + 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, + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self._refresh() class VerticalGlobalInfoBar(Win): def __init__(self, scr): @@ -68,42 +67,39 @@ class VerticalGlobalInfoBar(Win): self._win = scr def refresh(self): - with g_lock: - height, width = self._win.getmaxyx() - self._win.erase() - sorted_tabs = [tab for tab in self.core.tabs if tab] - if not config.get('show_inactive_tabs'): - sorted_tabs = [tab for tab in sorted_tabs if\ - tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL] - nb_tabs = len(sorted_tabs) - 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: - pos = y - break - # center the current tab as much as possible - if pos < height//2: - sorted_tabs = sorted_tabs[:height] - elif nb_tabs - pos <= height//2: - sorted_tabs = sorted_tabs[-height:] - else: - sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2] + height, width = self._win.getmaxyx() + self._win.erase() + sorted_tabs = [tab for tab in self.core.tabs if tab] + if not config.get('show_inactive_tabs'): + sorted_tabs = [tab for tab in sorted_tabs if\ + tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL] + nb_tabs = len(sorted_tabs) + use_nicks = config.get('use_tab_nicks') + if nb_tabs >= height: for y, tab in enumerate(sorted_tabs): - color = tab.vertical_color - - if not config.get('vertical_tab_list_sort') != 'asc': - y = height - y - 1 - self.addstr(y, 0, "%2d" % tab.nb, - 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)) - 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.attroff(separator) - self._refresh() - + 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: + sorted_tabs = sorted_tabs[:height] + elif nb_tabs - pos <= height//2: + sorted_tabs = sorted_tabs[-height:] + else: + sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2] + for y, tab in enumerate(sorted_tabs): + color = tab.vertical_color + if not config.get('vertical_tab_list_sort') != 'asc': + y = height - y - 1 + self.addstr(y, 0, "%2d" % tab.nb, + 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)) + 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.attroff(separator) + self._refresh() diff --git a/src/windows/info_wins.py b/src/windows/info_wins.py index 7c659a6c..766afb75 100644 --- a/src/windows/info_wins.py +++ b/src/windows/info_wins.py @@ -8,7 +8,7 @@ log = logging.getLogger(__name__) from common import safeJID -from . import Win, g_lock +from . import Win from . funcs import truncate_nick from theming import get_theme, to_curses_attr @@ -39,17 +39,16 @@ class XMLInfoWin(InfoWin): def refresh(self, filter_t='', filter='', window=None): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - bar = to_curses_attr(get_theme().COLOR_INFORMATION_BAR) - if not filter_t: - self.addstr('[No filter]', bar) - else: - info = '[%s] %s' % (filter_t, filter) - self.addstr(info, bar) - self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() + self._win.erase() + bar = to_curses_attr(get_theme().COLOR_INFORMATION_BAR) + if not filter_t: + self.addstr('[No filter]', bar) + else: + info = '[%s] %s' % (filter_t, filter) + self.addstr(info, bar) + self.print_scroll_position(window) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) + self._refresh() class PrivateInfoWin(InfoWin): """ @@ -61,14 +60,13 @@ class PrivateInfoWin(InfoWin): def refresh(self, name, window, chatstate, informations): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - self.write_room_name(name) - self.print_scroll_position(window) - self.write_chatstate(chatstate) - self.write_additional_informations(informations, name) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() + self._win.erase() + self.write_room_name(name) + self.print_scroll_position(window) + self.write_chatstate(chatstate) + self.write_additional_informations(informations, name) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) + self._refresh() def write_additional_informations(self, informations, jid): """ @@ -100,16 +98,15 @@ class MucListInfoWin(InfoWin): def refresh(self, name=None, window=None): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - if name: - self.addstr(name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - else: - 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() + self._win.erase() + if name: + self.addstr(name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + else: + 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): """ @@ -138,16 +135,15 @@ class ConversationInfoWin(InfoWin): # If contact is a Contact, then # resource can now be a Resource: user is in the roster and online # or resource is None: user is in the roster but offline - with g_lock: - self._win.erase() - self.write_contact_jid(jid) - self.write_contact_informations(contact) - self.write_resource_information(resource) - self.print_scroll_position(window) - self.write_chatstate(chatstate) - self.write_additional_informations(informations, jid) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() + self._win.erase() + self.write_contact_jid(jid) + self.write_contact_informations(contact) + self.write_resource_information(resource) + self.print_scroll_position(window) + self.write_chatstate(chatstate) + self.write_additional_informations(informations, jid) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) + self._refresh() def write_additional_informations(self, informations, jid): """ @@ -217,17 +213,16 @@ class MucInfoWin(InfoWin): def refresh(self, room, window=None): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - self.write_room_name(room) - self.write_participants_number(room) - self.write_own_nick(room) - self.write_disconnected(room) - self.write_role(room) - if window: - self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() + self._win.erase() + self.write_room_name(room) + self.write_participants_number(room) + self.write_own_nick(room) + self.write_disconnected(room) + self.write_role(room) + if window: + self.print_scroll_position(window) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) + self._refresh() def write_room_name(self, room): self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) @@ -289,12 +284,11 @@ class ConversationStatusMessageWin(InfoWin): resource = contact.get_highest_priority_resource() else: resource = None - with g_lock: - self._win.erase() - if resource: - self.write_status_message(resource) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() + self._win.erase() + if resource: + self.write_status_message(resource) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) + self._refresh() def write_status_message(self, resource): self.addstr(resource.status, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) diff --git a/src/windows/input_placeholders.py b/src/windows/input_placeholders.py index 6ede6b32..8bcf1524 100644 --- a/src/windows/input_placeholders.py +++ b/src/windows/input_placeholders.py @@ -7,7 +7,7 @@ import logging log = logging.getLogger(__name__) -from . import Win, g_lock +from . import Win from theming import get_theme, to_curses_attr @@ -25,11 +25,10 @@ class HelpText(Win): log.debug('Refresh: %s', self.__class__.__name__) if txt: self.txt = txt - with g_lock: - self._win.erase() - 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() + self._win.erase() + 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() def do_command(self, key, raw=False): return False @@ -61,11 +60,10 @@ class YesNoInput(Win): log.debug('Refresh: %s', self.__class__.__name__) if txt: self.txt = txt - with g_lock: - self._win.erase() - self.addstr(0, 0, self.txt[:self.width-1], to_curses_attr(get_theme().COLOR_WARNING_PROMPT)) - self.finish_line(get_theme().COLOR_WARNING_PROMPT) - self._refresh() + self._win.erase() + self.addstr(0, 0, self.txt[:self.width-1], to_curses_attr(get_theme().COLOR_WARNING_PROMPT)) + self.finish_line(get_theme().COLOR_WARNING_PROMPT) + self._refresh() def do_command(self, key, raw=False): if key.lower() in self.key_func: @@ -73,11 +71,14 @@ class YesNoInput(Win): def prompt(self): """Monopolizes the input while waiting for a recognized keypress""" - cl = [] - while self.value is None: - if len(cl) == 1 and cl[0] in self.key_func: - self.key_func[cl[0]]() - cl = self.core.read_keyboard() + def cb(key): + if key in self.key_func: + self.key_func[key]() + if self.value is None: + # We didn’t finish with this prompt, continue monopolizing + # it again until value is set + keyboard.continuation_keys_callback = cb + keyboard.continuation_keys_callback = cb def on_delete(self): return diff --git a/src/windows/inputs.py b/src/windows/inputs.py index 8e1673e1..d345443b 100644 --- a/src/windows/inputs.py +++ b/src/windows/inputs.py @@ -8,9 +8,10 @@ log = logging.getLogger(__name__) import curses import string +import keyboard import common import poopt -from . import Win, g_lock +from . import Win from . base_wins import format_chars from . funcs import find_first_format_char from config import config @@ -494,25 +495,24 @@ class Input(Win): length of text to display, and the position of the cursor. """ self.adjust_view_pos() - with g_lock: - text = self.text - 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') - self._win.attrset(0) - self.addstr_colored_lite(displayed_text) - # Fill the rest of the line with the input color - if self.color: - (_, x) = self._win.getyx() - 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]), '') - if self.color: - self._win.attroff(to_curses_attr(self.color)) - curses.curs_set(1) - self._refresh() + text = self.text + 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') + self._win.attrset(0) + self.addstr_colored_lite(displayed_text) + # Fill the rest of the line with the input color + if self.color: + (_, x) = self._win.getyx() + 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]), '') + if self.color: + self._win.attroff(to_curses_attr(self.color)) + curses.curs_set(1) + self._refresh() def adjust_view_pos(self): """ @@ -656,11 +656,12 @@ class MessageInput(HistoryInput): """ Read one more char (c), add the corresponding char from formats_char to the text string """ - attr_char = self.core.read_keyboard()[0] - if attr_char in self.text_attributes: - char = format_chars[self.text_attributes.index(attr_char)] - self.do_command(char, False) - self.rewrite_text() + 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): if self.history_enter(): diff --git a/src/windows/list.py b/src/windows/list.py index 3cfb8af5..677df6ff 100644 --- a/src/windows/list.py +++ b/src/windows/list.py @@ -7,7 +7,7 @@ log = logging.getLogger(__name__) import curses -from . import Win, g_lock +from . import Win from theming import to_curses_attr, get_theme @@ -86,26 +86,26 @@ class ListWin(Win): def refresh(self): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - 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(): - try: - txt = line[col[1]] or '' - except KeyError: - txt = '' - size = self._columns_sizes[col[0]] - 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)) - else: - self.addstr(y, x, txt[:size]) - x += size - self._refresh() + self._win.erase() + 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(): + try: + txt = line[col[1]] or '' + except KeyError: + txt = '' + size = self._columns_sizes[col[0]] + 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)) + else: + self.addstr(y, x, txt[:size]) + x += size + self._refresh() def move_cursor_down(self): """ @@ -175,25 +175,24 @@ class ColumnHeaderWin(Win): def refresh(self): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - x = 0 - for col in self._columns: - txt = col - if col in self._column_order: - if self._column_order_asc: - txt += get_theme().CHAR_COLUMN_ASC - else: - txt += get_theme().CHAR_COLUMN_DESC - #⇓⇑↑↓⇧⇩▲▼ - size = self._columns_sizes[col] - 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._win.erase() + x = 0 + for col in self._columns: + txt = col + if col in self._column_order: + if self._column_order_asc: + txt += get_theme().CHAR_COLUMN_ASC else: - self.addstr(0, x, txt, to_curses_attr(get_theme().COLOR_COLUMN_HEADER)) - x += size - self._refresh() + txt += get_theme().CHAR_COLUMN_DESC + #⇓⇑↑↓⇧⇩▲▼ + size = self._columns_sizes[col] + txt += ' ' * (size-len(txt)) + if col in self._column_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)) + x += size + self._refresh() def sel_column(self, dic): self._column_sel = dic diff --git a/src/windows/misc.py b/src/windows/misc.py index 0f6bce59..07c91bbd 100644 --- a/src/windows/misc.py +++ b/src/windows/misc.py @@ -7,7 +7,7 @@ log = logging.getLogger(__name__) import curses -from . import Win, g_lock +from . import Win from theming import get_theme, to_curses_attr class VerticalSeparator(Win): @@ -19,9 +19,9 @@ class VerticalSeparator(Win): Win.__init__(self) def rewrite_line(self): - with g_lock: - self._win.vline(0, 0, curses.ACS_VLINE, self.height, to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) - self._refresh() + self._win.vline(0, 0, curses.ACS_VLINE, self.height, + to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) + self._refresh() def refresh(self): log.debug('Refresh: %s', self.__class__.__name__) @@ -53,9 +53,8 @@ class SimpleTextWin(Win): def refresh(self): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - for y, line in enumerate(self.built_lines): - self.addstr_colored(line, y, 0) - self._refresh() + self._win.erase() + for y, line in enumerate(self.built_lines): + self.addstr_colored(line, y, 0) + self._refresh() diff --git a/src/windows/muc.py b/src/windows/muc.py index cd594c4c..7e3541ba 100644 --- a/src/windows/muc.py +++ b/src/windows/muc.py @@ -7,7 +7,7 @@ log = logging.getLogger(__name__) import curses -from . import Win, g_lock +from . import Win import poopt from config import config @@ -36,44 +36,43 @@ class UserList(Win): log.debug('Refresh: %s', self.__class__.__name__) if config.get('hide_user_list'): return # do not refresh if this win is hidden. - with g_lock: - self._win.erase() + self._win.erase() + if config.get('user_list_sort').lower() == 'asc': + y, x = self._win.getmaxyx() + y -= 1 + users = sorted(users) + else: + y = 0 + users = sorted(users) + + if len(users) < self.height: + self.pos = 0 + elif self.pos >= len(users) - self.height and self.pos != 0: + self.pos = len(users) - self.height + for user in users[self.pos:]: + 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)) if config.get('user_list_sort').lower() == 'asc': - y, x = self._win.getmaxyx() y -= 1 - users = sorted(users) else: - y = 0 - users = sorted(users) - - if len(users) < self.height: - self.pos = 0 - elif self.pos >= len(users) - self.height and self.pos != 0: - self.pos = len(users) - self.height - for user in users[self.pos:]: - 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)) - if config.get('user_list_sort').lower() == 'asc': - y -= 1 - else: - y += 1 - if y == self.height: - break - # draw indicators of position in the list - if self.pos > 0: - if config.get('user_list_sort').lower() == 'asc': - self.draw_plus(self.height-1) - else: - self.draw_plus(0) - if self.pos + self.height < len(users): - if config.get('user_list_sort').lower() == 'asc': - self.draw_plus(0) - else: - self.draw_plus(self.height-1) - self._refresh() + y += 1 + if y == self.height: + break + # draw indicators of position in the list + if self.pos > 0: + if config.get('user_list_sort').lower() == 'asc': + self.draw_plus(self.height-1) + else: + self.draw_plus(0) + if self.pos + self.height < len(users): + if config.get('user_list_sort').lower() == 'asc': + self.draw_plus(0) + else: + self.draw_plus(self.height-1) + self._refresh() def draw_role_affiliation(self, y, user): theme = get_theme() @@ -94,12 +93,11 @@ class UserList(Win): self.addstr(y, 0, char, to_curses_attr(show_col)) def resize(self, height, width, y, x): - with g_lock: - separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR) - self._resize(height, width, y, x) - self._win.attron(separator) - self._win.vline(0, 0, curses.ACS_VLINE, self.height) - self._win.attroff(separator) + separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR) + self._resize(height, width, y, x) + self._win.attron(separator) + self._win.vline(0, 0, curses.ACS_VLINE, self.height) + self._win.attroff(separator) class Topic(Win): def __init__(self): @@ -108,19 +106,18 @@ class Topic(Win): def refresh(self, topic=None): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - if topic: - msg = topic[:self.width-1] - else: - msg = self._message[:self.width-1] - self.addstr(0, 0, msg, to_curses_attr(get_theme().COLOR_TOPIC_BAR)) - (y, x) = self._win.getyx() - remaining_size = self.width - x - if remaining_size: - self.addnstr(' '*remaining_size, remaining_size, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self._refresh() + self._win.erase() + if topic: + msg = topic[:self.width-1] + else: + msg = self._message[:self.width-1] + self.addstr(0, 0, msg, to_curses_attr(get_theme().COLOR_TOPIC_BAR)) + (y, x) = self._win.getyx() + remaining_size = self.width - x + if 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/src/windows/roster_win.py b/src/windows/roster_win.py index d98f27ce..6ecb6128 100644 --- a/src/windows/roster_win.py +++ b/src/windows/roster_win.py @@ -7,7 +7,7 @@ log = logging.getLogger(__name__) from datetime import datetime -from . import Win, g_lock +from . import Win import common from config import config @@ -89,39 +89,39 @@ class RosterWin(Win): """ Regenerates the roster cache if needed """ - with g_lock: - if roster.needs_rebuild: - log.debug('The roster has changed, rebuilding the cache…') - # This is a search - if roster.contact_filter: - self.roster_cache = [] - sort = config.get('roster_sort') or 'jid:show' - for contact in roster.get_contacts_sorted_filtered(sort): - self.roster_cache.append(contact) - else: - show_offline = config.get('roster_show_offline') or roster.contact_filter - sort = config.get('roster_sort') or 'jid:show' - group_sort = config.get('roster_group_sort') or 'name' - self.roster_cache = [] - # build the cache - for group in roster.get_groups(group_sort): - contacts_filtered = group.get_contacts(roster.contact_filter) - 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 - for contact in group.get_contacts(roster.contact_filter, sort): - if not show_offline and len(contact) == 0: - 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: - self.pos = self.roster_cache.index(self.selected_row) + if not roster.needs_rebuild: + return + log.debug('The roster has changed, rebuilding the cache…') + # This is a search + if roster.contact_filter: + self.roster_cache = [] + sort = config.get('roster_sort', 'jid:show') or 'jid:show' + for contact in roster.get_contacts_sorted_filtered(sort): + self.roster_cache.append(contact) + else: + show_offline = config.get('roster_show_offline') or roster.contact_filter + sort = config.get('roster_sort') or 'jid:show' + group_sort = config.get('roster_group_sort') or 'name' + self.roster_cache = [] + # build the cache + for group in roster.get_groups(group_sort): + contacts_filtered = group.get_contacts(roster.contact_filter) + 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 + for contact in group.get_contacts(roster.contact_filter, sort): + if not show_offline and len(contact) == 0: + 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: + self.pos = self.roster_cache.index(self.selected_row) def refresh(self, roster): """ @@ -130,43 +130,42 @@ class RosterWin(Win): """ log.debug('Refresh: %s', self.__class__.__name__) self.build_roster_cache(roster) - with g_lock: - # make sure we are within bounds - 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() - self._win.move(0, 0) - self.draw_roster_information(roster) - 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)) - # draw the roster from the cache - roster_view = self.roster_cache[self.start_pos-1:self.start_pos+self.height] - - for item in roster_view: - draw_selected = False - if y -2 + self.start_pos == self.pos: - draw_selected = True - self.selected_row = item - - if isinstance(item, RosterGroup): - self.draw_group(y, item, draw_selected) - group = item.name - elif isinstance(item, Contact): - self.draw_contact_line(y, item, draw_selected, group) - elif isinstance(item, Resource): - self.draw_resource_line(y, item, draw_selected) - - y += 1 - - 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) - self._refresh() + # make sure we are within bounds + 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() + self._win.move(0, 0) + self.draw_roster_information(roster) + 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)) + # draw the roster from the cache + roster_view = self.roster_cache[self.start_pos-1:self.start_pos+self.height] + + for item in roster_view: + draw_selected = False + if y -2 + self.start_pos == self.pos: + draw_selected = True + self.selected_row = item + + if isinstance(item, RosterGroup): + self.draw_group(y, item, draw_selected) + group = item.name + elif isinstance(item, Contact): + self.draw_contact_line(y, item, draw_selected, group) + elif isinstance(item, Resource): + self.draw_resource_line(y, item, draw_selected) + + y += 1 + + 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) + self._refresh() def draw_plus(self, y): @@ -373,13 +372,11 @@ class ContactInfoWin(Win): def refresh(self, selected_row): log.debug('Refresh: %s', self.__class__.__name__) - with g_lock: - self._win.erase() - if isinstance(selected_row, RosterGroup): - self.draw_group_info(selected_row) - elif isinstance(selected_row, Contact): - self.draw_contact_info(selected_row) - # elif isinstance(selected_row, Resource): - # self.draw_contact_info(None, selected_row) - self._refresh() - + self._win.erase() + if isinstance(selected_row, RosterGroup): + self.draw_group_info(selected_row) + elif isinstance(selected_row, Contact): + self.draw_contact_info(selected_row) + # elif isinstance(selected_row, Resource): + # self.draw_contact_info(None, selected_row) + self._refresh() diff --git a/src/windows/text_win.py b/src/windows/text_win.py index 413d9421..6fe74f41 100644 --- a/src/windows/text_win.py +++ b/src/windows/text_win.py @@ -9,7 +9,7 @@ log = logging.getLogger(__name__) import curses from math import ceil, log10 -from . import Win, g_lock +from . import Win from . base_wins import FORMAT_CHAR, Line from . funcs import truncate_nick, parse_attrs @@ -268,74 +268,73 @@ class TextWin(Win): else: lines = self.built_lines[-self.height-self.pos:-self.pos] with_timestamps = config.get("show_timestamps") - with g_lock: - self._win.move(0, 0) - self._win.erase() - for y, line in enumerate(lines): - if line: - msg = line.msg - if line.start_pos == 0: - if msg.nick_color: - color = msg.nick_color - elif msg.user: - color = msg.user.color - else: - color = None - if with_timestamps: - self.write_time(msg.str_time) - if msg.ack: - self.write_ack() - if msg.me: - self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE)) - self.addstr('* ') - self.write_nickname(msg.nickname, color, msg.highlight) - if msg.revisions: - self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE)) - self.addstr('%d' % msg.revisions) - self._win.attrset(0) - self.addstr(' ') - else: - self.write_nickname(msg.nickname, color, msg.highlight) - if msg.revisions: - self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE)) - self.addstr('%d' % msg.revisions) - self._win.attrset(0) - self.addstr('> ') - if y != self.height-1: - self.addstr('\n') - self._win.attrset(0) - for y, line in enumerate(lines): - if not line: - self.write_line_separator(y) - else: - offset = 0 - # Offset for the timestamp (if any) plus a space after it + self._win.move(0, 0) + self._win.erase() + for y, line in enumerate(lines): + if line: + msg = line.msg + if line.start_pos == 0: + if msg.nick_color: + color = msg.nick_color + elif msg.user: + color = msg.user.color + else: + color = None if with_timestamps: - offset += len(line.msg.str_time) - if offset: - offset += 1 - - # Offset for the nickname (if any) - # plus a space and a > after it - if line.msg.nickname: - offset += poopt.wcswidth( - truncate_nick(line.msg.nickname)) - if line.msg.me: - offset += 3 - else: - offset += 2 - offset += ceil(log10(line.msg.revisions + 1)) - - if line.msg.ack: - offset += 1 + poopt.wcswidth( - get_theme().CHAR_ACK_RECEIVED) - - 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() + self.write_time(msg.str_time) + if msg.ack: + self.write_ack() + if msg.me: + self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE)) + self.addstr('* ') + self.write_nickname(msg.nickname, color, msg.highlight) + if msg.revisions: + self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE)) + self.addstr('%d' % msg.revisions) + self._win.attrset(0) + self.addstr(' ') + else: + self.write_nickname(msg.nickname, color, msg.highlight) + if msg.revisions: + self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE)) + self.addstr('%d' % msg.revisions) + self._win.attrset(0) + self.addstr('> ') + if y != self.height-1: + self.addstr('\n') + self._win.attrset(0) + for y, line in enumerate(lines): + if not line: + self.write_line_separator(y) + else: + offset = 0 + # Offset for the timestamp (if any) plus a space after it + if with_timestamps: + offset += len(line.msg.str_time) + if offset: + offset += 1 + + # Offset for the nickname (if any) + # plus a space and a > after it + if line.msg.nickname: + offset += poopt.wcswidth( + truncate_nick(line.msg.nickname)) + if line.msg.me: + offset += 3 + else: + offset += 2 + offset += ceil(log10(line.msg.revisions + 1)) + + if line.msg.ack: + offset += 1 + poopt.wcswidth( + get_theme().CHAR_ACK_RECEIVED) + + self.write_text(y, offset, + line.prepend+line.msg.txt[line.start_pos:line.end_pos]) + if y != self.height-1: + self.addstr('\n') + self._win.attrset(0) + self._refresh() def write_line_separator(self, y): char = get_theme().CHAR_NEW_TEXT_SEPARATOR @@ -387,23 +386,21 @@ class TextWin(Win): self.addstr(' ') def resize(self, height, width, y, x, room=None): - with g_lock: - if hasattr(self, 'width'): - old_width = self.width - else: - old_width = None - self._resize(height, width, y, x) - if room and self.width != old_width: - self.rebuild_everything(room) - - # reposition the scrolling after resize - # (see #2450) - buf_size = len(self.built_lines) - if buf_size - self.pos < self.height: - self.pos = buf_size - self.height - if self.pos < 0: - self.pos = 0 - + if hasattr(self, 'width'): + old_width = self.width + else: + old_width = None + self._resize(height, width, y, x) + if room and self.width != old_width: + self.rebuild_everything(room) + + # reposition the scrolling after resize + # (see #2450) + buf_size = len(self.built_lines) + if buf_size - self.pos < self.height: + self.pos = buf_size - self.height + if self.pos < 0: + self.pos = 0 def rebuild_everything(self, room): self.built_lines = [] diff --git a/src/xhtml.py b/src/xhtml.py index 69519f8d..01e2dfcd 100644 --- a/src/xhtml.py +++ b/src/xhtml.py @@ -17,7 +17,7 @@ import curses import hashlib import re from os import path -from sleekxmpp.xmlstream import ET +from slixmpp.xmlstream import ET from urllib.parse import unquote from io import BytesIO |