Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib/menu
diff options
context:
space:
mode:
authorWerner Llácer <wllacer@gmail.com>2022-02-28 16:17:10 +0100
committerGitHub <noreply@github.com>2022-02-28 16:17:10 +0100
commitf07704529f411b39756a616995c4a3f7725eb550 (patch)
treeda3a368726902ef5b5676c9de1468a79e6ae17f6 /archinstall/lib/menu
parent537b9cab037aecfd18edef156dd3ea55072918e9 (diff)
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
Diffstat (limited to 'archinstall/lib/menu')
-rw-r--r--archinstall/lib/menu/list_manager.py269
-rw-r--r--archinstall/lib/menu/menu.py20
2 files changed, 283 insertions, 6 deletions
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):