From f07704529f411b39756a616995c4a3f7725eb550 Mon Sep 17 00:00:00 2001 From: Werner Llácer Date: Mon, 28 Feb 2022 16:17:10 +0100 Subject: add new widget ListManager (#1005) * add new widget ListManager * flake8 complains * Null_action appears now in the main list (to simplify additions to the list) Formatted data are now at the from to the actions submenu * Define a default action in the menu, potentially independent of a null_action Both default and null actions don't have to be part of the element's action list Some cleanup --- archinstall/__init__.py | 2 + archinstall/lib/menu/list_manager.py | 269 +++++++++++++++++++++++++++++++++++ archinstall/lib/menu/menu.py | 20 ++- 3 files changed, 285 insertions(+), 6 deletions(-) create mode 100644 archinstall/lib/menu/list_manager.py diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 8c64a1f3..6a41bd49 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -34,6 +34,8 @@ from .lib.storage import * from .lib.systemd import * from .lib.user_interaction import * from .lib.menu import Menu +from .lib.menu.list_manager import ListManager +from .lib.menu.text_input import TextInput from .lib.menu.selection_menu import ( GlobalMenu, Selector, diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py new file mode 100644 index 00000000..e307e41f --- /dev/null +++ b/archinstall/lib/menu/list_manager.py @@ -0,0 +1,269 @@ +#!/usr/bin/python +""" +# Purpose +ListManager is a widget based on `menu` which allows the handling of repetitive operations in a list. +Imagine you have a list and want to add/copy/edit/delete their elements. With this widget you will be shown the list +``` +Vamos alla + +Use ESC to skip + + +> uno : 1 +dos : 2 +tres : 3 +cuatro : 4 +==> +Confirm and exit +Cancel +(Press "/" to search) +``` +Once you select one of the elements of the list, you will be promted with the action to be done to the selected element +``` + +uno : 1 +dos : 2 +> tres : 3 +cuatro : 4 +==> +Confirm and exit +Cancel +(Press "/" to search) + +Select an action for < {'tres': 3} > + + +Add +Copy +Edit +Delete +> Cancel +``` +You execute the action for this element (which might or not involve user interaction) and return to the list main page +till you call one of the options `confirm and exit` which returns the modified list or `cancel` which returns the original list unchanged. +If the list is empty one action can be defined as default (usually Add). We call it **null_action** +YOu can also define a **default_action** which will appear below the separator, not tied to any element of the list. Barring explicit definition, default_action will be the null_action +``` +==> +Add +Confirm and exit +Cancel +(Press "/" to search) +``` +The default implementation can handle simple lists and a key:value dictionary. The default actions are the shown above. +A sample of basic usage is included at the end of the source. + +More sophisticaded uses can be achieved by +* changing the action list and the null_action during intialization +``` + opciones = ListManager('Vamos alla',opciones,[str(_('Add')),str(_('Delete'))],_('Add')).run() +``` +* And using following methods to overwrite/define user actions and other details: +* * `reformat`. To change the appearance of the list elements +* * `action_list`. To modify the content of the action list once an element is defined. F.i. to avoid Delete to appear for certain elements, or to add/modify action based in the value of the element. +* * `exec_action` which contains the actual code to be executed when an action is selected + +The contents in the base class of this methods serve for a very basic usage, and are to be taken as samples. Thus the best use of this class would be to subclass in your code + +``` + class ObjectList(archinstall.ListManager): + def __init__(prompt,list): + self.ObjectAction = [... list of actions ...] + self.ObjectNullAction = one ObjectAction + super().__init__(prompt,list,ObjectActions,ObjectNullAction) + def reformat(self): + ... beautfy the output of the list + def action_list(self): + ... if you need some changes to the action list based on self.target + def exec_action(self): + if self.action == self.ObjectAction[0]: + performFirstAction(self.target, ...) + + ... + resultList = ObjectList(prompt,originallist).run() +``` + +""" + +from .text_input import TextInput +from .menu import Menu +from ..general import RequirementError +from os import system +from copy import copy + +class ListManager: + def __init__(self,prompt :str, base_list :list ,base_actions :list = None,null_action :str = None, default_action :str = None): + """ + param :prompt Text which will appear at the header + type param: string | DeferredTranslation + + param :base:_list list/dict of option to be shown / mainpulated + type param: list | dict + + param base_actions an alternate list of actions to the items of the object + type param: list + + param: null_action action which will be taken (if any) when base_list is empty + type param: string + + param: default_action action which will be presented at the bottom of the list. Shouldn't need a target. If not present, null_action is set there. + Both Null and Default actions can be defined outside the base_actions list, as long as they are launched in exec_action + type param: string + """ + + if not null_action and len(base_list) == 0: + raise RequirementError('Data list for ListManager can not be empty if there is no null_action') + + self.prompt = prompt if prompt else _('Choose an object from the list') + self.null_action = str(null_action) + if not default_action: + self.default_action = self.null_action + else: + self.default_action = str(default_action) + self.cancel_action = str(_('Cancel')) + self.confirm_action = str(_('Confirm and exit')) + self.separator = '==>' + self.bottom_list = [self.confirm_action,self.cancel_action] + self.bottom_item = [self.cancel_action] + self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))] + + self.base_data = base_list + self.data = copy(base_list) # as refs, changes are immediate + # default values for the null case + self.target = None + self.action = self.null_action + if len(self.data) == 0: + self.exec_action() + + def run(self): + while True: + self.data_formatted = self.reformat() + options = self.data_formatted + [self.separator] + if self.default_action: + options += [self.default_action] + options += self.bottom_list + system('clear') + target = Menu(self.prompt, + options, + sort=False, + clear_screen=False, + clear_menu_on_exit=False).run() + + if not target or target in self.bottom_list: + break + if target and target == self.separator: + continue + if target and target == self.default_action: + target = None + self.target = None + self.action = self.default_action + self.exec_action() + continue + if isinstance(self.data,dict): + key = list(self.data.keys())[self.data_formatted.index(target)] + self.target = {key: self.data[key]} + else: + self.target = self.data[self.data_formatted.index(target)] + # Possible enhacement. If run_actions returns false a message line indicating the failure + self.run_actions(target) + + if not target or target == self.cancel_action: # TODO dubious + return self.base_data # return the original list + else: + return self.data + + def run_actions(self,prompt_data=None): + options = self.action_list() + self.bottom_item + prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target) + self.action = Menu(prompt, + options, + sort=False, + skip=False, + clear_screen=False, + clear_menu_on_exit=False, + preset_values=self.bottom_item, + show_search_hint=False).run() + if self.action == self.cancel_action: + return False + else: + return self.exec_action() + """ + The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case + """ + + def reformat(self): + """ + method to get the data in a format suitable to be shown + It is executed once for run loop and processes the whole self.data structure + """ + if isinstance(self.data,dict): + return list(map(lambda x:f"{x} : {self.data[x]}",self.data)) + else: + return list(map(lambda x:str(x),self.data)) + + def action_list(self): + """ + can define alternate action list or customize the list for each item. + Executed after any item is selected, contained in self.target + """ + return self.base_actions + + def exec_action(self): + """ + what's executed one an item (self.target) and one action (self.action) is selected. + Should be overwritten by the user + The result is expected to update self.data in this routine, else it is ignored + The basic code is useful for simple lists and dictionaries (key:value pairs, both strings) + """ + # TODO guarantee unicity + + if isinstance(self.data,list): + if self.action == str(_('Add')): + self.target = TextInput(_('Add :'),None).run() + self.data.append(self.target) + if self.action == str(_('Copy')): + while True: + target = TextInput(_('Copy to :'),self.target).run() + if target != self.target: + self.data.append(self.target) + break + elif self.action == str(_('Edit')): + tgt = self.target + idx = self.data.index(self.target) + result = TextInput(_('Edite :'),tgt).run() + self.data[idx] = result + elif self.action == str(_('Delete')): + del self.data[self.data.index(self.target)] + elif isinstance(self.data,dict): + # allows overwrites + if self.target: + origkey,origval = list(self.target.items())[0] + else: + origkey = None + origval = None + if self.action == str(_('Add')): + key = TextInput(_('Key :'),None).run() + value = TextInput(_('Value :'),None).run() + self.data[key] = value + if self.action == str(_('Copy')): + while True: + key = TextInput(_('Copy to new key:'),origkey).run() + if key != origkey: + self.data[key] = origval + break + elif self.action == str(_('Edit')): + value = TextInput(_(f'Edit {origkey} :'),origval).run() + self.data[origkey] = value + elif self.action == str(_('Delete')): + del self.data[origkey] + + return True + + +if __name__ == "__main__": + # opciones = ['uno','dos','tres','cuatro'] + # opciones = archinstall.list_mirrors() + opciones = {'uno':1,'dos':2,'tres':3,'cuatro':4} + # acciones = ['editar','borrar','añadir'] + opciones = ListManager('Vamos alla',opciones,None,_('Add')).run() + print(opciones) diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 1d5d497e..d7b1605d 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -1,6 +1,7 @@ from typing import Dict, List, Union, Any, TYPE_CHECKING from archinstall.lib.menu.simple_menu import TerminalMenu + from ..exceptions import RequirementError from ..output import log @@ -22,7 +23,8 @@ class Menu(TerminalMenu): default_option :str = None, sort :bool = True, preset_values :Union[str, List[str]] = None, - cursor_index :int = None + cursor_index :int = None, + **kwargs ): """ Creates a new menu @@ -51,6 +53,8 @@ class Menu(TerminalMenu): :param cursor_index: The position where the cursor will be located. If it is not in range (number of elements of the menu) it goes to the first position :type cursor_index: int + + :param kwargs : any SimpleTerminal parameter """ # we guarantee the inmutability of the options outside the class. # an unknown number of iterables (.keys(),.values(),generator,...) can't be directly copied, in this case @@ -103,19 +107,23 @@ class Menu(TerminalMenu): cursor = "> " main_menu_cursor_style = ("fg_cyan", "bold") main_menu_style = ("bg_blue", "fg_gray") - + # defaults that can be changed up the stack + kwargs['clear_screen'] = kwargs.get('clear_screen',True) + kwargs['show_search_hint'] = kwargs.get('show_search_hint',True) + kwargs['cycle_cursor'] = kwargs.get('cycle_cursor',True) super().__init__( menu_entries=self.menu_options, title=menu_title, menu_cursor=cursor, menu_cursor_style=main_menu_cursor_style, menu_highlight_style=main_menu_style, - cycle_cursor=True, - clear_screen=True, + # cycle_cursor=True, + # clear_screen=True, multi_select=multi, - show_search_hint=True, + # show_search_hint=True, preselected_entries=self.preset_values, - cursor_index=self.cursor_index + cursor_index=self.cursor_index, + **kwargs, ) def _show(self): -- cgit v1.2.3-70-g09d2