From 5c3c1312a49e1c110d4c5825fbb8242868544900 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 14 Jun 2022 22:38:39 +1000 Subject: Update list manager (#1331) * Rework partition management * Update * Update list manager * Update * Update Co-authored-by: Daniel Girtler --- archinstall/lib/disk/partition.py | 35 ++- archinstall/lib/menu/list_manager.py | 262 +++------------------ .../lib/user_interaction/manage_users_conf.py | 22 +- archinstall/lib/user_interaction/network_conf.py | 20 +- .../lib/user_interaction/partitioning_conf.py | 14 +- .../lib/user_interaction/subvolume_config.py | 32 ++- 6 files changed, 109 insertions(+), 276 deletions(-) diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py index 062c79ab..17c24d57 100644 --- a/archinstall/lib/disk/partition.py +++ b/archinstall/lib/disk/partition.py @@ -17,14 +17,16 @@ from .btrfs.btrfs_helpers import subvolume_info_from_path from .btrfs.btrfssubvolumeinfo import BtrfsSubvolumeInfo class Partition: - def __init__(self, + def __init__( + self, path: str, block_device: BlockDevice, part_id :Optional[str] = None, filesystem :Optional[str] = None, mountpoint :Optional[str] = None, encrypted :bool = False, - autodetect_filesystem :bool = True): + autodetect_filesystem :bool = True + ): if not part_id: part_id = os.path.basename(path) @@ -76,7 +78,30 @@ class Partition: else: return f'Partition(path={self.path}, size={self.size}, PARTUUID={self._safe_uuid}, fs={self.filesystem}{mount_repr})' + def as_json(self) -> Dict[str, Any]: + """ + this is used for the table representation of the partition (see FormattedOutput) + """ + partition_info = { + 'type': 'primary', + 'PARTUUID': self._safe_uuid, + 'wipe': self.allow_formatting, + 'boot': self.boot, + 'ESP': self.boot, + 'mountpoint': self.target_mountpoint, + 'encrypted': self._encrypted, + 'start': self.start, + 'size': self.end, + 'filesystem': self.filesystem_type + } + + return partition_info + def __dump__(self) -> Dict[str, Any]: + # TODO remove this in favour of as_json + + log(get_filesystem_type(self.path)) + return { 'type': 'primary', 'PARTUUID': self._safe_uuid, @@ -88,10 +113,14 @@ class Partition: 'start': self.start, 'size': self.end, 'filesystem': { - 'format': get_filesystem_type(self.path) + 'format': self.filesystem_type } } + @property + def filesystem_type(self) -> Optional[str]: + return get_filesystem_type(self.path) + @property def mountpoint(self) -> Optional[str]: try: diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index fe491caa..e7a9c2ac 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -1,94 +1,7 @@ -#!/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 initialization -``` - 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() -``` - -""" import copy from os import system -from typing import Union, Any, TYPE_CHECKING, Dict, Optional, Tuple, List +from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List -from .text_input import TextInput from .menu import Menu if TYPE_CHECKING: @@ -98,58 +11,38 @@ if TYPE_CHECKING: class ListManager: def __init__( self, - prompt :str, - base_list :Union[list,dict] , - base_actions :list = None, - null_action :str = None, - default_action :Union[str,list] = None, - header :Union[str,list] = None): + prompt: str, + entries: List[Any], + base_actions: List[str], + sub_menu_actions: List[str] + ): """ - param :prompt Text which will appear at the header + :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 + :param entries: list/dict of option to be shown / manipulated 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 or list + :param base_actions: list of actions that is displayed in the main list manager, + usually global actions such as 'Add...' + type param: list - param: header one or more header lines for the list - type param: string or list + :param sub_menu_actions: list of actions available for a chosen entry + type param: list """ + self._original_data = copy.deepcopy(entries) + self._data = copy.deepcopy(entries) explainer = str(_('\n Choose an object from the list, and select one of the available actions for it to execute')) self._prompt = prompt + explainer if prompt else explainer - self._null_action = str(null_action) if null_action else None - - if not default_action: - self._default_action = [self._null_action] - elif isinstance(default_action,(list,tuple)): - self._default_action = default_action - else: - self._default_action = [str(default_action)] - - self._header = header if header else '' - 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._original_data = copy.deepcopy(base_list) - self._data = copy.deepcopy(base_list) # as refs, changes are immediate + self._confirm_action = str(_('Confirm and exit')) + self._cancel_action = str(_('Cancel')) - # default values for the null case - self.target: Optional[Any] = None - self.action = self._null_action + self._terminate_actions = [self._confirm_action, self._cancel_action] + self._base_actions = base_actions + self._sub_menu_actions = sub_menu_actions def run(self): while True: @@ -158,11 +51,6 @@ class ListManager: data_formatted = self.reformat(self._data) options, header = self._prepare_selection(data_formatted) - menu_header = self._header - - if header: - menu_header += header - system('clear') choice = Menu( @@ -177,27 +65,13 @@ class ListManager: show_search_hint=False ).run() - if not choice.value or choice.value in self._bottom_list: - self.action = choice + if choice.value in self._base_actions: + self._data = self.handle_action(choice.value, None, self._data) + elif choice.value in self._terminate_actions: break - - if choice.value and choice.value in self._default_action: - self.action = choice.value - self.target = None - self._data = self.exec_action(self._data) - continue - - if isinstance(self._data, dict): - data_key = data_formatted[choice.value] - key = self._data[data_key] - self.target = {data_key: key} - elif isinstance(self._data, list): - self.target = [d for d in self._data if d == data_formatted[choice.value]][0] - else: - self.target = self._data[data_formatted[choice.value]] - - # Possible enhancement. If run_actions returns false a message line indicating the failure - self.run_actions(choice.value) + else: # an entry of the existing selection was choosen + selected_entry = data_formatted[choice.value] + self._run_actions_on_entry(selected_entry) if choice.value == self._cancel_action: return self._original_data # return the original list @@ -217,16 +91,14 @@ class ListManager: if len(options) > 0: options.append(self._separator) - if self._default_action: - # done only for mypy -> todo fix the self._default_action declaration - options += [action for action in self._default_action if action] + options += self._base_actions + options += self._terminate_actions - options += self._bottom_list return options, header - def run_actions(self,prompt_data=''): - options = self.action_list() + self._bottom_item - display_value = self.selected_action_display(self.target) if self.target else prompt_data + def _run_actions_on_entry(self, entry: Any): + options = self._sub_menu_actions + [self._cancel_action] + display_value = self.selected_action_display(entry) prompt = _("Select an action for '{}'").format(display_value) @@ -236,14 +108,11 @@ class ListManager: sort=False, clear_screen=False, clear_menu_on_exit=False, - preset_values=self._bottom_item, show_search_hint=False ).run() - self.action = choice.value - - if self.action and self.action != self._cancel_action: - self._data = self.exec_action(self._data) + if choice.value and choice.value != self._cancel_action: + self._data = self.handle_action(choice.value, entry, self._data) def selected_action_display(self, selection: Any) -> str: # this will return the value to be displayed in the @@ -256,64 +125,7 @@ class ListManager: # in the header value (useful when displaying tables) raise NotImplementedError('Please implement me in the child class') - 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 - """ - active_entry = self.target if self.target else None - - if active_entry is None: - return [self._base_actions[0]] - else: - return self._base_actions[1:] - - def exec_action(self, data: Any): - """ - 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(_('Edit: '),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(_('Edit {}: ').format(origkey), origval).run() - self._data[origkey] = value - elif self.action == str(_('Delete')): - del self._data[origkey] - - return self._data + def handle_action(self, action: Any, entry: Optional[Any], data: List[Any]) -> List[Any]: + # this function is called when a base action or + # a specific action for an entry is triggered + raise NotImplementedError('Please implement me in the child class') diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py index 33c31342..a97328c2 100644 --- a/archinstall/lib/user_interaction/manage_users_conf.py +++ b/archinstall/lib/user_interaction/manage_users_conf.py @@ -25,7 +25,7 @@ class UserList(ListManager): str(_('Promote/Demote user')), str(_('Delete User')) ] - super().__init__(prompt, lusers, self._actions, self._actions[0]) + super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) def reformat(self, data: List[User]) -> Dict[str, User]: table = FormattedOutput.as_table(data) @@ -45,27 +45,25 @@ class UserList(ListManager): def selected_action_display(self, user: User) -> str: return user.username - def exec_action(self, data: List[User]) -> List[User]: - active_user = self.target if self.target else None - - if self.action == self._actions[0]: # add + def handle_action(self, action: str, entry: Optional[User], data: List[User]) -> List[User]: + if action == self._actions[0]: # add new_user = self._add_user() if new_user is not None: # in case a user with the same username as an existing user # was created we'll replace the existing one data = [d for d in data if d.username != new_user.username] data += [new_user] - elif self.action == self._actions[1]: # change password - prompt = str(_('Password for user "{}": ').format(active_user.username)) + elif action == self._actions[1]: # change password + prompt = str(_('Password for user "{}": ').format(entry.username)) new_password = get_password(prompt=prompt) if new_password: - user = next(filter(lambda x: x == active_user, data), 1) + user = next(filter(lambda x: x == entry, data), 1) user.password = new_password - elif self.action == self._actions[2]: # promote/demote - user = next(filter(lambda x: x == active_user, data), 1) + elif action == self._actions[2]: # promote/demote + user = next(filter(lambda x: x == entry, data), 1) user.sudo = False if user.sudo else True - elif self.action == self._actions[3]: # delete - data = [d for d in data if d != active_user] + elif action == self._actions[3]: # delete + data = [d for d in data if d != entry] return data diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py index 4f22b790..1908603e 100644 --- a/archinstall/lib/user_interaction/network_conf.py +++ b/archinstall/lib/user_interaction/network_conf.py @@ -29,7 +29,7 @@ class ManualNetworkConfig(ListManager): str(_('Delete interface')) ] - super().__init__(prompt, ifaces, self._actions, self._actions[0]) + super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: table = FormattedOutput.as_table(data) @@ -49,21 +49,19 @@ class ManualNetworkConfig(ListManager): def selected_action_display(self, iface: NetworkConfiguration) -> str: return iface.iface if iface.iface else '' - def exec_action(self, data: List[NetworkConfiguration]): - active_iface: Optional[NetworkConfiguration] = self.target if self.target else None - - if self.action == self._actions[0]: # add + def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): + if action == self._actions[0]: # add iface_name = self._select_iface(data) if iface_name: iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) iface = self._edit_iface(iface) data += [iface] - elif active_iface: - if self.action == self._actions[1]: # edit interface - data = [d for d in data if d.iface != active_iface.iface] - data.append(self._edit_iface(active_iface)) - elif self.action == self._actions[2]: # delete - data = [d for d in data if d != active_iface] + elif entry: + if action == self._actions[1]: # edit interface + data = [d for d in data if d.iface != entry.iface] + data.append(self._edit_iface(entry)) + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] return data diff --git a/archinstall/lib/user_interaction/partitioning_conf.py b/archinstall/lib/user_interaction/partitioning_conf.py index 0f4784b6..8bf76121 100644 --- a/archinstall/lib/user_interaction/partitioning_conf.py +++ b/archinstall/lib/user_interaction/partitioning_conf.py @@ -154,9 +154,6 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, block_device_struct = {"partitions": [partition.__dump__() for partition in block_device.partitions.values()]} original_layout = copy.deepcopy(block_device_struct) - # Test code: [part.__dump__() for part in block_device.partitions.values()] - # TODO: Squeeze in BTRFS subvolumes here - new_partition = str(_('Create a new partition')) suggest_partition_layout = str(_('Suggest partition layout')) delete_partition = str(_('Delete a partition')) @@ -209,6 +206,7 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, return original_layout elif task == save_and_exit: break + if task == new_partition: from ..disk import valid_parted_position @@ -222,8 +220,9 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, if fs_choice.type_ == MenuSelectionType.Esc: continue - prompt = _('Enter the start sector (percentage or block number, default: {}): ').format( - block_device.first_free_sector) + prompt = str(_('Enter the start sector (percentage or block number, default: {}): ')).format( + block_device.first_free_sector + ) start = input(prompt).strip() if not start.strip(): @@ -232,8 +231,9 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, else: end_suggested = '100%' - prompt = _('Enter the end sector of the partition (percentage or block number, ex: {}): ').format( - end_suggested) + prompt = str(_('Enter the end sector of the partition (percentage or block number, ex: {}): ')).format( + end_suggested + ) end = input(prompt).strip() if not end.strip(): diff --git a/archinstall/lib/user_interaction/subvolume_config.py b/archinstall/lib/user_interaction/subvolume_config.py index e2797bba..94150dee 100644 --- a/archinstall/lib/user_interaction/subvolume_config.py +++ b/archinstall/lib/user_interaction/subvolume_config.py @@ -12,13 +12,13 @@ if TYPE_CHECKING: class SubvolumeList(ListManager): - def __init__(self, prompt: str, current_volumes: List[Subvolume]): + def __init__(self, prompt: str, subvolumes: List[Subvolume]): self._actions = [ str(_('Add subvolume')), str(_('Edit subvolume')), str(_('Delete subvolume')) ] - super().__init__(prompt, current_volumes, self._actions, self._actions[0]) + super().__init__(prompt, subvolumes, [self._actions[0]], self._actions[1:]) def reformat(self, data: List[Subvolume]) -> Dict[str, Optional[Subvolume]]: table = FormattedOutput.as_table(data) @@ -75,13 +75,8 @@ class SubvolumeList(ListManager): return subvolume - def exec_action(self, data: List[Subvolume]) -> List[Subvolume]: - if self.target: - active_subvolume = self.target - else: - active_subvolume = None - - if self.action == self._actions[0]: # add + def handle_action(self, action: str, entry: Optional[Subvolume], data: List[Subvolume]) -> List[Subvolume]: + if action == self._actions[0]: # add new_subvolume = self._add_subvolume() if new_subvolume is not None: @@ -89,14 +84,15 @@ class SubvolumeList(ListManager): # was created we'll replace the existing one data = [d for d in data if d.name != new_subvolume.name] data += [new_subvolume] - elif self.action == self._actions[1]: # edit subvolume - new_subvolume = self._add_subvolume(active_subvolume) - - if new_subvolume is not None: - # we'll remove the original subvolume and add the modified version - data = [d for d in data if d.name != active_subvolume.name and d.name != new_subvolume.name] - data += [new_subvolume] - elif self.action == self._actions[2]: # delete - data = [d for d in data if d != active_subvolume] + elif entry is not None: + if action == self._actions[1]: # edit subvolume + new_subvolume = self._add_subvolume(entry) + + if new_subvolume is not None: + # we'll remove the original subvolume and add the modified version + data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name] + data += [new_subvolume] + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] return data -- cgit v1.2.3-54-g00ecf