From 908c7b8cc0a804e9522d93fcf0dc71034c53ccdb Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 3 Dec 2021 07:17:51 +1100 Subject: Add simple menu for better UX (#660) * Add simple menu for better UX * Add remove external dependency * Fix harddisk return value on skip * Table output for partitioning process * Switch partitioning to simple menu * fixup! Switch partitioning to simple menu * Ignoring complexity and binary operator issues Only in simple_menu.py * Added license text to the MIT licensed file * Added in versioning information * Fixed some imports and removed the last generic_select() from user_interaction. Also fixed a revert/merged fork of ask_for_main_filesystem_format() * Update color scheme to match Arch style better * Use cyan as default cursor color * Leave simple menu the same Co-authored-by: Daniel Girtler Co-authored-by: Anton Hvornum Co-authored-by: Dylan M. Taylor --- archinstall/lib/user_interaction.py | 564 ++++++++++++++---------------------- 1 file changed, 225 insertions(+), 339 deletions(-) (limited to 'archinstall/lib/user_interaction.py') diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index 7d90915f..2314b762 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -1,7 +1,6 @@ import getpass import ipaddress import logging -import pathlib import re import select # Used for char by char polling of sys.stdin import shutil @@ -9,17 +8,20 @@ import signal import sys import time -from .disk import BlockDevice, valid_fs_type, suggest_single_disk_layout, suggest_multi_disk_layout, valid_parted_position +from .disk import BlockDevice, suggest_single_disk_layout, suggest_multi_disk_layout, valid_parted_position, all_disks from .exceptions import RequirementError, UserError, DiskError from .hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics -from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, search_keyboard_layout +from .locale_helpers import list_keyboard_languages, list_timezones from .networking import list_interfaces +from .menu import Menu from .output import log from .profiles import Profile, list_profiles from .storage import storage +from .mirrors import list_mirrors # TODO: Some inconsistencies between the selection processes. # Some return the keys from the options, some the values? +from .. import fs_types def get_terminal_height(): @@ -117,81 +119,6 @@ def print_large_list(options, padding=5, margin_bottom=0, separator=': '): return column, row -def generic_multi_select(options, text="Select one or more of the options above (leave blank to continue): ", sort=True, default=None, allow_empty=False): - # Checking if the options are different from `list` or `dict` or if they are empty - if type(options) not in [list, dict, type({}.keys()), type({}.values())]: - log(f" * Generic multi-select doesn't support ({type(options)}) as type of options * ", fg='red') - log(" * If problem persists, please create an issue on https://github.com/archlinux/archinstall/issues * ", fg='yellow') - raise RequirementError("generic_multi_select() requires list or dictionary as options.") - if not options: - log(" * Generic multi-select didn't find any options to choose from * ", fg='red') - log(" * If problem persists, please create an issue on https://github.com/archlinux/archinstall/issues * ", fg='yellow') - raise RequirementError('generic_multi_select() requires at least one option to proceed.') - # After passing the checks, function continues to work - if type(options) == dict: - options = list(options.values()) - elif type(options) in (type({}.keys()), type({}.values())): - options = list(options) - if sort: - options = sorted(options) - - section = MiniCurses(get_terminal_width(), len(options)) - - selected_options = [] - - while True: - if not selected_options and default in options: - selected_options.append(default) - - printed_options = [] - for option in options: - if option in selected_options: - printed_options.append(f'>> {option}') - else: - printed_options.append(f'{option}') - - section.clear(0, get_terminal_height() - section._cursor_y - 1) - print_large_list(printed_options, margin_bottom=2) - section._cursor_y = len(printed_options) - section._cursor_x = 0 - section.write_line(text) - section.input_pos = section._cursor_x - selected_option = section.get_keyboard_input(end=None) - # This string check is necessary to correct work with it - # Without this, Python will raise AttributeError because of stripping `None` - # It also allows to remove empty spaces if the user accidentally entered them. - if isinstance(selected_option, str): - selected_option = selected_option.strip() - try: - if not selected_option: - if not selected_options and default: - selected_options = [default] - elif selected_options or allow_empty: - break - else: - raise RequirementError('Please select at least one option to continue') - elif selected_option.isnumeric(): - if (selected_option := int(selected_option)) >= len(options): - raise RequirementError(f'Selected option "{selected_option}" is out of range') - selected_option = options[selected_option] - if selected_option in selected_options: - selected_options.remove(selected_option) - else: - selected_options.append(selected_option) - elif selected_option in options: - if selected_option in selected_options: - selected_options.remove(selected_option) - else: - selected_options.append(selected_option) - else: - raise RequirementError(f'Selected option "{selected_option}" does not exist in available options') - except RequirementError as e: - log(f" * {e} * ", fg='red') - - sys.stdout.write('\n') - sys.stdout.flush() - return selected_options - def select_encrypted_partitions(block_devices :dict, password :str) -> dict: for device in block_devices: for partition in block_devices[device]['partitions']: @@ -208,6 +135,7 @@ def select_encrypted_partitions(block_devices :dict, password :str) -> dict: # TODO: Next version perhaps we can support mixed multiple encrypted partitions # Users might want to single out a partition for non-encryption to share between dualboot etc. + class MiniCurses: def __init__(self, width, height): self.width = width @@ -370,18 +298,17 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan def ask_for_a_timezone(): - while True: - timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip().strip('*.') - if timezone == '': - timezone = 'UTC' - if (pathlib.Path("/usr") / "share" / "zoneinfo" / timezone).exists(): - return timezone - else: - log( - f"Specified timezone {timezone} does not exist.", - level=logging.WARNING, - fg='red' - ) + timezones = list_timezones() + default = 'UTC' + + selected_tz = Menu( + f'Select a timezone or leave blank to use default "{default}"', + timezones, + skip=False, + default_option=default + ).run() + + return selected_tz def ask_for_bootloader(advanced_options=False) -> str: @@ -394,7 +321,7 @@ def ask_for_bootloader(advanced_options=False) -> str: else: # We use the common names for the bootloader as the selection, and map it back to the expected values. choices = ['systemd-boot', 'grub', 'efistub'] - selection = generic_select(choices, f'Choose a bootloader or leave blank to use systemd-boot: ', options_output=True) + selection = Menu('Choose a bootloader or leave blank to use systemd-boot', choices).run() if selection != "": if selection == 'systemd-boot': bootloader = 'systemd-bootctl' @@ -409,11 +336,8 @@ def ask_for_bootloader(advanced_options=False) -> str: def ask_for_audio_selection(desktop=True): audio = 'pipewire' if desktop else 'none' choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', 'none'] - selection = generic_select(choices, f'Choose an audio server or leave blank to use {audio}: ', options_output=True) - if selection != "": - audio = selection - - return audio + selected_audio = Menu(f'Choose an audio server or leave blank to use "{audio}"', choices, default_option=audio).run() + return selected_audio def ask_to_configure_network(): @@ -426,7 +350,8 @@ def ask_to_configure_network(): **list_interfaces() } - nic = generic_select(interfaces, "Select one network interface to configure (leave blank to skip): ") + nic = Menu('Select one network interface to configure', interfaces.values()).run() + if nic and nic != 'Copy ISO network configuration to installation': if nic == 'Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)': return {'nic': nic, 'NetworkManager': True} @@ -436,11 +361,15 @@ def ask_to_configure_network(): # printing out this part separate from options, passed in # `generic_select` modes = ['DHCP (auto detect)', 'IP (static)'] - for index, mode in enumerate(modes): - print(f"{index}: {mode}") + default_mode = 'DHCP (auto detect)' + + mode = Menu( + f'Select which mode to configure for "{nic}" or leave blank for default "{default_mode}"', + modes, + default_option=default_mode + ).run() - mode = generic_select(['DHCP', 'IP'], f"Select which mode to configure for {nic} or leave blank for DHCP: ", options_output=False) - if mode == 'IP': + if mode == 'IP (static)': while 1: ip = input(f"Enter the IP and subnet for {nic} (example: 192.168.0.5/24): ").strip() # Implemented new check for correct IP/subnet input @@ -483,15 +412,9 @@ def ask_to_configure_network(): return {} -def ask_for_disk_layout(): - options = { - 'keep-existing': 'Keep existing partition layout and select which ones to use where', - 'format-all': 'Format entire drive and setup a basic partition scheme', - 'abort': 'Abort the installation', - } - - value = generic_select(options, "Found partitions on the selected drive, (select by number) what you want to do: ", allow_empty_input=False, sort=True) - return next((key for key, val in options.items() if val == value), None) +def partition_overlap(partitions :list, start :str, end :str) -> bool: + # TODO: Implement sanity check + return False def ask_for_main_filesystem_format(advanced_options=False): @@ -509,77 +432,64 @@ def ask_for_main_filesystem_format(advanced_options=False): if advanced_options: options.update(advanced) - value = generic_select(options, "Select which filesystem your main partition should use (by number or name): ", allow_empty_input=False) - return next((key for key, val in options.items() if val == value), None) + return Menu('Select which filesystem your main partition should use', options, skip=False).run() -def generic_select(options, input_text="Select one of the above by index or absolute value: ", allow_empty_input=True, options_output=True, sort=False): - """ - A generic select function that does not output anything - other than the options and their indexes. As an example: +def current_partition_layout(partitions, with_idx=False): + def do_padding(name, max_len): + spaces = abs(len(str(name)) - max_len) + 2 + pad_left = int(spaces / 2) + pad_right = spaces - pad_left + return f'{pad_right * " "}{name}{pad_left * " "}|' - generic_select(["first", "second", "third option"]) - 0: first - 1: second - 2: third option + column_names = {} - When the user has entered the option correctly, - this function returns an item from list, a string, or None - """ + # this will add an initial index to the table for each partition + if with_idx: + column_names['index'] = max([len(str(len(partitions))), len('index')]) - # Checking if the options are different from `list` or `dict` or if they are empty - if type(options) not in [list, dict]: - log(f" * Generic select doesn't support ({type(options)}) as type of options * ", fg='red') - log(" * If problem persists, please create an issue on https://github.com/archlinux/archinstall/issues * ", fg='yellow') - raise RequirementError("generic_select() requires list or dictionary as options.") - if not options: - log(" * Generic select didn't find any options to choose from * ", fg='red') - log(" * If problem persists, please create an issue on https://github.com/archlinux/archinstall/issues * ", fg='yellow') - raise RequirementError('generic_select() requires at least one option to proceed.') - # After passing the checks, function continues to work - if type(options) == dict: - # To allow only `list` and `dict`, converting values of options here. - # Therefore, now we can only provide the dictionary itself - options = list(options.values()) - if sort: - # As we pass only list and dict (converted to list), we can skip converting to list - options = sorted(options) - - # Added ability to disable the output of options items, - # if another function displays something different from this - if options_output: - for index, option in enumerate(options): - print(f"{index}: {option}") + # determine all attribute names and the max length + # of the value among all partitions to know the width + # of the table cells + for p in partitions: + for attribute, value in p.items(): + if attribute in column_names.keys(): + column_names[attribute] = max([column_names[attribute], len(str(value)), len(attribute)]) + else: + column_names[attribute] = max([len(str(value)), len(attribute)]) - # The new changes introduce a single while loop for all inputs processed by this function - # Now the try...except block handles validation for invalid input from the user - while True: - try: - selected_option = input(input_text).strip() - if not selected_option: - # `allow_empty_input` parameter handles return of None on empty input, if necessary - # Otherwise raise `RequirementError` - if allow_empty_input: - return None - raise RequirementError('Please select an option to continue') - # Replaced `isdigit` with` isnumeric` to discard all negative numbers - elif selected_option.isnumeric(): - if (selected_option := int(selected_option)) >= len(options): - raise RequirementError(f'Selected option "{selected_option}" is out of range') - selected_option = options[selected_option] - break - elif selected_option in options: - break # We gave a correct absolute value + current_layout = '' + for name, max_len in column_names.items(): + current_layout += do_padding(name, max_len) + + current_layout = f'{current_layout[:-1]}\n{"-" * len(current_layout)}\n' + + for idx, p in enumerate(partitions): + row = '' + for name, max_len in column_names.items(): + if name == 'index': + row += do_padding(str(idx), max_len) + elif name in p: + row += do_padding(p[name], max_len) else: - raise RequirementError(f'Selected option "{selected_option}" does not exist in available options') - except RequirementError as err: - log(f" * {err} * ", fg='red') + row += ' ' * (max_len + 2) + '|' - return selected_option + current_layout += f'{row[:-1]}\n' -def partition_overlap(partitions :list, start :str, end :str) -> bool: - # TODO: Implement sanity check - return False + return f'\n\nCurrent partition layout:\n\n{current_layout}' + + +def select_partition(title, partitions, multiple=False): + partition_indexes = list(map(str, range(len(partitions)))) + partition = Menu(title, partition_indexes, multi=multiple).run() + + if partition is not None: + if isinstance(partition, list): + return [int(p) for p in partition] + else: + return int(partition) + + return None def get_default_partition_layout(block_devices, advanced_options=False): if len(block_devices) == 1: @@ -587,7 +497,6 @@ def get_default_partition_layout(block_devices, advanced_options=False): else: return suggest_multi_disk_layout(block_devices, advanced_options=advanced_options) - # TODO: Implement sane generic layout for 2+ drives def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: # if has_uefi(): @@ -624,7 +533,7 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: # return struct block_device_struct = { - "partitions" : [partition.__dump__() for partition in block_device.partitions.values()] + "partitions": [partition.__dump__() for partition in block_device.partitions.values()] } # Test code: [part.__dump__() for part in block_device.partitions.values()] # TODO: Squeeze in BTRFS subvolumes here @@ -632,25 +541,27 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: while True: modes = [ "Create a new partition", - f"Suggest partition layout for {block_device}", - "Delete a partition" if len(block_device_struct) else "", - "Clear/Delete all partitions" if len(block_device_struct) else "", - "Assign mount-point for a partition" if len(block_device_struct) else "", - "Mark/Unmark a partition to be formatted (wipes data)" if len(block_device_struct) else "", - "Mark/Unmark a partition as encrypted" if len(block_device_struct) else "", - "Mark/Unmark a partition as bootable (automatic for /boot)" if len(block_device_struct) else "", - "Set desired filesystem for a partition" if len(block_device_struct) else "", + f"Suggest partition layout for {block_device}" ] - # Print current partition layout: + if len(block_device_struct['partitions']): + modes += [ + "Delete a partition", + "Clear/Delete all partitions", + "Assign mount-point for a partition", + "Mark/Unmark a partition to be formatted (wipes data)", + "Mark/Unmark a partition as encrypted", + "Mark/Unmark a partition as bootable (automatic for /boot)", + "Set desired filesystem for a partition", + ] + + title = f'Select what to do with \n{block_device}' + + # show current partition layout: if len(block_device_struct["partitions"]): - print('Current partition layout:') - for partition in block_device_struct["partitions"]: - print(partition) - print() + title += current_partition_layout(block_device_struct['partitions']) + '\n' - task = generic_select(modes, - input_text=f"Select what to do with {block_device} (leave blank when done): ") + task = Menu(title, modes, sort=False).run() if not task: break @@ -661,7 +572,7 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: # # https://www.gnu.org/software/parted/manual/html_node/mklabel.html # name = input("Enter a desired name for the partition: ").strip() - fstype = input("Enter a desired filesystem type for the partition: ").strip() + fstype = Menu('Enter a desired filesystem type for the partition', fs_types(), skip=False).run() start = input(f"Enter the start sector (percentage or block number, default: {block_device.first_free_sector}): ").strip() if not start.strip(): @@ -669,17 +580,19 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: end_suggested = block_device.first_end_sector else: end_suggested = '100%' + end = input(f"Enter the end sector of the partition (percentage or block number, ex: {end_suggested}): ").strip() + if not end.strip(): end = end_suggested - if valid_parted_position(start) and valid_parted_position(end) and valid_fs_type(fstype): + if valid_parted_position(start) and valid_parted_position(end): if partition_overlap(block_device_struct["partitions"], start, end): log(f"This partition overlaps with other partitions on the drive! Ignoring this partition creation.", fg="red") continue block_device_struct["partitions"].append({ - "type" : "primary", # Strictly only allowed under MSDOS, but GPT accepts it so it's "safe" to inject + "type" : "primary", # Strictly only allowed under MSDOS, but GPT accepts it so it's "safe" to inject "start" : start, "size" : end, "mountpoint" : None, @@ -689,7 +602,7 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: } }) else: - log(f"Invalid start ({valid_parted_position(start)}), end ({valid_parted_position(end)}) or fstype ({valid_fs_type(fstype)}) for this partition. Ignoring this partition creation.", fg="red") + log(f"Invalid start ({valid_parted_position(start)}) or end ({valid_parted_position(end)}) for this partition. Ignoring this partition creation.", fg="red") continue elif task[:len("Suggest partition layout")] == "Suggest partition layout": if len(block_device_struct["partitions"]): @@ -700,77 +613,83 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: elif task is None: return block_device_struct else: - for index, partition in enumerate(block_device_struct["partitions"]): - print(f"{index}: Start: {partition['start']}, End: {partition['size']} ({partition['filesystem']['format']}{', mounting at: '+partition['mountpoint'] if partition['mountpoint'] else ''})") + current_layout = current_partition_layout(block_device_struct['partitions'], with_idx=True) if task == "Delete a partition": - if (partition := generic_select(block_device_struct["partitions"], 'Select which partition to delete: ', options_output=False)): - del(block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]) + title = f'{current_layout}\n\nSelect by index which partitions to delete' + to_delete = select_partition(title, block_device_struct["partitions"], multiple=True) + + if to_delete: + block_device_struct['partitions'] = [p for idx, p in enumerate(block_device_struct['partitions']) if idx not in to_delete] elif task == "Clear/Delete all partitions": block_device_struct["partitions"] = [] elif task == "Assign mount-point for a partition": - if (partition := generic_select(block_device_struct["partitions"], 'Select which partition to mount where: ', options_output=False)): + title = f'{current_layout}\n\nSelect by index which partition to mount where' + partition = select_partition(title, block_device_struct["partitions"]) + + if partition is not None: print(' * Partition mount-points are relative to inside the installation, the boot would be /boot as an example.') mountpoint = input('Select where to mount partition (leave blank to remove mountpoint): ').strip() if len(mountpoint): - block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['mountpoint'] = mountpoint + block_device_struct["partitions"][partition]['mountpoint'] = mountpoint if mountpoint == '/boot': log(f"Marked partition as bootable because mountpoint was set to /boot.", fg="yellow") block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['boot'] = True else: - del(block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['mountpoint']) + del(block_device_struct["partitions"][partition]['mountpoint']) elif task == "Mark/Unmark a partition to be formatted (wipes data)": - if (partition := generic_select(block_device_struct["partitions"], 'Select which partition to mask for formatting: ', options_output=False)): + title = f'{current_layout}\n\nSelect which partition to mask for formatting' + partition = select_partition(title, block_device_struct["partitions"]) + + if partition is not None: # If we mark a partition for formatting, but the format is CRYPTO LUKS, there's no point in formatting it really # without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set, # it's safe to change the filesystem for this partition. - if block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('filesystem', {}).get('format', 'crypto_LUKS') == 'crypto_LUKS': - if not block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('filesystem', None): - block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['filesystem'] = {} + if block_device_struct["partitions"][partition].get('filesystem', {}).get('format', 'crypto_LUKS') == 'crypto_LUKS': + if not block_device_struct["partitions"][partition].get('filesystem', None): + block_device_struct["partitions"][partition]['filesystem'] = {} - while True: - fstype = input("Enter a desired filesystem type for the partition: ").strip() - if not valid_fs_type(fstype): - log(f"Desired filesystem {fstype} is not a valid filesystem.", level=logging.ERROR, fg="red") - continue - break + fstype = Menu('Enter a desired filesystem type for the partition', fs_types(), skip=False).run() - block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['filesystem']['format'] = fstype + block_device_struct["partitions"][partition]['filesystem']['format'] = fstype # Negate the current wipe marking - block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['format'] = not block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('format', False) + block_device_struct["partitions"][partition]['format'] = not block_device_struct["partitions"][partition].get('format', False) elif task == "Mark/Unmark a partition as encrypted": - if (partition := generic_select(block_device_struct["partitions"], 'Select which partition to mark as encrypted: ', options_output=False)): + title = f'{current_layout}\n\nSelect which partition to mark as encrypted' + partition = select_partition(title, block_device_struct["partitions"]) + + if partition is not None: # Negate the current encryption marking - block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['encrypted'] = not block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('encrypted', False) + block_device_struct["partitions"][partition]['encrypted'] = not block_device_struct["partitions"][partition].get('encrypted', False) elif task == "Mark/Unmark a partition as bootable (automatic for /boot)": - if (partition := generic_select(block_device_struct["partitions"], 'Select which partition to mark as bootable: ', options_output=False)): - block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['boot'] = not block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('boot', False) + title = f'{current_layout}\n\nSelect which partition to mark as bootable' + partition = select_partition(title, block_device_struct["partitions"]) + + if partition is not None: + block_device_struct["partitions"][partition]['boot'] = not block_device_struct["partitions"][partition].get('boot', False) elif task == "Set desired filesystem for a partition": - if not block_device_struct["partitions"]: - log("No partitions found. Create some partitions first", level=logging.WARNING, fg='yellow') - continue - elif (partition := generic_select(block_device_struct["partitions"], 'Select which partition to set a filesystem on: ', options_output=False)): - if not block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('filesystem', None): - block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['filesystem'] = {} + title = f'{current_layout}\n\nSelect which partition to set a filesystem on' + partition = select_partition(title, block_device_struct["partitions"]) - while True: - fstype = input("Enter a desired filesystem type for the partition: ").strip() - if not valid_fs_type(fstype): - log(f"Desired filesystem {fstype} is not a valid filesystem.", level=logging.ERROR, fg="red") - continue - break + if partition is not None: + if not block_device_struct["partitions"][partition].get('filesystem', None): + block_device_struct["partitions"][partition]['filesystem'] = {} - block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['filesystem']['format'] = fstype + fstype_title = 'Enter a desired filesystem type for the partition: ' + fstype = Menu(fstype_title, fs_types(), skip=False).run() + + block_device_struct["partitions"][partition]['filesystem']['format'] = fstype return block_device_struct -def select_individual_blockdevice_usage(block_devices :list): + +def select_individual_blockdevice_usage(block_devices: list): result = {} for device in block_devices: @@ -787,7 +706,7 @@ def select_disk_layout(block_devices :list, advanced_options=False): "Select what to do with each individual drive (followed by partition usage)" ] - mode = generic_select(modes, input_text=f"Select what you wish to do with the selected block devices: ") + mode = Menu('Select what you wish to do with the selected block devices', modes, skip=False).run() if mode == 'Wipe all selected drives and use a best-effort default partition layout': return get_default_partition_layout(block_devices, advanced_options) @@ -812,7 +731,8 @@ def select_disk(dict_o_disks): print(f"{index}: {drive} ({dict_o_disks[drive]['size'], dict_o_disks[drive].device, dict_o_disks[drive]['label']})") log("You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)", fg="yellow") - drive = generic_select(drives, 'Select one of the above disks (by name or number) or leave blank to use /mnt: ', options_output=False) + + drive = Menu('Select one of the disks or skip and use "/mnt" as default"', drives).run() if not drive: return drive @@ -824,128 +744,90 @@ def select_disk(dict_o_disks): def select_profile(): """ - Asks the user to select a profile from the available profiles. + # Asks the user to select a profile from the available profiles. + # + # :return: The name/dictionary key of the selected profile + # :rtype: str + # """ + top_level_profiles = sorted(list(list_profiles(filter_top_level_profiles=True))) + options = {} - :return: The name/dictionary key of the selected profile - :rtype: str - """ - shown_profiles = sorted(list(list_profiles(filter_top_level_profiles=True))) - actual_profiles_raw = shown_profiles + sorted([profile for profile in list_profiles() if profile not in shown_profiles]) + for profile in top_level_profiles: + profile = Profile(None, profile) + description = profile.get_profile_description() - if len(shown_profiles) >= 1: - for index, profile in enumerate(shown_profiles): - description = Profile(None, profile).get_profile_description() - print(f"{index}: {profile}: {description}") + option = f'{profile.profile}: {description}' + options[option] = profile - print(' -- The above list is a set of pre-programmed profiles. --') - print(' -- They might make it easier to install things like desktop environments. --') - print(' -- (Leave blank and hit enter to skip this step and continue) --') + title = 'This is a list of pre-programmed profiles, ' \ + 'they might make it easier to install things like desktop environments' - selected_profile = generic_select(actual_profiles_raw, 'Enter a pre-programmed profile name if you want to install one: ', options_output=False) - if selected_profile: - return Profile(None, selected_profile) - else: - raise RequirementError("Selecting profiles require a least one profile to be given as an option.") + selection = Menu(title=title, options=options.keys()).run() + if selection is not None: + return options[selection] -def select_language(options, show_only_country_codes=True, input_text='Select one of the above keyboard languages (by number or full name): '): - """ - Asks the user to select a language from the `options` dictionary parameter. - Usually this is combined with :ref:`archinstall.list_keyboard_languages`. + return None - :param options: A `generator` or `list` where keys are the language name, value should be a dict containing language information. - :type options: generator or list - :param show_only_country_codes: Filters out languages that are not len(lang) == 2. This to limit the number of results from stuff like dvorak and x-latin1 alternatives. - :type show_only_country_codes: bool +def select_language(): + """ + Asks the user to select a language + Usually this is combined with :ref:`archinstall.list_keyboard_languages`. :return: The language/dictionary key of the selected language :rtype: str """ - default_keyboard_language = 'us' - - if show_only_country_codes: - languages = sorted([language for language in list(options) if len(language) == 2]) - else: - languages = sorted(list(options)) + kb_lang = list_keyboard_languages() + # sort alphabetically and then by length + # it's fine if the list is big because the Menu + # allows for searching anyways + sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) - if len(languages) >= 1: - print_large_list(languages, margin_bottom=4) + selected_lang = Menu('Select Keyboard layout', sorted_kb_lang, default_option='us', sort=False).run() + return selected_lang - print(" -- You can choose a layout that isn't in this list, but whose name you know --") - print(f" -- Also, you can enter '?' or 'help' to search for more languages, or skip to use {default_keyboard_language} layout --") - while True: - selected_language = input(input_text) - if not selected_language: - return default_keyboard_language - elif selected_language.lower() in ('?', 'help'): - while True: - filter_string = input("Search for layout containing (example: \"sv-\") or enter 'exit' to exit from search: ") - - if filter_string.lower() == 'exit': - return select_language(list_keyboard_languages()) - - new_options = list(search_keyboard_layout(filter_string)) - - if len(new_options) <= 0: - log(f"Search string '{filter_string}' yielded no results, please try another search.", fg='yellow') - continue - - return select_language(new_options, show_only_country_codes=False) - elif selected_language.isnumeric(): - selected_language = int(selected_language) - if selected_language >= len(languages): - log(' * Selected option is out of range * ', fg='red') - continue - return languages[selected_language] - elif verify_keyboard_layout(selected_language): - return selected_language - else: - log(" * Given language wasn't found * ", fg='red') - - raise RequirementError("Selecting languages require a least one language to be given as an option.") - - -def select_mirror_regions(mirrors, show_top_mirrors=True): +def select_mirror_regions(): """ - Asks the user to select a mirror or region from the `mirrors` dictionary parameter. + Asks the user to select a mirror or region Usually this is combined with :ref:`archinstall.list_mirrors`. - :param mirrors: A `dict` where keys are the mirror region name, value should be a dict containing mirror information. - :type mirrors: dict - - :param show_top_mirrors: Will limit the list to the top 10 fastest mirrors based on rank-mirror *(Currently not implemented but will be)*. - :type show_top_mirrors: bool - :return: The dictionary information about a mirror/region. :rtype: dict """ # TODO: Support multiple options and country codes, SE,UK for instance. - regions = sorted(list(mirrors.keys())) - selected_mirrors = {} - if len(regions) >= 1: - print_large_list(regions, margin_bottom=4) + mirrors = list_mirrors() + selected_mirror = Menu('Select one of the regions to download packages from', mirrors.keys()).run() - print(' -- You can skip this step by leaving the option blank --') - selected_mirror = generic_select(regions, 'Select one of the above regions to download packages from (by number or full name): ', options_output=False) - if not selected_mirror: - # Returning back empty options which can be both used to - # do "if x:" logic as well as do `x.get('mirror', {}).get('sub', None)` chaining - return {} + if selected_mirror is not None: + return {selected_mirror: mirrors[selected_mirror]} - # I'm leaving "mirrors" on purpose here. - # Since region possibly contains a known region of - # all possible regions, and we might want to write - # for instance Sweden (if we know that exists) without having to - # go through the search step. + return {} + + +def select_harddrives(): + """ + Asks the user to select one or multiple hard drives - selected_mirrors[selected_mirror] = mirrors[selected_mirror] - return selected_mirrors + :return: List of selected hard drives + :rtype: list + """ + hard_drives = all_disks().values() + options = {f'{option}': option for option in hard_drives} - raise RequirementError("Selecting mirror region require a least one region to be given as an option.") + selected_harddrive = Menu( + 'Select one or more hard drives to use and configure', + options.keys(), + multi=True + ).run() + + if selected_harddrive and len(selected_harddrive) > 0: + return [options[i] for i in selected_harddrive] + + return None def select_driver(options=AVAILABLE_GFX_DRIVERS): @@ -961,15 +843,18 @@ def select_driver(options=AVAILABLE_GFX_DRIVERS): if drivers: arguments = storage.get('arguments', {}) + title = '' + if has_amd_graphics(): - print('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.') + title += 'For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.\n' if has_intel_graphics(): - print('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.') + title += 'For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n' if has_nvidia_graphics(): - print('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.') + title += 'For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n' if not arguments.get('gfx_driver', None): - arguments['gfx_driver'] = generic_select(drivers, input_text="Select a graphics driver or leave blank to install all open-source drivers: ") + title += '\n\nSelect a graphics driver or leave blank to install all open-source drivers' + arguments['gfx_driver'] = Menu(title, drivers).run() if arguments.get('gfx_driver', None) is None: arguments['gfx_driver'] = "All open-source (default)" @@ -979,22 +864,23 @@ def select_driver(options=AVAILABLE_GFX_DRIVERS): raise RequirementError("Selecting drivers require a least one profile to be given as an option.") -def select_kernel(options): +def select_kernel(): """ Asks the user to select a kernel for system. - :param options: A `list` with kernel options - :type options: list - :return: The string as a selected kernel :rtype: string """ + kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] default_kernel = "linux" - kernels = sorted(list(options)) - - if kernels: - return generic_multi_select(kernels, f"Choose which kernels to use (leave blank for default: {default_kernel}): ", default=default_kernel, sort=False) + selected_kernels = Menu( + f'Choose which kernels to use or leave blank for default "{default_kernel}"', + kernels, + sort=True, + multi=True, + default_option=default_kernel + ).run() - raise RequirementError("Selecting kernels require a least one kernel to be given as an option.") + return selected_kernels -- cgit v1.2.3-54-g00ecf