diff options
Diffstat (limited to 'src/core/core.py')
-rw-r--r-- | src/core/core.py | 322 |
1 files changed, 186 insertions, 136 deletions
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 |