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:
Diffstat (limited to 'archinstall/lib/user_interaction.py')
-rw-r--r--archinstall/lib/user_interaction.py539
1 files changed, 412 insertions, 127 deletions
diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py
index df8668af..be01594e 100644
--- a/archinstall/lib/user_interaction.py
+++ b/archinstall/lib/user_interaction.py
@@ -1,11 +1,14 @@
-import getpass, pathlib, os, shutil, re
-import sys, time, signal
+import getpass, pathlib, os, shutil, re, time
+import sys, time, signal, ipaddress, logging
+import termios, tty, select # Used for char by char polling of sys.stdin
from .exceptions import *
from .profiles import Profile
-from .locale_helpers import search_keyboard_layout
-from .output import log, LOG_LEVELS
+from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, search_keyboard_layout
+from .output import log
from .storage import storage
from .networking import list_interfaces
+from .general import sys_command
+from .hardware import AVAILABLE_GFX_DRIVERS, hasUEFI
## TODO: Some inconsistencies between the selection processes.
## Some return the keys from the options, some the values?
@@ -24,7 +27,7 @@ def check_for_correct_username(username):
return True
log(
"The username you entered is invalid. Try again",
- level=LOG_LEVELS.Warning,
+ level=logging.WARNING,
fg='red'
)
return False
@@ -93,14 +96,181 @@ def print_large_list(options, padding=5, margin_bottom=0, separator=': '):
print(f"{str(column): >{highest_index_number_length}}{separator}{options[column]}", end = spaces)
print()
-def ask_for_superuser_account(prompt='Create a required super-user with sudo privileges: ', forced=False):
+ 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):
+ if sort:
+ options = sorted(options)
+
+ section = MiniCurses(get_terminal_width(), len(options))
+
+ selected_options = []
+
+ while True:
+ if len(selected_options) <= 0 and default 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)
+ x, y = 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)
+
+ if selected_option is None:
+ if len(selected_options) <= 0 and default:
+ selected_options = [default]
+
+ if len(selected_options) or allow_empty is True:
+ break
+ else:
+ log('* Need to select at least one option!', fg='red')
+ continue
+
+ elif selected_option.isdigit():
+ if (selected_option := int(selected_option)) >= len(options):
+ log('* Option is out of range, please select another one!', fg='red')
+ continue
+ selected_option = options[selected_option]
+ if selected_option in selected_options:
+ selected_options.remove(selected_option)
+ else:
+ selected_options.append(selected_option)
+
+ return selected_options
+
+
+class MiniCurses():
+ def __init__(self, width, height):
+ self.width = width
+ self.height = height
+
+ self._cursor_y = 0
+ self._cursor_x = 0
+
+ self.input_pos = 0
+
+ def write_line(self, text, clear_line=True):
+ if clear_line:
+ sys.stdout.flush()
+ sys.stdout.write("\033[%dG" % 0)
+ sys.stdout.flush()
+ sys.stdout.write(" " * (get_terminal_width()-1))
+ sys.stdout.flush()
+ sys.stdout.write("\033[%dG" % 0)
+ sys.stdout.flush()
+ sys.stdout.write(text)
+ sys.stdout.flush()
+ self._cursor_x += len(text)
+
+ def clear(self, x, y):
+ if x < 0: x = 0
+ if y < 0: y = 0
+
+ #import time
+ #sys.stdout.write(f"Clearing from: {x, y}")
+ #sys.stdout.flush()
+ #time.sleep(2)
+
+ sys.stdout.flush()
+ sys.stdout.write('\033[%d;%df' % (y, x))
+ for line in range(get_terminal_height()-y-1, y):
+ sys.stdout.write(" " * (get_terminal_width()-1))
+ sys.stdout.flush()
+ sys.stdout.write('\033[%d;%df' % (y, x))
+ sys.stdout.flush()
+
+ def deal_with_control_characters(self, char):
+ mapper = {
+ '\x7f' : 'BACKSPACE',
+ '\r' : 'CR',
+ '\n' : 'NL'
+ }
+
+ if (mapped_char := mapper.get(char, None)) == 'BACKSPACE':
+ if self._cursor_x <= self.input_pos:
+ # Don't backspace futher back than the cursor start position during input
+ return True
+ # Move back to the current known position (BACKSPACE doesn't updated x-pos)
+ sys.stdout.flush()
+ sys.stdout.write("\033[%dG" % (self._cursor_x))
+ sys.stdout.flush()
+
+ # Write a blank space
+ sys.stdout.flush()
+ sys.stdout.write(" ")
+ sys.stdout.flush()
+
+ # And move back again
+ sys.stdout.flush()
+ sys.stdout.write("\033[%dG" % (self._cursor_x))
+ sys.stdout.flush()
+
+ self._cursor_x -= 1
+
+ return True
+ elif mapped_char in ('CR', 'NL'):
+ return True
+
+ return None
+
+ def get_keyboard_input(self, strip_rowbreaks=True, end='\n'):
+ assert end in ['\r', '\n', None]
+
+ poller = select.epoll()
+ response = ''
+
+ sys_fileno = sys.stdin.fileno()
+ old_settings = termios.tcgetattr(sys_fileno)
+ tty.setraw(sys_fileno)
+
+ poller.register(sys.stdin.fileno(), select.EPOLLIN)
+
+ EOF = False
+ while EOF is False:
+ for fileno, event in poller.poll(0.025):
+ char = sys.stdin.read(1)
+
+ #sys.stdout.write(f"{[char]}")
+ #sys.stdout.flush()
+
+ if (newline := (char in ('\n', '\r'))):
+ EOF = True
+
+ if not newline or strip_rowbreaks is False:
+ response += char
+
+ if self.deal_with_control_characters(char) is not True:
+ self.write_line(response[-1], clear_line=False)
+
+ termios.tcsetattr(sys_fileno, termios.TCSADRAIN, old_settings)
+
+ if end:
+ sys.stdout.write(end)
+ sys.stdout.flush()
+ self._cursor_x = 0
+ self._cursor_y += 1
+
+ if response:
+ return response
+
+def ask_for_superuser_account(prompt='Username for required superuser with sudo privileges: ', forced=False):
while 1:
new_user = input(prompt).strip(' ')
if not new_user and forced:
# TODO: make this text more generic?
# It's only used to create the first sudo user when root is disabled in guided.py
- log(' * Since root is disabled, you need to create a least one (super) user!', fg='red')
+ log(' * Since root is disabled, you need to create a least one superuser!', fg='red')
continue
elif not new_user and not forced:
raise UserError("No superuser was created.")
@@ -112,7 +282,7 @@ def ask_for_superuser_account(prompt='Create a required super-user with sudo pri
def ask_for_additional_users(prompt='Any additional users to install (leave blank for no users): '):
users = {}
- super_users = {}
+ superusers = {}
while 1:
new_user = input(prompt).strip(' ')
@@ -122,26 +292,37 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan
continue
password = get_password(prompt=f'Password for user {new_user}: ')
- if input("Should this user be a sudo (super) user (y/N): ").strip(' ').lower() in ('y', 'yes'):
- super_users[new_user] = {"!password" : password}
+ if input("Should this user be a superuser (sudoer) [y/N]: ").strip(' ').lower() in ('y', 'yes'):
+ superusers[new_user] = {"!password" : password}
else:
users[new_user] = {"!password" : password}
- return users, super_users
+ return users, superusers
def ask_for_a_timezone():
- timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip()
- if timezone == '':
- timezone = 'UTC'
- if (pathlib.Path("/usr")/"share"/"zoneinfo"/timezone).exists():
- return 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'
+ )
+
+def ask_for_bootloader() -> str:
+ bootloader = "systemd-bootctl"
+ if hasUEFI()==False:
+ bootloader="grub-install"
else:
- log(
- f"Time zone {timezone} does not exist, continuing with system default.",
- level=LOG_LEVELS.Warning,
- fg='red'
- )
-
+ bootloader_choice = input("Would you like to use GRUB as a bootloader instead of systemd-boot? [y/N] ").lower()
+ if bootloader_choice == "y":
+ bootloader="grub-install"
+ return bootloader
+
def ask_for_audio_selection():
audio = "pulseaudio" # Default for most desktop environments
pipewire_choice = input("Would you like to install pipewire instead of pulseaudio as the default audio server? [Y/n] ").lower()
@@ -154,27 +335,56 @@ def ask_to_configure_network():
# Optionally configure one network interface.
#while 1:
# {MAC: Ifname}
- interfaces = {'ISO-CONFIG' : 'Copy ISO network configuration to installation','NetworkManager':'Use NetworkManager to control and manage your internet connection', **list_interfaces()}
+ interfaces = {
+ 'ISO-CONFIG' : 'Copy ISO network configuration to installation',
+ 'NetworkManager':'Use NetworkManager to control and manage your internet connection',
+ **list_interfaces()
+ }
- nic = generic_select(interfaces.values(), "Select one network interface to configure (leave blank to skip): ")
+ nic = generic_select(interfaces, "Select one network interface to configure (leave blank to skip): ")
if nic and nic != 'Copy ISO network configuration to installation':
if nic == 'Use NetworkManager to control and manage your internet connection':
return {'nic': nic,'NetworkManager':True}
- mode = generic_select(['DHCP (auto detect)', 'IP (static)'], f"Select which mode to configure for {nic}: ")
- if mode == 'IP (static)':
+
+ # Current workaround:
+ # For selecting modes without entering text within brackets,
+ # 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}")
+
+ mode = generic_select(['DHCP', 'IP'], f"Select which mode to configure for {nic} or leave blank for DHCP: ",
+ options_output=False)
+ if mode == 'IP':
while 1:
ip = input(f"Enter the IP and subnet for {nic} (example: 192.168.0.5/24): ").strip()
- if ip:
+ # Implemented new check for correct IP/subnet input
+ try:
+ ipaddress.ip_interface(ip)
break
- else:
+ except ValueError:
log(
"You need to enter a valid IP in IP-config mode.",
- level=LOG_LEVELS.Warning,
+ level=logging.WARNING,
fg='red'
)
- if not len(gateway := input('Enter your gateway (router) IP address or leave blank for none: ').strip()):
- gateway = None
+ # Implemented new check for correct gateway IP address
+ while 1:
+ gateway = input('Enter your gateway (router) IP address or leave blank for none: ').strip()
+ try:
+ if len(gateway) == 0:
+ gateway = None
+ else:
+ ipaddress.ip_address(gateway)
+ break
+ except ValueError:
+ log(
+ "You need to enter a valid gateway (router) IP address.",
+ level=logging.WARNING,
+ fg='red'
+ )
dns = None
if len(dns_input := input('Enter your DNS servers (space separated, blank for none): ').strip()):
@@ -190,12 +400,13 @@ def ask_to_configure_network():
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.'
+ '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.values(), "Found partitions on the selected drive, (select by number) what you want to do: ")
+ 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 ask_for_main_filesystem_format():
@@ -206,40 +417,71 @@ def ask_for_main_filesystem_format():
'f2fs' : 'f2fs'
}
- value = generic_select(options.values(), "Select which filesystem your main partition should use (by number or name): ")
+ 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)
-def generic_select(options, input_text="Select one of the above by index or absolute value: ", sort=True):
+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:
generic_select(["first", "second", "third option"])
- 1: first
- 2: second
- 3: third option
+ 0: first
+ 1: second
+ 2: third option
+
+ When the user has entered the option correctly,
+ this function returns an item from list, a string, or None
"""
- if type(options) == dict: options = list(options)
- if sort: options = sorted(list(options))
- if len(options) <= 0: raise RequirementError('generic_select() requires at least one option to operate.')
+ # Checking if options are different from `list` or `dict`
+ 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.")
+ # To allow only `list` and `dict`, converting values of options here.
+ # Therefore, now we can only provide the dictionary itself
+ if type(options) == dict: options = list(options.values())
+ if sort: options = sorted(options) # As we pass only list and dict (converted to list), we can skip converting to list
+ if len(options) == 0:
+ log(f" * 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.')
+
- for index, option in enumerate(options):
- print(f"{index}: {option}")
+ # 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}")
+
+ # 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)
+ if len(selected_option.strip()) == 0:
+ # `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():
+ selected_option = int(selected_option)
+ if 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
+ else:
+ raise RequirementError(f'Selected option "{selected_option}" does not exist in available options')
+ except RequirementError as err:
+ log(f" * {err} * ", fg='red')
+ continue
- selected_option = input(input_text)
- if len(selected_option.strip()) <= 0:
- return None
- elif selected_option.isdigit():
- selected_option = int(selected_option)
- if selected_option > len(options):
- raise RequirementError(f'Selected option "{selected_option}" is out of range')
- selected_option = options[selected_option]
- elif selected_option in options:
- pass # We gave a correct absolute value
- else:
- raise RequirementError(f'Selected option "{selected_option}" does not exist in available options: {options}')
-
return selected_option
def select_disk(dict_o_disks):
@@ -257,18 +499,14 @@ def select_disk(dict_o_disks):
if len(drives) >= 1:
for index, drive in enumerate(drives):
print(f"{index}: {drive} ({dict_o_disks[drive]['size'], dict_o_disks[drive].device, dict_o_disks[drive]['label']})")
- drive = input('Select one of the above disks (by number or full path) or write /mnt to skip partitioning: ')
- if drive.strip() == '/mnt':
- return None
- elif drive.isdigit():
- drive = int(drive)
- if drive >= len(drives):
- raise DiskError(f'Selected option "{drive}" is out of range')
- drive = dict_o_disks[drives[drive]]
- elif drive in dict_o_disks:
- drive = dict_o_disks[drive]
- else:
- raise DiskError(f'Selected drive does not exist: "{drive}"')
+
+ log(f"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)
+ if not drive:
+ return drive
+
+ drive = dict_o_disks[drive]
return drive
raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.')
@@ -293,29 +531,21 @@ def select_profile(options):
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) --')
- selected_profile = input('Enter a pre-programmed profile name if you want to install one: ')
-
- if len(selected_profile.strip()) <= 0:
- return None
-
- if selected_profile.isdigit() and (pos := int(selected_profile)) <= len(profiles)-1:
- selected_profile = profiles[pos]
- elif selected_profile in options:
- selected_profile = options[options.index(selected_profile)]
- else:
- RequirementError("Selected profile does not exist.")
- return Profile(None, selected_profile)
-
- raise RequirementError("Selecting profiles require a least one profile to be given as an option.")
+ selected_profile = generic_select(profiles, '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.")
def select_language(options, show_only_country_codes=True):
"""
Asks the user to select a language from the `options` dictionary parameter.
Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
- :param options: A `dict` where keys are the language name, value should be a dict containing language information.
- :type options: dict
+ :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
@@ -334,35 +564,37 @@ def select_language(options, show_only_country_codes=True):
for index, language in enumerate(languages):
print(f"{index}: {language}")
- print(' -- You can enter ? or help to search for more languages, or skip to use US layout --')
- selected_language = input('Select one of the above keyboard languages (by number or full name): ')
-
- if len(selected_language.strip()) == 0:
- return DEFAULT_KEYBOARD_LANGUAGE
- elif selected_language.lower() in ('?', 'help'):
- while True:
- filter_string = input('Search for layout containing (example: "sv-"): ')
- 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 or Ctrl+D to abort.", fg='yellow')
- continue
+ print(" -- You can choose a layout that isn't in this list, but whose name you know --")
+ print(" -- Also, you can enter '?' or 'help' to search for more languages, or skip to use US layout --")
- return select_language(new_options, show_only_country_codes=False)
+ while True:
+ selected_language = input('Select one of the above keyboard languages (by name or full name): ')
+ 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: ")
- elif selected_language.isdigit() and (pos := int(selected_language)) <= len(languages)-1:
- selected_language = languages[pos]
- return selected_language
- # I'm leaving "options" on purpose here.
- # Since languages possibly contains a filtered version of
- # all possible language layouts, and we might want to write
- # for instance sv-latin1 (if we know that exists) without having to
- # go through the search step.
- elif selected_language in options:
- selected_language = options[options.index(selected_language)]
- return selected_language
- else:
- raise RequirementError("Selected language does not exist.")
+ 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.")
@@ -389,23 +621,76 @@ def select_mirror_regions(mirrors, show_top_mirrors=True):
print_large_list(regions, margin_bottom=4)
print(' -- You can skip this step by leaving the option blank --')
- selected_mirror = input('Select one of the above regions to download packages from (by number or full name): ')
- if len(selected_mirror.strip()) == 0:
+ 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 {}
- elif selected_mirror.isdigit() and int(selected_mirror) <= len(regions)-1:
- # 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.
- region = regions[int(selected_mirror)]
- selected_mirrors[region] = mirrors[region]
- elif selected_mirror in mirrors:
- selected_mirrors[selected_mirror] = mirrors[selected_mirror]
- else:
- raise RequirementError("Selected region does not exist.")
+ # 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.
+ selected_mirrors[selected_mirror] = mirrors[selected_mirror]
return selected_mirrors
+
+ raise RequirementError("Selecting mirror region require a least one region to be given as an option.")
+
+def select_driver(options=AVAILABLE_GFX_DRIVERS):
+ """
+ Some what convoluted function, which's job is simple.
+ Select a graphics driver from a pre-defined set of popular options.
+
+ (The template xorg is for beginner users, not advanced, and should
+ there for appeal to the general public first and edge cases later)
+ """
+
+ drivers = sorted(list(options))
+
+ if drivers:
+ lspci = sys_command(f'/usr/bin/lspci')
+ for line in lspci.trace_log.split(b'\r\n'):
+ if b' vga ' in line.lower():
+ if b'nvidia' in line.lower():
+ print(' ** nvidia card detected, suggested driver: nvidia **')
+ elif b'amd' in line.lower():
+ print(' ** AMD card detected, suggested driver: AMD / ATI **')
+
+ initial_option = generic_select(drivers, input_text="Select your graphics card driver: ")
+ selected_driver = options[initial_option]
+
+ if type(selected_driver) == dict:
+ driver_options = sorted(list(selected_driver))
+
+ driver_package_group = generic_select(driver_options, f'Which driver-type do you want for {initial_option}: ',
+ allow_empty_input=False)
+ driver_package_group = selected_driver[driver_package_group]
+
+ return driver_package_group
+
+ return selected_driver
+
+ raise RequirementError("Selecting drivers require a least one profile to be given as an option.")
+
+def select_kernel(options):
+ """
+ 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
+ """
+
+ DEFAULT_KERNEL = "linux"
+
+ kernels = sorted(list(options))
+
+ if kernels:
+ return generic_multi_select(kernels, f"Choose which kernel to use (leave blank for default: {DEFAULT_KERNEL}): ", default=DEFAULT_KERNEL)
+
+ raise RequirementError("Selecting kernels require a least one kernel to be given as an option.")