diff options
-rw-r--r-- | poezio/bookmarks.py | 4 | ||||
-rw-r--r-- | poezio/colors.py | 8 | ||||
-rw-r--r-- | poezio/config.py | 54 | ||||
-rw-r--r-- | poezio/contact.py | 9 | ||||
-rw-r--r-- | poezio/core/command_defs.py | 4 | ||||
-rw-r--r-- | poezio/core/core.py | 35 | ||||
-rw-r--r-- | poezio/core/structs.py | 10 | ||||
-rw-r--r-- | poezio/decorators.py | 11 | ||||
-rw-r--r-- | poezio/logger.py | 2 | ||||
-rw-r--r-- | poezio/multiuserchat.py | 4 | ||||
-rw-r--r-- | poezio/plugin_e2ee.py | 23 | ||||
-rw-r--r-- | poezio/tabs/basetabs.py | 36 | ||||
-rw-r--r-- | poezio/tabs/bookmarkstab.py | 4 | ||||
-rw-r--r-- | poezio/tabs/muctab.py | 5 | ||||
-rw-r--r-- | poezio/tabs/privatetab.py | 4 | ||||
-rw-r--r-- | poezio/tabs/xmltab.py | 2 | ||||
-rwxr-xr-x | poezio/theming.py | 11 | ||||
-rw-r--r-- | poezio/ui/render.py | 5 | ||||
-rw-r--r-- | poezio/windows/bookmark_forms.py | 7 | ||||
-rw-r--r-- | poezio/windows/info_bar.py | 11 | ||||
-rw-r--r-- | poezio/windows/text_win.py | 38 | ||||
-rw-r--r-- | poezio/xhtml.py | 4 |
22 files changed, 179 insertions, 112 deletions
diff --git a/poezio/bookmarks.py b/poezio/bookmarks.py index 91dc060c..ff8d2b29 100644 --- a/poezio/bookmarks.py +++ b/poezio/bookmarks.py @@ -250,7 +250,7 @@ class BookmarkList: if core is not None: core.information('Bookmarks saved', 'Info') return result - except (IqError, IqTimeout) as iq: + except (IqError, IqTimeout): if core is not None: core.information( 'Could not save remote bookmarks.', @@ -318,7 +318,7 @@ class BookmarkList: self.append(b) -def stanza_storage(bookmarks: BookmarkList) -> Bookmarks: +def stanza_storage(bookmarks: Union[BookmarkList, List[Bookmark]]) -> Bookmarks: """Generate a <storage/> stanza with the conference elements.""" storage = Bookmarks() for b in (b for b in bookmarks if b.method == 'remote'): diff --git a/poezio/colors.py b/poezio/colors.py index adb2ca40..346e1fd0 100644 --- a/poezio/colors.py +++ b/poezio/colors.py @@ -1,4 +1,4 @@ -from typing import Tuple, Dict, List +from typing import Tuple, Dict, List, Union import curses import hashlib import math @@ -15,6 +15,9 @@ K_B = 1 - K_R - K_G def ncurses_color_to_rgb(color: int) -> Tuple[float, float, float]: if color <= 15: + r: Union[int, float] + g: Union[int, float] + b: Union[int, float] try: (r, g, b) = curses.color_content(color) except: # fallback in faulty terminals (e.g. xterm) @@ -83,6 +86,9 @@ def ccg_palette_lookup(palette: Palette, angle: float) -> int: best_metric = metric best = color + if best is None: + raise ValueError("No color in palette") + return best diff --git a/poezio/config.py b/poezio/config.py index c8b04079..bb51eba1 100644 --- a/poezio/config.py +++ b/poezio/config.py @@ -19,16 +19,19 @@ import pkg_resources from configparser import RawConfigParser, NoOptionError, NoSectionError from pathlib import Path from shutil import copy2 -from typing import Callable, Dict, List, Optional, Union, Tuple, cast +from typing import Callable, Dict, List, Optional, Union, Tuple, cast, TypeVar from poezio.args import parse_args from poezio import xdg + ConfigValue = Union[str, int, float, bool] +ConfigDict = Dict[str, Dict[str, ConfigValue]] + DEFSECTION = "Poezio" -DEFAULT_CONFIG = { +DEFAULT_CONFIG: ConfigDict = { 'Poezio': { 'ack_message_receipts': True, 'add_space_after_completion': True, @@ -155,9 +158,10 @@ DEFAULT_CONFIG = { 'muc_colors': {} } +T = TypeVar('T', bool, int, float, str) -class PoezioConfigParser(RawConfigParser): +class PoezioConfigParser(RawConfigParser): def optionxform(self, value) -> str: return str(value) @@ -169,14 +173,14 @@ class Config: configparser: PoezioConfigParser file_name: Path - default: Dict[str, Dict[str, ConfigValue]] + default: ConfigDict - def __init__(self, file_name: Path, default=None) -> None: + def __init__(self, file_name: Path, default: Optional[ConfigDict] = None) -> None: self.configparser = PoezioConfigParser() # make the options case sensitive self.file_name = file_name self.read_file() - self.default = default + self.default = default or {} def optionxform(self, value): return str(value) @@ -192,8 +196,8 @@ class Config: def get(self, option: str, - default: Optional[ConfigValue] = None, - section=DEFSECTION) -> Optional[ConfigValue]: + default: Optional[T] = None, + section=DEFSECTION) -> Optional[T]: """ get a value from the config but return a default value if it is not found @@ -201,22 +205,24 @@ class Config: returned """ if default is None: - if self.default: - default = self.default.get(section, {}).get(option) - else: - default = '' + section = self.default.get('section') + if section is not None: + option = section.get(option) + if option is not None: + default = cast(T, option) + res: T try: if isinstance(default, bool): res = self.configparser.getboolean(section, option) - elif isinstance(default, int): - res = self.configparser.getint(section, option) elif isinstance(default, float): res = self.configparser.getfloat(section, option) + elif isinstance(default, int): + res = self.configparser.getint(section, option) else: res = self.configparser.get(section, option) except (NoOptionError, NoSectionError, ValueError, AttributeError): - return default if default is not None else '' + return default if res is None: return default @@ -514,26 +520,26 @@ class Config: Set a value, save, and return True on success and False on failure """ if self.has_section(section): - self.configparser.set(section, option, value) + self.configparser.set(section, option, str(value)) else: self.add_section(section) - self.configparser.set(section, option, value) - return self.write_in_file(section, option, value) + self.configparser.set(section, option, str(value)) + return self.write_in_file(section, option, str(value)) def set(self, option: str, value: ConfigValue, section=DEFSECTION): """ Set the value of an option temporarily """ try: - self.configparser.set(section, option, value) + self.configparser.set(section, option, str(value)) except NoSectionError: pass - def to_dict(self) -> Dict[str, Dict[str, ConfigValue]]: + def to_dict(self) -> Dict[str, Dict[str, Optional[ConfigValue]]]: """ Returns a dict of the form {section: {option: value, option: value}, …} """ - res = {} # Dict[str, Dict[str, ConfigValue]] + res: Dict[str, Dict[str, Optional[ConfigValue]]] = {} for section in self.sections(): res[section] = {} for option in self.options(section): @@ -567,10 +573,10 @@ def file_ok(filepath: Path) -> bool: return bool(val) -def get_image_cache() -> Path: +def get_image_cache() -> Optional[Path]: if not config.get('extract_inline_images'): return None - tmp_dir = config.get('tmp_image_dir') + tmp_dir = config.getstr('tmp_image_dir') if tmp_dir: return Path(tmp_dir) return xdg.CACHE_HOME / 'images' @@ -727,7 +733,7 @@ firstrun = False config = None # type: Config # The logger object for this module -log = None # type: Optional[logging.Logger] +log = logging.getLogger(__name__) # type: logging.Logger # The command-line options options = None diff --git a/poezio/contact.py b/poezio/contact.py index 063405c9..8359e031 100644 --- a/poezio/contact.py +++ b/poezio/contact.py @@ -45,15 +45,18 @@ class Resource: @property def priority(self) -> int: - return self._data.get('priority') or 0 + try: + return int(self._data.get('priority', 0)) + except Exception: + return 0 @property def presence(self) -> str: - return self._data.get('show') or '' + return str(self._data.get('show')) or '' @property def status(self) -> str: - return self._data.get('status') or '' + return str(self._data.get('status')) or '' def __repr__(self) -> str: return '<%s>' % self._jid diff --git a/poezio/core/command_defs.py b/poezio/core/command_defs.py index fb5c21ee..e46b83a1 100644 --- a/poezio/core/command_defs.py +++ b/poezio/core/command_defs.py @@ -1,4 +1,4 @@ -from typing import Callable, List +from typing import Callable, List, Optional from poezio.core.commands import CommandCore from poezio.core.completions import CompletionCore @@ -22,7 +22,7 @@ CommandDict = TypedDict( "shortdesc": str, "desc": str, "usage": str, - "completion": Callable, + "completion": Optional[Callable], }, total=False, ) diff --git a/poezio/core/core.py b/poezio/core/core.py index 93ea5c5b..91c9b3cc 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -16,6 +16,7 @@ import time import uuid from collections import defaultdict from typing import ( + Any, cast, Callable, Dict, @@ -233,9 +234,9 @@ class Core: '_dnd': lambda: self.command.status('dnd'), '_xa': lambda: self.command.status('xa'), ##### Custom actions ######## - '_exc_': self.try_execute, } self.key_func.update(key_func) + self.key_func.try_execute = self.try_execute # Add handlers xmpp_event_handlers = [ @@ -796,12 +797,14 @@ class Core: def remove_timed_event(self, event: DelayedEvent) -> None: """Remove an existing timed event""" - event.handler.cancel() + if event.handler is not None: + event.handler.cancel() def add_timed_event(self, event: DelayedEvent) -> None: """Add a new timed event""" event.handler = asyncio.get_event_loop().call_later( - event.delay, event.callback, *event.args) + event.delay, event.callback, *event.args + ) ####################### XMPP-related actions ################################## @@ -1174,6 +1177,7 @@ class Core: provided, we open a StaticConversationTab, else a DynamicConversationTab """ + new_tab: tabs.ConversationTab if jid.resource: new_tab = tabs.StaticConversationTab(self, jid) else: @@ -1196,19 +1200,19 @@ class Core: self.tabs.set_current_tab(tab) return tab # create the new tab - tab = self.tabs.by_name_and_class(room_name, tabs.MucTab) - if not tab: + muc_tab = self.tabs.by_name_and_class(room_name, tabs.MucTab) + if not muc_tab: return None - new_tab = tabs.PrivateTab(self, complete_jid, tab.own_nick) + tab = tabs.PrivateTab(self, complete_jid, muc_tab.own_nick) if hasattr(tab, 'directed_presence'): - new_tab.directed_presence = tab.directed_presence + tab.directed_presence = tab.directed_presence if not focus: - new_tab.state = "private" + tab.state = "private" # insert it in the tabs - self.add_tab(new_tab, focus) + self.add_tab(tab, focus) self.refresh_window() - tab.privates.append(new_tab) - return new_tab + muc_tab.privates.append(tab) + return tab def open_new_room(self, room: str, @@ -1764,18 +1768,23 @@ class Core: self.refresh_window() -class KeyDict(dict): +class KeyDict(Dict[str, Callable[[str], Any]]): """ A dict, with a wrapper for get() that will return a custom value if the key starts with _exc_ """ + try_execute: Optional[Callable[[str], Any]] def get(self, key: str, default: Optional[Callable] = None) -> Callable: if isinstance(key, str) and key.startswith('_exc_') and len(key) > 5: - return lambda: dict.get(self, '_exc_')(key[5:]) + if self.try_execute is not None: + try_execute = self.try_execute + return lambda: try_execute(key[5:]) + raise ValueError("KeyDict not initialized") return dict.get(self, key, default) + def replace_key_with_bound(key: str) -> str: """ Replace an inputted key with the one defined as its replacement diff --git a/poezio/core/structs.py b/poezio/core/structs.py index e4d42551..31d31339 100644 --- a/poezio/core/structs.py +++ b/poezio/core/structs.py @@ -1,8 +1,12 @@ """ Module defining structures useful to the core class and related methods """ +from __future__ import annotations from dataclasses import dataclass -from typing import Any, Callable, List, Dict +from typing import Any, Callable, List, TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from poezio import windows __all__ = [ 'Command', @@ -34,6 +38,7 @@ class Completion: A completion result essentially currying the input completion call. """ __slots__ = ['func', 'args', 'kwargs', 'comp_list'] + def __init__( self, func: Callable[..., Any], @@ -49,11 +54,12 @@ class Completion: def run(self): return self.func(self.comp_list, *self.args, **self.kwargs) + @dataclass class Command: __slots__ = ('func', 'desc', 'comp', 'short_desc', 'usage') func: Callable[..., Any] desc: str - comp: Callable[['windows.Input'], Completion] + comp: Optional[Callable[['windows.Input'], Completion]] short_desc: str usage: str diff --git a/poezio/decorators.py b/poezio/decorators.py index a95e7348..ee138744 100644 --- a/poezio/decorators.py +++ b/poezio/decorators.py @@ -13,20 +13,19 @@ from typing import ( List, Optional, TypeVar, - TYPE_CHECKING, ) from poezio import common -if TYPE_CHECKING: - from poezio.tabs import RosterInfoTab T = TypeVar('T', bound=Callable[..., Any]) -BeforeFunc = Callable[[List[Any], Dict[str, Any]], Any] -AfterFunc = Callable[[List[Any], Dict[str, Any]], Any] -def wrap_generic(func: Callable, before: BeforeFunc=None, after: AfterFunc=None): +BeforeFunc = Optional[Callable[[List[Any], Dict[str, Any]], Any]] +AfterFunc = Optional[Callable[[Any, List[Any], Dict[str, Any]], Any]] + + +def wrap_generic(func: Callable, before: BeforeFunc = None, after: AfterFunc = None): """ Generic wrapper which can both wrap coroutines and normal functions. """ diff --git a/poezio/logger.py b/poezio/logger.py index 579639e3..b55e579d 100644 --- a/poezio/logger.py +++ b/poezio/logger.py @@ -306,4 +306,4 @@ def create_logger() -> None: logger = Logger() -logger = None # type: Logger +logger = Logger() diff --git a/poezio/multiuserchat.py b/poezio/multiuserchat.py index cc30cd92..4f63971a 100644 --- a/poezio/multiuserchat.py +++ b/poezio/multiuserchat.py @@ -32,7 +32,7 @@ log = logging.getLogger(__name__) if TYPE_CHECKING: from poezio.core.core import Core - from poezio.tabs import Tab + from poezio.tabs import MucTab def change_show( @@ -79,7 +79,7 @@ def join_groupchat( status: Optional[str] = None, show: Optional[str] = None, seconds: Optional[int] = None, - tab: Optional[Tab] = None + tab: Optional['MucTab'] = None ) -> None: xmpp = core.xmpp stanza = xmpp.make_presence( diff --git a/poezio/plugin_e2ee.py b/poezio/plugin_e2ee.py index de6a24c0..c0c2b865 100644 --- a/poezio/plugin_e2ee.py +++ b/poezio/plugin_e2ee.py @@ -10,7 +10,16 @@ Interface for E2EE (End-to-end Encryption) plugins. """ -from typing import Callable, Dict, List, Optional, Union, Tuple, Set +from typing import ( + Callable, + Dict, + List, + Optional, + Union, + Tuple, + Set, + Type, +) from slixmpp import InvalidJID, JID, Message from slixmpp.xmlstream import StanzaBase @@ -117,7 +126,7 @@ class E2EEPlugin(BasePlugin): _enabled_tabs: Dict[JID, Callable] = {} # Tabs that support this encryption mechanism - supported_tab_types: Tuple[ChatTabs] = tuple() + supported_tab_types: Tuple[Type[ChatTabs], ...] = tuple() # States for each remote entity trust_states: Dict[str, Set[str]] = {'accepted': set(), 'rejected': set()} @@ -224,7 +233,7 @@ class E2EEPlugin(BasePlugin): except InvalidJID: return "" - if self._encryption_enabled(jid): + if self._encryption_enabled(jid) and self.encryption_short_name: return " " + self.encryption_short_name return "" @@ -238,7 +247,7 @@ class E2EEPlugin(BasePlugin): '{} encryption disabled for {}'.format(self.encryption_name, jid), 'Info', ) - else: + elif self.encryption_short_name: self._enabled_tabs[jid] = self.encrypt config.set_and_save('encryption', self.encryption_short_name, section=jid) self.api.information( @@ -368,9 +377,9 @@ class E2EEPlugin(BasePlugin): # comes from a semi-anonymous MUC for example. Some plugins might be # fine with this so let them handle it. jid = message['from'] - muctab = tab - if isinstance(muctab, PrivateTab): + muctab = None + if isinstance(tab, PrivateTab): muctab = tab.parent_muc jid = None @@ -386,7 +395,7 @@ class E2EEPlugin(BasePlugin): log.debug('Decrypted %s message: %r', self.encryption_name, message['body']) return None - async def _encrypt(self, stanza: StanzaBase) -> Optional[StanzaBase]: + async def _encrypt(self, stanza: StanzaBase, passthrough: bool = True) -> Optional[StanzaBase]: if not isinstance(stanza, Message) or stanza['type'] not in ('normal', 'chat', 'groupchat'): raise NothingToEncrypt() message = stanza diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index 7e584a92..0c59b89c 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -26,6 +26,7 @@ from xml.sax import SAXParseException from typing import ( Any, Callable, + cast, Dict, List, Optional, @@ -48,6 +49,7 @@ from poezio.text_buffer import TextBuffer from poezio.theming import get_theme, dump_tuple from poezio.ui.funcs import truncate_nick from poezio.ui.types import BaseMessage, InfoMessage, Message +from poezio.timed_events import DelayedEvent from slixmpp import JID, InvalidJID, Message as SMessage @@ -117,6 +119,10 @@ class Tab: # Placeholder values, set on resize height = 1 width = 1 + core: Core + input: Optional[windows.Input] + key_func: Dict[str, Callable[[], Any]] + commands: Dict[str, Command] def __init__(self, core: Core): self.core = core @@ -234,7 +240,7 @@ class Tab: *, desc='', shortdesc='', - completion: Optional[Callable] = None, + completion: Optional[Callable[[windows.Input], Completion]] = None, usage=''): """ Add a command @@ -286,7 +292,6 @@ class Tab: comp = command.comp(the_input) if comp: return comp.run() - return comp return False def execute_command(self, provided_text: str) -> bool: @@ -294,8 +299,10 @@ class Tab: Execute the command in the input and return False if the input didn't contain a command """ + if self.input is None: + raise NotImplementedError txt = provided_text or self.input.key_enter() - if txt.startswith('/') and not txt.startswith('//') and\ + if txt and txt.startswith('/') and not txt.startswith('//') and\ not txt.startswith('/me '): command = txt.strip().split()[0][1:] arg = txt[2 + len(command):] # jump the '/' and the ' ' @@ -461,6 +468,9 @@ class Tab: class GapTab(Tab): + def __init__(self, core: Optional[Core], *args, **kwargs): + super().__init__(core, **args, **kwargs) + def __bool__(self): return False @@ -485,7 +495,10 @@ class ChatTab(Tab): """ plugin_commands: Dict[str, Command] = {} plugin_keys: Dict[str, Callable] = {} + last_sent_message: Optional[SMessage] message_type = 'chat' + timed_event_paused: Optional[DelayedEvent] + timed_event_not_paused: Optional[DelayedEvent] def __init__(self, core, jid: Union[JID, str]): Tab.__init__(self, core) @@ -507,7 +520,7 @@ class ChatTab(Tab): self.timed_event_paused = None self.timed_event_not_paused = None # Keeps the last sent message to complete it easily in completion_correct, and to replace it. - self.last_sent_message = {} + self.last_sent_message = None self.key_func['M-v'] = self.move_separator self.key_func['M-h'] = self.scroll_separator self.key_func['M-/'] = self.last_words_completion @@ -625,6 +638,8 @@ class ChatTab(Tab): self.input.auto_completion(words, ' ', quotify=False) def on_enter(self): + if self.input is None: + raise NotImplementedError txt = self.input.key_enter() if txt: if not self.execute_command(txt): @@ -740,16 +755,18 @@ class ChatTab(Tab): if self.timed_event_paused is not None: self.core.remove_timed_event(self.timed_event_paused) self.timed_event_paused = None - self.core.remove_timed_event(self.timed_event_not_paused) - self.timed_event_not_paused = None + if self.timed_event_not_paused is not None: + self.core.remove_timed_event(self.timed_event_not_paused) + self.timed_event_not_paused = None def set_last_sent_message(self, msg: SMessage, correct: bool = False) -> None: """Ensure last_sent_message is set with the correct attributes""" if correct: # XXX: Is the copy needed. Is the object passed here reused # afterwards? Who knows. - msg = copy(msg) - msg['id'] = self.last_sent_message['id'] + msg = cast(SMessage, copy(msg)) + if self.last_sent_message is not None: + msg['id'] = self.last_sent_message['id'] self.last_sent_message = msg @command_args_parser.raw @@ -783,7 +800,8 @@ class ChatTab(Tab): self.text_win.remove_line_separator() self.text_win.add_line_separator(self._text_buffer) self.text_win.refresh() - self.input.refresh() + if self.input: + self.input.refresh() def get_conversation_messages(self): return self._text_buffer.messages diff --git a/poezio/tabs/bookmarkstab.py b/poezio/tabs/bookmarkstab.py index a953c750..986e0b9d 100644 --- a/poezio/tabs/bookmarkstab.py +++ b/poezio/tabs/bookmarkstab.py @@ -34,8 +34,8 @@ class BookmarksTab(Tab): self.new_bookmarks: List[Bookmark] = [] self.removed_bookmarks: List[Bookmark] = [] self.header_win = windows.ColumnHeaderWin( - ('name', 'room@server/nickname', 'password', 'autojoin', - 'storage')) + ['name', 'room@server/nickname', 'password', 'autojoin', + 'storage']) self.bookmarks_win = windows.BookmarksWin( self.bookmarks, self.height - 4, self.width, 1, 0) self.help_win = windows.HelpText('Ctrl+Y: save, Ctrl+G: cancel, ' diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index 188ed156..b6360e92 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -166,6 +166,7 @@ class MucTab(ChatTab): """ Join the room """ + seconds: Optional[int] status = self.core.get_status() if self.last_connection: delta = to_utc(datetime.now()) - to_utc(self.last_connection) @@ -725,7 +726,7 @@ class MucTab(ChatTab): new_nick = presence.xml.find( '{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER) ).attrib['nick'] - old_color = user.color + old_color_tuple = user.color if user.nick == self.own_nick: self.own_nick = new_nick # also change our nick in all private discussions of this room @@ -741,7 +742,7 @@ class MucTab(ChatTab): if config.get_by_tabname('display_user_color_in_join_part', self.general_jid): color = dump_tuple(user.color) - old_color = dump_tuple(old_color) + old_color = dump_tuple(old_color_tuple) else: old_color = color = "3" info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py index 9d782df8..a00f032d 100644 --- a/poezio/tabs/privatetab.py +++ b/poezio/tabs/privatetab.py @@ -109,7 +109,7 @@ class PrivateTab(OneToOneTab): if not isinstance(msg, Message): return if not logger.log_message( - self.jid.full, msg.nickname, msg.txt, date=msg.time, typ=typ): + self.jid.full, msg.nickname or '', msg.txt or '', date=msg.time, typ=typ): self.core.information('Unable to write in the log file', 'Error') def on_close(self): @@ -160,7 +160,7 @@ class PrivateTab(OneToOneTab): self.core.events.trigger('private_say', msg, self) if not msg['body']: return - if correct or msg['replace']['id']: + if correct or msg['replace']['id'] and self.last_sent_message: msg['replace']['id'] = self.last_sent_message['id'] else: del msg['replace'] diff --git a/poezio/tabs/xmltab.py b/poezio/tabs/xmltab.py index 0409b445..15fd2ed4 100644 --- a/poezio/tabs/xmltab.py +++ b/poezio/tabs/xmltab.py @@ -10,6 +10,7 @@ log = logging.getLogger(__name__) import curses import os +from typing import Union, Optional from slixmpp import JID, InvalidJID from slixmpp.xmlstream import matcher, StanzaBase from slixmpp.xmlstream.tostring import tostring @@ -55,6 +56,7 @@ MATCHERS_MAPPINGS = { class XMLTab(Tab): + input: Optional[Union[windows.HelpText, windows.CommandInput]] def __init__(self, core): Tab.__init__(self, core) self.state = 'normal' diff --git a/poezio/theming.py b/poezio/theming.py index e1d7ec87..d8720d51 100755 --- a/poezio/theming.py +++ b/poezio/theming.py @@ -74,7 +74,7 @@ except ImportError: import curses import functools import os -from typing import Dict, List, Union, Tuple, Optional +from typing import Dict, List, Union, Tuple, Optional, cast from pathlib import Path from os import path from poezio import colors, xdg @@ -450,6 +450,7 @@ def to_curses_attr( returns a valid curses attr that can be passed directly to attron() or attroff() """ # extract the color from that tuple + colors: Union[Tuple[int, int], Tuple[int, int, str]] if len(color_tuple) == 3: colors = (color_tuple[0], color_tuple[1]) else: @@ -475,7 +476,7 @@ def to_curses_attr( curses_colors_dict[colors] = pair curses_pair = curses.color_pair(pair) if len(color_tuple) == 3: - additional_val = color_tuple[2] + _, _, additional_val = cast(Tuple[int, int, str], color_tuple) if 'b' in additional_val or bold is True: curses_pair = curses_pair | curses.A_BOLD if 'u' in additional_val: @@ -561,10 +562,10 @@ def reload_theme() -> Optional[str]: new_theme = None exc = None try: - loader = finder.find_module(theme_name, load_path) - if not loader: + spec = finder.find_spec(theme_name, path=load_path) + if not spec or not spec.loader: return 'Failed to load the theme %s' % theme_name - new_theme = loader.load_module() + new_theme = spec.loader.load_module(theme_name) except Exception as e: log.error('Failed to load the theme %s', theme_name, exc_info=True) exc = e diff --git a/poezio/ui/render.py b/poezio/ui/render.py index 13e493f1..f377df7f 100644 --- a/poezio/ui/render.py +++ b/poezio/ui/render.py @@ -10,6 +10,7 @@ from functools import singledispatch from math import ceil, log10 from typing import ( List, + Optional, Tuple, TYPE_CHECKING, ) @@ -52,7 +53,8 @@ class Line: LinePos = Tuple[int, int] -def generate_lines(lines: List[LinePos], msg: BaseMessage, default_color: str = '') -> List[Line]: + +def generate_lines(lines: List[LinePos], msg: BaseMessage, default_color: str = '') -> List[Line]: line_objects = [] attrs: List[str] = [] prepend = default_color if default_color else '' @@ -131,6 +133,7 @@ def write_pre_message(msg: Message, win: Win, with_timestamps: bool, nick_size: - nick (with a "* " for /me) - LMC number if present """ + color: Optional[Tuple] offset = 0 if with_timestamps: offset += PreMessageHelpers.write_time(win, msg.history, msg.time) diff --git a/poezio/windows/bookmark_forms.py b/poezio/windows/bookmark_forms.py index f1e737fd..10851b3b 100644 --- a/poezio/windows/bookmark_forms.py +++ b/poezio/windows/bookmark_forms.py @@ -384,5 +384,8 @@ class BookmarksWin(Win): def save(self) -> None: for line in self.lines: - for item in line: - item.save() + line[0].save() + line[1].save() + line[2].save() + line[3].save() + line[4].save() diff --git a/poezio/windows/info_bar.py b/poezio/windows/info_bar.py index 8c2dd1a1..8fb34d91 100644 --- a/poezio/windows/info_bar.py +++ b/poezio/windows/info_bar.py @@ -5,17 +5,18 @@ This window is the one listing the current opened tabs in poezio. The GlobalInfoBar can be either horizontal or vertical (VerticalGlobalInfoBar). """ -import logging +import curses import itertools -log = logging.getLogger(__name__) +import logging -import curses +from typing import List, Optional from poezio.config import config from poezio.windows.base_wins import Win from poezio.theming import get_theme, to_curses_attr from poezio.common import unique_prefix_of +log = logging.getLogger(__name__) class GlobalInfoBar(Win): __slots__ = ('core') @@ -31,14 +32,14 @@ class GlobalInfoBar(Win): self.addstr(0, 0, "[", to_curses_attr(theme.COLOR_INFORMATION_BAR)) - show_names = config.getboom('show_tab_names') + show_names = config.getbool('show_tab_names') show_nums = config.getbool('show_tab_numbers') use_nicks = config.getbool('use_tab_nicks') show_inactive = config.getbool('show_inactive_tabs') unique_prefix_tab_names = config.getbool('unique_prefix_tab_names') if unique_prefix_tab_names: - unique_prefixes = [None] * len(self.core.tabs) + unique_prefixes: List[Optional[str]] = [None] * len(self.core.tabs) sorted_tab_indices = sorted( (str(tab.name), i) for i, tab in enumerate(self.core.tabs) diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py index 2ddf7082..31dfb637 100644 --- a/poezio/windows/text_win.py +++ b/poezio/windows/text_win.py @@ -4,17 +4,13 @@ Can be locked, scrolled, has a separator, etc… """ import logging -import curses -from math import ceil, log10 from typing import Optional, List, Union -from poezio.windows.base_wins import Win, FORMAT_CHAR -from poezio.ui.funcs import truncate_nick, parse_attrs +from poezio.windows.base_wins import Win from poezio.text_buffer import TextBuffer -from poezio import poopt from poezio.config import config -from poezio.theming import to_curses_attr, get_theme, dump_tuple +from poezio.theming import to_curses_attr, get_theme from poezio.ui.types import Message, BaseMessage from poezio.ui.render import Line, build_lines, write_pre @@ -26,6 +22,8 @@ class TextWin(Win): 'separator_after', 'highlights', 'hl_pos', 'nb_of_highlights_after_separator') + hl_pos: Optional[int] + def __init__(self, lines_nb_limit: Optional[int] = None) -> None: Win.__init__(self) if lines_nb_limit is None: @@ -38,14 +36,14 @@ class TextWin(Win): self.lock = False self.lock_buffer: List[Union[None, Line]] = [] - self.separator_after: Optional[Line] = None + self.separator_after: Optional[BaseMessage] = None # the Lines of the highlights in that buffer self.highlights: List[Line] = [] # the current HL position in that list NaN means that we’re not on # an hl. -1 is a valid position (it's before the first hl of the # list. i.e the separator, in the case where there’s no hl before # it.) - self.hl_pos = float('nan') + self.hl_pos = None # Keep track of the number of hl after the separator. # This is useful to make “go to next highlight“ work after a “move to separator”. @@ -149,6 +147,7 @@ class TextWin(Win): self.addstr_colored(txt, y, x) def resize(self, height: int, width: int, y: int, x: int, room: TextBuffer=None) -> None: + old_width: Optional[int] if hasattr(self, 'width'): old_width = self.width else: @@ -202,7 +201,6 @@ class TextWin(Win): if room and room.messages: self.separator_after = room.messages[-1] - def write_line_separator(self, y) -> None: theme = get_theme() char = theme.CHAR_NEW_TEXT_SEPARATOR @@ -222,13 +220,13 @@ class TextWin(Win): highlights, scroll to the end of the buffer. """ log.debug('Going to the next highlight…') - if (not self.highlights or self.hl_pos != self.hl_pos + if (not self.highlights or self.hl_pos is None or self.hl_pos >= len(self.highlights) - 1): - self.hl_pos = float('nan') + self.hl_pos = None self.pos = 0 return hl_size = len(self.highlights) - 1 - if self.hl_pos < hl_size: + if self.hl_pos is not None and self.hl_pos < hl_size: self.hl_pos += 1 else: self.hl_pos = hl_size @@ -239,9 +237,10 @@ class TextWin(Win): try: pos = self.built_lines.index(hl) except ValueError: - del self.highlights[self.hl_pos] + if isinstance(self.hl_pos, int): + del self.highlights[self.hl_pos] if not self.highlights: - self.hl_pos = float('nan') + self.hl_pos = None self.pos = 0 return self.hl_pos = 0 @@ -258,11 +257,11 @@ class TextWin(Win): highlights, scroll to the end of the buffer. """ log.debug('Going to the previous highlight…') - if not self.highlights or self.hl_pos <= 0: - self.hl_pos = float('nan') + if not self.highlights or self.hl_pos and self.hl_pos <= 0: + self.hl_pos = None self.pos = 0 return - if self.hl_pos != self.hl_pos: + if self.hl_pos is None: self.hl_pos = len(self.highlights) - 1 else: self.hl_pos -= 1 @@ -273,9 +272,10 @@ class TextWin(Win): try: pos = self.built_lines.index(hl) except ValueError: - del self.highlights[self.hl_pos] + if self.hl_pos is not None: + del self.highlights[self.hl_pos] if not self.highlights: - self.hl_pos = float('nan') + self.hl_pos = None self.pos = 0 return self.hl_pos = 0 diff --git a/poezio/xhtml.py b/poezio/xhtml.py index d8ea49a6..e886bb3d 100644 --- a/poezio/xhtml.py +++ b/poezio/xhtml.py @@ -20,7 +20,7 @@ from pathlib import Path from io import BytesIO from xml import sax -from xml.sax import saxutils +from xml.sax import saxutils, ContentHandler from typing import List, Dict, Optional, Union, Tuple from slixmpp.xmlstream import ET @@ -299,7 +299,7 @@ def get_hash(data: bytes) -> str: b'/', b'-').decode() -class XHTMLHandler(sax.ContentHandler): +class XHTMLHandler(ContentHandler): def __init__(self, force_ns=False, tmp_image_dir: Optional[Path] = None) -> None: self.builder: List[str] = [] |