changeset 4076:b620a8e882e1

refactoring: rename `libervia.frontends.primitivus` to `libervia.tui`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:25:25 +0200
parents 47401850dec6
children d6837db456fd
files libervia/frontends/primitivus/__init__.py libervia/frontends/primitivus/base.py libervia/frontends/primitivus/chat.py libervia/frontends/primitivus/config.py libervia/frontends/primitivus/constants.py libervia/frontends/primitivus/contact_list.py libervia/frontends/primitivus/game_tarot.py libervia/frontends/primitivus/keys.py libervia/frontends/primitivus/notify.py libervia/frontends/primitivus/profile_manager.py libervia/frontends/primitivus/progress.py libervia/frontends/primitivus/status.py libervia/frontends/primitivus/widget.py libervia/frontends/primitivus/xmlui.py libervia/frontends/quick_frontend/quick_profile_manager.py libervia/tui/__init__.py libervia/tui/base.py libervia/tui/chat.py libervia/tui/config.py libervia/tui/constants.py libervia/tui/contact_list.py libervia/tui/game_tarot.py libervia/tui/keys.py libervia/tui/notify.py libervia/tui/profile_manager.py libervia/tui/progress.py libervia/tui/status.py libervia/tui/widget.py libervia/tui/xmlui.py setup.py
diffstat 28 files changed, 3727 insertions(+), 3727 deletions(-) [+]
line wrap: on
line diff
--- a/libervia/frontends/primitivus/base.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,863 +0,0 @@
-#!/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 libervia.backend.core.i18n import _, D_
-from libervia.frontends.primitivus.constants import Const as C
-from libervia.backend.core import log_config
-log_config.sat_configure(C.LOG_BACKEND_STANDARD, C)
-from libervia.backend.core import log as logging
-log = logging.getLogger(__name__)
-from libervia.backend.tools import config as sat_config
-import urwid
-from urwid.util import is_wide_char
-from urwid_satext import sat_widgets
-from libervia.frontends.quick_frontend.quick_app import QuickApp
-from libervia.frontends.quick_frontend import quick_utils
-from libervia.frontends.quick_frontend import quick_chat
-from libervia.frontends.primitivus.profile_manager import ProfileManager
-from libervia.frontends.primitivus.contact_list import ContactList
-from libervia.frontends.primitivus.chat import Chat
-from libervia.frontends.primitivus import xmlui
-from libervia.frontends.primitivus.progress import Progress
-from libervia.frontends.primitivus.notify import Notify
-from libervia.frontends.primitivus.keys import action_key_map as a_key
-from libervia.frontends.primitivus import config
-from libervia.frontends.tools.misc import InputHistory
-from libervia.backend.tools.common import dynamic_import
-from libervia.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.parse_main_conf()
-bridge_name = sat_config.config_get(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.set_completion_method(self._text_completion)
-        urwid.connect_signal(self, 'click', self.on_text_entered)
-
-    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 on_text_entered(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.message_send(
-                    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.show_dialog(_("Error while sending message ({})").format(failure), type="error"),
-                    profile_key=chat_widget.profile
-                    )
-                editBar.set_edit_text('')
-        elif self.mode == C.MODE_COMMAND:
-            self.command_handler()
-
-    def command_handler(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.on_exit()
-            raise urwid.ExitMainLoop()
-        elif command == 'messages':
-            wid = sat_widgets.GenericList(logging.memory_get())
-            self.host.select_widget(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.on_change(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
-        #     else:
-        #         self.host.status_bar.on_presence_click()
-        # elif command == 'status':
-        #     if args:
-        #         self.host.status_bar.on_change(user_data=sat_widgets.AdvancedEdit(args[0]))
-        #     else:
-        #         self.host.status_bar.on_status_click()
-        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.update_history(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.add_message(D_("Please specify the globbing pattern to search for"))
-                else:
-                    widget.update_history(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.set_filter(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.on_subject_dialog(new_title)
-        else:
-            return
-        self.set_edit_text('')
-
-    def _history_cb(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._update_input_history(self.get_edit_text(), mode=self.mode)
-            self.host._update_input_history(mode=C.MODE_NORMAL)
-        if self._mode == C.MODE_NORMAL and key in self._modes:
-            self.host._update_input_history(mode=self._modes[key][0])
-        if key == a_key['HISTORY_PREV']:
-            self.host._update_input_history(self.get_edit_text(), -1, self._history_cb, self.mode)
-            return
-        elif key == a_key['HISTORY_NEXT']:
-            self.host._update_input_history(self.get_edit_text(), +1, self._history_cb, self.mode)
-            return
-        elif key == a_key['EDIT_ENTER']:
-            self.host._update_input_history(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.chat_state_composing(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.widget_get(position=position),
-                             lambda pos, new_wid: self.widget_set(new_wid, position=pos))
-                   )
-        self.focus_position = len(self.contents)-1
-
-    def get_visible_positions(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.get_visible_positions().index(focus)
-            return
-
-        return super(PrimitivusTopWidget, self).keypress(size, key)
-
-    def widget_get(self,  position):
-        if not position in self.positions:
-            raise ValueError("Unknown position {}".format(position))
-        return getattr(self, "_{}".format(position))
-
-    def widget_set(self,  widget, position):
-        if not position in self.positions:
-            raise ValueError("Unknown position {}".format(position))
-        return setattr(self, "_{}".format(position), widget)
-
-    def hide_switch(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.widget_get(position)
-        idx = self.get_visible_positions(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.hide_switch(position)
-
-    def hide(self, position):
-        if not position in self._hidden:
-            self.hide_switch(position)
-
-
-class PrimitivusApp(QuickApp, InputHistory):
-    MB_HANDLER = False
-    AVATARS_HANDLER = False
-
-    def __init__(self):
-        bridge_module = dynamic_import.bridge(bridge_name, 'libervia.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.input_filter, unhandled_input=self.key_handler)
-
-    @classmethod
-    def run(cls):
-        cls().start()
-
-    def on_bridge_connected(self):
-
-        ##misc setup##
-        self._visible_widgets = set()
-        self.notif_bar = sat_widgets.NotificationBar()
-        urwid.connect_signal(self.notif_bar, 'change', self.on_notification)
-
-        self.progress_wid = self.widgets.get_or_create_widget(Progress, None, on_new_widget=None)
-        urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.select_widget(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.parse_main_conf()
-        self._bracketed_paste = C.bool(
-            sat_config.config_get(sat_conf, C.CONFIG_SECTION, '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.post_init()
-
-    @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 mode_hint(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.connect_bridge()
-        self.loop.run()
-
-    def post_init(self):
-        try:
-            config.apply_config(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.show_pop_up(popup)
-        super(PrimitivusApp, self).post_init(self.main_widget)
-
-    def keys_to_text(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 input_filter(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.keys_to_text(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 key_handler(self, input_):
-        if input_ == a_key['MENU_HIDE']:
-            """User want to (un)hide the menu roller"""
-            try:
-                self.main_widget.hide_switch('menu')
-            except AttributeError:
-                pass
-        elif input_ == a_key['NOTIFICATION_NEXT']:
-            """User wants to see next notification"""
-            self.notif_bar.show_next()
-        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.version_get(): #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.check_shortcuts(input_)
-        except AttributeError:
-            return input_
-
-    def add_menus(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.import_menu
-        @param menu_data: data to send with these menus
-
-        """
-        def add_menu_cb(callback_id):
-            self.action_launch(callback_id, menu_data, profile=self.current_profile)
-        for id_, type_, path, path_i18n, extra  in self.bridge.menus_get("", 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.add_menu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
-
-
-    def _build_menu_roller(self):
-        menu = sat_widgets.Menu(self.loop)
-        general = _("General")
-        menu.add_menu(general, _("Connect"), self.on_connect_request)
-        menu.add_menu(general, _("Disconnect"), self.on_disconnect_request)
-        menu.add_menu(general, _("Parameters"), self.on_param)
-        menu.add_menu(general, _("About"), self.on_about_request)
-        menu.add_menu(general, _("Exit"), self.on_exit_request, a_key['APP_QUIT'])
-        menu.add_menu(_("Contacts"))  # add empty menu to save the place in the menu order
-        groups = _("Groups")
-        menu.add_menu(groups)
-        menu.add_menu(groups, _("Join room"), self.on_join_room_request, a_key['ROOM_JOIN'])
-        #additionals menus
-        #FIXME: do this in a more generic way (in quickapp)
-        self.add_menus(menu, C.MENU_GLOBAL)
-
-        menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
-        return menu_roller
-
-    def _build_main_widget(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._build_menu_roller()
-        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._build_main_widget()
-        self.redraw()
-        try:
-            # if a popup arrived before main widget is build, we need to show it now
-            self.show_pop_up(self._early_popup)
-        except AttributeError:
-            pass
-        else:
-            del self._early_popup
-
-    def profile_plugged(self, profile):
-        QuickApp.profile_plugged(self, profile)
-        contact_list = self.widgets.get_or_create_widget(ContactList, None, on_new_widget=None, on_click=self.contact_selected, on_change=lambda w: self.redraw(), profile=profile)
-        self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
-        return contact_list
-
-    def is_hidden(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.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
-        self.show_pop_up(popup, width=75, height=20)
-        return popup
-
-    def remove_pop_up(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.remove_pop_up(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.get_next_popup()
-        if next_popup:
-            #we still have popup to show, we display it
-            self.show_pop_up(next_popup)
-        else:
-            self.redraw()
-
-    def show_pop_up(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.add_pop_up(pop_up_widget)
-
-    def bar_notify(self, message):
-        """"Notify message to user via notification bar"""
-        self.notif_bar.add_message(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.has_focus():
-            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.send_notification(message)
-
-
-    def new_widget(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.select_widget(widget)
-
-    def select_widget(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.remove_menu(C.MENU_ID_WIDGET)
-        except KeyError:
-            log.debug("No menu to delete")
-        self.selected_widget = widget
-        try:
-            on_selected = self.selected_widget.on_selected
-        except AttributeError:
-            pass
-        else:
-            on_selected()
-        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.get_widgets is not more appropriate
-            if isinstance(wid, Chat):
-                contact_list = self.contact_lists[wid.profile]
-                contact_list.select(wid.target)
-
-        self.redraw()
-
-    def remove_window(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 add_progress(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 set_progress(self, percentage):
-        """Set the progression shown in notification bar"""
-        self.notif_bar.set_progress(percentage)
-
-    def contact_selected(self, contact_list, entity):
-        self.clear_notifs(entity, profile=contact_list.profile)
-        if entity.resource:
-            # we have clicked on a private MUC conversation
-            chat_widget = self.widgets.get_or_create_widget(Chat, entity, on_new_widget=None, force_hash = Chat.get_private_hash(contact_list.profile, entity), profile=contact_list.profile)
-        else:
-            chat_widget = self.widgets.get_or_create_widget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
-        self.select_widget(chat_widget)
-        self.menu_roller.add_menu(_('Chat menu'), chat_widget.get_menu(), C.MENU_ID_WIDGET)
-
-    def _dialog_ok_cb(self, widget, data):
-        popup, answer_cb, answer_data = data
-        self.remove_pop_up(popup)
-        if answer_cb is not None:
-            answer_cb(True, answer_data)
-
-    def _dialog_cancel_cb(self, widget, data):
-        popup, answer_cb, answer_data = data
-        self.remove_pop_up(popup)
-        if answer_cb is not None:
-            answer_cb(False, answer_data)
-
-    def show_dialog(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.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
-        elif type == 'error':
-            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
-            if answer_cb is None:
-                popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
-        elif type == 'yes/no':
-            popup = sat_widgets.ConfirmDialog(message)
-            popup.set_callback('yes', self._dialog_ok_cb, (popup, answer_cb, answer_data))
-            popup.set_callback('no', self._dialog_cancel_cb, (popup, answer_cb, answer_data))
-        else:
-            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
-            if answer_cb is None:
-                popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
-            log.error(u'unmanaged dialog type: {}'.format(type))
-        self.show_pop_up(popup)
-
-    def dialog_failure(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 on_notification(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.can_hide():
-                #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 _action_manager_unknown_error(self):
-        self.alert(_("Error"), _(u"Unmanaged action"))
-
-    def room_joined_handler(self, room_jid_s, room_nicks, user_nick, subject, profile):
-        super(PrimitivusApp, self).room_joined_handler(room_jid_s, room_nicks, user_nick, subject, profile)
-        # if self.selected_widget is None:
-        #     for contact_list in self.widgets.get_widgets(ContactList):
-        #         if profile in contact_list.profiles:
-        #             contact_list.set_focus(jid.JID(room_jid_s), True)
-
-    def progress_started_handler(self, pid, metadata, profile):
-        super(PrimitivusApp, self).progress_started_handler(pid, metadata, profile)
-        self.add_progress(pid, metadata.get('name', _(u'unkown')), profile)
-
-    def progress_finished_handler(self, pid, metadata, profile):
-        log.info(u"Progress {} finished".format(pid))
-        super(PrimitivusApp, self).progress_finished_handler(pid, metadata, profile)
-
-    def progress_error_handler(self, pid, err_msg, profile):
-        log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
-        super(PrimitivusApp, self).progress_error_handler(pid, err_msg, profile)
-
-
-    ##DIALOGS CALLBACKS##
-    def on_join_room(self, button, edit):
-        self.remove_pop_up()
-        room_jid = jid.JID(edit.get_edit_text())
-        self.bridge.muc_join(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialog_failure)
-
-    #MENU EVENTS#
-    def on_connect_request(self, menu):
-        QuickApp.connect(self, self.current_profile)
-
-    def on_disconnect_request(self, menu):
-        self.disconnect(self.current_profile)
-
-    def on_param(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.param_ui_get(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
-
-    def on_exit_request(self, menu):
-        QuickApp.on_exit(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 on_join_room_request(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.muc_get_default_service(), ok_cb=self.on_join_room)
-        pop_up_widget.set_callback('cancel', lambda dummy: self.remove_pop_up(pop_up_widget))
-        self.show_pop_up(pop_up_widget)
-
-    def on_about_request(self, menu):
-        self.alert(_("About"), C.APP_NAME + " v" + self.bridge.version_get())
-
-    #MISC CALLBACKS#
-
-    def set_presence_status(self, show='', status=None, profile=C.PROF_KEY_NONE):
-        contact_list_wid = self.widgets.get_widget(ContactList, profiles=profile)
-        if contact_list_wid is not None:
-            contact_list_wid.status_bar.set_presence_status(show, status)
-        else:
-            log.warning(u"No ContactList widget found for profile {}".format(profile))
-
-if __name__ == '__main__':
-    PrimitivusApp().start()
--- a/libervia/frontends/primitivus/chat.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,708 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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 functools import total_ordering
-from pathlib import Path
-import bisect
-import urwid
-from urwid_satext import sat_widgets
-from libervia.backend.core.i18n import _
-from libervia.backend.core import log as logging
-from libervia.frontends.quick_frontend import quick_widgets
-from libervia.frontends.quick_frontend import quick_chat
-from libervia.frontends.quick_frontend import quick_games
-from libervia.frontends.primitivus import game_tarot
-from libervia.frontends.primitivus.constants import Const as C
-from libervia.frontends.primitivus.keys import action_key_map as a_key
-from libervia.frontends.primitivus.widget import PrimitivusWidget
-from libervia.frontends.primitivus.contact_list import ContactList
-
-
-log = logging.getLogger(__name__)
-
-
-OCCUPANTS_FOOTER = _("{} occupants")
-
-
-class MessageWidget(urwid.WidgetWrap, quick_chat.MessageWidget):
-    def __init__(self, mess_data):
-        """
-        @param mess_data(quick_chat.Message, None): message data
-            None: used only for non text widgets (e.g.: focus separator)
-        """
-        self.mess_data = mess_data
-        mess_data.widgets.add(self)
-        super(MessageWidget, self).__init__(urwid.Text(self.markup))
-
-    @property
-    def markup(self):
-        return (
-            self._generate_info_markup()
-            if self.mess_data.type == C.MESS_TYPE_INFO
-            else self._generate_markup()
-        )
-
-    @property
-    def info_type(self):
-        return self.mess_data.info_type
-
-    @property
-    def parent(self):
-        return self.mess_data.parent
-
-    @property
-    def message(self):
-        """Return currently displayed message"""
-        return self.mess_data.main_message
-
-    @message.setter
-    def message(self, value):
-        self.mess_data.message = {"": value}
-        self.redraw()
-
-    @property
-    def type(self):
-        try:
-            return self.mess_data.type
-        except AttributeError:
-            return C.MESS_TYPE_INFO
-
-    def redraw(self):
-        self._w.set_text(self.markup)
-        self.mess_data.parent.host.redraw()  # FIXME: should not be necessary
-
-    def selectable(self):
-        return True
-
-    def keypress(self, size, key):
-        return key
-
-    def get_cursor_coords(self, size):
-        return 0, 0
-
-    def render(self, size, focus=False):
-        # Text widget doesn't render cursor, but we want one
-        # so we add it here
-        canvas = urwid.CompositeCanvas(self._w.render(size, focus))
-        if focus:
-            canvas.set_cursor(self.get_cursor_coords(size))
-        return canvas
-
-    def _generate_info_markup(self):
-        return ("info_msg", self.message)
-
-    def _generate_markup(self):
-        """Generate text markup according to message data and Widget options"""
-        markup = []
-        d = self.mess_data
-        mention = d.mention
-
-        # message status
-        if d.status is None:
-            markup.append(" ")
-        elif d.status == "delivered":
-            markup.append(("msg_status_received", "✔"))
-        else:
-            log.warning("Unknown status: {}".format(d.status))
-
-        # timestamp
-        if self.parent.show_timestamp:
-            attr = "msg_mention" if mention else "date"
-            markup.append((attr, "[{}]".format(d.time_text)))
-        else:
-            if mention:
-                markup.append(("msg_mention", "[*]"))
-
-        # nickname
-        if self.parent.show_short_nick:
-            markup.append(
-                ("my_nick" if d.own_mess else "other_nick", "**" if d.own_mess else "*")
-            )
-        else:
-            markup.append(
-                ("my_nick" if d.own_mess else "other_nick", "[{}] ".format(d.nick or ""))
-            )
-
-        msg = self.message  # needed to generate self.selected_lang
-
-        if d.selected_lang:
-            markup.append(("msg_lang", "[{}] ".format(d.selected_lang)))
-
-        # message body
-        markup.append(msg)
-
-        return markup
-
-    # events
-    def update(self, update_dict=None):
-        """update all the linked message widgets
-
-        @param update_dict(dict, None): key=attribute updated value=new_value
-        """
-        self.redraw()
-
-
-@total_ordering
-class OccupantWidget(urwid.WidgetWrap):
-    def __init__(self, occupant_data):
-        self.occupant_data = occupant_data
-        occupant_data.widgets.add(self)
-        markup = self._generate_markup()
-        text = sat_widgets.ClickableText(markup)
-        urwid.connect_signal(
-            text,
-            "click",
-            self.occupant_data.parent._occupants_clicked,
-            user_args=[self.occupant_data],
-        )
-        super(OccupantWidget, self).__init__(text)
-
-    def __hash__(self):
-        return id(self)
-
-    def __eq__(self, other):
-        if other is None:
-            return False
-        return self.occupant_data.nick == other.occupant_data.nick
-
-    def __lt__(self, other):
-        return self.occupant_data.nick.lower() < other.occupant_data.nick.lower()
-
-    @property
-    def markup(self):
-        return self._generate_markup()
-
-    @property
-    def parent(self):
-        return self.mess_data.parent
-
-    @property
-    def nick(self):
-        return self.occupant_data.nick
-
-    def redraw(self):
-        self._w.set_text(self.markup)
-        self.occupant_data.parent.host.redraw()  # FIXME: should not be necessary
-
-    def selectable(self):
-        return True
-
-    def keypress(self, size, key):
-        return key
-
-    def get_cursor_coords(self, size):
-        return 0, 0
-
-    def render(self, size, focus=False):
-        # Text widget doesn't render cursor, but we want one
-        # so we add it here
-        canvas = urwid.CompositeCanvas(self._w.render(size, focus))
-        if focus:
-            canvas.set_cursor(self.get_cursor_coords(size))
-        return canvas
-
-    def _generate_markup(self):
-        # TODO: role and affiliation are shown in a Q&D way
-        #       should be more intuitive and themable
-        o = self.occupant_data
-        markup = []
-        markup.append(
-            ("info_msg", "{}{} ".format(o.role[0].upper(), o.affiliation[0].upper()))
-        )
-        markup.append(o.nick)
-        if o.state is not None:
-            markup.append(" {}".format(C.CHAT_STATE_ICON[o.state]))
-        return markup
-
-    # events
-    def update(self, update_dict=None):
-        self.redraw()
-
-
-class OccupantsWidget(urwid.WidgetWrap):
-    def __init__(self, parent):
-        self.parent = parent
-        self.occupants_walker = urwid.SimpleListWalker([])
-        self.occupants_footer = urwid.Text("", align="center")
-        self.update_footer()
-        occupants_widget = urwid.Frame(
-            urwid.ListBox(self.occupants_walker), footer=self.occupants_footer
-        )
-        super(OccupantsWidget, self).__init__(occupants_widget)
-        occupants_list = sorted(list(self.parent.occupants.keys()), key=lambda o: o.lower())
-        for occupant in occupants_list:
-            occupant_data = self.parent.occupants[occupant]
-            self.occupants_walker.append(OccupantWidget(occupant_data))
-
-    def clear(self):
-        del self.occupants_walker[:]
-
-    def update_footer(self):
-        """update footer widget"""
-        txt = OCCUPANTS_FOOTER.format(len(self.parent.occupants))
-        self.occupants_footer.set_text(txt)
-
-    def get_nicks(self, start=""):
-        """Return nicks of all occupants
-
-        @param start(unicode): only return nicknames which start with this text
-        """
-        return [
-            w.nick
-            for w in self.occupants_walker
-            if isinstance(w, OccupantWidget) and w.nick.startswith(start)
-        ]
-
-    def addUser(self, occupant_data):
-        """add a user to the list"""
-        bisect.insort(self.occupants_walker, OccupantWidget(occupant_data))
-        self.update_footer()
-        self.parent.host.redraw()  # FIXME: should not be necessary
-
-    def removeUser(self, occupant_data):
-        """remove a user from the list"""
-        for widget in occupant_data.widgets:
-            self.occupants_walker.remove(widget)
-        self.update_footer()
-        self.parent.host.redraw()  # FIXME: should not be necessary
-
-
-class Chat(PrimitivusWidget, quick_chat.QuickChat):
-    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None,
-                 subject=None, statuses=None, profiles=None):
-        self.filters = []  # list of filter callbacks to apply
-        self.mess_walker = urwid.SimpleListWalker([])
-        self.mess_widgets = urwid.ListBox(self.mess_walker)
-        self.chat_widget = urwid.Frame(self.mess_widgets)
-        self.chat_colums = urwid.Columns([("weight", 8, self.chat_widget)])
-        self.pile = urwid.Pile([self.chat_colums])
-        PrimitivusWidget.__init__(self, self.pile, target)
-        quick_chat.QuickChat.__init__(
-            self, host, target, type_, nick, occupants, subject, statuses,
-            profiles=profiles
-        )
-
-        # we must adapt the behaviour with the type
-        if type_ == C.CHAT_GROUP:
-            if len(self.chat_colums.contents) == 1:
-                self.occupants_widget = OccupantsWidget(self)
-                self.occupants_panel = sat_widgets.VerticalSeparator(
-                    self.occupants_widget
-                )
-                self._append_occupants_panel()
-                self.host.addListener("presence", self.presence_listener, [profiles])
-
-        # focus marker is a separator indicated last visible message before focus was lost
-        self.focus_marker = None  # link to current marker
-        self.focus_marker_set = None  # True if a new marker has been inserted
-        self.show_timestamp = True
-        self.show_short_nick = False
-        self.show_title = 1  # 0: clip title; 1: full title; 2: no title
-        self.post_init()
-
-    @property
-    def message_widgets_rev(self):
-        return reversed(self.mess_walker)
-
-    def keypress(self, size, key):
-        if key == a_key["OCCUPANTS_HIDE"]:  # user wants to (un)hide the occupants panel
-            if self.type == C.CHAT_GROUP:
-                widgets = [widget for (widget, options) in self.chat_colums.contents]
-                if self.occupants_panel in widgets:
-                    self._remove_occupants_panel()
-                else:
-                    self._append_occupants_panel()
-        elif key == a_key["TIMESTAMP_HIDE"]:  # user wants to (un)hide timestamp
-            self.show_timestamp = not self.show_timestamp
-            self.redraw()
-        elif key == a_key["SHORT_NICKNAME"]:  # user wants to (not) use short nick
-            self.show_short_nick = not self.show_short_nick
-            self.redraw()
-        elif (key == a_key["SUBJECT_SWITCH"]):
-            # user wants to (un)hide group's subject or change its apperance
-            if self.subject:
-                self.show_title = (self.show_title + 1) % 3
-                if self.show_title == 0:
-                    self.set_subject(self.subject, "clip")
-                elif self.show_title == 1:
-                    self.set_subject(self.subject, "space")
-                elif self.show_title == 2:
-                    self.chat_widget.header = None
-                self._invalidate()
-        elif key == a_key["GOTO_BOTTOM"]:  # user wants to focus last message
-            self.mess_widgets.focus_position = len(self.mess_walker) - 1
-
-        return super(Chat, self).keypress(size, key)
-
-    def completion(self, text, completion_data):
-        """Completion method which complete nicknames in group chat
-
-        for params, see [sat_widgets.AdvancedEdit]
-        """
-        if self.type != C.CHAT_GROUP:
-            return text
-
-        space = text.rfind(" ")
-        start = text[space + 1 :]
-        words = self.occupants_widget.get_nicks(start)
-        if not words:
-            return text
-        try:
-            word_idx = words.index(completion_data["last_word"]) + 1
-        except (KeyError, ValueError):
-            word_idx = 0
-        else:
-            if word_idx == len(words):
-                word_idx = 0
-        word = completion_data["last_word"] = words[word_idx]
-        return "{}{}{}".format(text[: space + 1], word, ": " if space < 0 else "")
-
-    def get_menu(self):
-        """Return Menu bar"""
-        menu = sat_widgets.Menu(self.host.loop)
-        if self.type == C.CHAT_GROUP:
-            self.host.add_menus(menu, C.MENU_ROOM, {"room_jid": self.target.bare})
-            game = _("Game")
-            menu.add_menu(game, "Tarot", self.on_tarot_request)
-        elif self.type == C.CHAT_ONE2ONE:
-            # FIXME: self.target is a bare jid, we need to check that
-            contact_list = self.host.contact_lists[self.profile]
-            if not self.target.resource:
-                full_jid = contact_list.get_full_jid(self.target)
-            else:
-                full_jid = self.target
-            self.host.add_menus(menu, C.MENU_SINGLE, {"jid": full_jid})
-        return menu
-
-    def set_filter(self, args):
-        """set filtering of messages
-
-        @param args(list[unicode]): filters following syntax "[filter]=[value]"
-            empty list to clear all filters
-            only lang=XX is handled for now
-        """
-        del self.filters[:]
-        if args:
-            if args[0].startswith("lang="):
-                lang = args[0][5:].strip()
-                self.filters.append(lambda mess_data: lang in mess_data.message)
-
-        self.print_messages()
-
-    def presence_listener(self, entity, show, priority, statuses, profile):
-        """Update entity's presence status
-
-        @param entity (jid.JID): entity updated
-        @param show: availability
-        @param priority: resource's priority
-        @param statuses: dict of statuses
-        @param profile: %(doc_profile)s
-        """
-        # FIXME: disable for refactoring, need to be checked and re-enabled
-        return
-        # assert self.type == C.CHAT_GROUP
-        # if entity.bare != self.target:
-        #     return
-        # self.update(entity)
-
-    def create_message(self, message):
-        self.appendMessage(message)
-
-    def _scrollDown(self):
-        """scroll down message only if we are already at the bottom (minus 1)"""
-        current_focus = self.mess_widgets.focus_position
-        bottom = len(self.mess_walker) - 1
-        if current_focus == bottom - 1:
-            self.mess_widgets.focus_position = bottom  # scroll down
-        self.host.redraw()  # FIXME: should not be necessary
-
-    def appendMessage(self, message, minor_notifs=True):
-        """Create a MessageWidget and append it
-
-        Can merge info messages together if desirable (e.g.: multiple joined/leave)
-        @param message(quick_chat.Message): message to add
-        @param minor_notifs(boolean): if True, basic notifications are allowed
-            If False, notification are not shown except if we have an important one
-            (like a mention).
-            False is generally used when printing history, when we don't want every
-            message to be notified.
-        """
-        if message.attachments:
-            # FIXME: Q&D way to see attachments in Primitivus
-            #   it should be done in a more user friendly way
-            for lang, body in message.message.items():
-                for attachment in message.attachments:
-                    if 'url' in attachment:
-                        body+=f"\n{attachment['url']}"
-                    elif 'path' in attachment:
-                        path = Path(attachment['path'])
-                        body+=f"\n{path.as_uri()}"
-                    else:
-                        log.warning(f'No "url" nor "path" in attachment: {attachment}')
-                    message.message[lang] = body
-
-        if self.filters:
-            if not all([f(message) for f in self.filters]):
-                return
-
-        if self.handle_user_moved(message):
-            return
-
-        if ((self.host.selected_widget != self or not self.host.x_notify.has_focus())
-            and self.focus_marker_set is not None):
-            if not self.focus_marker_set and not self._locked and self.mess_walker:
-                if self.focus_marker is not None:
-                    try:
-                        self.mess_walker.remove(self.focus_marker)
-                    except ValueError:
-                        # self.focus_marker may not be in mess_walker anymore if
-                        # mess_walker has been cleared, e.g. when showing search
-                        # result or using :history command
-                        pass
-                self.focus_marker = urwid.Divider("—")
-                self.mess_walker.append(self.focus_marker)
-                self.focus_marker_set = True
-                self._scrollDown()
-        else:
-            if self.focus_marker_set:
-                self.focus_marker_set = False
-
-        wid = MessageWidget(message)
-        self.mess_walker.append(wid)
-        self._scrollDown()
-        if self.is_user_moved(message):
-            return  # no notification for moved messages
-
-        # notifications
-
-        if self._locked:
-            # we don't want notifications when locked
-            # because that's history messages
-            return
-
-        if wid.mess_data.mention:
-            from_jid = wid.mess_data.from_jid
-            msg = _(
-                "You have been mentioned by {nick} in {room}".format(
-                    nick=wid.mess_data.nick, room=self.target
-                )
-            )
-            self.host.notify(
-                C.NOTIFY_MENTION, from_jid, msg, widget=self, profile=self.profile
-            )
-        elif not minor_notifs:
-            return
-        elif self.type == C.CHAT_ONE2ONE:
-            from_jid = wid.mess_data.from_jid
-            msg = _("{entity} is talking to you".format(entity=from_jid))
-            self.host.notify(
-                C.NOTIFY_MESSAGE, from_jid, msg, widget=self, profile=self.profile
-            )
-        else:
-            self.host.notify(
-                C.NOTIFY_MESSAGE, self.target, widget=self, profile=self.profile
-            )
-
-    def addUser(self, nick):
-        occupant = super(Chat, self).addUser(nick)
-        self.occupants_widget.addUser(occupant)
-
-    def removeUser(self, occupant_data):
-        occupant = super(Chat, self).removeUser(occupant_data)
-        if occupant is not None:
-            self.occupants_widget.removeUser(occupant)
-
-    def occupants_clear(self):
-        super(Chat, self).occupants_clear()
-        self.occupants_widget.clear()
-
-    def _occupants_clicked(self, occupant, clicked_wid):
-        assert self.type == C.CHAT_GROUP
-        contact_list = self.host.contact_lists[self.profile]
-
-        # we have a click on a nick, we need to create the widget if it doesn't exists
-        self.get_or_create_private_widget(occupant.jid)
-
-        # now we select the new window
-        for contact_list in self.host.widgets.get_widgets(
-            ContactList, profiles=(self.profile,)
-        ):
-            contact_list.set_focus(occupant.jid, True)
-
-    def _append_occupants_panel(self):
-        self.chat_colums.contents.append((self.occupants_panel, ("weight", 2, False)))
-
-    def _remove_occupants_panel(self):
-        for widget, options in self.chat_colums.contents:
-            if widget is self.occupants_panel:
-                self.chat_colums.contents.remove((widget, options))
-                break
-
-    def add_game_panel(self, widget):
-        """Insert a game panel to this Chat dialog.
-
-        @param widget (Widget): the game panel
-        """
-        assert len(self.pile.contents) == 1
-        self.pile.contents.insert(0, (widget, ("weight", 1)))
-        self.pile.contents.insert(1, (urwid.Filler(urwid.Divider("-"), ("fixed", 1))))
-        self.host.redraw()
-
-    def remove_game_panel(self, widget):
-        """Remove the game panel from this Chat dialog.
-
-        @param widget (Widget): the game panel
-        """
-        assert len(self.pile.contents) == 3
-        del self.pile.contents[0]
-        self.host.redraw()
-
-    def set_subject(self, subject, wrap="space"):
-        """Set title for a group chat"""
-        quick_chat.QuickChat.set_subject(self, subject)
-        self.subj_wid = urwid.Text(
-            str(subject.replace("\n", "|") if wrap == "clip" else subject),
-            align="left" if wrap == "clip" else "center",
-            wrap=wrap,
-        )
-        self.chat_widget.header = urwid.AttrMap(self.subj_wid, "title")
-        self.host.redraw()
-
-    ## Messages
-
-    def print_messages(self, clear=True):
-        """generate message widgets
-
-        @param clear(bool): clear message before printing if true
-        """
-        if clear:
-            del self.mess_walker[:]
-        for message in self.messages.values():
-            self.appendMessage(message, minor_notifs=False)
-
-    def redraw(self):
-        """redraw all messages"""
-        for w in self.mess_walker:
-            try:
-                w.redraw()
-            except AttributeError:
-                pass
-
-    def update_history(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile="@NONE@"):
-        del self.mess_walker[:]
-        if filters and "search" in filters:
-            self.mess_walker.append(
-                urwid.Text(
-                    _("Results for searching the globbing pattern: {}").format(
-                        filters["search"]
-                    )
-                )
-            )
-            self.mess_walker.append(
-                urwid.Text(_("Type ':history <lines>' to reset the chat history"))
-            )
-        super(Chat, self).update_history(size, filters, profile)
-
-    def _on_history_printed(self):
-        """Refresh or scroll down the focus after the history is printed"""
-        self.print_messages(clear=False)
-        super(Chat, self)._on_history_printed()
-
-    def on_private_created(self, widget):
-        self.host.contact_lists[widget.profile].set_special(
-            widget.target, C.CONTACT_SPECIAL_GROUP
-        )
-
-    def on_selected(self):
-        self.focus_marker_set = False
-
-    def notify(self, contact="somebody", msg=""):
-        """Notify the user of a new message if primitivus doesn't have the focus.
-
-        @param contact (unicode): contact who wrote to the users
-        @param msg (unicode): the message that has been received
-        """
-        # FIXME: not called anymore after refactoring
-        if msg == "":
-            return
-        if self.mess_widgets.get_focus()[1] == len(self.mess_walker) - 2:
-            # we don't change focus if user is not at the bottom
-            # as that mean that he is probably watching discussion history
-            self.mess_widgets.focus_position = len(self.mess_walker) - 1
-        self.host.redraw()
-        if not self.host.x_notify.has_focus():
-            if self.type == C.CHAT_ONE2ONE:
-                self.host.x_notify.send_notification(
-                    _("Primitivus: %s is talking to you") % contact
-                )
-            elif self.nick is not None and self.nick.lower() in msg.lower():
-                self.host.x_notify.send_notification(
-                    _("Primitivus: %(user)s mentioned you in room '%(room)s'")
-                    % {"user": contact, "room": self.target}
-                )
-
-    # MENU EVENTS #
-    def on_tarot_request(self, menu):
-        # TODO: move this to plugin_misc_tarot with dynamic menu
-        if len(self.occupants) != 4:
-            self.host.show_pop_up(
-                sat_widgets.Alert(
-                    _("Can't start game"),
-                    _(
-                        "You need to be exactly 4 peoples in the room to start a Tarot game"
-                    ),
-                    ok_cb=self.host.remove_pop_up,
-                )
-            )
-        else:
-            self.host.bridge.tarot_game_create(
-                self.target, list(self.occupants), self.profile
-            )
-
-    # MISC EVENTS #
-
-    def on_delete(self):
-        # FIXME: to be checked after refactoring
-        super(Chat, self).on_delete()
-        if self.type == C.CHAT_GROUP:
-            self.host.removeListener("presence", self.presence_listener)
-
-    def on_chat_state(self, from_jid, state, profile):
-        super(Chat, self).on_chat_state(from_jid, state, profile)
-        if self.type == C.CHAT_ONE2ONE:
-            self.title_dynamic = C.CHAT_STATE_ICON[state]
-            self.host.redraw()  # FIXME: should not be necessary
-
-    def _on_subject_dialog_cb(self, button, dialog):
-        self.change_subject(dialog.text)
-        self.host.remove_pop_up(dialog)
-
-    def on_subject_dialog(self, new_subject=None):
-        dialog = sat_widgets.InputDialog(
-            _("Change title"),
-            _("Enter the new title"),
-            default_txt=new_subject if new_subject is not None else self.subject,
-        )
-        dialog.set_callback("ok", self._on_subject_dialog_cb, dialog)
-        dialog.set_callback("cancel", lambda __: self.host.remove_pop_up(dialog))
-        self.host.show_pop_up(dialog)
-
-
-quick_widgets.register(quick_chat.QuickChat, Chat)
-quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)
--- a/libervia/frontends/primitivus/config.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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/>.
-
-"""This module manage configuration specific to Primitivus"""
-
-from libervia.frontends.primitivus.constants import Const as C
-from libervia.frontends.primitivus.keys import action_key_map
-import configparser
-
-
-def apply_config(host):
-    """Parse configuration and apply found change
-
-    raise: can raise various Exceptions if configuration is not good
-    """
-    config = configparser.SafeConfigParser()
-    config.read(C.CONFIG_FILES)
-    try:
-        options = config.items(C.CONFIG_SECTION)
-    except configparser.NoSectionError:
-        options = []
-    shortcuts = {}
-    for name, value in options:
-        if name.startswith(C.CONFIG_OPT_KEY_PREFIX.lower()):
-            action = name[len(C.CONFIG_OPT_KEY_PREFIX) :].upper()
-            shortcut = value
-            if not action or not shortcut:
-                raise ValueError("Bad option: {} = {}".format(name, value))
-            shortcuts[action] = shortcut
-        if name == "disable_mouse":
-            host.loop.screen.set_mouse_tracking(False)
-
-    action_key_map.replace(shortcuts)
-    action_key_map.check_namespaces()
--- a/libervia/frontends/primitivus/constants.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-#!/usr/bin/env python3
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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 libervia.frontends.quick_frontend import constants
-
-
-class Const(constants.Const):
-
-    APP_NAME = "Libervia TUI"
-    APP_COMPONENT = "TUI"
-    APP_NAME_ALT = "Primitivus"
-    APP_NAME_FILE = "libervia_tui"
-    CONFIG_SECTION = APP_COMPONENT.lower()
-    PALETTE = [
-        ("title", "black", "light gray", "standout,underline"),
-        ("title_focus", "white,bold", "light gray", "standout,underline"),
-        ("selected", "default", "dark red"),
-        ("selected_focus", "default,bold", "dark red"),
-        ("default", "default", "default"),
-        ("default_focus", "default,bold", "default"),
-        ("cl_notifs", "yellow", "default"),
-        ("cl_notifs_focus", "yellow,bold", "default"),
-        ("cl_mention", "light red", "default"),
-        ("cl_mention_focus", "dark red,bold", "default"),
-        # Messages
-        ("date", "light gray", "default"),
-        ("my_nick", "dark red,bold", "default"),
-        ("other_nick", "dark cyan,bold", "default"),
-        ("info_msg", "yellow", "default", "bold"),
-        ("msg_lang", "dark cyan", "default"),
-        ("msg_mention", "dark red, bold", "default"),
-        ("msg_status_received", "light green, bold", "default"),
-        ("menubar", "light gray,bold", "dark red"),
-        ("menubar_focus", "light gray,bold", "dark green"),
-        ("selected_menu", "light gray,bold", "dark green"),
-        ("menuitem", "light gray,bold", "dark red"),
-        ("menuitem_focus", "light gray,bold", "dark green"),
-        ("notifs", "black,bold", "yellow"),
-        ("notifs_focus", "dark red", "yellow"),
-        ("card_neutral", "dark gray", "white", "standout,underline"),
-        ("card_neutral_selected", "dark gray", "dark green", "standout,underline"),
-        ("card_special", "brown", "white", "standout,underline"),
-        ("card_special_selected", "brown", "dark green", "standout,underline"),
-        ("card_red", "dark red", "white", "standout,underline"),
-        ("card_red_selected", "dark red", "dark green", "standout,underline"),
-        ("card_black", "black", "white", "standout,underline"),
-        ("card_black_selected", "black", "dark green", "standout,underline"),
-        ("directory", "dark cyan, bold", "default"),
-        ("directory_focus", "dark cyan, bold", "dark green"),
-        ("separator", "brown", "default"),
-        ("warning", "light red", "default"),
-        ("progress_normal", "default", "brown"),
-        ("progress_complete", "default", "dark green"),
-        ("show_disconnected", "dark gray", "default"),
-        ("show_normal", "default", "default"),
-        ("show_normal_focus", "default, bold", "default"),
-        ("show_chat", "dark green", "default"),
-        ("show_chat_focus", "dark green, bold", "default"),
-        ("show_away", "brown", "default"),
-        ("show_away_focus", "brown, bold", "default"),
-        ("show_dnd", "dark red", "default"),
-        ("show_dnd_focus", "dark red, bold", "default"),
-        ("show_xa", "dark red", "default"),
-        ("show_xa_focus", "dark red, bold", "default"),
-        ("resource", "light blue", "default"),
-        ("resource_main", "dark blue", "default"),
-        ("status", "yellow", "default"),
-        ("status_focus", "yellow, bold", "default"),
-        ("param_selected", "default, bold", "dark red"),
-        ("table_selected", "default, bold", "default"),
-    ]
-    PRESENCE = {
-        "unavailable": ("⨯", "show_disconnected"),
-        "": ("✔", "show_normal"),
-        "chat": ("✆", "show_chat"),
-        "away": ("✈", "show_away"),
-        "dnd": ("✖", "show_dnd"),
-        "xa": ("☄", "show_xa"),
-    }
-    LOG_OPT_SECTION = APP_NAME.lower()
-    LOG_OPT_OUTPUT = (
-        "output",
-        constants.Const.LOG_OPT_OUTPUT_SEP + constants.Const.LOG_OPT_OUTPUT_MEMORY,
-    )
-    CONFIG_OPT_KEY_PREFIX = "KEY_"
-
-    MENU_ID_MAIN = "MAIN_MENU"
-    MENU_ID_WIDGET = "WIDGET_MENU"
-
-    MODE_NORMAL = "NORMAL"
-    MODE_INSERTION = "INSERTION"
-    MODE_COMMAND = "COMMAND"
-
-    GROUP_DATA_FOLDED = "folded"
--- a/libervia/frontends/primitivus/contact_list.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,364 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
-import urwid
-from urwid_satext import sat_widgets
-from libervia.frontends.quick_frontend.quick_contact_list import QuickContactList
-from libervia.frontends.primitivus.status import StatusBar
-from libervia.frontends.primitivus.constants import Const as C
-from libervia.frontends.primitivus.keys import action_key_map as a_key
-from libervia.frontends.primitivus.widget import PrimitivusWidget
-from libervia.frontends.tools import jid
-from libervia.backend.core import log as logging
-
-log = logging.getLogger(__name__)
-from libervia.frontends.quick_frontend import quick_widgets
-
-
-class ContactList(PrimitivusWidget, QuickContactList):
-    PROFILES_MULTIPLE = False
-    PROFILES_ALLOW_NONE = False
-    signals = ["click", "change"]
-    # FIXME: Only single profile is managed so far
-
-    def __init__(
-        self, host, target, on_click=None, on_change=None, user_data=None, profiles=None
-    ):
-        QuickContactList.__init__(self, host, profiles)
-        self.contact_list = self.host.contact_lists[self.profile]
-
-        # we now build the widget
-        self.status_bar = StatusBar(host)
-        self.frame = sat_widgets.FocusFrame(self._build_list(), None, self.status_bar)
-        PrimitivusWidget.__init__(self, self.frame, _("Contacts"))
-        if on_click:
-            urwid.connect_signal(self, "click", on_click, user_data)
-        if on_change:
-            urwid.connect_signal(self, "change", on_change, user_data)
-        self.host.addListener("notification", self.on_notification, [self.profile])
-        self.host.addListener("notificationsClear", self.on_notification, [self.profile])
-        self.post_init()
-
-    def update(self, entities=None, type_=None, profile=None):
-        """Update display, keep focus"""
-        # FIXME: full update is done each time, must handle entities, type_ and profile
-        widget, position = self.frame.body.get_focus()
-        self.frame.body = self._build_list()
-        if position:
-            try:
-                self.frame.body.focus_position = position
-            except IndexError:
-                pass
-        self._invalidate()
-        self.host.redraw()  # FIXME: check if can be avoided
-
-    def keypress(self, size, key):
-        # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
-        #        and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element
-        if key in sat_widgets.FOCUS_KEYS:
-            if (
-                key == a_key["FOCUS_SWITCH"]
-                or (key == a_key["FOCUS_UP"] and self.frame.focus_position == "body")
-                or (key == a_key["FOCUS_DOWN"] and self.frame.focus_position == "footer")
-            ):
-                return key
-        if key == a_key["STATUS_HIDE"]:  # user wants to (un)hide contacts' statuses
-            self.contact_list.show_status = not self.contact_list.show_status
-            self.update()
-        elif (
-            key == a_key["DISCONNECTED_HIDE"]
-        ):  # user wants to (un)hide disconnected contacts
-            self.host.bridge.param_set(
-                C.SHOW_OFFLINE_CONTACTS,
-                C.bool_const(not self.contact_list.show_disconnected),
-                "General",
-                profile_key=self.profile,
-            )
-        elif key == a_key["RESOURCES_HIDE"]:  # user wants to (un)hide contacts resources
-            self.contact_list.show_resources(not self.contact_list.show_resources)
-            self.update()
-        return super(ContactList, self).keypress(size, key)
-
-    # QuickWidget methods
-
-    @staticmethod
-    def get_widget_hash(target, profiles):
-        profiles = sorted(profiles)
-        return tuple(profiles)
-
-    # modify the contact list
-
-    def set_focus(self, text, select=False):
-        """give focus to the first element that matches the given text. You can also
-        pass in text a libervia.frontends.tools.jid.JID (it's a subclass of unicode).
-
-        @param text: contact group name, contact or muc userhost, muc private dialog jid
-        @param select: if True, the element is also clicked
-        """
-        idx = 0
-        for widget in self.frame.body.body:
-            try:
-                if isinstance(widget, sat_widgets.ClickableText):
-                    # contact group
-                    value = widget.get_value()
-                elif isinstance(widget, sat_widgets.SelectableText):
-                    # contact or muc
-                    value = widget.data
-                else:
-                    # Divider instance
-                    continue
-                # there's sometimes a leading space
-                if text.strip() == value.strip():
-                    self.frame.body.focus_position = idx
-                    if select:
-                        self._contact_clicked(False, widget, True)
-                    return
-            except AttributeError:
-                pass
-            idx += 1
-
-        log.debug("Not element found for {} in set_focus".format(text))
-
-    # events
-
-    def _group_clicked(self, group_wid):
-        group = group_wid.get_value()
-        data = self.contact_list.get_group_data(group)
-        data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False)
-        self.set_focus(group)
-        self.update()
-
-    def _contact_clicked(self, use_bare_jid, contact_wid, selected):
-        """Method called when a contact is clicked
-
-        @param use_bare_jid: True if use_bare_jid is set in self._build_entity_widget.
-        @param contact_wid: widget of the contact, must have the entity set in data attribute
-        @param selected: boolean returned by the widget, telling if it is selected
-        """
-        entity = contact_wid.data
-        self.host.mode_hint(C.MODE_INSERTION)
-        self._emit("click", entity)
-
-    def on_notification(self, entity, notif, profile):
-        notifs = list(self.host.get_notifs(C.ENTITY_ALL, profile=self.profile))
-        if notifs:
-            self.title_dynamic = "({})".format(len(notifs))
-        else:
-            self.title_dynamic = None
-        self.host.redraw()  # FIXME: should not be necessary
-
-    # Methods to build the widget
-
-    def _build_entity_widget(
-        self,
-        entity,
-        keys=None,
-        use_bare_jid=False,
-        with_notifs=True,
-        with_show_attr=True,
-        markup_prepend=None,
-        markup_append=None,
-        special=False,
-    ):
-        """Build one contact markup data
-
-        @param entity (jid.JID): entity to build
-        @param keys (iterable): value to markup, in preferred order.
-            The first available key will be used.
-            If key starts with "cache_", it will be checked in cache,
-            else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
-            If nothing full or keys is None, full entity is used.
-        @param use_bare_jid (bool): if True, use bare jid for selected comparisons
-        @param with_notifs (bool): if True, show notification count
-        @param with_show_attr (bool): if True, show color corresponding to presence status
-        @param markup_prepend (list): markup to prepend to the generated one before building the widget
-        @param markup_append (list): markup to append to the generated one before building the widget
-        @param special (bool): True if entity is a special one
-        @return (list): markup data are expected by Urwid text widgets
-        """
-        markup = []
-        if use_bare_jid:
-            selected = {entity.bare for entity in self.contact_list._selected}
-        else:
-            selected = self.contact_list._selected
-        if keys is None:
-            entity_txt = entity
-        else:
-            cache = self.contact_list.getCache(entity)
-            for key in keys:
-                if key.startswith("cache_"):
-                    entity_txt = cache.get(key[6:])
-                else:
-                    entity_txt = getattr(entity, key)
-                if entity_txt:
-                    break
-            if not entity_txt:
-                entity_txt = entity
-
-        if with_show_attr:
-            show = self.contact_list.getCache(entity, C.PRESENCE_SHOW, default=None)
-            if show is None:
-                show = C.PRESENCE_UNAVAILABLE
-            show_icon, entity_attr = C.PRESENCE.get(show, ("", "default"))
-            markup.insert(0, "{} ".format(show_icon))
-        else:
-            entity_attr = "default"
-
-        notifs = list(
-            self.host.get_notifs(entity, exact_jid=special, profile=self.profile)
-        )
-        mentions = list(
-                self.host.get_notifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile)
-            )
-        if notifs or mentions:
-            attr = 'cl_mention' if mentions else 'cl_notifs'
-            header = [(attr, "({})".format(len(notifs) + len(mentions))), " "]
-        else:
-            header = ""
-
-        markup.append((entity_attr, entity_txt))
-        if markup_prepend:
-            markup.insert(0, markup_prepend)
-        if markup_append:
-            markup.extend(markup_append)
-
-        widget = sat_widgets.SelectableText(
-            markup, selected=entity in selected, header=header
-        )
-        widget.data = entity
-        widget.comp = entity_txt.lower()  # value to use for sorting
-        urwid.connect_signal(
-            widget, "change", self._contact_clicked, user_args=[use_bare_jid]
-        )
-        return widget
-
-    def _build_entities(self, content, entities):
-        """Add entity representation in widget list
-
-        @param content: widget list, e.g. SimpleListWalker
-        @param entities (iterable): iterable of JID to display
-        """
-        if not entities:
-            return
-        widgets = []  # list of built widgets
-
-        for entity in entities:
-            if (
-                entity in self.contact_list._specials
-                or not self.contact_list.entity_visible(entity)
-            ):
-                continue
-            markup_extra = []
-            if self.contact_list.show_resources:
-                for resource in self.contact_list.getCache(entity, C.CONTACT_RESOURCES):
-                    resource_disp = (
-                        "resource_main"
-                        if resource
-                        == self.contact_list.getCache(entity, C.CONTACT_MAIN_RESOURCE)
-                        else "resource",
-                        "\n  " + resource,
-                    )
-                    markup_extra.append(resource_disp)
-                    if self.contact_list.show_status:
-                        status = self.contact_list.getCache(
-                            jid.JID("%s/%s" % (entity, resource)), "status", default=None
-                        )
-                        status_disp = ("status", "\n    " + status) if status else ""
-                        markup_extra.append(status_disp)
-
-            else:
-                if self.contact_list.show_status:
-                    status = self.contact_list.getCache(entity, "status", default=None)
-                    status_disp = ("status", "\n  " + status) if status else ""
-                    markup_extra.append(status_disp)
-            widget = self._build_entity_widget(
-                entity,
-                ("cache_nick", "cache_name", "node"),
-                use_bare_jid=True,
-                markup_append=markup_extra,
-            )
-            widgets.append(widget)
-
-        widgets.sort(key=lambda widget: widget.comp)
-
-        for widget in widgets:
-            content.append(widget)
-
-    def _build_specials(self, content):
-        """Build the special entities"""
-        specials = sorted(self.contact_list.get_specials())
-        current = None
-        for entity in specials:
-            if current is not None and current.bare == entity.bare:
-                # nested entity (e.g. MUC private conversations)
-                widget = self._build_entity_widget(
-                    entity, ("resource",), markup_prepend="  ", special=True
-                )
-            else:
-                # the special widgets
-                if entity.resource:
-                    widget = self._build_entity_widget(entity, ("resource",), special=True)
-                else:
-                    widget = self._build_entity_widget(
-                        entity,
-                        ("cache_nick", "cache_name", "node"),
-                        with_show_attr=False,
-                        special=True,
-                    )
-            content.append(widget)
-
-    def _build_list(self):
-        """Build the main contact list widget"""
-        content = urwid.SimpleListWalker([])
-
-        self._build_specials(content)
-        if self.contact_list._specials:
-            content.append(urwid.Divider("="))
-
-        groups = list(self.contact_list._groups)
-        groups.sort(key=lambda x: x.lower() if x else '')
-        for group in groups:
-            data = self.contact_list.get_group_data(group)
-            folded = data.get(C.GROUP_DATA_FOLDED, False)
-            jids = list(data["jids"])
-            if group is not None and (
-                self.contact_list.any_entity_visible(jids)
-                or self.contact_list.show_empty_groups
-            ):
-                header = "[-]" if not folded else "[+]"
-                widget = sat_widgets.ClickableText(group, header=header + " ")
-                content.append(widget)
-                urwid.connect_signal(widget, "click", self._group_clicked)
-            if not folded:
-                self._build_entities(content, jids)
-        not_in_roster = (
-            set(self.contact_list._cache)
-            .difference(self.contact_list._roster)
-            .difference(self.contact_list._specials)
-            .difference((self.contact_list.whoami.bare,))
-        )
-        if not_in_roster:
-            content.append(urwid.Divider("-"))
-            self._build_entities(content, not_in_roster)
-
-        return urwid.ListBox(content)
-
-
-quick_widgets.register(QuickContactList, ContactList)
--- a/libervia/frontends/primitivus/game_tarot.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,397 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
-import urwid
-from urwid_satext import sat_widgets
-from libervia.frontends.tools.games import TarotCard
-from libervia.frontends.quick_frontend.quick_game_tarot import QuickTarotGame
-from libervia.frontends.primitivus import xmlui
-from libervia.frontends.primitivus.keys import action_key_map as a_key
-
-
-class CardDisplayer(urwid.Text):
-    """Show a card"""
-
-    signals = ["click"]
-
-    def __init__(self, card):
-        self.__selected = False
-        self.card = card
-        urwid.Text.__init__(self, card.get_attr_text())
-
-    def selectable(self):
-        return True
-
-    def keypress(self, size, key):
-        if key == a_key["CARD_SELECT"]:
-            self.select(not self.__selected)
-            self._emit("click")
-        return key
-
-    def mouse_event(self, size, event, button, x, y, focus):
-        if urwid.is_mouse_event(event) and button == 1:
-            self.select(not self.__selected)
-            self._emit("click")
-            return True
-
-        return False
-
-    def select(self, state=True):
-        self.__selected = state
-        attr, txt = self.card.get_attr_text()
-        if self.__selected:
-            attr += "_selected"
-        self.set_text((attr, txt))
-        self._invalidate()
-
-    def is_selected(self):
-        return self.__selected
-
-    def get_card(self):
-        return self.card
-
-    def render(self, size, focus=False):
-        canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus))
-        if focus:
-            canvas.set_cursor((0, 0))
-        return canvas
-
-
-class Hand(urwid.WidgetWrap):
-    """Used to display several cards, and manage a hand"""
-
-    signals = ["click"]
-
-    def __init__(self, hand=[], selectable=False, on_click=None, user_data=None):
-        """@param hand: list of Card"""
-        self.__selectable = selectable
-        self.columns = urwid.Columns([], dividechars=1)
-        if on_click:
-            urwid.connect_signal(self, "click", on_click, user_data)
-        if hand:
-            self.update(hand)
-        urwid.WidgetWrap.__init__(self, self.columns)
-
-    def selectable(self):
-        return self.__selectable
-
-    def keypress(self, size, key):
-
-        if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]:
-            return self.columns.keypress(size, key)
-        else:
-            # No card displayed, we still have to manage the clicks
-            if key == a_key["CARD_SELECT"]:
-                self._emit("click", None)
-            return key
-
-    def get_selected(self):
-        """Return a list of selected cards"""
-        _selected = []
-        for wid in self.columns.widget_list:
-            if isinstance(wid, CardDisplayer) and wid.is_selected():
-                _selected.append(wid.get_card())
-        return _selected
-
-    def update(self, hand):
-        """Update the hand displayed in this widget
-        @param hand: list of Card"""
-        try:
-            del self.columns.widget_list[:]
-            del self.columns.column_types[:]
-        except IndexError:
-            pass
-        self.columns.contents.append((urwid.Text(""), ("weight", 1, False)))
-        for card in hand:
-            widget = CardDisplayer(card)
-            self.columns.widget_list.append(widget)
-            self.columns.column_types.append(("fixed", 3))
-            urwid.connect_signal(widget, "click", self.__on_click)
-        self.columns.contents.append((urwid.Text(""), ("weight", 1, False)))
-        self.columns.focus_position = 1
-
-    def __on_click(self, card_wid):
-        self._emit("click", card_wid)
-
-
-class Card(TarotCard):
-    """This class is used to represent a card, logically
-    and give a text representation with attributes"""
-
-    SIZE = 3  # size of a displayed card
-
-    def __init__(self, suit, value):
-        """@param file: path of the PNG file"""
-        TarotCard.__init__(self, (suit, value))
-
-    def get_attr_text(self):
-        """return text representation of the card with attributes"""
-        try:
-            value = "%02i" % int(self.value)
-        except ValueError:
-            value = self.value[0].upper() + self.value[1]
-        if self.suit == "atout":
-            if self.value == "excuse":
-                suit = "c"
-            else:
-                suit = "A"
-            color = "neutral"
-        elif self.suit == "pique":
-            suit = "♠"
-            color = "black"
-        elif self.suit == "trefle":
-            suit = "♣"
-            color = "black"
-        elif self.suit == "coeur":
-            suit = "♥"
-            color = "red"
-        elif self.suit == "carreau":
-            suit = "♦"
-            color = "red"
-        if self.bout:
-            color = "special"
-        return ("card_%s" % color, "%s%s" % (value, suit))
-
-    def get_widget(self):
-        """Return a widget representing the card"""
-        return CardDisplayer(self)
-
-
-class Table(urwid.FlowWidget):
-    """Represent the cards currently on the table"""
-
-    def __init__(self):
-        self.top = self.left = self.bottom = self.right = None
-
-    def put_card(self, location, card):
-        """Put a card on the table
-        @param location: where to put the card (top, left, bottom or right)
-        @param card: Card to play or None"""
-        assert location in ["top", "left", "bottom", "right"]
-        assert isinstance(card, Card) or card == None
-        if [getattr(self, place) for place in ["top", "left", "bottom", "right"]].count(
-            None
-        ) == 0:
-            # If the table is full of card, we remove them
-            self.top = self.left = self.bottom = self.right = None
-        setattr(self, location, card)
-        self._invalidate()
-
-    def rows(self, size, focus=False):
-        return self.display_widget(size, focus).rows(size, focus)
-
-    def render(self, size, focus=False):
-        return self.display_widget(size, focus).render(size, focus)
-
-    def display_widget(self, size, focus):
-        cards = {}
-        max_col, = size
-        separator = " - "
-        margin = max((max_col - Card.SIZE) / 2, 0) * " "
-        margin_center = max((max_col - Card.SIZE * 2 - len(separator)) / 2, 0) * " "
-        for location in ["top", "left", "bottom", "right"]:
-            card = getattr(self, location)
-            cards[location] = card.get_attr_text() if card else Card.SIZE * " "
-        render_wid = [
-            urwid.Text([margin, cards["top"]]),
-            urwid.Text([margin_center, cards["left"], separator, cards["right"]]),
-            urwid.Text([margin, cards["bottom"]]),
-        ]
-        return urwid.Pile(render_wid)
-
-
-class TarotGame(QuickTarotGame, urwid.WidgetWrap):
-    """Widget for card games"""
-
-    def __init__(self, parent, referee, players):
-        QuickTarotGame.__init__(self, parent, referee, players)
-        self.load_cards()
-        self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), "center")])
-        # self.parent.host.debug()
-        self.table = Table()
-        self.center = urwid.Columns(
-            [
-                ("fixed", len(self.left_nick), urwid.Filler(urwid.Text(self.left_nick))),
-                urwid.Filler(self.table),
-                (
-                    "fixed",
-                    len(self.right_nick),
-                    urwid.Filler(urwid.Text(self.right_nick)),
-                ),
-            ]
-        )
-        """urwid.Pile([urwid.Padding(self.top_card_wid,'center'),
-                             urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)),
-                                            urwid.Padding(self.center_cards_wid,'center'),
-                                            ('fixed',len(self.right_nick),urwid.Text(self.right_nick))
-                                           ]),
-                             urwid.Padding(self.bottom_card_wid,'center')
-                             ])"""
-        self.hand_wid = Hand(selectable=True, on_click=self.on_click)
-        self.main_frame = urwid.Frame(
-            self.center, header=self.top, footer=self.hand_wid, focus_part="footer"
-        )
-        urwid.WidgetWrap.__init__(self, self.main_frame)
-        self.parent.host.bridge.tarot_game_ready(
-            self.player_nick, referee, self.parent.profile
-        )
-
-    def load_cards(self):
-        """Load all the cards in memory"""
-        QuickTarotGame.load_cards(self)
-        for value in list(map(str, list(range(1, 22)))) + ["excuse"]:
-            card = Card("atout", value)
-            self.cards[card.suit, card.value] = card
-            self.deck.append(card)
-        for suit in ["pique", "coeur", "carreau", "trefle"]:
-            for value in list(map(str, list(range(1, 11)))) + ["valet", "cavalier", "dame", "roi"]:
-                card = Card(suit, value)
-                self.cards[card.suit, card.value] = card
-                self.deck.append(card)
-
-    def tarot_game_new_handler(self, hand):
-        """Start a new game, with given hand"""
-        if hand is []:  # reset the display after the scores have been showed
-            self.reset_round()
-            for location in ["top", "left", "bottom", "right"]:
-                self.table.put_card(location, None)
-            self.parent.host.redraw()
-            self.parent.host.bridge.tarot_game_ready(
-                self.player_nick, self.referee, self.parent.profile
-            )
-            return
-        QuickTarotGame.tarot_game_new_handler(self, hand)
-        self.hand_wid.update(self.hand)
-        self.parent.host.redraw()
-
-    def tarot_game_choose_contrat_handler(self, xml_data):
-        """Called when the player has to select his contrat
-        @param xml_data: SàT xml representation of the form"""
-        form = xmlui.create(
-            self.parent.host,
-            xml_data,
-            title=_("Please choose your contrat"),
-            flags=["NO_CANCEL"],
-            profile=self.parent.profile,
-        )
-        form.show(valign="top")
-
-    def tarot_game_show_cards_handler(self, game_stage, cards, data):
-        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
-        QuickTarotGame.tarot_game_show_cards_handler(self, game_stage, cards, data)
-        self.center.widget_list[1] = urwid.Filler(Hand(self.to_show))
-        self.parent.host.redraw()
-
-    def tarot_game_your_turn_handler(self):
-        QuickTarotGame.tarot_game_your_turn_handler(self)
-
-    def tarot_game_score_handler(self, xml_data, winners, loosers):
-        """Called when the round is over, display the scores
-        @param xml_data: SàT xml representation of the form"""
-        if not winners and not loosers:
-            title = _("Draw game")
-        else:
-            title = _("You win \o/") if self.player_nick in winners else _("You loose :(")
-        form = xmlui.create(
-            self.parent.host,
-            xml_data,
-            title=title,
-            flags=["NO_CANCEL"],
-            profile=self.parent.profile,
-        )
-        form.show()
-
-    def tarot_game_invalid_cards_handler(self, phase, played_cards, invalid_cards):
-        """Invalid cards have been played
-        @param phase: phase of the game
-        @param played_cards: all the cards played
-        @param invalid_cards: cards which are invalid"""
-        QuickTarotGame.tarot_game_invalid_cards_handler(
-            self, phase, played_cards, invalid_cards
-        )
-        self.hand_wid.update(self.hand)
-        if self._autoplay == None:  # No dialog if there is autoplay
-            self.parent.host.bar_notify(_("Cards played are invalid !"))
-        self.parent.host.redraw()
-
-    def tarot_game_cards_played_handler(self, player, cards):
-        """A card has been played by player"""
-        QuickTarotGame.tarot_game_cards_played_handler(self, player, cards)
-        self.table.put_card(self.get_player_location(player), self.played[player])
-        self._checkState()
-        self.parent.host.redraw()
-
-    def _checkState(self):
-        if isinstance(
-            self.center.widget_list[1].original_widget, Hand
-        ):  # if we have a hand displayed
-            self.center.widget_list[1] = urwid.Filler(
-                self.table
-            )  # we show again the table
-            if self.state == "chien":
-                self.to_show = []
-                self.state = "wait"
-            elif self.state == "wait_for_ecart":
-                self.state = "ecart"
-                self.hand.extend(self.to_show)
-                self.hand.sort()
-                self.to_show = []
-                self.hand_wid.update(self.hand)
-
-    ##EVENTS##
-    def on_click(self, hand, card_wid):
-        """Called when user do an action on the hand"""
-        if not self.state in ["play", "ecart", "wait_for_ecart"]:
-            # it's not our turn, we ignore the click
-            card_wid.select(False)
-            return
-        self._checkState()
-        if self.state == "ecart":
-            if len(self.hand_wid.get_selected()) == 6:
-                pop_up_widget = sat_widgets.ConfirmDialog(
-                    _("Do you put these cards in chien ?"),
-                    yes_cb=self.on_ecart_done,
-                    no_cb=self.parent.host.remove_pop_up,
-                )
-                self.parent.host.show_pop_up(pop_up_widget)
-        elif self.state == "play":
-            card = card_wid.get_card()
-            self.parent.host.bridge.tarot_game_play_cards(
-                self.player_nick,
-                self.referee,
-                [(card.suit, card.value)],
-                self.parent.profile,
-            )
-            self.hand.remove(card)
-            self.hand_wid.update(self.hand)
-            self.state = "wait"
-
-    def on_ecart_done(self, button):
-        """Called when player has finished his écart"""
-        ecart = []
-        for card in self.hand_wid.get_selected():
-            ecart.append((card.suit, card.value))
-            self.hand.remove(card)
-        self.hand_wid.update(self.hand)
-        self.parent.host.bridge.tarot_game_play_cards(
-            self.player_nick, self.referee, ecart, self.parent.profile
-        )
-        self.state = "wait"
-        self.parent.host.remove_pop_up()
--- a/libervia/frontends/primitivus/keys.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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/>.
-
-"""This file manage the action <=> key map"""
-
-from urwid_satext.keys import action_key_map
-
-
-action_key_map.update(
-    {
-        # Edit bar
-        ("edit", "MODE_INSERTION"): "i",
-        ("edit", "MODE_COMMAND"): ":",
-        ("edit", "HISTORY_PREV"): "up",
-        ("edit", "HISTORY_NEXT"): "down",
-        # global
-        ("global", "MENU_HIDE"): "meta m",
-        ("global", "NOTIFICATION_NEXT"): "ctrl n",
-        ("global", "OVERLAY_HIDE"): "ctrl s",
-        ("global", "DEBUG"): "ctrl d",
-        ("global", "CONTACTS_HIDE"): "f2",
-        (
-            "global",
-            "REFRESH_SCREEN",
-        ): "ctrl l",  # ctrl l is used by Urwid to refresh screen
-        # global menu
-        ("menu_global", "APP_QUIT"): "ctrl x",
-        ("menu_global", "ROOM_JOIN"): "meta j",
-        # primitivus widgets
-        ("primitivus_widget", "DECORATION_HIDE"): "meta l",
-        # contact list
-        ("contact_list", "STATUS_HIDE"): "meta s",
-        ("contact_list", "DISCONNECTED_HIDE"): "meta d",
-        ("contact_list", "RESOURCES_HIDE"): "meta r",
-        # chat panel
-        ("chat_panel", "OCCUPANTS_HIDE"): "meta p",
-        ("chat_panel", "TIMESTAMP_HIDE"): "meta t",
-        ("chat_panel", "SHORT_NICKNAME"): "meta n",
-        ("chat_panel", "SUBJECT_SWITCH"): "meta s",
-        ("chat_panel", "GOTO_BOTTOM"): "G",
-        # card game
-        ("card_game", "CARD_SELECT"): " ",
-        # focus
-        ("focus", "FOCUS_EXTRA"): "ctrl f",
-    }
-)
-
-
-action_key_map.set_close_namespaces(tuple(), ("global", "focus", "menu_global"))
-action_key_map.check_namespaces()
--- a/libervia/frontends/primitivus/notify.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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/>.
-
-import dbus
-
-
-class Notify(object):
-    """Used to send notification and detect if we have focus"""
-
-    def __init__(self):
-
-        # X11 stuff
-        self.display = None
-        self.X11_id = -1
-
-        try:
-            from Xlib import display as X_display
-
-            self.display = X_display.Display()
-            self.X11_id = self.get_focus()
-        except:
-            pass
-
-        # Now we try to connect to Freedesktop D-Bus API
-        try:
-            bus = dbus.SessionBus()
-            db_object = bus.get_object(
-                "org.freedesktop.Notifications",
-                "/org/freedesktop/Notifications",
-                follow_name_owner_changes=True,
-            )
-            self.freedesktop_int = dbus.Interface(
-                db_object, dbus_interface="org.freedesktop.Notifications"
-            )
-        except:
-            self.freedesktop_int = None
-
-    def get_focus(self):
-        if not self.display:
-            return 0
-        return self.display.get_input_focus().focus.id
-
-    def has_focus(self):
-        return (self.get_focus() == self.X11_id) if self.display else True
-
-    def use_x11(self):
-        return bool(self.display)
-
-    def send_notification(self, summ_mess, body_mess=""):
-        """Send notification to the user if possible"""
-        # TODO: check options before sending notifications
-        if self.freedesktop_int:
-            self.send_fd_notification(summ_mess, body_mess)
-
-    def send_fd_notification(self, summ_mess, body_mess=""):
-        """Send notification with the FreeDesktop D-Bus API"""
-        if self.freedesktop_int:
-            app_name = "Primitivus"
-            replaces_id = 0
-            app_icon = ""
-            summary = summ_mess
-            body = body_mess
-            actions = dbus.Array(signature="s")
-            hints = dbus.Dictionary(signature="sv")
-            expire_timeout = -1
-
-            self.freedesktop_int.Notify(
-                app_name,
-                replaces_id,
-                app_icon,
-                summary,
-                body,
-                actions,
-                hints,
-                expire_timeout,
-            )
--- a/libervia/frontends/primitivus/profile_manager.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,228 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
-from libervia.backend.core import log as logging
-
-log = logging.getLogger(__name__)
-from libervia.frontends.quick_frontend.quick_profile_manager import QuickProfileManager
-from libervia.frontends.primitivus.constants import Const as C
-from libervia.frontends.primitivus.keys import action_key_map as a_key
-from urwid_satext import sat_widgets
-import urwid
-
-
-class ProfileManager(QuickProfileManager, urwid.WidgetWrap):
-    def __init__(self, host, autoconnect=None):
-        QuickProfileManager.__init__(self, host, autoconnect)
-
-        # login & password box must be created before list because of on_profile_change
-        self.login_wid = sat_widgets.AdvancedEdit(_("Login:"), align="center")
-        self.pass_wid = sat_widgets.Password(_("Password:"), align="center")
-
-        style = ["no_first_select"]
-        profiles = host.bridge.profiles_list_get()
-        profiles.sort()
-        self.list_profile = sat_widgets.List(
-            profiles, style=style, align="center", on_change=self.on_profile_change
-        )
-
-        # new & delete buttons
-        buttons = [
-            urwid.Button(_("New"), self.on_new_profile),
-            urwid.Button(_("Delete"), self.on_delete_profile),
-        ]
-        buttons_flow = urwid.GridFlow(
-            buttons,
-            max([len(button.get_label()) for button in buttons]) + 4,
-            1,
-            1,
-            "center",
-        )
-
-        # second part: login information:
-        divider = urwid.Divider("-")
-
-        # connect button
-        connect_button = sat_widgets.CustomButton(
-            _("Connect"), self.on_connect_profiles, align="center"
-        )
-
-        # we now build the widget
-        list_walker = urwid.SimpleFocusListWalker(
-            [
-                buttons_flow,
-                self.list_profile,
-                divider,
-                self.login_wid,
-                self.pass_wid,
-                connect_button,
-            ]
-        )
-        frame_body = urwid.ListBox(list_walker)
-        frame = urwid.Frame(
-            frame_body,
-            urwid.AttrMap(urwid.Text(_("Profile Manager"), align="center"), "title"),
-        )
-        self.main_widget = urwid.LineBox(frame)
-        urwid.WidgetWrap.__init__(self, self.main_widget)
-
-        self.go(autoconnect)
-
-    def keypress(self, size, key):
-        if key == a_key["APP_QUIT"]:
-            self.host.on_exit()
-            raise urwid.ExitMainLoop()
-        elif key in (a_key["FOCUS_UP"], a_key["FOCUS_DOWN"]):
-            focus_diff = 1 if key == a_key["FOCUS_DOWN"] else -1
-            list_box = self.main_widget.base_widget.body
-            current_focus = list_box.body.get_focus()[1]
-            if current_focus is None:
-                return
-            while True:
-                current_focus += focus_diff
-                if current_focus < 0 or current_focus >= len(list_box.body):
-                    break
-                if list_box.body[current_focus].selectable():
-                    list_box.set_focus(
-                        current_focus, "above" if focus_diff == 1 else "below"
-                    )
-                    list_box._invalidate()
-                    return
-        return super(ProfileManager, self).keypress(size, key)
-
-    def cancel_dialog(self, button):
-        self.host.remove_pop_up()
-
-    def new_profile(self, button, edit):
-        """Create the profile"""
-        name = edit.get_edit_text()
-        self.host.bridge.profile_create(
-            name,
-            callback=lambda: self.new_profile_created(name),
-            errback=self.profile_creation_failure,
-        )
-
-    def new_profile_created(self, profile):
-        # new profile will be selected, and a selected profile assume the session is started
-        self.host.bridge.profile_start_session(
-            "",
-            profile,
-            callback=lambda __: self.new_profile_session_started(profile),
-            errback=self.profile_creation_failure,
-        )
-
-    def new_profile_session_started(self, profile):
-        self.host.remove_pop_up()
-        self.refill_profiles()
-        self.list_profile.select_value(profile)
-        self.current.profile = profile
-        self.get_connection_params(profile)
-        self.host.redraw()
-
-    def profile_creation_failure(self, reason):
-        self.host.remove_pop_up()
-        message = self._get_error_message(reason)
-        self.host.alert(_("Can't create profile"), message)
-
-    def delete_profile(self, button):
-        self._delete_profile()
-        self.host.remove_pop_up()
-
-    def on_new_profile(self, e):
-        pop_up_widget = sat_widgets.InputDialog(
-            _("New profile"),
-            _("Please enter a new profile name"),
-            cancel_cb=self.cancel_dialog,
-            ok_cb=self.new_profile,
-        )
-        self.host.show_pop_up(pop_up_widget)
-
-    def on_delete_profile(self, e):
-        if self.current.profile:
-            pop_up_widget = sat_widgets.ConfirmDialog(
-                _("Are you sure you want to delete the profile {} ?").format(
-                    self.current.profile
-                ),
-                no_cb=self.cancel_dialog,
-                yes_cb=self.delete_profile,
-            )
-            self.host.show_pop_up(pop_up_widget)
-
-    def on_connect_profiles(self, button):
-        """Connect the profiles and start the main widget
-
-        @param button: the connect button
-        """
-        self._on_connect_profiles()
-
-    def reset_fields(self):
-        """Set profile to None, and reset fields"""
-        super(ProfileManager, self).reset_fields()
-        self.list_profile.unselect_all(invisible=True)
-
-    def set_profiles(self, profiles):
-        """Update the list of profiles"""
-        self.list_profile.change_values(profiles)
-        self.host.redraw()
-
-    def get_profiles(self):
-        return self.list_profile.get_selected_values()
-
-    def get_jid(self):
-        return self.login_wid.get_edit_text()
-
-    def getPassword(self):
-        return self.pass_wid.get_edit_text()
-
-    def set_jid(self, jid_):
-        self.login_wid.set_edit_text(jid_)
-        self.current.login = jid_
-        self.host.redraw()  # FIXME: redraw should be avoided
-
-    def set_password(self, password):
-        self.pass_wid.set_edit_text(password)
-        self.current.password = password
-        self.host.redraw()
-
-    def on_profile_change(self, list_wid, widget=None, selected=None):
-        """This is called when a profile is selected in the profile list.
-
-        @param list_wid: the List widget who sent the event
-        """
-        self.update_connection_params()
-        focused = list_wid.focus
-        selected = focused.get_state() if focused is not None else False
-        if not selected:  # profile was just unselected
-            return
-        focused.set_state(
-            False, invisible=True
-        )  # we don't want the widget to be selected until we are sure we can access it
-
-        def authenticate_cb(data, cb_id, profile):
-            if C.bool(data.pop("validated", C.BOOL_FALSE)):
-                self.current.profile = profile
-                focused.set_state(True, invisible=True)
-                self.get_connection_params(profile)
-                self.host.redraw()
-            self.host.action_manager(data, callback=authenticate_cb, profile=profile)
-
-        self.host.action_launch(
-            C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text
-        )
--- a/libervia/frontends/primitivus/progress.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
-import urwid
-from urwid_satext import sat_widgets
-from libervia.frontends.quick_frontend import quick_widgets
-
-
-class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget):
-    PROFILES_ALLOW_NONE = True
-
-    def __init__(self, host, target, profiles):
-        assert target is None and profiles is None
-        quick_widgets.QuickWidget.__init__(self, host, target)
-        self.host = host
-        self.progress_list = urwid.SimpleListWalker([])
-        self.progress_dict = {}
-        listbox = urwid.ListBox(self.progress_list)
-        buttons = []
-        buttons.append(sat_widgets.CustomButton(_("Clear progress list"), self._on_clear))
-        max_len = max([button.get_size() for button in buttons])
-        buttons_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center")
-        main_wid = sat_widgets.FocusFrame(listbox, footer=buttons_wid)
-        urwid.WidgetWrap.__init__(self, main_wid)
-
-    def add(self, progress_id, message, profile):
-        mess_wid = urwid.Text(message)
-        progr_wid = urwid.ProgressBar("progress_normal", "progress_complete")
-        column = urwid.Columns([mess_wid, progr_wid])
-        self.progress_dict[(progress_id, profile)] = {
-            "full": column,
-            "progress": progr_wid,
-            "state": "init",
-        }
-        self.progress_list.append(column)
-        self.progress_cb(self.host.loop, (progress_id, message, profile))
-
-    def progress_cb(self, loop, data):
-        progress_id, message, profile = data
-        data = self.host.bridge.progress_get(progress_id, profile)
-        pbar = self.progress_dict[(progress_id, profile)]["progress"]
-        if data:
-            if self.progress_dict[(progress_id, profile)]["state"] == "init":
-                # first answer, we must construct the bar
-                self.progress_dict[(progress_id, profile)]["state"] = "progress"
-                pbar.done = float(data["size"])
-
-            pbar.set_completion(float(data["position"]))
-            self.update_not_bar()
-        else:
-            if self.progress_dict[(progress_id, profile)]["state"] == "progress":
-                self.progress_dict[(progress_id, profile)]["state"] = "done"
-                pbar.set_completion(pbar.done)
-                self.update_not_bar()
-                return
-
-        loop.set_alarm_in(0.2, self.progress_cb, (progress_id, message, profile))
-
-    def _remove_bar(self, progress_id, profile):
-        wid = self.progress_dict[(progress_id, profile)]["full"]
-        self.progress_list.remove(wid)
-        del (self.progress_dict[(progress_id, profile)])
-
-    def _on_clear(self, button):
-        to_remove = []
-        for progress_id, profile in self.progress_dict:
-            if self.progress_dict[(progress_id, profile)]["state"] == "done":
-                to_remove.append((progress_id, profile))
-        for progress_id, profile in to_remove:
-            self._remove_bar(progress_id, profile)
-        self.update_not_bar()
-
-    def update_not_bar(self):
-        if not self.progress_dict:
-            self.host.set_progress(None)
-            return
-        progress = 0
-        nb_bars = 0
-        for progress_id, profile in self.progress_dict:
-            pbar = self.progress_dict[(progress_id, profile)]["progress"]
-            progress += pbar.current / pbar.done * 100
-            nb_bars += 1
-        av_progress = progress / float(nb_bars)
-        self.host.set_progress(av_progress)
--- a/libervia/frontends/primitivus/status.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.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 libervia.backend.core.i18n import _
-import urwid
-from urwid_satext import sat_widgets
-from libervia.frontends.quick_frontend.constants import Const as commonConst
-from libervia.frontends.primitivus.constants import Const as C
-
-
-class StatusBar(urwid.Columns):
-    def __init__(self, host):
-        self.host = host
-        self.presence = sat_widgets.ClickableText("")
-        status_prefix = urwid.Text("[")
-        status_suffix = urwid.Text("]")
-        self.status = sat_widgets.ClickableText("")
-        self.set_presence_status(C.PRESENCE_UNAVAILABLE, "")
-        urwid.Columns.__init__(
-            self,
-            [
-                ("weight", 1, self.presence),
-                ("weight", 1, status_prefix),
-                ("weight", 9, self.status),
-                ("weight", 1, status_suffix),
-            ],
-        )
-        urwid.connect_signal(self.presence, "click", self.on_presence_click)
-        urwid.connect_signal(self.status, "click", self.on_status_click)
-
-    def on_presence_click(self, sender=None):
-        if not self.host.bridge.is_connected(
-            self.host.current_profile
-        ):  # FIXME: manage multi-profiles
-            return
-        options = [commonConst.PRESENCE[presence] for presence in commonConst.PRESENCE]
-        list_widget = sat_widgets.GenericList(
-            options=options, option_type=sat_widgets.ClickableText, on_click=self.on_change
-        )
-        decorated = sat_widgets.LabelLine(
-            list_widget, sat_widgets.SurroundedText(_("Set your presence"))
-        )
-        self.host.show_pop_up(decorated)
-
-    def on_status_click(self, sender=None):
-        if not self.host.bridge.is_connected(
-            self.host.current_profile
-        ):  # FIXME: manage multi-profiles
-            return
-        pop_up_widget = sat_widgets.InputDialog(
-            _("Set your status"),
-            _("New status"),
-            default_txt=self.status.get_text(),
-            cancel_cb=lambda _: self.host.remove_pop_up(),
-            ok_cb=self.on_change,
-        )
-        self.host.show_pop_up(pop_up_widget)
-
-    def on_change(self, sender=None, user_data=None):
-        new_value = user_data.get_text()
-        previous = (
-            [key for key in C.PRESENCE if C.PRESENCE[key][0] == self.presence.get_text()][
-                0
-            ],
-            self.status.get_text(),
-        )
-        if isinstance(user_data, sat_widgets.ClickableText):
-            new = (
-                [
-                    key
-                    for key in commonConst.PRESENCE
-                    if commonConst.PRESENCE[key] == new_value
-                ][0],
-                previous[1],
-            )
-        elif isinstance(user_data, sat_widgets.AdvancedEdit):
-            new = (previous[0], new_value[0])
-        if new != previous:
-            statuses = {
-                C.PRESENCE_STATUSES_DEFAULT: new[1]
-            }  # FIXME: manage multilingual statuses
-            for (
-                profile
-            ) in (
-                self.host.profiles
-            ):  # FIXME: for now all the profiles share the same status
-                self.host.bridge.presence_set(
-                    show=new[0], statuses=statuses, profile_key=profile
-                )
-            self.set_presence_status(new[0], new[1])
-        self.host.remove_pop_up()
-
-    def set_presence_status(self, show, status):
-        show_icon, show_attr = C.PRESENCE.get(show)
-        self.presence.set_text(("show_normal", show_icon))
-        if status is not None:
-            self.status.set_text((show_attr, status))
-        self.host.redraw()
--- a/libervia/frontends/primitivus/widget.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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 libervia.backend.core import log as logging
-
-log = logging.getLogger(__name__)
-import urwid
-from urwid_satext import sat_widgets
-from libervia.frontends.primitivus.keys import action_key_map as a_key
-
-
-class PrimitivusWidget(urwid.WidgetWrap):
-    """Base widget for Primitivus"""
-
-    def __init__(self, w, title=""):
-        self._title = title
-        self._title_dynamic = None
-        self._original_widget = w
-        urwid.WidgetWrap.__init__(self, self._get_decoration(w))
-
-    @property
-    def title(self):
-        """Text shown in title bar of the widget"""
-
-        # profiles currently managed by frontend
-        try:
-            all_profiles = self.host.profiles
-        except AttributeError:
-            all_profiles = []
-
-        # profiles managed by the widget
-        try:
-            profiles = self.profiles
-        except AttributeError:
-            try:
-                profiles = [self.profile]
-            except AttributeError:
-                profiles = []
-
-        title_elts = []
-        if self._title:
-            title_elts.append(self._title)
-        if self._title_dynamic:
-            title_elts.append(self._title_dynamic)
-        if len(all_profiles) > 1 and profiles:
-            title_elts.append("[{}]".format(", ".join(profiles)))
-        return sat_widgets.SurroundedText(" ".join(title_elts))
-
-    @title.setter
-    def title(self, value):
-        self._title = value
-        if self.decoration_visible:
-            self.show_decoration()
-
-    @property
-    def title_dynamic(self):
-        """Dynamic part of title"""
-        return self._title_dynamic
-
-    @title_dynamic.setter
-    def title_dynamic(self, value):
-        self._title_dynamic = value
-        if self.decoration_visible:
-            self.show_decoration()
-
-    @property
-    def decoration_visible(self):
-        """True if the decoration is visible"""
-        return isinstance(self._w, sat_widgets.LabelLine)
-
-    def keypress(self, size, key):
-        if key == a_key["DECORATION_HIDE"]:  # user wants to (un)hide widget decoration
-            show = not self.decoration_visible
-            self.show_decoration(show)
-        else:
-            return super(PrimitivusWidget, self).keypress(size, key)
-
-    def _get_decoration(self, widget):
-        return sat_widgets.LabelLine(widget, self.title)
-
-    def show_decoration(self, show=True):
-        """Show/Hide the decoration around the window"""
-        self._w = (
-            self._get_decoration(self._original_widget) if show else self._original_widget
-        )
-
-    def get_menu(self):
-        raise NotImplementedError
--- a/libervia/frontends/primitivus/xmlui.py	Fri Jun 02 14:54:26 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,528 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
-import urwid
-import copy
-from libervia.backend.core import exceptions
-from urwid_satext import sat_widgets
-from urwid_satext import files_management
-from libervia.backend.core.log import getLogger
-
-log = getLogger(__name__)
-from libervia.frontends.primitivus.constants import Const as C
-from libervia.frontends.primitivus.widget import PrimitivusWidget
-from libervia.frontends.tools import xmlui
-
-
-class PrimitivusEvents(object):
-    """ Used to manage change event of Primitivus widgets """
-
-    def _event_callback(self, ctrl, *args, **kwargs):
-        """" Call xmlui callback and ignore any extra argument """
-        args[-1](ctrl)
-
-    def _xmlui_on_change(self, callback):
-        """ Call callback with widget as only argument """
-        urwid.connect_signal(self, "change", self._event_callback, callback)
-
-
-class PrimitivusEmptyWidget(xmlui.EmptyWidget, urwid.Text):
-    def __init__(self, _xmlui_parent):
-        urwid.Text.__init__(self, "")
-
-
-class PrimitivusTextWidget(xmlui.TextWidget, urwid.Text):
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        urwid.Text.__init__(self, value)
-
-
-class PrimitivusLabelWidget(xmlui.LabelWidget, PrimitivusTextWidget):
-    def __init__(self, _xmlui_parent, value):
-        super(PrimitivusLabelWidget, self).__init__(_xmlui_parent, value + ": ")
-
-
-class PrimitivusJidWidget(xmlui.JidWidget, PrimitivusTextWidget):
-    pass
-
-
-class PrimitivusDividerWidget(xmlui.DividerWidget, urwid.Divider):
-    def __init__(self, _xmlui_parent, style="line"):
-        if style == "line":
-            div_char = "─"
-        elif style == "dot":
-            div_char = "·"
-        elif style == "dash":
-            div_char = "-"
-        elif style == "plain":
-            div_char = "█"
-        elif style == "blank":
-            div_char = " "
-        else:
-            log.warning(_("Unknown div_char"))
-            div_char = "─"
-
-        urwid.Divider.__init__(self, div_char)
-
-
-class PrimitivusStringWidget(
-    xmlui.StringWidget, sat_widgets.AdvancedEdit, PrimitivusEvents
-):
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        sat_widgets.AdvancedEdit.__init__(self, edit_text=value)
-        self.read_only = read_only
-
-    def selectable(self):
-        if self.read_only:
-            return False
-        return super(PrimitivusStringWidget, self).selectable()
-
-    def _xmlui_set_value(self, value):
-        self.set_edit_text(value)
-
-    def _xmlui_get_value(self):
-        return self.get_edit_text()
-
-
-class PrimitivusJidInputWidget(xmlui.JidInputWidget, PrimitivusStringWidget):
-    pass
-
-
-class PrimitivusPasswordWidget(
-    xmlui.PasswordWidget, sat_widgets.Password, PrimitivusEvents
-):
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        sat_widgets.Password.__init__(self, edit_text=value)
-        self.read_only = read_only
-
-    def selectable(self):
-        if self.read_only:
-            return False
-        return super(PrimitivusPasswordWidget, self).selectable()
-
-    def _xmlui_set_value(self, value):
-        self.set_edit_text(value)
-
-    def _xmlui_get_value(self):
-        return self.get_edit_text()
-
-
-class PrimitivusTextBoxWidget(
-    xmlui.TextBoxWidget, sat_widgets.AdvancedEdit, PrimitivusEvents
-):
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        sat_widgets.AdvancedEdit.__init__(self, edit_text=value, multiline=True)
-        self.read_only = read_only
-
-    def selectable(self):
-        if self.read_only:
-            return False
-        return super(PrimitivusTextBoxWidget, self).selectable()
-
-    def _xmlui_set_value(self, value):
-        self.set_edit_text(value)
-
-    def _xmlui_get_value(self):
-        return self.get_edit_text()
-
-
-class PrimitivusBoolWidget(xmlui.BoolWidget, urwid.CheckBox, PrimitivusEvents):
-    def __init__(self, _xmlui_parent, state, read_only=False):
-        urwid.CheckBox.__init__(self, "", state=state)
-        self.read_only = read_only
-
-    def selectable(self):
-        if self.read_only:
-            return False
-        return super(PrimitivusBoolWidget, self).selectable()
-
-    def _xmlui_set_value(self, value):
-        self.set_state(value == "true")
-
-    def _xmlui_get_value(self):
-        return C.BOOL_TRUE if self.get_state() else C.BOOL_FALSE
-
-
-class PrimitivusIntWidget(xmlui.IntWidget, sat_widgets.AdvancedEdit, PrimitivusEvents):
-    def __init__(self, _xmlui_parent, value, read_only=False):
-        sat_widgets.AdvancedEdit.__init__(self, edit_text=value)
-        self.read_only = read_only
-
-    def selectable(self):
-        if self.read_only:
-            return False
-        return super(PrimitivusIntWidget, self).selectable()
-
-    def _xmlui_set_value(self, value):
-        self.set_edit_text(value)
-
-    def _xmlui_get_value(self):
-        return self.get_edit_text()
-
-
-class PrimitivusButtonWidget(
-    xmlui.ButtonWidget, sat_widgets.CustomButton, PrimitivusEvents
-):
-    def __init__(self, _xmlui_parent, value, click_callback):
-        sat_widgets.CustomButton.__init__(self, value, on_press=click_callback)
-
-    def _xmlui_on_click(self, callback):
-        urwid.connect_signal(self, "click", callback)
-
-
-class PrimitivusListWidget(xmlui.ListWidget, sat_widgets.List, PrimitivusEvents):
-    def __init__(self, _xmlui_parent, options, selected, flags):
-        sat_widgets.List.__init__(self, options=options, style=flags)
-        self._xmlui_select_values(selected)
-
-    def _xmlui_select_value(self, value):
-        return self.select_value(value)
-
-    def _xmlui_select_values(self, values):
-        return self.select_values(values)
-
-    def _xmlui_get_selected_values(self):
-        return [option.value for option in self.get_selected_values()]
-
-    def _xmlui_add_values(self, values, select=True):
-        current_values = self.get_all_values()
-        new_values = copy.deepcopy(current_values)
-        for value in values:
-            if value not in current_values:
-                new_values.append(value)
-        if select:
-            selected = self._xmlui_get_selected_values()
-        self.change_values(new_values)
-        if select:
-            for value in values:
-                if value not in selected:
-                    selected.append(value)
-            self._xmlui_select_values(selected)
-
-
-class PrimitivusJidsListWidget(xmlui.ListWidget, sat_widgets.List, PrimitivusEvents):
-    def __init__(self, _xmlui_parent, jids, styles):
-        sat_widgets.List.__init__(
-            self,
-            options=jids + [""],  # the empty field is here to add new jids if needed
-            option_type=lambda txt, align: sat_widgets.AdvancedEdit(
-                edit_text=txt, align=align
-            ),
-            on_change=self._on_change,
-        )
-        self.delete = 0
-
-    def _on_change(self, list_widget, jid_widget=None, text=None):
-        if jid_widget is not None:
-            if jid_widget != list_widget.contents[-1] and not text:
-                # if a field is empty, we delete the line (except for the last line)
-                list_widget.contents.remove(jid_widget)
-            elif jid_widget == list_widget.contents[-1] and text:
-                # we always want an empty field as last value to be able to add jids
-                list_widget.contents.append(sat_widgets.AdvancedEdit())
-
-    def _xmlui_get_selected_values(self):
-        # XXX: there is not selection in this list, so we return all non empty values
-        return [jid_ for jid_ in self.get_all_values() if jid_]
-
-
-class PrimitivusAdvancedListContainer(
-    xmlui.AdvancedListContainer, sat_widgets.TableContainer, PrimitivusEvents
-):
-    def __init__(self, _xmlui_parent, columns, selectable="no"):
-        options = {"ADAPT": ()}
-        if selectable != "no":
-            options["HIGHLIGHT"] = ()
-        sat_widgets.TableContainer.__init__(
-            self, columns=columns, options=options, row_selectable=selectable != "no"
-        )
-
-    def _xmlui_append(self, widget):
-        self.add_widget(widget)
-
-    def _xmlui_add_row(self, idx):
-        self.set_row_index(idx)
-
-    def _xmlui_get_selected_widgets(self):
-        return self.get_selected_widgets()
-
-    def _xmlui_get_selected_index(self):
-        return self.get_selected_index()
-
-    def _xmlui_on_select(self, callback):
-        """ Call callback with widget as only argument """
-        urwid.connect_signal(self, "click", self._event_callback, callback)
-
-
-class PrimitivusPairsContainer(xmlui.PairsContainer, sat_widgets.TableContainer):
-    def __init__(self, _xmlui_parent):
-        options = {"ADAPT": (0,), "HIGHLIGHT": (0,)}
-        if self._xmlui_main.type == "param":
-            options["FOCUS_ATTR"] = "param_selected"
-        sat_widgets.TableContainer.__init__(self, columns=2, options=options)
-
-    def _xmlui_append(self, widget):
-        if isinstance(widget, PrimitivusEmptyWidget):
-            # we don't want highlight on empty widgets
-            widget = urwid.AttrMap(widget, "default")
-        self.add_widget(widget)
-
-
-class PrimitivusLabelContainer(PrimitivusPairsContainer, xmlui.LabelContainer):
-    pass
-
-
-class PrimitivusTabsContainer(xmlui.TabsContainer, sat_widgets.TabsContainer):
-    def __init__(self, _xmlui_parent):
-        sat_widgets.TabsContainer.__init__(self)
-
-    def _xmlui_append(self, widget):
-        self.body.append(widget)
-
-    def _xmlui_add_tab(self, label, selected):
-        tab = PrimitivusVerticalContainer(None)
-        self.add_tab(label, tab, selected)
-        return tab
-
-
-class PrimitivusVerticalContainer(xmlui.VerticalContainer, urwid.ListBox):
-    BOX_HEIGHT = 5
-
-    def __init__(self, _xmlui_parent):
-        urwid.ListBox.__init__(self, urwid.SimpleListWalker([]))
-        self._last_size = None
-
-    def _xmlui_append(self, widget):
-        if "flow" not in widget.sizing():
-            widget = urwid.BoxAdapter(widget, self.BOX_HEIGHT)
-        self.body.append(widget)
-
-    def render(self, size, focus=False):
-        if size != self._last_size:
-            (maxcol, maxrow) = size
-            if self.body:
-                widget = self.body[0]
-                if isinstance(widget, urwid.BoxAdapter):
-                    widget.height = maxrow
-            self._last_size = size
-        return super(PrimitivusVerticalContainer, self).render(size, focus)
-
-
-### Dialogs ###
-
-
-class PrimitivusDialog(object):
-    def __init__(self, _xmlui_parent):
-        self.host = _xmlui_parent.host
-
-    def _xmlui_show(self):
-        self.host.show_pop_up(self)
-
-    def _xmlui_close(self):
-        self.host.remove_pop_up(self)
-
-
-class PrimitivusMessageDialog(PrimitivusDialog, xmlui.MessageDialog, sat_widgets.Alert):
-    def __init__(self, _xmlui_parent, title, message, level):
-        PrimitivusDialog.__init__(self, _xmlui_parent)
-        xmlui.MessageDialog.__init__(self, _xmlui_parent)
-        sat_widgets.Alert.__init__(
-            self, title, message, ok_cb=lambda __: self._xmlui_close()
-        )
-
-
-class PrimitivusNoteDialog(xmlui.NoteDialog, PrimitivusMessageDialog):
-    # TODO: separate NoteDialog
-    pass
-
-
-class PrimitivusConfirmDialog(
-    PrimitivusDialog, xmlui.ConfirmDialog, sat_widgets.ConfirmDialog
-):
-    def __init__(self, _xmlui_parent, title, message, level, buttons_set):
-        PrimitivusDialog.__init__(self, _xmlui_parent)
-        xmlui.ConfirmDialog.__init__(self, _xmlui_parent)
-        sat_widgets.ConfirmDialog.__init__(
-            self,
-            title,
-            message,
-            no_cb=lambda __: self._xmlui_cancelled(),
-            yes_cb=lambda __: self._xmlui_validated(),
-        )
-
-
-class PrimitivusFileDialog(
-    PrimitivusDialog, xmlui.FileDialog, files_management.FileDialog
-):
-    def __init__(self, _xmlui_parent, title, message, level, filetype):
-        # TODO: message is not managed yet
-        PrimitivusDialog.__init__(self, _xmlui_parent)
-        xmlui.FileDialog.__init__(self, _xmlui_parent)
-        style = []
-        if filetype == C.XMLUI_DATA_FILETYPE_DIR:
-            style.append("dir")
-        files_management.FileDialog.__init__(
-            self,
-            ok_cb=lambda path: self._xmlui_validated({"path": path}),
-            cancel_cb=lambda __: self._xmlui_cancelled(),
-            message=message,
-            title=title,
-            style=style,
-        )
-
-
-class GenericFactory(object):
-    def __getattr__(self, attr):
-        if attr.startswith("create"):
-            cls = globals()[
-                "Primitivus" + attr[6:]
-            ]  # XXX: we prefix with "Primitivus" to work around an Urwid bug, WidgetMeta in Urwid don't manage multiple inheritance with same names
-            return cls
-
-
-class WidgetFactory(GenericFactory):
-    def __getattr__(self, attr):
-        if attr.startswith("create"):
-            cls = GenericFactory.__getattr__(self, attr)
-            cls._xmlui_main = self._xmlui_main
-            return cls
-
-
-class XMLUIPanel(xmlui.XMLUIPanel, PrimitivusWidget):
-    widget_factory = WidgetFactory()
-
-    def __init__(
-        self,
-        host,
-        parsed_xml,
-        title=None,
-        flags=None,
-        callback=None,
-        ignore=None,
-        whitelist=None,
-        profile=C.PROF_KEY_NONE,
-    ):
-        self.widget_factory._xmlui_main = self
-        self._dest = None
-        xmlui.XMLUIPanel.__init__(
-            self,
-            host,
-            parsed_xml,
-            title=title,
-            flags=flags,
-            callback=callback,
-            ignore=ignore,
-            profile=profile,
-        )
-        PrimitivusWidget.__init__(self, self.main_cont, self.xmlui_title)
-
-
-    def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None):
-        # Small hack to always have a VerticalContainer as main container in Primitivus.
-        # this used to be the default behaviour for all frontends, but now
-        # TabsContainer can also be the main container.
-        if _xmlui_parent is self:
-            node = current_node.childNodes[0]
-            if node.nodeName == "container" and node.getAttribute("type") == "tabs":
-                _xmlui_parent = self.widget_factory.createVerticalContainer(self)
-                self.main_cont = _xmlui_parent
-        return super(XMLUIPanel, self)._parse_childs(_xmlui_parent, current_node, wanted,
-                                                    data)
-
-
-    def construct_ui(self, parsed_dom):
-        def post_treat():
-            assert self.main_cont.body
-
-            if self.type in ("form", "popup"):
-                buttons = []
-                if self.type == "form":
-                    buttons.append(urwid.Button(_("Submit"), self.on_form_submitted))
-                    if not "NO_CANCEL" in self.flags:
-                        buttons.append(urwid.Button(_("Cancel"), self.on_form_cancelled))
-                else:
-                    buttons.append(
-                        urwid.Button(_("OK"), on_press=lambda __: self._xmlui_close())
-                    )
-                max_len = max([len(button.get_label()) for button in buttons])
-                grid_wid = urwid.GridFlow(buttons, max_len + 4, 1, 0, "center")
-                self.main_cont.body.append(grid_wid)
-            elif self.type == "param":
-                tabs_cont = self.main_cont.body[0].base_widget
-                assert isinstance(tabs_cont, sat_widgets.TabsContainer)
-                buttons = []
-                buttons.append(sat_widgets.CustomButton(_("Save"), self.on_save_params))
-                buttons.append(
-                    sat_widgets.CustomButton(
-                        _("Cancel"), lambda x: self.host.remove_window()
-                    )
-                )
-                max_len = max([button.get_size() for button in buttons])
-                grid_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center")
-                tabs_cont.add_footer(grid_wid)
-
-        xmlui.XMLUIPanel.construct_ui(self, parsed_dom, post_treat)
-        urwid.WidgetWrap.__init__(self, self.main_cont)
-
-    def show(self, show_type=None, valign="middle"):
-        """Show the constructed UI
-        @param show_type: how to show the UI:
-            - None (follow XMLUI's recommendation)
-            - 'popup'
-            - 'window'
-        @param valign: vertical alignment when show_type is 'popup'.
-            Ignored when show_type is 'window'.
-
-        """
-        if show_type is None:
-            if self.type in ("window", "param"):
-                show_type = "window"
-            elif self.type in ("popup", "form"):
-                show_type = "popup"
-
-        if show_type not in ("popup", "window"):
-            raise ValueError("Invalid show_type [%s]" % show_type)
-
-        self._dest = show_type
-        if show_type == "popup":
-            self.host.show_pop_up(self, valign=valign)
-        elif show_type == "window":
-            self.host.new_widget(self, user_action=self.user_action)
-        else:
-            assert False
-        self.host.redraw()
-
-    def _xmlui_close(self):
-        if self._dest == "window":
-            self.host.remove_window()
-        elif self._dest == "popup":
-            self.host.remove_pop_up(self)
-        else:
-            raise exceptions.InternalError(
-                "self._dest unknown, are you sure you have called XMLUI.show ?"
-            )
-
-
-class XMLUIDialog(xmlui.XMLUIDialog):
-    dialog_factory = GenericFactory()
-
-
-xmlui.register_class(xmlui.CLASS_PANEL, XMLUIPanel)
-xmlui.register_class(xmlui.CLASS_DIALOG, XMLUIDialog)
-create = xmlui.create
--- a/libervia/frontends/quick_frontend/quick_profile_manager.py	Fri Jun 02 14:54:26 2023 +0200
+++ b/libervia/frontends/quick_frontend/quick_profile_manager.py	Fri Jun 02 16:25:25 2023 +0200
@@ -21,7 +21,7 @@
 from libervia.backend.core import log as logging
 
 log = logging.getLogger(__name__)
-from libervia.frontends.primitivus.constants import Const as C
+from libervia.tui.constants import Const as C
 
 
 class ProfileRecord(object):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/base.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,863 @@
+#!/usr/bin/env python3
+
+# Libervia TUI
+# 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 libervia.backend.core.i18n import _, D_
+from libervia.tui.constants import Const as C
+from libervia.backend.core import log_config
+log_config.sat_configure(C.LOG_BACKEND_STANDARD, C)
+from libervia.backend.core import log as logging
+log = logging.getLogger(__name__)
+from libervia.backend.tools import config as sat_config
+import urwid
+from urwid.util import is_wide_char
+from urwid_satext import sat_widgets
+from libervia.frontends.quick_frontend.quick_app import QuickApp
+from libervia.frontends.quick_frontend import quick_utils
+from libervia.frontends.quick_frontend import quick_chat
+from libervia.tui.profile_manager import ProfileManager
+from libervia.tui.contact_list import ContactList
+from libervia.tui.chat import Chat
+from libervia.tui import xmlui
+from libervia.tui.progress import Progress
+from libervia.tui.notify import Notify
+from libervia.tui.keys import action_key_map as a_key
+from libervia.tui import config
+from libervia.frontends.tools.misc import InputHistory
+from libervia.backend.tools.common import dynamic_import
+from libervia.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.parse_main_conf()
+bridge_name = sat_config.config_get(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.set_completion_method(self._text_completion)
+        urwid.connect_signal(self, 'click', self.on_text_entered)
+
+    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 on_text_entered(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.message_send(
+                    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.show_dialog(_("Error while sending message ({})").format(failure), type="error"),
+                    profile_key=chat_widget.profile
+                    )
+                editBar.set_edit_text('')
+        elif self.mode == C.MODE_COMMAND:
+            self.command_handler()
+
+    def command_handler(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.on_exit()
+            raise urwid.ExitMainLoop()
+        elif command == 'messages':
+            wid = sat_widgets.GenericList(logging.memory_get())
+            self.host.select_widget(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.on_change(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
+        #     else:
+        #         self.host.status_bar.on_presence_click()
+        # elif command == 'status':
+        #     if args:
+        #         self.host.status_bar.on_change(user_data=sat_widgets.AdvancedEdit(args[0]))
+        #     else:
+        #         self.host.status_bar.on_status_click()
+        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.update_history(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.add_message(D_("Please specify the globbing pattern to search for"))
+                else:
+                    widget.update_history(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.set_filter(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.on_subject_dialog(new_title)
+        else:
+            return
+        self.set_edit_text('')
+
+    def _history_cb(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._update_input_history(self.get_edit_text(), mode=self.mode)
+            self.host._update_input_history(mode=C.MODE_NORMAL)
+        if self._mode == C.MODE_NORMAL and key in self._modes:
+            self.host._update_input_history(mode=self._modes[key][0])
+        if key == a_key['HISTORY_PREV']:
+            self.host._update_input_history(self.get_edit_text(), -1, self._history_cb, self.mode)
+            return
+        elif key == a_key['HISTORY_NEXT']:
+            self.host._update_input_history(self.get_edit_text(), +1, self._history_cb, self.mode)
+            return
+        elif key == a_key['EDIT_ENTER']:
+            self.host._update_input_history(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.chat_state_composing(self.host.selected_widget.target, self.host.selected_widget.profile)
+
+        return super(EditBar, self).keypress(size, key)
+
+
+class LiberviaTUITopWidget(sat_widgets.FocusPile):
+    """Top most widget used in LiberviaTUI"""
+    _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(LiberviaTUITopWidget, self).__init__([('pack', self._menu), self._body, ('pack', self._edit_bar)])
+        for position in self.positions:
+            setattr(self,
+                    position,
+                    property(lambda: self, self.widget_get(position=position),
+                             lambda pos, new_wid: self.widget_set(new_wid, position=pos))
+                   )
+        self.focus_position = len(self.contents)-1
+
+    def get_visible_positions(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(LiberviaTUITopWidget, self).keypress(size, key)
+
+            if focus in self._hidden:
+                return
+
+            self.focus_position = self.get_visible_positions().index(focus)
+            return
+
+        return super(LiberviaTUITopWidget, self).keypress(size, key)
+
+    def widget_get(self,  position):
+        if not position in self.positions:
+            raise ValueError("Unknown position {}".format(position))
+        return getattr(self, "_{}".format(position))
+
+    def widget_set(self,  widget, position):
+        if not position in self.positions:
+            raise ValueError("Unknown position {}".format(position))
+        return setattr(self, "_{}".format(position), widget)
+
+    def hide_switch(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.widget_get(position)
+        idx = self.get_visible_positions(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.hide_switch(position)
+
+    def hide(self, position):
+        if not position in self._hidden:
+            self.hide_switch(position)
+
+
+class LiberviaTUIApp(QuickApp, InputHistory):
+    MB_HANDLER = False
+    AVATARS_HANDLER = False
+
+    def __init__(self):
+        bridge_module = dynamic_import.bridge(bridge_name, 'libervia.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.input_filter, unhandled_input=self.key_handler)
+
+    @classmethod
+    def run(cls):
+        cls().start()
+
+    def on_bridge_connected(self):
+
+        ##misc setup##
+        self._visible_widgets = set()
+        self.notif_bar = sat_widgets.NotificationBar()
+        urwid.connect_signal(self.notif_bar, 'change', self.on_notification)
+
+        self.progress_wid = self.widgets.get_or_create_widget(Progress, None, on_new_widget=None)
+        urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.select_widget(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.parse_main_conf()
+        self._bracketed_paste = C.bool(
+            sat_config.config_get(sat_conf, C.CONFIG_SECTION, '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.post_init()
+
+    @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 mode_hint(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.connect_bridge()
+        self.loop.run()
+
+    def post_init(self):
+        try:
+            config.apply_config(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.show_pop_up(popup)
+        super(LiberviaTUIApp, self).post_init(self.main_widget)
+
+    def keys_to_text(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 input_filter(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 LiberviaTUI 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.keys_to_text(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 key_handler(self, input_):
+        if input_ == a_key['MENU_HIDE']:
+            """User want to (un)hide the menu roller"""
+            try:
+                self.main_widget.hide_switch('menu')
+            except AttributeError:
+                pass
+        elif input_ == a_key['NOTIFICATION_NEXT']:
+            """User wants to see next notification"""
+            self.notif_bar.show_next()
+        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.version_get(): #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.check_shortcuts(input_)
+        except AttributeError:
+            return input_
+
+    def add_menus(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.import_menu
+        @param menu_data: data to send with these menus
+
+        """
+        def add_menu_cb(callback_id):
+            self.action_launch(callback_id, menu_data, profile=self.current_profile)
+        for id_, type_, path, path_i18n, extra  in self.bridge.menus_get("", 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.add_menu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
+
+
+    def _build_menu_roller(self):
+        menu = sat_widgets.Menu(self.loop)
+        general = _("General")
+        menu.add_menu(general, _("Connect"), self.on_connect_request)
+        menu.add_menu(general, _("Disconnect"), self.on_disconnect_request)
+        menu.add_menu(general, _("Parameters"), self.on_param)
+        menu.add_menu(general, _("About"), self.on_about_request)
+        menu.add_menu(general, _("Exit"), self.on_exit_request, a_key['APP_QUIT'])
+        menu.add_menu(_("Contacts"))  # add empty menu to save the place in the menu order
+        groups = _("Groups")
+        menu.add_menu(groups)
+        menu.add_menu(groups, _("Join room"), self.on_join_room_request, a_key['ROOM_JOIN'])
+        #additionals menus
+        #FIXME: do this in a more generic way (in quickapp)
+        self.add_menus(menu, C.MENU_GLOBAL)
+
+        menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
+        return menu_roller
+
+    def _build_main_widget(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._build_menu_roller()
+        self.main_widget = LiberviaTUITopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar)
+        return self.main_widget
+
+    def plugging_profiles(self):
+        self.loop.widget = self._build_main_widget()
+        self.redraw()
+        try:
+            # if a popup arrived before main widget is build, we need to show it now
+            self.show_pop_up(self._early_popup)
+        except AttributeError:
+            pass
+        else:
+            del self._early_popup
+
+    def profile_plugged(self, profile):
+        QuickApp.profile_plugged(self, profile)
+        contact_list = self.widgets.get_or_create_widget(ContactList, None, on_new_widget=None, on_click=self.contact_selected, on_change=lambda w: self.redraw(), profile=profile)
+        self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
+        return contact_list
+
+    def is_hidden(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.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
+        self.show_pop_up(popup, width=75, height=20)
+        return popup
+
+    def remove_pop_up(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.remove_pop_up(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.get_next_popup()
+        if next_popup:
+            #we still have popup to show, we display it
+            self.show_pop_up(next_popup)
+        else:
+            self.redraw()
+
+    def show_pop_up(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.LiberviaTUINoteDialog) else 135
+        if height == None:
+            height = 20 if isinstance(pop_up_widget, xmlui.LiberviaTUINoteDialog) 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.add_pop_up(pop_up_widget)
+
+    def bar_notify(self, message):
+        """"Notify message to user via notification bar"""
+        self.notif_bar.add_message(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(LiberviaTUIApp, 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.has_focus():
+            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.send_notification(message)
+
+
+    def new_widget(self, widget, user_action=False):
+        """Method called when a new widget is created
+
+        if suitable, the widget will be displayed
+        @param widget(widget.LiberviaTUIWidget): 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.select_widget(widget)
+
+    def select_widget(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.remove_menu(C.MENU_ID_WIDGET)
+        except KeyError:
+            log.debug("No menu to delete")
+        self.selected_widget = widget
+        try:
+            on_selected = self.selected_widget.on_selected
+        except AttributeError:
+            pass
+        else:
+            on_selected()
+        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.get_widgets is not more appropriate
+            if isinstance(wid, Chat):
+                contact_list = self.contact_lists[wid.profile]
+                contact_list.select(wid.target)
+
+        self.redraw()
+
+    def remove_window(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 add_progress(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 set_progress(self, percentage):
+        """Set the progression shown in notification bar"""
+        self.notif_bar.set_progress(percentage)
+
+    def contact_selected(self, contact_list, entity):
+        self.clear_notifs(entity, profile=contact_list.profile)
+        if entity.resource:
+            # we have clicked on a private MUC conversation
+            chat_widget = self.widgets.get_or_create_widget(Chat, entity, on_new_widget=None, force_hash = Chat.get_private_hash(contact_list.profile, entity), profile=contact_list.profile)
+        else:
+            chat_widget = self.widgets.get_or_create_widget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
+        self.select_widget(chat_widget)
+        self.menu_roller.add_menu(_('Chat menu'), chat_widget.get_menu(), C.MENU_ID_WIDGET)
+
+    def _dialog_ok_cb(self, widget, data):
+        popup, answer_cb, answer_data = data
+        self.remove_pop_up(popup)
+        if answer_cb is not None:
+            answer_cb(True, answer_data)
+
+    def _dialog_cancel_cb(self, widget, data):
+        popup, answer_cb, answer_data = data
+        self.remove_pop_up(popup)
+        if answer_cb is not None:
+            answer_cb(False, answer_data)
+
+    def show_dialog(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.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
+        elif type == 'error':
+            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
+            if answer_cb is None:
+                popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
+        elif type == 'yes/no':
+            popup = sat_widgets.ConfirmDialog(message)
+            popup.set_callback('yes', self._dialog_ok_cb, (popup, answer_cb, answer_data))
+            popup.set_callback('no', self._dialog_cancel_cb, (popup, answer_cb, answer_data))
+        else:
+            popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
+            if answer_cb is None:
+                popup.set_callback('ok', lambda dummy: self.remove_pop_up(popup))
+            log.error(u'unmanaged dialog type: {}'.format(type))
+        self.show_pop_up(popup)
+
+    def dialog_failure(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 on_notification(self, notif_bar):
+        """Called when a new notification has been received"""
+        if not isinstance(self.main_widget, LiberviaTUITopWidget):
+            #if we are not in the main configuration, we ignore the notifications bar
+            return
+        if self.notif_bar.can_hide():
+                #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 _action_manager_unknown_error(self):
+        self.alert(_("Error"), _(u"Unmanaged action"))
+
+    def room_joined_handler(self, room_jid_s, room_nicks, user_nick, subject, profile):
+        super(LiberviaTUIApp, self).room_joined_handler(room_jid_s, room_nicks, user_nick, subject, profile)
+        # if self.selected_widget is None:
+        #     for contact_list in self.widgets.get_widgets(ContactList):
+        #         if profile in contact_list.profiles:
+        #             contact_list.set_focus(jid.JID(room_jid_s), True)
+
+    def progress_started_handler(self, pid, metadata, profile):
+        super(LiberviaTUIApp, self).progress_started_handler(pid, metadata, profile)
+        self.add_progress(pid, metadata.get('name', _(u'unkown')), profile)
+
+    def progress_finished_handler(self, pid, metadata, profile):
+        log.info(u"Progress {} finished".format(pid))
+        super(LiberviaTUIApp, self).progress_finished_handler(pid, metadata, profile)
+
+    def progress_error_handler(self, pid, err_msg, profile):
+        log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
+        super(LiberviaTUIApp, self).progress_error_handler(pid, err_msg, profile)
+
+
+    ##DIALOGS CALLBACKS##
+    def on_join_room(self, button, edit):
+        self.remove_pop_up()
+        room_jid = jid.JID(edit.get_edit_text())
+        self.bridge.muc_join(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialog_failure)
+
+    #MENU EVENTS#
+    def on_connect_request(self, menu):
+        QuickApp.connect(self, self.current_profile)
+
+    def on_disconnect_request(self, menu):
+        self.disconnect(self.current_profile)
+
+    def on_param(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.param_ui_get(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
+
+    def on_exit_request(self, menu):
+        QuickApp.on_exit(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 on_join_room_request(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.muc_get_default_service(), ok_cb=self.on_join_room)
+        pop_up_widget.set_callback('cancel', lambda dummy: self.remove_pop_up(pop_up_widget))
+        self.show_pop_up(pop_up_widget)
+
+    def on_about_request(self, menu):
+        self.alert(_("About"), C.APP_NAME + " v" + self.bridge.version_get())
+
+    #MISC CALLBACKS#
+
+    def set_presence_status(self, show='', status=None, profile=C.PROF_KEY_NONE):
+        contact_list_wid = self.widgets.get_widget(ContactList, profiles=profile)
+        if contact_list_wid is not None:
+            contact_list_wid.status_bar.set_presence_status(show, status)
+        else:
+            log.warning(u"No ContactList widget found for profile {}".format(profile))
+
+if __name__ == '__main__':
+    LiberviaTUIApp().start()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/chat.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,708 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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 functools import total_ordering
+from pathlib import Path
+import bisect
+import urwid
+from urwid_satext import sat_widgets
+from libervia.backend.core.i18n import _
+from libervia.backend.core import log as logging
+from libervia.frontends.quick_frontend import quick_widgets
+from libervia.frontends.quick_frontend import quick_chat
+from libervia.frontends.quick_frontend import quick_games
+from libervia.tui import game_tarot
+from libervia.tui.constants import Const as C
+from libervia.tui.keys import action_key_map as a_key
+from libervia.tui.widget import LiberviaTUIWidget
+from libervia.tui.contact_list import ContactList
+
+
+log = logging.getLogger(__name__)
+
+
+OCCUPANTS_FOOTER = _("{} occupants")
+
+
+class MessageWidget(urwid.WidgetWrap, quick_chat.MessageWidget):
+    def __init__(self, mess_data):
+        """
+        @param mess_data(quick_chat.Message, None): message data
+            None: used only for non text widgets (e.g.: focus separator)
+        """
+        self.mess_data = mess_data
+        mess_data.widgets.add(self)
+        super(MessageWidget, self).__init__(urwid.Text(self.markup))
+
+    @property
+    def markup(self):
+        return (
+            self._generate_info_markup()
+            if self.mess_data.type == C.MESS_TYPE_INFO
+            else self._generate_markup()
+        )
+
+    @property
+    def info_type(self):
+        return self.mess_data.info_type
+
+    @property
+    def parent(self):
+        return self.mess_data.parent
+
+    @property
+    def message(self):
+        """Return currently displayed message"""
+        return self.mess_data.main_message
+
+    @message.setter
+    def message(self, value):
+        self.mess_data.message = {"": value}
+        self.redraw()
+
+    @property
+    def type(self):
+        try:
+            return self.mess_data.type
+        except AttributeError:
+            return C.MESS_TYPE_INFO
+
+    def redraw(self):
+        self._w.set_text(self.markup)
+        self.mess_data.parent.host.redraw()  # FIXME: should not be necessary
+
+    def selectable(self):
+        return True
+
+    def keypress(self, size, key):
+        return key
+
+    def get_cursor_coords(self, size):
+        return 0, 0
+
+    def render(self, size, focus=False):
+        # Text widget doesn't render cursor, but we want one
+        # so we add it here
+        canvas = urwid.CompositeCanvas(self._w.render(size, focus))
+        if focus:
+            canvas.set_cursor(self.get_cursor_coords(size))
+        return canvas
+
+    def _generate_info_markup(self):
+        return ("info_msg", self.message)
+
+    def _generate_markup(self):
+        """Generate text markup according to message data and Widget options"""
+        markup = []
+        d = self.mess_data
+        mention = d.mention
+
+        # message status
+        if d.status is None:
+            markup.append(" ")
+        elif d.status == "delivered":
+            markup.append(("msg_status_received", "✔"))
+        else:
+            log.warning("Unknown status: {}".format(d.status))
+
+        # timestamp
+        if self.parent.show_timestamp:
+            attr = "msg_mention" if mention else "date"
+            markup.append((attr, "[{}]".format(d.time_text)))
+        else:
+            if mention:
+                markup.append(("msg_mention", "[*]"))
+
+        # nickname
+        if self.parent.show_short_nick:
+            markup.append(
+                ("my_nick" if d.own_mess else "other_nick", "**" if d.own_mess else "*")
+            )
+        else:
+            markup.append(
+                ("my_nick" if d.own_mess else "other_nick", "[{}] ".format(d.nick or ""))
+            )
+
+        msg = self.message  # needed to generate self.selected_lang
+
+        if d.selected_lang:
+            markup.append(("msg_lang", "[{}] ".format(d.selected_lang)))
+
+        # message body
+        markup.append(msg)
+
+        return markup
+
+    # events
+    def update(self, update_dict=None):
+        """update all the linked message widgets
+
+        @param update_dict(dict, None): key=attribute updated value=new_value
+        """
+        self.redraw()
+
+
+@total_ordering
+class OccupantWidget(urwid.WidgetWrap):
+    def __init__(self, occupant_data):
+        self.occupant_data = occupant_data
+        occupant_data.widgets.add(self)
+        markup = self._generate_markup()
+        text = sat_widgets.ClickableText(markup)
+        urwid.connect_signal(
+            text,
+            "click",
+            self.occupant_data.parent._occupants_clicked,
+            user_args=[self.occupant_data],
+        )
+        super(OccupantWidget, self).__init__(text)
+
+    def __hash__(self):
+        return id(self)
+
+    def __eq__(self, other):
+        if other is None:
+            return False
+        return self.occupant_data.nick == other.occupant_data.nick
+
+    def __lt__(self, other):
+        return self.occupant_data.nick.lower() < other.occupant_data.nick.lower()
+
+    @property
+    def markup(self):
+        return self._generate_markup()
+
+    @property
+    def parent(self):
+        return self.mess_data.parent
+
+    @property
+    def nick(self):
+        return self.occupant_data.nick
+
+    def redraw(self):
+        self._w.set_text(self.markup)
+        self.occupant_data.parent.host.redraw()  # FIXME: should not be necessary
+
+    def selectable(self):
+        return True
+
+    def keypress(self, size, key):
+        return key
+
+    def get_cursor_coords(self, size):
+        return 0, 0
+
+    def render(self, size, focus=False):
+        # Text widget doesn't render cursor, but we want one
+        # so we add it here
+        canvas = urwid.CompositeCanvas(self._w.render(size, focus))
+        if focus:
+            canvas.set_cursor(self.get_cursor_coords(size))
+        return canvas
+
+    def _generate_markup(self):
+        # TODO: role and affiliation are shown in a Q&D way
+        #       should be more intuitive and themable
+        o = self.occupant_data
+        markup = []
+        markup.append(
+            ("info_msg", "{}{} ".format(o.role[0].upper(), o.affiliation[0].upper()))
+        )
+        markup.append(o.nick)
+        if o.state is not None:
+            markup.append(" {}".format(C.CHAT_STATE_ICON[o.state]))
+        return markup
+
+    # events
+    def update(self, update_dict=None):
+        self.redraw()
+
+
+class OccupantsWidget(urwid.WidgetWrap):
+    def __init__(self, parent):
+        self.parent = parent
+        self.occupants_walker = urwid.SimpleListWalker([])
+        self.occupants_footer = urwid.Text("", align="center")
+        self.update_footer()
+        occupants_widget = urwid.Frame(
+            urwid.ListBox(self.occupants_walker), footer=self.occupants_footer
+        )
+        super(OccupantsWidget, self).__init__(occupants_widget)
+        occupants_list = sorted(list(self.parent.occupants.keys()), key=lambda o: o.lower())
+        for occupant in occupants_list:
+            occupant_data = self.parent.occupants[occupant]
+            self.occupants_walker.append(OccupantWidget(occupant_data))
+
+    def clear(self):
+        del self.occupants_walker[:]
+
+    def update_footer(self):
+        """update footer widget"""
+        txt = OCCUPANTS_FOOTER.format(len(self.parent.occupants))
+        self.occupants_footer.set_text(txt)
+
+    def get_nicks(self, start=""):
+        """Return nicks of all occupants
+
+        @param start(unicode): only return nicknames which start with this text
+        """
+        return [
+            w.nick
+            for w in self.occupants_walker
+            if isinstance(w, OccupantWidget) and w.nick.startswith(start)
+        ]
+
+    def addUser(self, occupant_data):
+        """add a user to the list"""
+        bisect.insort(self.occupants_walker, OccupantWidget(occupant_data))
+        self.update_footer()
+        self.parent.host.redraw()  # FIXME: should not be necessary
+
+    def removeUser(self, occupant_data):
+        """remove a user from the list"""
+        for widget in occupant_data.widgets:
+            self.occupants_walker.remove(widget)
+        self.update_footer()
+        self.parent.host.redraw()  # FIXME: should not be necessary
+
+
+class Chat(LiberviaTUIWidget, quick_chat.QuickChat):
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None,
+                 subject=None, statuses=None, profiles=None):
+        self.filters = []  # list of filter callbacks to apply
+        self.mess_walker = urwid.SimpleListWalker([])
+        self.mess_widgets = urwid.ListBox(self.mess_walker)
+        self.chat_widget = urwid.Frame(self.mess_widgets)
+        self.chat_colums = urwid.Columns([("weight", 8, self.chat_widget)])
+        self.pile = urwid.Pile([self.chat_colums])
+        LiberviaTUIWidget.__init__(self, self.pile, target)
+        quick_chat.QuickChat.__init__(
+            self, host, target, type_, nick, occupants, subject, statuses,
+            profiles=profiles
+        )
+
+        # we must adapt the behaviour with the type
+        if type_ == C.CHAT_GROUP:
+            if len(self.chat_colums.contents) == 1:
+                self.occupants_widget = OccupantsWidget(self)
+                self.occupants_panel = sat_widgets.VerticalSeparator(
+                    self.occupants_widget
+                )
+                self._append_occupants_panel()
+                self.host.addListener("presence", self.presence_listener, [profiles])
+
+        # focus marker is a separator indicated last visible message before focus was lost
+        self.focus_marker = None  # link to current marker
+        self.focus_marker_set = None  # True if a new marker has been inserted
+        self.show_timestamp = True
+        self.show_short_nick = False
+        self.show_title = 1  # 0: clip title; 1: full title; 2: no title
+        self.post_init()
+
+    @property
+    def message_widgets_rev(self):
+        return reversed(self.mess_walker)
+
+    def keypress(self, size, key):
+        if key == a_key["OCCUPANTS_HIDE"]:  # user wants to (un)hide the occupants panel
+            if self.type == C.CHAT_GROUP:
+                widgets = [widget for (widget, options) in self.chat_colums.contents]
+                if self.occupants_panel in widgets:
+                    self._remove_occupants_panel()
+                else:
+                    self._append_occupants_panel()
+        elif key == a_key["TIMESTAMP_HIDE"]:  # user wants to (un)hide timestamp
+            self.show_timestamp = not self.show_timestamp
+            self.redraw()
+        elif key == a_key["SHORT_NICKNAME"]:  # user wants to (not) use short nick
+            self.show_short_nick = not self.show_short_nick
+            self.redraw()
+        elif (key == a_key["SUBJECT_SWITCH"]):
+            # user wants to (un)hide group's subject or change its apperance
+            if self.subject:
+                self.show_title = (self.show_title + 1) % 3
+                if self.show_title == 0:
+                    self.set_subject(self.subject, "clip")
+                elif self.show_title == 1:
+                    self.set_subject(self.subject, "space")
+                elif self.show_title == 2:
+                    self.chat_widget.header = None
+                self._invalidate()
+        elif key == a_key["GOTO_BOTTOM"]:  # user wants to focus last message
+            self.mess_widgets.focus_position = len(self.mess_walker) - 1
+
+        return super(Chat, self).keypress(size, key)
+
+    def completion(self, text, completion_data):
+        """Completion method which complete nicknames in group chat
+
+        for params, see [sat_widgets.AdvancedEdit]
+        """
+        if self.type != C.CHAT_GROUP:
+            return text
+
+        space = text.rfind(" ")
+        start = text[space + 1 :]
+        words = self.occupants_widget.get_nicks(start)
+        if not words:
+            return text
+        try:
+            word_idx = words.index(completion_data["last_word"]) + 1
+        except (KeyError, ValueError):
+            word_idx = 0
+        else:
+            if word_idx == len(words):
+                word_idx = 0
+        word = completion_data["last_word"] = words[word_idx]
+        return "{}{}{}".format(text[: space + 1], word, ": " if space < 0 else "")
+
+    def get_menu(self):
+        """Return Menu bar"""
+        menu = sat_widgets.Menu(self.host.loop)
+        if self.type == C.CHAT_GROUP:
+            self.host.add_menus(menu, C.MENU_ROOM, {"room_jid": self.target.bare})
+            game = _("Game")
+            menu.add_menu(game, "Tarot", self.on_tarot_request)
+        elif self.type == C.CHAT_ONE2ONE:
+            # FIXME: self.target is a bare jid, we need to check that
+            contact_list = self.host.contact_lists[self.profile]
+            if not self.target.resource:
+                full_jid = contact_list.get_full_jid(self.target)
+            else:
+                full_jid = self.target
+            self.host.add_menus(menu, C.MENU_SINGLE, {"jid": full_jid})
+        return menu
+
+    def set_filter(self, args):
+        """set filtering of messages
+
+        @param args(list[unicode]): filters following syntax "[filter]=[value]"
+            empty list to clear all filters
+            only lang=XX is handled for now
+        """
+        del self.filters[:]
+        if args:
+            if args[0].startswith("lang="):
+                lang = args[0][5:].strip()
+                self.filters.append(lambda mess_data: lang in mess_data.message)
+
+        self.print_messages()
+
+    def presence_listener(self, entity, show, priority, statuses, profile):
+        """Update entity's presence status
+
+        @param entity (jid.JID): entity updated
+        @param show: availability
+        @param priority: resource's priority
+        @param statuses: dict of statuses
+        @param profile: %(doc_profile)s
+        """
+        # FIXME: disable for refactoring, need to be checked and re-enabled
+        return
+        # assert self.type == C.CHAT_GROUP
+        # if entity.bare != self.target:
+        #     return
+        # self.update(entity)
+
+    def create_message(self, message):
+        self.appendMessage(message)
+
+    def _scrollDown(self):
+        """scroll down message only if we are already at the bottom (minus 1)"""
+        current_focus = self.mess_widgets.focus_position
+        bottom = len(self.mess_walker) - 1
+        if current_focus == bottom - 1:
+            self.mess_widgets.focus_position = bottom  # scroll down
+        self.host.redraw()  # FIXME: should not be necessary
+
+    def appendMessage(self, message, minor_notifs=True):
+        """Create a MessageWidget and append it
+
+        Can merge info messages together if desirable (e.g.: multiple joined/leave)
+        @param message(quick_chat.Message): message to add
+        @param minor_notifs(boolean): if True, basic notifications are allowed
+            If False, notification are not shown except if we have an important one
+            (like a mention).
+            False is generally used when printing history, when we don't want every
+            message to be notified.
+        """
+        if message.attachments:
+            # FIXME: Q&D way to see attachments in LiberviaTUI
+            #   it should be done in a more user friendly way
+            for lang, body in message.message.items():
+                for attachment in message.attachments:
+                    if 'url' in attachment:
+                        body+=f"\n{attachment['url']}"
+                    elif 'path' in attachment:
+                        path = Path(attachment['path'])
+                        body+=f"\n{path.as_uri()}"
+                    else:
+                        log.warning(f'No "url" nor "path" in attachment: {attachment}')
+                    message.message[lang] = body
+
+        if self.filters:
+            if not all([f(message) for f in self.filters]):
+                return
+
+        if self.handle_user_moved(message):
+            return
+
+        if ((self.host.selected_widget != self or not self.host.x_notify.has_focus())
+            and self.focus_marker_set is not None):
+            if not self.focus_marker_set and not self._locked and self.mess_walker:
+                if self.focus_marker is not None:
+                    try:
+                        self.mess_walker.remove(self.focus_marker)
+                    except ValueError:
+                        # self.focus_marker may not be in mess_walker anymore if
+                        # mess_walker has been cleared, e.g. when showing search
+                        # result or using :history command
+                        pass
+                self.focus_marker = urwid.Divider("—")
+                self.mess_walker.append(self.focus_marker)
+                self.focus_marker_set = True
+                self._scrollDown()
+        else:
+            if self.focus_marker_set:
+                self.focus_marker_set = False
+
+        wid = MessageWidget(message)
+        self.mess_walker.append(wid)
+        self._scrollDown()
+        if self.is_user_moved(message):
+            return  # no notification for moved messages
+
+        # notifications
+
+        if self._locked:
+            # we don't want notifications when locked
+            # because that's history messages
+            return
+
+        if wid.mess_data.mention:
+            from_jid = wid.mess_data.from_jid
+            msg = _(
+                "You have been mentioned by {nick} in {room}".format(
+                    nick=wid.mess_data.nick, room=self.target
+                )
+            )
+            self.host.notify(
+                C.NOTIFY_MENTION, from_jid, msg, widget=self, profile=self.profile
+            )
+        elif not minor_notifs:
+            return
+        elif self.type == C.CHAT_ONE2ONE:
+            from_jid = wid.mess_data.from_jid
+            msg = _("{entity} is talking to you".format(entity=from_jid))
+            self.host.notify(
+                C.NOTIFY_MESSAGE, from_jid, msg, widget=self, profile=self.profile
+            )
+        else:
+            self.host.notify(
+                C.NOTIFY_MESSAGE, self.target, widget=self, profile=self.profile
+            )
+
+    def addUser(self, nick):
+        occupant = super(Chat, self).addUser(nick)
+        self.occupants_widget.addUser(occupant)
+
+    def removeUser(self, occupant_data):
+        occupant = super(Chat, self).removeUser(occupant_data)
+        if occupant is not None:
+            self.occupants_widget.removeUser(occupant)
+
+    def occupants_clear(self):
+        super(Chat, self).occupants_clear()
+        self.occupants_widget.clear()
+
+    def _occupants_clicked(self, occupant, clicked_wid):
+        assert self.type == C.CHAT_GROUP
+        contact_list = self.host.contact_lists[self.profile]
+
+        # we have a click on a nick, we need to create the widget if it doesn't exists
+        self.get_or_create_private_widget(occupant.jid)
+
+        # now we select the new window
+        for contact_list in self.host.widgets.get_widgets(
+            ContactList, profiles=(self.profile,)
+        ):
+            contact_list.set_focus(occupant.jid, True)
+
+    def _append_occupants_panel(self):
+        self.chat_colums.contents.append((self.occupants_panel, ("weight", 2, False)))
+
+    def _remove_occupants_panel(self):
+        for widget, options in self.chat_colums.contents:
+            if widget is self.occupants_panel:
+                self.chat_colums.contents.remove((widget, options))
+                break
+
+    def add_game_panel(self, widget):
+        """Insert a game panel to this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        assert len(self.pile.contents) == 1
+        self.pile.contents.insert(0, (widget, ("weight", 1)))
+        self.pile.contents.insert(1, (urwid.Filler(urwid.Divider("-"), ("fixed", 1))))
+        self.host.redraw()
+
+    def remove_game_panel(self, widget):
+        """Remove the game panel from this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        assert len(self.pile.contents) == 3
+        del self.pile.contents[0]
+        self.host.redraw()
+
+    def set_subject(self, subject, wrap="space"):
+        """Set title for a group chat"""
+        quick_chat.QuickChat.set_subject(self, subject)
+        self.subj_wid = urwid.Text(
+            str(subject.replace("\n", "|") if wrap == "clip" else subject),
+            align="left" if wrap == "clip" else "center",
+            wrap=wrap,
+        )
+        self.chat_widget.header = urwid.AttrMap(self.subj_wid, "title")
+        self.host.redraw()
+
+    ## Messages
+
+    def print_messages(self, clear=True):
+        """generate message widgets
+
+        @param clear(bool): clear message before printing if true
+        """
+        if clear:
+            del self.mess_walker[:]
+        for message in self.messages.values():
+            self.appendMessage(message, minor_notifs=False)
+
+    def redraw(self):
+        """redraw all messages"""
+        for w in self.mess_walker:
+            try:
+                w.redraw()
+            except AttributeError:
+                pass
+
+    def update_history(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile="@NONE@"):
+        del self.mess_walker[:]
+        if filters and "search" in filters:
+            self.mess_walker.append(
+                urwid.Text(
+                    _("Results for searching the globbing pattern: {}").format(
+                        filters["search"]
+                    )
+                )
+            )
+            self.mess_walker.append(
+                urwid.Text(_("Type ':history <lines>' to reset the chat history"))
+            )
+        super(Chat, self).update_history(size, filters, profile)
+
+    def _on_history_printed(self):
+        """Refresh or scroll down the focus after the history is printed"""
+        self.print_messages(clear=False)
+        super(Chat, self)._on_history_printed()
+
+    def on_private_created(self, widget):
+        self.host.contact_lists[widget.profile].set_special(
+            widget.target, C.CONTACT_SPECIAL_GROUP
+        )
+
+    def on_selected(self):
+        self.focus_marker_set = False
+
+    def notify(self, contact="somebody", msg=""):
+        """Notify the user of a new message if Libervia TUI doesn't have the focus.
+
+        @param contact (unicode): contact who wrote to the users
+        @param msg (unicode): the message that has been received
+        """
+        # FIXME: not called anymore after refactoring
+        if msg == "":
+            return
+        if self.mess_widgets.get_focus()[1] == len(self.mess_walker) - 2:
+            # we don't change focus if user is not at the bottom
+            # as that mean that he is probably watching discussion history
+            self.mess_widgets.focus_position = len(self.mess_walker) - 1
+        self.host.redraw()
+        if not self.host.x_notify.has_focus():
+            if self.type == C.CHAT_ONE2ONE:
+                self.host.x_notify.send_notification(
+                    _("LiberviaTUI: %s is talking to you") % contact
+                )
+            elif self.nick is not None and self.nick.lower() in msg.lower():
+                self.host.x_notify.send_notification(
+                    _("LiberviaTUI: %(user)s mentioned you in room '%(room)s'")
+                    % {"user": contact, "room": self.target}
+                )
+
+    # MENU EVENTS #
+    def on_tarot_request(self, menu):
+        # TODO: move this to plugin_misc_tarot with dynamic menu
+        if len(self.occupants) != 4:
+            self.host.show_pop_up(
+                sat_widgets.Alert(
+                    _("Can't start game"),
+                    _(
+                        "You need to be exactly 4 peoples in the room to start a Tarot game"
+                    ),
+                    ok_cb=self.host.remove_pop_up,
+                )
+            )
+        else:
+            self.host.bridge.tarot_game_create(
+                self.target, list(self.occupants), self.profile
+            )
+
+    # MISC EVENTS #
+
+    def on_delete(self):
+        # FIXME: to be checked after refactoring
+        super(Chat, self).on_delete()
+        if self.type == C.CHAT_GROUP:
+            self.host.removeListener("presence", self.presence_listener)
+
+    def on_chat_state(self, from_jid, state, profile):
+        super(Chat, self).on_chat_state(from_jid, state, profile)
+        if self.type == C.CHAT_ONE2ONE:
+            self.title_dynamic = C.CHAT_STATE_ICON[state]
+            self.host.redraw()  # FIXME: should not be necessary
+
+    def _on_subject_dialog_cb(self, button, dialog):
+        self.change_subject(dialog.text)
+        self.host.remove_pop_up(dialog)
+
+    def on_subject_dialog(self, new_subject=None):
+        dialog = sat_widgets.InputDialog(
+            _("Change title"),
+            _("Enter the new title"),
+            default_txt=new_subject if new_subject is not None else self.subject,
+        )
+        dialog.set_callback("ok", self._on_subject_dialog_cb, dialog)
+        dialog.set_callback("cancel", lambda __: self.host.remove_pop_up(dialog))
+        self.host.show_pop_up(dialog)
+
+
+quick_widgets.register(quick_chat.QuickChat, Chat)
+quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/config.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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/>.
+
+"""This module manage configuration specific to LiberviaTUI"""
+
+from libervia.tui.constants import Const as C
+from libervia.tui.keys import action_key_map
+import configparser
+
+
+def apply_config(host):
+    """Parse configuration and apply found change
+
+    raise: can raise various Exceptions if configuration is not good
+    """
+    config = configparser.SafeConfigParser()
+    config.read(C.CONFIG_FILES)
+    try:
+        options = config.items(C.CONFIG_SECTION)
+    except configparser.NoSectionError:
+        options = []
+    shortcuts = {}
+    for name, value in options:
+        if name.startswith(C.CONFIG_OPT_KEY_PREFIX.lower()):
+            action = name[len(C.CONFIG_OPT_KEY_PREFIX) :].upper()
+            shortcut = value
+            if not action or not shortcut:
+                raise ValueError("Bad option: {} = {}".format(name, value))
+            shortcuts[action] = shortcut
+        if name == "disable_mouse":
+            host.loop.screen.set_mouse_tracking(False)
+
+    action_key_map.replace(shortcuts)
+    action_key_map.check_namespaces()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/constants.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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 libervia.frontends.quick_frontend import constants
+
+
+class Const(constants.Const):
+
+    APP_NAME = "Libervia TUI"
+    APP_COMPONENT = "TUI"
+    APP_NAME_ALT = "LiberviaTUI"
+    APP_NAME_FILE = "libervia_tui"
+    CONFIG_SECTION = APP_COMPONENT.lower()
+    PALETTE = [
+        ("title", "black", "light gray", "standout,underline"),
+        ("title_focus", "white,bold", "light gray", "standout,underline"),
+        ("selected", "default", "dark red"),
+        ("selected_focus", "default,bold", "dark red"),
+        ("default", "default", "default"),
+        ("default_focus", "default,bold", "default"),
+        ("cl_notifs", "yellow", "default"),
+        ("cl_notifs_focus", "yellow,bold", "default"),
+        ("cl_mention", "light red", "default"),
+        ("cl_mention_focus", "dark red,bold", "default"),
+        # Messages
+        ("date", "light gray", "default"),
+        ("my_nick", "dark red,bold", "default"),
+        ("other_nick", "dark cyan,bold", "default"),
+        ("info_msg", "yellow", "default", "bold"),
+        ("msg_lang", "dark cyan", "default"),
+        ("msg_mention", "dark red, bold", "default"),
+        ("msg_status_received", "light green, bold", "default"),
+        ("menubar", "light gray,bold", "dark red"),
+        ("menubar_focus", "light gray,bold", "dark green"),
+        ("selected_menu", "light gray,bold", "dark green"),
+        ("menuitem", "light gray,bold", "dark red"),
+        ("menuitem_focus", "light gray,bold", "dark green"),
+        ("notifs", "black,bold", "yellow"),
+        ("notifs_focus", "dark red", "yellow"),
+        ("card_neutral", "dark gray", "white", "standout,underline"),
+        ("card_neutral_selected", "dark gray", "dark green", "standout,underline"),
+        ("card_special", "brown", "white", "standout,underline"),
+        ("card_special_selected", "brown", "dark green", "standout,underline"),
+        ("card_red", "dark red", "white", "standout,underline"),
+        ("card_red_selected", "dark red", "dark green", "standout,underline"),
+        ("card_black", "black", "white", "standout,underline"),
+        ("card_black_selected", "black", "dark green", "standout,underline"),
+        ("directory", "dark cyan, bold", "default"),
+        ("directory_focus", "dark cyan, bold", "dark green"),
+        ("separator", "brown", "default"),
+        ("warning", "light red", "default"),
+        ("progress_normal", "default", "brown"),
+        ("progress_complete", "default", "dark green"),
+        ("show_disconnected", "dark gray", "default"),
+        ("show_normal", "default", "default"),
+        ("show_normal_focus", "default, bold", "default"),
+        ("show_chat", "dark green", "default"),
+        ("show_chat_focus", "dark green, bold", "default"),
+        ("show_away", "brown", "default"),
+        ("show_away_focus", "brown, bold", "default"),
+        ("show_dnd", "dark red", "default"),
+        ("show_dnd_focus", "dark red, bold", "default"),
+        ("show_xa", "dark red", "default"),
+        ("show_xa_focus", "dark red, bold", "default"),
+        ("resource", "light blue", "default"),
+        ("resource_main", "dark blue", "default"),
+        ("status", "yellow", "default"),
+        ("status_focus", "yellow, bold", "default"),
+        ("param_selected", "default, bold", "dark red"),
+        ("table_selected", "default, bold", "default"),
+    ]
+    PRESENCE = {
+        "unavailable": ("⨯", "show_disconnected"),
+        "": ("✔", "show_normal"),
+        "chat": ("✆", "show_chat"),
+        "away": ("✈", "show_away"),
+        "dnd": ("✖", "show_dnd"),
+        "xa": ("☄", "show_xa"),
+    }
+    LOG_OPT_SECTION = APP_NAME.lower()
+    LOG_OPT_OUTPUT = (
+        "output",
+        constants.Const.LOG_OPT_OUTPUT_SEP + constants.Const.LOG_OPT_OUTPUT_MEMORY,
+    )
+    CONFIG_OPT_KEY_PREFIX = "KEY_"
+
+    MENU_ID_MAIN = "MAIN_MENU"
+    MENU_ID_WIDGET = "WIDGET_MENU"
+
+    MODE_NORMAL = "NORMAL"
+    MODE_INSERTION = "INSERTION"
+    MODE_COMMAND = "COMMAND"
+
+    GROUP_DATA_FOLDED = "folded"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/contact_list.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,364 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
+import urwid
+from urwid_satext import sat_widgets
+from libervia.frontends.quick_frontend.quick_contact_list import QuickContactList
+from libervia.tui.status import StatusBar
+from libervia.tui.constants import Const as C
+from libervia.tui.keys import action_key_map as a_key
+from libervia.tui.widget import LiberviaTUIWidget
+from libervia.frontends.tools import jid
+from libervia.backend.core import log as logging
+
+log = logging.getLogger(__name__)
+from libervia.frontends.quick_frontend import quick_widgets
+
+
+class ContactList(LiberviaTUIWidget, QuickContactList):
+    PROFILES_MULTIPLE = False
+    PROFILES_ALLOW_NONE = False
+    signals = ["click", "change"]
+    # FIXME: Only single profile is managed so far
+
+    def __init__(
+        self, host, target, on_click=None, on_change=None, user_data=None, profiles=None
+    ):
+        QuickContactList.__init__(self, host, profiles)
+        self.contact_list = self.host.contact_lists[self.profile]
+
+        # we now build the widget
+        self.status_bar = StatusBar(host)
+        self.frame = sat_widgets.FocusFrame(self._build_list(), None, self.status_bar)
+        LiberviaTUIWidget.__init__(self, self.frame, _("Contacts"))
+        if on_click:
+            urwid.connect_signal(self, "click", on_click, user_data)
+        if on_change:
+            urwid.connect_signal(self, "change", on_change, user_data)
+        self.host.addListener("notification", self.on_notification, [self.profile])
+        self.host.addListener("notificationsClear", self.on_notification, [self.profile])
+        self.post_init()
+
+    def update(self, entities=None, type_=None, profile=None):
+        """Update display, keep focus"""
+        # FIXME: full update is done each time, must handle entities, type_ and profile
+        widget, position = self.frame.body.get_focus()
+        self.frame.body = self._build_list()
+        if position:
+            try:
+                self.frame.body.focus_position = position
+            except IndexError:
+                pass
+        self._invalidate()
+        self.host.redraw()  # FIXME: check if can be avoided
+
+    def keypress(self, size, key):
+        # FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
+        #        and FOCUS_UP/DOWN is transwmitter to parent if we are respectively on the first or last element
+        if key in sat_widgets.FOCUS_KEYS:
+            if (
+                key == a_key["FOCUS_SWITCH"]
+                or (key == a_key["FOCUS_UP"] and self.frame.focus_position == "body")
+                or (key == a_key["FOCUS_DOWN"] and self.frame.focus_position == "footer")
+            ):
+                return key
+        if key == a_key["STATUS_HIDE"]:  # user wants to (un)hide contacts' statuses
+            self.contact_list.show_status = not self.contact_list.show_status
+            self.update()
+        elif (
+            key == a_key["DISCONNECTED_HIDE"]
+        ):  # user wants to (un)hide disconnected contacts
+            self.host.bridge.param_set(
+                C.SHOW_OFFLINE_CONTACTS,
+                C.bool_const(not self.contact_list.show_disconnected),
+                "General",
+                profile_key=self.profile,
+            )
+        elif key == a_key["RESOURCES_HIDE"]:  # user wants to (un)hide contacts resources
+            self.contact_list.show_resources(not self.contact_list.show_resources)
+            self.update()
+        return super(ContactList, self).keypress(size, key)
+
+    # QuickWidget methods
+
+    @staticmethod
+    def get_widget_hash(target, profiles):
+        profiles = sorted(profiles)
+        return tuple(profiles)
+
+    # modify the contact list
+
+    def set_focus(self, text, select=False):
+        """give focus to the first element that matches the given text. You can also
+        pass in text a libervia.frontends.tools.jid.JID (it's a subclass of unicode).
+
+        @param text: contact group name, contact or muc userhost, muc private dialog jid
+        @param select: if True, the element is also clicked
+        """
+        idx = 0
+        for widget in self.frame.body.body:
+            try:
+                if isinstance(widget, sat_widgets.ClickableText):
+                    # contact group
+                    value = widget.get_value()
+                elif isinstance(widget, sat_widgets.SelectableText):
+                    # contact or muc
+                    value = widget.data
+                else:
+                    # Divider instance
+                    continue
+                # there's sometimes a leading space
+                if text.strip() == value.strip():
+                    self.frame.body.focus_position = idx
+                    if select:
+                        self._contact_clicked(False, widget, True)
+                    return
+            except AttributeError:
+                pass
+            idx += 1
+
+        log.debug("Not element found for {} in set_focus".format(text))
+
+    # events
+
+    def _group_clicked(self, group_wid):
+        group = group_wid.get_value()
+        data = self.contact_list.get_group_data(group)
+        data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False)
+        self.set_focus(group)
+        self.update()
+
+    def _contact_clicked(self, use_bare_jid, contact_wid, selected):
+        """Method called when a contact is clicked
+
+        @param use_bare_jid: True if use_bare_jid is set in self._build_entity_widget.
+        @param contact_wid: widget of the contact, must have the entity set in data attribute
+        @param selected: boolean returned by the widget, telling if it is selected
+        """
+        entity = contact_wid.data
+        self.host.mode_hint(C.MODE_INSERTION)
+        self._emit("click", entity)
+
+    def on_notification(self, entity, notif, profile):
+        notifs = list(self.host.get_notifs(C.ENTITY_ALL, profile=self.profile))
+        if notifs:
+            self.title_dynamic = "({})".format(len(notifs))
+        else:
+            self.title_dynamic = None
+        self.host.redraw()  # FIXME: should not be necessary
+
+    # Methods to build the widget
+
+    def _build_entity_widget(
+        self,
+        entity,
+        keys=None,
+        use_bare_jid=False,
+        with_notifs=True,
+        with_show_attr=True,
+        markup_prepend=None,
+        markup_append=None,
+        special=False,
+    ):
+        """Build one contact markup data
+
+        @param entity (jid.JID): entity to build
+        @param keys (iterable): value to markup, in preferred order.
+            The first available key will be used.
+            If key starts with "cache_", it will be checked in cache,
+            else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
+            If nothing full or keys is None, full entity is used.
+        @param use_bare_jid (bool): if True, use bare jid for selected comparisons
+        @param with_notifs (bool): if True, show notification count
+        @param with_show_attr (bool): if True, show color corresponding to presence status
+        @param markup_prepend (list): markup to prepend to the generated one before building the widget
+        @param markup_append (list): markup to append to the generated one before building the widget
+        @param special (bool): True if entity is a special one
+        @return (list): markup data are expected by Urwid text widgets
+        """
+        markup = []
+        if use_bare_jid:
+            selected = {entity.bare for entity in self.contact_list._selected}
+        else:
+            selected = self.contact_list._selected
+        if keys is None:
+            entity_txt = entity
+        else:
+            cache = self.contact_list.getCache(entity)
+            for key in keys:
+                if key.startswith("cache_"):
+                    entity_txt = cache.get(key[6:])
+                else:
+                    entity_txt = getattr(entity, key)
+                if entity_txt:
+                    break
+            if not entity_txt:
+                entity_txt = entity
+
+        if with_show_attr:
+            show = self.contact_list.getCache(entity, C.PRESENCE_SHOW, default=None)
+            if show is None:
+                show = C.PRESENCE_UNAVAILABLE
+            show_icon, entity_attr = C.PRESENCE.get(show, ("", "default"))
+            markup.insert(0, "{} ".format(show_icon))
+        else:
+            entity_attr = "default"
+
+        notifs = list(
+            self.host.get_notifs(entity, exact_jid=special, profile=self.profile)
+        )
+        mentions = list(
+                self.host.get_notifs(entity.bare, C.NOTIFY_MENTION, profile=self.profile)
+            )
+        if notifs or mentions:
+            attr = 'cl_mention' if mentions else 'cl_notifs'
+            header = [(attr, "({})".format(len(notifs) + len(mentions))), " "]
+        else:
+            header = ""
+
+        markup.append((entity_attr, entity_txt))
+        if markup_prepend:
+            markup.insert(0, markup_prepend)
+        if markup_append:
+            markup.extend(markup_append)
+
+        widget = sat_widgets.SelectableText(
+            markup, selected=entity in selected, header=header
+        )
+        widget.data = entity
+        widget.comp = entity_txt.lower()  # value to use for sorting
+        urwid.connect_signal(
+            widget, "change", self._contact_clicked, user_args=[use_bare_jid]
+        )
+        return widget
+
+    def _build_entities(self, content, entities):
+        """Add entity representation in widget list
+
+        @param content: widget list, e.g. SimpleListWalker
+        @param entities (iterable): iterable of JID to display
+        """
+        if not entities:
+            return
+        widgets = []  # list of built widgets
+
+        for entity in entities:
+            if (
+                entity in self.contact_list._specials
+                or not self.contact_list.entity_visible(entity)
+            ):
+                continue
+            markup_extra = []
+            if self.contact_list.show_resources:
+                for resource in self.contact_list.getCache(entity, C.CONTACT_RESOURCES):
+                    resource_disp = (
+                        "resource_main"
+                        if resource
+                        == self.contact_list.getCache(entity, C.CONTACT_MAIN_RESOURCE)
+                        else "resource",
+                        "\n  " + resource,
+                    )
+                    markup_extra.append(resource_disp)
+                    if self.contact_list.show_status:
+                        status = self.contact_list.getCache(
+                            jid.JID("%s/%s" % (entity, resource)), "status", default=None
+                        )
+                        status_disp = ("status", "\n    " + status) if status else ""
+                        markup_extra.append(status_disp)
+
+            else:
+                if self.contact_list.show_status:
+                    status = self.contact_list.getCache(entity, "status", default=None)
+                    status_disp = ("status", "\n  " + status) if status else ""
+                    markup_extra.append(status_disp)
+            widget = self._build_entity_widget(
+                entity,
+                ("cache_nick", "cache_name", "node"),
+                use_bare_jid=True,
+                markup_append=markup_extra,
+            )
+            widgets.append(widget)
+
+        widgets.sort(key=lambda widget: widget.comp)
+
+        for widget in widgets:
+            content.append(widget)
+
+    def _build_specials(self, content):
+        """Build the special entities"""
+        specials = sorted(self.contact_list.get_specials())
+        current = None
+        for entity in specials:
+            if current is not None and current.bare == entity.bare:
+                # nested entity (e.g. MUC private conversations)
+                widget = self._build_entity_widget(
+                    entity, ("resource",), markup_prepend="  ", special=True
+                )
+            else:
+                # the special widgets
+                if entity.resource:
+                    widget = self._build_entity_widget(entity, ("resource",), special=True)
+                else:
+                    widget = self._build_entity_widget(
+                        entity,
+                        ("cache_nick", "cache_name", "node"),
+                        with_show_attr=False,
+                        special=True,
+                    )
+            content.append(widget)
+
+    def _build_list(self):
+        """Build the main contact list widget"""
+        content = urwid.SimpleListWalker([])
+
+        self._build_specials(content)
+        if self.contact_list._specials:
+            content.append(urwid.Divider("="))
+
+        groups = list(self.contact_list._groups)
+        groups.sort(key=lambda x: x.lower() if x else '')
+        for group in groups:
+            data = self.contact_list.get_group_data(group)
+            folded = data.get(C.GROUP_DATA_FOLDED, False)
+            jids = list(data["jids"])
+            if group is not None and (
+                self.contact_list.any_entity_visible(jids)
+                or self.contact_list.show_empty_groups
+            ):
+                header = "[-]" if not folded else "[+]"
+                widget = sat_widgets.ClickableText(group, header=header + " ")
+                content.append(widget)
+                urwid.connect_signal(widget, "click", self._group_clicked)
+            if not folded:
+                self._build_entities(content, jids)
+        not_in_roster = (
+            set(self.contact_list._cache)
+            .difference(self.contact_list._roster)
+            .difference(self.contact_list._specials)
+            .difference((self.contact_list.whoami.bare,))
+        )
+        if not_in_roster:
+            content.append(urwid.Divider("-"))
+            self._build_entities(content, not_in_roster)
+
+        return urwid.ListBox(content)
+
+
+quick_widgets.register(QuickContactList, ContactList)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/game_tarot.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,397 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
+import urwid
+from urwid_satext import sat_widgets
+from libervia.frontends.tools.games import TarotCard
+from libervia.frontends.quick_frontend.quick_game_tarot import QuickTarotGame
+from libervia.tui import xmlui
+from libervia.tui.keys import action_key_map as a_key
+
+
+class CardDisplayer(urwid.Text):
+    """Show a card"""
+
+    signals = ["click"]
+
+    def __init__(self, card):
+        self.__selected = False
+        self.card = card
+        urwid.Text.__init__(self, card.get_attr_text())
+
+    def selectable(self):
+        return True
+
+    def keypress(self, size, key):
+        if key == a_key["CARD_SELECT"]:
+            self.select(not self.__selected)
+            self._emit("click")
+        return key
+
+    def mouse_event(self, size, event, button, x, y, focus):
+        if urwid.is_mouse_event(event) and button == 1:
+            self.select(not self.__selected)
+            self._emit("click")
+            return True
+
+        return False
+
+    def select(self, state=True):
+        self.__selected = state
+        attr, txt = self.card.get_attr_text()
+        if self.__selected:
+            attr += "_selected"
+        self.set_text((attr, txt))
+        self._invalidate()
+
+    def is_selected(self):
+        return self.__selected
+
+    def get_card(self):
+        return self.card
+
+    def render(self, size, focus=False):
+        canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus))
+        if focus:
+            canvas.set_cursor((0, 0))
+        return canvas
+
+
+class Hand(urwid.WidgetWrap):
+    """Used to display several cards, and manage a hand"""
+
+    signals = ["click"]
+
+    def __init__(self, hand=[], selectable=False, on_click=None, user_data=None):
+        """@param hand: list of Card"""
+        self.__selectable = selectable
+        self.columns = urwid.Columns([], dividechars=1)
+        if on_click:
+            urwid.connect_signal(self, "click", on_click, user_data)
+        if hand:
+            self.update(hand)
+        urwid.WidgetWrap.__init__(self, self.columns)
+
+    def selectable(self):
+        return self.__selectable
+
+    def keypress(self, size, key):
+
+        if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]:
+            return self.columns.keypress(size, key)
+        else:
+            # No card displayed, we still have to manage the clicks
+            if key == a_key["CARD_SELECT"]:
+                self._emit("click", None)
+            return key
+
+    def get_selected(self):
+        """Return a list of selected cards"""
+        _selected = []
+        for wid in self.columns.widget_list:
+            if isinstance(wid, CardDisplayer) and wid.is_selected():
+                _selected.append(wid.get_card())
+        return _selected
+
+    def update(self, hand):
+        """Update the hand displayed in this widget
+        @param hand: list of Card"""
+        try:
+            del self.columns.widget_list[:]
+            del self.columns.column_types[:]
+        except IndexError:
+            pass
+        self.columns.contents.append((urwid.Text(""), ("weight", 1, False)))
+        for card in hand:
+            widget = CardDisplayer(card)
+            self.columns.widget_list.append(widget)
+            self.columns.column_types.append(("fixed", 3))
+            urwid.connect_signal(widget, "click", self.__on_click)
+        self.columns.contents.append((urwid.Text(""), ("weight", 1, False)))
+        self.columns.focus_position = 1
+
+    def __on_click(self, card_wid):
+        self._emit("click", card_wid)
+
+
+class Card(TarotCard):
+    """This class is used to represent a card, logically
+    and give a text representation with attributes"""
+
+    SIZE = 3  # size of a displayed card
+
+    def __init__(self, suit, value):
+        """@param file: path of the PNG file"""
+        TarotCard.__init__(self, (suit, value))
+
+    def get_attr_text(self):
+        """return text representation of the card with attributes"""
+        try:
+            value = "%02i" % int(self.value)
+        except ValueError:
+            value = self.value[0].upper() + self.value[1]
+        if self.suit == "atout":
+            if self.value == "excuse":
+                suit = "c"
+            else:
+                suit = "A"
+            color = "neutral"
+        elif self.suit == "pique":
+            suit = "♠"
+            color = "black"
+        elif self.suit == "trefle":
+            suit = "♣"
+            color = "black"
+        elif self.suit == "coeur":
+            suit = "♥"
+            color = "red"
+        elif self.suit == "carreau":
+            suit = "♦"
+            color = "red"
+        if self.bout:
+            color = "special"
+        return ("card_%s" % color, "%s%s" % (value, suit))
+
+    def get_widget(self):
+        """Return a widget representing the card"""
+        return CardDisplayer(self)
+
+
+class Table(urwid.FlowWidget):
+    """Represent the cards currently on the table"""
+
+    def __init__(self):
+        self.top = self.left = self.bottom = self.right = None
+
+    def put_card(self, location, card):
+        """Put a card on the table
+        @param location: where to put the card (top, left, bottom or right)
+        @param card: Card to play or None"""
+        assert location in ["top", "left", "bottom", "right"]
+        assert isinstance(card, Card) or card == None
+        if [getattr(self, place) for place in ["top", "left", "bottom", "right"]].count(
+            None
+        ) == 0:
+            # If the table is full of card, we remove them
+            self.top = self.left = self.bottom = self.right = None
+        setattr(self, location, card)
+        self._invalidate()
+
+    def rows(self, size, focus=False):
+        return self.display_widget(size, focus).rows(size, focus)
+
+    def render(self, size, focus=False):
+        return self.display_widget(size, focus).render(size, focus)
+
+    def display_widget(self, size, focus):
+        cards = {}
+        max_col, = size
+        separator = " - "
+        margin = max((max_col - Card.SIZE) / 2, 0) * " "
+        margin_center = max((max_col - Card.SIZE * 2 - len(separator)) / 2, 0) * " "
+        for location in ["top", "left", "bottom", "right"]:
+            card = getattr(self, location)
+            cards[location] = card.get_attr_text() if card else Card.SIZE * " "
+        render_wid = [
+            urwid.Text([margin, cards["top"]]),
+            urwid.Text([margin_center, cards["left"], separator, cards["right"]]),
+            urwid.Text([margin, cards["bottom"]]),
+        ]
+        return urwid.Pile(render_wid)
+
+
+class TarotGame(QuickTarotGame, urwid.WidgetWrap):
+    """Widget for card games"""
+
+    def __init__(self, parent, referee, players):
+        QuickTarotGame.__init__(self, parent, referee, players)
+        self.load_cards()
+        self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), "center")])
+        # self.parent.host.debug()
+        self.table = Table()
+        self.center = urwid.Columns(
+            [
+                ("fixed", len(self.left_nick), urwid.Filler(urwid.Text(self.left_nick))),
+                urwid.Filler(self.table),
+                (
+                    "fixed",
+                    len(self.right_nick),
+                    urwid.Filler(urwid.Text(self.right_nick)),
+                ),
+            ]
+        )
+        """urwid.Pile([urwid.Padding(self.top_card_wid,'center'),
+                             urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)),
+                                            urwid.Padding(self.center_cards_wid,'center'),
+                                            ('fixed',len(self.right_nick),urwid.Text(self.right_nick))
+                                           ]),
+                             urwid.Padding(self.bottom_card_wid,'center')
+                             ])"""
+        self.hand_wid = Hand(selectable=True, on_click=self.on_click)
+        self.main_frame = urwid.Frame(
+            self.center, header=self.top, footer=self.hand_wid, focus_part="footer"
+        )
+        urwid.WidgetWrap.__init__(self, self.main_frame)
+        self.parent.host.bridge.tarot_game_ready(
+            self.player_nick, referee, self.parent.profile
+        )
+
+    def load_cards(self):
+        """Load all the cards in memory"""
+        QuickTarotGame.load_cards(self)
+        for value in list(map(str, list(range(1, 22)))) + ["excuse"]:
+            card = Card("atout", value)
+            self.cards[card.suit, card.value] = card
+            self.deck.append(card)
+        for suit in ["pique", "coeur", "carreau", "trefle"]:
+            for value in list(map(str, list(range(1, 11)))) + ["valet", "cavalier", "dame", "roi"]:
+                card = Card(suit, value)
+                self.cards[card.suit, card.value] = card
+                self.deck.append(card)
+
+    def tarot_game_new_handler(self, hand):
+        """Start a new game, with given hand"""
+        if hand is []:  # reset the display after the scores have been showed
+            self.reset_round()
+            for location in ["top", "left", "bottom", "right"]:
+                self.table.put_card(location, None)
+            self.parent.host.redraw()
+            self.parent.host.bridge.tarot_game_ready(
+                self.player_nick, self.referee, self.parent.profile
+            )
+            return
+        QuickTarotGame.tarot_game_new_handler(self, hand)
+        self.hand_wid.update(self.hand)
+        self.parent.host.redraw()
+
+    def tarot_game_choose_contrat_handler(self, xml_data):
+        """Called when the player has to select his contrat
+        @param xml_data: SàT xml representation of the form"""
+        form = xmlui.create(
+            self.parent.host,
+            xml_data,
+            title=_("Please choose your contrat"),
+            flags=["NO_CANCEL"],
+            profile=self.parent.profile,
+        )
+        form.show(valign="top")
+
+    def tarot_game_show_cards_handler(self, game_stage, cards, data):
+        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
+        QuickTarotGame.tarot_game_show_cards_handler(self, game_stage, cards, data)
+        self.center.widget_list[1] = urwid.Filler(Hand(self.to_show))
+        self.parent.host.redraw()
+
+    def tarot_game_your_turn_handler(self):
+        QuickTarotGame.tarot_game_your_turn_handler(self)
+
+    def tarot_game_score_handler(self, xml_data, winners, loosers):
+        """Called when the round is over, display the scores
+        @param xml_data: SàT xml representation of the form"""
+        if not winners and not loosers:
+            title = _("Draw game")
+        else:
+            title = _("You win \o/") if self.player_nick in winners else _("You loose :(")
+        form = xmlui.create(
+            self.parent.host,
+            xml_data,
+            title=title,
+            flags=["NO_CANCEL"],
+            profile=self.parent.profile,
+        )
+        form.show()
+
+    def tarot_game_invalid_cards_handler(self, phase, played_cards, invalid_cards):
+        """Invalid cards have been played
+        @param phase: phase of the game
+        @param played_cards: all the cards played
+        @param invalid_cards: cards which are invalid"""
+        QuickTarotGame.tarot_game_invalid_cards_handler(
+            self, phase, played_cards, invalid_cards
+        )
+        self.hand_wid.update(self.hand)
+        if self._autoplay == None:  # No dialog if there is autoplay
+            self.parent.host.bar_notify(_("Cards played are invalid !"))
+        self.parent.host.redraw()
+
+    def tarot_game_cards_played_handler(self, player, cards):
+        """A card has been played by player"""
+        QuickTarotGame.tarot_game_cards_played_handler(self, player, cards)
+        self.table.put_card(self.get_player_location(player), self.played[player])
+        self._checkState()
+        self.parent.host.redraw()
+
+    def _checkState(self):
+        if isinstance(
+            self.center.widget_list[1].original_widget, Hand
+        ):  # if we have a hand displayed
+            self.center.widget_list[1] = urwid.Filler(
+                self.table
+            )  # we show again the table
+            if self.state == "chien":
+                self.to_show = []
+                self.state = "wait"
+            elif self.state == "wait_for_ecart":
+                self.state = "ecart"
+                self.hand.extend(self.to_show)
+                self.hand.sort()
+                self.to_show = []
+                self.hand_wid.update(self.hand)
+
+    ##EVENTS##
+    def on_click(self, hand, card_wid):
+        """Called when user do an action on the hand"""
+        if not self.state in ["play", "ecart", "wait_for_ecart"]:
+            # it's not our turn, we ignore the click
+            card_wid.select(False)
+            return
+        self._checkState()
+        if self.state == "ecart":
+            if len(self.hand_wid.get_selected()) == 6:
+                pop_up_widget = sat_widgets.ConfirmDialog(
+                    _("Do you put these cards in chien ?"),
+                    yes_cb=self.on_ecart_done,
+                    no_cb=self.parent.host.remove_pop_up,
+                )
+                self.parent.host.show_pop_up(pop_up_widget)
+        elif self.state == "play":
+            card = card_wid.get_card()
+            self.parent.host.bridge.tarot_game_play_cards(
+                self.player_nick,
+                self.referee,
+                [(card.suit, card.value)],
+                self.parent.profile,
+            )
+            self.hand.remove(card)
+            self.hand_wid.update(self.hand)
+            self.state = "wait"
+
+    def on_ecart_done(self, button):
+        """Called when player has finished his écart"""
+        ecart = []
+        for card in self.hand_wid.get_selected():
+            ecart.append((card.suit, card.value))
+            self.hand.remove(card)
+        self.hand_wid.update(self.hand)
+        self.parent.host.bridge.tarot_game_play_cards(
+            self.player_nick, self.referee, ecart, self.parent.profile
+        )
+        self.state = "wait"
+        self.parent.host.remove_pop_up()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/keys.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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/>.
+
+"""This file manage the action <=> key map"""
+
+from urwid_satext.keys import action_key_map
+
+
+action_key_map.update(
+    {
+        # Edit bar
+        ("edit", "MODE_INSERTION"): "i",
+        ("edit", "MODE_COMMAND"): ":",
+        ("edit", "HISTORY_PREV"): "up",
+        ("edit", "HISTORY_NEXT"): "down",
+        # global
+        ("global", "MENU_HIDE"): "meta m",
+        ("global", "NOTIFICATION_NEXT"): "ctrl n",
+        ("global", "OVERLAY_HIDE"): "ctrl s",
+        ("global", "DEBUG"): "ctrl d",
+        ("global", "CONTACTS_HIDE"): "f2",
+        (
+            "global",
+            "REFRESH_SCREEN",
+        ): "ctrl l",  # ctrl l is used by Urwid to refresh screen
+        # global menu
+        ("menu_global", "APP_QUIT"): "ctrl x",
+        ("menu_global", "ROOM_JOIN"): "meta j",
+        # Libervia TUI widgets
+        ("libervia_tui_widget", "DECORATION_HIDE"): "meta l",
+        # contact list
+        ("contact_list", "STATUS_HIDE"): "meta s",
+        ("contact_list", "DISCONNECTED_HIDE"): "meta d",
+        ("contact_list", "RESOURCES_HIDE"): "meta r",
+        # chat panel
+        ("chat_panel", "OCCUPANTS_HIDE"): "meta p",
+        ("chat_panel", "TIMESTAMP_HIDE"): "meta t",
+        ("chat_panel", "SHORT_NICKNAME"): "meta n",
+        ("chat_panel", "SUBJECT_SWITCH"): "meta s",
+        ("chat_panel", "GOTO_BOTTOM"): "G",
+        # card game
+        ("card_game", "CARD_SELECT"): " ",
+        # focus
+        ("focus", "FOCUS_EXTRA"): "ctrl f",
+    }
+)
+
+
+action_key_map.set_close_namespaces(tuple(), ("global", "focus", "menu_global"))
+action_key_map.check_namespaces()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/notify.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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/>.
+
+import dbus
+
+
+class Notify(object):
+    """Used to send notification and detect if we have focus"""
+
+    def __init__(self):
+
+        # X11 stuff
+        self.display = None
+        self.X11_id = -1
+
+        try:
+            from Xlib import display as X_display
+
+            self.display = X_display.Display()
+            self.X11_id = self.get_focus()
+        except:
+            pass
+
+        # Now we try to connect to Freedesktop D-Bus API
+        try:
+            bus = dbus.SessionBus()
+            db_object = bus.get_object(
+                "org.freedesktop.Notifications",
+                "/org/freedesktop/Notifications",
+                follow_name_owner_changes=True,
+            )
+            self.freedesktop_int = dbus.Interface(
+                db_object, dbus_interface="org.freedesktop.Notifications"
+            )
+        except:
+            self.freedesktop_int = None
+
+    def get_focus(self):
+        if not self.display:
+            return 0
+        return self.display.get_input_focus().focus.id
+
+    def has_focus(self):
+        return (self.get_focus() == self.X11_id) if self.display else True
+
+    def use_x11(self):
+        return bool(self.display)
+
+    def send_notification(self, summ_mess, body_mess=""):
+        """Send notification to the user if possible"""
+        # TODO: check options before sending notifications
+        if self.freedesktop_int:
+            self.send_fd_notification(summ_mess, body_mess)
+
+    def send_fd_notification(self, summ_mess, body_mess=""):
+        """Send notification with the FreeDesktop D-Bus API"""
+        if self.freedesktop_int:
+            app_name = "LiberviaTUI"
+            replaces_id = 0
+            app_icon = ""
+            summary = summ_mess
+            body = body_mess
+            actions = dbus.Array(signature="s")
+            hints = dbus.Dictionary(signature="sv")
+            expire_timeout = -1
+
+            self.freedesktop_int.Notify(
+                app_name,
+                replaces_id,
+                app_icon,
+                summary,
+                body,
+                actions,
+                hints,
+                expire_timeout,
+            )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/profile_manager.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,228 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
+from libervia.backend.core import log as logging
+
+log = logging.getLogger(__name__)
+from libervia.frontends.quick_frontend.quick_profile_manager import QuickProfileManager
+from libervia.tui.constants import Const as C
+from libervia.tui.keys import action_key_map as a_key
+from urwid_satext import sat_widgets
+import urwid
+
+
+class ProfileManager(QuickProfileManager, urwid.WidgetWrap):
+    def __init__(self, host, autoconnect=None):
+        QuickProfileManager.__init__(self, host, autoconnect)
+
+        # login & password box must be created before list because of on_profile_change
+        self.login_wid = sat_widgets.AdvancedEdit(_("Login:"), align="center")
+        self.pass_wid = sat_widgets.Password(_("Password:"), align="center")
+
+        style = ["no_first_select"]
+        profiles = host.bridge.profiles_list_get()
+        profiles.sort()
+        self.list_profile = sat_widgets.List(
+            profiles, style=style, align="center", on_change=self.on_profile_change
+        )
+
+        # new & delete buttons
+        buttons = [
+            urwid.Button(_("New"), self.on_new_profile),
+            urwid.Button(_("Delete"), self.on_delete_profile),
+        ]
+        buttons_flow = urwid.GridFlow(
+            buttons,
+            max([len(button.get_label()) for button in buttons]) + 4,
+            1,
+            1,
+            "center",
+        )
+
+        # second part: login information:
+        divider = urwid.Divider("-")
+
+        # connect button
+        connect_button = sat_widgets.CustomButton(
+            _("Connect"), self.on_connect_profiles, align="center"
+        )
+
+        # we now build the widget
+        list_walker = urwid.SimpleFocusListWalker(
+            [
+                buttons_flow,
+                self.list_profile,
+                divider,
+                self.login_wid,
+                self.pass_wid,
+                connect_button,
+            ]
+        )
+        frame_body = urwid.ListBox(list_walker)
+        frame = urwid.Frame(
+            frame_body,
+            urwid.AttrMap(urwid.Text(_("Profile Manager"), align="center"), "title"),
+        )
+        self.main_widget = urwid.LineBox(frame)
+        urwid.WidgetWrap.__init__(self, self.main_widget)
+
+        self.go(autoconnect)
+
+    def keypress(self, size, key):
+        if key == a_key["APP_QUIT"]:
+            self.host.on_exit()
+            raise urwid.ExitMainLoop()
+        elif key in (a_key["FOCUS_UP"], a_key["FOCUS_DOWN"]):
+            focus_diff = 1 if key == a_key["FOCUS_DOWN"] else -1
+            list_box = self.main_widget.base_widget.body
+            current_focus = list_box.body.get_focus()[1]
+            if current_focus is None:
+                return
+            while True:
+                current_focus += focus_diff
+                if current_focus < 0 or current_focus >= len(list_box.body):
+                    break
+                if list_box.body[current_focus].selectable():
+                    list_box.set_focus(
+                        current_focus, "above" if focus_diff == 1 else "below"
+                    )
+                    list_box._invalidate()
+                    return
+        return super(ProfileManager, self).keypress(size, key)
+
+    def cancel_dialog(self, button):
+        self.host.remove_pop_up()
+
+    def new_profile(self, button, edit):
+        """Create the profile"""
+        name = edit.get_edit_text()
+        self.host.bridge.profile_create(
+            name,
+            callback=lambda: self.new_profile_created(name),
+            errback=self.profile_creation_failure,
+        )
+
+    def new_profile_created(self, profile):
+        # new profile will be selected, and a selected profile assume the session is started
+        self.host.bridge.profile_start_session(
+            "",
+            profile,
+            callback=lambda __: self.new_profile_session_started(profile),
+            errback=self.profile_creation_failure,
+        )
+
+    def new_profile_session_started(self, profile):
+        self.host.remove_pop_up()
+        self.refill_profiles()
+        self.list_profile.select_value(profile)
+        self.current.profile = profile
+        self.get_connection_params(profile)
+        self.host.redraw()
+
+    def profile_creation_failure(self, reason):
+        self.host.remove_pop_up()
+        message = self._get_error_message(reason)
+        self.host.alert(_("Can't create profile"), message)
+
+    def delete_profile(self, button):
+        self._delete_profile()
+        self.host.remove_pop_up()
+
+    def on_new_profile(self, e):
+        pop_up_widget = sat_widgets.InputDialog(
+            _("New profile"),
+            _("Please enter a new profile name"),
+            cancel_cb=self.cancel_dialog,
+            ok_cb=self.new_profile,
+        )
+        self.host.show_pop_up(pop_up_widget)
+
+    def on_delete_profile(self, e):
+        if self.current.profile:
+            pop_up_widget = sat_widgets.ConfirmDialog(
+                _("Are you sure you want to delete the profile {} ?").format(
+                    self.current.profile
+                ),
+                no_cb=self.cancel_dialog,
+                yes_cb=self.delete_profile,
+            )
+            self.host.show_pop_up(pop_up_widget)
+
+    def on_connect_profiles(self, button):
+        """Connect the profiles and start the main widget
+
+        @param button: the connect button
+        """
+        self._on_connect_profiles()
+
+    def reset_fields(self):
+        """Set profile to None, and reset fields"""
+        super(ProfileManager, self).reset_fields()
+        self.list_profile.unselect_all(invisible=True)
+
+    def set_profiles(self, profiles):
+        """Update the list of profiles"""
+        self.list_profile.change_values(profiles)
+        self.host.redraw()
+
+    def get_profiles(self):
+        return self.list_profile.get_selected_values()
+
+    def get_jid(self):
+        return self.login_wid.get_edit_text()
+
+    def getPassword(self):
+        return self.pass_wid.get_edit_text()
+
+    def set_jid(self, jid_):
+        self.login_wid.set_edit_text(jid_)
+        self.current.login = jid_
+        self.host.redraw()  # FIXME: redraw should be avoided
+
+    def set_password(self, password):
+        self.pass_wid.set_edit_text(password)
+        self.current.password = password
+        self.host.redraw()
+
+    def on_profile_change(self, list_wid, widget=None, selected=None):
+        """This is called when a profile is selected in the profile list.
+
+        @param list_wid: the List widget who sent the event
+        """
+        self.update_connection_params()
+        focused = list_wid.focus
+        selected = focused.get_state() if focused is not None else False
+        if not selected:  # profile was just unselected
+            return
+        focused.set_state(
+            False, invisible=True
+        )  # we don't want the widget to be selected until we are sure we can access it
+
+        def authenticate_cb(data, cb_id, profile):
+            if C.bool(data.pop("validated", C.BOOL_FALSE)):
+                self.current.profile = profile
+                focused.set_state(True, invisible=True)
+                self.get_connection_params(profile)
+                self.host.redraw()
+            self.host.action_manager(data, callback=authenticate_cb, profile=profile)
+
+        self.host.action_launch(
+            C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/progress.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
+import urwid
+from urwid_satext import sat_widgets
+from libervia.frontends.quick_frontend import quick_widgets
+
+
+class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget):
+    PROFILES_ALLOW_NONE = True
+
+    def __init__(self, host, target, profiles):
+        assert target is None and profiles is None
+        quick_widgets.QuickWidget.__init__(self, host, target)
+        self.host = host
+        self.progress_list = urwid.SimpleListWalker([])
+        self.progress_dict = {}
+        listbox = urwid.ListBox(self.progress_list)
+        buttons = []
+        buttons.append(sat_widgets.CustomButton(_("Clear progress list"), self._on_clear))
+        max_len = max([button.get_size() for button in buttons])
+        buttons_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center")
+        main_wid = sat_widgets.FocusFrame(listbox, footer=buttons_wid)
+        urwid.WidgetWrap.__init__(self, main_wid)
+
+    def add(self, progress_id, message, profile):
+        mess_wid = urwid.Text(message)
+        progr_wid = urwid.ProgressBar("progress_normal", "progress_complete")
+        column = urwid.Columns([mess_wid, progr_wid])
+        self.progress_dict[(progress_id, profile)] = {
+            "full": column,
+            "progress": progr_wid,
+            "state": "init",
+        }
+        self.progress_list.append(column)
+        self.progress_cb(self.host.loop, (progress_id, message, profile))
+
+    def progress_cb(self, loop, data):
+        progress_id, message, profile = data
+        data = self.host.bridge.progress_get(progress_id, profile)
+        pbar = self.progress_dict[(progress_id, profile)]["progress"]
+        if data:
+            if self.progress_dict[(progress_id, profile)]["state"] == "init":
+                # first answer, we must construct the bar
+                self.progress_dict[(progress_id, profile)]["state"] = "progress"
+                pbar.done = float(data["size"])
+
+            pbar.set_completion(float(data["position"]))
+            self.update_not_bar()
+        else:
+            if self.progress_dict[(progress_id, profile)]["state"] == "progress":
+                self.progress_dict[(progress_id, profile)]["state"] = "done"
+                pbar.set_completion(pbar.done)
+                self.update_not_bar()
+                return
+
+        loop.set_alarm_in(0.2, self.progress_cb, (progress_id, message, profile))
+
+    def _remove_bar(self, progress_id, profile):
+        wid = self.progress_dict[(progress_id, profile)]["full"]
+        self.progress_list.remove(wid)
+        del (self.progress_dict[(progress_id, profile)])
+
+    def _on_clear(self, button):
+        to_remove = []
+        for progress_id, profile in self.progress_dict:
+            if self.progress_dict[(progress_id, profile)]["state"] == "done":
+                to_remove.append((progress_id, profile))
+        for progress_id, profile in to_remove:
+            self._remove_bar(progress_id, profile)
+        self.update_not_bar()
+
+    def update_not_bar(self):
+        if not self.progress_dict:
+            self.host.set_progress(None)
+            return
+        progress = 0
+        nb_bars = 0
+        for progress_id, profile in self.progress_dict:
+            pbar = self.progress_dict[(progress_id, profile)]["progress"]
+            progress += pbar.current / pbar.done * 100
+            nb_bars += 1
+        av_progress = progress / float(nb_bars)
+        self.host.set_progress(av_progress)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/status.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.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 libervia.backend.core.i18n import _
+import urwid
+from urwid_satext import sat_widgets
+from libervia.frontends.quick_frontend.constants import Const as commonConst
+from libervia.tui.constants import Const as C
+
+
+class StatusBar(urwid.Columns):
+    def __init__(self, host):
+        self.host = host
+        self.presence = sat_widgets.ClickableText("")
+        status_prefix = urwid.Text("[")
+        status_suffix = urwid.Text("]")
+        self.status = sat_widgets.ClickableText("")
+        self.set_presence_status(C.PRESENCE_UNAVAILABLE, "")
+        urwid.Columns.__init__(
+            self,
+            [
+                ("weight", 1, self.presence),
+                ("weight", 1, status_prefix),
+                ("weight", 9, self.status),
+                ("weight", 1, status_suffix),
+            ],
+        )
+        urwid.connect_signal(self.presence, "click", self.on_presence_click)
+        urwid.connect_signal(self.status, "click", self.on_status_click)
+
+    def on_presence_click(self, sender=None):
+        if not self.host.bridge.is_connected(
+            self.host.current_profile
+        ):  # FIXME: manage multi-profiles
+            return
+        options = [commonConst.PRESENCE[presence] for presence in commonConst.PRESENCE]
+        list_widget = sat_widgets.GenericList(
+            options=options, option_type=sat_widgets.ClickableText, on_click=self.on_change
+        )
+        decorated = sat_widgets.LabelLine(
+            list_widget, sat_widgets.SurroundedText(_("Set your presence"))
+        )
+        self.host.show_pop_up(decorated)
+
+    def on_status_click(self, sender=None):
+        if not self.host.bridge.is_connected(
+            self.host.current_profile
+        ):  # FIXME: manage multi-profiles
+            return
+        pop_up_widget = sat_widgets.InputDialog(
+            _("Set your status"),
+            _("New status"),
+            default_txt=self.status.get_text(),
+            cancel_cb=lambda _: self.host.remove_pop_up(),
+            ok_cb=self.on_change,
+        )
+        self.host.show_pop_up(pop_up_widget)
+
+    def on_change(self, sender=None, user_data=None):
+        new_value = user_data.get_text()
+        previous = (
+            [key for key in C.PRESENCE if C.PRESENCE[key][0] == self.presence.get_text()][
+                0
+            ],
+            self.status.get_text(),
+        )
+        if isinstance(user_data, sat_widgets.ClickableText):
+            new = (
+                [
+                    key
+                    for key in commonConst.PRESENCE
+                    if commonConst.PRESENCE[key] == new_value
+                ][0],
+                previous[1],
+            )
+        elif isinstance(user_data, sat_widgets.AdvancedEdit):
+            new = (previous[0], new_value[0])
+        if new != previous:
+            statuses = {
+                C.PRESENCE_STATUSES_DEFAULT: new[1]
+            }  # FIXME: manage multilingual statuses
+            for (
+                profile
+            ) in (
+                self.host.profiles
+            ):  # FIXME: for now all the profiles share the same status
+                self.host.bridge.presence_set(
+                    show=new[0], statuses=statuses, profile_key=profile
+                )
+            self.set_presence_status(new[0], new[1])
+        self.host.remove_pop_up()
+
+    def set_presence_status(self, show, status):
+        show_icon, show_attr = C.PRESENCE.get(show)
+        self.presence.set_text(("show_normal", show_icon))
+        if status is not None:
+            self.status.set_text((show_attr, status))
+        self.host.redraw()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/widget.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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 libervia.backend.core import log as logging
+
+log = logging.getLogger(__name__)
+import urwid
+from urwid_satext import sat_widgets
+from libervia.tui.keys import action_key_map as a_key
+
+
+class LiberviaTUIWidget(urwid.WidgetWrap):
+    """Base widget for LiberviaTUI"""
+
+    def __init__(self, w, title=""):
+        self._title = title
+        self._title_dynamic = None
+        self._original_widget = w
+        urwid.WidgetWrap.__init__(self, self._get_decoration(w))
+
+    @property
+    def title(self):
+        """Text shown in title bar of the widget"""
+
+        # profiles currently managed by frontend
+        try:
+            all_profiles = self.host.profiles
+        except AttributeError:
+            all_profiles = []
+
+        # profiles managed by the widget
+        try:
+            profiles = self.profiles
+        except AttributeError:
+            try:
+                profiles = [self.profile]
+            except AttributeError:
+                profiles = []
+
+        title_elts = []
+        if self._title:
+            title_elts.append(self._title)
+        if self._title_dynamic:
+            title_elts.append(self._title_dynamic)
+        if len(all_profiles) > 1 and profiles:
+            title_elts.append("[{}]".format(", ".join(profiles)))
+        return sat_widgets.SurroundedText(" ".join(title_elts))
+
+    @title.setter
+    def title(self, value):
+        self._title = value
+        if self.decoration_visible:
+            self.show_decoration()
+
+    @property
+    def title_dynamic(self):
+        """Dynamic part of title"""
+        return self._title_dynamic
+
+    @title_dynamic.setter
+    def title_dynamic(self, value):
+        self._title_dynamic = value
+        if self.decoration_visible:
+            self.show_decoration()
+
+    @property
+    def decoration_visible(self):
+        """True if the decoration is visible"""
+        return isinstance(self._w, sat_widgets.LabelLine)
+
+    def keypress(self, size, key):
+        if key == a_key["DECORATION_HIDE"]:  # user wants to (un)hide widget decoration
+            show = not self.decoration_visible
+            self.show_decoration(show)
+        else:
+            return super(LiberviaTUIWidget, self).keypress(size, key)
+
+    def _get_decoration(self, widget):
+        return sat_widgets.LabelLine(widget, self.title)
+
+    def show_decoration(self, show=True):
+        """Show/Hide the decoration around the window"""
+        self._w = (
+            self._get_decoration(self._original_widget) if show else self._original_widget
+        )
+
+    def get_menu(self):
+        raise NotImplementedError
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/tui/xmlui.py	Fri Jun 02 16:25:25 2023 +0200
@@ -0,0 +1,528 @@
+#!/usr/bin/env python3
+
+
+# Libervia TUI
+# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
+import urwid
+import copy
+from libervia.backend.core import exceptions
+from urwid_satext import sat_widgets
+from urwid_satext import files_management
+from libervia.backend.core.log import getLogger
+
+log = getLogger(__name__)
+from libervia.tui.constants import Const as C
+from libervia.tui.widget import LiberviaTUIWidget
+from libervia.frontends.tools import xmlui
+
+
+class LiberviaTUIEvents(object):
+    """ Used to manage change event of LiberviaTUI widgets """
+
+    def _event_callback(self, ctrl, *args, **kwargs):
+        """" Call xmlui callback and ignore any extra argument """
+        args[-1](ctrl)
+
+    def _xmlui_on_change(self, callback):
+        """ Call callback with widget as only argument """
+        urwid.connect_signal(self, "change", self._event_callback, callback)
+
+
+class LiberviaTUIEmptyWidget(xmlui.EmptyWidget, urwid.Text):
+    def __init__(self, _xmlui_parent):
+        urwid.Text.__init__(self, "")
+
+
+class LiberviaTUITextWidget(xmlui.TextWidget, urwid.Text):
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        urwid.Text.__init__(self, value)
+
+
+class LiberviaTUILabelWidget(xmlui.LabelWidget, LiberviaTUITextWidget):
+    def __init__(self, _xmlui_parent, value):
+        super(LiberviaTUILabelWidget, self).__init__(_xmlui_parent, value + ": ")
+
+
+class LiberviaTUIJidWidget(xmlui.JidWidget, LiberviaTUITextWidget):
+    pass
+
+
+class LiberviaTUIDividerWidget(xmlui.DividerWidget, urwid.Divider):
+    def __init__(self, _xmlui_parent, style="line"):
+        if style == "line":
+            div_char = "─"
+        elif style == "dot":
+            div_char = "·"
+        elif style == "dash":
+            div_char = "-"
+        elif style == "plain":
+            div_char = "█"
+        elif style == "blank":
+            div_char = " "
+        else:
+            log.warning(_("Unknown div_char"))
+            div_char = "─"
+
+        urwid.Divider.__init__(self, div_char)
+
+
+class LiberviaTUIStringWidget(
+    xmlui.StringWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents
+):
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        sat_widgets.AdvancedEdit.__init__(self, edit_text=value)
+        self.read_only = read_only
+
+    def selectable(self):
+        if self.read_only:
+            return False
+        return super(LiberviaTUIStringWidget, self).selectable()
+
+    def _xmlui_set_value(self, value):
+        self.set_edit_text(value)
+
+    def _xmlui_get_value(self):
+        return self.get_edit_text()
+
+
+class LiberviaTUIJidInputWidget(xmlui.JidInputWidget, LiberviaTUIStringWidget):
+    pass
+
+
+class LiberviaTUIPasswordWidget(
+    xmlui.PasswordWidget, sat_widgets.Password, LiberviaTUIEvents
+):
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        sat_widgets.Password.__init__(self, edit_text=value)
+        self.read_only = read_only
+
+    def selectable(self):
+        if self.read_only:
+            return False
+        return super(LiberviaTUIPasswordWidget, self).selectable()
+
+    def _xmlui_set_value(self, value):
+        self.set_edit_text(value)
+
+    def _xmlui_get_value(self):
+        return self.get_edit_text()
+
+
+class LiberviaTUITextBoxWidget(
+    xmlui.TextBoxWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents
+):
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        sat_widgets.AdvancedEdit.__init__(self, edit_text=value, multiline=True)
+        self.read_only = read_only
+
+    def selectable(self):
+        if self.read_only:
+            return False
+        return super(LiberviaTUITextBoxWidget, self).selectable()
+
+    def _xmlui_set_value(self, value):
+        self.set_edit_text(value)
+
+    def _xmlui_get_value(self):
+        return self.get_edit_text()
+
+
+class LiberviaTUIBoolWidget(xmlui.BoolWidget, urwid.CheckBox, LiberviaTUIEvents):
+    def __init__(self, _xmlui_parent, state, read_only=False):
+        urwid.CheckBox.__init__(self, "", state=state)
+        self.read_only = read_only
+
+    def selectable(self):
+        if self.read_only:
+            return False
+        return super(LiberviaTUIBoolWidget, self).selectable()
+
+    def _xmlui_set_value(self, value):
+        self.set_state(value == "true")
+
+    def _xmlui_get_value(self):
+        return C.BOOL_TRUE if self.get_state() else C.BOOL_FALSE
+
+
+class LiberviaTUIIntWidget(xmlui.IntWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents):
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        sat_widgets.AdvancedEdit.__init__(self, edit_text=value)
+        self.read_only = read_only
+
+    def selectable(self):
+        if self.read_only:
+            return False
+        return super(LiberviaTUIIntWidget, self).selectable()
+
+    def _xmlui_set_value(self, value):
+        self.set_edit_text(value)
+
+    def _xmlui_get_value(self):
+        return self.get_edit_text()
+
+
+class LiberviaTUIButtonWidget(
+    xmlui.ButtonWidget, sat_widgets.CustomButton, LiberviaTUIEvents
+):
+    def __init__(self, _xmlui_parent, value, click_callback):
+        sat_widgets.CustomButton.__init__(self, value, on_press=click_callback)
+
+    def _xmlui_on_click(self, callback):
+        urwid.connect_signal(self, "click", callback)
+
+
+class LiberviaTUIListWidget(xmlui.ListWidget, sat_widgets.List, LiberviaTUIEvents):
+    def __init__(self, _xmlui_parent, options, selected, flags):
+        sat_widgets.List.__init__(self, options=options, style=flags)
+        self._xmlui_select_values(selected)
+
+    def _xmlui_select_value(self, value):
+        return self.select_value(value)
+
+    def _xmlui_select_values(self, values):
+        return self.select_values(values)
+
+    def _xmlui_get_selected_values(self):
+        return [option.value for option in self.get_selected_values()]
+
+    def _xmlui_add_values(self, values, select=True):
+        current_values = self.get_all_values()
+        new_values = copy.deepcopy(current_values)
+        for value in values:
+            if value not in current_values:
+                new_values.append(value)
+        if select:
+            selected = self._xmlui_get_selected_values()
+        self.change_values(new_values)
+        if select:
+            for value in values:
+                if value not in selected:
+                    selected.append(value)
+            self._xmlui_select_values(selected)
+
+
+class LiberviaTUIJidsListWidget(xmlui.ListWidget, sat_widgets.List, LiberviaTUIEvents):
+    def __init__(self, _xmlui_parent, jids, styles):
+        sat_widgets.List.__init__(
+            self,
+            options=jids + [""],  # the empty field is here to add new jids if needed
+            option_type=lambda txt, align: sat_widgets.AdvancedEdit(
+                edit_text=txt, align=align
+            ),
+            on_change=self._on_change,
+        )
+        self.delete = 0
+
+    def _on_change(self, list_widget, jid_widget=None, text=None):
+        if jid_widget is not None:
+            if jid_widget != list_widget.contents[-1] and not text:
+                # if a field is empty, we delete the line (except for the last line)
+                list_widget.contents.remove(jid_widget)
+            elif jid_widget == list_widget.contents[-1] and text:
+                # we always want an empty field as last value to be able to add jids
+                list_widget.contents.append(sat_widgets.AdvancedEdit())
+
+    def _xmlui_get_selected_values(self):
+        # XXX: there is not selection in this list, so we return all non empty values
+        return [jid_ for jid_ in self.get_all_values() if jid_]
+
+
+class LiberviaTUIAdvancedListContainer(
+    xmlui.AdvancedListContainer, sat_widgets.TableContainer, LiberviaTUIEvents
+):
+    def __init__(self, _xmlui_parent, columns, selectable="no"):
+        options = {"ADAPT": ()}
+        if selectable != "no":
+            options["HIGHLIGHT"] = ()
+        sat_widgets.TableContainer.__init__(
+            self, columns=columns, options=options, row_selectable=selectable != "no"
+        )
+
+    def _xmlui_append(self, widget):
+        self.add_widget(widget)
+
+    def _xmlui_add_row(self, idx):
+        self.set_row_index(idx)
+
+    def _xmlui_get_selected_widgets(self):
+        return self.get_selected_widgets()
+
+    def _xmlui_get_selected_index(self):
+        return self.get_selected_index()
+
+    def _xmlui_on_select(self, callback):
+        """ Call callback with widget as only argument """
+        urwid.connect_signal(self, "click", self._event_callback, callback)
+
+
+class LiberviaTUIPairsContainer(xmlui.PairsContainer, sat_widgets.TableContainer):
+    def __init__(self, _xmlui_parent):
+        options = {"ADAPT": (0,), "HIGHLIGHT": (0,)}
+        if self._xmlui_main.type == "param":
+            options["FOCUS_ATTR"] = "param_selected"
+        sat_widgets.TableContainer.__init__(self, columns=2, options=options)
+
+    def _xmlui_append(self, widget):
+        if isinstance(widget, LiberviaTUIEmptyWidget):
+            # we don't want highlight on empty widgets
+            widget = urwid.AttrMap(widget, "default")
+        self.add_widget(widget)
+
+
+class LiberviaTUILabelContainer(LiberviaTUIPairsContainer, xmlui.LabelContainer):
+    pass
+
+
+class LiberviaTUITabsContainer(xmlui.TabsContainer, sat_widgets.TabsContainer):
+    def __init__(self, _xmlui_parent):
+        sat_widgets.TabsContainer.__init__(self)
+
+    def _xmlui_append(self, widget):
+        self.body.append(widget)
+
+    def _xmlui_add_tab(self, label, selected):
+        tab = LiberviaTUIVerticalContainer(None)
+        self.add_tab(label, tab, selected)
+        return tab
+
+
+class LiberviaTUIVerticalContainer(xmlui.VerticalContainer, urwid.ListBox):
+    BOX_HEIGHT = 5
+
+    def __init__(self, _xmlui_parent):
+        urwid.ListBox.__init__(self, urwid.SimpleListWalker([]))
+        self._last_size = None
+
+    def _xmlui_append(self, widget):
+        if "flow" not in widget.sizing():
+            widget = urwid.BoxAdapter(widget, self.BOX_HEIGHT)
+        self.body.append(widget)
+
+    def render(self, size, focus=False):
+        if size != self._last_size:
+            (maxcol, maxrow) = size
+            if self.body:
+                widget = self.body[0]
+                if isinstance(widget, urwid.BoxAdapter):
+                    widget.height = maxrow
+            self._last_size = size
+        return super(LiberviaTUIVerticalContainer, self).render(size, focus)
+
+
+### Dialogs ###
+
+
+class LiberviaTUIDialog(object):
+    def __init__(self, _xmlui_parent):
+        self.host = _xmlui_parent.host
+
+    def _xmlui_show(self):
+        self.host.show_pop_up(self)
+
+    def _xmlui_close(self):
+        self.host.remove_pop_up(self)
+
+
+class LiberviaTUIMessageDialog(LiberviaTUIDialog, xmlui.MessageDialog, sat_widgets.Alert):
+    def __init__(self, _xmlui_parent, title, message, level):
+        LiberviaTUIDialog.__init__(self, _xmlui_parent)
+        xmlui.MessageDialog.__init__(self, _xmlui_parent)
+        sat_widgets.Alert.__init__(
+            self, title, message, ok_cb=lambda __: self._xmlui_close()
+        )
+
+
+class LiberviaTUINoteDialog(xmlui.NoteDialog, LiberviaTUIMessageDialog):
+    # TODO: separate NoteDialog
+    pass
+
+
+class LiberviaTUIConfirmDialog(
+    LiberviaTUIDialog, xmlui.ConfirmDialog, sat_widgets.ConfirmDialog
+):
+    def __init__(self, _xmlui_parent, title, message, level, buttons_set):
+        LiberviaTUIDialog.__init__(self, _xmlui_parent)
+        xmlui.ConfirmDialog.__init__(self, _xmlui_parent)
+        sat_widgets.ConfirmDialog.__init__(
+            self,
+            title,
+            message,
+            no_cb=lambda __: self._xmlui_cancelled(),
+            yes_cb=lambda __: self._xmlui_validated(),
+        )
+
+
+class LiberviaTUIFileDialog(
+    LiberviaTUIDialog, xmlui.FileDialog, files_management.FileDialog
+):
+    def __init__(self, _xmlui_parent, title, message, level, filetype):
+        # TODO: message is not managed yet
+        LiberviaTUIDialog.__init__(self, _xmlui_parent)
+        xmlui.FileDialog.__init__(self, _xmlui_parent)
+        style = []
+        if filetype == C.XMLUI_DATA_FILETYPE_DIR:
+            style.append("dir")
+        files_management.FileDialog.__init__(
+            self,
+            ok_cb=lambda path: self._xmlui_validated({"path": path}),
+            cancel_cb=lambda __: self._xmlui_cancelled(),
+            message=message,
+            title=title,
+            style=style,
+        )
+
+
+class GenericFactory(object):
+    def __getattr__(self, attr):
+        if attr.startswith("create"):
+            cls = globals()[
+                "LiberviaTUI" + attr[6:]
+            ]  # XXX: we prefix with "LiberviaTUI" to work around an Urwid bug, WidgetMeta in Urwid don't manage multiple inheritance with same names
+            return cls
+
+
+class WidgetFactory(GenericFactory):
+    def __getattr__(self, attr):
+        if attr.startswith("create"):
+            cls = GenericFactory.__getattr__(self, attr)
+            cls._xmlui_main = self._xmlui_main
+            return cls
+
+
+class XMLUIPanel(xmlui.XMLUIPanel, LiberviaTUIWidget):
+    widget_factory = WidgetFactory()
+
+    def __init__(
+        self,
+        host,
+        parsed_xml,
+        title=None,
+        flags=None,
+        callback=None,
+        ignore=None,
+        whitelist=None,
+        profile=C.PROF_KEY_NONE,
+    ):
+        self.widget_factory._xmlui_main = self
+        self._dest = None
+        xmlui.XMLUIPanel.__init__(
+            self,
+            host,
+            parsed_xml,
+            title=title,
+            flags=flags,
+            callback=callback,
+            ignore=ignore,
+            profile=profile,
+        )
+        LiberviaTUIWidget.__init__(self, self.main_cont, self.xmlui_title)
+
+
+    def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None):
+        # Small hack to always have a VerticalContainer as main container in LiberviaTUI.
+        # this used to be the default behaviour for all frontends, but now
+        # TabsContainer can also be the main container.
+        if _xmlui_parent is self:
+            node = current_node.childNodes[0]
+            if node.nodeName == "container" and node.getAttribute("type") == "tabs":
+                _xmlui_parent = self.widget_factory.createVerticalContainer(self)
+                self.main_cont = _xmlui_parent
+        return super(XMLUIPanel, self)._parse_childs(_xmlui_parent, current_node, wanted,
+                                                    data)
+
+
+    def construct_ui(self, parsed_dom):
+        def post_treat():
+            assert self.main_cont.body
+
+            if self.type in ("form", "popup"):
+                buttons = []
+                if self.type == "form":
+                    buttons.append(urwid.Button(_("Submit"), self.on_form_submitted))
+                    if not "NO_CANCEL" in self.flags:
+                        buttons.append(urwid.Button(_("Cancel"), self.on_form_cancelled))
+                else:
+                    buttons.append(
+                        urwid.Button(_("OK"), on_press=lambda __: self._xmlui_close())
+                    )
+                max_len = max([len(button.get_label()) for button in buttons])
+                grid_wid = urwid.GridFlow(buttons, max_len + 4, 1, 0, "center")
+                self.main_cont.body.append(grid_wid)
+            elif self.type == "param":
+                tabs_cont = self.main_cont.body[0].base_widget
+                assert isinstance(tabs_cont, sat_widgets.TabsContainer)
+                buttons = []
+                buttons.append(sat_widgets.CustomButton(_("Save"), self.on_save_params))
+                buttons.append(
+                    sat_widgets.CustomButton(
+                        _("Cancel"), lambda x: self.host.remove_window()
+                    )
+                )
+                max_len = max([button.get_size() for button in buttons])
+                grid_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center")
+                tabs_cont.add_footer(grid_wid)
+
+        xmlui.XMLUIPanel.construct_ui(self, parsed_dom, post_treat)
+        urwid.WidgetWrap.__init__(self, self.main_cont)
+
+    def show(self, show_type=None, valign="middle"):
+        """Show the constructed UI
+        @param show_type: how to show the UI:
+            - None (follow XMLUI's recommendation)
+            - 'popup'
+            - 'window'
+        @param valign: vertical alignment when show_type is 'popup'.
+            Ignored when show_type is 'window'.
+
+        """
+        if show_type is None:
+            if self.type in ("window", "param"):
+                show_type = "window"
+            elif self.type in ("popup", "form"):
+                show_type = "popup"
+
+        if show_type not in ("popup", "window"):
+            raise ValueError("Invalid show_type [%s]" % show_type)
+
+        self._dest = show_type
+        if show_type == "popup":
+            self.host.show_pop_up(self, valign=valign)
+        elif show_type == "window":
+            self.host.new_widget(self, user_action=self.user_action)
+        else:
+            assert False
+        self.host.redraw()
+
+    def _xmlui_close(self):
+        if self._dest == "window":
+            self.host.remove_window()
+        elif self._dest == "popup":
+            self.host.remove_pop_up(self)
+        else:
+            raise exceptions.InternalError(
+                "self._dest unknown, are you sure you have called XMLUI.show ?"
+            )
+
+
+class XMLUIDialog(xmlui.XMLUIDialog):
+    dialog_factory = GenericFactory()
+
+
+xmlui.register_class(xmlui.CLASS_PANEL, XMLUIPanel)
+xmlui.register_class(xmlui.CLASS_DIALOG, XMLUIDialog)
+create = xmlui.create
--- a/setup.py	Fri Jun 02 14:54:26 2023 +0200
+++ b/setup.py	Fri Jun 02 16:25:25 2023 +0200
@@ -137,8 +137,8 @@
             "jp = libervia.cli.base:LiberviaCli.run",
 
             # TUI + alias
-            "libervia-tui = libervia.frontends.primitivus.base:PrimitivusApp.run",
-            "primitivus = libervia.frontends.primitivus.base:PrimitivusApp.run",
+            "libervia-tui = libervia.tui.base:LiberviaTUIApp.run",
+            "primitivus = libervia.tui.base:LiberviaTUIApp.run",
             ],
         },
     zip_safe=False,