Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib/user_interaction.py
diff options
context:
space:
mode:
authorDaniel <blackrabbit256@gmail.com>2021-12-03 07:17:51 +1100
committerGitHub <noreply@github.com>2021-12-02 21:17:51 +0100
commit908c7b8cc0a804e9522d93fcf0dc71034c53ccdb (patch)
tree6ca53ef0354481512920997a5e48e51b36d591f6 /archinstall/lib/user_interaction.py
parent22ee2d90a1d2fb6af2ab5e80235e31f9da7a0550 (diff)
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 <girtler.daniel@gmail.com> Co-authored-by: Anton Hvornum <anton.feeds+github@gmail.com> Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Diffstat (limited to 'archinstall/lib/user_interaction.py')
-rw-r--r--archinstall/lib/user_interaction.py564
1 files changed, 225 insertions, 339 deletions
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