summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore117
-rw-r--r--.gitlab-ci.yml9
-rw-r--r--Dockerfile2
-rw-r--r--README.rst4
-rw-r--r--data/io.poez.Poezio.json2
-rw-r--r--doc/source/commands.rst23
-rw-r--r--doc/source/configuration.rst19
-rw-r--r--doc/source/dev/contributing.rst4
-rw-r--r--doc/source/dev/overview.rst6
-rw-r--r--doc/source/dev/xep.rst2
-rw-r--r--doc/source/install.rst4
-rw-r--r--doc/source/misc/client_certs.rst2
-rw-r--r--doc/source/misc/separate.rst2
-rw-r--r--plugins/disco.py4
-rw-r--r--plugins/embed.py2
-rw-r--r--plugins/upload.py2
-rw-r--r--poezio/config.py1
-rw-r--r--poezio/core/commands.py19
-rw-r--r--poezio/core/completions.py13
-rw-r--r--poezio/core/core.py94
-rw-r--r--poezio/core/handlers.py81
-rw-r--r--poezio/decorators.py8
-rw-r--r--poezio/logger.py27
-rw-r--r--poezio/tabs/conversationtab.py6
-rw-r--r--poezio/tabs/muctab.py133
-rw-r--r--poezio/tabs/privatetab.py29
-rw-r--r--poezio/windows/base_wins.py2
-rw-r--r--poezio/windows/bookmark_forms.py28
-rw-r--r--poezio/windows/confirm.py2
-rw-r--r--poezio/windows/data_forms.py48
-rw-r--r--poezio/windows/funcs.py2
-rw-r--r--poezio/windows/image.py55
-rw-r--r--poezio/windows/info_bar.py25
-rw-r--r--poezio/windows/info_wins.py112
-rw-r--r--poezio/windows/input_placeholders.py2
-rw-r--r--poezio/windows/inputs.py5
-rw-r--r--poezio/windows/list.py20
-rw-r--r--poezio/windows/misc.py4
-rw-r--r--poezio/windows/muc.py20
-rw-r--r--poezio/windows/roster_win.py81
-rw-r--r--poezio/windows/text_win.py49
-rw-r--r--requirements-plugins.txt2
-rw-r--r--requirements.txt2
-rw-r--r--test/test_completion.py14
-rw-r--r--test/test_logger.py8
-rw-r--r--test/test_windows.py8
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'"
diff --git a/Dockerfile b/Dockerfile
index 0283483f..6b2bb7fb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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 && \
diff --git a/README.rst b/README.rst
index 454d2f42..34c01b1f 100644
--- a/README.rst
+++ b/README.rst
@@ -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):