changeset 3481:7892585b7e17

core (setup), jp, primitivus: update console scripts + classifiers: - console scripts have been updated following the renaming with: - "libervia-cli" and "libervia-tui" for "jp" and "primitivus" - "li" alias as a shortcut for libervia-cli - "jp" and "primitivus" are kept as alias - updated classifiers to reflect Python 3.9 support
author Goffi <goffi@goffi.org>
date Sat, 20 Mar 2021 20:42:07 +0100
parents 7550ae9cfbac
children acb28399480f
files sat_frontends/jp/base.py sat_frontends/jp/cmd_shell.py sat_frontends/jp/jp sat_frontends/primitivus/base.py sat_frontends/primitivus/primitivus setup.py
diffstat 6 files changed, 883 insertions(+), 889 deletions(-) [+]
line wrap: on
line diff
--- a/sat_frontends/jp/base.py	Sat Mar 20 20:42:04 2021 +0100
+++ b/sat_frontends/jp/base.py	Sat Mar 20 20:42:07 2021 +0100
@@ -84,7 +84,7 @@
     return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL)
 
 
-class Jp(object):
+class LiberviaCli:
     """
     This class can be use to establish a connection with the
     bridge. Moreover, it should manage a main loop.
@@ -669,10 +669,14 @@
         except QuitException:
             return
 
-    def run(self, args=None, namespace=None):
+    def _run(self, args=None, namespace=None):
         self.loop = JPLoop()
         self.loop.run(self, args, namespace)
 
+    @classmethod
+    def run(cls):
+        cls()._run()
+
     def _read_stdin(self, stdin_fut):
         """Callback called by ainput to read stdin"""
         line = sys.stdin.readline()
--- a/sat_frontends/jp/cmd_shell.py	Sat Mar 20 20:42:04 2021 +0100
+++ b/sat_frontends/jp/cmd_shell.py	Sat Mar 20 20:42:07 2021 +0100
@@ -112,7 +112,7 @@
         # XXX: below is a way to launch the command without creating a new process
         #      may be used when a solution to the aforementioned issue is there
         # try:
-        #     self.host.run(args)
+        #     self.host._run(args)
         # except SystemExit as e:
         #     ret_code = e.code
         # except Exception as e:
--- a/sat_frontends/jp/jp	Sat Mar 20 20:42:04 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# jp: a SAT command line tool
-# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from sat_frontends.jp import base
-
-if __name__ == "__main__":
-    jp = base.Jp()
-    jp.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat_frontends/primitivus/base.py	Sat Mar 20 20:42:07 2021 +0100
@@ -0,0 +1,861 @@
+#!/usr/bin/env python3
+
+# Primitivus: a SAT frontend
+# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from sat.core.i18n import _, D_
+from sat_frontends.primitivus.constants import Const as C
+from sat.core import log_config
+log_config.satConfigure(C.LOG_BACKEND_STANDARD, C)
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.tools import config as sat_config
+import urwid
+from urwid.util import is_wide_char
+from urwid_satext import sat_widgets
+from sat_frontends.quick_frontend.quick_app import QuickApp
+from sat_frontends.quick_frontend import quick_utils
+from sat_frontends.quick_frontend import quick_chat
+from sat_frontends.primitivus.profile_manager import ProfileManager
+from sat_frontends.primitivus.contact_list import ContactList
+from sat_frontends.primitivus.chat import Chat
+from sat_frontends.primitivus import xmlui
+from sat_frontends.primitivus.progress import Progress
+from sat_frontends.primitivus.notify import Notify
+from sat_frontends.primitivus.keys import action_key_map as a_key
+from sat_frontends.primitivus import config
+from sat_frontends.tools.misc import InputHistory
+from sat.tools.common import dynamic_import
+from sat_frontends.tools import jid
+import signal
+import sys
+## bridge handling
+# we get bridge name from conf and initialise the right class accordingly
+main_config = sat_config.parseMainConf()
+bridge_name = sat_config.getConfig(main_config, '', 'bridge', 'dbus')
+if 'dbus' not in bridge_name:
+    print(u"only D-Bus bridge is currently supported")
+    sys.exit(3)
+
+
+class EditBar(sat_widgets.ModalEdit):
+    """
+    The modal edit bar where you would enter messages and commands.
+    """
+
+    def __init__(self, host):
+        modes = {None: (C.MODE_NORMAL, u''),
+                 a_key['MODE_INSERTION']: (C.MODE_INSERTION, u'> '),
+                 a_key['MODE_COMMAND']: (C.MODE_COMMAND, u':')} #XXX: captions *MUST* be unicode
+        super(EditBar, self).__init__(modes)
+        self.host = host
+        self.setCompletionMethod(self._text_completion)
+        urwid.connect_signal(self, 'click', self.onTextEntered)
+
+    def _text_completion(self, text, completion_data, mode):
+        if mode == C.MODE_INSERTION:
+            if self.host.selected_widget is not None:
+                try:
+                    completion = self.host.selected_widget.completion
+                except AttributeError:
+                    return text
+                else:
+                    return completion(text, completion_data)
+        else:
+            return text
+
+    def onTextEntered(self, editBar):
+        """Called when text is entered in the main edit bar"""
+        if self.mode == C.MODE_INSERTION:
+            if isinstance(self.host.selected_widget, quick_chat.QuickChat):
+                chat_widget = self.host.selected_widget
+                self.host.messageSend(
+                    chat_widget.target,
+                    {'': editBar.get_edit_text()}, # TODO: handle language
+                    mess_type = C.MESS_TYPE_GROUPCHAT if chat_widget.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
+                    errback=lambda failure: self.host.showDialog(_("Error while sending message ({})").format(failure), type="error"),
+                    profile_key=chat_widget.profile
+                    )
+                editBar.set_edit_text('')
+        elif self.mode == C.MODE_COMMAND:
+            self.commandHandler()
+
+    def commandHandler(self):
+        #TODO: separate class with auto documentation (with introspection)
+        #      and completion method
+        tokens = self.get_edit_text().split(' ')
+        command, args = tokens[0], tokens[1:]
+        if command == 'quit':
+            self.host.onExit()
+            raise urwid.ExitMainLoop()
+        elif command == 'messages':
+            wid = sat_widgets.GenericList(logging.memoryGet())
+            self.host.selectWidget(wid)
+        # FIXME: reactivate the command
+        # elif command == 'presence':
+        #     values = [value for value in commonConst.PRESENCE.keys()]
+        #     values = [value if value else 'online' for value in values]  # the empty value actually means 'online'
+        #     if args and args[0] in values:
+        #         presence = '' if args[0] == 'online' else args[0]
+        #         self.host.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
+        #     else:
+        #         self.host.status_bar.onPresenceClick()
+        # elif command == 'status':
+        #     if args:
+        #         self.host.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
+        #     else:
+        #         self.host.status_bar.onStatusClick()
+        elif command == 'history':
+            widget = self.host.selected_widget
+            if isinstance(widget, quick_chat.QuickChat):
+                try:
+                    limit = int(args[0])
+                except (IndexError, ValueError):
+                    limit = 50
+                widget.updateHistory(size=limit, profile=widget.profile)
+        elif command == 'search':
+            widget = self.host.selected_widget
+            if isinstance(widget, quick_chat.QuickChat):
+                pattern = " ".join(args)
+                if not pattern:
+                    self.host.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
+                else:
+                    widget.updateHistory(size=C.HISTORY_LIMIT_NONE, filters={'search': pattern}, profile=widget.profile)
+        elif command == 'filter':
+            # FIXME: filter is now only for current widget,
+            #        need to be able to set it globally or per widget
+            widget = self.host.selected_widget
+            # FIXME: Q&D way, need to be more generic
+            if isinstance(widget, quick_chat.QuickChat):
+                widget.setFilter(args)
+        elif command in ('topic', 'suject', 'title'):
+            try:
+                new_title = args[0].strip()
+            except IndexError:
+                new_title = None
+            widget = self.host.selected_widget
+            if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP:
+                widget.onSubjectDialog(new_title)
+        else:
+            return
+        self.set_edit_text('')
+
+    def _historyCb(self, text):
+        self.set_edit_text(text)
+        self.set_edit_pos(len(text))
+
+    def keypress(self, size, key):
+        """Callback when a key is pressed. Send "composing" states
+        and move the index of the temporary history stack."""
+        if key == a_key['MODAL_ESCAPE']:
+            # first save the text to the current mode, then change to NORMAL
+            self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
+            self.host._updateInputHistory(mode=C.MODE_NORMAL)
+        if self._mode == C.MODE_NORMAL and key in self._modes:
+            self.host._updateInputHistory(mode=self._modes[key][0])
+        if key == a_key['HISTORY_PREV']:
+            self.host._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
+            return
+        elif key == a_key['HISTORY_NEXT']:
+            self.host._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
+            return
+        elif key == a_key['EDIT_ENTER']:
+            self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
+        else:
+            if (self._mode == C.MODE_INSERTION
+                and isinstance(self.host.selected_widget, quick_chat.QuickChat)
+                and key not in sat_widgets.FOCUS_KEYS
+                and key not in (a_key['HISTORY_PREV'], a_key['HISTORY_NEXT'])
+                and self.host.sync):
+                self.host.bridge.chatStateComposing(self.host.selected_widget.target, self.host.selected_widget.profile)
+
+        return super(EditBar, self).keypress(size, key)
+
+
+class PrimitivusTopWidget(sat_widgets.FocusPile):
+    """Top most widget used in Primitivus"""
+    _focus_inversed = True
+    positions = ('menu', 'body', 'notif_bar', 'edit_bar')
+    can_hide = ('menu', 'notif_bar')
+
+    def __init__(self, body, menu, notif_bar, edit_bar):
+        self._body = body
+        self._menu = menu
+        self._notif_bar = notif_bar
+        self._edit_bar = edit_bar
+        self._hidden = {'notif_bar'}
+        self._focus_extra = False
+        super(PrimitivusTopWidget, self).__init__([('pack', self._menu), self._body, ('pack', self._edit_bar)])
+        for position in self.positions:
+            setattr(self,
+                    position,
+                    property(lambda: self, self.widgetGet(position=position),
+                             lambda pos, new_wid: self.widgetSet(new_wid, position=pos))
+                   )
+        self.focus_position = len(self.contents)-1
+
+    def getVisiblePositions(self, keep=None):
+        """Return positions that are not hidden in the right order
+
+        @param keep: if not None, this position will be keep in the right order, even if it's hidden
+                    (can be useful to find its index)
+        @return (list): list of visible positions
+        """
+        return [pos for pos in self.positions if (keep and pos == keep) or pos not in self._hidden]
+
+    def keypress(self, size, key):
+        """Manage FOCUS keys that focus directly a main part (one of self.positions)
+
+        To avoid key conflicts, a combinaison must be made with FOCUS_EXTRA then an other key
+        """
+        if key == a_key['FOCUS_EXTRA']:
+            self._focus_extra = True
+            return
+        if self._focus_extra:
+            self._focus_extra = False
+            if key in ('m', '1'):
+                focus = 'menu'
+            elif key in ('b', '2'):
+                focus = 'body'
+            elif key in ('n', '3'):
+                focus = 'notif_bar'
+            elif key in ('e', '4'):
+                focus = 'edit_bar'
+            else:
+                return super(PrimitivusTopWidget, self).keypress(size, key)
+
+            if focus in self._hidden:
+                return
+
+            self.focus_position = self.getVisiblePositions().index(focus)
+            return
+
+        return super(PrimitivusTopWidget, self).keypress(size, key)
+
+    def widgetGet(self,  position):
+        if not position in self.positions:
+            raise ValueError("Unknown position {}".format(position))
+        return getattr(self, "_{}".format(position))
+
+    def widgetSet(self,  widget, position):
+        if not position in self.positions:
+            raise ValueError("Unknown position {}".format(position))
+        return setattr(self, "_{}".format(position), widget)
+
+    def hideSwitch(self, position):
+        if not position in self.can_hide:
+            raise ValueError("Can't switch position {}".format(position))
+        hide = not position in self._hidden
+        widget = self.widgetGet(position)
+        idx = self.getVisiblePositions(position).index(position)
+        if hide:
+            del self.contents[idx]
+            self._hidden.add(position)
+        else:
+            self.contents.insert(idx, (widget, ('pack', None)))
+            self._hidden.remove(position)
+
+    def show(self, position):
+        if position in self._hidden:
+            self.hideSwitch(position)
+
+    def hide(self, position):
+        if not position in self._hidden:
+            self.hideSwitch(position)
+
+
+class PrimitivusApp(QuickApp, InputHistory):
+    MB_HANDLER = False
+    AVATARS_HANDLER = False
+
+    def __init__(self):
+        bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
+        if bridge_module is None:
+            log.error(u"Can't import {} bridge".format(bridge_name))
+            sys.exit(3)
+        else:
+            log.debug(u"Loading {} bridge".format(bridge_name))
+        QuickApp.__init__(self, bridge_factory=bridge_module.Bridge, xmlui=xmlui, check_options=quick_utils.check_options, connect_bridge=False)
+        ## main loop setup ##
+        event_loop = urwid.GLibEventLoop if 'dbus' in bridge_name else urwid.TwistedEventLoop
+        self.loop = urwid.MainLoop(urwid.SolidFill(), C.PALETTE, event_loop=event_loop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler)
+
+    @classmethod
+    def run(cls):
+        cls().start()
+
+    def onBridgeConnected(self):
+
+        ##misc setup##
+        self._visible_widgets = set()
+        self.notif_bar = sat_widgets.NotificationBar()
+        urwid.connect_signal(self.notif_bar, 'change', self.onNotification)
+
+        self.progress_wid = self.widgets.getOrCreateWidget(Progress, None, on_new_widget=None)
+        urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.selectWidget(self.progress_wid))
+        self.__saved_overlay = None
+
+        self.x_notify = Notify()
+
+        # we already manage exit with a_key['APP_QUIT'], so we don't want C-c
+        signal.signal(signal.SIGINT, signal.SIG_IGN)
+        sat_conf = sat_config.parseMainConf()
+        self._bracketed_paste = C.bool(sat_config.getConfig(sat_conf, C.SECTION_NAME, 'bracketed_paste', 'false'))
+        if self._bracketed_paste:
+            log.debug("setting bracketed paste mode as requested")
+            sys.stdout.write("\033[?2004h")
+            self._bracketed_mode_set = True
+
+        self.loop.widget = self.main_widget = ProfileManager(self)
+        self.postInit()
+
+    @property
+    def visible_widgets(self):
+        return self._visible_widgets
+
+    @property
+    def mode(self):
+        return self.editBar.mode
+
+    @mode.setter
+    def mode(self, value):
+        self.editBar.mode = value
+
+    def modeHint(self, value):
+        """Change mode if make sens (i.e.: if there is nothing in the editBar)"""
+        if not self.editBar.get_edit_text():
+            self.mode = value
+
+    def debug(self):
+        """convenient method to reset screen and launch (i)p(u)db"""
+        log.info('Entered debug mode')
+        try:
+            import pudb
+            pudb.set_trace()
+        except ImportError:
+            import os
+            os.system('reset')
+            try:
+                import ipdb
+                ipdb.set_trace()
+            except ImportError:
+                import pdb
+                pdb.set_trace()
+
+    def redraw(self):
+        """redraw the screen"""
+        try:
+            self.loop.draw_screen()
+        except AttributeError:
+            pass
+
+    def start(self):
+        self.connectBridge()
+        self.loop.run()
+
+    def postInit(self):
+        try:
+            config.applyConfig(self)
+        except Exception as e:
+            log.error(u"configuration error: {}".format(e))
+            popup = self.alert(_(u"Configuration Error"), _(u"Something went wrong while reading the configuration, please check :messages"))
+            if self.options.profile:
+                self._early_popup = popup
+            else:
+                self.showPopUp(popup)
+        super(PrimitivusApp, self).postInit(self.main_widget)
+
+    def keysToText(self, keys):
+        """Generator return normal text from urwid keys"""
+        for k in keys:
+            if k == 'tab':
+                yield u'\t'
+            elif k == 'enter':
+                yield u'\n'
+            elif is_wide_char(k,0) or (len(k)==1 and ord(k) >= 32):
+                yield k
+
+    def inputFilter(self, input_, raw):
+        if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']:
+            return
+
+        ## paste detection/handling
+        if (len(input_) > 1 and                  # XXX: it may be needed to increase this value if buffer
+            not isinstance(input_[0], tuple) and #      or other things result in several chars at once
+            not 'window resize' in input_):      #      (e.g. using Primitivus through ssh). Need some testing
+                                                 #      and experience to adjust value.
+            if input_[0] == 'begin paste' and not self._bracketed_paste:
+                log.info(u"Bracketed paste mode detected")
+                self._bracketed_paste = True
+
+            if self._bracketed_paste:
+                # after this block, extra will contain non pasted keys
+                # and input_ will contain pasted keys
+                try:
+                    begin_idx = input_.index('begin paste')
+                except ValueError:
+                    # this is not a paste, maybe we have something buffering
+                    # or bracketed mode is set in conf but not enabled in term
+                    extra = input_
+                    input_ = []
+                else:
+                    try:
+                        end_idx = input_.index('end paste')
+                    except ValueError:
+                        log.warning(u"missing end paste sequence, discarding paste")
+                        extra = input_[:begin_idx]
+                        del input_[begin_idx:]
+                    else:
+                        extra = input_[:begin_idx] + input_[end_idx+1:]
+                        input_ = input_[begin_idx+1:end_idx]
+            else:
+                extra = None
+
+            log.debug(u"Paste detected (len {})".format(len(input_)))
+            try:
+                edit_bar = self.editBar
+            except AttributeError:
+                log.warning(u"Paste treated as normal text: there is no edit bar yet")
+                if extra is None:
+                    extra = []
+                extra.extend(input_)
+            else:
+                if self.main_widget.focus == edit_bar:
+                    # XXX: if a paste is detected, we append it directly to the edit bar text
+                    #      so the user can check it and press [enter] if it's OK
+                    buf_paste = u''.join(self.keysToText(input_))
+                    pos = edit_bar.edit_pos
+                    edit_bar.set_edit_text(u'{}{}{}'.format(edit_bar.edit_text[:pos], buf_paste, edit_bar.edit_text[pos:]))
+                    edit_bar.edit_pos+=len(buf_paste)
+                else:
+                    # we are not on the edit_bar,
+                    # so we treat pasted text as normal text
+                    if extra is None:
+                        extra = []
+                    extra.extend(input_)
+            if not extra:
+                return
+            input_ = extra
+        ## end of paste detection/handling
+
+        for i in input_:
+            if isinstance(i,tuple):
+                if i[0] == 'mouse press':
+                    if i[1] == 4: #Mouse wheel up
+                        input_[input_.index(i)] = a_key['HISTORY_PREV']
+                    if i[1] == 5: #Mouse wheel down
+                        input_[input_.index(i)] = a_key['HISTORY_NEXT']
+        return input_
+
+    def keyHandler(self, input_):
+        if input_ == a_key['MENU_HIDE']:
+            """User want to (un)hide the menu roller"""
+            try:
+                self.main_widget.hideSwitch('menu')
+            except AttributeError:
+                pass
+        elif input_ == a_key['NOTIFICATION_NEXT']:
+            """User wants to see next notification"""
+            self.notif_bar.showNext()
+        elif input_ == a_key['OVERLAY_HIDE']:
+            """User wants to (un)hide overlay window"""
+            if isinstance(self.loop.widget,urwid.Overlay):
+                self.__saved_overlay = self.loop.widget
+                self.loop.widget = self.main_widget
+            else:
+                if self.__saved_overlay:
+                    self.loop.widget = self.__saved_overlay
+                    self.__saved_overlay = None
+
+        elif input_ == a_key['DEBUG'] and 'D' in self.bridge.getVersion(): #Debug only for dev versions
+            self.debug()
+        elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists
+            try:
+                for wid, options in self.center_part.contents:
+                    if self.contact_lists_pile is wid:
+                        self.center_part.contents.remove((wid, options))
+                        break
+                else:
+                    self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False)))
+            except AttributeError:
+                #The main widget is not built (probably in Profile Manager)
+                pass
+        elif input_ == 'window resize':
+            width,height = self.loop.screen_size
+            if height<=5 and width<=35:
+                if not 'save_main_widget' in dir(self):
+                    self.save_main_widget = self.loop.widget
+                    self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !")))
+            else:
+                if 'save_main_widget' in dir(self):
+                    self.loop.widget = self.save_main_widget
+                    del self.save_main_widget
+        try:
+            return self.menu_roller.checkShortcuts(input_)
+        except AttributeError:
+            return input_
+
+    def addMenus(self, menu, type_filter, menu_data=None):
+        """Add cached menus to instance
+        @param menu: sat_widgets.Menu instance
+        @param type_filter: menu type like is sat.core.sat_main.importMenu
+        @param menu_data: data to send with these menus
+
+        """
+        def add_menu_cb(callback_id):
+            self.launchAction(callback_id, menu_data, profile=self.current_profile)
+        for id_, type_, path, path_i18n, extra  in self.bridge.menusGet("", C.NO_SECURITY_LIMIT ): # TODO: manage extra
+            if type_ != type_filter:
+                continue
+            if len(path) != 2:
+                raise NotImplementedError("Menu with a path != 2 are not implemented yet")
+            menu.addMenu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
+
+
+    def _buildMenuRoller(self):
+        menu = sat_widgets.Menu(self.loop)
+        general = _("General")
+        menu.addMenu(general, _("Connect"), self.onConnectRequest)
+        menu.addMenu(general, _("Disconnect"), self.onDisconnectRequest)
+        menu.addMenu(general, _("Parameters"), self.onParam)
+        menu.addMenu(general, _("About"), self.onAboutRequest)
+        menu.addMenu(general, _("Exit"), self.onExitRequest, a_key['APP_QUIT'])
+        menu.addMenu(_("Contacts"))  # add empty menu to save the place in the menu order
+        groups = _("Groups")
+        menu.addMenu(groups)
+        menu.addMenu(groups, _("Join room"), self.onJoinRoomRequest, a_key['ROOM_JOIN'])
+        #additionals menus
+        #FIXME: do this in a more generic way (in quickapp)
+        self.addMenus(menu, C.MENU_GLOBAL)
+
+        menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
+        return menu_roller
+
+    def _buildMainWidget(self):
+        self.contact_lists_pile = urwid.Pile([])
+        #self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))])
+        self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))])
+
+        self.editBar = EditBar(self)
+        self.menu_roller = self._buildMenuRoller()
+        self.main_widget = PrimitivusTopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar)
+        return self.main_widget
+
+    def plugging_profiles(self):
+        self.loop.widget = self._buildMainWidget()
+        self.redraw()
+        try:
+            # if a popup arrived before main widget is build, we need to show it now
+            self.showPopUp(self._early_popup)
+        except AttributeError:
+            pass
+        else:
+            del self._early_popup
+
+    def profilePlugged(self, profile):
+        QuickApp.profilePlugged(self, profile)
+        contact_list = self.widgets.getOrCreateWidget(ContactList, None, on_new_widget=None, on_click=self.contactSelected, on_change=lambda w: self.redraw(), profile=profile)
+        self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
+        return contact_list
+
+    def isHidden(self):
+        """Tells if the frontend window is hidden.
+
+        @return bool
+        """
+        return False  # FIXME: implement when necessary
+
+    def alert(self, title, message):
+        """Shortcut method to create an alert message
+
+        Alert will have an "OK" button, which remove it if pressed
+        @param title(unicode): title of the dialog
+        @param message(unicode): body of the dialog
+        @return (urwid_satext.Alert): the created Alert instance
+        """
+        popup = sat_widgets.Alert(title, message)
+        popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
+        self.showPopUp(popup, width=75, height=20)
+        return popup
+
+    def removePopUp(self, widget=None):
+        """Remove current pop-up, and if there is other in queue, show it
+
+        @param widget(None, urwid.Widget): if not None remove this popup from front or queue
+        """
+        # TODO: refactor popup management in a cleaner way
+        # buttons' callback use themselve as first argument, and we never use
+        # a Button directly in a popup, so we consider urwid.Button as None
+        if widget is not None and not isinstance(widget, urwid.Button):
+            if isinstance(self.loop.widget, urwid.Overlay):
+                current_popup = self.loop.widget.top_w
+                if not current_popup == widget:
+                    try:
+                        self.notif_bar.removePopUp(widget)
+                    except ValueError:
+                        log.warning(u"Trying to remove an unknown widget {}".format(widget))
+                    return
+        self.loop.widget = self.main_widget
+        next_popup = self.notif_bar.getNextPopup()
+        if next_popup:
+            #we still have popup to show, we display it
+            self.showPopUp(next_popup)
+        else:
+            self.redraw()
+
+    def showPopUp(self, pop_up_widget, width=None, height=None, align='center',
+                  valign='middle'):
+        """Show a pop-up window if possible, else put it in queue
+
+        @param pop_up_widget: pop up to show
+        @param width(int, None): width of the popup
+            None to use default
+        @param height(int, None): height of the popup
+            None to use default
+        @param align: same as for [urwid.Overlay]
+        """
+        if width == None:
+            width = 75 if isinstance(pop_up_widget, xmlui.PrimitivusNoteDialog) else 135
+        if height == None:
+            height = 20 if isinstance(pop_up_widget, xmlui.PrimitivusNoteDialog) else 40
+        if not isinstance(self.loop.widget, urwid.Overlay):
+            display_widget = urwid.Overlay(
+                pop_up_widget, self.main_widget, align, width, valign, height)
+            self.loop.widget = display_widget
+            self.redraw()
+        else:
+            self.notif_bar.addPopUp(pop_up_widget)
+
+    def barNotify(self, message):
+        """"Notify message to user via notification bar"""
+        self.notif_bar.addMessage(message)
+        self.redraw()
+
+    def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE):
+        if widget is None or widget is not None and widget != self.selected_widget:
+            # we ignore notification if the widget is selected but we can
+            # still do a desktop notification is the X window has not the focus
+            super(PrimitivusApp, self).notify(type_, entity, message, subject, callback, cb_args, widget, profile)
+        # we don't want notifications without message on desktop
+        if message is not None and not self.x_notify.hasFocus():
+            if message is None:
+                message = _("{app}: a new event has just happened{entity}").format(
+                    app=C.APP_NAME,
+                    entity=u' ({})'.format(entity) if entity else '')
+            self.x_notify.sendNotification(message)
+
+
+    def newWidget(self, widget, user_action=False):
+        """Method called when a new widget is created
+
+        if suitable, the widget will be displayed
+        @param widget(widget.PrimitivusWidget): created widget
+        @param user_action(bool): if True, the widget has been created following an
+            explicit user action. In this case, the widget may get focus immediately
+        """
+        # FIXME: when several widgets are possible (e.g. with :split)
+        #        do not replace current widget when self.selected_widget != None
+        if user_action or self.selected_widget is None:
+            self.selectWidget(widget)
+
+    def selectWidget(self, widget):
+        """Display a widget if possible,
+
+        else add it in the notification bar queue
+        @param widget: BoxWidget
+        """
+        assert len(self.center_part.widget_list)<=2
+        wid_idx = len(self.center_part.widget_list)-1
+        self.center_part.widget_list[wid_idx] = widget
+        try:
+            self.menu_roller.removeMenu(C.MENU_ID_WIDGET)
+        except KeyError:
+            log.debug("No menu to delete")
+        self.selected_widget = widget
+        try:
+            onSelected = self.selected_widget.onSelected
+        except AttributeError:
+            pass
+        else:
+            onSelected()
+        self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
+        self.contact_lists.select(None)
+
+        for wid in self.visible_widgets: # FIXME: check if widgets.getWidgets is not more appropriate
+            if isinstance(wid, Chat):
+                contact_list = self.contact_lists[wid.profile]
+                contact_list.select(wid.target)
+
+        self.redraw()
+
+    def removeWindow(self):
+        """Remove window showed on the right column"""
+        #TODO: better Window management than this hack
+        assert len(self.center_part.widget_list) <= 2
+        wid_idx = len(self.center_part.widget_list)-1
+        self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text(''))
+        self.center_part.focus_position = 0
+        self.redraw()
+
+    def addProgress(self, pid, message, profile):
+        """Follow a SàT progression
+
+        @param pid: progression id
+        @param message: message to show to identify the progression
+        """
+        self.progress_wid.add(pid, message, profile)
+
+    def setProgress(self, percentage):
+        """Set the progression shown in notification bar"""
+        self.notif_bar.setProgress(percentage)
+
+    def contactSelected(self, contact_list, entity):
+        self.clearNotifs(entity, profile=contact_list.profile)
+        if entity.resource:
+            # we have clicked on a private MUC conversation
+            chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, force_hash = Chat.getPrivateHash(contact_list.profile, entity), profile=contact_list.profile)
+        else:
+            chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
+        self.selectWidget(chat_widget)
+        self.menu_roller.addMenu(_('Chat menu'), chat_widget.getMenu(), C.MENU_ID_WIDGET)
+
+    def _dialogOkCb(self, widget, data):
+        popup, answer_cb, answer_data = data
+        self.removePopUp(popup)
+        if answer_cb is not None:
+            answer_cb(True, answer_data)
+
+    def _dialogCancelCb(self, widget, data):
+        popup, answer_cb, answer_data = data
+        self.removePopUp(popup)
+        if answer_cb is not None:
+            answer_cb(False, answer_data)
+
+    def showDialog(self, message, title="", type="info", answer_cb = None, answer_data = None):
+        if type == 'info':
+            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
+            if answer_cb is None:
+                popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
+        elif type == 'error':
+            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
+            if answer_cb is None:
+                popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
+        elif type == 'yes/no':
+            popup = sat_widgets.ConfirmDialog(message)
+            popup.setCallback('yes', self._dialogOkCb, (popup, answer_cb, answer_data))
+            popup.setCallback('no', self._dialogCancelCb, (popup, answer_cb, answer_data))
+        else:
+            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
+            if answer_cb is None:
+                popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
+            log.error(u'unmanaged dialog type: {}'.format(type))
+        self.showPopUp(popup)
+
+    def dialogFailure(self, failure):
+        """Show a failure that has been returned by an asynchronous bridge method.
+
+        @param failure (defer.Failure): Failure instance
+        """
+        self.alert(failure.classname, failure.message)
+
+    def onNotification(self, notif_bar):
+        """Called when a new notification has been received"""
+        if not isinstance(self.main_widget, PrimitivusTopWidget):
+            #if we are not in the main configuration, we ignore the notifications bar
+            return
+        if self.notif_bar.canHide():
+                #No notification left, we can hide the bar
+                self.main_widget.hide('notif_bar')
+        else:
+            self.main_widget.show('notif_bar')
+            self.redraw() # FIXME: invalidate cache in a more efficient way
+
+    def _actionManagerUnknownError(self):
+        self.alert(_("Error"), _(u"Unmanaged action"))
+
+    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, subject, profile):
+        super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, subject, profile)
+        # if self.selected_widget is None:
+        #     for contact_list in self.widgets.getWidgets(ContactList):
+        #         if profile in contact_list.profiles:
+        #             contact_list.setFocus(jid.JID(room_jid_s), True)
+
+    def progressStartedHandler(self, pid, metadata, profile):
+        super(PrimitivusApp, self).progressStartedHandler(pid, metadata, profile)
+        self.addProgress(pid, metadata.get('name', _(u'unkown')), profile)
+
+    def progressFinishedHandler(self, pid, metadata, profile):
+        log.info(u"Progress {} finished".format(pid))
+        super(PrimitivusApp, self).progressFinishedHandler(pid, metadata, profile)
+
+    def progressErrorHandler(self, pid, err_msg, profile):
+        log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
+        super(PrimitivusApp, self).progressErrorHandler(pid, err_msg, profile)
+
+
+    ##DIALOGS CALLBACKS##
+    def onJoinRoom(self, button, edit):
+        self.removePopUp()
+        room_jid = jid.JID(edit.get_edit_text())
+        self.bridge.mucJoin(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialogFailure)
+
+    #MENU EVENTS#
+    def onConnectRequest(self, menu):
+        QuickApp.connect(self, self.current_profile)
+
+    def onDisconnectRequest(self, menu):
+        self.disconnect(self.current_profile)
+
+    def onParam(self, menu):
+        def success(params):
+            ui = xmlui.create(self, xml_data=params, profile=self.current_profile)
+            ui.show()
+
+        def failure(error):
+            self.alert(_("Error"), _("Can't get parameters (%s)") % error)
+        self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
+
+    def onExitRequest(self, menu):
+        QuickApp.onExit(self)
+        try:
+            if self._bracketed_mode_set: # we don't unset if bracketed paste mode was detected automatically (i.e. not in conf)
+                log.debug("unsetting bracketed paste mode")
+                sys.stdout.write("\033[?2004l")
+        except AttributeError:
+            pass
+        raise urwid.ExitMainLoop()
+
+    def onJoinRoomRequest(self, menu):
+        """User wants to join a MUC room"""
+        pop_up_widget = sat_widgets.InputDialog(_("Entering a MUC room"), _("Please enter MUC's JID"), default_txt=self.bridge.mucGetDefaultService(), ok_cb=self.onJoinRoom)
+        pop_up_widget.setCallback('cancel', lambda dummy: self.removePopUp(pop_up_widget))
+        self.showPopUp(pop_up_widget)
+
+    def onAboutRequest(self, menu):
+        self.alert(_("About"), C.APP_NAME + " v" + self.bridge.getVersion())
+
+    #MISC CALLBACKS#
+
+    def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE):
+        contact_list_wid = self.widgets.getWidget(ContactList, profiles=profile)
+        if contact_list_wid is not None:
+            contact_list_wid.status_bar.setPresenceStatus(show, status)
+        else:
+            log.warning(u"No ContactList widget found for profile {}".format(profile))
+
+if __name__ == '__main__':
+    PrimitivusApp().start()
--- a/sat_frontends/primitivus/primitivus	Sat Mar 20 20:42:04 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,858 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-from sat.core.i18n import _, D_
-from sat_frontends.primitivus.constants import Const as C
-from sat.core import log_config
-log_config.satConfigure(C.LOG_BACKEND_STANDARD, C)
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.tools import config as sat_config
-import urwid
-from urwid.util import is_wide_char
-from urwid_satext import sat_widgets
-from sat_frontends.quick_frontend.quick_app import QuickApp
-from sat_frontends.quick_frontend import quick_utils
-from sat_frontends.quick_frontend import quick_chat
-from sat_frontends.primitivus.profile_manager import ProfileManager
-from sat_frontends.primitivus.contact_list import ContactList
-from sat_frontends.primitivus.chat import Chat
-from sat_frontends.primitivus import xmlui
-from sat_frontends.primitivus.progress import Progress
-from sat_frontends.primitivus.notify import Notify
-from sat_frontends.primitivus.keys import action_key_map as a_key
-from sat_frontends.primitivus import config
-from sat_frontends.tools.misc import InputHistory
-from sat.tools.common import dynamic_import
-from sat_frontends.tools import jid
-import signal
-import sys
-## bridge handling
-# we get bridge name from conf and initialise the right class accordingly
-main_config = sat_config.parseMainConf()
-bridge_name = sat_config.getConfig(main_config, '', 'bridge', 'dbus')
-if 'dbus' not in bridge_name:
-    print(u"only D-Bus bridge is currently supported")
-    sys.exit(3)
-
-
-class EditBar(sat_widgets.ModalEdit):
-    """
-    The modal edit bar where you would enter messages and commands.
-    """
-
-    def __init__(self, host):
-        modes = {None: (C.MODE_NORMAL, u''),
-                 a_key['MODE_INSERTION']: (C.MODE_INSERTION, u'> '),
-                 a_key['MODE_COMMAND']: (C.MODE_COMMAND, u':')} #XXX: captions *MUST* be unicode
-        super(EditBar, self).__init__(modes)
-        self.host = host
-        self.setCompletionMethod(self._text_completion)
-        urwid.connect_signal(self, 'click', self.onTextEntered)
-
-    def _text_completion(self, text, completion_data, mode):
-        if mode == C.MODE_INSERTION:
-            if self.host.selected_widget is not None:
-                try:
-                    completion = self.host.selected_widget.completion
-                except AttributeError:
-                    return text
-                else:
-                    return completion(text, completion_data)
-        else:
-            return text
-
-    def onTextEntered(self, editBar):
-        """Called when text is entered in the main edit bar"""
-        if self.mode == C.MODE_INSERTION:
-            if isinstance(self.host.selected_widget, quick_chat.QuickChat):
-                chat_widget = self.host.selected_widget
-                self.host.messageSend(
-                    chat_widget.target,
-                    {'': editBar.get_edit_text()}, # TODO: handle language
-                    mess_type = C.MESS_TYPE_GROUPCHAT if chat_widget.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
-                    errback=lambda failure: self.host.showDialog(_("Error while sending message ({})").format(failure), type="error"),
-                    profile_key=chat_widget.profile
-                    )
-                editBar.set_edit_text('')
-        elif self.mode == C.MODE_COMMAND:
-            self.commandHandler()
-
-    def commandHandler(self):
-        #TODO: separate class with auto documentation (with introspection)
-        #      and completion method
-        tokens = self.get_edit_text().split(' ')
-        command, args = tokens[0], tokens[1:]
-        if command == 'quit':
-            self.host.onExit()
-            raise urwid.ExitMainLoop()
-        elif command == 'messages':
-            wid = sat_widgets.GenericList(logging.memoryGet())
-            self.host.selectWidget(wid)
-        # FIXME: reactivate the command
-        # elif command == 'presence':
-        #     values = [value for value in commonConst.PRESENCE.keys()]
-        #     values = [value if value else 'online' for value in values]  # the empty value actually means 'online'
-        #     if args and args[0] in values:
-        #         presence = '' if args[0] == 'online' else args[0]
-        #         self.host.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
-        #     else:
-        #         self.host.status_bar.onPresenceClick()
-        # elif command == 'status':
-        #     if args:
-        #         self.host.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
-        #     else:
-        #         self.host.status_bar.onStatusClick()
-        elif command == 'history':
-            widget = self.host.selected_widget
-            if isinstance(widget, quick_chat.QuickChat):
-                try:
-                    limit = int(args[0])
-                except (IndexError, ValueError):
-                    limit = 50
-                widget.updateHistory(size=limit, profile=widget.profile)
-        elif command == 'search':
-            widget = self.host.selected_widget
-            if isinstance(widget, quick_chat.QuickChat):
-                pattern = " ".join(args)
-                if not pattern:
-                    self.host.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
-                else:
-                    widget.updateHistory(size=C.HISTORY_LIMIT_NONE, filters={'search': pattern}, profile=widget.profile)
-        elif command == 'filter':
-            # FIXME: filter is now only for current widget,
-            #        need to be able to set it globally or per widget
-            widget = self.host.selected_widget
-            # FIXME: Q&D way, need to be more generic
-            if isinstance(widget, quick_chat.QuickChat):
-                widget.setFilter(args)
-        elif command in ('topic', 'suject', 'title'):
-            try:
-                new_title = args[0].strip()
-            except IndexError:
-                new_title = None
-            widget = self.host.selected_widget
-            if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP:
-                widget.onSubjectDialog(new_title)
-        else:
-            return
-        self.set_edit_text('')
-
-    def _historyCb(self, text):
-        self.set_edit_text(text)
-        self.set_edit_pos(len(text))
-
-    def keypress(self, size, key):
-        """Callback when a key is pressed. Send "composing" states
-        and move the index of the temporary history stack."""
-        if key == a_key['MODAL_ESCAPE']:
-            # first save the text to the current mode, then change to NORMAL
-            self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
-            self.host._updateInputHistory(mode=C.MODE_NORMAL)
-        if self._mode == C.MODE_NORMAL and key in self._modes:
-            self.host._updateInputHistory(mode=self._modes[key][0])
-        if key == a_key['HISTORY_PREV']:
-            self.host._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
-            return
-        elif key == a_key['HISTORY_NEXT']:
-            self.host._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
-            return
-        elif key == a_key['EDIT_ENTER']:
-            self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
-        else:
-            if (self._mode == C.MODE_INSERTION
-                and isinstance(self.host.selected_widget, quick_chat.QuickChat)
-                and key not in sat_widgets.FOCUS_KEYS
-                and key not in (a_key['HISTORY_PREV'], a_key['HISTORY_NEXT'])
-                and self.host.sync):
-                self.host.bridge.chatStateComposing(self.host.selected_widget.target, self.host.selected_widget.profile)
-
-        return super(EditBar, self).keypress(size, key)
-
-
-class PrimitivusTopWidget(sat_widgets.FocusPile):
-    """Top most widget used in Primitivus"""
-    _focus_inversed = True
-    positions = ('menu', 'body', 'notif_bar', 'edit_bar')
-    can_hide = ('menu', 'notif_bar')
-
-    def __init__(self, body, menu, notif_bar, edit_bar):
-        self._body = body
-        self._menu = menu
-        self._notif_bar = notif_bar
-        self._edit_bar = edit_bar
-        self._hidden = {'notif_bar'}
-        self._focus_extra = False
-        super(PrimitivusTopWidget, self).__init__([('pack', self._menu), self._body, ('pack', self._edit_bar)])
-        for position in self.positions:
-            setattr(self,
-                    position,
-                    property(lambda: self, self.widgetGet(position=position),
-                             lambda pos, new_wid: self.widgetSet(new_wid, position=pos))
-                   )
-        self.focus_position = len(self.contents)-1
-
-    def getVisiblePositions(self, keep=None):
-        """Return positions that are not hidden in the right order
-
-        @param keep: if not None, this position will be keep in the right order, even if it's hidden
-                    (can be useful to find its index)
-        @return (list): list of visible positions
-        """
-        return [pos for pos in self.positions if (keep and pos == keep) or pos not in self._hidden]
-
-    def keypress(self, size, key):
-        """Manage FOCUS keys that focus directly a main part (one of self.positions)
-
-        To avoid key conflicts, a combinaison must be made with FOCUS_EXTRA then an other key
-        """
-        if key == a_key['FOCUS_EXTRA']:
-            self._focus_extra = True
-            return
-        if self._focus_extra:
-            self._focus_extra = False
-            if key in ('m', '1'):
-                focus = 'menu'
-            elif key in ('b', '2'):
-                focus = 'body'
-            elif key in ('n', '3'):
-                focus = 'notif_bar'
-            elif key in ('e', '4'):
-                focus = 'edit_bar'
-            else:
-                return super(PrimitivusTopWidget, self).keypress(size, key)
-
-            if focus in self._hidden:
-                return
-
-            self.focus_position = self.getVisiblePositions().index(focus)
-            return
-
-        return super(PrimitivusTopWidget, self).keypress(size, key)
-
-    def widgetGet(self,  position):
-        if not position in self.positions:
-            raise ValueError("Unknown position {}".format(position))
-        return getattr(self, "_{}".format(position))
-
-    def widgetSet(self,  widget, position):
-        if not position in self.positions:
-            raise ValueError("Unknown position {}".format(position))
-        return setattr(self, "_{}".format(position), widget)
-
-    def hideSwitch(self, position):
-        if not position in self.can_hide:
-            raise ValueError("Can't switch position {}".format(position))
-        hide = not position in self._hidden
-        widget = self.widgetGet(position)
-        idx = self.getVisiblePositions(position).index(position)
-        if hide:
-            del self.contents[idx]
-            self._hidden.add(position)
-        else:
-            self.contents.insert(idx, (widget, ('pack', None)))
-            self._hidden.remove(position)
-
-    def show(self, position):
-        if position in self._hidden:
-            self.hideSwitch(position)
-
-    def hide(self, position):
-        if not position in self._hidden:
-            self.hideSwitch(position)
-
-
-class PrimitivusApp(QuickApp, InputHistory):
-    MB_HANDLER = False
-    AVATARS_HANDLER = False
-
-    def __init__(self):
-        bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
-        if bridge_module is None:
-            log.error(u"Can't import {} bridge".format(bridge_name))
-            sys.exit(3)
-        else:
-            log.debug(u"Loading {} bridge".format(bridge_name))
-        QuickApp.__init__(self, bridge_factory=bridge_module.Bridge, xmlui=xmlui, check_options=quick_utils.check_options, connect_bridge=False)
-        ## main loop setup ##
-        event_loop = urwid.GLibEventLoop if 'dbus' in bridge_name else urwid.TwistedEventLoop
-        self.loop = urwid.MainLoop(urwid.SolidFill(), C.PALETTE, event_loop=event_loop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler)
-
-    def onBridgeConnected(self):
-
-        ##misc setup##
-        self._visible_widgets = set()
-        self.notif_bar = sat_widgets.NotificationBar()
-        urwid.connect_signal(self.notif_bar, 'change', self.onNotification)
-
-        self.progress_wid = self.widgets.getOrCreateWidget(Progress, None, on_new_widget=None)
-        urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.selectWidget(self.progress_wid))
-        self.__saved_overlay = None
-
-        self.x_notify = Notify()
-
-        # we already manage exit with a_key['APP_QUIT'], so we don't want C-c
-        signal.signal(signal.SIGINT, signal.SIG_IGN)
-        sat_conf = sat_config.parseMainConf()
-        self._bracketed_paste = C.bool(sat_config.getConfig(sat_conf, C.SECTION_NAME, 'bracketed_paste', 'false'))
-        if self._bracketed_paste:
-            log.debug("setting bracketed paste mode as requested")
-            sys.stdout.write("\033[?2004h")
-            self._bracketed_mode_set = True
-
-        self.loop.widget = self.main_widget = ProfileManager(self)
-        self.postInit()
-
-    @property
-    def visible_widgets(self):
-        return self._visible_widgets
-
-    @property
-    def mode(self):
-        return self.editBar.mode
-
-    @mode.setter
-    def mode(self, value):
-        self.editBar.mode = value
-
-    def modeHint(self, value):
-        """Change mode if make sens (i.e.: if there is nothing in the editBar)"""
-        if not self.editBar.get_edit_text():
-            self.mode = value
-
-    def debug(self):
-        """convenient method to reset screen and launch (i)p(u)db"""
-        log.info('Entered debug mode')
-        try:
-            import pudb
-            pudb.set_trace()
-        except ImportError:
-            import os
-            os.system('reset')
-            try:
-                import ipdb
-                ipdb.set_trace()
-            except ImportError:
-                import pdb
-                pdb.set_trace()
-
-    def redraw(self):
-        """redraw the screen"""
-        try:
-            self.loop.draw_screen()
-        except AttributeError:
-            pass
-
-    def start(self):
-        self.connectBridge()
-        self.loop.run()
-
-    def postInit(self):
-        try:
-            config.applyConfig(self)
-        except Exception as e:
-            log.error(u"configuration error: {}".format(e))
-            popup = self.alert(_(u"Configuration Error"), _(u"Something went wrong while reading the configuration, please check :messages"))
-            if self.options.profile:
-                self._early_popup = popup
-            else:
-                self.showPopUp(popup)
-        super(PrimitivusApp, self).postInit(self.main_widget)
-
-    def keysToText(self, keys):
-        """Generator return normal text from urwid keys"""
-        for k in keys:
-            if k == 'tab':
-                yield u'\t'
-            elif k == 'enter':
-                yield u'\n'
-            elif is_wide_char(k,0) or (len(k)==1 and ord(k) >= 32):
-                yield k
-
-    def inputFilter(self, input_, raw):
-        if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']:
-            return
-
-        ## paste detection/handling
-        if (len(input_) > 1 and                  # XXX: it may be needed to increase this value if buffer
-            not isinstance(input_[0], tuple) and #      or other things result in several chars at once
-            not 'window resize' in input_):      #      (e.g. using Primitivus through ssh). Need some testing
-                                                 #      and experience to adjust value.
-            if input_[0] == 'begin paste' and not self._bracketed_paste:
-                log.info(u"Bracketed paste mode detected")
-                self._bracketed_paste = True
-
-            if self._bracketed_paste:
-                # after this block, extra will contain non pasted keys
-                # and input_ will contain pasted keys
-                try:
-                    begin_idx = input_.index('begin paste')
-                except ValueError:
-                    # this is not a paste, maybe we have something buffering
-                    # or bracketed mode is set in conf but not enabled in term
-                    extra = input_
-                    input_ = []
-                else:
-                    try:
-                        end_idx = input_.index('end paste')
-                    except ValueError:
-                        log.warning(u"missing end paste sequence, discarding paste")
-                        extra = input_[:begin_idx]
-                        del input_[begin_idx:]
-                    else:
-                        extra = input_[:begin_idx] + input_[end_idx+1:]
-                        input_ = input_[begin_idx+1:end_idx]
-            else:
-                extra = None
-
-            log.debug(u"Paste detected (len {})".format(len(input_)))
-            try:
-                edit_bar = self.editBar
-            except AttributeError:
-                log.warning(u"Paste treated as normal text: there is no edit bar yet")
-                if extra is None:
-                    extra = []
-                extra.extend(input_)
-            else:
-                if self.main_widget.focus == edit_bar:
-                    # XXX: if a paste is detected, we append it directly to the edit bar text
-                    #      so the user can check it and press [enter] if it's OK
-                    buf_paste = u''.join(self.keysToText(input_))
-                    pos = edit_bar.edit_pos
-                    edit_bar.set_edit_text(u'{}{}{}'.format(edit_bar.edit_text[:pos], buf_paste, edit_bar.edit_text[pos:]))
-                    edit_bar.edit_pos+=len(buf_paste)
-                else:
-                    # we are not on the edit_bar,
-                    # so we treat pasted text as normal text
-                    if extra is None:
-                        extra = []
-                    extra.extend(input_)
-            if not extra:
-                return
-            input_ = extra
-        ## end of paste detection/handling
-
-        for i in input_:
-            if isinstance(i,tuple):
-                if i[0] == 'mouse press':
-                    if i[1] == 4: #Mouse wheel up
-                        input_[input_.index(i)] = a_key['HISTORY_PREV']
-                    if i[1] == 5: #Mouse wheel down
-                        input_[input_.index(i)] = a_key['HISTORY_NEXT']
-        return input_
-
-    def keyHandler(self, input_):
-        if input_ == a_key['MENU_HIDE']:
-            """User want to (un)hide the menu roller"""
-            try:
-                self.main_widget.hideSwitch('menu')
-            except AttributeError:
-                pass
-        elif input_ == a_key['NOTIFICATION_NEXT']:
-            """User wants to see next notification"""
-            self.notif_bar.showNext()
-        elif input_ == a_key['OVERLAY_HIDE']:
-            """User wants to (un)hide overlay window"""
-            if isinstance(self.loop.widget,urwid.Overlay):
-                self.__saved_overlay = self.loop.widget
-                self.loop.widget = self.main_widget
-            else:
-                if self.__saved_overlay:
-                    self.loop.widget = self.__saved_overlay
-                    self.__saved_overlay = None
-
-        elif input_ == a_key['DEBUG'] and 'D' in self.bridge.getVersion(): #Debug only for dev versions
-            self.debug()
-        elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists
-            try:
-                for wid, options in self.center_part.contents:
-                    if self.contact_lists_pile is wid:
-                        self.center_part.contents.remove((wid, options))
-                        break
-                else:
-                    self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False)))
-            except AttributeError:
-                #The main widget is not built (probably in Profile Manager)
-                pass
-        elif input_ == 'window resize':
-            width,height = self.loop.screen_size
-            if height<=5 and width<=35:
-                if not 'save_main_widget' in dir(self):
-                    self.save_main_widget = self.loop.widget
-                    self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !")))
-            else:
-                if 'save_main_widget' in dir(self):
-                    self.loop.widget = self.save_main_widget
-                    del self.save_main_widget
-        try:
-            return self.menu_roller.checkShortcuts(input_)
-        except AttributeError:
-            return input_
-
-    def addMenus(self, menu, type_filter, menu_data=None):
-        """Add cached menus to instance
-        @param menu: sat_widgets.Menu instance
-        @param type_filter: menu type like is sat.core.sat_main.importMenu
-        @param menu_data: data to send with these menus
-
-        """
-        def add_menu_cb(callback_id):
-            self.launchAction(callback_id, menu_data, profile=self.current_profile)
-        for id_, type_, path, path_i18n, extra  in self.bridge.menusGet("", C.NO_SECURITY_LIMIT ): # TODO: manage extra
-            if type_ != type_filter:
-                continue
-            if len(path) != 2:
-                raise NotImplementedError("Menu with a path != 2 are not implemented yet")
-            menu.addMenu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
-
-
-    def _buildMenuRoller(self):
-        menu = sat_widgets.Menu(self.loop)
-        general = _("General")
-        menu.addMenu(general, _("Connect"), self.onConnectRequest)
-        menu.addMenu(general, _("Disconnect"), self.onDisconnectRequest)
-        menu.addMenu(general, _("Parameters"), self.onParam)
-        menu.addMenu(general, _("About"), self.onAboutRequest)
-        menu.addMenu(general, _("Exit"), self.onExitRequest, a_key['APP_QUIT'])
-        menu.addMenu(_("Contacts"))  # add empty menu to save the place in the menu order
-        groups = _("Groups")
-        menu.addMenu(groups)
-        menu.addMenu(groups, _("Join room"), self.onJoinRoomRequest, a_key['ROOM_JOIN'])
-        #additionals menus
-        #FIXME: do this in a more generic way (in quickapp)
-        self.addMenus(menu, C.MENU_GLOBAL)
-
-        menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
-        return menu_roller
-
-    def _buildMainWidget(self):
-        self.contact_lists_pile = urwid.Pile([])
-        #self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))])
-        self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))])
-
-        self.editBar = EditBar(self)
-        self.menu_roller = self._buildMenuRoller()
-        self.main_widget = PrimitivusTopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar)
-        return self.main_widget
-
-    def plugging_profiles(self):
-        self.loop.widget = self._buildMainWidget()
-        self.redraw()
-        try:
-            # if a popup arrived before main widget is build, we need to show it now
-            self.showPopUp(self._early_popup)
-        except AttributeError:
-            pass
-        else:
-            del self._early_popup
-
-    def profilePlugged(self, profile):
-        QuickApp.profilePlugged(self, profile)
-        contact_list = self.widgets.getOrCreateWidget(ContactList, None, on_new_widget=None, on_click=self.contactSelected, on_change=lambda w: self.redraw(), profile=profile)
-        self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
-        return contact_list
-
-    def isHidden(self):
-        """Tells if the frontend window is hidden.
-
-        @return bool
-        """
-        return False  # FIXME: implement when necessary
-
-    def alert(self, title, message):
-        """Shortcut method to create an alert message
-
-        Alert will have an "OK" button, which remove it if pressed
-        @param title(unicode): title of the dialog
-        @param message(unicode): body of the dialog
-        @return (urwid_satext.Alert): the created Alert instance
-        """
-        popup = sat_widgets.Alert(title, message)
-        popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
-        self.showPopUp(popup, width=75, height=20)
-        return popup
-
-    def removePopUp(self, widget=None):
-        """Remove current pop-up, and if there is other in queue, show it
-
-        @param widget(None, urwid.Widget): if not None remove this popup from front or queue
-        """
-        # TODO: refactor popup management in a cleaner way
-        # buttons' callback use themselve as first argument, and we never use
-        # a Button directly in a popup, so we consider urwid.Button as None
-        if widget is not None and not isinstance(widget, urwid.Button):
-            if isinstance(self.loop.widget, urwid.Overlay):
-                current_popup = self.loop.widget.top_w
-                if not current_popup == widget:
-                    try:
-                        self.notif_bar.removePopUp(widget)
-                    except ValueError:
-                        log.warning(u"Trying to remove an unknown widget {}".format(widget))
-                    return
-        self.loop.widget = self.main_widget
-        next_popup = self.notif_bar.getNextPopup()
-        if next_popup:
-            #we still have popup to show, we display it
-            self.showPopUp(next_popup)
-        else:
-            self.redraw()
-
-    def showPopUp(self, pop_up_widget, width=None, height=None, align='center',
-                  valign='middle'):
-        """Show a pop-up window if possible, else put it in queue
-
-        @param pop_up_widget: pop up to show
-        @param width(int, None): width of the popup
-            None to use default
-        @param height(int, None): height of the popup
-            None to use default
-        @param align: same as for [urwid.Overlay]
-        """
-        if width == None:
-            width = 75 if isinstance(pop_up_widget, xmlui.PrimitivusNoteDialog) else 135
-        if height == None:
-            height = 20 if isinstance(pop_up_widget, xmlui.PrimitivusNoteDialog) else 40
-        if not isinstance(self.loop.widget, urwid.Overlay):
-            display_widget = urwid.Overlay(
-                pop_up_widget, self.main_widget, align, width, valign, height)
-            self.loop.widget = display_widget
-            self.redraw()
-        else:
-            self.notif_bar.addPopUp(pop_up_widget)
-
-    def barNotify(self, message):
-        """"Notify message to user via notification bar"""
-        self.notif_bar.addMessage(message)
-        self.redraw()
-
-    def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE):
-        if widget is None or widget is not None and widget != self.selected_widget:
-            # we ignore notification if the widget is selected but we can
-            # still do a desktop notification is the X window has not the focus
-            super(PrimitivusApp, self).notify(type_, entity, message, subject, callback, cb_args, widget, profile)
-        # we don't want notifications without message on desktop
-        if message is not None and not self.x_notify.hasFocus():
-            if message is None:
-                message = _("{app}: a new event has just happened{entity}").format(
-                    app=C.APP_NAME,
-                    entity=u' ({})'.format(entity) if entity else '')
-            self.x_notify.sendNotification(message)
-
-
-    def newWidget(self, widget, user_action=False):
-        """Method called when a new widget is created
-
-        if suitable, the widget will be displayed
-        @param widget(widget.PrimitivusWidget): created widget
-        @param user_action(bool): if True, the widget has been created following an
-            explicit user action. In this case, the widget may get focus immediately
-        """
-        # FIXME: when several widgets are possible (e.g. with :split)
-        #        do not replace current widget when self.selected_widget != None
-        if user_action or self.selected_widget is None:
-            self.selectWidget(widget)
-
-    def selectWidget(self, widget):
-        """Display a widget if possible,
-
-        else add it in the notification bar queue
-        @param widget: BoxWidget
-        """
-        assert len(self.center_part.widget_list)<=2
-        wid_idx = len(self.center_part.widget_list)-1
-        self.center_part.widget_list[wid_idx] = widget
-        try:
-            self.menu_roller.removeMenu(C.MENU_ID_WIDGET)
-        except KeyError:
-            log.debug("No menu to delete")
-        self.selected_widget = widget
-        try:
-            onSelected = self.selected_widget.onSelected
-        except AttributeError:
-            pass
-        else:
-            onSelected()
-        self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
-        self.contact_lists.select(None)
-
-        for wid in self.visible_widgets: # FIXME: check if widgets.getWidgets is not more appropriate
-            if isinstance(wid, Chat):
-                contact_list = self.contact_lists[wid.profile]
-                contact_list.select(wid.target)
-
-        self.redraw()
-
-    def removeWindow(self):
-        """Remove window showed on the right column"""
-        #TODO: better Window management than this hack
-        assert len(self.center_part.widget_list) <= 2
-        wid_idx = len(self.center_part.widget_list)-1
-        self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text(''))
-        self.center_part.focus_position = 0
-        self.redraw()
-
-    def addProgress(self, pid, message, profile):
-        """Follow a SàT progression
-
-        @param pid: progression id
-        @param message: message to show to identify the progression
-        """
-        self.progress_wid.add(pid, message, profile)
-
-    def setProgress(self, percentage):
-        """Set the progression shown in notification bar"""
-        self.notif_bar.setProgress(percentage)
-
-    def contactSelected(self, contact_list, entity):
-        self.clearNotifs(entity, profile=contact_list.profile)
-        if entity.resource:
-            # we have clicked on a private MUC conversation
-            chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, force_hash = Chat.getPrivateHash(contact_list.profile, entity), profile=contact_list.profile)
-        else:
-            chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
-        self.selectWidget(chat_widget)
-        self.menu_roller.addMenu(_('Chat menu'), chat_widget.getMenu(), C.MENU_ID_WIDGET)
-
-    def _dialogOkCb(self, widget, data):
-        popup, answer_cb, answer_data = data
-        self.removePopUp(popup)
-        if answer_cb is not None:
-            answer_cb(True, answer_data)
-
-    def _dialogCancelCb(self, widget, data):
-        popup, answer_cb, answer_data = data
-        self.removePopUp(popup)
-        if answer_cb is not None:
-            answer_cb(False, answer_data)
-
-    def showDialog(self, message, title="", type="info", answer_cb = None, answer_data = None):
-        if type == 'info':
-            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
-            if answer_cb is None:
-                popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
-        elif type == 'error':
-            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
-            if answer_cb is None:
-                popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
-        elif type == 'yes/no':
-            popup = sat_widgets.ConfirmDialog(message)
-            popup.setCallback('yes', self._dialogOkCb, (popup, answer_cb, answer_data))
-            popup.setCallback('no', self._dialogCancelCb, (popup, answer_cb, answer_data))
-        else:
-            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
-            if answer_cb is None:
-                popup.setCallback('ok', lambda dummy: self.removePopUp(popup))
-            log.error(u'unmanaged dialog type: {}'.format(type))
-        self.showPopUp(popup)
-
-    def dialogFailure(self, failure):
-        """Show a failure that has been returned by an asynchronous bridge method.
-
-        @param failure (defer.Failure): Failure instance
-        """
-        self.alert(failure.classname, failure.message)
-
-    def onNotification(self, notif_bar):
-        """Called when a new notification has been received"""
-        if not isinstance(self.main_widget, PrimitivusTopWidget):
-            #if we are not in the main configuration, we ignore the notifications bar
-            return
-        if self.notif_bar.canHide():
-                #No notification left, we can hide the bar
-                self.main_widget.hide('notif_bar')
-        else:
-            self.main_widget.show('notif_bar')
-            self.redraw() # FIXME: invalidate cache in a more efficient way
-
-    def _actionManagerUnknownError(self):
-        self.alert(_("Error"), _(u"Unmanaged action"))
-
-    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, subject, profile):
-        super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, subject, profile)
-        # if self.selected_widget is None:
-        #     for contact_list in self.widgets.getWidgets(ContactList):
-        #         if profile in contact_list.profiles:
-        #             contact_list.setFocus(jid.JID(room_jid_s), True)
-
-    def progressStartedHandler(self, pid, metadata, profile):
-        super(PrimitivusApp, self).progressStartedHandler(pid, metadata, profile)
-        self.addProgress(pid, metadata.get('name', _(u'unkown')), profile)
-
-    def progressFinishedHandler(self, pid, metadata, profile):
-        log.info(u"Progress {} finished".format(pid))
-        super(PrimitivusApp, self).progressFinishedHandler(pid, metadata, profile)
-
-    def progressErrorHandler(self, pid, err_msg, profile):
-        log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
-        super(PrimitivusApp, self).progressErrorHandler(pid, err_msg, profile)
-
-
-    ##DIALOGS CALLBACKS##
-    def onJoinRoom(self, button, edit):
-        self.removePopUp()
-        room_jid = jid.JID(edit.get_edit_text())
-        self.bridge.mucJoin(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialogFailure)
-
-    #MENU EVENTS#
-    def onConnectRequest(self, menu):
-        QuickApp.connect(self, self.current_profile)
-
-    def onDisconnectRequest(self, menu):
-        self.disconnect(self.current_profile)
-
-    def onParam(self, menu):
-        def success(params):
-            ui = xmlui.create(self, xml_data=params, profile=self.current_profile)
-            ui.show()
-
-        def failure(error):
-            self.alert(_("Error"), _("Can't get parameters (%s)") % error)
-        self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
-
-    def onExitRequest(self, menu):
-        QuickApp.onExit(self)
-        try:
-            if self._bracketed_mode_set: # we don't unset if bracketed paste mode was detected automatically (i.e. not in conf)
-                log.debug("unsetting bracketed paste mode")
-                sys.stdout.write("\033[?2004l")
-        except AttributeError:
-            pass
-        raise urwid.ExitMainLoop()
-
-    def onJoinRoomRequest(self, menu):
-        """User wants to join a MUC room"""
-        pop_up_widget = sat_widgets.InputDialog(_("Entering a MUC room"), _("Please enter MUC's JID"), default_txt=self.bridge.mucGetDefaultService(), ok_cb=self.onJoinRoom)
-        pop_up_widget.setCallback('cancel', lambda dummy: self.removePopUp(pop_up_widget))
-        self.showPopUp(pop_up_widget)
-
-    def onAboutRequest(self, menu):
-        self.alert(_("About"), C.APP_NAME + " v" + self.bridge.getVersion())
-
-    #MISC CALLBACKS#
-
-    def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE):
-        contact_list_wid = self.widgets.getWidget(ContactList, profiles=profile)
-        if contact_list_wid is not None:
-            contact_list_wid.status_bar.setPresenceStatus(show, status)
-        else:
-            log.warning(u"No ContactList widget found for profile {}".format(profile))
-
-primitivus = PrimitivusApp()
-primitivus.start()
--- a/setup.py	Sat Mar 20 20:42:04 2021 +0100
+++ b/setup.py	Sat Mar 20 20:42:07 2021 +0100
@@ -84,9 +84,9 @@
 setup(
     name=NAME,
     version=VERSION,
-    description="Salut à Toi multipurpose and multi frontend XMPP client",
-    long_description="Salut à Toi (SàT) is a XMPP client based on a daemon/frontend "
-                     "architecture. Its multi frontend (desktop, web, console "
+    description="Libervia multipurpose and multi frontend XMPP client",
+    long_description="Libervia is a XMPP client based on a daemon/frontend "
+                     "architecture. It's multi frontend (desktop, web, console "
                      "interface, CLI, etc) and multipurpose (instant messaging, "
                      "microblogging, games, file sharing, etc).",
     author="Association « Salut à Toi »",
@@ -96,6 +96,7 @@
         "Programming Language :: Python :: 3 :: Only",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
         "Development Status :: 5 - Production/Stable",
         "Environment :: Console",
         "Framework :: Twisted",
@@ -113,10 +114,20 @@
                 ],
     entry_points={
         "console_scripts": [
+            # backend + alias
+            "libervia-backend = sat.core.launcher:Launcher.run",
             "sat = sat.core.launcher:Launcher.run",
+
+            # CLI + aliases
+            "libervia-cli = sat_frontends.jp.base:LiberviaCli.run",
+            "li = sat_frontends.jp.base:LiberviaCli.run",
+            "jp = sat_frontends.jp.base:LiberviaCli.run",
+
+            # TUI + alias
+            "libervia-tui = sat_frontends.primitivus.base:PrimitivusApp.run",
+            "primitivus = sat_frontends.primitivus.base:PrimitivusApp.run",
             ],
         },
-    scripts=["sat_frontends/jp/jp", "sat_frontends/primitivus/primitivus"],
     zip_safe=False,
     setup_requires=["setuptools_scm"] if is_dev_version else [],
     use_scm_version=sat_dev_version if is_dev_version else False,