tui: Q&D adaptation to new code:

use the new `quick_bridge` adapter and do few changes to make TUI work with new backend.
TUI will probably be rewritten at some point, this is just an update to keep it working.

`urwid` has been updated to have a version working with Python 3.14.
This commit is contained in:
Goffi 2026-11-04 17:59:56 +01:00
commit e5ed4e3d7c
7 changed files with 135 additions and 77 deletions

View file

@ -28,9 +28,10 @@ from libervia.backend.core import log as logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from libervia.backend.tools import config as tools_config from libervia.backend.tools import config as tools_config
import urwid import urwid
from urwid.util import is_wide_char from urwid import is_wide_char
from urwid_satext import sat_widgets from urwid_satext import sat_widgets
from libervia.frontends.quick_frontend.quick_app import QuickApp from libervia.frontends.quick_frontend.quick_app import QuickApp
from libervia.frontends.quick_frontend import quick_bridge
from libervia.frontends.quick_frontend import quick_utils from libervia.frontends.quick_frontend import quick_utils
from libervia.frontends.quick_frontend import quick_chat from libervia.frontends.quick_frontend import quick_chat
from libervia.tui.profile_manager import ProfileManager from libervia.tui.profile_manager import ProfileManager
@ -45,6 +46,7 @@ from libervia.tui.keys import action_key_map as a_key
from libervia.frontends.tools.misc import InputHistory from libervia.frontends.tools.misc import InputHistory
from libervia.backend.tools.common import dynamic_import from libervia.backend.tools.common import dynamic_import
from libervia.frontends.tools import jid from libervia.frontends.tools import jid
import asyncio
import signal import signal
import sys import sys
@ -67,10 +69,19 @@ class TUISettings(tools_config.FrontendSettings, key="tui"):
## bridge handling ## bridge handling
# we get bridge name from conf and initialise the right class accordingly # we get bridge name from conf and initialise the right class accordingly
main_config = tools_config.parse_main_conf(ignore_cache=True) main_config = tools_config.parse_main_conf(ignore_cache=True)
bridge_name = next(iter(main_config.common.bridges)).type bridge_name = next(iter(main_config.bridges)).type
if "dbus" not in bridge_name: if "dbus" not in bridge_name:
print("only D-Bus bridge is currently supported") print("only D-Bus bridge is currently supported")
sys.exit(3) sys.exit(3)
# install the asyncio twisted reactor
try:
asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
from twisted.internet import asyncioreactor
asyncioreactor.install()
class EditBar(sat_widgets.ModalEdit): class EditBar(sat_widgets.ModalEdit):
@ -338,9 +349,7 @@ class LiberviaTUIApp(QuickApp, InputHistory):
AVATARS_HANDLER = False AVATARS_HANDLER = False
def __init__(self): def __init__(self):
bridge_module = dynamic_import.import_bridge( bridge_module = dynamic_import.import_bridge(bridge_name)
bridge_name, "libervia.frontends.bridge"
)
if bridge_module is None: if bridge_module is None:
log.error("Can't import {} bridge".format(bridge_name)) log.error("Can't import {} bridge".format(bridge_name))
sys.exit(3) sys.exit(3)
@ -348,16 +357,18 @@ class LiberviaTUIApp(QuickApp, InputHistory):
log.debug("Loading {} bridge".format(bridge_name)) log.debug("Loading {} bridge".format(bridge_name))
QuickApp.__init__( QuickApp.__init__(
self, self,
bridge_factory=bridge_module.bridge, # AIOBridge is the only bridge compatible with the asyncio loop
bridge_factory=lambda: quick_bridge.QuickBridgeAdapter(
bridge_module.AIOBridge()
),
xmlui=xmlui, xmlui=xmlui,
check_options=quick_utils.check_options, check_options=quick_utils.check_options,
connect_bridge=False, connect_bridge=False,
) )
self.tui_settings = main_config.get_settings(TUISettings) self.tui_settings = main_config.get_settings(TUISettings)
## main loop setup ## ## main loop setup ##
event_loop = ( # the new D-Bus bridge is twisted based (txdbus)
urwid.GLibEventLoop if "dbus" in bridge_name else urwid.TwistedEventLoop event_loop = urwid.TwistedEventLoop
)
self.loop = urwid.MainLoop( self.loop = urwid.MainLoop(
urwid.SolidFill(), urwid.SolidFill(),
C.PALETTE, C.PALETTE,
@ -373,6 +384,10 @@ class LiberviaTUIApp(QuickApp, InputHistory):
def on_bridge_connected(self): def on_bridge_connected(self):
##misc setup## ##misc setup##
self._version = None
self.bridge.version_get(
callback=self._version_get_cb, errback=lambda __: None
)
self._visible_widgets = set() self._visible_widgets = set()
self.notif_bar = sat_widgets.NotificationBar() self.notif_bar = sat_widgets.NotificationBar()
urwid.connect_signal(self.notif_bar, "change", self.on_notification) urwid.connect_signal(self.notif_bar, "change", self.on_notification)
@ -400,6 +415,9 @@ class LiberviaTUIApp(QuickApp, InputHistory):
self.loop.widget = self.main_widget = ProfileManager(self) self.loop.widget = self.main_widget = ProfileManager(self)
self.post_init() self.post_init()
def _version_get_cb(self, version):
self._version = version
@property @property
def visible_widgets(self): def visible_widgets(self):
return self._visible_widgets return self._visible_widgets
@ -579,7 +597,7 @@ class LiberviaTUIApp(QuickApp, InputHistory):
self.__saved_overlay = None self.__saved_overlay = None
elif ( elif (
input_ == a_key["DEBUG"] and ".dev0" in self.bridge.version_get() input_ == a_key["DEBUG"] and ".dev0" in (self._version or "")
): # Debug only for dev versions ): # Debug only for dev versions
self.debug() self.debug()
elif input_ == a_key["CONTACTS_HIDE"]: # user wants to (un)hide the contact lists elif input_ == a_key["CONTACTS_HIDE"]: # user wants to (un)hide the contact lists
@ -614,6 +632,9 @@ class LiberviaTUIApp(QuickApp, InputHistory):
def add_menus(self, menu, type_filter, menu_data=None): def add_menus(self, menu, type_filter, menu_data=None):
"""Add cached menus to instance """Add cached menus to instance
The menus are retrieved asynchronously from the bridge.
@param menu: sat_widgets.Menu instance @param menu: sat_widgets.Menu instance
@param type_filter: menu type like is sat.core.sat_main.import_menu @param type_filter: menu type like is sat.core.sat_main.import_menu
@param menu_data: data to send with these menus @param menu_data: data to send with these menus
@ -623,17 +644,25 @@ class LiberviaTUIApp(QuickApp, InputHistory):
def add_menu_cb(callback_id): def add_menu_cb(callback_id):
self.action_launch(callback_id, menu_data, profile=self.current_profile) self.action_launch(callback_id, menu_data, profile=self.current_profile)
for id_, type_, path, path_i18n, extra in self.bridge.menus_get( def menus_get_cb(menus):
"", C.NO_SECURITY_LIMIT for id_, type_, path, path_i18n, extra in menus: # TODO: manage extra
): # TODO: manage extra
if type_ != type_filter: if type_ != type_filter:
continue continue
if len(path) != 2: if len(path) != 2:
raise NotImplementedError("Menu with a path != 2 are not implemented yet") raise NotImplementedError(
"Menu with a path != 2 are not implemented yet"
)
menu.add_menu( menu.add_menu(
path_i18n[0], path_i18n[1], lambda dummy, id_=id_: add_menu_cb(id_) path_i18n[0], path_i18n[1], lambda dummy, id_=id_: add_menu_cb(id_)
) )
def menus_get_eb(failure):
log.error("Can't get menus: {}".format(failure))
self.bridge.menus_get(
"", C.NO_SECURITY_LIMIT, callback=menus_get_cb, errback=menus_get_eb
)
def _build_menu_roller(self): def _build_menu_roller(self):
menu = sat_widgets.Menu(self.loop) menu = sat_widgets.Menu(self.loop)
general = _("General") general = _("General")
@ -1016,10 +1045,12 @@ class LiberviaTUIApp(QuickApp, InputHistory):
def on_join_room_request(self, menu): def on_join_room_request(self, menu):
"""User wants to join a MUC room""" """User wants to join a MUC room"""
def show_join_dialog(default_service):
pop_up_widget = sat_widgets.InputDialog( pop_up_widget = sat_widgets.InputDialog(
_("Entering a MUC room"), _("Entering a MUC room"),
_("Please enter MUC's JID"), _("Please enter MUC's JID"),
default_txt=self.bridge.muc_get_default_service(), default_txt=default_service,
ok_cb=self.on_join_room, ok_cb=self.on_join_room,
) )
pop_up_widget.set_callback( pop_up_widget.set_callback(
@ -1027,8 +1058,12 @@ class LiberviaTUIApp(QuickApp, InputHistory):
) )
self.show_pop_up(pop_up_widget) self.show_pop_up(pop_up_widget)
self.bridge.muc_get_default_service(
callback=show_join_dialog, errback=lambda __: show_join_dialog("")
)
def on_about_request(self, menu): def on_about_request(self, menu):
self.alert(_("About"), C.APP_NAME + " v" + self.bridge.version_get()) self.alert(_("About"), C.APP_NAME + " v" + (self._version or ""))
# MISC CALLBACKS# # MISC CALLBACKS#

View file

@ -174,10 +174,13 @@ class Card(TarotCard):
return CardDisplayer(self) return CardDisplayer(self)
class Table(urwid.FlowWidget): class Table(urwid.Widget):
"""Represent the cards currently on the table""" """Represent the cards currently on the table"""
_sizing = frozenset([urwid.Sizing.FLOW])
def __init__(self): def __init__(self):
super().__init__()
self.top = self.left = self.bottom = self.right = None self.top = self.left = self.bottom = self.right = None
def put_card(self, location, card): def put_card(self, location, card):
@ -313,7 +316,7 @@ class TarotGame(QuickTarotGame, urwid.WidgetWrap):
if not winners and not loosers: if not winners and not loosers:
title = _("Draw game") title = _("Draw game")
else: else:
title = _("You win \o/") if self.player_nick in winners else _("You loose :(") title = _("You win \\o/") if self.player_nick in winners else _("You loose :(")
form = xmlui.create( form = xmlui.create(
self.parent.host, self.parent.host,
xml_data, xml_data,

View file

@ -17,7 +17,10 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import dbus try:
import dbus
except ImportError:
dbus = None
class Notify(object): class Notify(object):

View file

@ -37,11 +37,11 @@ class ProfileManager(QuickProfileManager, urwid.WidgetWrap):
self.pass_wid = sat_widgets.Password(_("Password:"), align="center") self.pass_wid = sat_widgets.Password(_("Password:"), align="center")
style = ["no_first_select"] style = ["no_first_select"]
profiles = host.bridge.profiles_list_get()
profiles.sort()
self.list_profile = sat_widgets.List( self.list_profile = sat_widgets.List(
profiles, style=style, align="center", on_change=self.on_profile_change [], style=style, align="center", on_change=self.on_profile_change
) )
# the profiles list is retrieved asynchronously
self.refill_profiles()
# new & delete buttons # new & delete buttons
buttons = [ buttons = [

View file

@ -54,25 +54,32 @@ class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget):
def progress_cb(self, loop, data): def progress_cb(self, loop, data):
progress_id, message, profile = data progress_id, message, profile = data
data = self.host.bridge.progress_get(progress_id, profile)
pbar = self.progress_dict[(progress_id, profile)]["progress"] def progress_get_cb(data):
entry = self.progress_dict.get((progress_id, profile))
if entry is None:
# the bar has been removed in the meantime
return
pbar = entry["progress"]
if data: if data:
if self.progress_dict[(progress_id, profile)]["state"] == "init": if entry["state"] == "init":
# first answer, we must construct the bar # first answer, we must construct the bar
self.progress_dict[(progress_id, profile)]["state"] = "progress" entry["state"] = "progress"
pbar.done = float(data["size"]) pbar.done = float(data["size"])
pbar.set_completion(float(data["position"])) pbar.set_completion(float(data["position"]))
self.update_not_bar() self.update_not_bar()
else: else:
if self.progress_dict[(progress_id, profile)]["state"] == "progress": if entry["state"] == "progress":
self.progress_dict[(progress_id, profile)]["state"] = "done" entry["state"] = "done"
pbar.set_completion(pbar.done) pbar.set_completion(pbar.done)
self.update_not_bar() self.update_not_bar()
return return
loop.set_alarm_in(0.2, self.progress_cb, (progress_id, message, profile)) loop.set_alarm_in(0.2, self.progress_cb, (progress_id, message, profile))
self.host.bridge.progress_get(progress_id, profile, callback=progress_get_cb)
def _remove_bar(self, progress_id, profile): def _remove_bar(self, progress_id, profile):
wid = self.progress_dict[(progress_id, profile)]["full"] wid = self.progress_dict[(progress_id, profile)]["full"]
self.progress_list.remove(wid) self.progress_list.remove(wid)

View file

@ -45,11 +45,12 @@ class StatusBar(urwid.Columns):
urwid.connect_signal(self.status, "click", self.on_status_click) urwid.connect_signal(self.status, "click", self.on_status_click)
def on_presence_click(self, sender=None): def on_presence_click(self, sender=None):
if not self.host.bridge.is_connected( def show_presence_popup(connected):
self.host.current_profile if not connected:
): # FIXME: manage multi-profiles
return return
options = [commonConst.PRESENCE[presence] for presence in commonConst.PRESENCE] options = [
commonConst.PRESENCE[presence] for presence in commonConst.PRESENCE
]
list_widget = sat_widgets.GenericList( list_widget = sat_widgets.GenericList(
options=options, options=options,
option_type=sat_widgets.ClickableText, option_type=sat_widgets.ClickableText,
@ -60,10 +61,14 @@ class StatusBar(urwid.Columns):
) )
self.host.show_pop_up(decorated) self.host.show_pop_up(decorated)
# FIXME: manage multi-profiles
self.host.bridge.is_connected(
self.host.current_profile, callback=show_presence_popup
)
def on_status_click(self, sender=None): def on_status_click(self, sender=None):
if not self.host.bridge.is_connected( def show_status_popup(connected):
self.host.current_profile if not connected:
): # FIXME: manage multi-profiles
return return
pop_up_widget = sat_widgets.InputDialog( pop_up_widget = sat_widgets.InputDialog(
_("Set your status"), _("Set your status"),
@ -74,6 +79,11 @@ class StatusBar(urwid.Columns):
) )
self.host.show_pop_up(pop_up_widget) self.host.show_pop_up(pop_up_widget)
# FIXME: manage multi-profiles
self.host.bridge.is_connected(
self.host.current_profile, callback=show_status_popup
)
def on_change(self, sender=None, user_data=None): def on_change(self, sender=None, user_data=None):
new_value = user_data.get_text() new_value = user_data.get_text()
previous = ( previous = (

View file

@ -57,7 +57,7 @@ dependencies = [
"twisted[tls] >= 22.10", "twisted[tls] >= 22.10",
"twomemo >= 1.0.0, < 2", "twomemo >= 1.0.0, < 2",
"txdbus", "txdbus",
"urwid >= 1.2.0, < 3", "urwid >= 4.2.5, < 5",
"urwid-satext", "urwid-satext",
"xmlschema < 4.3.1", "xmlschema < 4.3.1",
"tx-xmpp>=0.15", "tx-xmpp>=0.15",