Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Hvornum <anton@hvornum.se>2021-04-22 08:55:54 +0000
committerGitHub <noreply@github.com>2021-04-22 08:55:54 +0000
commit331bb0789b8dad7e0fae4447311c23adaebc6c81 (patch)
tree55f3d5ee12909ceb3692571fe6ecbe6914010f53
parentbf9f7bdb169b3a70df2e52421145bb02fde608dd (diff)
parent4d65639724e1402ef584221bbbb0e80dba827ab7 (diff)
Merge pull request #325 from SecondThundeR/generic-select-update
Rework of the generic_select function and fixes for various input checks
-rw-r--r--archinstall/lib/profiles.py7
-rw-r--r--archinstall/lib/user_interaction.py219
-rw-r--r--examples/guided.py6
-rw-r--r--profiles/desktop.py3
-rw-r--r--profiles/i3.py3
-rw-r--r--profiles/xorg.py36
6 files changed, 157 insertions, 117 deletions
diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py
index 21ec5f6f..95962fd6 100644
--- a/archinstall/lib/profiles.py
+++ b/archinstall/lib/profiles.py
@@ -15,7 +15,7 @@ def grab_url_data(path):
response = urllib.request.urlopen(safe_path, context=ssl_context)
return response.read()
-def list_profiles(filter_irrelevant_macs=True, subpath=''):
+def list_profiles(filter_irrelevant_macs=True, subpath='', filter_top_level_profiles=False):
# TODO: Grab from github page as well, not just local static files
if filter_irrelevant_macs:
local_macs = list_interfaces()
@@ -63,6 +63,11 @@ def list_profiles(filter_irrelevant_macs=True, subpath=''):
cache[profile[:-3]] = {'path' : os.path.join(storage["UPSTREAM_URL"]+subpath, profile), 'description' : profile_list[profile], 'tailored' : tailored}
+ if filter_top_level_profiles:
+ for profile in list(cache.keys()):
+ if Profile(None, profile).is_top_level_profile() is False:
+ del(cache[profile])
+
return cache
class Script():
diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py
index dfc3553b..484205fb 100644
--- a/archinstall/lib/user_interaction.py
+++ b/archinstall/lib/user_interaction.py
@@ -1,5 +1,5 @@
import getpass, pathlib, os, shutil, re
-import sys, time, signal
+import sys, time, signal, ipaddress
from .exceptions import *
from .profiles import Profile
from .locale_helpers import search_keyboard_layout
@@ -154,27 +154,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,
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=LOG_LEVELS.Warning,
+ fg='red'
+ )
dns = None
if len(dns_input := input('Enter your DNS servers (space separated, blank for none): ').strip()):
@@ -190,12 +219,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 +236,72 @@ 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.')
-
- for index, option in enumerate(options):
- print(f"{index}: {option}")
-
- 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}')
+ # 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.')
+
+ # 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...else 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]
+ 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
+ else:
+ break
+
return selected_option
def select_disk(dict_o_disks):
@@ -257,18 +319,11 @@ 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}"')
+ drive = generic_select(drives, 'Select one of the above disks (by number or full path) or leave blank to skip partitioning: ',
+ 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,21 +348,13 @@ 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):
"""
@@ -334,12 +381,18 @@ 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): ')
+ # Current workaround for passing `generic_select`,
+ # if these values are provided as input
+ languages.extend(['?', 'help'])
+ languages_length = len(languages)
+
+ print(f' -- You can enter ? ({languages_length - 2}) or help ({languages_length - 1}) to search for more languages, or skip to use US layout --')
+ selected_language = generic_select(languages, 'Select one of the above keyboard languages (by number or full name): ',
+ options_output=False)
- if len(selected_language.strip()) == 0:
+ if not selected_language:
return DEFAULT_KEYBOARD_LANGUAGE
- elif selected_language.lower() in ('?', 'help'):
+ elif selected_language in ('?', 'help'):
while True:
filter_string = input('Search for layout containing (example: "sv-"): ')
new_options = list(search_keyboard_layout(filter_string))
@@ -350,18 +403,13 @@ def select_language(options, show_only_country_codes=True):
return select_language(new_options, show_only_country_codes=False)
- 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 languages:
- return selected_language
- else:
- raise RequirementError("Selected language does not exist.")
+
+ return selected_language
raise RequirementError("Selecting languages require a least one language to be given as an option.")
@@ -388,23 +436,18 @@ 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
diff --git a/examples/guided.py b/examples/guided.py
index 7bf088ca..ef447abb 100644
--- a/examples/guided.py
+++ b/examples/guided.py
@@ -76,7 +76,9 @@ def ask_user_questions():
archinstall.log(f" ** The root would be a simple / and the boot partition /boot (as all paths are relative inside the installation). **")
while True:
# Select a partition
- partition = archinstall.generic_select(partition_mountpoints.keys(),
+ # If we provide keys as options, it's better to convert them to list and sort before passing
+ mountpoints_list = sorted(list(partition_mountpoints.keys()))
+ partition = archinstall.generic_select(mountpoints_list,
"Select a partition by number that you want to set a mount-point for (leave blank when done): ")
if not partition:
break
@@ -162,7 +164,7 @@ def ask_user_questions():
# Ask for archinstall-specific profiles (such as desktop environments etc)
if not archinstall.arguments.get('profile', None):
- archinstall.arguments['profile'] = archinstall.select_profile(filter(lambda profile: (Profile(None, profile).is_top_level_profile()), archinstall.list_profiles()))
+ archinstall.arguments['profile'] = archinstall.select_profile(archinstall.list_profiles(filter_top_level_profiles=True))
else:
archinstall.arguments['profile'] = archinstall.list_profiles()[archinstall.arguments['profile']]
diff --git a/profiles/desktop.py b/profiles/desktop.py
index 846182e2..2aea6d30 100644
--- a/profiles/desktop.py
+++ b/profiles/desktop.py
@@ -17,7 +17,8 @@ def _prep_function(*args, **kwargs):
"""
supported_desktops = ['gnome', 'kde', 'awesome', 'sway', 'cinnamon', 'xfce4', 'lxqt', 'i3', 'budgie', 'mate']
- desktop = archinstall.generic_select(supported_desktops, 'Select your desired desktop environment: ')
+ desktop = archinstall.generic_select(supported_desktops, 'Select your desired desktop environment: ',
+ allow_empty_input=False, sort=True)
# Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded
diff --git a/profiles/i3.py b/profiles/i3.py
index 67028b2d..b82c03d6 100644
--- a/profiles/i3.py
+++ b/profiles/i3.py
@@ -17,7 +17,8 @@ def _prep_function(*args, **kwargs):
"""
supported_configurations = ['i3-wm', 'i3-gaps']
- desktop = archinstall.generic_select(supported_configurations, 'Select your desired configuration: ')
+ desktop = archinstall.generic_select(supported_configurations, 'Select your desired configuration: ',
+ allow_empty_input=False, sort=True)
# Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded
diff --git a/profiles/xorg.py b/profiles/xorg.py
index e905d533..130f3ed0 100644
--- a/profiles/xorg.py
+++ b/profiles/xorg.py
@@ -1,6 +1,7 @@
# A system with "xorg" installed
-import archinstall, os
+import os
+from archinstall import generic_select, sys_command, RequirementError
is_top_level_profile = True
@@ -39,7 +40,7 @@ def select_driver(options):
print(' -- The above list are supported graphic card drivers. --')
print(' -- You need to select (and read about) which one you need. --')
- lspci = archinstall.sys_command(f'/usr/bin/lspci')
+ 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():
@@ -47,7 +48,8 @@ def select_driver(options):
elif b'amd' in line.lower():
print(' ** AMD card detected, suggested driver: AMD / ATI **')
- selected_driver = input('Select your graphics card driver: ')
+ selected_driver = generic_select(drivers, 'Select your graphics card driver: ',
+ allow_empty_input=False, options_output=False)
initial_option = selected_driver
# Disabled search for now, only a few profiles exist anyway
@@ -57,35 +59,21 @@ def select_driver(options):
# filter_string = input('Search for layout containing (example: "sv-"): ')
# new_options = search_keyboard_layout(filter_string)
# return select_language(new_options)
- if selected_driver.isdigit() and (pos := int(selected_driver)) <= len(drivers)-1:
- selected_driver = options[drivers[pos]]
- elif selected_driver in options:
- selected_driver = options[options.index(selected_driver)]
- elif len(selected_driver) == 0:
- raise archinstall.RequirementError("At least one graphics driver is needed to support a graphical environment. Please restart the installer and try again.")
- else:
- raise archinstall.RequirementError("Selected driver does not exist.")
+
+ selected_driver = options[selected_driver]
if type(selected_driver) == dict:
driver_options = sorted(list(selected_driver))
- for index, driver_package_group in enumerate(driver_options):
- print(f"{index}: {driver_package_group}")
- selected_driver_package_group = input(f'Which driver-type do you want for {initial_option}: ')
- if selected_driver_package_group.isdigit() and (pos := int(selected_driver_package_group)) <= len(driver_options)-1:
- selected_driver_package_group = selected_driver[driver_options[pos]]
- elif selected_driver_package_group in selected_driver:
- selected_driver_package_group = selected_driver[selected_driver.index(selected_driver_package_group)]
- elif len(selected_driver_package_group) == 0:
- raise archinstall.RequirementError(f"At least one driver package is required for a graphical environment using {selected_driver}. Please restart the installer and try again.")
- else:
- raise archinstall.RequirementError(f"Selected driver-type does not exist for {initial_option}.")
+ 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 selected_driver_package_group
+ return driver_package_group
return selected_driver
- raise archinstall.RequirementError("Selecting drivers require a least one profile to be given as an option.")
+ raise RequirementError("Selecting drivers require a least one profile to be given as an option.")
def _prep_function(*args, **kwargs):
"""