Mercurial > libervia-backend
diff libervia/cli/xmlui_manager.py @ 4075:47401850dec6
refactoring: rename `libervia.frontends.jp` to `libervia.cli`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 14:54:26 +0200 |
parents | libervia/frontends/jp/xmlui_manager.py@26b7ed2817da |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/cli/xmlui_manager.py Fri Jun 02 14:54:26 2023 +0200 @@ -0,0 +1,652 @@ +#!/usr/bin/env python3 + + +# Libervia CLI +# 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 partial +from libervia.backend.core.log import getLogger +from libervia.frontends.tools import xmlui as xmlui_base +from libervia.cli.constants import Const as C +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format + +log = getLogger(__name__) + +# workflow constants + +SUBMIT = "SUBMIT" # submit form + + +## Widgets ## + + +class Base(object): + """Base for Widget and Container""" + + type = None + _root = None + + def __init__(self, xmlui_parent): + self.xmlui_parent = xmlui_parent + self.host = self.xmlui_parent.host + + @property + def root(self): + """retrieve main XMLUI parent class""" + if self._root is not None: + return self._root + root = self + while not isinstance(root, xmlui_base.XMLUIBase): + root = root.xmlui_parent + self._root = root + return root + + def disp(self, *args, **kwargs): + self.host.disp(*args, **kwargs) + + +class Widget(Base): + category = "widget" + enabled = True + + @property + def name(self): + return self._xmlui_name + + async def show(self): + """display current widget + + must be overriden by subclasses + """ + raise NotImplementedError(self.__class__) + + def verbose_name(self, elems=None, value=None): + """add name in color to the elements + + helper method to display name which can then be used to automate commands + elems is only modified if verbosity is > 0 + @param elems(list[unicode], None): elements to display + None to display name directly + @param value(unicode, None): value to show + use self.name if None + """ + if value is None: + value = self.name + if self.host.verbosity: + to_disp = [ + A.FG_MAGENTA, + " " if elems else "", + "({})".format(value), + A.RESET, + ] + if elems is None: + self.host.disp(A.color(*to_disp)) + else: + elems.extend(to_disp) + + +class ValueWidget(Widget): + def __init__(self, xmlui_parent, value): + super(ValueWidget, self).__init__(xmlui_parent) + self.value = value + + @property + def values(self): + return [self.value] + + +class InputWidget(ValueWidget): + def __init__(self, xmlui_parent, value, read_only=False): + super(InputWidget, self).__init__(xmlui_parent, value) + self.read_only = read_only + + def _xmlui_get_value(self): + return self.value + + +class OptionsWidget(Widget): + def __init__(self, xmlui_parent, options, selected, style): + super(OptionsWidget, self).__init__(xmlui_parent) + self.options = options + self.selected = selected + self.style = style + + @property + def values(self): + return self.selected + + @values.setter + def values(self, values): + self.selected = values + + @property + def value(self): + return self.selected[0] + + @value.setter + def value(self, value): + self.selected = [value] + + def _xmlui_select_value(self, value): + self.value = value + + def _xmlui_select_values(self, values): + self.values = values + + def _xmlui_get_selected_values(self): + return self.values + + @property + def labels(self): + """return only labels from self.items""" + for value, label in self.items: + yield label + + @property + def items(self): + """return suitable items, according to style""" + no_select = self.no_select + for value, label in self.options: + if no_select or value in self.selected: + yield value, label + + @property + def inline(self): + return "inline" in self.style + + @property + def no_select(self): + return "noselect" in self.style + + +class EmptyWidget(xmlui_base.EmptyWidget, Widget): + def __init__(self, xmlui_parent): + Widget.__init__(self, xmlui_parent) + + async def show(self): + self.host.disp("") + + +class TextWidget(xmlui_base.TextWidget, ValueWidget): + type = "text" + + async def show(self): + self.host.disp(self.value) + + +class LabelWidget(xmlui_base.LabelWidget, ValueWidget): + type = "label" + + @property + def for_name(self): + try: + return self._xmlui_for_name + except AttributeError: + return None + + async def show(self, end="\n", ansi=""): + """show label + + @param end(str): same as for [LiberviaCli.disp] + @param ansi(unicode): ansi escape code to print before label + """ + self.disp(A.color(ansi, self.value), end=end) + + +class JidWidget(xmlui_base.JidWidget, TextWidget): + type = "jid" + + +class StringWidget(xmlui_base.StringWidget, InputWidget): + type = "string" + + async def show(self): + if self.read_only or self.root.read_only: + self.disp(self.value) + else: + elems = [] + self.verbose_name(elems) + if self.value: + elems.append(_("(enter: {value})").format(value=self.value)) + elems.extend([C.A_HEADER, "> "]) + value = await self.host.ainput(A.color(*elems)) + if value: + # TODO: empty value should be possible + # an escape key should be used for default instead of enter with empty value + self.value = value + + +class JidInputWidget(xmlui_base.JidInputWidget, StringWidget): + type = "jid_input" + + +class PasswordWidget(xmlui_base.PasswordWidget, StringWidget): + type = "password" + + +class TextBoxWidget(xmlui_base.TextWidget, StringWidget): + type = "textbox" + # TODO: use a more advanced input method + + async def show(self): + self.verbose_name() + if self.read_only or self.root.read_only: + self.disp(self.value) + else: + if self.value: + self.disp( + A.color(C.A_HEADER, "↓ current value ↓\n", A.FG_CYAN, self.value, "") + ) + + values = [] + while True: + try: + if not values: + line = await self.host.ainput( + A.color(C.A_HEADER, "[Ctrl-D to finish]> ") + ) + else: + line = await self.host.ainput() + values.append(line) + except EOFError: + break + + self.value = "\n".join(values).rstrip() + + +class XHTMLBoxWidget(xmlui_base.XHTMLBoxWidget, StringWidget): + type = "xhtmlbox" + + async def show(self): + # FIXME: we use bridge in a blocking way as permitted by python-dbus + # this only for now to make it simpler, it must be refactored to use async + # when libervia-cli will be fully async (expected for 0.8) + self.value = await self.host.bridge.syntax_convert( + self.value, C.SYNTAX_XHTML, "markdown", False, self.host.profile + ) + await super(XHTMLBoxWidget, self).show() + + +class ListWidget(xmlui_base.ListWidget, OptionsWidget): + type = "list" + # TODO: handle flags, notably multi + + async def show(self): + if self.root.values_only: + for value in self.values: + self.disp(self.value) + return + if not self.options: + return + + # list display + self.verbose_name() + + for idx, (value, label) in enumerate(self.options): + elems = [] + if not self.root.read_only: + elems.extend([C.A_SUBHEADER, str(idx), A.RESET, ": "]) + elems.append(label) + self.verbose_name(elems, value) + self.disp(A.color(*elems)) + + if self.root.read_only: + return + + if len(self.options) == 1: + # we have only one option, no need to ask + self.value = self.options[0][0] + return + + # we ask use to choose an option + choice = None + limit_max = len(self.options) - 1 + while choice is None or choice < 0 or choice > limit_max: + choice = await self.host.ainput( + A.color( + C.A_HEADER, + _("your choice (0-{limit_max}): ").format(limit_max=limit_max), + ) + ) + try: + choice = int(choice) + except ValueError: + choice = None + self.value = self.options[choice][0] + self.disp("") + + +class BoolWidget(xmlui_base.BoolWidget, InputWidget): + type = "bool" + + async def show(self): + disp_true = A.color(A.FG_GREEN, "TRUE") + disp_false = A.color(A.FG_RED, "FALSE") + if self.read_only or self.root.read_only: + self.disp(disp_true if self.value else disp_false) + else: + self.disp( + A.color( + C.A_HEADER, "0: ", disp_false, A.RESET, " *" if not self.value else "" + ) + ) + self.disp( + A.color(C.A_HEADER, "1: ", disp_true, A.RESET, " *" if self.value else "") + ) + choice = None + while choice not in ("0", "1"): + elems = [C.A_HEADER, _("your choice (0,1): ")] + self.verbose_name(elems) + choice = await self.host.ainput(A.color(*elems)) + self.value = bool(int(choice)) + self.disp("") + + def _xmlui_get_value(self): + return C.bool_const(self.value) + + ## Containers ## + + +class Container(Base): + category = "container" + + def __init__(self, xmlui_parent): + super(Container, self).__init__(xmlui_parent) + self.children = [] + + def __iter__(self): + return iter(self.children) + + def _xmlui_append(self, widget): + self.children.append(widget) + + def _xmlui_remove(self, widget): + self.children.remove(widget) + + async def show(self): + for child in self.children: + await child.show() + + +class VerticalContainer(xmlui_base.VerticalContainer, Container): + type = "vertical" + + +class PairsContainer(xmlui_base.PairsContainer, Container): + type = "pairs" + + +class LabelContainer(xmlui_base.PairsContainer, Container): + type = "label" + + async def show(self): + for child in self.children: + end = "\n" + # we check linked widget type + # to see if we want the label on the same line or not + if child.type == "label": + for_name = child.for_name + if for_name: + for_widget = self.root.widgets[for_name] + wid_type = for_widget.type + if self.root.values_only or wid_type in ( + "text", + "string", + "jid_input", + ): + end = " " + elif wid_type == "bool" and for_widget.read_only: + end = " " + await child.show(end=end, ansi=A.FG_CYAN) + else: + await child.show() + + ## Dialogs ## + + +class Dialog(object): + def __init__(self, xmlui_parent): + self.xmlui_parent = xmlui_parent + self.host = self.xmlui_parent.host + + def disp(self, *args, **kwargs): + self.host.disp(*args, **kwargs) + + async def show(self): + """display current dialog + + must be overriden by subclasses + """ + raise NotImplementedError(self.__class__) + + +class MessageDialog(xmlui_base.MessageDialog, Dialog): + def __init__(self, xmlui_parent, title, message, level): + Dialog.__init__(self, xmlui_parent) + xmlui_base.MessageDialog.__init__(self, xmlui_parent) + self.title, self.message, self.level = title, message, level + + async def show(self): + # TODO: handle level + if self.title: + self.disp(A.color(C.A_HEADER, self.title)) + self.disp(self.message) + + +class NoteDialog(xmlui_base.NoteDialog, Dialog): + def __init__(self, xmlui_parent, title, message, level): + Dialog.__init__(self, xmlui_parent) + xmlui_base.NoteDialog.__init__(self, xmlui_parent) + self.title, self.message, self.level = title, message, level + + async def show(self): + # TODO: handle title + error = self.level in (C.XMLUI_DATA_LVL_WARNING, C.XMLUI_DATA_LVL_ERROR) + if self.level == C.XMLUI_DATA_LVL_WARNING: + msg = A.color(C.A_WARNING, self.message) + elif self.level == C.XMLUI_DATA_LVL_ERROR: + msg = A.color(C.A_FAILURE, self.message) + else: + msg = self.message + self.disp(msg, error=error) + + +class ConfirmDialog(xmlui_base.ConfirmDialog, Dialog): + def __init__(self, xmlui_parent, title, message, level, buttons_set): + Dialog.__init__(self, xmlui_parent) + xmlui_base.ConfirmDialog.__init__(self, xmlui_parent) + self.title, self.message, self.level, self.buttons_set = ( + title, + message, + level, + buttons_set, + ) + + async def show(self): + # TODO: handle buttons_set and level + self.disp(self.message) + if self.title: + self.disp(A.color(C.A_HEADER, self.title)) + input_ = None + while input_ not in ("y", "n"): + input_ = await self.host.ainput(f"{self.message} (y/n)? ") + input_ = input_.lower() + if input_ == "y": + self._xmlui_validated() + else: + self._xmlui_cancelled() + + ## Factory ## + + +class WidgetFactory(object): + def __getattr__(self, attr): + if attr.startswith("create"): + cls = globals()[attr[6:]] + return cls + + +class XMLUIPanel(xmlui_base.AIOXMLUIPanel): + widget_factory = WidgetFactory() + _actions = 0 # use to keep track of bridge's action_launch calls + read_only = False + values_only = False + workflow = None + _submit_cb = None + + def __init__( + self, + host, + parsed_dom, + title=None, + flags=None, + callback=None, + ignore=None, + whitelist=None, + profile=None, + ): + xmlui_base.XMLUIPanel.__init__( + self, + host, + parsed_dom, + title=title, + flags=flags, + ignore=ignore, + whitelist=whitelist, + profile=host.profile, + ) + self.submitted = False + + @property + def command(self): + return self.host.command + + def disp(self, *args, **kwargs): + self.host.disp(*args, **kwargs) + + async def show(self, workflow=None, read_only=False, values_only=False): + """display the panel + + @param workflow(list, None): command to execute if not None + put here for convenience, the main workflow is the class attribute + (because workflow can continue in subclasses) + command are a list of consts or lists: + - SUBMIT is the only constant so far, it submits the XMLUI + - list must contain widget name/widget value to fill + @param read_only(bool): if True, don't request values + @param values_only(bool): if True, only show select values (imply read_only) + """ + self.read_only = read_only + self.values_only = values_only + if self.values_only: + self.read_only = True + if workflow: + XMLUIPanel.workflow = workflow + if XMLUIPanel.workflow: + await self.run_workflow() + else: + await self.main_cont.show() + + async def run_workflow(self): + """loop into workflow commands and execute commands + + SUBMIT will interrupt workflow (which will be continue on callback) + @param workflow(list): same as [show] + """ + workflow = XMLUIPanel.workflow + while True: + try: + cmd = workflow.pop(0) + except IndexError: + break + if cmd == SUBMIT: + await self.on_form_submitted() + self.submit_id = None # avoid double submit + return + elif isinstance(cmd, list): + name, value = cmd + widget = self.widgets[name] + if widget.type == "bool": + value = C.bool(value) + widget.value = value + await self.show() + + async def submit_form(self, callback=None): + XMLUIPanel._submit_cb = callback + await self.on_form_submitted() + + async def on_form_submitted(self, ignore=None): + # self.submitted is a Q&D workaround to avoid + # double submit when a workflow is set + if self.submitted: + return + self.submitted = True + await super(XMLUIPanel, self).on_form_submitted(ignore) + + def _xmlui_close(self): + pass + + async def _launch_action_cb(self, data): + XMLUIPanel._actions -= 1 + assert XMLUIPanel._actions >= 0 + if "xmlui" in data: + xmlui_raw = data["xmlui"] + xmlui = create(self.host, xmlui_raw) + await xmlui.show() + if xmlui.submit_id: + await xmlui.on_form_submitted() + # TODO: handle data other than XMLUI + if not XMLUIPanel._actions: + if self._submit_cb is None: + self.host.quit() + else: + self._submit_cb() + + async def _xmlui_launch_action(self, action_id, data): + XMLUIPanel._actions += 1 + try: + data = data_format.deserialise( + await self.host.bridge.action_launch( + action_id, + data_format.serialise(data), + self.profile, + ) + ) + except Exception as e: + self.disp(f"can't launch XMLUI action: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self._launch_action_cb(data) + + +class XMLUIDialog(xmlui_base.XMLUIDialog): + type = "dialog" + dialog_factory = WidgetFactory() + read_only = False + + async def show(self, __=None): + await self.dlg.show() + + def _xmlui_close(self): + pass + + +create = partial( + xmlui_base.create, + class_map={xmlui_base.CLASS_PANEL: XMLUIPanel, xmlui_base.CLASS_DIALOG: XMLUIDialog}, +)