summaryrefslogtreecommitdiff
path: root/src/core/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/core.py')
-rw-r--r--src/core/core.py322
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