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>
This commit is contained in:
Daniel 2021-12-03 07:17:51 +11:00 committed by GitHub
parent 22ee2d90a1
commit 908c7b8cc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2322 additions and 381 deletions

View File

@ -6,4 +6,4 @@ max-complexity = 40
max-line-length = 236 max-line-length = 236
show-source = True show-source = True
statistics = True statistics = True
per-file-ignores = __init__.py:F401,F403,F405 per-file-ignores = __init__.py:F401,F403,F405 simple_menu.py:C901,W503

1
.gitignore vendored
View File

@ -25,5 +25,6 @@ SAFETY_LOCK
/guided.py /guided.py
/install.log /install.log
venv venv
.venv
.idea/** .idea/**
**/install.log **/install.log

View File

@ -20,6 +20,7 @@ from .lib.services import *
from .lib.storage import * from .lib.storage import *
from .lib.systemd import * from .lib.systemd import *
from .lib.user_interaction import * from .lib.user_interaction import *
from .lib.menu import Menu
parser = ArgumentParser() parser = ArgumentParser()
@ -88,7 +89,6 @@ if arguments.get('plugin', None):
# TODO: Learn the dark arts of argparse... (I summon thee dark spawn of cPython) # TODO: Learn the dark arts of argparse... (I summon thee dark spawn of cPython)
def run_as_a_module(): def run_as_a_module():
""" """
Since we're running this as a 'python -m archinstall' module OR Since we're running this as a 'python -m archinstall' module OR

View File

@ -16,7 +16,8 @@ def valid_parted_position(pos :str):
return False return False
def valid_fs_type(fstype :str) -> bool:
def fs_types():
# https://www.gnu.org/software/parted/manual/html_node/mkpart.html # https://www.gnu.org/software/parted/manual/html_node/mkpart.html
# Above link doesn't agree with `man parted` /mkpart documentation: # Above link doesn't agree with `man parted` /mkpart documentation:
""" """
@ -27,16 +28,19 @@ def valid_fs_type(fstype :str) -> bool:
"linux-swap", "ntfs", "reis "linux-swap", "ntfs", "reis
erfs", "udf", or "xfs". erfs", "udf", or "xfs".
""" """
return [
return fstype.lower() in [
"btrfs", "btrfs",
"ext2", "ext2",
"ext3", "ext4", # `man parted` allows these "ext3", "ext4", # `man parted` allows these
"fat16", "fat32", "fat16", "fat32",
"hfs", "hfs+", # "hfsx", not included in `man parted` "hfs", "hfs+", # "hfsx", not included in `man parted`
"linux-swap", "linux-swap",
"ntfs", "ntfs",
"reiserfs", "reiserfs",
"udf", # "ufs", not included in `man parted` "udf", # "ufs", not included in `man parted`
"xfs", # `man parted` allows this "xfs", # `man parted` allows this
] ]
def valid_fs_type(fstype :str) -> bool:
return fstype.lower() in fs_types()

View File

@ -47,3 +47,8 @@ def set_keyboard_language(locale):
return True return True
return False return False
def list_timezones():
for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}):
yield line.decode('UTF-8').strip()

91
archinstall/lib/menu.py Normal file
View File

@ -0,0 +1,91 @@
from .simple_menu import TerminalMenu
class Menu(TerminalMenu):
def __init__(self, title, options, skip=True, multi=False, default_option=None, sort=True):
"""
Creates a new menu
:param title: Text that will be displayed above the menu
:type title: str
:param options: Options to be displayed in the menu to chose from;
if dict is specified then the keys of such will be used as options
:type options: list, dict
:param skip: Indicate if the selection is not mandatory and can be skipped
:type skip: bool
:param multi: Indicate if multiple options can be selected
:type multi: bool
:param default_option: The default option to be used in case the selection processes is skipped
:type default_option: str
:param sort: Indicate if the options should be sorted alphabetically before displaying
:type sort: bool
"""
if isinstance(options, dict):
options = list(options)
if sort:
options = sorted(options)
self.menu_options = options
self.skip = skip
self.default_option = default_option
self.multi = multi
menu_title = f'\n{title}\n\n'
if skip:
menu_title += "Use ESC to skip\n\n"
if default_option:
# if a default value was specified we move that one
# to the top of the list and mark it as default as well
default = f'{default_option} (default)'
self.menu_options = [default] + [o for o in self.menu_options if default_option != o]
cursor = "> "
main_menu_cursor_style = ("fg_cyan", "bold")
main_menu_style = ("bg_blue", "fg_gray")
super().__init__(
menu_entries=self.menu_options,
title=menu_title,
menu_cursor=cursor,
menu_cursor_style=main_menu_cursor_style,
menu_highlight_style=main_menu_style,
cycle_cursor=True,
clear_screen=True,
multi_select=multi,
show_search_hint=True
)
def _show(self):
idx = self.show()
if idx is not None:
if isinstance(idx, (list, tuple)):
return [self.menu_options[i] for i in idx]
else:
selected = self.menu_options[idx]
if ' (default)' in selected and self.default_option:
return self.default_option
return selected
else:
if self.default_option:
if self.multi:
return [self.default_option]
else:
return self.default_option
return None
def run(self):
ret = self._show()
if ret is None and not self.skip:
return self.run()
return ret

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
import getpass import getpass
import ipaddress import ipaddress
import logging import logging
import pathlib
import re import re
import select # Used for char by char polling of sys.stdin import select # Used for char by char polling of sys.stdin
import shutil import shutil
@ -9,17 +8,20 @@ import signal
import sys import sys
import time 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 .exceptions import RequirementError, UserError, DiskError
from .hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics 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 .networking import list_interfaces
from .menu import Menu
from .output import log from .output import log
from .profiles import Profile, list_profiles from .profiles import Profile, list_profiles
from .storage import storage from .storage import storage
from .mirrors import list_mirrors
# TODO: Some inconsistencies between the selection processes. # TODO: Some inconsistencies between the selection processes.
# Some return the keys from the options, some the values? # Some return the keys from the options, some the values?
from .. import fs_types
def get_terminal_height(): def get_terminal_height():
@ -117,81 +119,6 @@ def print_large_list(options, padding=5, margin_bottom=0, separator=': '):
return column, row 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: def select_encrypted_partitions(block_devices :dict, password :str) -> dict:
for device in block_devices: for device in block_devices:
for partition in block_devices[device]['partitions']: 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 # 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. # Users might want to single out a partition for non-encryption to share between dualboot etc.
class MiniCurses: class MiniCurses:
def __init__(self, width, height): def __init__(self, width, height):
self.width = width 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(): def ask_for_a_timezone():
while True: timezones = list_timezones()
timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip().strip('*.') default = 'UTC'
if timezone == '':
timezone = 'UTC' selected_tz = Menu(
if (pathlib.Path("/usr") / "share" / "zoneinfo" / timezone).exists(): f'Select a timezone or leave blank to use default "{default}"',
return timezone timezones,
else: skip=False,
log( default_option=default
f"Specified timezone {timezone} does not exist.", ).run()
level=logging.WARNING,
fg='red' return selected_tz
)
def ask_for_bootloader(advanced_options=False) -> str: def ask_for_bootloader(advanced_options=False) -> str:
@ -394,7 +321,7 @@ def ask_for_bootloader(advanced_options=False) -> str:
else: else:
# We use the common names for the bootloader as the selection, and map it back to the expected values. # We use the common names for the bootloader as the selection, and map it back to the expected values.
choices = ['systemd-boot', 'grub', 'efistub'] 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 != "":
if selection == 'systemd-boot': if selection == 'systemd-boot':
bootloader = 'systemd-bootctl' bootloader = 'systemd-bootctl'
@ -409,11 +336,8 @@ def ask_for_bootloader(advanced_options=False) -> str:
def ask_for_audio_selection(desktop=True): def ask_for_audio_selection(desktop=True):
audio = 'pipewire' if desktop else 'none' audio = 'pipewire' if desktop else 'none'
choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', '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) selected_audio = Menu(f'Choose an audio server or leave blank to use "{audio}"', choices, default_option=audio).run()
if selection != "": return selected_audio
audio = selection
return audio
def ask_to_configure_network(): def ask_to_configure_network():
@ -426,7 +350,8 @@ def ask_to_configure_network():
**list_interfaces() **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 and nic != 'Copy ISO network configuration to installation':
if nic == 'Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)': if nic == 'Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)':
return {'nic': nic, 'NetworkManager': True} return {'nic': nic, 'NetworkManager': True}
@ -436,11 +361,15 @@ def ask_to_configure_network():
# printing out this part separate from options, passed in # printing out this part separate from options, passed in
# `generic_select` # `generic_select`
modes = ['DHCP (auto detect)', 'IP (static)'] modes = ['DHCP (auto detect)', 'IP (static)']
for index, mode in enumerate(modes): default_mode = 'DHCP (auto detect)'
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) mode = Menu(
if mode == 'IP': f'Select which mode to configure for "{nic}" or leave blank for default "{default_mode}"',
modes,
default_option=default_mode
).run()
if mode == 'IP (static)':
while 1: while 1:
ip = input(f"Enter the IP and subnet for {nic} (example: 192.168.0.5/24): ").strip() 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 # Implemented new check for correct IP/subnet input
@ -483,15 +412,9 @@ def ask_to_configure_network():
return {} return {}
def ask_for_disk_layout(): def partition_overlap(partitions :list, start :str, end :str) -> bool:
options = { # TODO: Implement sanity check
'keep-existing': 'Keep existing partition layout and select which ones to use where', return False
'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 ask_for_main_filesystem_format(advanced_options=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: if advanced_options:
options.update(advanced) options.update(advanced)
value = generic_select(options, "Select which filesystem your main partition should use (by number or name): ", allow_empty_input=False) return Menu('Select which filesystem your main partition should use', options, skip=False).run()
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: ", allow_empty_input=True, options_output=True, sort=False): def current_partition_layout(partitions, with_idx=False):
""" def do_padding(name, max_len):
A generic select function that does not output anything spaces = abs(len(str(name)) - max_len) + 2
other than the options and their indexes. As an example: pad_left = int(spaces / 2)
pad_right = spaces - pad_left
return f'{pad_right * " "}{name}{pad_left * " "}|'
generic_select(["first", "second", "third option"]) column_names = {}
0: first
1: second
2: third option
When the user has entered the option correctly, # this will add an initial index to the table for each partition
this function returns an item from list, a string, or None 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 # determine all attribute names and the max length
if type(options) not in [list, dict]: # of the value among all partitions to know the width
log(f" * Generic select doesn't support ({type(options)}) as type of options * ", fg='red') # of the table cells
log(" * If problem persists, please create an issue on https://github.com/archlinux/archinstall/issues * ", fg='yellow') for p in partitions:
raise RequirementError("generic_select() requires list or dictionary as options.") for attribute, value in p.items():
if not options: if attribute in column_names.keys():
log(" * Generic select didn't find any options to choose from * ", fg='red') column_names[attribute] = max([column_names[attribute], len(str(value)), len(attribute)])
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}")
# 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
else: else:
raise RequirementError(f'Selected option "{selected_option}" does not exist in available options') column_names[attribute] = max([len(str(value)), len(attribute)])
except RequirementError as err:
log(f" * {err} * ", fg='red')
return selected_option current_layout = ''
for name, max_len in column_names.items():
current_layout += do_padding(name, max_len)
def partition_overlap(partitions :list, start :str, end :str) -> bool: current_layout = f'{current_layout[:-1]}\n{"-" * len(current_layout)}\n'
# TODO: Implement sanity check
return False 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:
row += ' ' * (max_len + 2) + '|'
current_layout += f'{row[:-1]}\n'
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): def get_default_partition_layout(block_devices, advanced_options=False):
if len(block_devices) == 1: if len(block_devices) == 1:
@ -587,7 +497,6 @@ def get_default_partition_layout(block_devices, advanced_options=False):
else: else:
return suggest_multi_disk_layout(block_devices, advanced_options=advanced_options) 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: def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict:
# if has_uefi(): # if has_uefi():
@ -624,7 +533,7 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict:
# return struct # return struct
block_device_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()] # Test code: [part.__dump__() for part in block_device.partitions.values()]
# TODO: Squeeze in BTRFS subvolumes here # TODO: Squeeze in BTRFS subvolumes here
@ -632,25 +541,27 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict:
while True: while True:
modes = [ modes = [
"Create a new partition", "Create a new partition",
f"Suggest partition layout for {block_device}", 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 "",
] ]
# Print current partition layout: if len(block_device_struct['partitions']):
if len(block_device_struct["partitions"]): modes += [
print('Current partition layout:') "Delete a partition",
for partition in block_device_struct["partitions"]: "Clear/Delete all partitions",
print(partition) "Assign mount-point for a partition",
print() "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",
]
task = generic_select(modes, title = f'Select what to do with \n{block_device}'
input_text=f"Select what to do with {block_device} (leave blank when done): ")
# show current partition layout:
if len(block_device_struct["partitions"]):
title += current_partition_layout(block_device_struct['partitions']) + '\n'
task = Menu(title, modes, sort=False).run()
if not task: if not task:
break 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 # # https://www.gnu.org/software/parted/manual/html_node/mklabel.html
# name = input("Enter a desired name for the partition: ").strip() # 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() start = input(f"Enter the start sector (percentage or block number, default: {block_device.first_free_sector}): ").strip()
if not start.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 end_suggested = block_device.first_end_sector
else: else:
end_suggested = '100%' end_suggested = '100%'
end = input(f"Enter the end sector of the partition (percentage or block number, ex: {end_suggested}): ").strip() end = input(f"Enter the end sector of the partition (percentage or block number, ex: {end_suggested}): ").strip()
if not end.strip(): if not end.strip():
end = end_suggested 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): 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") log(f"This partition overlaps with other partitions on the drive! Ignoring this partition creation.", fg="red")
continue continue
block_device_struct["partitions"].append({ 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, "start" : start,
"size" : end, "size" : end,
"mountpoint" : None, "mountpoint" : None,
@ -689,7 +602,7 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict:
} }
}) })
else: 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 continue
elif task[:len("Suggest partition layout")] == "Suggest partition layout": elif task[:len("Suggest partition layout")] == "Suggest partition layout":
if len(block_device_struct["partitions"]): if len(block_device_struct["partitions"]):
@ -700,77 +613,83 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict:
elif task is None: elif task is None:
return block_device_struct return block_device_struct
else: else:
for index, partition in enumerate(block_device_struct["partitions"]): current_layout = current_partition_layout(block_device_struct['partitions'], with_idx=True)
print(f"{index}: Start: {partition['start']}, End: {partition['size']} ({partition['filesystem']['format']}{', mounting at: '+partition['mountpoint'] if partition['mountpoint'] else ''})")
if task == "Delete a partition": if task == "Delete a partition":
if (partition := generic_select(block_device_struct["partitions"], 'Select which partition to delete: ', options_output=False)): title = f'{current_layout}\n\nSelect by index which partitions to delete'
del(block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]) 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": elif task == "Clear/Delete all partitions":
block_device_struct["partitions"] = [] block_device_struct["partitions"] = []
elif task == "Assign mount-point for a partition": 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.') 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() mountpoint = input('Select where to mount partition (leave blank to remove mountpoint): ').strip()
if len(mountpoint): 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': if mountpoint == '/boot':
log(f"Marked partition as bootable because mountpoint was set to /boot.", fg="yellow") 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 block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['boot'] = True
else: 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)": 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 # 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, # 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. # 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 block_device_struct["partitions"][partition].get('filesystem', {}).get('format', 'crypto_LUKS') == 'crypto_LUKS':
if not block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('filesystem', None): if not block_device_struct["partitions"][partition].get('filesystem', None):
block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['filesystem'] = {} block_device_struct["partitions"][partition]['filesystem'] = {}
while True: fstype = Menu('Enter a desired filesystem type for the partition', fs_types(), skip=False).run()
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
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 # 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": 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 # 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)": 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)): title = f'{current_layout}\n\nSelect which partition to mark as bootable'
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) 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": elif task == "Set desired filesystem for a partition":
if not block_device_struct["partitions"]: title = f'{current_layout}\n\nSelect which partition to set a filesystem on'
log("No partitions found. Create some partitions first", level=logging.WARNING, fg='yellow') partition = select_partition(title, block_device_struct["partitions"])
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'] = {}
while True: if partition is not None:
fstype = input("Enter a desired filesystem type for the partition: ").strip() if not block_device_struct["partitions"][partition].get('filesystem', None):
if not valid_fs_type(fstype): block_device_struct["partitions"][partition]['filesystem'] = {}
log(f"Desired filesystem {fstype} is not a valid filesystem.", level=logging.ERROR, fg="red")
continue
break
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 return block_device_struct
def select_individual_blockdevice_usage(block_devices :list):
def select_individual_blockdevice_usage(block_devices: list):
result = {} result = {}
for device in block_devices: 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)" "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': if mode == 'Wipe all selected drives and use a best-effort default partition layout':
return get_default_partition_layout(block_devices, advanced_options) 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']})") 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") 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: if not drive:
return drive return drive
@ -824,128 +744,90 @@ def select_disk(dict_o_disks):
def select_profile(): 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 for profile in top_level_profiles:
:rtype: str profile = Profile(None, profile)
description = profile.get_profile_description()
option = f'{profile.profile}: {description}'
options[option] = profile
title = 'This is a list of pre-programmed profiles, ' \
'they might make it easier to install things like desktop environments'
selection = Menu(title=title, options=options.keys()).run()
if selection is not None:
return options[selection]
return None
def select_language():
""" """
shown_profiles = sorted(list(list_profiles(filter_top_level_profiles=True))) Asks the user to select a language
actual_profiles_raw = shown_profiles + sorted([profile for profile in list_profiles() if profile not in shown_profiles])
if len(shown_profiles) >= 1:
for index, profile in enumerate(shown_profiles):
description = Profile(None, profile).get_profile_description()
print(f"{index}: {profile}: {description}")
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 = 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.")
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`. Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
: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
:return: The language/dictionary key of the selected language :return: The language/dictionary key of the selected language
:rtype: str :rtype: str
""" """
default_keyboard_language = 'us' 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 show_only_country_codes: selected_lang = Menu('Select Keyboard layout', sorted_kb_lang, default_option='us', sort=False).run()
languages = sorted([language for language in list(options) if len(language) == 2]) return selected_lang
else:
languages = sorted(list(options))
if len(languages) >= 1:
print_large_list(languages, margin_bottom=4)
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`. 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. :return: The dictionary information about a mirror/region.
:rtype: dict :rtype: dict
""" """
# TODO: Support multiple options and country codes, SE,UK for instance. # TODO: Support multiple options and country codes, SE,UK for instance.
regions = sorted(list(mirrors.keys()))
selected_mirrors = {}
if len(regions) >= 1: mirrors = list_mirrors()
print_large_list(regions, margin_bottom=4) 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 --') if selected_mirror is not None:
selected_mirror = generic_select(regions, 'Select one of the above regions to download packages from (by number or full name): ', options_output=False) return {selected_mirror: mirrors[selected_mirror]}
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 {}
# I'm leaving "mirrors" on purpose here. return {}
# 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_harddrives():
"""
Asks the user to select one or multiple hard drives
:return: List of selected hard drives
:rtype: list
"""
hard_drives = all_disks().values()
options = {f'{option}': option for option in hard_drives}
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): def select_driver(options=AVAILABLE_GFX_DRIVERS):
@ -961,15 +843,18 @@ def select_driver(options=AVAILABLE_GFX_DRIVERS):
if drivers: if drivers:
arguments = storage.get('arguments', {}) arguments = storage.get('arguments', {})
title = ''
if has_amd_graphics(): 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(): 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(): 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): 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: if arguments.get('gfx_driver', None) is None:
arguments['gfx_driver'] = "All open-source (default)" 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.") 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. 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 :return: The string as a selected kernel
:rtype: string :rtype: string
""" """
kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"]
default_kernel = "linux" default_kernel = "linux"
kernels = sorted(list(options)) 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()
if kernels: return selected_kernels
return generic_multi_select(kernels, f"Choose which kernels to use (leave blank for default: {default_kernel}): ", default=default_kernel, sort=False)
raise RequirementError("Selecting kernels require a least one kernel to be given as an option.")

View File

@ -63,6 +63,7 @@ def load_config():
except: except:
raise ValueError("--disk_layouts=<json> needs either a JSON file or a JSON string given with a valid disk layout.") raise ValueError("--disk_layouts=<json> needs either a JSON file or a JSON string given with a valid disk layout.")
def ask_user_questions(): def ask_user_questions():
""" """
First, we'll ask the user for a bunch of user input. First, we'll ask the user for a bunch of user input.
@ -70,12 +71,7 @@ def ask_user_questions():
will we continue with the actual installation steps. will we continue with the actual installation steps.
""" """
if not archinstall.arguments.get('keyboard-layout', None): if not archinstall.arguments.get('keyboard-layout', None):
while True: archinstall.arguments['keyboard-layout'] = archinstall.select_language()
try:
archinstall.arguments['keyboard-layout'] = archinstall.select_language(archinstall.list_keyboard_languages()).strip()
break
except archinstall.RequirementError as err:
archinstall.log(err, fg="red")
# Before continuing, set the preferred keyboard layout/language in the current terminal. # Before continuing, set the preferred keyboard layout/language in the current terminal.
# This will just help the user with the next following questions. # This will just help the user with the next following questions.
@ -84,12 +80,7 @@ def ask_user_questions():
# Set which region to download packages from during the installation # Set which region to download packages from during the installation
if not archinstall.arguments.get('mirror-region', None): if not archinstall.arguments.get('mirror-region', None):
while True: archinstall.arguments['mirror-region'] = archinstall.select_mirror_regions()
try:
archinstall.arguments['mirror-region'] = archinstall.select_mirror_regions(archinstall.list_mirrors())
break
except archinstall.RequirementError as e:
archinstall.log(e, fg="red")
if not archinstall.arguments.get('sys-language', None) and archinstall.arguments.get('advanced', False): if not archinstall.arguments.get('sys-language', None) and archinstall.arguments.get('advanced', False):
archinstall.arguments['sys-language'] = input("Enter a valid locale (language) for your OS, (Default: en_US): ").strip() archinstall.arguments['sys-language'] = input("Enter a valid locale (language) for your OS, (Default: en_US): ").strip()
@ -104,9 +95,7 @@ def ask_user_questions():
# Ask which harddrives/block-devices we will install to # Ask which harddrives/block-devices we will install to
# and convert them into archinstall.BlockDevice() objects. # and convert them into archinstall.BlockDevice() objects.
if archinstall.arguments.get('harddrives', None) is None: if archinstall.arguments.get('harddrives', None) is None:
archinstall.arguments['harddrives'] = archinstall.generic_multi_select(archinstall.all_disks(), archinstall.arguments['harddrives'] = archinstall.select_harddrives()
text="Select one or more harddrives to use and configure (leave blank to skip this step): ",
allow_empty=True)
if archinstall.arguments.get('harddrives', None) is not None and archinstall.storage.get('disk_layouts', None) is None: if archinstall.arguments.get('harddrives', None) is not None and archinstall.storage.get('disk_layouts', None) is None:
archinstall.storage['disk_layouts'] = archinstall.select_disk_layout(archinstall.arguments['harddrives'], archinstall.arguments.get('advanced', False)) archinstall.storage['disk_layouts'] = archinstall.select_disk_layout(archinstall.arguments['harddrives'], archinstall.arguments.get('advanced', False))
@ -150,7 +139,8 @@ def ask_user_questions():
# Check the potentially selected profiles preparations to get early checks if some additional questions are needed. # Check the potentially selected profiles preparations to get early checks if some additional questions are needed.
if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_prep_function(): if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_prep_function():
with archinstall.arguments['profile'].load_instructions(namespace=f"{archinstall.arguments['profile'].namespace}.py") as imported: namespace = f"{archinstall.arguments['profile'].namespace}.py"
with archinstall.arguments['profile'].load_instructions(namespace=namespace) as imported:
if not imported._prep_function(): if not imported._prep_function():
archinstall.log(' * Profile\'s preparation requirements was not fulfilled.', fg='red') archinstall.log(' * Profile\'s preparation requirements was not fulfilled.', fg='red')
exit(1) exit(1)
@ -162,8 +152,7 @@ def ask_user_questions():
# Ask for preferred kernel: # Ask for preferred kernel:
if not archinstall.arguments.get("kernels", None): if not archinstall.arguments.get("kernels", None):
kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] archinstall.arguments['kernels'] = archinstall.select_kernel()
archinstall.arguments['kernels'] = archinstall.select_kernel(kernels)
# Additional packages (with some light weight error handling for invalid package names) # Additional packages (with some light weight error handling for invalid package names)
print("Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.") print("Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.")

View File

@ -1,5 +1,4 @@
# A desktop environment selector. # A desktop environment selector.
import archinstall import archinstall
is_top_level_profile = True is_top_level_profile = True
@ -44,8 +43,7 @@ def _prep_function(*args, **kwargs):
other code in this stage. So it's a safe way to ask the user other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start. for more input before any other installer steps start.
""" """
desktop = archinstall.Menu('Select your desired desktop environment', __supported__, skip=False).run()
desktop = archinstall.generic_select(__supported__, 'Select your desired desktop environment: ', allow_empty_input=False, sort=True)
# Temporarily store the selected desktop profile # Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded # in a session-safe location, since this module will get reloaded

View File

@ -26,7 +26,8 @@ def _prep_function(*args, **kwargs):
""" """
supported_configurations = ['i3-wm', 'i3-gaps'] supported_configurations = ['i3-wm', 'i3-gaps']
desktop = archinstall.generic_select(supported_configurations, 'Select your desired configuration: ', allow_empty_input=False, sort=True)
desktop = archinstall.Menu('Select your desired configuration', supported_configurations, skip=False).run()
# Temporarily store the selected desktop profile # Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded # in a session-safe location, since this module will get reloaded

View File

@ -27,8 +27,12 @@ def _prep_function(*args, **kwargs):
before continuing any further. before continuing any further.
""" """
if not archinstall.storage.get('_selected_servers', None): if not archinstall.storage.get('_selected_servers', None):
selected_servers = archinstall.generic_multi_select(available_servers, "Choose which servers to install and enable (leave blank for a minimal installation): ") servers = archinstall.Menu(
archinstall.storage['_selected_servers'] = selected_servers 'Choose which servers to install, if none then a minimal installation wil be done', available_servers,
multi=True
).run()
archinstall.storage['_selected_servers'] = servers
return True return True

View File

@ -15,6 +15,7 @@ classifiers = [ "License :: OSI Approved :: GNU General Public License v3 or lat
] ]
description-file = "README.md" description-file = "README.md"
requires-python=">=3.8" requires-python=">=3.8"
[tool.flit.metadata.urls] [tool.flit.metadata.urls]
Source = "https://github.com/archlinux/archinstall" Source = "https://github.com/archlinux/archinstall"
Documentation = "https://archinstall.readthedocs.io/" Documentation = "https://archinstall.readthedocs.io/"

View File

@ -12,7 +12,7 @@ license_files =
project_urls = project_urls =
Source = https://github.com/archlinux/archinstall Source = https://github.com/archlinux/archinstall
Documentation = https://archinstall.readthedocs.io/ Documentation = https://archinstall.readthedocs.io/
classifers = classifiers =
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9