index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
author | Daniel Girtler <blackrabbit256@gmail.com> | 2022-06-09 22:54:12 +1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-06-09 14:54:12 +0200 |
commit | 0bdb46f3081aaed095f87e35b18b3714d8782ed1 (patch) | |
tree | 894ef3050fdfae1b455fea5ad06aac980c9e26a5 /archinstall/lib | |
parent | aec86eb04e96f4c178cf89f7e72a257d12019fdc (diff) |
-rw-r--r-- | archinstall/lib/menu/global_menu.py | 16 | ||||
-rw-r--r-- | archinstall/lib/menu/list_manager.py | 100 | ||||
-rw-r--r-- | archinstall/lib/menu/menu.py | 21 | ||||
-rw-r--r-- | archinstall/lib/models/network_configuration.py | 67 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/manage_users_conf.py | 42 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/network_conf.py | 96 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/subvolume_config.py | 24 |
diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py index b73fb48f..1a292476 100644 --- a/archinstall/lib/menu/global_menu.py +++ b/archinstall/lib/menu/global_menu.py @@ -163,7 +163,8 @@ class GlobalMenu(GeneralMenu): Selector( _('Network configuration'), ask_to_configure_network, - display_func=lambda x: self._prev_network_configuration(x), + display_func=lambda x: self._display_network_conf(x), + preview_func=self._prev_network_config, default={}) self._menu_options['timezone'] = \ Selector( @@ -226,16 +227,23 @@ class GlobalMenu(GeneralMenu): return _('Install ({} config(s) missing)').format(missing) return _('Install') - def _prev_network_configuration(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: + def _display_network_conf(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: if not cur_value: return _('Not configured, unavailable unless setup manually') else: if isinstance(cur_value, list): - ifaces = [x.iface for x in cur_value] - return f'Configured ifaces: {ifaces}' + return str(_('Configured {} interfaces')).format(len(cur_value)) else: return str(cur_value) + def _prev_network_config(self) -> Optional[str]: + selector = self._menu_options['nic'] + if selector.has_selection(): + ifaces = selector.current_selection + if isinstance(ifaces, list): + return FormattedOutput.as_table(ifaces) + return None + def _prev_harddrives(self) -> Optional[str]: selector = self._menu_options['harddrives'] if selector.has_selection(): diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index 40d01ce3..fe491caa 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -86,7 +86,7 @@ The contents in the base class of this methods serve for a very basic usage, and """ import copy from os import system -from typing import Union, Any, TYPE_CHECKING, Dict, Optional +from typing import Union, Any, TYPE_CHECKING, Dict, Optional, Tuple, List from .text_input import TextInput from .menu import Menu @@ -135,9 +135,9 @@ class ListManager: elif isinstance(default_action,(list,tuple)): self._default_action = default_action else: - self._default_action = [str(default_action),] + self._default_action = [str(default_action)] - self._header = header if header else None + self._header = header if header else '' self._cancel_action = str(_('Cancel')) self._confirm_action = str(_('Confirm and exit')) self._separator = '' @@ -155,61 +155,81 @@ class ListManager: while True: # this will return a dictionary with the key as the menu entry to be displayed # and the value is the original value from the self._data container - data_formatted = self.reformat(self._data) - options = list(data_formatted.keys()) - - if len(options) > 0: - options.append(self._separator) + options, header = self._prepare_selection(data_formatted) - if self._default_action: - options += self._default_action + menu_header = self._header - options += self._bottom_list + if header: + menu_header += header system('clear') - target = Menu( + choice = Menu( self._prompt, options, sort=False, clear_screen=False, clear_menu_on_exit=False, - header=self._header, + header=header, skip_empty_entries=True, - skip=False + skip=False, + show_search_hint=False ).run() - if not target.value or target.value in self._bottom_list: - self.action = target + if not choice.value or choice.value in self._bottom_list: + self.action = choice break - if target.value and target.value in self._default_action: - self.action = target.value + 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[target.value] + 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[target.value]][0] + self.target = [d for d in self._data if d == data_formatted[choice.value]][0] else: - self.target = self._data[data_formatted[target.value]] + self.target = self._data[data_formatted[choice.value]] # Possible enhancement. If run_actions returns false a message line indicating the failure - self.run_actions(target.value) + self.run_actions(choice.value) - if target.value == self._cancel_action: # TODO dubious + if choice.value == self._cancel_action: return self._original_data # return the original list else: return self._data - def run_actions(self,prompt_data=None): + def _prepare_selection(self, data_formatted: Dict[str, Any]) -> Tuple[List[str], str]: + # header rows are mapped to None so make sure + # to exclude those from the selectable data + options: List[str] = [key for key, val in data_formatted.items() if val is not None] + header = '' + + if len(options) > 0: + table_header = [key for key, val in data_formatted.items() if val is None] + header = '\n'.join(table_header) + + 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._bottom_list + return options, header + + def run_actions(self,prompt_data=''): options = self.action_list() + self._bottom_item - prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target) + display_value = self.selected_action_display(self.target) if self.target else prompt_data + + prompt = _("Select an action for '{}'").format(display_value) + choice = Menu( prompt, options, @@ -225,26 +245,28 @@ class ListManager: if self.action and self.action != self._cancel_action: self._data = self.exec_action(self._data) - """ - The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case - """ + def selected_action_display(self, selection: Any) -> str: + # this will return the value to be displayed in the + # "Select an action for '{}'" string + raise NotImplementedError('Please implement me in the child class') - def reformat(self, data: Any) -> Dict[str, Any]: - """ - 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(data,dict): - return {f'{k}: {v}': k for k, v in data.items()} - else: - return {str(k): k for k in data} + def reformat(self, data: List[Any]) -> Dict[str, Any]: + # this should return a dictionary of display string to actual data entry + # mapping; if the value for a given display string is None it will be used + # 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 """ - return self._base_actions + 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): """ diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 3a26f6e7..80982db0 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from enum import Enum, auto +from os import system from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional from archinstall.lib.menu.simple_menu import TerminalMenu @@ -57,7 +58,11 @@ class Menu(TerminalMenu): header :Union[List[str],str] = None, explode_on_interrupt :bool = False, explode_warning :str = '', - **kwargs + clear_screen: bool = True, + show_search_hint: bool = True, + cycle_cursor: bool = True, + clear_menu_on_exit: bool = True, + skip_empty_entries: bool = False ): """ Creates a new menu @@ -153,8 +158,7 @@ class Menu(TerminalMenu): if header: if not isinstance(header,(list,tuple)): header = [header] - header = '\n'.join(header) - menu_title += f'\n{header}\n' + menu_title += '\n'.join(header) action_info = '' if skip: @@ -178,10 +182,6 @@ 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, @@ -200,7 +200,11 @@ class Menu(TerminalMenu): preview_title=preview_title, explode_on_interrupt=self._explode_on_interrupt, multi_select_select_on_accept=False, - **kwargs, + clear_screen=clear_screen, + show_search_hint=show_search_hint, + cycle_cursor=cycle_cursor, + clear_menu_on_exit=clear_menu_on_exit, + skip_empty_entries=skip_empty_entries ) def _show(self) -> MenuSelection: @@ -238,6 +242,7 @@ class Menu(TerminalMenu): return self.run() if ret.type_ is not MenuSelectionType.Selection and not self._skip: + system('clear') return self.run() return ret diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index 4f135da5..e026e97b 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -39,8 +39,22 @@ class NetworkConfiguration: else: return 'Unknown type' - # for json serialization when calling json.dumps(...) on this class - def json(self): + def as_json(self) -> Dict: + exclude_fields = ['type'] + data = {} + for k, v in self.__dict__.items(): + if k not in exclude_fields: + if isinstance(v, list) and len(v) == 0: + v = '' + elif v is None: + v = '' + + data[k] = v + + return data + + def json(self) -> Dict: + # for json serialization when calling json.dumps(...) on this class return self.__dict__ def is_iso(self) -> bool: @@ -111,19 +125,10 @@ class NetworkConfigurationHandler: else: # not recognized return None - def _parse_manual_config(self, config: Dict[str, Any]) -> Union[None, List[NetworkConfiguration]]: - manual_configs: List = config.get('config', []) - - if not manual_configs: - return None - - if not isinstance(manual_configs, list): - log(_('Manual configuration setting must be a list')) - exit(1) - + def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]: configurations = [] - for manual_config in manual_configs: + for manual_config in configs: iface = manual_config.get('iface', None) if iface is None: @@ -135,7 +140,7 @@ class NetworkConfigurationHandler: NetworkConfiguration(NicType.MANUAL, iface=iface) ) else: - ip = config.get('ip', '') + ip = manual_config.get('ip', '') if not ip: log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red') exit(1) @@ -145,32 +150,34 @@ class NetworkConfigurationHandler: NicType.MANUAL, iface=iface, ip=ip, - gateway=config.get('gateway', ''), - dns=config.get('dns', []), + gateway=manual_config.get('gateway', ''), + dns=manual_config.get('dns', []), dhcp=False ) ) return configurations - def parse_arguments(self, config: Any): - nic_type = config.get('type', None) - - if not nic_type: - # old style definitions - network_config = self._backwards_compability_config(config) - if network_config: - return network_config - return None - + def _parse_nic_type(self, nic_type: str) -> NicType: try: - type_ = NicType(nic_type) + return NicType(nic_type) except ValueError: options = [e.value for e in NicType] log(_('Unknown nic type: {}. Possible values are {}').format(nic_type, options), fg='red') exit(1) - if type_ != NicType.MANUAL: - self._configuration = NetworkConfiguration(type_) - else: # manual configuration settings + def parse_arguments(self, config: Any): + if isinstance(config, list): # new data format self._configuration = self._parse_manual_config(config) + elif nic_type := config.get('type', None): # new data format + type_ = self._parse_nic_type(nic_type) + + if type_ != NicType.MANUAL: + self._configuration = NetworkConfiguration(type_) + else: # manual configuration settings + self._configuration = self._parse_manual_config([config]) + else: # old style definitions + network_config = self._backwards_compability_config(config) + if network_config: + return network_config + return None diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py index 567a2964..33c31342 100644 --- a/archinstall/lib/user_interaction/manage_users_conf.py +++ b/archinstall/lib/user_interaction/manage_users_conf.py @@ -7,6 +7,7 @@ from .utils import get_password from ..menu import Menu from ..menu.list_manager import ListManager from ..models.users import User +from ..output import FormattedOutput if TYPE_CHECKING: _: Any @@ -18,13 +19,6 @@ class UserList(ListManager): """ def __init__(self, prompt: str, lusers: List[User]): - """ - param: prompt - type: str - param: lusers dict with the users already defined for the system - type: Dict - param: sudo. boolean to determine if we handle superusers or users. If None handles both types - """ self._actions = [ str(_('Add a user')), str(_('Change password')), @@ -34,21 +28,25 @@ class UserList(ListManager): super().__init__(prompt, lusers, self._actions, self._actions[0]) def reformat(self, data: List[User]) -> Dict[str, User]: - return {e.display(): e for e in data} + table = FormattedOutput.as_table(data) + rows = table.split('\n') - def action_list(self): - active_user = self.target if self.target else None + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, user in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = user - if active_user is None: - return [self._actions[0]] - else: - return self._actions[1:] + return display_data + + def selected_action_display(self, user: User) -> str: + return user.username def exec_action(self, data: List[User]) -> List[User]: - if self.target: - active_user = self.target - else: - active_user = None + active_user = self.target if self.target else None if self.action == self._actions[0]: # add new_user = self._add_user() @@ -77,8 +75,7 @@ class UserList(ListManager): return False def _add_user(self) -> Optional[User]: - print(_('\nDefine a new user\n')) - prompt = str(_('Enter username (leave blank to skip): ')) + prompt = '\n\n' + str(_('Enter username (leave blank to skip): ')) while True: username = input(prompt).strip(' ') @@ -94,7 +91,9 @@ class UserList(ListManager): choice = Menu( str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), skip=False, - default_option=Menu.no() + default_option=Menu.no(), + clear_screen=False, + show_search_hint=False ).run() sudo = True if choice.value == Menu.yes() else False @@ -102,6 +101,5 @@ class UserList(ListManager): def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]: - prompt = prompt if prompt else _('Enter username (leave blank to skip): ') users = UserList(prompt, defined_users).run() return users diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py index 5154d8b1..4f22b790 100644 --- a/archinstall/lib/user_interaction/network_conf.py +++ b/archinstall/lib/user_interaction/network_conf.py @@ -2,7 +2,7 @@ from __future__ import annotations import ipaddress import logging -from typing import Any, Optional, TYPE_CHECKING, List, Union +from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict from ..menu.menu import MenuSelectionType from ..menu.text_input import TextInput @@ -10,7 +10,7 @@ from ..models.network_configuration import NetworkConfiguration, NicType from ..networking import list_interfaces from ..menu import Menu -from ..output import log +from ..output import log, FormattedOutput from ..menu.list_manager import ListManager if TYPE_CHECKING: @@ -19,55 +19,57 @@ if TYPE_CHECKING: class ManualNetworkConfig(ListManager): """ - subclass of ListManager for the managing of network configuration accounts + subclass of ListManager for the managing of network configurations """ - def __init__(self, prompt: str, ifaces: Union[None, NetworkConfiguration, List[NetworkConfiguration]]): - """ - param: prompt - type: str - param: ifaces already defined previously - type: Dict - """ + def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): + self._actions = [ + str(_('Add interface')), + str(_('Edit interface')), + str(_('Delete interface')) + ] - if ifaces is not None and isinstance(ifaces, list): - display_values = {iface.iface: iface for iface in ifaces} - else: - display_values = {} + super().__init__(prompt, ifaces, self._actions, self._actions[0]) + + def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') - self._action_add = str(_('Add interface')) - self._action_edit = str(_('Edit interface')) - self._action_delete = str(_('Delete interface')) + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} - self._iface_actions = [self._action_edit, self._action_delete] + for row, iface in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = iface - super().__init__(prompt, display_values, self._iface_actions, self._action_add) + return display_data - def run_manual(self) -> List[NetworkConfiguration]: - ifaces = super().run() - if ifaces is not None: - return list(ifaces.values()) - return [] + def selected_action_display(self, iface: NetworkConfiguration) -> str: + return iface.iface if iface.iface else '' - def exec_action(self, data: Any): - if self.action == self._action_add: - iface_name = self._select_iface(data.keys()) + 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 + iface_name = self._select_iface(data) if iface_name: iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) - data[iface_name] = self._edit_iface(iface) - elif self.target: - iface_name = list(self.target.keys())[0] - iface = data[iface_name] - - if self.action == self._action_edit: - data[iface_name] = self._edit_iface(iface) - elif self.action == self._action_delete: - del data[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] return data - def _select_iface(self, existing_ifaces: List[str]) -> Optional[Any]: + def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: all_ifaces = list_interfaces().values() + existing_ifaces = [d.iface for d in data] available = set(all_ifaces) - set(existing_ifaces) choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() @@ -76,7 +78,7 @@ class ManualNetworkConfig(ListManager): return choice.value - def _edit_iface(self, edit_iface :NetworkConfiguration): + def _edit_iface(self, edit_iface: NetworkConfiguration): iface_name = edit_iface.iface modes = ['DHCP (auto detect)', 'IP (static)'] default_mode = 'DHCP (auto detect)' @@ -99,11 +101,13 @@ class ManualNetworkConfig(ListManager): gateway = None while 1: - gateway_input = TextInput(_('Enter your gateway (router) IP address or leave blank for none: '), - edit_iface.gateway).run().strip() + gateway = TextInput( + _('Enter your gateway (router) IP address or leave blank for none: '), + edit_iface.gateway + ).run().strip() try: - if len(gateway_input) > 0: - ipaddress.ip_address(gateway_input) + if len(gateway) > 0: + ipaddress.ip_address(gateway) break except ValueError: log("You need to enter a valid gateway (router) IP address.", level=logging.WARNING, fg='red') @@ -124,7 +128,9 @@ class ManualNetworkConfig(ListManager): return NetworkConfiguration(NicType.MANUAL, iface=iface_name) -def ask_to_configure_network(preset: Union[None, NetworkConfiguration, List[NetworkConfiguration]]) -> Optional[Union[List[NetworkConfiguration], NetworkConfiguration]]: +def ask_to_configure_network( + preset: Union[NetworkConfiguration, List[NetworkConfiguration]] +) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: """ Configure the network on the newly installed system """ @@ -165,7 +171,7 @@ def ask_to_configure_network(preset: Union[None, NetworkConfiguration, List[Netw elif choice.value == network_options['network_manager']: return NetworkConfiguration(NicType.NM) elif choice.value == network_options['manual']: - manual = ManualNetworkConfig('Configure interfaces', preset) - return manual.run_manual() + preset_ifaces = preset if isinstance(preset, list) else [] + return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() return preset diff --git a/archinstall/lib/user_interaction/subvolume_config.py b/archinstall/lib/user_interaction/subvolume_config.py index a54ec891..e2797bba 100644 --- a/archinstall/lib/user_interaction/subvolume_config.py +++ b/archinstall/lib/user_interaction/subvolume_config.py @@ -5,6 +5,7 @@ from ..menu.menu import MenuSelectionType from ..menu.text_input import TextInput from ..menu import Menu from ..models.subvolume import Subvolume +from ... import FormattedOutput if TYPE_CHECKING: _: Any @@ -19,16 +20,23 @@ class SubvolumeList(ListManager): ] super().__init__(prompt, current_volumes, self._actions, self._actions[0]) - def reformat(self, data: List[Subvolume]) -> Dict[str, Subvolume]: - return {e.display(): e for e in data} + def reformat(self, data: List[Subvolume]) -> Dict[str, Optional[Subvolume]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') - def action_list(self): - active_user = self.target if self.target else None + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[Subvolume]] = {f' {rows[0]}': None, f' {rows[1]}': None} - if active_user is None: - return [self._actions[0]] - else: - return self._actions[1:] + for row, subvol in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = subvol + + return display_data + + def selected_action_display(self, subvolume: Subvolume) -> str: + return subvolume.name def _prompt_options(self, editing: Optional[Subvolume] = None) -> List[str]: preset_options = [] |