diff options
46 files changed, 794 insertions, 310 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9e314f62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ +# https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f1f09443..93129d0d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -86,12 +86,3 @@ pylint-plugins: - pip3 install -r requirements-plugins.txt - python3 setup.py install - pylint -E plugins - -formatting-check: - stage: test - image: python:3 - allow_failure: true - script: - - pip3 install yapf - - yapf -dpr poezio - - "[ -n \"$(yapf -dpr poezio)\" ] && echo 'Formatting check failed, please run yapf' && exit 1 || echo 'Formatting check succeeded'" @@ -4,7 +4,7 @@ RUN apk add --update build-base git python3 python3-dev libidn-dev && python3 -m WORKDIR /tmp/ ARG version=HEAD # Don’t ADD local files in order to keep layers at a minimal size -RUN git clone https://git.poez.io/poezio.git poezio-git-dir && \ +RUN git clone https://lab.louiz.org/poezio/poezio.git poezio-git-dir && \ cd poezio-git-dir && \ git archive --prefix="poezio-archive-${version}/" -o /tmp/poezio-archive.tar "${version}" && \ cd /tmp/ && tar xvf poezio-archive.tar && \ @@ -3,7 +3,7 @@ poezio Homepage: https://poez.io -Forge Page: https://dev.poez.io +Forge Page: https://lab.louiz.org/poezio/poezio Poezio is a console Jabber/XMPP client. Its goal is to use anonymous connections to simply let the user join MultiUserChats. This way, the user @@ -69,7 +69,7 @@ Contact/support Jabber ChatRoom: `poezio@muc.poez.io <xmpp:poezio@muc.poez.io?join>`_ -Report a bug: https://dev.poez.io/new +Report a bug: https://lab.louiz.org/poezio/poezio/issues/new License ======= diff --git a/data/io.poez.Poezio.json b/data/io.poez.Poezio.json index 79539257..98a56571 100644 --- a/data/io.poez.Poezio.json +++ b/data/io.poez.Poezio.json @@ -88,7 +88,7 @@ "sources": [ { "type": "git", - "url": "git://git.poez.io/poezio.git" + "url": "https://lab.louiz.org/poezio/poezio.git" } ] } diff --git a/doc/source/commands.rst b/doc/source/commands.rst index f28f992f..4a45d15d 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -14,7 +14,7 @@ You can get the same help as below from inside poezio with the :term:`/help` com .. note:: Use command parameters like this: - Do not use quotes if they are unnecessary (words without special chars or spaces) - - If the command takes several agrguments, you need to put quotes around arguments containing special chars such as backslashes or quotes + - If the command takes several arguments, you need to put quotes around arguments containing special chars such as backslashes or quotes - If the command always takes only one argument, then do not use quotes even for words containing special chars .. _global-commands: @@ -97,7 +97,7 @@ These commands work in *any* tab. **Usage:** ``/status <availability> [status message]`` Set your availability and - (optionaly) your status message. The <availability> argument is one of + (optionally) your status message. The <availability> argument is one of "available, chat, away, afk, dnd, busy, xa" and the optional [status] argument will be your status message.' @@ -218,6 +218,13 @@ These commands work in *any* tab. /invitations Show the pending invitations. + /impromptu + **Usage:** ``/impromptu <jid> [jid ..]`` + + Invite specified JIDs into a newly created room. + + .. versionadded:: 0.13 + /activity **Usage:** ``/activity [<general> [specific] [comment]]`` @@ -380,7 +387,7 @@ MultiUserChat tab commands Using the auto-completion of this command writes the current topic in the input, to help the user make a small change to the topic - whithout having to rewrite it all by hand. + without having to rewrite it all by hand. If no subject is specified as an argument, the current topic is displayed, unchanged. @@ -414,7 +421,7 @@ MultiUserChat tab commands /cycle **Usage:** ``/cycle [message]`` - Leave the current room an rejoint it immediatly. You can + Leave the current room an rejoint it immediately. You can specify an optional quit message. /info @@ -472,6 +479,14 @@ Normal Conversation tab commands Get the software version of the current interlocutor (usually its XMPP client and Operating System). + /invite + **Usage:** ``/invite <jid> [jid ..]`` + + Invite specified JIDs, with this contact, into a newly + created room. + + .. versionadded:: 0.13 + .. _rostertab-commands: Contact list tab commands diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 6baa6a27..3a5f2ef9 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -81,6 +81,15 @@ and certificate validation. you know what you are doing, see the :ref:`ciphers` dedicated section for more details. + default_muc_service + + **Default value:** ``[empty]`` + + If specified, will be used instead of the MUC service provided by + the user domain. + + .. versionadded:: 0.13 + force_encryption **Default value:** ``true`` @@ -145,7 +154,7 @@ Options related to account configuration, nickname… **Default value:** ``anon.jeproteste.info`` The server to use for anonymous authentication; - make sure it supports anonymous authentification. + make sure it supports anonymous authentication. Note that this option doesn’t do anything at all if you’re using your own JID. @@ -431,7 +440,7 @@ to understand what is :ref:`carbons <carbons-details>` or **Default value:** ``true`` XHTML-IM is an XMPP extension letting users send messages containing - XHTML and CSS formating. We can use this to make colored text for example. + XHTML and CSS formatting. We can use this to make colored text for example. Set to ``true`` if you want to see colored (and otherwise formatted) messages. enable_css_parsing @@ -593,7 +602,7 @@ or the way messages are displayed. **Default value:** ``[empty]`` A list of words or sentences separated by colons (":"). All the - informational mesages (described above) containing at least one of those + informational messages (described above) containing at least one of those values will not be shown. hide_exit_join @@ -769,7 +778,7 @@ or the way messages are displayed. show_roster_subscriptions - **Defalt value:** ``[empty]`` + **Default value:** ``[empty]`` Select the level of display of subscriptions with a char in the contact list. @@ -903,7 +912,7 @@ Options related to logging. **Default value:** ``true`` - Logs all the tracebacks and erors of poezio/slixmpp in + Logs all the tracebacks and errors of poezio/slixmpp in :term:`log_dir`/errors.log by default. ``false`` disables this option. use_log diff --git a/doc/source/dev/contributing.rst b/doc/source/dev/contributing.rst index ca7de049..8d386c87 100644 --- a/doc/source/dev/contributing.rst +++ b/doc/source/dev/contributing.rst @@ -5,7 +5,7 @@ Conventions ----------- We don’t have a strict set of conventions, but you should respect PEP8 mostly -(e.g. 4 spaces, class names in CamelCase and methods lowercased with +(e.g. 4 spaces, class names in CamelCase and methods lowercase with underscores) except if it means less-readable code (80 chars is often a hassle, and if you look inside poezio you’ll see lots of long lines, mostly because of strings). @@ -18,7 +18,7 @@ for the application as a whole. Commit guidelines ----------------- -Commits **should** have a meaninful title (first line), and *may* have a detailed +Commits **should** have a meaningful title (first line), and *may* have a detailed description below. There are of course exceptions (for example, a single-line commit that takes care of a typo right behind a big commit does not need to say ``fix a typo ("azre" → "are") in toto.py line 45454``, since the metainfos diff --git a/doc/source/dev/overview.rst b/doc/source/dev/overview.rst index fcf5ff22..96d4435b 100644 --- a/doc/source/dev/overview.rst +++ b/doc/source/dev/overview.rst @@ -40,7 +40,7 @@ method (inherited empty from the Tab class), call a scrolling method from the appropriate **window**. All tabs types inherit from the class **Tab**, and the tabs featuring -chat functionnality will inherit from **ChatTab** (which inherits from **Tab**). +chat functionality will inherit from **ChatTab** (which inherits from **Tab**). Examples of **tabs**: MUCTab, XMLTab, RosterTab, MUCListTab, etc… @@ -80,9 +80,9 @@ or /command "arg1 with spaces" arg2 -However, when creating a command, you wil deal with _one_ str, no matter what. +However, when creating a command, you will deal with _one_ str, no matter what. There are utilities to deal with it (common.shell_split), but it is not always -necessary. Commands are registered in the **commands** dictionnary of a tab +necessary. Commands are registered in the **commands** dictionary of a tab structured as key (command name) -> tuple(command function, help string, completion). Completions are a bit tricky, but it’s easy once you get used to it: diff --git a/doc/source/dev/xep.rst b/doc/source/dev/xep.rst index 339553ff..7feca4cf 100644 --- a/doc/source/dev/xep.rst +++ b/doc/source/dev/xep.rst @@ -91,7 +91,7 @@ Table of all XEPs implemented in poezio. +----------+-------------------------+---------------------+ |0270 |Compliance Suites 2010 |Advanced Client | +----------+-------------------------+---------------------+ -|0280 |Messsage Carbons |100% | +|0280 |Message Carbons |100% | +----------+-------------------------+---------------------+ |0296 |Best Practices for |0% | | |Resource Locking | | diff --git a/doc/source/install.rst b/doc/source/install.rst index 3146958c..f4f8e887 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -54,7 +54,7 @@ support. Therefore, you might want to use the git version. .. code-block:: bash - git clone git://git.poez.io/poezio + git clone https://lab.louiz.org/poezio/poezio cd poezio """"""" @@ -102,7 +102,7 @@ Poezio depends on slixmpp, a non-threaded fork of the SleekXMPP library. .. code-block:: bash - git clone git://git.poez.io/slixmpp + git clone https://lab.louiz.org/poezio/slixmpp python3 setup.py install --user diff --git a/doc/source/misc/client_certs.rst b/doc/source/misc/client_certs.rst index df09ea3c..1eacad0f 100644 --- a/doc/source/misc/client_certs.rst +++ b/doc/source/misc/client_certs.rst @@ -1,7 +1,7 @@ Using client certificates to login ================================== -Passwordless authentication is possible in XMPP through the use of mecanisms +Passwordless authentication is possible in XMPP through the use of mechanisms such as `SASL External`_. This mechanism has to be supported by both the client and the server. This page does not cover the server setup, but prosody has a `mod_client_certs`_ module which can perform this kind of authentication, and diff --git a/doc/source/misc/separate.rst b/doc/source/misc/separate.rst index 6c4605d8..66e42cdf 100644 --- a/doc/source/misc/separate.rst +++ b/doc/source/misc/separate.rst @@ -3,7 +3,7 @@ Using several accounts Poezio does not support multi-accounts, and we do not plan to do so in a foreseeable future. However, you can run several poezio instances (e.g. with -tmux or screen) to have similar functionnality. +tmux or screen) to have similar functionality. You can specify a different configuration file than the default with: diff --git a/plugins/disco.py b/plugins/disco.py index f6769146..ec0a04cd 100644 --- a/plugins/disco.py +++ b/plugins/disco.py @@ -29,6 +29,10 @@ class Plugin(BasePlugin): help='Get the disco#info of a JID') def on_disco(self, iq): + if iq['type'] == 'error': + self.api.information(iq['error']['text'] or iq['error']['condition'], 'Error') + return + info = iq['disco_info'] identities = (str(identity) for identity in info['identities']) self.api.information('\n'.join(identities), 'Identities') diff --git a/plugins/embed.py b/plugins/embed.py index 726b1eb2..0cdc41d2 100644 --- a/plugins/embed.py +++ b/plugins/embed.py @@ -20,7 +20,7 @@ from poezio.theming import get_theme class Plugin(BasePlugin): def init(self): - for tab_t in [tabs.MucTab, tabs.ConversationTab, tabs.PrivateTab]: + for tab_t in [tabs.MucTab, tabs.StaticConversationTab, tabs.DynamicConversationTab, tabs.PrivateTab]: self.api.add_tab_command( tab_t, 'embed', diff --git a/plugins/upload.py b/plugins/upload.py index db8615c2..7e25070e 100644 --- a/plugins/upload.py +++ b/plugins/upload.py @@ -33,7 +33,7 @@ class Plugin(BasePlugin): def init(self): if not self.core.xmpp['xep_0363']: raise Exception('slixmpp XEP-0363 plugin failed to load') - for _class in (tabs.PrivateTab, tabs.ConversationTab, tabs.MucTab): + for _class in (tabs.PrivateTab, tabs.StaticConversationTab, tabs.DynamicConversationTab, tabs.MucTab): self.api.add_tab_command( _class, 'upload', diff --git a/poezio/config.py b/poezio/config.py index a1f3dd49..d5a81c0e 100644 --- a/poezio/config.py +++ b/poezio/config.py @@ -49,6 +49,7 @@ DEFAULT_CONFIG = { 'custom_host': '', 'custom_port': '', 'default_nick': '', + 'default_muc_service': '', 'deterministic_nick_colors': True, 'device_id': '', 'nick_color_aliases': True, diff --git a/poezio/core/commands.py b/poezio/core/commands.py index 5c8199c0..2cb2b291 100644 --- a/poezio/core/commands.py +++ b/poezio/core/commands.py @@ -6,6 +6,7 @@ import logging log = logging.getLogger(__name__) +import asyncio from xml.etree import cElementTree as ET from slixmpp.exceptions import XMPPError @@ -763,6 +764,24 @@ class CommandCore: self.core.invite(to.full, room, reason=reason) self.core.information('Invited %s to %s' % (to.bare, room), 'Info') + @command_args_parser.quoted(1, 0) + def impromptu(self, args: str) -> None: + """/impromptu <jid> [<jid> ...]""" + + if args is None: + return self.help('impromptu') + + jids = set() + current_tab = self.core.tabs.current_tab + if isinstance(current_tab, tabs.ConversationTab): + jids.add(current_tab.general_jid) + + for jid in common.shell_split(' '.join(args)): + jids.add(safeJID(jid).bare) + + asyncio.ensure_future(self.core.impromptu(jids)) + self.core.information('Invited %s to a random room' % (', '.join(jids)), 'Info') + @command_args_parser.quoted(1, 1, ['']) def decline(self, args): """/decline <room@server.tld> [reason]""" diff --git a/poezio/core/completions.py b/poezio/core/completions.py index b283950e..87bb2d47 100644 --- a/poezio/core/completions.py +++ b/poezio/core/completions.py @@ -289,6 +289,19 @@ class CompletionCore: return Completion( the_input.new_completion, rooms, n, '', quotify=True) + def impromptu(self, the_input): + """Completion for /impromptu""" + n = the_input.get_argument_position(quoted=True) + onlines = [] + offlines = [] + for barejid in roster.jids(): + if len(roster[barejid]): + onlines.append(barejid) + else: + offlines.append(barejid) + comp = sorted(onlines) + sorted(offlines) + return Completion(the_input.new_completion, comp, n, quotify=True) + def activity(self, the_input): """Completion for /activity""" n = the_input.get_argument_position(quoted=True) diff --git a/poezio/core/core.py b/poezio/core/core.py index eec0d49b..9651a73b 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -13,12 +13,16 @@ import pipes import sys import shutil import time +import uuid from collections import defaultdict -from typing import Callable, Dict, List, Optional, Tuple, Type +from typing import Callable, Dict, List, Optional, Set, Tuple, Type +from xml.etree import cElementTree as ET +from functools import partial from slixmpp import JID from slixmpp.util import FileSystemPerJidCache from slixmpp.xmlstream.handler import Callback +from slixmpp.exceptions import IqError, IqTimeout from poezio import connection from poezio import decorators @@ -155,10 +159,12 @@ class Core: "KEY_F(5)": self.rotate_rooms_left, "^P": self.rotate_rooms_left, "M-[-D": self.rotate_rooms_left, + "M-[1;3D": self.rotate_rooms_left, 'kLFT3': self.rotate_rooms_left, "KEY_F(6)": self.rotate_rooms_right, "^N": self.rotate_rooms_right, "M-[-C": self.rotate_rooms_right, + "M-[1;3C": self.rotate_rooms_right, 'kRIT3': self.rotate_rooms_right, "KEY_F(4)": self.toggle_left_pane, "KEY_F(7)": self.shrink_information_win, @@ -868,6 +874,85 @@ class Core: self.xmpp.plugin['xep_0030'].get_info( jid=jid, timeout=5, callback=callback) + def _impromptu_room_form(self, room): + fields = [ + ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/muc#roomconfig'), + ('boolean', 'muc#roomconfig_changesubject', True), + ('boolean', 'muc#roomconfig_allowinvites', True), + ('boolean', 'muc#roomconfig_persistent', True), + ('boolean', 'muc#roomconfig_membersonly', True), + ('boolean', 'muc#roomconfig_publicroom', False), + ('list-single', 'muc#roomconfig_whois', 'anyone'), + # MAM + ('boolean', 'muc#roomconfig_enablearchiving', True), # Prosody + ('boolean', 'mam', True), # Ejabberd community + ('boolean', 'muc#roomconfig_mam', True), # Ejabberd saas + ] + + form = self.xmpp['xep_0004'].make_form() + form['type'] = 'submit' + for field in fields: + form.add_field( + ftype=field[0], + var=field[1], + value=field[2], + ) + + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = room + query = ET.Element('{http://jabber.org/protocol/muc#owner}query') + query.append(form.xml) + iq.append(query) + return iq + + async def impromptu(self, jids: Set[JID]) -> None: + """ + Generates a new "Impromptu" room with a random localpart on the muc + component of the user who initiated the request. One the room is + created and the first user has joined, send invites for specified + contacts to join in. + """ + + results = await self.xmpp['xep_0030'].get_info_from_domain() + + muc_from_identity = '' + for info in results: + for identity in info['disco_info']['identities']: + if identity[0] == 'conference' and identity[1] == 'text': + muc_from_identity = info['from'].bare + + # Use config.default_muc_service as muc component if available, + # otherwise find muc component by disco#items-ing the user domain. + # If not, give up + default_muc = config.get('default_muc_service', muc_from_identity) + if not default_muc: + self.information( + "Error finding a MUC service to join. If your server does not " + "provide one, set 'default_muc_service' manually to a MUC " + "service that allows room creation.", + 'Error' + ) + return + + nick = self.own_nick + localpart = uuid.uuid4().hex + room = '{!s}@{!s}'.format(localpart, default_muc) + + self.open_new_room(room, nick).join() + iq = self._impromptu_room_form(room) + try: + await iq.send() + except (IqError, IqTimeout): + self.information('Failed to configure impromptu room.', 'Info') + # TODO: destroy? leave room. + return None + + self.information('Room %s created' % room, 'Info') + + for jid in jids: + self.invite(jid, room) + def get_error_message(self, stanza, deprecated: bool = False): """ Takes a stanza of the form <message type='error'><error/></message> @@ -1789,6 +1874,13 @@ class Core: shortdesc='Invite someone in a room.', completion=self.completion.invite) self.register_command( + 'impromptu', + self.command.impromptu, + usage='<jid> [jid ...]', + desc='Invite specified JIDs into a newly created room.', + shortdesc='Invite specified JIDs into newly created room.', + completion=self.completion.impromptu) + self.register_command( 'invitations', self.command.invitations, shortdesc='Show the pending invitations.') diff --git a/poezio/core/handlers.py b/poezio/core/handlers.py index 0e655d68..94d05ee2 100644 --- a/poezio/core/handlers.py +++ b/poezio/core/handlers.py @@ -97,6 +97,11 @@ class HandlerCore: self.core.xmpp.plugin['xep_0030'].get_info( jid=self.core.xmpp.boundjid.domain, callback=callback) + def find_identities(self, _): + asyncio.ensure_future( + self.core.xmpp['xep_0030'].get_info_from_domain(), + ) + def on_carbon_received(self, message): """ Carbon <received/> received @@ -1063,7 +1068,8 @@ class HandlerCore: '{http://jabber.org/protocol/muc#user}x') is not None: return jid = presence['from'] - if not logger.log_roster_change(jid.bare, 'got offline'): + status = presence['status'] + if not logger.log_roster_change(jid.bare, 'got offline{}'.format(' ({})'.format(status) if status else '')): self.core.information('Unable to write in the log file', 'Error') # If a resource got offline, display the message in the conversation with this # precise resource. @@ -1073,12 +1079,15 @@ class HandlerCore: roster.connected -= 1 if contact.name: name = contact.name + offline_msg = '%s is \x191}offline' % name + if status: + offline_msg += ' (\x19o%s\x191})' % status if jid.resource: self.core.add_information_message_to_conversation_tab( - jid.full, '\x195}%s is \x191}offline' % name) + jid.full, '\x195}' + offline_msg) self.core.add_information_message_to_conversation_tab( - jid.bare, '\x195}%s is \x191}offline' % name) - self.core.information('\x193}%s \x195}is \x191}offline' % name, + jid.bare, '\x195}' + offline_msg) + self.core.information('\x193}' + offline_msg, 'Roster') roster.modified() if isinstance(self.core.tabs.current_tab, tabs.RosterInfoTab): @@ -1261,71 +1270,40 @@ class HandlerCore: semi_anon = '173' in status_codes full_anon = '174' in status_codes modif = False + info_col = {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)} if show_unavailable or hide_unavailable or non_priv or logging_off\ or non_anon or semi_anon or full_anon: tab.add_message( - '\x19%(info_col)s}Info: A configuration change not privacy-related occurred.' - % { - 'info_col': dump_tuple( - get_theme().COLOR_INFORMATION_TEXT) - }, + '\x19%(info_col)s}Info: A configuration change not privacy-related occurred.' % info_col, typ=2) modif = True if show_unavailable: tab.add_message( - '\x19%(info_col)s}Info: The unavailable members are now shown.' - % { - 'info_col': dump_tuple( - get_theme().COLOR_INFORMATION_TEXT) - }, + '\x19%(info_col)s}Info: The unavailable members are now shown.' % info_col, typ=2) elif hide_unavailable: tab.add_message( - '\x19%(info_col)s}Info: The unavailable members are now hidden.' - % { - 'info_col': dump_tuple( - get_theme().COLOR_INFORMATION_TEXT) - }, + '\x19%(info_col)s}Info: The unavailable members are now hidden.' % info_col, typ=2) if non_anon: tab.add_message( - '\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' - % { - 'info_col': dump_tuple( - get_theme().COLOR_INFORMATION_TEXT) - }, + '\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % info_col, typ=2) elif semi_anon: tab.add_message( - '\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' - % { - 'info_col': dump_tuple( - get_theme().COLOR_INFORMATION_TEXT) - }, + '\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % info_col, typ=2) elif full_anon: tab.add_message( - '\x19%(info_col)s}Info: The room is now fully anonymous.' % - { - 'info_col': dump_tuple( - get_theme().COLOR_INFORMATION_TEXT) - }, + '\x19%(info_col)s}Info: The room is now fully anonymous.' % info_col, typ=2) if logging_on: tab.add_message( - '\x191}Warning: \x19%(info_col)s}This room is publicly logged' - % { - 'info_col': dump_tuple( - get_theme().COLOR_INFORMATION_TEXT) - }, + '\x191}Warning: \x19%(info_col)s}This room is publicly logged' % info_col, typ=2) elif logging_off: tab.add_message( - '\x19%(info_col)s}Info: This room is not logged anymore.' % - { - 'info_col': dump_tuple( - get_theme().COLOR_INFORMATION_TEXT) - }, + '\x19%(info_col)s}Info: This room is not logged anymore.' % info_col, typ=2) if modif: self.core.refresh_window() @@ -1343,9 +1321,10 @@ class HandlerCore: if subject != tab.topic: # Do not display the message if the subject did not change or if we # receive an empty topic when joining the room. + theme = get_theme() fmt = { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), - 'text_col': dump_tuple(get_theme().COLOR_NORMAL_TEXT), + 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT), + 'text_col': dump_tuple(theme.COLOR_NORMAL_TEXT), 'subject': subject, 'user': '', } @@ -1439,17 +1418,18 @@ class HandlerCore: xhtml_text, force=True).rstrip('\x19o').strip() else: poezio_colored = str(stanza) + char = get_theme().CHAR_XML_OUT self.core.add_message_to_text_buffer( self.core.xml_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_OUT) + nickname=char) try: if self.core.xml_tab.match_stanza( ElementBase(ET.fromstring(stanza))): self.core.add_message_to_text_buffer( self.core.xml_tab.filtered_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_OUT) + nickname=char) except: log.debug('', exc_info=True) @@ -1468,16 +1448,17 @@ class HandlerCore: xhtml_text, force=True).rstrip('\x19o').strip() else: poezio_colored = str(stanza) + char = get_theme().CHAR_XML_IN self.core.add_message_to_text_buffer( self.core.xml_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_IN) + nickname=char) try: if self.core.xml_tab.match_stanza(stanza): self.core.add_message_to_text_buffer( self.core.xml_tab.filtered_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_IN) + nickname=char) except: log.debug('', exc_info=True) if isinstance(self.core.tabs.current_tab, tabs.XMLTab): diff --git a/poezio/decorators.py b/poezio/decorators.py index bf1c2ebe..4b5d0320 100644 --- a/poezio/decorators.py +++ b/poezio/decorators.py @@ -91,18 +91,18 @@ class CommandArgParser: the numbers `mandatory` and `optional`. If the string doesn’t contain at least `mandatory` arguments, we return - None because the given arguments are invalid. + None because the given arguments are invalid. If there are any remaining arguments after `mandatory` and `optional` arguments have been found (and “ignore_trailing_arguments" is not True), - we happen them to the last argument of the list. + we append them to the last argument of the list. - An argument is a string (with or without whitespaces) between to quotes + An argument is a string (with or without whitespaces) between two quotes ("), or a whitespace separated word (if not inside quotes). The argument `defaults` is a list of strings that are used when an optional argument is missing. For example if we accept one optional - argument, zero is available but we have one value in the `defaults` + argument and none is provided, but we have one value in the `defaults` list, we use that string inplace. The `defaults` list can only replace missing optional arguments, not mandatory ones. And it should not contain more than `mandatory` values. Also you cannot diff --git a/poezio/logger.py b/poezio/logger.py index 7ac7ad7e..d43cc759 100644 --- a/poezio/logger.py +++ b/poezio/logger.py @@ -56,14 +56,14 @@ class LogMessage(LogItem): self.nick = nick -def parse_log_line(msg: str) -> Optional[LogItem]: +def parse_log_line(msg: str, jid: str) -> Optional[LogItem]: match = re.match(MESSAGE_LOG_RE, msg) if match: return LogMessage(*match.groups()) match = re.match(INFO_LOG_RE, msg) if match: return LogInfo(*match.groups()) - log.debug('Error while parsing "%s"', msg) + log.debug('Error while parsing %s’s logs: “%s”', jid, msg) return None @@ -169,14 +169,14 @@ class Logger: # do that efficiently, instead of seek()s and read()s which are costly. with fd: try: - lines = get_lines_from_fd(fd, nb=nb) + lines = _get_lines_from_fd(fd, nb=nb) except Exception: # file probably empty log.error( 'Unable to mmap the log file for (%s)', filename, exc_info=True) return None - return parse_log_lines(lines) + return parse_log_lines(lines, jid) def log_message(self, jid: str, @@ -290,26 +290,23 @@ def build_log_message(nick: str, return logged_msg + ''.join(' %s\n' % line for line in lines) -def get_lines_from_fd(fd: IO[Any], nb: int = 10) -> List[str]: +def _get_lines_from_fd(fd: IO[Any], nb: int = 10) -> List[str]: """ Get the last log lines from a fileno """ with mmap.mmap(fd.fileno(), 0, prot=mmap.PROT_READ) as m: - pos = m.rfind(b"\nM") # start of messages begin with MI or MR, - # after a \n + # start of messages begin with MI or MR, after a \n + pos = m.rfind(b"\nM") + 1 # number of message found so far count = 0 - while pos != -1 and count < nb - 1: + while pos != 0 and count < nb - 1: count += 1 - pos = m.rfind(b"\nM", 0, pos) - if pos == -1: # If we don't have enough lines in the file - pos = 1 # 1, because we do -1 just on the next line - # to get 0 (start of the file) - lines = m[pos - 1:].decode(errors='replace').splitlines() + pos = m.rfind(b"\nM", 0, pos) + 1 + lines = m[pos:].decode(errors='replace').splitlines() return lines -def parse_log_lines(lines: List[str]) -> List[Dict[str, Any]]: +def parse_log_lines(lines: List[str], jid: str) -> List[Dict[str, Any]]: """ Parse raw log lines into poezio log objects """ @@ -323,7 +320,7 @@ def parse_log_lines(lines: List[str]) -> List[Dict[str, Any]]: idx += 1 log.debug('fail?') continue - log_item = parse_log_line(lines[idx]) + log_item = parse_log_line(lines[idx], jid) idx += 1 if not isinstance(log_item, LogItem): log.debug('wrong log format? %s', log_item) diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py index 7e7a7488..94f1d719 100644 --- a/poezio/tabs/conversationtab.py +++ b/poezio/tabs/conversationtab.py @@ -79,6 +79,12 @@ class ConversationTab(OneToOneTab): ' allow you to see his presence, and allow them to' ' see your presence.', shortdesc='Add a user to your roster.') + self.register_command( + 'invite', + self.core.command.impromptu, + desc='Invite people into an impromptu room.', + shortdesc='Invite other users to the discussion', + completion=self.core.completion.impromptu) self.update_commands() self.update_keys() diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index 405c2b1f..80631388 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -52,6 +52,8 @@ class MucTab(ChatTab): message_type = 'groupchat' plugin_commands = {} # type: Dict[str, Command] plugin_keys = {} # type: Dict[str, Callable] + additional_information = {} # type: Dict[str, Callable[[str], str]] + lagged = False def __init__(self, core, jid, nick, password=None): ChatTab.__init__(self, core, jid) @@ -106,6 +108,20 @@ class MucTab(ChatTab): return last_message.time return None + @staticmethod + def add_information_element(plugin_name: str, callback: Callable[[str], str]) -> None: + """ + Lets a plugin add its own information to the MucInfoWin + """ + MucTab.additional_information[plugin_name] = callback + + @staticmethod + def remove_information_element(plugin_name: str) -> None: + """ + Lets a plugin add its own information to the MucInfoWin + """ + del MucTab.additional_information[plugin_name] + def cancel_config(self, form): """ The user do not want to send his/her config, send an iq cancel @@ -141,13 +157,14 @@ class MucTab(ChatTab): def leave_room(self, message: str): if self.joined: - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - char_quit = get_theme().CHAR_QUIT - spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR) + theme = get_theme() + info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT) + char_quit = theme.CHAR_QUIT + spec_col = dump_tuple(theme.COLOR_QUIT_CHAR) if config.get_by_tabname('display_user_color_in_join_part', self.general_jid): - color = dump_tuple(get_theme().COLOR_OWN_NICK) + color = dump_tuple(theme.COLOR_OWN_NICK) else: color = "3" @@ -285,8 +302,9 @@ class MucTab(ChatTab): """ Print the current topic """ - info_text = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - norm_text = dump_tuple(get_theme().COLOR_NORMAL_TEXT) + theme = get_theme() + info_text = dump_tuple(theme.COLOR_INFORMATION_TEXT) + norm_text = dump_tuple(theme.COLOR_NORMAL_TEXT) if self.topic_from: user = self.get_user_by_name(self.topic_from) if user: @@ -396,6 +414,8 @@ class MucTab(ChatTab): if self.joined: if self.input.text: self.state = 'nonempty' + elif self.lagged: + self.state = 'disconnected' else: self.state = 'normal' else: @@ -421,13 +441,14 @@ class MucTab(ChatTab): """ Handle MUC presence """ + self.reset_lag() status_codes = set() for status_code in presence.xml.findall(STATUS_XPATH): status_codes.add(status_code.attrib['code']) if presence['type'] == 'error': self.core.room_error(presence, self.name) elif not self.joined: - if '110' in status_codes: + if '110' in status_codes or self.own_nick == presence['from'].resource: self.process_presence_buffer(presence) else: self.presence_buffer.append(presence) @@ -440,7 +461,9 @@ class MucTab(ChatTab): if self.core.tabs.current_tab is self: self.text_win.refresh() self.user_win.refresh_if_changed(self.users) - self.info_header.refresh(self, self.text_win, user=self.own_user) + self.info_header.refresh( + self, self.text_win, user=self.own_user, + information=MucTab.additional_information) self.input.refresh() self.core.doupdate() @@ -500,7 +523,8 @@ class MucTab(ChatTab): if (self.core.tabs.current_tab is self and self.core.status.show not in ('xa', 'away')): self.send_chat_state('active') - new_user.color = get_theme().COLOR_OWN_NICK + theme = get_theme() + new_user.color = theme.COLOR_OWN_NICK if config.get_by_tabname('display_user_color_in_join_part', self.general_jid): @@ -508,14 +532,14 @@ class MucTab(ChatTab): else: color = "3" - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - warn_col = dump_tuple(get_theme().COLOR_WARNING_TEXT) - spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR) + info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT) + warn_col = dump_tuple(theme.COLOR_WARNING_TEXT) + spec_col = dump_tuple(theme.COLOR_JOIN_CHAR) enable_message = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You ' '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined' ' the room') % { 'nick': from_nick, - 'spec': get_theme().CHAR_JOIN, + 'spec': theme.CHAR_JOIN, 'color_spec': spec_col, 'nick_col': color, 'info_col': info_col, @@ -630,9 +654,10 @@ class MucTab(ChatTab): color = dump_tuple(user.color) else: color = 3 - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR) - char_join = get_theme().CHAR_JOIN + theme = get_theme() + info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT) + spec_col = dump_tuple(theme.COLOR_JOIN_CHAR) + char_join = theme.CHAR_JOIN if not jid.full: msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s' '\x19%(info_col)s} joined the room') % { @@ -651,7 +676,7 @@ class MucTab(ChatTab): 'color': color, 'jid': jid.full, 'info_col': info_col, - 'jid_color': dump_tuple(get_theme().COLOR_MUC_JID), + 'jid_color': dump_tuple(theme.COLOR_MUC_JID), 'color_spec': spec_col, } self.add_message(msg, typ=2) @@ -710,8 +735,9 @@ class MucTab(ChatTab): else: by = None - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - char_kick = get_theme().CHAR_KICK + theme = get_theme() + info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT) + char_kick = theme.CHAR_KICK if from_nick == self.own_nick: # we are banned if by: @@ -786,8 +812,9 @@ class MucTab(ChatTab): reason = presence.xml.find('{%s}x/{%s}item/{%s}reason' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) by = None - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - char_kick = get_theme().CHAR_KICK + theme = get_theme() + info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT) + char_kick = theme.CHAR_KICK if actor_elem is not None: by = actor_elem.get('nick') or actor_elem.get('jid') if from_nick == self.own_nick: # we are kicked @@ -880,8 +907,9 @@ class MucTab(ChatTab): color = dump_tuple(user.color) else: color = 3 - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR) + theme = get_theme() + info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT) + spec_col = dump_tuple(theme.COLOR_QUIT_CHAR) error_leave_txt = '' if server_initiated: @@ -893,18 +921,18 @@ class MucTab(ChatTab): 'room%(error_leave)s') % { 'nick': from_nick, 'color': color, - 'spec': get_theme().CHAR_QUIT, + 'spec': theme.CHAR_QUIT, 'info_col': info_col, 'color_spec': spec_col, 'error_leave': error_leave_txt, } else: - jid_col = dump_tuple(get_theme().COLOR_MUC_JID) + jid_col = dump_tuple(theme.COLOR_MUC_JID) leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}' '%(nick)s\x19%(info_col)s} (\x19%(jid_col)s}' '%(jid)s\x19%(info_col)s}) has left the ' 'room%(error_leave)s') % { - 'spec': get_theme().CHAR_QUIT, + 'spec': theme.CHAR_QUIT, 'nick': from_nick, 'color': color, 'jid': jid.full, @@ -931,16 +959,17 @@ class MucTab(ChatTab): color = dump_tuple(user.color) else: color = 3 + info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) if from_nick == self.own_nick: msg = '\x19%(color)s}You\x19%(info_col)s} changed: ' % { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), + 'info_col': info_col, 'color': color } else: msg = '\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ' % { 'nick': from_nick, 'color': color, - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + 'info_col': info_col } if affiliation != user.affiliation: msg += 'affiliation: %s, ' % affiliation @@ -1126,6 +1155,7 @@ class MucTab(ChatTab): self.command_cycle(iq["error"]["text"] or "not in this room") self.core.refresh_window() else: # Re-send a self-ping in a few seconds + self.reset_lag() self.enable_self_ping_event() def search_for_color(self, nick): @@ -1145,8 +1175,26 @@ class MucTab(ChatTab): return color def on_self_ping_failed(self, iq): - self.command_cycle("the MUC server is not responding") - self.core.refresh_window() + if not self.lagged: + self.lagged = True + info_text = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + self._text_buffer.add_message( + "\x19%s}MUC service not responding." % info_text) + self._state = 'disconnected' + self.core.refresh_window() + self.enable_self_ping_event() + + def reset_lag(self): + if self.lagged: + self.lagged = False + info_text = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + self._text_buffer.add_message( + "\x19%s}MUC service is responding again." % info_text) + if self != self.core.tabs.current_tab: + self._state = 'joined' + else: + self._state = 'normal' + self.core.refresh_window() ########################## UI ONLY ##################################### @@ -1225,7 +1273,9 @@ class MucTab(ChatTab): if display_user_list: self.v_separator.refresh() self.user_win.refresh(self.users) - self.info_header.refresh(self, self.text_win, user=self.own_user) + self.info_header.refresh( + self, self.text_win, user=self.own_user, + information=MucTab.additional_information) self.refresh_tab_win() if display_info_win: self.info_win.refresh() @@ -1454,23 +1504,24 @@ class MucTab(ChatTab): if not self.joined: return + theme = get_theme() aff = { - 'owner': get_theme().CHAR_AFFILIATION_OWNER, - 'admin': get_theme().CHAR_AFFILIATION_ADMIN, - 'member': get_theme().CHAR_AFFILIATION_MEMBER, - 'none': get_theme().CHAR_AFFILIATION_NONE, + 'owner': theme.CHAR_AFFILIATION_OWNER, + 'admin': theme.CHAR_AFFILIATION_ADMIN, + 'member': theme.CHAR_AFFILIATION_MEMBER, + 'none': theme.CHAR_AFFILIATION_NONE, } colors = {} - colors["visitor"] = dump_tuple(get_theme().COLOR_USER_VISITOR) - colors["moderator"] = dump_tuple(get_theme().COLOR_USER_MODERATOR) - colors["participant"] = dump_tuple(get_theme().COLOR_USER_PARTICIPANT) - color_other = dump_tuple(get_theme().COLOR_USER_NONE) + colors["visitor"] = dump_tuple(theme.COLOR_USER_VISITOR) + colors["moderator"] = dump_tuple(theme.COLOR_USER_MODERATOR) + colors["participant"] = dump_tuple(theme.COLOR_USER_PARTICIPANT) + color_other = dump_tuple(theme.COLOR_USER_NONE) buff = ['Users: %s \n' % len(self.users)] for user in self.users: affiliation = aff.get(user.affiliation, - get_theme().CHAR_AFFILIATION_NONE) + theme.CHAR_AFFILIATION_NONE) color = colors.get(user.role, color_other) buff.append( '\x19%s}%s\x19o\x19%s}%s\x19o' % @@ -1529,7 +1580,7 @@ class MucTab(ChatTab): @command_args_parser.quoted(2) def command_affiliation(self, args): """ - /affiliation <nick> <role> + /affiliation <nick or jid> <affiliation> Changes the affiliation of an user affiliations can be: outcast, none, member, admin, owner """ diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py index 8f5f4d6f..4811f14e 100644 --- a/poezio/tabs/privatetab.py +++ b/poezio/tabs/privatetab.py @@ -345,21 +345,22 @@ class PrivateTab(OneToOneTab): The user left the associated MUC """ self.deactivate() + theme = get_theme() if config.get_by_tabname('display_user_color_in_join_part', self.general_jid): color = dump_tuple(user.color) else: - color = dump_tuple(get_theme().COLOR_REMOTE_USER) + color = dump_tuple(theme.COLOR_REMOTE_USER) if not status_message: self.add_message( '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' '%(nick)s\x19%(info_col)s} has left the room' % { 'nick': user.nick, - 'spec': get_theme().CHAR_QUIT, + 'spec': theme.CHAR_QUIT, 'nick_col': color, - 'quit_col': dump_tuple(get_theme().COLOR_QUIT_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + 'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR), + 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT) }, typ=2) else: @@ -369,10 +370,10 @@ class PrivateTab(OneToOneTab): ' (%(status)s)' % { 'status': status_message, 'nick': user.nick, - 'spec': get_theme().CHAR_QUIT, + 'spec': theme.CHAR_QUIT, 'nick_col': color, - 'quit_col': dump_tuple(get_theme().COLOR_QUIT_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + 'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR), + 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT) }, typ=2) return self.core.tabs.current_tab is self @@ -385,7 +386,8 @@ class PrivateTab(OneToOneTab): self.activate() self.check_features() tab = self.parent_muc - color = dump_tuple(get_theme().COLOR_REMOTE_USER) + theme = get_theme() + color = dump_tuple(theme.COLOR_REMOTE_USER) if tab and config.get_by_tabname('display_user_color_in_join_part', self.general_jid): user = tab.get_user_by_name(nick) @@ -396,9 +398,9 @@ class PrivateTab(OneToOneTab): '%(info_col)s} joined the room' % { 'nick': nick, 'color': color, - 'spec': get_theme().CHAR_JOIN, - 'join_col': dump_tuple(get_theme().COLOR_JOIN_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + 'spec': theme.CHAR_JOIN, + 'join_col': dump_tuple(theme.COLOR_JOIN_CHAR), + 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT) }, typ=2) return self.core.tabs.current_tab is self @@ -417,12 +419,13 @@ class PrivateTab(OneToOneTab): return [(3, safeJID(self.name).resource), (4, self.name)] def add_error(self, error_message): - error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK), + theme = get_theme() + error = '\x19%s}%s\x19o' % (dump_tuple(theme.COLOR_CHAR_NACK), error_message) self.add_message( error, highlight=True, nickname='Error', - nick_color=get_theme().COLOR_ERROR_MSG, + nick_color=theme.COLOR_ERROR_MSG, typ=2) self.core.refresh_window() diff --git a/poezio/windows/base_wins.py b/poezio/windows/base_wins.py index b14b44c3..6dabd7b8 100644 --- a/poezio/windows/base_wins.py +++ b/poezio/windows/base_wins.py @@ -37,6 +37,8 @@ class DummyWin: class Win: + __slots__ = ('_win', 'height', 'width', 'y', 'x') + def __init__(self) -> None: self._win = None self.height, self.width = 0, 0 diff --git a/poezio/windows/bookmark_forms.py b/poezio/windows/bookmark_forms.py index b7875e3c..8b9150d6 100644 --- a/poezio/windows/bookmark_forms.py +++ b/poezio/windows/bookmark_forms.py @@ -152,6 +152,9 @@ class BookmarkAutojoinWin(FieldInputMixin): class BookmarksWin(Win): + __slots__ = ('scroll_pos', '_current_input', 'current_horizontal_input', + '_bookmarks', 'lines') + def __init__(self, bookmarks: BookmarkList, height: int, width: int, y: int, x: int) -> None: self._win = base_wins.TAB_WIN.derwin(height, width, y, x) self.scroll_pos = 0 @@ -242,9 +245,10 @@ class BookmarksWin(Win): return if self.current_input == 0: return + theme = get_theme() self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) self.current_input -= 1 # Adjust the scroll position if the current_input would be outside # of the visible area @@ -253,20 +257,21 @@ class BookmarksWin(Win): self.refresh() self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) def go_to_next_horizontal_input(self) -> None: if not self.lines: return + theme = get_theme() self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) self.current_horizontal_input += 1 if self.current_horizontal_input > 3: self.current_horizontal_input = 0 self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) def go_to_next_page(self) -> bool: if not self.lines: @@ -275,9 +280,10 @@ class BookmarksWin(Win): if self.current_input == len(self.lines) - 1: return False + theme = get_theme() self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) inc = min(self.height, len(self.lines) - self.current_input - 1) if self.current_input + inc - self.scroll_pos > self.height - 1: @@ -288,7 +294,7 @@ class BookmarksWin(Win): self.current_input += inc self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) return True def go_to_previous_page(self) -> bool: @@ -298,9 +304,10 @@ class BookmarksWin(Win): if self.current_input == 0: return False + theme = get_theme() self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) dec = min(self.height, self.current_input) self.current_input -= dec @@ -311,7 +318,7 @@ class BookmarksWin(Win): self.refresh() self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) return True def go_to_previous_horizontal_input(self) -> None: @@ -319,13 +326,14 @@ class BookmarksWin(Win): return if self.current_horizontal_input == 0: return + theme = get_theme() self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) self.current_horizontal_input -= 1 self.lines[self.current_input][ self.current_horizontal_input].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) def on_input(self, key: str) -> None: if not self.lines: diff --git a/poezio/windows/confirm.py b/poezio/windows/confirm.py index 65878509..0a8de67b 100644 --- a/poezio/windows/confirm.py +++ b/poezio/windows/confirm.py @@ -4,6 +4,8 @@ from poezio.theming import get_theme, to_curses_attr class Dialog(Win): + __slots__ = ('text', 'accept', 'critical') + str_accept = " Accept " str_refuse = " Reject " diff --git a/poezio/windows/data_forms.py b/poezio/windows/data_forms.py index dc954bd7..3ec44b97 100644 --- a/poezio/windows/data_forms.py +++ b/poezio/windows/data_forms.py @@ -20,6 +20,9 @@ class FieldInput: 'windows' library. """ + # XXX: This conflicts with Win in the FieldInputMixin. + #__slots__ = ('_field', 'color') + def __init__(self, field): self._field = field self.color = get_theme().COLOR_NORMAL_TEXT @@ -47,6 +50,8 @@ class FieldInput: class FieldInputMixin(FieldInput, Win): "Mix both FieldInput and Win" + __slots__ = () + def __init__(self, field): FieldInput.__init__(self, field) Win.__init__(self) @@ -60,6 +65,8 @@ class FieldInputMixin(FieldInput, Win): class ColoredLabel(Win): + __slots__ = ('text', 'color') + def __init__(self, text): self.text = text self.color = get_theme().COLOR_NORMAL_TEXT @@ -85,6 +92,8 @@ class DummyInput(FieldInputMixin): Used for fields that do not require any input ('fixed') """ + __slots__ = () + def __init__(self, field): FieldInputMixin.__init__(self, field) @@ -99,6 +108,8 @@ class DummyInput(FieldInputMixin): class BooleanWin(FieldInputMixin): + __slots__ = ('last_key', 'value') + def __init__(self, field): FieldInputMixin.__init__(self, field) self.last_key = 'KEY_RIGHT' @@ -133,6 +144,8 @@ class BooleanWin(FieldInputMixin): class TextMultiWin(FieldInputMixin): + __slots__ = ('options', 'val_pos', 'edition_input') + def __init__(self, field): FieldInputMixin.__init__(self, field) options = field.get_value() @@ -212,6 +225,8 @@ class TextMultiWin(FieldInputMixin): class ListMultiWin(FieldInputMixin): + __slots__ = ('options', 'val_pos') + def __init__(self, field): FieldInputMixin.__init__(self, field) values = field.get_value() or [] @@ -261,6 +276,8 @@ class ListMultiWin(FieldInputMixin): class ListSingleWin(FieldInputMixin): + __slots__ = ('options', 'val_pos') + def __init__(self, field): FieldInputMixin.__init__(self, field) # the option list never changes @@ -306,6 +323,8 @@ class ListSingleWin(FieldInputMixin): class TextSingleWin(FieldInputMixin, Input): + __slots__ = ('text', 'pos') + def __init__(self, field): FieldInputMixin.__init__(self, field) Input.__init__(self) @@ -323,6 +342,8 @@ class TextSingleWin(FieldInputMixin, Input): class TextPrivateWin(TextSingleWin): + __slots__ = () + def __init__(self, field): TextSingleWin.__init__(self, field) @@ -352,6 +373,8 @@ class FormWin: On resize, move and resize all the subwin and define how the text will be written On refresh, write all the text, and refresh all the subwins """ + __slots__ = ('_win', 'height', 'width', '_form', 'scroll_pos', 'current_input', 'inputs') + input_classes = { 'boolean': BooleanWin, 'fixed': DummyInput, @@ -415,10 +438,11 @@ class FormWin: return if self.current_input == len(self.inputs) - 1: return + theme = get_theme() self.inputs[self.current_input]['input'].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) self.inputs[self.current_input]['label'].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) self.current_input += 1 jump = 0 while self.current_input + jump != len( @@ -437,19 +461,20 @@ class FormWin: self.scroll_pos += 1 self.refresh() self.inputs[self.current_input]['input'].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) self.inputs[self.current_input]['label'].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) def go_to_previous_input(self): if not self.inputs: return if self.current_input == 0: return + theme = get_theme() self.inputs[self.current_input]['input'].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) self.inputs[self.current_input]['label'].set_color( - get_theme().COLOR_NORMAL_TEXT) + theme.COLOR_NORMAL_TEXT) self.current_input -= 1 jump = 0 while self.current_input - jump > 0 and self.inputs[self.current_input @@ -466,9 +491,9 @@ class FormWin: self.refresh() self.current_input -= jump self.inputs[self.current_input]['input'].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) self.inputs[self.current_input]['label'].set_color( - get_theme().COLOR_SELECTED_ROW) + theme.COLOR_SELECTED_ROW) def on_input(self, key, raw=False): if not self.inputs: @@ -498,11 +523,10 @@ class FormWin: inp['input'].refresh() inp['label'].refresh() if self.inputs and self.current_input < self.height - 1: - self.inputs[self.current_input]['input'].set_color( - get_theme().COLOR_SELECTED_ROW) + color = get_theme().COLOR_SELECTED_ROW + self.inputs[self.current_input]['input'].set_color(color) self.inputs[self.current_input]['input'].refresh() - self.inputs[self.current_input]['label'].set_color( - get_theme().COLOR_SELECTED_ROW) + self.inputs[self.current_input]['label'].set_color(color) self.inputs[self.current_input]['label'].refresh() def refresh_current_input(self): diff --git a/poezio/windows/funcs.py b/poezio/windows/funcs.py index 69edace2..22977374 100644 --- a/poezio/windows/funcs.py +++ b/poezio/windows/funcs.py @@ -22,7 +22,7 @@ def find_first_format_char(text: str, return pos -def truncate_nick(nick: str, size=10) -> str: +def truncate_nick(nick: Optional[str], size=10) -> Optional[str]: if size < 1: size = 1 if nick and len(nick) > size: diff --git a/poezio/windows/image.py b/poezio/windows/image.py index 309fe0e6..96636ec7 100644 --- a/poezio/windows/image.py +++ b/poezio/windows/image.py @@ -9,8 +9,20 @@ try: from PIL import Image HAS_PIL = True except ImportError: + class Image: + class Image: + pass HAS_PIL = False +try: + import gi + gi.require_version('Rsvg', '2.0') + from gi.repository import Rsvg + import cairo + HAS_RSVG = True +except (ImportError, ValueError): + HAS_RSVG = False + from poezio.windows.base_wins import Win from poezio.theming import get_theme, to_curses_attr from poezio.xhtml import _parse_css_color @@ -19,13 +31,45 @@ from poezio.config import config from typing import Tuple, Optional, Callable +MAX_SIZE = 16 + + +def render_svg(svg: bytes) -> Optional[Image.Image]: + if not HAS_RSVG: + return None + try: + handle = Rsvg.Handle.new_from_data(svg) + dimensions = handle.get_dimensions() + biggest_dimension = max(dimensions.width, dimensions.height) + scale = MAX_SIZE / biggest_dimension + translate_x = (biggest_dimension - dimensions.width) / 2 + translate_y = (biggest_dimension - dimensions.height) / 2 + + surface = cairo.ImageSurface(cairo.Format.ARGB32, MAX_SIZE, MAX_SIZE) + context = cairo.Context(surface) + context.scale(scale, scale) + context.translate(translate_x, translate_y) + handle.render_cairo(context) + data = surface.get_data() + image = Image.frombytes('RGBA', (MAX_SIZE, MAX_SIZE), data.tobytes()) + # This is required because Cairo uses a BGRA (in host endianess) + # format, and PIL an ABGR (in byte order) format. Yes, this is + # confusing. + b, g, r, a = image.split() + return Image.merge('RGB', (r, g, b)) + except Exception: + return None + + class ImageWin(Win): """ A window which contains either an image or a border. """ + __slots__ = ('_image', '_display_avatar') + def __init__(self) -> None: - self._image = None # type: Optional[Image] + self._image = None # type: Optional[Image.Image] Win.__init__(self) if config.get('image_use_half_blocks'): self._display_avatar = self._display_avatar_half_blocks # type: Callable[[int, int], None] @@ -43,7 +87,14 @@ class ImageWin(Win): if data is not None and HAS_PIL: image_file = BytesIO(data) try: - image = Image.open(image_file) + try: + image = Image.open(image_file) + except OSError: + # TODO: Make the caller pass the MIME type, so we don’t + # have to try all renderers like that. + image = render_svg(data) + if image is None: + raise except OSError: self._display_border() else: diff --git a/poezio/windows/info_bar.py b/poezio/windows/info_bar.py index 96382d0f..ac900103 100644 --- a/poezio/windows/info_bar.py +++ b/poezio/windows/info_bar.py @@ -16,6 +16,8 @@ from poezio.theming import get_theme, to_curses_attr class GlobalInfoBar(Win): + __slots__ = ('core') + def __init__(self, core) -> None: Win.__init__(self) self.core = core @@ -23,8 +25,9 @@ class GlobalInfoBar(Win): def refresh(self) -> None: log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() + theme = get_theme() self.addstr(0, 0, "[", - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(theme.COLOR_INFORMATION_BAR)) show_names = config.get('show_tab_names') show_nums = config.get('show_tab_numbers') @@ -35,7 +38,7 @@ class GlobalInfoBar(Win): if not tab: continue color = tab.color - if not show_inactive and color is get_theme().COLOR_TAB_NORMAL: + if not show_inactive and color is theme.COLOR_TAB_NORMAL: continue try: if show_nums or not show_names: @@ -49,20 +52,22 @@ class GlobalInfoBar(Win): else: self.addstr("%s" % tab.name, to_curses_attr(color)) self.addstr("|", - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(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)) + to_curses_attr(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)) + to_curses_attr(theme.COLOR_INFORMATION_BAR)) self._refresh() class VerticalGlobalInfoBar(Win): + __slots__ = ('core') + def __init__(self, core, scr) -> None: Win.__init__(self) self.core = core @@ -72,17 +77,17 @@ class VerticalGlobalInfoBar(Win): height, width = self._win.getmaxyx() self._win.erase() sorted_tabs = [tab for tab in self.core.tabs if tab] + theme = get_theme() 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 + if tab.vertical_color != 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: + if tab.vertical_color == theme.COLOR_VERTICAL_TAB_CURRENT: pos = y break # center the current tab as much as possible @@ -98,14 +103,14 @@ class VerticalGlobalInfoBar(Win): if asc_sort: y = height - y - 1 self.addstr(y, 0, "%2d" % tab.nb, - to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER)) + to_curses_attr(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) + separator = to_curses_attr(theme.COLOR_VERTICAL_SEPARATOR) self._win.attron(separator) self._win.vline(0, width - 1, curses.ACS_VLINE, height) self._win.attroff(separator) diff --git a/poezio/windows/info_wins.py b/poezio/windows/info_wins.py index 27f9e1cf..3a8d1863 100644 --- a/poezio/windows/info_wins.py +++ b/poezio/windows/info_wins.py @@ -20,6 +20,8 @@ class InfoWin(Win): MucInfoWin, etc. Provides some useful methods. """ + __slots__ = () + def __init__(self): Win.__init__(self) @@ -40,6 +42,8 @@ class XMLInfoWin(InfoWin): Info about the latest xml filter used and the state of the buffer. """ + __slots__ = () + def __init__(self): InfoWin.__init__(self) @@ -63,6 +67,8 @@ class PrivateInfoWin(InfoWin): about the MUC user we are talking to """ + __slots__ = () + def __init__(self): InfoWin.__init__(self) @@ -81,16 +87,17 @@ class PrivateInfoWin(InfoWin): Write all information added by plugins by getting the value returned by the callbacks. """ - for key in information: - self.addstr(information[key](jid), + for plugin in information.values(): + self.addstr(plugin(jid), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_room_name(self, name): jid = safeJID(name) room_name, nick = jid.bare, jid.resource - self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME)) + theme = get_theme() + self.addstr(nick, to_curses_attr(theme.COLOR_PRIVATE_NAME)) txt = ' from room %s' % room_name - self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(txt, to_curses_attr(theme.COLOR_INFORMATION_BAR)) def write_chatstate(self, state): if state: @@ -104,6 +111,8 @@ class MucListInfoWin(InfoWin): about the muc server being listed """ + __slots__ = ('message') + def __init__(self, message=''): InfoWin.__init__(self) self.message = message @@ -111,15 +120,16 @@ class MucListInfoWin(InfoWin): def refresh(self, name=None, window=None): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() + theme = get_theme() if name: self.addstr(name, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(theme.COLOR_INFORMATION_BAR)) else: self.addstr(self.message, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(theme.COLOR_INFORMATION_BAR)) if window: self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) + self.finish_line(theme.COLOR_INFORMATION_BAR) self._refresh() @@ -129,6 +139,8 @@ class ConversationInfoWin(InfoWin): about the user we are talking to """ + __slots__ = () + def __init__(self): InfoWin.__init__(self) @@ -166,9 +178,9 @@ class ConversationInfoWin(InfoWin): Write all information added by plugins by getting the value returned by the callbacks. """ - for key in information: - self.addstr(information[key](jid), - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + color = to_curses_attr(get_theme().COLOR_INFORMATION_BAR) + for plugin in information.values(): + self.addstr(plugin(jid), color) def write_resource_information(self, resource): """ @@ -178,38 +190,40 @@ class ConversationInfoWin(InfoWin): presence = "unavailable" else: presence = resource.presence - color = get_theme().color_show(presence) + theme = get_theme() + color = theme.color_show(presence) if not presence: - presence = get_theme().CHAR_STATUS - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + presence = theme.CHAR_STATUS + self.addstr('[', to_curses_attr(theme.COLOR_INFORMATION_BAR)) self.addstr(presence, to_curses_attr(color)) if resource and resource.status: shortened = resource.status[:20] + (resource.status[:20] and '…') self.addstr(' %s' % shortened, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(theme.COLOR_INFORMATION_BAR)) + self.addstr(']', to_curses_attr(theme.COLOR_INFORMATION_BAR)) def write_contact_information(self, contact): """ Write the information about the contact """ + color = to_curses_attr(get_theme().COLOR_INFORMATION_BAR) if not contact: - self.addstr("(contact not in roster)", - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr("(contact not in roster)", color) return display_name = contact.name if display_name: - self.addstr('%s ' % (display_name), - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr('%s ' % (display_name), color) def write_contact_jid(self, jid): """ Just write the jid that we are talking to """ - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + theme = get_theme() + color = to_curses_attr(theme.COLOR_INFORMATION_BAR) + self.addstr('[', color) self.addstr(jid.full, - to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) - self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(theme.COLOR_CONVERSATION_NAME)) + self.addstr('] ', color) def write_chatstate(self, state): if state: @@ -218,20 +232,24 @@ class ConversationInfoWin(InfoWin): class DynamicConversationInfoWin(ConversationInfoWin): + __slots__ = () + def write_contact_jid(self, jid): """ Just displays the resource in an other color """ log.debug("write_contact_jid DynamicConversationInfoWin, jid: %s", jid.resource) - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + theme = get_theme() + color = to_curses_attr(theme.COLOR_INFORMATION_BAR) + self.addstr('[', color) self.addstr(jid.bare, - to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) + to_curses_attr(theme.COLOR_CONVERSATION_NAME)) if jid.resource: self.addstr( "/%s" % (jid.resource, ), - to_curses_attr(get_theme().COLOR_CONVERSATION_RESOURCE)) - self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(theme.COLOR_CONVERSATION_RESOURCE)) + self.addstr('] ', color) class MucInfoWin(InfoWin): @@ -240,10 +258,12 @@ class MucInfoWin(InfoWin): about the MUC we are viewing """ + __slots__ = () + def __init__(self): InfoWin.__init__(self) - def refresh(self, room, window=None, user=None): + def refresh(self, room, window=None, user=None, information=None): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() self.write_room_name(room) @@ -251,23 +271,38 @@ class MucInfoWin(InfoWin): self.write_own_nick(room) self.write_disconnected(room) self.write_role(room, user) + if information: + self.write_additional_information(information, room.name) if window: self.print_scroll_position(window) self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() + def write_additional_information(self, information, jid): + """ + Write all information added by plugins by getting the + value returned by the callbacks. + """ + color = to_curses_attr(get_theme().COLOR_INFORMATION_BAR) + for plugin in information.values(): + self.addstr(plugin(jid), color) + def write_room_name(self, room): - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + theme = get_theme() + color = to_curses_attr(theme.COLOR_INFORMATION_BAR) + self.addstr('[', color) self.addstr(room.name, - to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) - self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(theme.COLOR_GROUPCHAT_NAME)) + self.addstr(']', color) def write_participants_number(self, room): - self.addstr('{', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + theme = get_theme() + color = to_curses_attr(theme.COLOR_INFORMATION_BAR) + self.addstr('{', color) self.addstr( str(len(room.users)), - to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) - self.addstr('} ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + to_curses_attr(theme.COLOR_GROUPCHAT_NAME)) + self.addstr('} ', color) def write_disconnected(self, room): """ @@ -306,6 +341,8 @@ class ConversationStatusMessageWin(InfoWin): The upper bar displaying the status message of the contact """ + __slots__ = () + def __init__(self): InfoWin.__init__(self) @@ -331,6 +368,8 @@ class ConversationStatusMessageWin(InfoWin): class BookmarksInfoWin(InfoWin): + __slots__ = () + def __init__(self): InfoWin.__init__(self) @@ -347,6 +386,8 @@ class BookmarksInfoWin(InfoWin): class ConfirmStatusWin(Win): + __slots__ = ('text', 'critical') + def __init__(self, text, critical=False): Win.__init__(self) self.text = text @@ -355,10 +396,11 @@ class ConfirmStatusWin(Win): def refresh(self): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() + theme = get_theme() if self.critical: - color = get_theme().COLOR_WARNING_PROMPT + color = theme.COLOR_WARNING_PROMPT else: - color = get_theme().COLOR_INFORMATION_BAR + color = theme.COLOR_INFORMATION_BAR c_color = to_curses_attr(color) self.addstr(self.text, c_color) self.finish_line(color) diff --git a/poezio/windows/input_placeholders.py b/poezio/windows/input_placeholders.py index c5656f72..4d414636 100644 --- a/poezio/windows/input_placeholders.py +++ b/poezio/windows/input_placeholders.py @@ -19,6 +19,8 @@ class HelpText(Win): command mode. """ + __slots__ = ('txt') + def __init__(self, text: str = '') -> None: Win.__init__(self) self.txt = text # type: str diff --git a/poezio/windows/inputs.py b/poezio/windows/inputs.py index 6b0bc798..c0c73419 100644 --- a/poezio/windows/inputs.py +++ b/poezio/windows/inputs.py @@ -32,6 +32,9 @@ class Input(Win): passing the list of items that can be used to complete. The completion can be used in a very flexible way. """ + __slots__ = ('key_func', 'text', 'pos', 'view_pos', 'on_input', 'color', + 'last_completion', 'hit_list') + text_attributes = 'bou1234567ti' clipboard = '' # A common clipboard for all the inputs, this makes @@ -586,6 +589,8 @@ class HistoryInput(Input): An input with colors and stuff, plus an history ^R allows to search inside the history (as in a shell) """ + __slots__ = ('help_message', 'histo_pos', 'current_completed', 'search') + history = [] # type: List[str] def __init__(self) -> None: diff --git a/poezio/windows/list.py b/poezio/windows/list.py index b24b8aea..350255c6 100644 --- a/poezio/windows/list.py +++ b/poezio/windows/list.py @@ -19,6 +19,9 @@ class ListWin(Win): scrolled up and down, with one selected line at a time """ + __slots__ = ('_columns', '_columns_sizes', 'sorted_by', 'lines', + '_selected_row', '_starting_pos') + def __init__(self, columns: Dict[str, int], with_headers: bool = True) -> None: Win.__init__(self) self._columns = columns # type: Dict[str, int] @@ -91,6 +94,7 @@ class ListWin(Win): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() lines = self.lines[self._starting_pos:self._starting_pos + self.height] + color = to_curses_attr(get_theme().COLOR_INFORMATION_BAR) for y, line in enumerate(lines): x = 0 for col in self._columns.items(): @@ -103,9 +107,7 @@ class ListWin(Win): 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)) + self.addstr(y, x, txt[:size], color) else: self.addstr(y, x, txt[:size]) x += size @@ -165,6 +167,9 @@ class ColumnHeaderWin(Win): A class displaying the column's names """ + __slots__ = ('_columns', '_columns_sizes', '_column_sel', '_column_order', + '_column_order_asc') + def __init__(self, columns: List[str]) -> None: Win.__init__(self) self._columns = columns @@ -183,23 +188,24 @@ class ColumnHeaderWin(Win): log.debug('Refresh: %s', self.__class__.__name__) self._win.erase() x = 0 + theme = get_theme() for col in self._columns: txt = col if col in self._column_order: if self._column_order_asc: - txt += get_theme().CHAR_COLUMN_ASC + txt += theme.CHAR_COLUMN_ASC else: - txt += get_theme().CHAR_COLUMN_DESC + txt += 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)) + to_curses_attr(theme.COLOR_COLUMN_HEADER_SEL)) else: self.addstr(0, x, txt, - to_curses_attr(get_theme().COLOR_COLUMN_HEADER)) + to_curses_attr(theme.COLOR_COLUMN_HEADER)) x += size self._refresh() diff --git a/poezio/windows/misc.py b/poezio/windows/misc.py index e6596622..6c04b814 100644 --- a/poezio/windows/misc.py +++ b/poezio/windows/misc.py @@ -19,6 +19,8 @@ class VerticalSeparator(Win): refreshed only on resize, but never on refresh, for efficiency """ + __slots__ = () + def rewrite_line(self) -> None: self._win.vline(0, 0, curses.ACS_VLINE, self.height, to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) @@ -30,6 +32,8 @@ class VerticalSeparator(Win): class SimpleTextWin(Win): + __slots__ = ('_text', 'built_lines') + def __init__(self, text) -> None: Win.__init__(self) self._text = text diff --git a/poezio/windows/muc.py b/poezio/windows/muc.py index 3e52f63d..951940e1 100644 --- a/poezio/windows/muc.py +++ b/poezio/windows/muc.py @@ -28,6 +28,8 @@ def userlist_to_cache(userlist: List[User]) -> List[CachedUser]: class UserList(Win): + __slots__ = ('pos', 'cache') + def __init__(self) -> None: Win.__init__(self) self.pos = 0 @@ -108,15 +110,16 @@ class UserList(Win): self.addstr(y, 1, symbol, to_curses_attr(color)) def draw_status_chatstate(self, y: int, user: User) -> None: - show_col = get_theme().color_show(user.show) + theme = get_theme() + show_col = theme.color_show(user.show) if user.chatstate == 'composing': - char = get_theme().CHAR_CHATSTATE_COMPOSING + char = theme.CHAR_CHATSTATE_COMPOSING elif user.chatstate == 'active': - char = get_theme().CHAR_CHATSTATE_ACTIVE + char = theme.CHAR_CHATSTATE_ACTIVE elif user.chatstate == 'paused': - char = get_theme().CHAR_CHATSTATE_PAUSED + char = theme.CHAR_CHATSTATE_PAUSED else: - char = get_theme().CHAR_STATUS + char = theme.CHAR_STATUS self.addstr(y, 0, char, to_curses_attr(show_col)) def resize(self, height: int, width: int, y: int, x: int) -> None: @@ -128,23 +131,26 @@ class UserList(Win): class Topic(Win): + __slots__ = ('_message') + def __init__(self) -> None: Win.__init__(self) self._message = '' def refresh(self, topic: Optional[str] = None) -> None: log.debug('Refresh: %s', self.__class__.__name__) + theme = get_theme() self._win.erase() if topic is not None: 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)) + self.addstr(0, 0, msg, to_curses_attr(theme.COLOR_TOPIC_BAR)) _, 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)) + to_curses_attr(theme.COLOR_INFORMATION_BAR)) self._refresh() def set_message(self, message) -> None: diff --git a/poezio/windows/roster_win.py b/poezio/windows/roster_win.py index 3497e342..2efdd324 100644 --- a/poezio/windows/roster_win.py +++ b/poezio/windows/roster_win.py @@ -20,6 +20,8 @@ Row = Union[RosterGroup, Contact] class RosterWin(Win): + __slots__ = ('pos', 'start_pos', 'selected_row', 'roster_cache') + def __init__(self) -> None: Win.__init__(self) self.pos = 0 # cursor position in the contact list @@ -193,18 +195,20 @@ class RosterWin(Win): """ The header at the top """ + color = get_theme().COLOR_INFORMATION_BAR self.addstr( 'Roster: %s/%s contacts' % (roster.get_nb_connected_contacts(), len(roster)), - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) + to_curses_attr(color)) + self.finish_line(color) def draw_group(self, y: int, group: RosterGroup, colored: bool) -> None: """ Draw a groupname on a line """ + color = to_curses_attr(get_theme().COLOR_SELECTED_ROW) if colored: - self._win.attron(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) + self._win.attron(color) if group.folded: self.addstr(y, 0, '[+] ') else: @@ -215,7 +219,7 @@ class RosterWin(Win): self.truncate_name(group.name, len(contacts) + 4) + contacts) if colored: - self._win.attroff(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) + self._win.attroff(color) self.finish_line() def truncate_name(self, name, added): @@ -261,17 +265,17 @@ class RosterWin(Win): added += 4 if contact.ask: - added += len(get_theme().CHAR_ROSTER_ASKED) + added += len(theme.CHAR_ROSTER_ASKED) if show_s2s_errors and contact.error: - added += len(get_theme().CHAR_ROSTER_ERROR) + added += len(theme.CHAR_ROSTER_ERROR) if contact.tune: - added += len(get_theme().CHAR_ROSTER_TUNE) + added += len(theme.CHAR_ROSTER_TUNE) if contact.mood: - added += len(get_theme().CHAR_ROSTER_MOOD) + added += len(theme.CHAR_ROSTER_MOOD) if contact.activity: - added += len(get_theme().CHAR_ROSTER_ACTIVITY) + added += len(theme.CHAR_ROSTER_ACTIVITY) if contact.gaming: - added += len(get_theme().CHAR_ROSTER_GAMING) + added += len(theme.CHAR_ROSTER_GAMING) if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): added += len( @@ -289,7 +293,7 @@ class RosterWin(Win): if colored: self.addstr(display_name, - to_curses_attr(get_theme().COLOR_SELECTED_ROW)) + to_curses_attr(theme.COLOR_SELECTED_ROW)) else: self.addstr(display_name) @@ -300,34 +304,35 @@ class RosterWin(Win): contact.subscription, keep=show_roster_sub), to_curses_attr(theme.COLOR_ROSTER_SUBSCRIPTION)) if contact.ask: - self.addstr(get_theme().CHAR_ROSTER_ASKED, - to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) + self.addstr(theme.CHAR_ROSTER_ASKED, + to_curses_attr(theme.COLOR_IMPORTANT_TEXT)) if show_s2s_errors and contact.error: - self.addstr(get_theme().CHAR_ROSTER_ERROR, - to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) + self.addstr(theme.CHAR_ROSTER_ERROR, + to_curses_attr(theme.COLOR_ROSTER_ERROR)) if contact.tune: - self.addstr(get_theme().CHAR_ROSTER_TUNE, - to_curses_attr(get_theme().COLOR_ROSTER_TUNE)) + self.addstr(theme.CHAR_ROSTER_TUNE, + to_curses_attr(theme.COLOR_ROSTER_TUNE)) if contact.activity: - self.addstr(get_theme().CHAR_ROSTER_ACTIVITY, - to_curses_attr(get_theme().COLOR_ROSTER_ACTIVITY)) + self.addstr(theme.CHAR_ROSTER_ACTIVITY, + to_curses_attr(theme.COLOR_ROSTER_ACTIVITY)) if contact.mood: - self.addstr(get_theme().CHAR_ROSTER_MOOD, - to_curses_attr(get_theme().COLOR_ROSTER_MOOD)) + self.addstr(theme.CHAR_ROSTER_MOOD, + to_curses_attr(theme.COLOR_ROSTER_MOOD)) if contact.gaming: - self.addstr(get_theme().CHAR_ROSTER_GAMING, - to_curses_attr(get_theme().COLOR_ROSTER_GAMING)) + self.addstr(theme.CHAR_ROSTER_GAMING, + to_curses_attr(theme.COLOR_ROSTER_GAMING)) self.finish_line() def draw_resource_line(self, y: int, resource: Resource, colored: bool) -> None: """ Draw a specific resource line """ - color = get_theme().color_show(resource.presence) - self.addstr(y, 4, get_theme().CHAR_STATUS, to_curses_attr(color)) + theme = get_theme() + color = theme.color_show(resource.presence) + self.addstr(y, 4, theme.CHAR_STATUS, to_curses_attr(color)) if colored: self.addstr(y, 8, self.truncate_name(str(resource.jid), 6), - to_curses_attr(get_theme().COLOR_SELECTED_ROW)) + to_curses_attr(theme.COLOR_SELECTED_ROW)) else: self.addstr(y, 8, self.truncate_name(str(resource.jid), 6)) self.finish_line() @@ -342,10 +347,13 @@ class RosterWin(Win): class ContactInfoWin(Win): + __slots__ = () + def draw_contact_info(self, contact: Contact) -> None: """ draw the contact information """ + theme = get_theme() resource = contact.get_highest_priority_resource() if contact: jid = str(contact.bare_jid) @@ -361,8 +369,8 @@ class ContactInfoWin(Win): self.addstr(0, 0, '%s (%s)' % ( jid, presence, - ), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) + ), to_curses_attr(theme.COLOR_INFORMATION_BAR)) + self.finish_line(theme.COLOR_INFORMATION_BAR) i += 1 self.addstr(i, 0, 'Subscription: %s' % (contact.subscription, )) self.finish_line() @@ -370,7 +378,7 @@ class ContactInfoWin(Win): if contact.ask: if contact.ask == 'asked': self.addstr(i, 0, 'Ask: %s' % (contact.ask, ), - to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) + to_curses_attr(theme.COLOR_IMPORTANT_TEXT)) else: self.addstr(i, 0, 'Ask: %s' % (contact.ask, )) self.finish_line() @@ -382,33 +390,33 @@ class ContactInfoWin(Win): if contact.error: self.addstr(i, 0, 'Error: %s' % contact.error, - to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) + to_curses_attr(theme.COLOR_ROSTER_ERROR)) self.finish_line() i += 1 if contact.tune: self.addstr(i, 0, 'Tune: %s' % common.format_tune_string(contact.tune), - to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) + to_curses_attr(theme.COLOR_NORMAL_TEXT)) self.finish_line() i += 1 if contact.mood: self.addstr(i, 0, 'Mood: %s' % contact.mood, - to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) + to_curses_attr(theme.COLOR_NORMAL_TEXT)) self.finish_line() i += 1 if contact.activity: self.addstr(i, 0, 'Activity: %s' % contact.activity, - to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) + to_curses_attr(theme.COLOR_NORMAL_TEXT)) self.finish_line() i += 1 if contact.gaming: self.addstr( i, 0, 'Game: %s' % common.format_gaming_string(contact.gaming), - to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) + to_curses_attr(theme.COLOR_NORMAL_TEXT)) self.finish_line() i += 1 @@ -416,9 +424,10 @@ class ContactInfoWin(Win): """ draw the group information """ + theme = get_theme() self.addstr(0, 0, group.name, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) + to_curses_attr(theme.COLOR_INFORMATION_BAR)) + self.finish_line(theme.COLOR_INFORMATION_BAR) def refresh(self, selected_row: Row) -> None: log.debug('Refresh: %s', self.__class__.__name__) diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py index 76c7d2d7..1de905ea 100644 --- a/poezio/windows/text_win.py +++ b/poezio/windows/text_win.py @@ -32,6 +32,9 @@ class Line: class BaseTextWin(Win): + __slots__ = ('lines_nb_limit', 'pos', 'built_lines', 'lock', 'lock_buffer', + 'separator_after') + def __init__(self, lines_nb_limit: Optional[int] = None) -> None: if lines_nb_limit is None: lines_nb_limit = config.get('max_lines_in_memory') @@ -175,6 +178,8 @@ class BaseTextWin(Win): class TextWin(BaseTextWin): + __slots__ = ('highlights', 'hl_pos', 'nb_of_highlights_after_separator') + def __init__(self, lines_nb_limit: Optional[int] = None) -> None: BaseTextWin.__init__(self, lines_nb_limit) @@ -190,8 +195,6 @@ class TextWin(BaseTextWin): # This is useful to make “go to next highlight“ work after a “move to separator”. self.nb_of_highlights_after_separator = 0 - self.separator_after = None - def next_highlight(self) -> None: """ Go to the next highlight in the buffer. @@ -347,9 +350,10 @@ class TextWin(BaseTextWin): txt = message.txt if not txt: return [] + theme = get_theme() if len(message.str_time) > 8: default_color = ( - FORMAT_CHAR + dump_tuple(get_theme().COLOR_LOG_MSG) + '}') # type: Optional[str] + FORMAT_CHAR + dump_tuple(theme.COLOR_LOG_MSG) + '}') # type: Optional[str] else: default_color = None ret = [] # type: List[Union[None, Line]] @@ -357,9 +361,9 @@ class TextWin(BaseTextWin): offset = 0 if message.ack: if message.ack > 0: - offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 + offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1 else: - offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1 + offset += poopt.wcswidth(theme.CHAR_NACK) + 1 if nick: offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length if message.revisions > 0: @@ -369,9 +373,9 @@ class TextWin(BaseTextWin): if timestamp: if message.str_time: offset += 1 + len(message.str_time) - if get_theme().CHAR_TIME_LEFT and message.str_time: + if theme.CHAR_TIME_LEFT and message.str_time: offset += 1 - if get_theme().CHAR_TIME_RIGHT and message.str_time: + if theme.CHAR_TIME_RIGHT and message.str_time: offset += 1 lines = poopt.cut_text(txt, self.width - offset - 1) prepend = default_color if default_color else '' @@ -436,10 +440,11 @@ class TextWin(BaseTextWin): nick = truncate_nick(msg.nickname, nick_size) offset += poopt.wcswidth(nick) if msg.ack: + theme = get_theme() if msg.ack > 0: - offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 + offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1 else: - offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1 + offset += poopt.wcswidth(theme.CHAR_NACK) + 1 if msg.me: offset += 3 else: @@ -494,25 +499,28 @@ class TextWin(BaseTextWin): return 0 def write_line_separator(self, y) -> None: - char = get_theme().CHAR_NEW_TEXT_SEPARATOR + theme = get_theme() + char = theme.CHAR_NEW_TEXT_SEPARATOR self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width, - to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR)) + to_curses_attr(theme.COLOR_NEW_TEXT_SEPARATOR)) def write_ack(self) -> int: - color = get_theme().COLOR_CHAR_ACK + theme = get_theme() + color = theme.COLOR_CHAR_ACK self._win.attron(to_curses_attr(color)) - self.addstr(get_theme().CHAR_ACK_RECEIVED) + self.addstr(theme.CHAR_ACK_RECEIVED) self._win.attroff(to_curses_attr(color)) self.addstr(' ') - return poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 + return poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1 def write_nack(self) -> int: - color = get_theme().COLOR_CHAR_NACK + theme = get_theme() + color = theme.COLOR_CHAR_NACK self._win.attron(to_curses_attr(color)) - self.addstr(get_theme().CHAR_NACK) + self.addstr(theme.CHAR_NACK) self._win.attroff(to_curses_attr(color)) self.addstr(' ') - return poopt.wcswidth(get_theme().CHAR_NACK) + 1 + return poopt.wcswidth(theme.CHAR_NACK) + 1 def write_nickname(self, nickname, color, highlight=False) -> None: """ @@ -563,6 +571,8 @@ class TextWin(BaseTextWin): class XMLTextWin(BaseTextWin): + __slots__ = () + def __init__(self) -> None: BaseTextWin.__init__(self) @@ -621,9 +631,10 @@ class XMLTextWin(BaseTextWin): offset += poopt.wcswidth(nick) + 1 # + nick + ' ' length if message.str_time: offset += 1 + len(message.str_time) - if get_theme().CHAR_TIME_LEFT and message.str_time: + theme = get_theme() + if theme.CHAR_TIME_LEFT and message.str_time: offset += 1 - if get_theme().CHAR_TIME_RIGHT and message.str_time: + if theme.CHAR_TIME_RIGHT and message.str_time: offset += 1 lines = poopt.cut_text(txt, self.width - offset - 1) prepend = default_color if default_color else '' diff --git a/requirements-plugins.txt b/requirements-plugins.txt index 64d101de..c50dbf31 100644 --- a/requirements-plugins.txt +++ b/requirements-plugins.txt @@ -1,4 +1,4 @@ -git+git://github.com/afflux/pure-python-otr.git#egg=python-potr +git+https://github.com/afflux/pure-python-otr.git#egg=python-potr pyinotify python-mpd2 aiohttp diff --git a/requirements.txt b/requirements.txt index 7cf90325..e865ed37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cython>=0.27.3 --e git+git://git.louiz.org/slixmpp#egg=slixmpp +-e git+https://lab.louiz.org/poezio/slixmpp.git/#egg=slixmpp aiodns==1.1.1 pycares==2.3.0 pyasn1==0.4.2 diff --git a/test/test_completion.py b/test/test_completion.py index 4c5bc400..620b5658 100644 --- a/test/test_completion.py +++ b/test/test_completion.py @@ -15,13 +15,19 @@ config.config = ConfigShim() from poezio.windows import Input +class SubInput(Input): + def resize(self, *args, **kwargs): + pass + def rewrite_text(self, *args, **kwargs): + pass + def refresh(self, *args, **kwargs): + pass + + @pytest.fixture(scope="function") def input_obj(): - obj = Input() + obj = SubInput() obj.reset_completion() - obj.resize = lambda: None - obj.rewrite_text = lambda: None - obj.refresh = lambda: None return obj @pytest.fixture(scope="module") diff --git a/test/test_logger.py b/test/test_logger.py index f1851d60..09ba720e 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -7,13 +7,13 @@ from poezio.common import get_utc_time, get_local_time def test_parse_message(): line = 'MR 20170909T09:09:09Z 000 <nick> body' - assert vars(parse_log_line(line)) == vars(LogMessage('2017', '09', '09', '09', '09', '09', '0', 'nick', 'body')) + assert vars(parse_log_line(line, 'user@domain')) == vars(LogMessage('2017', '09', '09', '09', '09', '09', '0', 'nick', 'body')) line = '<>' - assert parse_log_line(line) is None + assert parse_log_line(line, 'user@domain') is None line = 'MR 20170908T07:05:04Z 003 <nick> ' - assert vars(parse_log_line(line)) == vars(LogMessage('2017', '09', '08', '07', '05', '04', '003', 'nick', '')) + assert vars(parse_log_line(line, 'user@domain')) == vars(LogMessage('2017', '09', '08', '07', '05', '04', '003', 'nick', '')) def test_log_and_parse_messages(): @@ -27,7 +27,7 @@ def test_log_and_parse_messages(): msg2_utc = get_utc_time(msg2['date']) assert built_msg2 == 'MR %s 001 <toto> coucou\n coucou\n' % (msg2_utc.strftime('%Y%m%dT%H:%M:%SZ')) - assert parse_log_lines((built_msg1 + built_msg2).split('\n')) == [ + assert parse_log_lines((built_msg1 + built_msg2).split('\n'), 'user@domain') == [ {'time': msg1['date'], 'history': True, 'txt': '\x195,-1}coucou', 'nickname': 'toto'}, {'time': msg2['date'], 'history': True, 'txt': '\x195,-1}coucou\ncoucou', 'nickname': 'toto'}, ] diff --git a/test/test_windows.py b/test/test_windows.py index cb7c86b7..af1b9d4a 100644 --- a/test/test_windows.py +++ b/test/test_windows.py @@ -9,11 +9,13 @@ config.config = ConfigShim() from poezio.windows import Input, HistoryInput, MessageInput +class SubInput(Input): + def rewrite_text(self, *args, **kwargs): + return None + @pytest.fixture def input(): - input = Input() - input.rewrite_text = lambda: None - return input + return SubInput() class TestInput(object): |