...
 
Commits (20)
......@@ -4,7 +4,7 @@ https://dev.louiz.org/projects/poezio/roadmap
* Poezio 0.14 - dev
* Poezio 0.13.1 - dev
* Poezio 0.13.1
# Bug fixes
......@@ -23,6 +23,8 @@ https://dev.louiz.org/projects/poezio/roadmap
- Fix marquee and dice plugin to use newer Last Message Correction format.
- Bookmarks tab properly displays bookmarks method (local/remote) and allows
to switch between them again.
- Updated manpages and added manpages built from the doc in the setup process.
Packagers need to run setup.py build_man to have them built.
# Forgotten additions
......
......@@ -68,6 +68,7 @@
</provides>
<releases>
<release version="0.13.1" date="2020-05-31"/>
<release version="0.13" date="2020-05-24"/>
<release version="0.12.1" date="2018-09-12"/>
<release version="0.12" date="2018-08-13"/>
......
.\" Copyright 2010 Le Coz Florent
.\" This man page is distributed under the GPLv3 license.
.\" See COPYING file
.TH "Poezio" "1" "September 26, 2011" "Poezio dev team" ""
.TH "Poezio" "1" "May 31, 2020" "Poezio dev team" ""
.SH "NAME"
Poezio \- a ncurses jabber client written in python3
Poezio \- a ncurses jabber client written in python
.SH "SYNOPSIS"
.B poezio [\-f \fICONFIG_FILE\fR] [\-d \fIDEBUG_FILE\fR] [\-h]
.SH "DESCRIPTION"
.B Poezio
is a console jabber (XMPP) client written in Python and using ncurses to draw its interface. It aims at being similar to the most famous IRC clients, like weechat or irssi. The keyboard shortcuts are inspired from emacs. For more information on XMPP see http://xmpp.org and on Poezio see https://poez.io
is a console jabber (XMPP) client written in Python and using ncurses to draw its interface. It aims at being similar to the most famous IRC clients, like weechat or irssi. Some keyboard shortcuts are inspired from emacs. For more information on XMPP see http://xmpp.org and on Poezio see https://poez.io
.PP
.SH "OPTIONS"
.TP
\fB\-f\fR, \fB\-\-file \fICONFIG_FILE\fR
Run poezio using \fICONFIG_FILE\fR as the config file instead of ~/.config/poezio/poezio.cfg
Run poezio using \fICONFIG_FILE\fR as the config file instead of ~/.config/poezio/poezio.cfg.
.TP
\fB\-d\fR, \fB\-\-debug \fIDEBUG_FILE\fR
Log debug from both poezio and SleekXMPP in \fIDEBUG_FILE\fR. Debug contains incoming and outgoing stanzas in addition to various message helping poezio's debugging.
Log debug from both poezio and slixmpp in \fIDEBUG_FILE\fR. Debug contains incoming and outgoing stanzas in addition to various message helping poezio's debugging.
.TP
\fB\-h\fR
Display an help message
\fB\-c\fR, \fB\-\-check\-config\fR
Display the list of modified/unmodified config options, with their changes from the default.
.TP
\fB\-h\fR, \fB\-\-help\fR
Display the poezio help message.
.SH "BASICS"
The following sections will give you a short overview on how to use poezio. Poezio has many more options, commands and key bindings, please refer to \fIpoezio.cfg(7)\fR, \fIpoezio.commands(7)\fR, \fIpoezio.keys(7)\fR or the full documentation which should have been provided alongside the source code, or check it online at https://doc.poez.io/.
.SH "TABS"
A \fItab\fR, in Poezio, is the base structure of the interface. A tab may contains one or more \fIwindows\fR, and can be of different types:
A \fItab\fR, in Poezio, is the base structure of the interface. A tab may contains one or more \fIwindows\fR, the main types are:
.RS
.TP 6
.I Roster \fRtab
It contains a list of your contacts on the left, as well as an info window on the right.
It contains a browsable list of your contacts on the left, as well as an info window on the right.
.TP
.I MUC \fRtab
MUC stands for "Multi-User Chat".
.I Chatroom \fRtab
This tab displays the contents of a multi-user chat.
.TP
.I Conversation \fRtab
It is used for one-to-one communication, usually when using a real Jabber account.
.TP
.I Private \fRtab
It is used to privately communicate with someone in a MUC.
.SH "KEY BINDINGS"
While most of the keyboard shortcuts are common to all types of tabs, some of them are tab-specific.
.SS Text edition
.SS Text edition
These shortcuts work in any kind of tab; most of them are identical to emacs' ones.
.RS
.TP 8
......@@ -146,9 +149,6 @@ The opposite of \fI/accept\fR.
.SS MUC-specific commands
.RS
.TP 5
.B /recolor
Change the color of the nicknames in the conversation. Useful when a few people are talking and their random color happen to be the same: using this command will let you differentiate them more easily.
.TP
.B /kick <user>
Kick the specified user from the room.
.TP
......@@ -165,7 +165,7 @@ View or change the topic of the room.
Talk privately with the specified participant.
.TP
.B /part
Leave the current room.
Leave the current room.
.SH "BUGS"
Sure.
......@@ -174,8 +174,8 @@ Sure.
If you're using a terminal multiplexer such as \fIscreen\fR or \fItmux\fR, it may be setting $TERM to "screen", which breaks 256-color support. Consider setting your $TERM to something like "screen-256color".
.SH "FEEDBACK"
You are encouraged to report bugs or feature requests on https://dev.louiz.org/projects/poezio.
You can also find us on the Jabber chatroom poezio@muc.poez.io
You are encouraged to report bugs or feature requests on https://lab.louiz.org/poezio/poezio.
You can also find us on the Jabber chatroom xmpp:poezio@muc.poez.io?join
.SH "AUTHORS"
Written by Florent Le Coz <louiz@louiz.org>
......
......@@ -53,9 +53,9 @@ copyright = '%s, Mathieu Pasquet - Florent Le Coz - Emmanuel Gil Peyrot' % time.
# built documents.
#
# The short X.Y version.
version = '0.14'
version = '0.13'
# The full version, including alpha/beta/rc tags.
release = '0.14-dev'
release = '0.13.1'
add_function_parentheses = True
......
......@@ -9,7 +9,7 @@ if [ -e .git ]
then
args=$(git show --format='%h %ci' | head -n1)
else
args="0.12-dev"
args="0.13.1-dev"
fi
if [ -e "$POEZIO_VENV" ]
......
......@@ -14,7 +14,7 @@ from datetime import (
timezone,
)
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
from typing import Dict, List, Optional, Tuple, Union, Any
import os
import subprocess
......@@ -44,7 +44,7 @@ def _get_output_of_command(command: str) -> Optional[List[str]]:
return None
def _is_in_path(command: str, return_abs_path=False) -> Union[bool, str]:
def _is_in_path(command: str, return_abs_path: bool = False) -> Union[bool, str]:
"""
Check if *command* is in the $PATH or not.
......@@ -111,10 +111,12 @@ def get_os_info() -> str:
stdout=subprocess.PIPE,
close_fds=True)
process.wait()
output = process.stdout.readline().decode('utf-8').strip()
# some distros put n/a in places, so remove those
output = output.replace('n/a', '').replace('N/A', '')
return output
if process.stdout is not None:
out = process.stdout.readline().decode('utf-8').strip()
# some distros put n/a in places, so remove those
out = out.replace('n/a', '').replace('N/A', '')
return out
return ''
# lsb_release executable not available, so parse files
for distro_name in DISTRO_INFO:
......@@ -287,7 +289,7 @@ def shell_split(st: str) -> List[str]:
return ret
def find_argument(pos: int, text: str, quoted=True) -> int:
def find_argument(pos: int, text: str, quoted: bool = True) -> int:
"""
Split an input into a list of arguments, return the number of the
argument selected by pos.
......@@ -342,7 +344,7 @@ def _find_argument_unquoted(pos: int, text: str) -> int:
return argnum + 1
def parse_str_to_secs(duration='') -> int:
def parse_str_to_secs(duration: str = '') -> int:
"""
Parse a string of with a number of d, h, m, s.
......@@ -370,7 +372,7 @@ def parse_str_to_secs(duration='') -> int:
return result
def parse_secs_to_str(duration=0) -> str:
def parse_secs_to_str(duration: int = 0) -> str:
"""
Do the reverse operation of :py:func:`parse_str_to_secs`.
......@@ -457,7 +459,7 @@ def format_gaming_string(infos: Dict[str, str]) -> str:
return name
def safeJID(*args, **kwargs) -> JID:
def safeJID(*args: Any, **kwargs: Any) -> JID:
"""
Construct a :py:class:`slixmpp.JID` object from a string.
......
......@@ -15,7 +15,17 @@ import shutil
import time
import uuid
from collections import defaultdict
from typing import Callable, Dict, List, Optional, Set, Tuple, Type
from typing import (
cast,
Callable,
Dict,
List,
Optional,
Set,
Tuple,
Type,
TypeVar,
)
from xml.etree import ElementTree as ET
from functools import partial
......@@ -65,6 +75,7 @@ from poezio.ui.types import Message, InfoMessage
log = logging.getLogger(__name__)
T = TypeVar('T', bound=tabs.Tab)
class Core:
"""
......@@ -99,8 +110,10 @@ class Core:
# that are displayed in almost all tabs, in an
# information window.
self.information_buffer = TextBuffer()
self.information_win_size = config.get(
'info_win_height', section='var')
self.information_win_size = cast(
int,
config.get('info_win_height', section='var'),
)
self.information_win = windows.TextWin(300)
self.information_buffer.add_window(self.information_win)
self.left_tab_win = None
......@@ -813,7 +826,7 @@ class Core:
####################### XMPP-related actions ##################################
def get_status(self) -> str:
def get_status(self) -> Status:
"""
Get the last status that was previously set
"""
......@@ -1016,7 +1029,7 @@ class Core:
### Tab getters ###
def get_tabs(self, cls: Type[tabs.Tab] = None) -> List[tabs.Tab]:
def get_tabs(self, cls: Type[T] = None) -> List[T]:
"Get all the tabs of a type"
if cls is None:
return self.tabs.get_tabs()
......@@ -1324,7 +1337,7 @@ class Core:
if tab.name.startswith(room_name):
tab.activate(reason=reason)
def on_user_changed_status_in_private(self, jid: JID, status: str) -> None:
def on_user_changed_status_in_private(self, jid: JID, status: Status) -> None:
tab = self.tabs.by_name_and_class(jid, tabs.ChatTab)
if tab is not None: # display the message in private
tab.update_status(status)
......@@ -1652,7 +1665,7 @@ class Core:
return
else:
scr = self.stdscr
tabs.Tab.resize(scr)
tabs.Tab.initial_resize(scr)
self.resize_global_info_bar()
self.resize_global_information_win()
for tab in self.tabs:
......@@ -2105,7 +2118,7 @@ class Core:
self.bookmarks.get_remote(self.xmpp, self.information,
_join_remote_only)
def room_error(self, error, room_name):
def room_error(self, error: IqError, room_name: str) -> None:
"""
Display the error in the tab
"""
......
......@@ -789,6 +789,7 @@ class HandlerCore:
if not replaced:
# Messages coming from MUC barejid (Server maintenance, IRC mode
# changes from biboumi, etc.) are displayed as info messages.
highlight = False
if message['from'].resource:
highlight = tab.message_is_highlight(body, nick_from, delayed)
ui_msg = PMessage(
......
"""
Module defining structures useful to the core class and related methods
"""
from dataclasses import dataclass
from typing import Any, Callable, List, Dict
__all__ = [
'ERROR_AND_STATUS_CODES', 'DEPRECATED_ERRORS', 'POSSIBLE_SHOW', 'Status',
......@@ -51,23 +53,11 @@ POSSIBLE_SHOW = {
}
@dataclass
class Status:
__slots__ = ('show', 'message')
def __init__(self, show, message):
self.show = show
self.message = message
class Command:
__slots__ = ('func', 'desc', 'comp', 'short_desc', 'usage')
def __init__(self, func, desc, comp, short_desc, usage):
self.func = func
self.desc = desc
self.comp = comp
self.short_desc = short_desc
self.usage = usage
show: str
message: str
class Completion:
......@@ -75,8 +65,13 @@ class Completion:
A completion result essentially currying the input completion call.
"""
__slots__ = ['func', 'args', 'kwargs', 'comp_list']
def __init__(self, func, comp_list, *args, **kwargs):
def __init__(
self,
func: Callable[..., Any],
comp_list: List[str],
*args: Any,
**kwargs: Any
) -> None:
self.func = func
self.comp_list = comp_list
self.args = args
......@@ -84,3 +79,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]
short_desc: str
usage: str
......@@ -347,16 +347,16 @@ class Tabs:
if new_pos < len(self._tabs):
old_tab = self._tabs[old_pos]
self._tabs[new_pos], self._tabs[
old_pos] = old_tab, tabs.GapTab(self)
old_pos] = old_tab, tabs.GapTab(None)
else:
self._tabs.append(self._tabs[old_pos])
self._tabs[old_pos] = tabs.GapTab(self)
self._tabs[old_pos] = tabs.GapTab(None)
else:
if new_pos > old_pos:
self._tabs.insert(new_pos, tab)
self._tabs[old_pos] = tabs.GapTab(self)
self._tabs[old_pos] = tabs.GapTab(None)
elif new_pos < old_pos:
self._tabs[old_pos] = tabs.GapTab(self)
self._tabs[old_pos] = tabs.GapTab(None)
self._tabs.insert(new_pos, tab)
else:
return False
......
"""
Module containing various decorators
"""
from typing import Any, Callable, List, Optional
from typing import (
cast,
Any,
Callable,
List,
Optional,
TypeVar,
TYPE_CHECKING,
)
from poezio import common
if TYPE_CHECKING:
from poezio.tabs import RosterInfoTab
T = TypeVar('T', bound=Callable[..., Any])
class RefreshWrapper:
def __init__(self):
def __init__(self) -> None:
self.core = None
def conditional(self, func: Callable) -> Callable:
def conditional(self, func: T) -> T:
"""
Decorator to refresh the UI if the wrapped function
returns True
"""
def wrap(*args, **kwargs):
def wrap(*args: Any, **kwargs: Any) -> Any:
ret = func(*args, **kwargs)
if self.core and ret:
self.core.refresh_window()
return ret
return wrap
return cast(T, wrap)
def always(self, func: Callable) -> Callable:
def always(self, func: T) -> T:
"""
Decorator that refreshs the UI no matter what after the function
"""
def wrap(*args, **kwargs):
def wrap(*args: Any, **kwargs: Any) -> Any:
ret = func(*args, **kwargs)
if self.core:
self.core.refresh_window()
return ret
return wrap
return cast(T, wrap)
def update(self, func: Callable) -> Callable:
def update(self, func: T) -> T:
"""
Decorator that only updates the screen
"""
def wrap(*args, **kwargs):
def wrap(*args: Any, **kwargs: Any) -> Any:
ret = func(*args, **kwargs)
if self.core:
self.core.doupdate()
return ret
return wrap
return cast(T, wrap)
refresh_wrapper = RefreshWrapper()
......@@ -61,32 +75,32 @@ class CommandArgParser:
"""
@staticmethod
def raw(func: Callable) -> Callable:
def raw(func: T) -> T:
"""Just call the function with a single string, which is the original string
untouched
"""
def wrap(self, args, *a, **kw):
def wrap(self: Any, args: Any, *a: Any, **kw: Any) -> Any:
return func(self, args, *a, **kw)
return wrap
return cast(T, wrap)
@staticmethod
def ignored(func: Callable) -> Callable:
def ignored(func: T) -> T:
"""
Call the function without any argument
"""
def wrap(self, args=None, *a, **kw):
def wrap(self: Any, args: Any = None, *a: Any, **kw: Any) -> Any:
return func(self, *a, **kw)
return wrap
return cast(T, wrap)
@staticmethod
def quoted(mandatory: int,
optional=0,
optional: int = 0,
defaults: Optional[List[Any]] = None,
ignore_trailing_arguments=False):
ignore_trailing_arguments: bool = False) -> Callable[[T], T]:
"""The function receives a list with a number of arguments that is between
the numbers `mandatory` and `optional`.
......@@ -131,8 +145,8 @@ class CommandArgParser:
"""
default_args_outer = defaults or []
def first(func: Callable):
def second(self, args: str, *a, **kw):
def first(func: T) -> T:
def second(self: Any, args: str, *a: Any, **kw: Any) -> Any:
default_args = default_args_outer
if args and args.strip():
split_args = common.shell_split(args)
......@@ -156,8 +170,7 @@ class CommandArgParser:
res[-1] += " " + " ".join(split_args)
return func(self, res, *a, **kw)
return second
return cast(T, second)
return first
......@@ -166,11 +179,11 @@ command_args_parser = CommandArgParser()
def deny_anonymous(func: Callable) -> Callable:
"""Decorator to disable commands when using an anonymous account."""
def wrap(self: 'RosterInfoTab', *args, **kwargs):
def wrap(self: 'RosterInfoTab', *args: Any, **kwargs: Any) -> Any:
if self.core.xmpp.anon:
return self.core.information(
'This command is not available for anonymous accounts.',
'Info'
)
return func(self, *args, **kwargs)
return wrap
return cast(T, wrap)
......@@ -5,7 +5,8 @@ upstream.
TODO: Check that they are fixed and remove those hacks
"""
from slixmpp.stanza import Message
from typing import Callable, Any
from slixmpp import Message, Iq, ClientXMPP
from slixmpp.xmlstream import ET
import logging
......@@ -25,7 +26,7 @@ def has_identity(xmpp, jid, identity, on_true=None, on_false=None):
xmpp.plugin['xep_0030'].get_info(jid=jid, callback=_cb)
def get_room_form(xmpp, room, callback):
def get_room_form(xmpp: ClientXMPP, room: str, callback: Callable[[Iq], Any]):
def _cb(result):
if result["type"] == "error":
return callback(None)
......
......@@ -11,18 +11,39 @@ slix plugin
"""
from xml.etree import ElementTree as ET
from typing import (
Callable,
Optional,
TYPE_CHECKING,
)
from poezio.common import safeJID
from slixmpp import JID
from slixmpp.exceptions import IqError, IqTimeout
from slixmpp import (
JID,
ClientXMPP,
Iq,
)
import logging
log = logging.getLogger(__name__)
if TYPE_CHECKING:
from poezio.core import Core
from poezio.tabs import Tab
from slixmpp.plugins.xep_0004 import Form
NS_MUC_ADMIN = 'http://jabber.org/protocol/muc#admin'
NS_MUC_OWNER = 'http://jabber.org/protocol/muc#owner'
def destroy_room(xmpp, room, reason='', altroom=''):
def destroy_room(
xmpp: ClientXMPP,
room: str,
reason: str = '',
altroom: str = ''
) -> bool:
"""
destroy a room
"""
......@@ -42,7 +63,7 @@ def destroy_room(xmpp, room, reason='', altroom=''):
query.append(destroy)
iq.append(query)
def callback(iq):
def callback(iq: Iq) -> None:
if not iq or iq['type'] == 'error':
xmpp.core.information('Unable to destroy room %s' % room, 'Info')
else:
......@@ -52,23 +73,13 @@ def destroy_room(xmpp, room, reason='', altroom=''):
return True
def send_private_message(xmpp, jid, line):
"""
Send a private message
"""
jid = safeJID(jid)
xmpp.send_message(mto=jid, mbody=line, mtype='chat')
def send_groupchat_message(xmpp, jid, line):
"""
Send a message to the groupchat
"""
jid = safeJID(jid)
xmpp.send_message(mto=jid, mbody=line, mtype='groupchat')
def change_show(xmpp, jid: JID, own_nick: str, show, status):
def change_show(
xmpp: ClientXMPP,
jid: JID,
own_nick: str,
show: str,
status: Optional[str]
) -> None:
"""
Change our 'Show'
"""
......@@ -81,7 +92,7 @@ def change_show(xmpp, jid: JID, own_nick: str, show, status):
pres.send()
def change_subject(xmpp, jid, subject):
def change_subject(xmpp: ClientXMPP, jid: JID, subject: str) -> None:
"""
Change the room subject
"""
......@@ -92,7 +103,13 @@ def change_subject(xmpp, jid, subject):
msg.send()
def change_nick(core, jid, nick, status=None, show=None):
def change_nick(
core: 'Core',
jid: JID,
nick: str,
status: Optional[str] = None,
show: Optional[str] = None
) -> None:
"""
Change our own nick in a room
"""
......@@ -103,14 +120,16 @@ def change_nick(core, jid, nick, status=None, show=None):
presence.send()
def join_groupchat(core,
jid,
nick,
passwd='',
status=None,
show=None,
seconds=None,
tab=None):
def join_groupchat(
core: 'Core',
jid: JID,
nick: str,
passwd: str = '',
status: Optional[str] = None,
show: Optional[str] = None,
seconds: Optional[int] = None,
tab: Optional['Tab'] = None
) -> None:
xmpp = core.xmpp
stanza = xmpp.make_presence(
pto='%s/%s' % (jid, nick), pstatus=status, pshow=show)
......@@ -119,8 +138,10 @@ def join_groupchat(core,
passelement = ET.Element('password')
passelement.text = passwd
x.append(passelement)
def on_disco(iq):
if 'urn:xmpp:mam:2' in iq['disco_info'].get_features() or (tab and tab._text_buffer.last_message):
def on_disco(iq: Iq) -> None:
if ('urn:xmpp:mam:2' in iq['disco_info'].get_features()
or (tab and tab._text_buffer.last_message)):
history = ET.Element('{http://jabber.org/protocol/muc}history')
history.attrib['seconds'] = str(0)
x.append(history)
......@@ -136,13 +157,15 @@ def join_groupchat(core,
xmpp.plugin['xep_0045'].rooms[jid] = {}
xmpp.plugin['xep_0045'].our_nicks[jid] = to.resource
try:
xmpp.plugin['xep_0030'].get_info(jid=jid, callback=on_disco)
except (IqError, IqTimeout):
return core.information('Failed to retrieve messages', 'Error')
xmpp.plugin['xep_0030'].get_info(jid=jid, callback=on_disco)
def leave_groupchat(xmpp, jid, own_nick, msg):
def leave_groupchat(
xmpp: ClientXMPP,
jid: JID,
own_nick: str,
msg: str
) -> None:
"""
Leave the groupchat
"""
......@@ -156,7 +179,14 @@ def leave_groupchat(xmpp, jid, own_nick, msg):
exc_info=True)
def set_user_role(xmpp, jid, nick, reason, role, callback=None):
def set_user_role(
xmpp: ClientXMPP,
jid: JID,
nick: str,
reason: str,
role: str,
callback: Callable[[Iq], None]
) -> None:
"""
(try to) Set the role of a MUC user
(role = 'none': eject user)
......@@ -172,21 +202,18 @@ def set_user_role(xmpp, jid, nick, reason, role, callback=None):
query.append(item)
iq.append(query)
iq['to'] = jid
if callback:
return iq.send(callback=callback)
try:
return iq.send()
except (IqError, IqTimeout) as e:
return e.iq
def set_user_affiliation(xmpp,
muc_jid,
affiliation,
nick=None,
jid=None,
reason=None,
callback=None):
iq.send(callback=callback)
def set_user_affiliation(
xmpp: ClientXMPP,
muc_jid: JID,
affiliation: str,
callback: Callable[[Iq], None],
nick: Optional[str] = None,
jid: Optional[JID] = None,
reason: Optional[str] = None
) -> None:
"""
(try to) Set the affiliation of a MUC user
"""
......@@ -212,18 +239,10 @@ def set_user_affiliation(xmpp,
query.append(item)
iq = xmpp.make_iq_set(query)
iq['to'] = muc_jid
if callback:
return iq.send(callback=callback)
try:
return xmpp.plugin['xep_0045'].set_affiliation(
str(muc_jid),
str(jid) if jid else None, nick, affiliation)
except:
log.debug('Error setting the affiliation: %s', exc_info=True)
return False
iq.send(callback=callback)
def cancel_config(xmpp, room):
def cancel_config(xmpp: ClientXMPP, room: str) -> None:
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
x = ET.Element('{jabber:x:data}x', type='cancel')
query.append(x)
......@@ -232,7 +251,7 @@ def cancel_config(xmpp, room):
iq.send()
def configure_room(xmpp, room, form):
def configure_room(xmpp: ClientXMPP, room: str, form: 'Form') -> None:
if form is None:
return
iq = xmpp.make_iq_set()
......
......@@ -104,7 +104,7 @@ def main():
logger.create_logger()
from poezio import roster
roster.create_roster()
roster.roster.reset()
from poezio.core.core import Core
......
from typing import List, Tuple, Any, TextIO, Union, Optional, Iterable, TypeVar
import sys
def split(s: str, comments: bool = ..., posix: bool = ...) -> List[str]: ...
if sys.version_info >= (3, 8):
def join(split_command: Iterable[str]) -> str: ...
def quote(s: str) -> str: ...
_SLT = TypeVar('_SLT', bound=shlex)
class shlex(Iterable[str]):
commenters: str
wordchars: str
whitespace: str
escape: str
quotes: str
escapedquotes: str
whitespace_split: bool
infile: str
instream: TextIO
source: str
debug: int
lineno: int
token: str
eof: str
if sys.version_info >= (3, 6):
punctuation_chars: str
if sys.version_info >= (3, 6):
def __init__(self, instream: Union[str, TextIO] = ..., infile: Optional[str] = ...,
posix: bool = ..., punctuation_chars: Union[bool, str] = ...) -> None: ...
else:
def __init__(self, instream: Union[str, TextIO] = ..., infile: Optional[str] = ...,
posix: bool = ...) -> None: ...
def get_token(self) -> Tuple[int, int, str]: ...
def push_token(self, tok: str) -> None: ...
def read_token(self) -> str: ...
def sourcehook(self, filename: str) -> Tuple[str, TextIO]: ...
# TODO argument types
def push_source(self, newstream: Any, newfile: Any = ...) -> None: ...
def pop_source(self) -> None: ...
def error_leader(self, infile: str = ...,
lineno: int = ...) -> None: ...
def __iter__(self: _SLT) -> _SLT: ...
def __next__(self) -> str: ...
......@@ -10,6 +10,8 @@ Defines the Roster and RosterGroup classes
import logging
log = logging.getLogger(__name__)
from typing import List
from poezio.config import config
from poezio.contact import Contact
from poezio.roster_sorting import SORTING_METHODS, GROUP_SORTING_METHODS
......@@ -18,6 +20,7 @@ from os import path as p
from datetime import datetime
from poezio.common import safeJID
from slixmpp.exceptions import IqError, IqTimeout
from slixmpp import JID
class Roster:
......@@ -29,6 +32,22 @@ class Roster:
DEFAULT_FILTER = (lambda x, y: None, None)
def __init__(self):
self.__node = None
# A tuple(function, *args) function to filter contacts
# on search, for example
self.contact_filter = self.DEFAULT_FILTER
self.groups = {}
self.contacts = {}
self.length = 0
self.connected = 0
self.folded_groups = []
# Used for caching roster infos
self.last_built = datetime.now()
self.last_modified = datetime.now()
def reset(self):
"""
node: the RosterSingle from slixmpp
"""
......@@ -143,7 +162,7 @@ class Roster:
"""Subscribe to a jid"""
self.__node.subscribe(jid)
def jids(self):
def jids(self) -> List[JID]:
"""List of the contact JIDS"""
l = []
for key in self.__node.keys():
......@@ -335,11 +354,6 @@ class RosterGroup:
return len([1 for contact in self.contacts if len(contact)])
def create_roster():
"Create the global roster object"
global roster
roster = Roster()
# Shared roster object
roster = None
roster = Roster()
......@@ -28,6 +28,7 @@ from typing import (
List,
Optional,
Union,
Tuple,
TYPE_CHECKING,
)
......@@ -52,6 +53,8 @@ from slixmpp import JID, InvalidJID, Message as SMessage
if TYPE_CHECKING:
from _curses import _CursesWindow # pylint: disable=E0611
from poezio.size_manager import SizeManager
from poezio.core.core import Core
log = logging.getLogger(__name__)
......@@ -117,7 +120,7 @@ class Tab:
height = 1
width = 1
def __init__(self, core):
def __init__(self, core: 'Core'):
self.core = core
self.nb = 0
if not hasattr(self, 'name'):
......@@ -133,7 +136,7 @@ class Tab:
self.commands = {} # and their own commands
@property
def size(self) -> int:
def size(self) -> 'SizeManager':
return self.core.size
@staticmethod
......@@ -196,7 +199,7 @@ class Tab:
self._state = 'normal'
@staticmethod
def resize(scr: '_CursesWindow'):
def initial_resize(scr: '_CursesWindow'):
Tab.height, Tab.width = scr.getmaxyx()
windows.base_wins.TAB_WIN = scr
......@@ -327,7 +330,7 @@ class Tab:
else:
return False
def refresh_tab_win(self):
def refresh_tab_win(self) -> None:
if config.get('enable_vertical_tab_list'):
left_tab_win = self.core.left_tab_win
if left_tab_win and not self.size.core_degrade_x:
......@@ -371,12 +374,12 @@ class Tab:
"""
pass
def update_commands(self):
def update_commands(self) -> None:
for c in self.plugin_commands:
if c not in self.commands:
self.commands[c] = self.plugin_commands[c]
def update_keys(self):
def update_keys(self) -> None:
for k in self.plugin_keys:
if k not in self.key_func:
self.key_func[k] = self.plugin_keys[k]
......@@ -435,7 +438,7 @@ class Tab:
"""
pass
def on_close(self):
def on_close(self) -> None:
"""
Called when the tab is to be closed
"""
......@@ -443,7 +446,7 @@ class Tab:
self.input.on_delete()
self.closed = True
def matching_names(self) -> List[str]:
def matching_names(self) -> List[Tuple[int, str]]:
"""
Returns a list of strings that are used to name a tab with the /win
command. For example you could switch to a tab that returns
......@@ -532,7 +535,7 @@ class ChatTab(Tab):
desc='Fix the last message with whatever you want.',
shortdesc='Correct the last message.',
completion=self.completion_correct)
self.chat_state = None
self.chat_state = None # type: Optional[str]
self.update_commands()
self.update_keys()
......@@ -667,11 +670,11 @@ class ChatTab(Tab):
self._text_buffer.messages = []
self.text_win.rebuild_everything(self._text_buffer)
def check_send_chat_state(self):
def check_send_chat_state(self) -> bool:
"If we should send a chat state"
return True
def send_chat_state(self, state, always_send=False):
def send_chat_state(self, state: str, always_send: bool = False) -> None:
"""
Send an empty chatstate message
"""
......@@ -691,9 +694,8 @@ class ChatTab(Tab):
x = ET.Element('{%s}x' % NS_MUC_USER)
msg.append(x)
msg.send()
return True
def send_composing_chat_state(self, empty_after):
def send_composing_chat_state(self, empty_after: bool) -> None:
"""
Send the "active" or "composing" chatstate, depending
on the the current status of the input
......@@ -729,7 +731,7 @@ class ChatTab(Tab):
self.core.add_timed_event(new_event)
self.timed_event_not_paused = new_event
def cancel_paused_delay(self):
def cancel_paused_delay(self) -> None:
"""
Remove that event from the list and set it to None.
Called for example when the input is emptied, or when the message
......@@ -741,7 +743,7 @@ class ChatTab(Tab):
self.core.remove_timed_event(self.timed_event_not_paused)
self.timed_event_not_paused = None
def set_last_sent_message(self, msg, correct=False):
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
......@@ -751,7 +753,7 @@ class ChatTab(Tab):
self.last_sent_message = msg
@command_args_parser.raw
def command_correct(self, line):
def command_correct(self, line: str) -> None:
"""
/correct <fixed message>
"""
......@@ -777,7 +779,7 @@ class ChatTab(Tab):
return self.core.status.show in ('xa', 'away') or\
(hasattr(self, 'directed_presence') and not self.directed_presence)
def move_separator(self):
def move_separator(self) -> None:
self.text_win.remove_line_separator()
self.text_win.add_line_separator(self._text_buffer)
self.text_win.refresh()
......@@ -786,7 +788,7 @@ class ChatTab(Tab):
def get_conversation_messages(self):
return self._text_buffer.messages
def check_scrolled(self):
def check_scrolled(self) -> None:
if self.text_win.pos != 0:
self.state = 'scrolled'
......
......@@ -95,7 +95,7 @@ class BookmarksTab(Tab):
def send_cb(success):
if success:
self.core.information('Bookmarks saved.', 'Info')
self.core.information('Bookmarks saved', 'Info')
else:
self.core.information('Remote bookmarks not saved.', 'Error')
......
This diff is collapsed.
......@@ -145,7 +145,7 @@ class PrivateTab(OneToOneTab):
@refresh_wrapper.always
@command_args_parser.raw
def command_say(self, line, attention=False, correct=False):
def command_say(self, line: str, attention: bool = False, correct: bool = False) -> None:
if not self.on:
return
our_jid = JID(self.jid.bare)
......
......@@ -32,6 +32,7 @@ from poezio.ui.types import (
if TYPE_CHECKING:
from poezio.windows.text_win import TextWin
from poezio.user import User
class CorrectionError(Exception):
......@@ -249,7 +250,7 @@ class TextBuffer:
new_id: str,
highlight: bool = False,
time: Optional[datetime] = None,
user: Optional[str] = None,
user: Optional['User'] = None,
jid: Optional[str] = None) -> Message:
"""
Correct a message in a text buffer.
......
......@@ -55,7 +55,7 @@ class User:
else:
self.color = choice(get_theme().LIST_COLOR_NICKNAMES)
def set_deterministic_color(self):
def set_deterministic_color(self) -> None:
theme = get_theme()
if theme.ccg_palette:
# use XEP-0392 CCG
......
__version__ = '0.14'
__version__ = '0.13.1'
__version_info__ = (0, 13, 1)
......@@ -3,6 +3,8 @@ Module defining all the "info wins", ie the bar which is on top of the
info buffer in normal tabs
"""
from typing import Optional, Dict, TYPE_CHECKING, Any
import logging
log = logging.getLogger(__name__)
......@@ -13,6 +15,11 @@ from poezio.windows.base_wins import Win
from poezio.ui.funcs import truncate_nick
from poezio.theming import get_theme, to_curses_attr
if TYPE_CHECKING:
from poezio.user import User
from poezio.tabs import MucTab
from poezio.windows import TextWin
class InfoWin(Win):
"""
......@@ -260,10 +267,16 @@ class MucInfoWin(InfoWin):
__slots__ = ()
def __init__(self):
def __init__(self) -> None:
InfoWin.__init__(self)
def refresh(self, room, window=None, user=None, information=None):
def refresh(
self,
room: 'MucTab',
window: Optional['TextWin'] = None,
user: Optional['User'] = None,
information: Optional[Dict[str, Any]] = None
) -> None:
log.debug('Refresh: %s', self.__class__.__name__)
self._win.erase()
self.write_room_name(room)
......
#!/usr/bin/env python3
import os
import subprocess
import sys
from tempfile import TemporaryFile
try:
from setuptools import setup, Extension
except ImportError:
print('\nSetuptools was not found. Install setuptools for python 3.\n')
import sys
sys.exit(1)
import os
import subprocess
from tempfile import TemporaryFile
cmdclass = {}
try:
from sphinx.setup_command import BuildDoc
cmdclass = {'build_man': BuildDoc}
except ImportError:
print('\nSphinx not found, the build_man command will be unavailable.\n')
current_dir = os.path.dirname(__file__)
from poezio.version import __version__
def get_relative_dir(folder, stopper):
"""
Find the path from a directory to a pseudo-root in order to recreate
......@@ -57,8 +65,30 @@ def check_include(library_name, header):
print('%s headers not found.' % library_name)
return False
def sphinx_man():
expected_sphinx_files = [
'build/sphinx/man/poezio.cfg.7',
'build/sphinx/man/poezio.keys.7',
'build/sphinx/man/poezio.commands.7'
]
found = []
for item in expected_sphinx_files:
if os.path.exists(item):
found.append(item)
if found:
return [('share/man/man7/', found)]
return []
sphinx_files_found = sphinx_man()
if not sphinx_files_found:
print(
'\nSphinx-built manpages not found. Only the '
'short handwritten manpages will be installed\n'
)
if not check_include('python3', 'Python.h'):
import sys
sys.exit(2)
module_poopt = Extension('poezio.poopt',
......@@ -83,57 +113,68 @@ if os.path.exists(git_dir):
except:
version = '.dev1'
else:
version = '.dev1'
version = ''
with open('README.rst', encoding='utf-8') as readme_fd:
LONG_DESCRIPTION = readme_fd.read()
setup(name="poezio",
version="0.14" + version,
description="A console XMPP client",
long_description=LONG_DESCRIPTION,
ext_modules=[module_poopt],
url='https://poez.io/',
license='zlib',
download_url='https://dev.louiz.org/projects/poezio/files',
author='Florent Le Coz',
author_email='louiz@louiz.org',
maintainer='Mathieu Pasquet',
maintainer_email='mathieui@mathieui.net',
classifiers=['Development Status :: 5 - Production/Stable',
'Topic :: Communications :: Chat',
'Topic :: Internet :: XMPP',
'Environment :: Console :: Curses',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: zlib/libpng License',
'Natural Language :: English',
'Operating System :: Unix',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3 :: Only'],
keywords=['jabber', 'xmpp', 'client', 'chat', 'im', 'console'],
packages=['poezio', 'poezio.core', 'poezio.tabs', 'poezio.windows',
'poezio.ui', 'poezio_plugins', 'poezio_themes'],
package_dir={'poezio': 'poezio',
'poezio_plugins': 'plugins',
'poezio_themes': 'data/themes'},
package_data={'poezio': ['default_config.cfg']},
scripts=['scripts/poezio_logs'],
entry_points={'console_scripts': ['poezio = poezio.__main__:run']},
data_files=([('share/man/man1/', ['data/poezio.1',
'data/poezio_logs.1']),
('share/poezio/', ['README.rst', 'COPYING', 'CHANGELOG']),
('share/applications/', ['data/io.poez.Poezio.desktop']),
('share/metainfo/', ['data/io.poez.Poezio.appdata.xml'])]
+ find_doc('share/doc/poezio/source', 'source')
+ find_doc('share/doc/poezio/html', 'build/html')),
install_requires=['slixmpp>=1.5.2', 'aiodns', 'pyasn1_modules', 'pyasn1'],
extras_require={'OTR plugin': 'python-potr>=1.0',
'Screen autoaway plugin': 'pyinotify==0.9.4',
'Avoiding cython': 'cffi'})
setup(
name="poezio",
version=__version__ + version,
description="A console XMPP client",
long_description=LONG_DESCRIPTION,
ext_modules=[module_poopt],
url='https://poez.io/',
license='zlib',
download_url='https://dev.louiz.org/projects/poezio/files',
author='Florent Le Coz',
author_email='louiz@louiz.org',
maintainer='Mathieu Pasquet',
maintainer_email='mathieui@mathieui.net',
classifiers=['Development Status :: 5 - Production/Stable',
'Topic :: Communications :: Chat',
'Topic :: Internet :: XMPP',
'Environment :: Console :: Curses',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: zlib/libpng License',
'Natural Language :: English',
'Operating System :: Unix',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3 :: Only'],
keywords=['jabber', 'xmpp', 'client', 'chat', 'im', 'console'],
packages=['poezio', 'poezio.core', 'poezio.tabs', 'poezio.windows',
'poezio.ui', 'poezio_plugins', 'poezio_themes'],
package_dir={'poezio': 'poezio',
'poezio_plugins': 'plugins',
'poezio_themes': 'data/themes'},
package_data={'poezio': ['default_config.cfg']},
scripts=['scripts/poezio_logs'],
entry_points={'console_scripts': ['poezio = poezio.__main__:run']},
data_files=([
('share/man/man1/', ['data/poezio.1', 'data/poezio_logs.1']),
('share/poezio/', ['README.rst', 'COPYING', 'CHANGELOG']),
('share/applications/', ['data/io.poez.Poezio.desktop']),
('share/metainfo/', ['data/io.poez.Poezio.appdata.xml'])
]
+ find_doc('share/doc/poezio/source', 'source')
+ find_doc('share/doc/poezio/html', 'build/html')
+ sphinx_files_found
),
install_requires=['slixmpp>=1.5.2', 'aiodns', 'pyasn1_modules', 'pyasn1'],
extras_require={'OTR plugin': 'python-potr>=1.0',
'Screen autoaway plugin': 'pyinotify==0.9.4',
'Avoiding cython': 'cffi'},
cmdclass=cmdclass,
command_options={
'build_man' : {
'builder': ('setup.py', 'man'),
}
},
)
# Remove the link afterwards
if (os.path.exists(os.path.join(current_dir, 'poezio', 'default_config.cfg')) and
......