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__)
from libervia.backend.tools import config as tools_config
import urwid
from urwid.util import is_wide_char
from urwid 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_bridge
from libervia.frontends.quick_frontend import quick_utils
from libervia.frontends.quick_frontend import quick_chat
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.backend.tools.common import dynamic_import
from libervia.frontends.tools import jid
import asyncio
import signal
import sys
@ -67,10 +69,19 @@ class TUISettings(tools_config.FrontendSettings, key="tui"):
## bridge handling
# we get bridge name from conf and initialise the right class accordingly
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:
print("only D-Bus bridge is currently supported")
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):
@ -338,9 +349,7 @@ class LiberviaTUIApp(QuickApp, InputHistory):
AVATARS_HANDLER = False
def __init__(self):
bridge_module = dynamic_import.import_bridge(
bridge_name, "libervia.frontends.bridge"
)
bridge_module = dynamic_import.import_bridge(bridge_name)
if bridge_module is None:
log.error("Can't import {} bridge".format(bridge_name))
sys.exit(3)
@ -348,16 +357,18 @@ class LiberviaTUIApp(QuickApp, InputHistory):
log.debug("Loading {} bridge".format(bridge_name))
QuickApp.__init__(
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,
check_options=quick_utils.check_options,
connect_bridge=False,
)
self.tui_settings = main_config.get_settings(TUISettings)
## main loop setup ##
event_loop = (
urwid.GLibEventLoop if "dbus" in bridge_name else urwid.TwistedEventLoop
)
# the new D-Bus bridge is twisted based (txdbus)
event_loop = urwid.TwistedEventLoop
self.loop = urwid.MainLoop(
urwid.SolidFill(),
C.PALETTE,
@ -373,6 +384,10 @@ class LiberviaTUIApp(QuickApp, InputHistory):
def on_bridge_connected(self):
##misc setup##
self._version = None
self.bridge.version_get(
callback=self._version_get_cb, errback=lambda __: None
)
self._visible_widgets = set()
self.notif_bar = sat_widgets.NotificationBar()
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.post_init()
def _version_get_cb(self, version):
self._version = version
@property
def visible_widgets(self):
return self._visible_widgets
@ -579,7 +597,7 @@ class LiberviaTUIApp(QuickApp, InputHistory):
self.__saved_overlay = None
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
self.debug()
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):
"""Add cached menus to instance
The menus are retrieved asynchronously from the bridge.
@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
@ -623,16 +644,24 @@ class LiberviaTUIApp(QuickApp, InputHistory):
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 menus_get_cb(menus):
for id_, type_, path, path_i18n, extra in menus: # 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 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):
menu = sat_widgets.Menu(self.loop)
@ -1016,19 +1045,25 @@ class LiberviaTUIApp(QuickApp, InputHistory):
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,
def show_join_dialog(default_service):
pop_up_widget = sat_widgets.InputDialog(
_("Entering a MUC room"),
_("Please enter MUC's JID"),
default_txt=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)
self.bridge.muc_get_default_service(
callback=show_join_dialog, errback=lambda __: show_join_dialog("")
)
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())
self.alert(_("About"), C.APP_NAME + " v" + (self._version or ""))
# MISC CALLBACKS#

View file

@ -174,10 +174,13 @@ class Card(TarotCard):
return CardDisplayer(self)
class Table(urwid.FlowWidget):
class Table(urwid.Widget):
"""Represent the cards currently on the table"""
_sizing = frozenset([urwid.Sizing.FLOW])
def __init__(self):
super().__init__()
self.top = self.left = self.bottom = self.right = None
def put_card(self, location, card):
@ -313,7 +316,7 @@ class TarotGame(QuickTarotGame, urwid.WidgetWrap):
if not winners and not loosers:
title = _("Draw game")
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(
self.parent.host,
xml_data,

View file

@ -17,7 +17,10 @@
# 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
try:
import dbus
except ImportError:
dbus = None
class Notify(object):

View file

@ -37,11 +37,11 @@ class ProfileManager(QuickProfileManager, urwid.WidgetWrap):
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
[], style=style, align="center", on_change=self.on_profile_change
)
# the profiles list is retrieved asynchronously
self.refill_profiles()
# new & delete buttons
buttons = [

View file

@ -54,24 +54,31 @@ class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget):
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()
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 entry["state"] == "init":
# first answer, we must construct the bar
entry["state"] = "progress"
pbar.done = float(data["size"])
loop.set_alarm_in(0.2, self.progress_cb, (progress_id, message, profile))
pbar.set_completion(float(data["position"]))
self.update_not_bar()
else:
if entry["state"] == "progress":
entry["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))
self.host.bridge.progress_get(progress_id, profile, callback=progress_get_cb)
def _remove_bar(self, progress_id, profile):
wid = self.progress_dict[(progress_id, profile)]["full"]

View file

@ -45,34 +45,44 @@ class StatusBar(urwid.Columns):
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,
def show_presence_popup(connected):
if not connected:
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)
# FIXME: manage multi-profiles
self.host.bridge.is_connected(
self.host.current_profile, callback=show_presence_popup
)
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,
def show_status_popup(connected):
if not connected:
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)
# FIXME: manage multi-profiles
self.host.bridge.is_connected(
self.host.current_profile, callback=show_status_popup
)
self.host.show_pop_up(pop_up_widget)
def on_change(self, sender=None, user_data=None):
new_value = user_data.get_text()