Mercurial > libervia-backend
view libervia/cli/xmlui_manager.py @ 4340:ea72364131d5 default tip @
doc (components): Update Email Gateway documentation:
A section has been added to explain how attachments are handled.
fix 453
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 03 Dec 2024 00:53:18 +0100 |
parents | 47401850dec6 |
children |
line wrap: on
line source
#!/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}, )