Integrate new curses menu (#2663)

* Edit text menu

* Fix alignment

* Scroll functionality

* Fix flake8

* Migrate locales menu

* Fix language translation

* Fix interrupt

* Fix flake8

* Edit mode preset

* Convert print to tui prints

* Fix mypy

* Fix cycling through long menu

* Fix profile view

* Fix scrolling

* Fix scrolling

* Fix mypy

* Fix swiss script

* Display asterisk for passwords

* Corrected a variable usage in the local mirror parsing

* Made sure that curses menu selection on mirrors use url object from mirror.url instead of the class instance

* Fixed mypy type on mirror list

---------

Co-authored-by: Torxed <torxed@archlinux.org>
This commit is contained in:
Daniel Girtler 2024-11-15 18:23:22 +11:00 committed by GitHub
parent 591b8317ea
commit 88b91ae201
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 3527 additions and 3195 deletions

View File

@ -24,7 +24,7 @@ body:
- type: textarea
id: bug-report
attributes:
label: The installation log
label: The installation log
description: 'note: located at `/var/log/archinstall/install.log`'
placeholder: |
Hardware model detected: Dell Inc. Precision 7670; UEFI mode: True
@ -82,4 +82,4 @@ body:
**Note**: Feel free to modify the textarea above as you wish.
But it will grately help us in testing if we can generate the specific qemu command line,
for instance via:
`sudo virsh domxml-to-native qemu-argv --domain my-arch-machine`
`sudo virsh domxml-to-native qemu-argv --domain my-arch-machine`

View File

@ -14,4 +14,4 @@ body:
description: >
Feel free to write any feature you think others might benefit from:
validations:
required: true
required: true

View File

@ -20,4 +20,4 @@ jobs:
- run: python --version
- run: mypy --version
- name: run mypy
run: mypy
run: mypy --config-file pyproject.toml

2
.gitignore vendored
View File

@ -38,4 +38,4 @@ venv
requirements.txt
/.gitconfig
/actions-runner
/cmd_output.txt
/cmd_output.txt

View File

@ -38,6 +38,9 @@ repos:
rev: v1.13.0
hooks:
- id: mypy
args: [
'--config-file=pyproject.toml'
]
fail_fast: true
additional_dependencies:
- pydantic

View File

@ -3,4 +3,4 @@ index-servers =
pypi
[pypi]
repository = https://upload.pypi.org/legacy/
repository = https://upload.pypi.org/legacy/

View File

@ -12,4 +12,4 @@ sphinx:
build:
os: "ubuntu-22.04"
tools:
python: "3.11"
python: "3.11"

View File

@ -10,7 +10,6 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Union
from .lib import disk
from .lib import menu
from .lib import models
from .lib import packages
from .lib import exceptions
@ -32,6 +31,7 @@ from .lib.boot import Boot
from .lib.translationhandler import TranslationHandler, Language, DeferredTranslation
from .lib.plugins import plugins, load_plugin
from .lib.configuration import ConfigurationOutput
from .tui import Tui
from .lib.general import (
generate_password, locate_binary, clear_vt100_escape_codes,
@ -330,24 +330,6 @@ def main() -> None:
importlib.import_module(mod_name)
def _shutdown_curses() -> None:
try:
curses.nocbreak()
try:
from archinstall.tui.curses_menu import tui
tui.screen.keypad(False)
except Exception:
pass
curses.echo()
curses.curs_set(True)
curses.endwin()
except Exception:
# this may happen when curses has not been initialized
pass
def run_as_a_module() -> None:
exc = None
@ -357,7 +339,7 @@ def run_as_a_module() -> None:
exc = e
finally:
# restore the terminal to the original state
_shutdown_curses()
Tui.shutdown()
if exc:
err = ''.join(traceback.format_exception(exc))

View File

@ -1,10 +1,14 @@
from typing import Any, TYPE_CHECKING, List, Optional, Dict
from archinstall.lib import menu
from archinstall.lib.output import info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, GreeterType
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, ResultType, PreviewStyle
)
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
_: Any
@ -52,22 +56,36 @@ class DesktopProfile(Profile):
for profile in self.current_selection:
profile.do_on_select()
def do_on_select(self) -> SelectResult:
choice = profile_handler.select_profile(
profile_handler.get_desktop_profiles(),
self.current_selection,
title=str(_('Select your desired desktop environment')),
multi=True
)
def do_on_select(self) -> Optional[SelectResult]:
items = [
MenuItem(
p.name,
value=p,
preview_action=lambda x: x.value.preview_text()
) for p in profile_handler.get_desktop_profiles()
]
match choice.type_:
case menu.MenuSelectionType.Selection:
self.current_selection = choice.value # type: ignore
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(self.current_selection)
result = SelectMenu(
group,
multi=True,
allow_reset=True,
allow_skip=True,
preview_style=PreviewStyle.RIGHT,
preview_size='auto',
preview_frame=FrameProperties.max('Info')
).run()
match result.type_:
case ResultType.Selection:
self.current_selection = result.get_values()
self._do_on_select_profiles()
return SelectResult.NewSelection
case menu.MenuSelectionType.Skip:
case ResultType.Skip:
return SelectResult.SameSelection
case menu.MenuSelectionType.Reset:
case ResultType.Reset:
return SelectResult.ResetCurrent
def post_install(self, install_session: 'Installer') -> None:

View File

@ -1,9 +1,12 @@
from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Any
from archinstall.default_profiles.profile import ProfileType, GreeterType
from archinstall.default_profiles.profile import ProfileType, GreeterType, SelectResult
from archinstall.default_profiles.xorg import XorgProfile
from archinstall.lib.menu import Menu
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, ResultType, Alignment
)
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
@ -49,20 +52,30 @@ class HyprlandProfile(XorgProfile):
def _ask_seat_access(self) -> None:
# need to activate seat service and add to seat group
title = str(_('Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)'))
title += str(_('\n\nChoose an option to give Hyprland access to your hardware'))
header = str(_('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)'))
header += '\n' + str(_('Choose an option to give Sway access to your hardware')) + '\n'
options = [e.value for e in SeatAccess]
default = None
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
if seat := self.custom_settings.get('seat_access', None):
default = seat
default = self.custom_settings.get('seat_access', None)
group.set_default_by_value(default)
choice = Menu(title, options, skip=False, preset_values=default).run()
self.custom_settings['seat_access'] = choice.single_value
result = SelectMenu(
group,
header=header,
allow_skip=False,
frame=FrameProperties.min(str(_('Seat access'))),
alignment=Alignment.CENTER
).run()
def do_on_select(self):
if result.type_ == ResultType.Selection:
if result.item() is not None:
self.custom_settings['seat_access'] = result.get_value()
def do_on_select(self) -> Optional[SelectResult]:
self._ask_seat_access()
return None
def install(self, install_session: 'Installer') -> None:
super().install(install_session)

View File

@ -1,9 +1,13 @@
from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Any
from archinstall.default_profiles.profile import ProfileType, GreeterType
from archinstall.default_profiles.profile import ProfileType, GreeterType, SelectResult
from archinstall.default_profiles.xorg import XorgProfile
from archinstall.lib.menu import Menu
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType
)
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
@ -58,20 +62,30 @@ class SwayProfile(XorgProfile):
def _ask_seat_access(self) -> None:
# need to activate seat service and add to seat group
title = str(_('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)'))
title += str(_('\n\nChoose an option to give Sway access to your hardware'))
header = str(_('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)'))
header += '\n' + str(_('Choose an option to give Sway access to your hardware')) + '\n'
options = [e.value for e in SeatAccess]
default = None
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
if seat := self.custom_settings.get('seat_access', None):
default = seat
default = self.custom_settings.get('seat_access', None)
group.set_default_by_value(default)
choice = Menu(title, options, skip=False, preset_values=default).run()
self.custom_settings['seat_access'] = choice.single_value
result = SelectMenu(
group,
header=header,
allow_skip=False,
frame=FrameProperties.min(str(_('Seat access'))),
alignment=Alignment.CENTER
).run()
def do_on_select(self):
if result.type_ == ResultType.Selection:
if result.item() is not None:
self.custom_settings['seat_access'] = result.get_value()
def do_on_select(self) -> Optional[SelectResult]:
self._ask_seat_access()
return None
def install(self, install_session: 'Installer') -> None:
super().install(install_session)

View File

@ -4,7 +4,6 @@ import sys
from enum import Enum, auto
from typing import List, Optional, Any, Dict, TYPE_CHECKING
from ..lib.utils.util import format_cols
from ..lib.storage import storage
if TYPE_CHECKING:
@ -126,7 +125,7 @@ class Profile:
"""
return {}
def do_on_select(self) -> SelectResult:
def do_on_select(self) -> Optional[SelectResult]:
"""
Hook that will be called when a profile is selected
"""
@ -187,24 +186,20 @@ class Profile:
"""
return self.packages_text()
def packages_text(self, include_sub_packages: bool = False) -> Optional[str]:
header = str(_('Installed packages'))
text = ''
packages = []
def packages_text(self, include_sub_packages: bool = False) -> str:
packages = set()
if self.packages:
packages = self.packages
packages = set(self.packages)
if include_sub_packages:
for p in self.current_selection:
if p.packages:
packages += p.packages
for sub_profile in self.current_selection:
if sub_profile.packages:
packages.update(sub_profile.packages)
text += format_cols(sorted(set(packages)))
text = str(_('Installed packages')) + ':\n'
if text:
text = f'{header}: \n{text}'
return text
for pkg in sorted(packages):
text += f'\t- {pkg}\n'
return None
return text

View File

@ -1,10 +1,14 @@
from typing import Any, TYPE_CHECKING, List
from typing import Any, TYPE_CHECKING, List, Optional
from archinstall.lib.output import info
from archinstall.lib.menu import MenuSelectionType
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.default_profiles.profile import ProfileType, Profile, SelectResult
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, ResultType, PreviewStyle
)
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
_: Any
@ -19,23 +23,36 @@ class ServerProfile(Profile):
current_selection=current_value
)
def do_on_select(self) -> SelectResult:
available_servers = profile_handler.get_server_profiles()
def do_on_select(self) -> Optional[SelectResult]:
items = [
MenuItem(
p.name,
value=p,
preview_action=lambda x: x.value.preview_text()
) for p in profile_handler.get_server_profiles()
]
choice = profile_handler.select_profile(
available_servers,
self.current_selection,
title=str(_('Choose which servers to install, if none then a minimal installation will be done')),
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(self.current_selection)
result = SelectMenu(
group,
allow_reset=True,
allow_skip=True,
preview_style=PreviewStyle.RIGHT,
preview_size='auto',
preview_frame=FrameProperties.max('Info'),
multi=True
)
).run()
match choice.type_:
case MenuSelectionType.Selection:
self.current_selection = choice.value # type: ignore
match result.type_:
case ResultType.Selection:
selections = result.get_values()
self.current_selection = selections
return SelectResult.NewSelection
case MenuSelectionType.Skip:
case ResultType.Skip:
return SelectResult.SameSelection
case MenuSelectionType.Reset:
case ResultType.Reset:
return SelectResult.ResetCurrent
def post_install(self, install_session: 'Installer') -> None:

View File

@ -5,10 +5,16 @@ import readline
from pathlib import Path
from typing import Optional, Dict, Any, TYPE_CHECKING
from .menu import Menu, MenuSelectionType
from .storage import storage
from .general import JSON, UNSAFE_JSON
from .output import debug, info, warn
from .output import debug, warn
from .utils.util import prompt_dir
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
PreviewStyle, Orientation, Tui
)
if TYPE_CHECKING:
_: Any
@ -68,12 +74,35 @@ class ConfigurationOutput:
return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON)
return None
def show(self) -> None:
print(_('\nThis is your chosen configuration:'))
def write_debug(self) -> None:
debug(" -- Chosen configuration --")
debug(self.user_config_to_json())
info(self.user_config_to_json())
print()
def confirm_config(self) -> bool:
header = f'{str(_("The specified configuration will be applied"))}. '
header += str(_('Would you like to continue?')) + '\n'
with Tui():
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.yes()
group.set_preview_for_all(lambda x: self.user_config_to_json())
result = SelectMenu(
group,
header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
allow_skip=False,
preview_size='auto',
preview_style=PreviewStyle.BOTTOM,
preview_frame=FrameProperties.max(str(_('Configuration')))
).run()
if result.item() != MenuItem.yes():
return False
return True
def _is_valid_path(self, dest_path: Path) -> bool:
dest_path_ok = dest_path.exists() and dest_path.is_dir()
@ -105,9 +134,9 @@ class ConfigurationOutput:
self.save_user_creds(dest_path)
def save_config(config: Dict) -> None:
def preview(selection: str) -> Optional[str]:
match options[selection]:
def save_config(config: Dict[str, Any]) -> None:
def preview(item: MenuItem) -> Optional[str]:
match item.value:
case "user_config":
serialized = config_output.user_config_to_json()
return f"{config_output.user_configuration_file}\n{serialized}"
@ -122,60 +151,80 @@ def save_config(config: Dict) -> None:
return '\n'.join(output)
return None
try:
config_output = ConfigurationOutput(config)
config_output = ConfigurationOutput(config)
options = {
str(_("Save user configuration (including disk layout)")): "user_config",
str(_("Save user credentials")): "user_creds",
str(_("Save all")): "all",
}
items = [
MenuItem(
str(_("Save user configuration (including disk layout)")),
value="user_config",
preview_action=lambda x: preview(x)
),
MenuItem(
str(_("Save user credentials")),
value="user_creds",
preview_action=lambda x: preview(x)
),
MenuItem(
str(_("Save all")),
value="all",
preview_action=lambda x: preview(x)
)
]
save_choice = Menu(
_("Choose which configuration to save"),
list(options),
sort=False,
skip=True,
preview_size=0.75,
preview_command=preview,
).run()
group = MenuItemGroup(items)
result = SelectMenu(
group,
allow_skip=True,
preview_frame=FrameProperties.max(str(_('Configuration'))),
preview_size='auto',
preview_style=PreviewStyle.RIGHT
).run()
if save_choice.type_ == MenuSelectionType.Skip:
match result.type_:
case ResultType.Skip:
return
case ResultType.Selection:
save_option = result.get_value()
case _:
raise ValueError('Unhandled return type')
readline.set_completer_delims("\t\n=")
readline.parse_and_bind("tab: complete")
while True:
path = input(
_(
"Enter a directory for the configuration(s) to be saved (tab completion enabled)\nSave directory: "
)
).strip(" ")
dest_path = Path(path)
if dest_path.exists() and dest_path.is_dir():
break
info(_("Not a valid directory: {}").format(dest_path), fg="red")
readline.set_completer_delims("\t\n=")
readline.parse_and_bind("tab: complete")
if not path:
return
dest_path = prompt_dir(
str(_('Directory')),
str(_('Enter a directory for the configuration(s) to be saved (tab completion enabled)')) + '\n',
allow_skip=True
)
prompt = _(
"Do you want to save {} configuration file(s) in the following location?\n\n{}"
).format(options[str(save_choice.value)], dest_path.absolute())
save_confirmation = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run()
if save_confirmation == Menu.no():
return
debug("Saving {} configuration files to {}".format(options[str(save_choice.value)], dest_path.absolute()))
match options[str(save_choice.value)]:
case "user_config":
config_output.save_user_config(dest_path)
case "user_creds":
config_output.save_user_creds(dest_path)
case "all":
config_output.save(dest_path)
except (KeyboardInterrupt, EOFError):
if not dest_path:
return
header = str(_("Do you want to save the configuration file(s) to {}?")).format(dest_path)
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.yes()
result = SelectMenu(
group,
header=header,
allow_skip=False,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL
).run()
match result.type_:
case ResultType.Selection:
if result.item() == MenuItem.no():
return
debug("Saving configuration files to {}".format(dest_path.absolute()))
match save_option:
case "user_config":
config_output.save_user_config(dest_path)
case "user_creds":
config_output.save_user_creds(dest_path)
case "all":
config_output.save(dest_path)

View File

@ -7,11 +7,12 @@ from ..disk import (
)
from ..interactions import select_disk_config
from ..interactions.disk_conf import select_lvm_config
from ..menu import (
Selector,
AbstractSubMenu
)
from ..output import FormattedOutput
from ..menu import AbstractSubMenu
from archinstall.tui import (
MenuItemGroup, MenuItem
)
if TYPE_CHECKING:
_: Any
@ -21,37 +22,38 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
def __init__(
self,
disk_layout_config: Optional[DiskLayoutConfiguration],
data_store: Dict[str, Any],
advanced: bool = False
):
self._disk_layout_config = disk_layout_config
self._advanced = advanced
self._data_store: Dict[str, Any] = {}
super().__init__(data_store=data_store, preview_size=0.5)
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
def setup_selection_menu_options(self) -> None:
self._menu_options['disk_config'] = \
Selector(
_('Partitioning'),
lambda x: self._select_disk_layout_config(x),
display_func=lambda x: self._display_disk_layout(x),
preview_func=self._prev_disk_layouts,
default=self._disk_layout_config,
enabled=True
)
self._menu_options['lvm_config'] = \
Selector(
f'{_('LVM - Logical Volume Management')} (BETA)',
lambda x: self._select_lvm_config(x),
display_func=lambda x: self.defined_text if x else '',
preview_func=self._prev_lvm_config,
default=self._disk_layout_config.lvm_config if self._disk_layout_config else None,
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
def _define_menu_options(self) -> List[MenuItem]:
return [
MenuItem(
text=str(_('Partitioning')),
action=lambda x: self._select_disk_layout_config(x),
value=self._disk_layout_config,
preview_action=self._prev_disk_layouts,
key='disk_config'
),
MenuItem(
text='LVM (BETA)',
action=lambda x: self._select_lvm_config(x),
value=self._disk_layout_config.lvm_config if self._disk_layout_config else None,
preview_action=self._prev_lvm_config,
dependencies=[self._check_dep_lvm],
enabled=True
)
key='lvm_config'
),
]
def run(self, allow_reset: bool = True) -> Optional[DiskLayoutConfiguration]:
super().run(allow_reset=allow_reset)
def run(self) -> Optional[DiskLayoutConfiguration]:
super().run()
disk_layout_config: Optional[DiskLayoutConfiguration] = self._data_store.get('disk_config', None)
@ -61,7 +63,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
return disk_layout_config
def _check_dep_lvm(self) -> bool:
disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_item_group.find_by_key('disk_config').value
if disk_layout_conf and disk_layout_conf.config_type == DiskLayoutType.Default:
return True
@ -75,66 +77,72 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
disk_config = select_disk_config(preset, advanced_option=self._advanced)
if disk_config != preset:
self._menu_options['lvm_config'].set_current_selection(None)
self._menu_item_group.find_by_key('lvm_config').value = None
return disk_config
def _select_lvm_config(self, preset: Optional[LvmConfiguration]) -> Optional[LvmConfiguration]:
disk_config: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
disk_config: Optional[DiskLayoutConfiguration] = self._item_group.find_by_key('disk_config').value
if disk_config:
return select_lvm_config(disk_config, preset=preset)
return preset
def _display_disk_layout(self, current_value: Optional[DiskLayoutConfiguration] = None) -> str:
if current_value:
return current_value.config_type.display_msg()
return ''
def _prev_disk_layouts(self, item: MenuItem) -> Optional[str]:
if not item.value:
return None
def _prev_disk_layouts(self) -> Optional[str]:
disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
disk_layout_conf: DiskLayoutConfiguration = item.get_value()
if disk_layout_conf:
device_mods: List[DeviceModification] = \
list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications))
if disk_layout_conf.config_type == DiskLayoutType.Pre_mount:
msg = str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) + '\n'
msg += str(_('Mountpoint')) + ': ' + str(disk_layout_conf.mountpoint)
return msg
if device_mods:
output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg())
output_btrfs = ''
device_mods: List[DeviceModification] = \
list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications))
for mod in device_mods:
# create partition table
partition_table = FormattedOutput.as_table(mod.partitions)
if device_mods:
output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg())
output_btrfs = ''
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
output_partition += partition_table + '\n'
for mod in device_mods:
# create partition table
partition_table = FormattedOutput.as_table(mod.partitions)
# create btrfs table
btrfs_partitions = list(
filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions)
)
for partition in btrfs_partitions:
output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
output_partition += partition_table + '\n'
output = output_partition + output_btrfs
return output.rstrip()
# create btrfs table
btrfs_partitions = list(
filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions)
)
for partition in btrfs_partitions:
output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
output = output_partition + output_btrfs
return output.rstrip()
return None
def _prev_lvm_config(self) -> Optional[str]:
lvm_config: Optional[LvmConfiguration] = self._menu_options['lvm_config'].current_selection
def _prev_lvm_config(self, item: MenuItem) -> Optional[str]:
if not item.value:
return None
if lvm_config:
output = '{}: {}\n'.format(str(_('Configuration')), lvm_config.config_type.display_msg())
lvm_config: LvmConfiguration = item.value
for vol_gp in lvm_config.vol_groups:
pv_table = FormattedOutput.as_table(vol_gp.pvs)
output += '{}:\n{}'.format(str(_('Physical volumes')), pv_table)
output = '{}: {}\n'.format(str(_('Configuration')), lvm_config.config_type.display_msg())
output += f'\nVolume Group: {vol_gp.name}'
for vol_gp in lvm_config.vol_groups:
pv_table = FormattedOutput.as_table(vol_gp.pvs)
output += '{}:\n{}'.format(str(_('Physical volumes')), pv_table)
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(str(_('Volumes')), lvm_volumes)
output += f'\nVolume Group: {vol_gp.name}'
return output
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(str(_('Volumes')), lvm_volumes)
return output
return None

View File

@ -9,17 +9,17 @@ from ..disk import (
DiskEncryption,
EncryptionType
)
from ..menu import (
Selector,
AbstractSubMenu,
MenuSelectionType,
TableMenu
)
from ..interactions.utils import get_password
from ..menu import Menu
from ..general import secret
from ..menu import AbstractSubMenu
from .fido import Fido2Device, Fido2
from ..output import FormattedOutput
from ..utils.util import get_password
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType
)
from archinstall.lib.menu.menu_helper import MenuHelper
if TYPE_CHECKING:
_: Any
@ -29,7 +29,6 @@ class DiskEncryptionMenu(AbstractSubMenu):
def __init__(
self,
disk_config: DiskLayoutConfiguration,
data_store: Dict[str, Any],
preset: Optional[DiskEncryption] = None
):
if preset:
@ -37,57 +36,56 @@ class DiskEncryptionMenu(AbstractSubMenu):
else:
self._preset = DiskEncryption()
self._data_store: Dict[str, Any] = {}
self._disk_config = disk_config
super().__init__(data_store=data_store)
def setup_selection_menu_options(self) -> None:
self._menu_options['encryption_type'] = \
Selector(
_('Encryption type'),
func=lambda preset: select_encryption_type(self._disk_config, preset),
display_func=lambda x: EncryptionType.type_to_text(x) if x else None,
default=self._preset.encryption_type,
enabled=True,
)
self._menu_options['encryption_password'] = \
Selector(
_('Encryption password'),
lambda x: select_encrypted_password(),
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
def _define_menu_options(self) -> List[MenuItem]:
return [
MenuItem(
text=str(_('Encryption type')),
action=lambda x: select_encryption_type(self._disk_config, x),
value=self._preset.encryption_type,
preview_action=self._preview,
key='encryption_type'
),
MenuItem(
text=str(_('Encryption password')),
action=lambda x: select_encrypted_password(),
value=self._preset.encryption_password,
dependencies=[self._check_dep_enc_type],
display_func=lambda x: secret(x) if x else '',
default=self._preset.encryption_password,
enabled=True
)
self._menu_options['partitions'] = \
Selector(
_('Partitions'),
func=lambda preset: select_partitions_to_encrypt(self._disk_config.device_modifications, preset),
display_func=lambda x: f'{len(x)} {_("Partitions")}' if x else None,
preview_action=self._preview,
key='encryption_password'
),
MenuItem(
text=str(_('Partitions')),
action=lambda x: select_partitions_to_encrypt(self._disk_config.device_modifications, x),
value=self._preset.partitions,
dependencies=[self._check_dep_partitions],
default=self._preset.partitions,
preview_func=self._prev_partitions,
enabled=True
)
self._menu_options['lvm_vols'] = \
Selector(
_('LVM volumes'),
func=lambda preset: self._select_lvm_vols(preset),
display_func=lambda x: f'{len(x)} {_("LVM volumes")}' if x else None,
preview_action=self._preview,
key='partitions'
),
MenuItem(
text=str(_('LVM volumes')),
action=lambda x: self._select_lvm_vols(x),
value=self._preset.lvm_volumes,
dependencies=[self._check_dep_lvm_vols],
default=self._preset.lvm_volumes,
preview_func=self._prev_lvm_vols,
enabled=True
)
self._menu_options['HSM'] = \
Selector(
description=_('Use HSM to unlock encrypted drive'),
func=lambda preset: select_hsm(preset),
display_func=lambda x: self._display_hsm(x),
preview_func=self._prev_hsm,
preview_action=self._preview,
key='lvm_vols'
),
MenuItem(
text=str(_('HSM')),
action=lambda x: select_hsm(x),
value=self._preset.hsm_device,
dependencies=[self._check_dep_enc_type],
default=self._preset.hsm_device,
enabled=True
)
preview_action=self._preview,
key='HSM'
),
]
def _select_lvm_vols(self, preset: List[LvmVolume]) -> List[LvmVolume]:
if self._disk_config.lvm_config:
@ -95,30 +93,34 @@ class DiskEncryptionMenu(AbstractSubMenu):
return []
def _check_dep_enc_type(self) -> bool:
enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection
enc_type: Optional[EncryptionType] = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type != EncryptionType.NoEncryption:
return True
return False
def _check_dep_partitions(self) -> bool:
enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection
enc_type: Optional[EncryptionType] = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks]:
return True
return False
def _check_dep_lvm_vols(self) -> bool:
enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection
enc_type: Optional[EncryptionType] = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type == EncryptionType.LuksOnLvm:
return True
return False
def run(self, allow_reset: bool = True) -> Optional[DiskEncryption]:
super().run(allow_reset=allow_reset)
def run(self) -> Optional[DiskEncryption]:
super().run()
enc_type = self._data_store.get('encryption_type', None)
enc_password = self._data_store.get('encryption_password', None)
enc_partitions = self._data_store.get('partitions', None)
enc_lvm_vols = self._data_store.get('lvm_vols', None)
enc_type: Optional[EncryptionType] = self._item_group.find_by_key('encryption_type').value
enc_password: Optional[str] = self._item_group.find_by_key('encryption_password').value
enc_partitions = self._item_group.find_by_key('partitions').value
enc_lvm_vols = self._item_group.find_by_key('lvm_vols').value
assert enc_type is not None
assert enc_partitions is not None
assert enc_lvm_vols is not None
if enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and enc_partitions:
enc_lvm_vols = []
@ -137,14 +139,50 @@ class DiskEncryptionMenu(AbstractSubMenu):
return None
def _display_hsm(self, device: Optional[Fido2Device]) -> Optional[str]:
if device:
return device.manufacturer
def _preview(self, item: MenuItem) -> Optional[str]:
output = ''
if (enc_type := self._prev_type()) is not None:
output += enc_type
if (enc_pwd := self._prev_password()) is not None:
output += f'\n{enc_pwd}'
if (fido_device := self._prev_hsm()) is not None:
output += f'\n{fido_device}'
if (partitions := self._prev_partitions()) is not None:
output += f'\n\n{partitions}'
if (lvm := self._prev_lvm_vols()) is not None:
output += f'\n\n{lvm}'
if not output:
return None
return output
def _prev_type(self) -> Optional[str]:
enc_type = self._item_group.find_by_key('encryption_type').value
if enc_type:
enc_text = EncryptionType.type_to_text(enc_type)
return f'{str(_("Encryption type"))}: {enc_text}'
return None
def _prev_password(self) -> Optional[str]:
enc_pwd = self._item_group.find_by_key('encryption_password').value
if enc_pwd:
pwd_text = '*' * len(enc_pwd)
return f'{str(_("Encryption password"))}: {pwd_text}'
return None
def _prev_partitions(self) -> Optional[str]:
partitions: Optional[List[PartitionModification]] = self._menu_options['partitions'].current_selection
partitions: Optional[List[PartitionModification]] = self._item_group.find_by_key('partitions').value
if partitions:
output = str(_('Partitions to be encrypted')) + '\n'
output += FormattedOutput.as_table(partitions)
@ -153,7 +191,8 @@ class DiskEncryptionMenu(AbstractSubMenu):
return None
def _prev_lvm_vols(self) -> Optional[str]:
volumes: Optional[List[PartitionModification]] = self._menu_options['lvm_vols'].current_selection
volumes: Optional[List[PartitionModification]] = self._item_group.find_by_key('lvm_vols').value
if volumes:
output = str(_('LVM volumes to be encrypted')) + '\n'
output += FormattedOutput.as_table(volumes)
@ -162,51 +201,57 @@ class DiskEncryptionMenu(AbstractSubMenu):
return None
def _prev_hsm(self) -> Optional[str]:
try:
Fido2.get_fido2_devices()
except ValueError:
return str(_('Unable to determine fido2 devices. Is libfido2 installed?'))
fido_device: Optional[Fido2Device] = self._item_group.find_by_key('HSM').value
fido_device: Optional[Fido2Device] = self._menu_options['HSM'].current_selection
if not fido_device:
return None
if fido_device:
output = '{}: {}'.format(str(_('Path')), fido_device.path)
output += '{}: {}'.format(str(_('Manufacturer')), fido_device.manufacturer)
output += '{}: {}'.format(str(_('Product')), fido_device.product)
return output
return None
output = str(fido_device.path)
output += f' ({fido_device.manufacturer}, {fido_device.product})'
return f'{str(_("HSM device"))}: {output}'
def select_encryption_type(disk_config: DiskLayoutConfiguration, preset: EncryptionType) -> Optional[EncryptionType]:
title = str(_('Select disk encryption option'))
if disk_config.lvm_config:
options = [
EncryptionType.type_to_text(EncryptionType.LvmOnLuks),
EncryptionType.type_to_text(EncryptionType.LuksOnLvm)
]
else:
options = [EncryptionType.type_to_text(EncryptionType.Luks)]
options: List[EncryptionType] = []
preset_value = EncryptionType.type_to_text(preset)
choice = Menu(title, options, preset_values=preset_value).run()
if disk_config.lvm_config:
options = [EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm]
else:
options = [EncryptionType.Luks]
match choice.type_:
case MenuSelectionType.Reset: return None
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return EncryptionType.text_to_type(choice.value) # type: ignore
items = [MenuItem(EncryptionType.type_to_text(o), value=o) for o in options]
group = MenuItemGroup(items)
group.set_focus_by_value(preset_value)
result = SelectMenu(
group,
allow_skip=True,
allow_reset=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Encryption type')))
).run()
match result.type_:
case ResultType.Reset: return None
case ResultType.Skip: return preset
case ResultType.Selection:
return result.get_value()
def select_encrypted_password() -> Optional[str]:
if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))):
return passwd
return None
header = str(_('Enter disk encryption password (leave blank for no encryption)')) + '\n'
password = get_password(
text=str(_('Disk encryption password')),
header=header,
allow_skip=True
)
return password
def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]:
title = _('Select a FIDO2 device to use for HSM')
header = str(_('Select a FIDO2 device to use for HSM'))
try:
fido_devices = Fido2.get_fido2_devices()
@ -214,14 +259,20 @@ def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]:
return None
if fido_devices:
choice = TableMenu(title, data=fido_devices).run()
match choice.type_:
case MenuSelectionType.Reset:
return None
case MenuSelectionType.Skip:
return preset
case MenuSelectionType.Selection:
return choice.value # type: ignore
group, table_header = MenuHelper.create_table(data=fido_devices)
header = f'{header}\n\n{table_header}'
result = SelectMenu(
group,
header=header,
alignment=Alignment.CENTER,
).run()
match result.type_:
case ResultType.Reset: return None
case ResultType.Skip: return preset
case ResultType.Selection:
return result.get_value()
return None
@ -240,23 +291,22 @@ def select_partitions_to_encrypt(
avail_partitions = list(filter(lambda x: not x.exists(), partitions))
if avail_partitions:
title = str(_('Select which partitions to encrypt'))
partition_table = FormattedOutput.as_table(avail_partitions)
group, header = MenuHelper.create_table(data=avail_partitions)
choice = TableMenu(
title,
table_data=(avail_partitions, partition_table),
preset=preset,
result = SelectMenu(
group,
header=header,
alignment=Alignment.CENTER,
multi=True
).run()
match choice.type_:
case MenuSelectionType.Reset:
return []
case MenuSelectionType.Skip:
return preset
case MenuSelectionType.Selection:
return choice.multi_value
match result.type_:
case ResultType.Reset: return []
case ResultType.Skip: return preset
case ResultType.Selection:
partitions = result.get_values()
return partitions
return []
@ -267,22 +317,20 @@ def select_lvm_vols_to_encrypt(
volumes: List[LvmVolume] = lvm_config.get_all_volumes()
if volumes:
title = str(_('Select which LVM volumes to encrypt'))
partition_table = FormattedOutput.as_table(volumes)
group, header = MenuHelper.create_table(data=volumes)
choice = TableMenu(
title,
table_data=(volumes, partition_table),
preset=preset,
result = SelectMenu(
group,
header=header,
alignment=Alignment.CENTER,
multi=True
).run()
match choice.type_:
case MenuSelectionType.Reset:
return []
case MenuSelectionType.Skip:
return preset
case MenuSelectionType.Selection:
return choice.multi_value
match result.type_:
case ResultType.Reset: return []
case ResultType.Skip: return preset
case ResultType.Selection:
volumes = result.get_values()
return volumes
return []

View File

@ -1,11 +1,10 @@
from __future__ import annotations
import signal
import sys
import time
from pathlib import Path
from typing import Any, Optional, TYPE_CHECKING, List, Dict, Set
from ..interactions.general_conf import ask_abort
from .device_handler import device_handler
from .device_model import (
DiskLayoutConfiguration, DiskLayoutType, PartitionTable,
@ -15,8 +14,11 @@ from .device_model import (
)
from ..hardware import SysInfo
from ..luks import Luks2
from ..menu import Menu
from ..output import debug, info
from archinstall.tui import (
Tui
)
if TYPE_CHECKING:
_: Any
@ -44,12 +46,8 @@ class FilesystemHandler:
device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods])
# Issue a final warning before we continue with something un-revertable.
# We mention the drive one last time, and count from 5 to 0.
print(str(_(' ! Formatting {} in ')).format(device_paths))
if show_countdown:
self._do_countdown()
self._final_warning(device_paths)
# Setup the blockdevice, filesystem (and optionally encryption).
# Once that's done, we'll hand over to perform_installation()
@ -339,40 +337,19 @@ class FilesystemHandler:
Size(256, Unit.MiB, SectorSize.default())
)
def _do_countdown(self) -> bool:
SIG_TRIGGER = False
def _final_warning(self, device_paths: str) -> bool:
# Issue a final warning before we continue with something un-revertable.
# We mention the drive one last time, and count from 5 to 0.
out = str(_(' ! Formatting {} in ')).format(device_paths)
Tui.print(out, row=0, endl='', clear_screen=True)
def kill_handler(sig: int, frame: Any) -> None:
print()
exit(0)
def sig_handler(sig: int, frame: Any) -> None:
signal.signal(signal.SIGINT, kill_handler)
original_sigint_handler = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGINT, sig_handler)
for i in range(5, 0, -1):
print(f"{i}", end='')
for x in range(4):
sys.stdout.flush()
try:
countdown = '\n5...4...3...2...1'
for c in countdown:
Tui.print(c, row=0, endl='')
time.sleep(0.25)
print(".", end='')
if SIG_TRIGGER:
prompt = _('Do you really want to abort?')
choice = Menu(prompt, Menu.yes_no(), skip=False).run()
if choice.value == Menu.yes():
exit(0)
if SIG_TRIGGER is False:
sys.stdin.read()
SIG_TRIGGER = False
signal.signal(signal.SIGINT, sig_handler)
print()
signal.signal(signal.SIGINT, original_sigint_handler)
except KeyboardInterrupt:
with Tui():
ask_abort()
return True

View File

@ -5,16 +5,23 @@ from pathlib import Path
from typing import Any, TYPE_CHECKING, List, Optional, Tuple
from dataclasses import dataclass
from ..utils.util import prompt_dir
from .device_model import (
PartitionModification, FilesystemType, BDevice,
Size, Unit, PartitionType, PartitionFlag,
ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption
)
from ..hardware import SysInfo
from ..menu import Menu, ListManager, MenuSelection, TextInput
from ..output import FormattedOutput, warn
from ..menu import ListManager
from ..output import FormattedOutput
from .subvolume_menu import SubvolumeMenu
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, EditMenu,
Orientation, ResultType
)
if TYPE_CHECKING:
_: Any
@ -26,9 +33,6 @@ class DefaultFreeSector:
class PartitioningList(ListManager):
"""
subclass of ListManager for the managing of user accounts
"""
def __init__(self, prompt: str, device: BDevice, device_partitions: List[PartitionModification]):
self._device = device
self._actions = {
@ -49,7 +53,10 @@ class PartitioningList(ListManager):
super().__init__(prompt, device_partitions, display_actions[:2], display_actions[3:])
def selected_action_display(self, partition: PartitionModification) -> str:
return str(_('Partition'))
if partition.status == ModificationStatus.Create:
return str(_('Partition - New'))
else:
return str(partition.dev_path)
def filter_options(self, selection: PartitionModification, options: List[str]) -> List[str]:
not_filter = []
@ -100,8 +107,7 @@ class PartitioningList(ListManager):
if len(new_partitions) > 0:
data = new_partitions
case 'remove_added_partitions':
choice = self._reset_confirmation()
if choice.value == Menu.yes():
if self._reset_confirmation():
data = [part for part in data if part.is_exists_or_modify()]
case 'assign_mountpoint' if entry:
entry.mountpoint = self._prompt_mountpoint()
@ -169,7 +175,7 @@ class PartitioningList(ListManager):
def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None:
partition.btrfs_subvols = SubvolumeMenu(
_("Manage btrfs subvolumes for current partition"),
str(_("Manage btrfs subvolumes for current partition")),
partition.btrfs_subvols
).run()
@ -185,7 +191,7 @@ class PartitioningList(ListManager):
# without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set,
# it's safe to change the filesystem for this partition.
if partition.fs_type == FilesystemType.Crypto_luks:
prompt = str(_('This partition is currently encrypted, to format it a filesystem has to be specified'))
prompt = str(_('This partition is currently encrypted, to format it a filesystem has to be specified')) + '\n'
fs_type = self._prompt_partition_fs_type(prompt)
partition.fs_type = fs_type
@ -195,25 +201,31 @@ class PartitioningList(ListManager):
def _prompt_mountpoint(self) -> Path:
header = str(_('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.')) + '\n'
header += str(_('If mountpoint /boot is set, then the partition will also be marked as bootable.')) + '\n'
prompt = str(_('Mountpoint: '))
prompt = str(_('Mountpoint'))
print(header)
while True:
value = TextInput(prompt).run().strip()
if value:
mountpoint = Path(value)
break
mountpoint = prompt_dir(prompt, header, allow_skip=False)
assert mountpoint
return mountpoint
def _prompt_partition_fs_type(self, prompt: str = '') -> FilesystemType:
options = {fs.value: fs for fs in FilesystemType if fs != FilesystemType.Crypto_luks}
def _prompt_partition_fs_type(self, prompt: Optional[str] = None) -> FilesystemType:
fs_types = filter(lambda fs: fs != FilesystemType.Crypto_luks, FilesystemType)
items = [MenuItem(fs.value, value=fs) for fs in fs_types]
group = MenuItemGroup(items, sort_items=False)
prompt = prompt + '\n' + str(_('Enter a desired filesystem type for the partition'))
choice = Menu(prompt, options, sort=False, skip=False).run()
return options[choice.single_value]
result = SelectMenu(
group,
header=prompt,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Filesystem'))),
allow_skip=False
).run()
match result.type_:
case ResultType.Selection:
return result.get_value()
case _:
raise ValueError('Unhandled result type')
def _validate_value(
self,
@ -246,22 +258,34 @@ class PartitioningList(ListManager):
self,
sector_size: SectorSize,
total_size: Size,
prompt: str,
text: str,
header: str,
default: Size,
start: Optional[Size],
) -> Size:
while True:
value = TextInput(prompt).run().strip()
size: Optional[Size] = None
if not value:
size = default
else:
size = self._validate_value(sector_size, total_size, value, start)
def validate(value: str) -> Optional[str]:
size = self._validate_value(sector_size, total_size, value, start)
if not size:
return str(_('Invalid size'))
return None
if size:
return size
result = EditMenu(
text,
header=f'{header}\b',
allow_skip=True,
validator=validate
).input()
warn(f'Invalid value: {value}')
size: Optional[Size] = None
value = result.text()
if value is None:
size = default
else:
size = self._validate_value(sector_size, total_size, value, start)
assert size
return size
def _prompt_size(self) -> Tuple[Size, Size]:
device_info = self._device.device_info
@ -276,7 +300,6 @@ class PartitioningList(ListManager):
prompt += str(_('Total: {} / {}')).format(total_sectors, total_bytes) + '\n\n'
prompt += str(_('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...')) + '\n'
prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n'
print(prompt)
default_free_sector = self._find_default_free_space()
@ -287,27 +310,32 @@ class PartitioningList(ListManager):
)
# prompt until a valid start sector was entered
start_prompt = str(_('Enter start (default: sector {}): ')).format(default_free_sector.start.value)
start_text = str(_('Start (default: sector {}): ')).format(default_free_sector.start.value)
start_size = self._enter_size(
device_info.sector_size,
device_info.total_size,
start_prompt,
start_text,
prompt,
default_free_sector.start,
None
)
prompt += f'\nStart: {start_size.as_text()}\n'
if start_size.value == default_free_sector.start.value and default_free_sector.end.value != 0:
end_size = default_free_sector.end
else:
end_size = device_info.total_size
# prompt until valid end sector was entered
end_prompt = str(_('Enter end (default: {}): ')).format(end_size.as_text())
end_text = str(_('End (default: {}): ')).format(end_size.as_text())
end_size = self._enter_size(
device_info.sector_size,
device_info.total_size,
end_prompt,
end_text,
prompt,
end_size,
start_size
)
@ -358,9 +386,6 @@ class PartitioningList(ListManager):
start_size, end_size = self._prompt_size()
length = end_size - start_size
# new line for the next prompt
print()
mountpoint = None
if fs_type != FilesystemType.Btrfs:
mountpoint = self._prompt_mountpoint()
@ -381,17 +406,26 @@ class PartitioningList(ListManager):
return partition
def _reset_confirmation(self) -> MenuSelection:
prompt = str(_('This will remove all newly added partitions, continue?'))
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run()
return choice
def _reset_confirmation(self) -> bool:
prompt = str(_('This will remove all newly added partitions, continue?')) + '\n'
result = SelectMenu(
MenuItemGroup.yes_no(),
header=prompt,
alignment=Alignment.CENTER,
orientation=Orientation.HORIZONTAL,
columns=2,
reset_warning_msg=prompt,
allow_skip=False
).run()
return result.item() == MenuItem.yes()
def _suggest_partition_layout(self, data: List[PartitionModification]) -> List[PartitionModification]:
# if modifications have been done already, inform the user
# that this operation will erase those modifications
if any([not entry.exists() for entry in data]):
choice = self._reset_confirmation()
if choice.value == Menu.no():
if not self._reset_confirmation():
return []
from ..interactions.disk_conf import suggest_single_disk_layout

View File

@ -2,7 +2,12 @@ from pathlib import Path
from typing import List, Optional, Any, TYPE_CHECKING
from .device_model import SubvolumeModification
from ..menu import TextInput, ListManager
from ..menu import ListManager
from ..utils.util import prompt_dir
from archinstall.tui import (
Alignment, EditMenu, ResultType
)
if TYPE_CHECKING:
_: Any
@ -20,18 +25,34 @@ class SubvolumeMenu(ListManager):
def selected_action_display(self, subvolume: SubvolumeModification) -> str:
return str(subvolume.name)
def _add_subvolume(self, editing: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]:
name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run()
def _add_subvolume(self, preset: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]:
result = EditMenu(
str(_('Subvolume name')),
alignment=Alignment.CENTER,
allow_skip=True,
default_text=str(preset.name) if preset else None
).input()
if not name:
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
name = result.text()
case ResultType.Reset:
raise ValueError('Unhandled result type')
header = f"{str(_('Subvolume name'))}: {name}\n"
path = prompt_dir(
str(_("Subvolume mountpoint")),
header=header,
allow_skip=True
)
if not path:
return None
mountpoint = TextInput(f'{_("Subvolume mountpoint")}: ', str(editing.mountpoint) if editing else '').run()
if not mountpoint:
return None
return SubvolumeModification(Path(name), Path(mountpoint))
return SubvolumeModification(Path(name), path)
def handle_action(
self,

View File

@ -6,29 +6,32 @@ from . import disk
from .general import secret
from .hardware import SysInfo
from .locale.locale_menu import LocaleConfiguration, LocaleMenu
from .menu import Selector, AbstractMenu
from .menu import AbstractMenu
from .mirrors import MirrorConfiguration, MirrorMenu
from .models import NetworkConfiguration, NicType
from .models.bootloader import Bootloader
from .models.audio_configuration import Audio, AudioConfiguration
from .models.audio_configuration import AudioConfiguration
from .models.users import User
from .output import FormattedOutput
from .profile.profile_menu import ProfileConfiguration
from .configuration import save_config
from .interactions import add_number_of_parallel_downloads
from .interactions import ask_additional_packages_to_install
from .interactions import ask_for_additional_users
from .interactions import ask_for_audio_selection
from .interactions import ask_for_bootloader
from .interactions import ask_for_uki
from .interactions import ask_for_swap
from .interactions import ask_hostname
from .interactions import ask_to_configure_network
from .interactions import get_password, ask_for_a_timezone
from .interactions import select_additional_repositories
from .interactions import select_kernel
from .interactions import (
ask_for_audio_selection, ask_for_swap,
ask_for_bootloader, ask_for_uki, ask_hostname,
add_number_of_parallel_downloads, select_kernel,
ask_additional_packages_to_install, select_additional_repositories,
ask_for_a_timezone, ask_ntp, ask_to_configure_network
)
from .utils.util import get_password
from .utils.util import format_cols
from .interactions import ask_ntp
from .configuration import save_config
from archinstall.tui import (
MenuItemGroup, MenuItem
)
from .translationhandler import Language, TranslationHandler
if TYPE_CHECKING:
_: Any
@ -36,175 +39,211 @@ if TYPE_CHECKING:
class GlobalMenu(AbstractMenu):
def __init__(self, data_store: Dict[str, Any]):
super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3)
self._data_store = data_store
self._translation_handler = TranslationHandler()
def setup_selection_menu_options(self) -> None:
# archinstall.Language will not use preset values
self._menu_options['archinstall-language'] = \
Selector(
_('Archinstall language'),
lambda x: self._select_archinstall_language(x),
display_func=lambda x: x.display_name,
default=self.translation_handler.get_language_by_abbr('en'))
self._menu_options['locale_config'] = \
Selector(
_('Locales'),
lambda preset: self._locale_selection(preset),
preview_func=self._prev_locale,
display_func=lambda x: self.defined_text if x else '')
self._menu_options['mirror_config'] = \
Selector(
_('Mirrors'),
lambda preset: self._mirror_configuration(preset),
display_func=lambda x: self.defined_text if x else '',
preview_func=self._prev_mirror_config
)
self._menu_options['disk_config'] = \
Selector(
_('Disk configuration'),
lambda preset: self._select_disk_config(preset),
preview_func=self._prev_disk_config,
display_func=lambda x: self.defined_text if x else '',
)
self._menu_options['disk_encryption'] = \
Selector(
_('Disk encryption'),
lambda preset: self._disk_encryption(preset),
preview_func=self._prev_disk_encryption,
display_func=lambda x: self._display_disk_encryption(x),
if 'archinstall-language' not in data_store:
data_store['archinstall-language'] = self._translation_handler.get_language_by_abbr('en')
menu_optioons = self._get_menu_options(data_store)
self._item_group = MenuItemGroup(
menu_optioons,
sort_items=False,
checkmarks=True
)
super().__init__(self._item_group, data_store)
def _get_menu_options(self, data_store: Dict[str, Any]) -> List[MenuItem]:
return [
MenuItem(
text=str(_('Archinstall language')),
action=lambda x: self._select_archinstall_language(x),
display_action=lambda x: x.display_name if x else '',
key='archinstall-language'
),
MenuItem(
text=str(_('Locales')),
action=lambda x: self._locale_selection(x),
preview_action=self._prev_locale,
key='locale_config'
),
MenuItem(
text=str(_('Mirrors')),
action=lambda x: self._mirror_configuration(x),
preview_action=self._prev_mirror_config,
key='mirror_config'
),
MenuItem(
text=str(_('Disk configuration')),
action=lambda x: self._select_disk_config(x),
preview_action=self._prev_disk_config,
mandatory=True,
key='disk_config'
),
MenuItem(
text=str(_('Disk encryption')),
action=lambda x: self._disk_encryption(x),
preview_action=self._prev_disk_encryption,
key='disk_encryption',
dependencies=['disk_config']
),
MenuItem(
text=str(_('Swap')),
value=True,
action=lambda x: ask_for_swap(x),
preview_action=self._prev_swap,
key='swap',
),
MenuItem(
text=str(_('Bootloader')),
value=Bootloader.get_default(),
action=lambda x: self._select_bootloader(x),
preview_action=self._prev_bootloader,
mandatory=True,
key='bootloader',
),
MenuItem(
text=str(_('Unified kernel images')),
value=False,
action=lambda x: ask_for_uki(x),
preview_action=self._prev_uki,
key='uki',
),
MenuItem(
text=str(_('Hostname')),
value='archlinux',
action=lambda x: ask_hostname(x),
preview_action=self._prev_hostname,
key='hostname',
),
MenuItem(
text=str(_('Root password')),
action=lambda x: self._set_root_password(x),
preview_action=self._prev_root_pwd,
key='!root-password',
),
MenuItem(
text=str(_('User account')),
action=lambda x: self._create_user_account(x),
preview_action=self._prev_users,
key='!users'
),
MenuItem(
text=str(_('Profile')),
action=lambda x: self._select_profile(x),
preview_action=self._prev_profile,
key='profile_config'
),
MenuItem(
text=str(_('Audio')),
action=lambda x: ask_for_audio_selection(x),
preview_action=self._prev_audio,
key='audio_config'
),
MenuItem(
text=str(_('Kernels')),
value=['linux'],
action=lambda x: select_kernel(x),
preview_action=self._prev_kernel,
mandatory=True,
key='kernels'
),
MenuItem(
text=str(_('Network configuration')),
action=lambda x: ask_to_configure_network(x),
value={},
preview_action=self._prev_network_config,
key='network_config'
),
MenuItem(
text=str(_('Parallel Downloads')),
action=lambda x: add_number_of_parallel_downloads(x),
value=0,
preview_action=self._prev_parallel_dw,
key='parallel downloads'
),
MenuItem(
text=str(_('Additional packages')),
action=lambda x: ask_additional_packages_to_install(x),
value=[],
preview_action=self._prev_additional_pkgs,
key='packages'
),
MenuItem(
text=str(_('Optional repositories')),
action=lambda x: select_additional_repositories(x),
value=[],
preview_action=self._prev_additional_repos,
key='additional-repositories'
),
MenuItem(
text=str(_('Timezone')),
action=lambda x: ask_for_a_timezone(x),
value='UTC',
preview_action=self._prev_tz,
key='timezone'
),
MenuItem(
text=str(_('Automatic time sync (NTP)')),
action=lambda x: ask_ntp(x),
value=True,
preview_action=self._prev_ntp,
key='ntp'
),
MenuItem(
text=''
),
MenuItem(
text=str(_('Save configuration')),
action=lambda x: self._safe_config(),
key='save_config'
),
MenuItem(
text=str(_('Install')),
preview_action=self._prev_install_invalid_config,
key='install'
),
MenuItem(
text=str(_('Abort')),
action=lambda x: exit(1),
key='abort'
)
self._menu_options['swap'] = \
Selector(
_('Swap'),
lambda preset: ask_for_swap(preset),
default=True)
self._menu_options['bootloader'] = \
Selector(
_('Bootloader'),
lambda preset: ask_for_bootloader(preset),
display_func=lambda x: x.value,
default=Bootloader.get_default())
self._menu_options['uki'] = \
Selector(
_('Unified kernel images'),
lambda preset: ask_for_uki(preset),
default=False)
self._menu_options['hostname'] = \
Selector(
_('Hostname'),
lambda preset: ask_hostname(preset),
default='archlinux')
# root password won't have preset value
self._menu_options['!root-password'] = \
Selector(
_('Root password'),
lambda preset: self._set_root_password(),
display_func=lambda x: secret(x) if x else '')
self._menu_options['!users'] = \
Selector(
_('User account'),
lambda x: self._create_user_account(x),
default=[],
display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else '',
preview_func=self._prev_users)
self._menu_options['profile_config'] = \
Selector(
_('Profile'),
lambda preset: self._select_profile(preset),
display_func=lambda x: x.profile.name if x else '',
preview_func=self._prev_profile
)
self._menu_options['audio_config'] = \
Selector(
_('Audio'),
lambda preset: self._select_audio(preset),
display_func=lambda x: self._display_audio(x)
)
self._menu_options['parallel downloads'] = \
Selector(
_('Parallel Downloads'),
lambda preset: add_number_of_parallel_downloads(preset),
display_func=lambda x: x if x else '0',
default=0
)
self._menu_options['kernels'] = \
Selector(
_('Kernels'),
lambda preset: select_kernel(preset),
display_func=lambda x: ', '.join(x) if x else None,
default=['linux'])
self._menu_options['packages'] = \
Selector(
_('Additional packages'),
lambda preset: ask_additional_packages_to_install(preset),
display_func=lambda x: self.defined_text if x else '',
preview_func=self._prev_additional_pkgs,
default=[])
self._menu_options['additional-repositories'] = \
Selector(
_('Optional repositories'),
lambda preset: select_additional_repositories(preset),
display_func=lambda x: ', '.join(x) if x else None,
default=[])
self._menu_options['network_config'] = \
Selector(
_('Network configuration'),
lambda preset: ask_to_configure_network(preset),
display_func=lambda x: self._display_network_conf(x),
preview_func=self._prev_network_config,
default={})
self._menu_options['timezone'] = \
Selector(
_('Timezone'),
lambda preset: ask_for_a_timezone(preset),
default='UTC')
self._menu_options['ntp'] = \
Selector(
_('Automatic time sync (NTP)'),
lambda preset: ask_ntp(preset),
default=True)
self._menu_options['__separator__'] = \
Selector('')
self._menu_options['save_config'] = \
Selector(
_('Save configuration'),
lambda preset: save_config(self._data_store),
no_store=True)
self._menu_options['install'] = \
Selector(
self._install_text(),
exec_func=lambda n, v: self._is_config_valid(),
preview_func=self._prev_install_invalid_config,
no_store=True)
]
self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n, v: exit(1))
def _safe_config(self) -> None:
data: Dict[str, Any] = {}
for item in self._item_group.items:
if item.key is not None:
data[item.key] = item.value
save_config(data)
def _missing_configs(self) -> List[str]:
def check(s: str) -> bool:
obj = self._menu_options.get(s)
if obj and obj.has_selection():
return True
return False
def check(s) -> bool:
item = self._item_group.find_by_key(s)
return item.has_value()
def has_superuser() -> bool:
sel = self._menu_options['!users']
if sel.current_selection:
return any([u.sudo for u in sel.current_selection])
item = self._item_group.find_by_key('!users')
if item.has_value():
users = item.value
if users:
return any([u.sudo for u in users])
return False
mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items()))
missing = set()
for key, selector in mandatory_fields.items():
if key in ['!root-password', '!users']:
for item in self._item_group.items:
if item.key in ['!root-password', '!users']:
if not check('!root-password') and not has_superuser():
missing.add(
str(_('Either root-password or at least 1 user with sudo privileges must be specified'))
)
elif key == 'disk_config':
if not check('disk_config'):
missing.add(self._menu_options['disk_config'].description)
elif item.mandatory:
if not check(item.key):
missing.add(item.text)
return list(missing)
@ -216,36 +255,28 @@ class GlobalMenu(AbstractMenu):
return False
return self._validate_bootloader() is None
def _update_uki_display(self, name: Optional[str] = None) -> None:
if bootloader := self._menu_options['bootloader'].current_selection:
if not SysInfo.has_uefi() or not bootloader.has_uki_support():
self._menu_options['uki'].set_current_selection(False)
self._menu_options['uki'].set_enabled(False)
elif name and name == 'bootloader':
self._menu_options['uki'].set_enabled(True)
def _select_archinstall_language(self, preset: Language) -> Language:
from .interactions.general_conf import select_archinstall_language
language = select_archinstall_language(self._translation_handler.translated_languages, preset)
self._translation_handler.activate(language)
def _update_install_text(self, name: Optional[str] = None, value: Any = None) -> None:
text = self._install_text()
self._menu_options['install'].update_description(text)
self._upate_lang_text()
def post_callback(self, name: Optional[str] = None, value: Any = None) -> None:
self._update_uki_display(name)
self._update_install_text(name, value)
return language
def _install_text(self) -> str:
missing = len(self._missing_configs())
if missing > 0:
return _('Install ({} config(s) missing)').format(missing)
return _('Install')
def _upate_lang_text(self) -> None:
"""
The options for the global menu are generated with a static text;
each entry of the menu needs to be updated with the new translation
"""
new_options = self._get_menu_options(self._data_store)
def _display_network_conf(self, config: Optional[NetworkConfiguration]) -> str:
if not config:
return str(_('Not configured, unavailable unless setup manually'))
return config.type.display_msg()
for o in new_options:
if o.key is not None:
self._item_group.find_by_key(o.key).text = o.text
def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]:
disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
disk_config: Optional[disk.DiskLayoutConfiguration] = self._item_group.find_by_key('disk_config').value
if not disk_config:
# this should not happen as the encryption menu has the disk_config as dependency
@ -254,91 +285,140 @@ class GlobalMenu(AbstractMenu):
if not disk.DiskEncryption.validate_enc(disk_config):
return None
data_store: Dict[str, Any] = {}
disk_encryption = disk.DiskEncryptionMenu(disk_config, data_store, preset=preset).run()
disk_encryption = disk.DiskEncryptionMenu(disk_config, preset=preset).run()
return disk_encryption
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
data_store: Dict[str, Any] = {}
locale_config = LocaleMenu(data_store, preset).run()
locale_config = LocaleMenu(preset).run()
return locale_config
def _prev_locale(self) -> Optional[str]:
selector = self._menu_options['locale_config']
if selector.has_selection():
config: LocaleConfiguration = selector.current_selection # type: ignore
output = '{}: {}\n'.format(str(_('Keyboard layout')), config.kb_layout)
output += '{}: {}\n'.format(str(_('Locale language')), config.sys_lang)
output += '{}: {}'.format(str(_('Locale encoding')), config.sys_enc)
def _prev_locale(self, item: MenuItem) -> Optional[str]:
if not item.value:
return None
config: LocaleConfiguration = item.value
return config.preview()
def _prev_network_config(self, item: MenuItem) -> Optional[str]:
if item.value:
network_config: NetworkConfiguration = item.value
if network_config.type == NicType.MANUAL:
output = FormattedOutput.as_table(network_config.nics)
else:
output = f'{str(_('Network configuration'))}:\n{network_config.type.display_msg()}'
return output
return None
def _prev_network_config(self) -> Optional[str]:
selector: Optional[NetworkConfiguration] = self._menu_options['network_config'].current_selection
if selector:
if selector.type == NicType.MANUAL:
output = FormattedOutput.as_table(selector.nics)
return output
def _prev_additional_pkgs(self, item: MenuItem) -> Optional[str]:
if item.value:
return format_cols(item.value, None)
return None
def _prev_additional_pkgs(self) -> Optional[str]:
selector = self._menu_options['packages']
if selector.current_selection:
packages: List[str] = selector.current_selection
return format_cols(packages, None)
def _prev_additional_repos(self, item: MenuItem) -> Optional[str]:
if item.value:
repos = ', '.join(item.value)
return f'{str(_("Additional repositories"))}: {repos}'
return None
def _prev_disk_config(self) -> Optional[str]:
selector = self._menu_options['disk_config']
disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection
def _prev_tz(self, item: MenuItem) -> Optional[str]:
if item.value:
return f'{str(_("Timezone"))}: {item.value}'
return None
def _prev_ntp(self, item: MenuItem) -> Optional[str]:
if item.value is not None:
output = f'{str(_("NTP"))}: '
output += str(_('Enabled')) if item.value else str(_('Disabled'))
return output
return None
def _prev_disk_config(self, item: MenuItem) -> Optional[str]:
disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = item.value
output = ''
if disk_layout_conf:
output += str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg())
output = str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) + '\n'
if disk_layout_conf.config_type == disk.DiskLayoutType.Pre_mount:
output += str(_('Mountpoint')) + ': ' + str(disk_layout_conf.mountpoint)
if disk_layout_conf.lvm_config:
output += '\n{}: {}'.format(str(_('LVM configuration type')), disk_layout_conf.lvm_config.config_type.display_msg())
output += '{}: {}'.format(str(_('LVM configuration type')), disk_layout_conf.lvm_config.config_type.display_msg())
if output:
return output
return None
def _display_disk_config(self, current_value: Optional[disk.DiskLayoutConfiguration] = None) -> str:
if current_value:
return current_value.config_type.display_msg()
return ''
def _prev_swap(self, item: MenuItem) -> Optional[str]:
if item.value is not None:
output = f'{str(_("Swap on zram"))}: '
output += str(_('Enabled')) if item.value else str(_('Disabled'))
return output
return None
def _prev_disk_encryption(self) -> Optional[str]:
disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
def _prev_uki(self, item: MenuItem) -> Optional[str]:
if item.value is not None:
output = f'{str(_('Unified kernel images'))}: '
output += str(_('Enabled')) if item.value else str(_('Disabled'))
return output
return None
def _prev_hostname(self, item: MenuItem) -> Optional[str]:
if item.value is not None:
return f'{str(_("Hostname"))}: {item.value}'
return None
def _prev_root_pwd(self, item: MenuItem) -> Optional[str]:
if item.value is not None:
return f'{str(_("Root password"))}: {secret(item.value)}'
return None
def _prev_audio(self, item: MenuItem) -> Optional[str]:
if item.value is not None:
config: AudioConfiguration = item.value
return f'{str(_("Audio"))}: {config.audio.value}'
return None
def _prev_parallel_dw(self, item: MenuItem) -> Optional[str]:
if item.value is not None:
return f'{str(_("Parallel Downloads"))}: {item.value}'
return None
def _prev_kernel(self, item: MenuItem) -> Optional[str]:
if item.value:
kernel = ', '.join(item.value)
return f'{str(_("Kernel"))}: {kernel}'
return None
def _prev_bootloader(self, item: MenuItem) -> Optional[str]:
if item.value is not None:
return f'{str(_("Bootloader"))}: {item.value.value}'
return None
def _prev_disk_encryption(self, item: MenuItem) -> Optional[str]:
disk_config: Optional[disk.DiskLayoutConfiguration] = self._item_group.find_by_key('disk_config').value
enc_config: Optional[disk.DiskEncryption] = item.value
if disk_config and not disk.DiskEncryption.validate_enc(disk_config):
return str(_('LVM disk encryption with more than 2 partitions is currently not supported'))
encryption: Optional[disk.DiskEncryption] = self._menu_options['disk_encryption'].current_selection
if encryption:
enc_type = disk.EncryptionType.type_to_text(encryption.encryption_type)
if enc_config:
enc_type = disk.EncryptionType.type_to_text(enc_config.encryption_type)
output = str(_('Encryption type')) + f': {enc_type}\n'
output += str(_('Password')) + f': {secret(encryption.encryption_password)}\n'
output += str(_('Password')) + f': {secret(enc_config.encryption_password)}\n'
if encryption.partitions:
output += 'Partitions: {} selected'.format(len(encryption.partitions)) + '\n'
elif encryption.lvm_volumes:
output += 'LVM volumes: {} selected'.format(len(encryption.lvm_volumes)) + '\n'
if enc_config.partitions:
output += 'Partitions: {} selected'.format(len(enc_config.partitions)) + '\n'
elif enc_config.lvm_volumes:
output += 'LVM volumes: {} selected'.format(len(enc_config.lvm_volumes)) + '\n'
if encryption.hsm_device:
output += f'HSM: {encryption.hsm_device.manufacturer}'
if enc_config.hsm_device:
output += f'HSM: {enc_config.hsm_device.manufacturer}'
return output
return None
def _display_disk_encryption(self, current_value: Optional[disk.DiskEncryption]) -> str:
if current_value:
return disk.EncryptionType.type_to_text(current_value.encryption_type)
return ''
def _validate_bootloader(self) -> Optional[str]:
"""
Checks the selected bootloader is valid for the selected filesystem
@ -350,10 +430,10 @@ class GlobalMenu(AbstractMenu):
XXX: The caller is responsible for wrapping the string with the translation
shim if necessary.
"""
bootloader = self._menu_options['bootloader'].current_selection
bootloader = self._item_group.find_by_key('bootloader').value
boot_partition: Optional[disk.PartitionModification] = None
if disk_config := self._menu_options['disk_config'].current_selection:
if disk_config := self._item_group.find_by_key('disk_config').value:
for layout in disk_config.device_modifications:
if boot_partition := layout.get_boot_partition():
break
@ -369,7 +449,7 @@ class GlobalMenu(AbstractMenu):
return None
def _prev_install_invalid_config(self) -> Optional[str]:
def _prev_install_invalid_config(self, item: MenuItem) -> Optional[str]:
if missing := self._missing_configs():
text = str(_('Missing configurations:\n'))
for m in missing:
@ -381,17 +461,15 @@ class GlobalMenu(AbstractMenu):
return None
def _prev_users(self) -> Optional[str]:
selector = self._menu_options['!users']
users: Optional[List[User]] = selector.current_selection
def _prev_users(self, item: MenuItem) -> Optional[str]:
users: Optional[List[User]] = item.value
if users:
return FormattedOutput.as_table(users)
return None
def _prev_profile(self) -> Optional[str]:
selector = self._menu_options['profile_config']
profile_config: Optional[ProfileConfiguration] = selector.current_selection
def _prev_profile(self, item: MenuItem) -> Optional[str]:
profile_config: Optional[ProfileConfiguration] = item.value
if profile_config and profile_config.profile:
output = str(_('Profiles')) + ': '
@ -410,63 +488,59 @@ class GlobalMenu(AbstractMenu):
return None
def _set_root_password(self) -> Optional[str]:
prompt = str(_('Enter root password (leave blank to disable root): '))
password = get_password(prompt=prompt)
def _set_root_password(self, preset: Optional[str] = None) -> Optional[str]:
password = get_password(text=str(_('Root password')), allow_skip=True)
return password
def _select_disk_config(
self,
preset: Optional[disk.DiskLayoutConfiguration] = None
) -> Optional[disk.DiskLayoutConfiguration]:
data_store: Dict[str, Any] = {}
disk_config = disk.DiskLayoutConfigurationMenu(preset, data_store).run()
disk_config = disk.DiskLayoutConfigurationMenu(preset).run()
if disk_config != preset:
self._menu_options['disk_encryption'].set_current_selection(None)
self._menu_item_group.find_by_key('disk_encryption').value = None
return disk_config
def _select_bootloader(self, preset: Optional[Bootloader]) -> Optional[Bootloader]:
bootloader = ask_for_bootloader(preset)
if bootloader:
uki = self._item_group.find_by_key('uki')
if not SysInfo.has_uefi() or not bootloader.has_uki_support():
uki.value = False
uki.enabled = False
else:
uki.enabled = True
return bootloader
def _select_profile(self, current_profile: Optional[ProfileConfiguration]):
from .profile.profile_menu import ProfileMenu
store: Dict[str, Any] = {}
profile_config = ProfileMenu(store, preset=current_profile).run()
profile_config = ProfileMenu(preset=current_profile).run()
return profile_config
def _select_audio(
self,
current: Optional[AudioConfiguration] = None
) -> Optional[AudioConfiguration]:
selection = ask_for_audio_selection(current)
return selection
def _display_audio(self, current: Optional[AudioConfiguration]) -> str:
if not current:
return Audio.no_audio_text()
else:
return current.audio.name
def _create_user_account(self, defined_users: List[User]) -> List[User]:
users = ask_for_additional_users(defined_users=defined_users)
def _create_user_account(self, preset: Optional[List[User]] = None) -> List[User]:
preset = [] if preset is None else preset
users = ask_for_additional_users(defined_users=preset)
return users
def _mirror_configuration(self, preset: Optional[MirrorConfiguration] = None) -> Optional[MirrorConfiguration]:
data_store: Dict[str, Any] = {}
mirror_configuration = MirrorMenu(data_store, preset=preset).run()
mirror_configuration = MirrorMenu(preset=preset).run()
return mirror_configuration
def _prev_mirror_config(self) -> Optional[str]:
selector = self._menu_options['mirror_config']
def _prev_mirror_config(self, item: MenuItem) -> Optional[str]:
if not item.value:
return None
if selector.has_selection():
mirror_config: MirrorConfiguration = selector.current_selection # type: ignore
output = ''
if mirror_config.regions:
output += '{}: {}\n\n'.format(str(_('Mirror regions')), mirror_config.regions)
if mirror_config.custom_mirrors:
table = FormattedOutput.as_table(mirror_config.custom_mirrors)
output += '{}\n{}'.format(str(_('Custom mirrors')), table)
mirror_config: MirrorConfiguration = item.value
return output.strip()
output = ''
if mirror_config.regions:
output += '{}: {}\n\n'.format(str(_('Mirror regions')), mirror_config.regions)
if mirror_config.custom_mirrors:
table = FormattedOutput.as_table(mirror_config.custom_mirrors)
output += '{}\n{}'.format(str(_('Custom mirrors')), table)
return None
return output.strip()

View File

@ -8,7 +8,6 @@ from .exceptions import SysCallError
from .general import SysCommand
from .networking import list_interfaces, enrich_iface_types
from .output import debug
from .utils.util import format_cols
if TYPE_CHECKING:
_: Any
@ -78,9 +77,12 @@ class GfxDriver(Enum):
return False
def packages_text(self) -> str:
text = str(_('Installed packages')) + ':\n'
pkg_names = [p.value for p in self.gfx_packages()]
text += format_cols(sorted(pkg_names))
text = str(_('Installed packages')) + ':\n'
for p in sorted(pkg_names):
text += f'\t- {p}\n'
return text
def gfx_packages(self) -> List[GfxPackage]:

View File

@ -24,6 +24,7 @@ from . import pacman
from .pacman import Pacman
from .plugins import plugins
from .storage import storage
from archinstall.tui.curses_menu import Tui
if TYPE_CHECKING:
_: Any
@ -105,9 +106,9 @@ class Installer:
# We avoid printing /mnt/<log path> because that might confuse people if they note it down
# and then reboot, and a identical log file will be found in the ISO medium anyway.
print(_("[!] A log file has been created here: {}").format(
os.path.join(storage['LOG_PATH'], storage['LOG_FILE'])))
print(_(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues"))
log_file = os.path.join(storage['LOG_PATH'], storage['LOG_FILE'])
Tui.print(str(_("[!] A log file has been created here: {}").format(log_file)))
Tui.print(str(_('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues')))
raise exc_val
if not (missing_steps := self.post_install_check()):

View File

@ -1,6 +1,5 @@
from .manage_users_conf import UserList, ask_for_additional_users
from .network_menu import ManualNetworkConfig, ask_to_configure_network
from .utils import get_password
from .disk_conf import (
select_devices, select_disk_config, get_default_partition_layout,

View File

@ -7,25 +7,22 @@ from typing import Optional, List, Tuple
from .. import disk
from ..disk.device_model import BtrfsMountOption
from ..hardware import SysInfo
from ..menu import Menu
from ..menu import TableMenu
from ..menu.menu import MenuSelectionType
from ..output import FormattedOutput, debug
from ..utils.util import prompt_dir
from ..storage import storage
from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
Orientation
)
if TYPE_CHECKING:
_: Any
def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]:
"""
Asks the user to select one or multiple devices
:return: List of selected devices
:rtype: list
"""
def select_devices(preset: Optional[List[disk.BDevice]] = []) -> List[disk.BDevice]:
def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]:
dev = disk.device_handler.get_device(selection.path)
if dev and dev.partition_infos:
@ -35,30 +32,25 @@ def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]:
if preset is None:
preset = []
title = str(_('Select one or more devices to use and configure'))
warning = str(_('If you reset the device selection this will also reset the current disk layout. Are you sure?'))
devices = disk.device_handler.devices
options = [d.device_info for d in devices]
preset_value = [p.device_info for p in preset]
presets = [p.device_info for p in preset]
choice = TableMenu(
title,
data=options,
multi=True,
preset=preset_value,
preview_command=_preview_device_selection,
preview_title=str(_('Existing Partitions')),
preview_size=0.2,
allow_reset=True,
allow_reset_warning_msg=warning
group, header = MenuHelper.create_table(data=options)
group.set_selected_by_value(presets)
result = SelectMenu(
group,
header=header,
alignment=Alignment.CENTER,
search_enabled=False,
multi=True
).run()
match choice.type_:
case MenuSelectionType.Reset: return []
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection:
selected_device_info: List[disk._DeviceInfo] = choice.single_value
match result.type_:
case ResultType.Reset: return []
case ResultType.Skip: return preset
case ResultType.Selection:
selected_device_info: List[disk._DeviceInfo] = result.get_values()
selected_devices = []
for device in devices:
@ -113,35 +105,40 @@ def select_disk_config(
manual_mode = disk.DiskLayoutType.Manual.display_msg()
pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg()
options = [default_layout, manual_mode, pre_mount_mode]
preset_value = preset.config_type.display_msg() if preset else None
warning = str(_('Are you sure you want to reset this setting?'))
items = [
MenuItem(default_layout, value=default_layout),
MenuItem(manual_mode, value=manual_mode),
MenuItem(pre_mount_mode, value=pre_mount_mode)
]
group = MenuItemGroup(items, sort_items=False)
choice = Menu(
_('Select a partitioning option'),
options,
allow_reset=True,
allow_reset_warning_msg=warning,
sort=False,
preview_size=0.2,
preset_values=preset_value
if preset:
group.set_selected_by_value(preset.config_type.display_msg())
result = SelectMenu(
group,
allow_skip=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Disk configuration type'))),
allow_reset=True
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Reset: return None
case MenuSelectionType.Selection:
if choice.single_value == pre_mount_mode:
match result.type_:
case ResultType.Skip: return preset
case ResultType.Reset: return None
case ResultType.Selection:
selection = result.get_value()
if selection == pre_mount_mode:
output = 'You will use whatever drive-setup is mounted at the specified directory\n'
output += "WARNING: Archinstall won't check the suitability of this setup\n"
try:
path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output)
except (KeyboardInterrupt, EOFError):
return preset
path = prompt_dir(str(_('Root mount directory')), output, allow_skip=False)
assert path is not None
mods = disk.device_handler.detect_pre_mounted_mods(path)
storage['MOUNT_POINT'] = Path(path)
storage['MOUNT_POINT'] = path
return disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Pre_mount,
@ -155,14 +152,14 @@ def select_disk_config(
if not devices:
return None
if choice.value == default_layout:
if result.get_value() == default_layout:
modifications = get_default_partition_layout(devices, advanced_option=advanced_option)
if modifications:
return disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Default,
device_modifications=modifications
)
elif choice.value == manual_mode:
elif result.get_value() == manual_mode:
preset_mods = preset.device_modifications if preset else []
modifications = _manual_partitioning(preset_mods, devices)
@ -179,30 +176,29 @@ def select_lvm_config(
disk_config: disk.DiskLayoutConfiguration,
preset: Optional[disk.LvmConfiguration] = None,
) -> Optional[disk.LvmConfiguration]:
preset_value = preset.config_type.display_msg() if preset else None
default_mode = disk.LvmLayoutType.Default.display_msg()
options = [default_mode]
items = [MenuItem(default_mode, value=default_mode)]
group = MenuItemGroup(items)
group.set_focus_by_value(preset_value)
preset_value = preset.config_type.display_msg() if preset else None
warning = str(_('Are you sure you want to reset this setting?'))
choice = Menu(
_('Select a LVM option'),
options,
result = SelectMenu(
group,
allow_reset=True,
allow_reset_warning_msg=warning,
sort=False,
preview_size=0.2,
preset_values=preset_value
allow_skip=True,
frame=FrameProperties.min(str(_('LVM configuration type'))),
alignment=Alignment.CENTER
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Reset: return None
case MenuSelectionType.Selection:
if choice.single_value == default_mode:
match result.type_:
case ResultType.Skip: return preset
case ResultType.Reset: return None
case ResultType.Selection:
if result.get_value() == default_mode:
return suggest_lvm_layout(disk_config)
return preset
return None
def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.PartitionModification:
@ -227,33 +223,56 @@ def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.Parti
def select_main_filesystem_format(advanced_options: bool = False) -> disk.FilesystemType:
options = {
'btrfs': disk.FilesystemType.Btrfs,
'ext4': disk.FilesystemType.Ext4,
'xfs': disk.FilesystemType.Xfs,
'f2fs': disk.FilesystemType.F2fs
}
items = [
MenuItem('btrfs', value=disk.FilesystemType.Btrfs),
MenuItem('ext4', value=disk.FilesystemType.Ext4),
MenuItem('xfs', value=disk.FilesystemType.Xfs),
MenuItem('f2fs', value=disk.FilesystemType.F2fs)
]
if advanced_options:
options.update({'ntfs': disk.FilesystemType.Ntfs})
items.append(MenuItem('ntfs', value=disk.FilesystemType.Ntfs))
prompt = _('Select which filesystem your main partition should use')
choice = Menu(prompt, options, skip=False, sort=False).run()
return options[choice.single_value]
group = MenuItemGroup(items, sort_items=False)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min('Filesystem'),
allow_skip=False
).run()
match result.type_:
case ResultType.Selection:
return result.get_value()
case _:
raise ValueError('Unhandled result type')
def select_mount_options() -> List[str]:
prompt = str(_('Would you like to use compression or disable CoW?'))
options = [str(_('Use compression')), str(_('Disable Copy-on-Write'))]
choice = Menu(prompt, options, sort=False).run()
prompt = str(_('Would you like to use compression or disable CoW?')) + '\n'
compression = str(_('Use compression'))
disable_cow = str(_('Disable Copy-on-Write'))
if choice.type_ == MenuSelectionType.Selection:
if choice.single_value == options[0]:
return [BtrfsMountOption.compress.value]
else:
return [BtrfsMountOption.nodatacow.value]
items = [
MenuItem(compression, value=BtrfsMountOption.compress.value),
MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value),
]
group = MenuItemGroup(items, sort_items=False)
result = SelectMenu(
group,
header=prompt,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
search_enabled=False,
allow_skip=False
).run()
return []
match result.type_:
case ResultType.Selection:
return [result.get_value()]
case _:
raise ValueError('Unhandled result type')
def process_root_partition_size(total_size: disk.Size, sector_size: disk.SectorSize) -> disk.Size:
@ -286,9 +305,19 @@ def suggest_single_disk_layout(
min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB, sector_size)
if filesystem_type == disk.FilesystemType.Btrfs:
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
using_subvolumes = choice.value == Menu.yes()
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(MenuItem.yes().value)
result = SelectMenu(
group,
header=prompt,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
allow_skip=False
).run()
using_subvolumes = result.item() == MenuItem.yes()
mount_options = select_mount_options()
else:
using_subvolumes = False
@ -325,9 +354,19 @@ def suggest_single_disk_layout(
elif separate_home:
using_home_partition = True
else:
prompt = str(_('Would you like to create a separate partition for /home?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
using_home_partition = choice.value == Menu.yes()
prompt = str(_('Would you like to create a separate partition for /home?')) + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(MenuItem.yes().value)
result = SelectMenu(
group,
header=prompt,
orientation=Orientation.HORIZONTAL,
columns=2,
alignment=Alignment.CENTER,
allow_skip=False
).run()
using_home_partition = result.item() == MenuItem.yes()
# root partition
root_start = boot_partition.start + boot_partition.length
@ -417,10 +456,14 @@ def suggest_multi_disk_layout(
root_device: Optional[disk.BDevice] = sorted_delta[0][0]
if home_device is None or root_device is None:
text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n')
text += _('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB))
text += _('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB))
Menu(str(text), [str(_('Continue'))], skip=False).run()
text = str(_('The selected drives do not have the minimum capacity required for an automatic suggestion\n'))
text += str(_('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)))
text += str(_('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)))
items = [MenuItem(str(_('Continue')))]
group = MenuItemGroup(items)
SelectMenu(group).run()
return []
if filesystem_type == disk.FilesystemType.Btrfs:
@ -503,10 +546,21 @@ def suggest_lvm_layout(
filesystem_type = select_main_filesystem_format()
if filesystem_type == disk.FilesystemType.Btrfs:
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
using_subvolumes = choice.value == Menu.yes()
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(MenuItem.yes().value)
result = SelectMenu(
group,
header=prompt,
search_enabled=False,
allow_skip=False,
orientation=Orientation.HORIZONTAL,
columns=2,
alignment=Alignment.CENTER,
).run()
using_subvolumes = MenuItem.yes() == result.item()
mount_options = select_mount_options()
if using_subvolumes:

View File

@ -4,86 +4,114 @@ import pathlib
from typing import List, Any, Optional, TYPE_CHECKING
from ..locale import list_timezones
from ..menu import MenuSelectionType, Menu, TextInput
from ..models.audio_configuration import Audio, AudioConfiguration
from ..output import warn
from ..packages.packages import validate_package_list
from ..storage import storage
from ..translationhandler import Language
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
EditMenu, Orientation, Tui
)
if TYPE_CHECKING:
_: Any
def ask_ntp(preset: bool = True) -> bool:
prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n'))
prompt += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki'))
if preset:
preset_val = Menu.yes()
else:
preset_val = Menu.no()
choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run()
header = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) + '\n'
header += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki')) + '\n'
return False if choice.value == Menu.no() else True
preset_val = MenuItem.yes() if preset else MenuItem.no()
group = MenuItemGroup.yes_no()
group.focus_item = preset_val
result = SelectMenu(
group,
header=header,
allow_skip=True,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.item() == MenuItem.yes()
case _:
raise ValueError('Unhandled return type')
def ask_hostname(preset: str = '') -> str:
hostname = TextInput(
str(_('Desired hostname for the installation: ')),
preset
).run().strip()
def ask_hostname(preset: Optional[str] = None) -> Optional[str]:
result = EditMenu(
str(_('Hostname')),
alignment=Alignment.CENTER,
allow_skip=True,
default_text=preset
).input()
if not hostname:
return preset
return hostname
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
hostname = result.text()
if len(hostname) < 1:
return None
return hostname
case ResultType.Reset:
raise ValueError('Unhandled result type')
def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]:
timezones = list_timezones()
default = 'UTC'
timezones = list_timezones()
choice = Menu(
_('Select a timezone'),
timezones,
preset_values=preset,
default_option=default
items = [MenuItem(tz, value=tz) for tz in timezones]
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(preset)
group.set_default_by_value(default)
result = SelectMenu(
group,
allow_reset=True,
allow_skip=True,
frame=FrameProperties.min(str(_('Timezone'))),
alignment=Alignment.CENTER,
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return choice.single_value
return None
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return default
case ResultType.Selection:
return result.get_value()
def ask_for_audio_selection(
current: Optional[AudioConfiguration] = None
) -> Optional[AudioConfiguration]:
choices = [
Audio.Pipewire.name, # pylint: disable=no-member
Audio.Pulseaudio.name, # pylint: disable=no-member
Audio.no_audio_text()
]
def ask_for_audio_selection(preset: Optional[AudioConfiguration] = None) -> Optional[AudioConfiguration]:
items = [MenuItem(a.value, value=a) for a in Audio]
group = MenuItemGroup(items)
preset = current.audio.name if current else None
if preset:
group.set_focus_by_value(preset.audio)
choice = Menu(
_('Choose an audio server'),
choices,
preset_values=preset
result = SelectMenu(
group,
allow_skip=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Audio')))
).run()
match choice.type_:
case MenuSelectionType.Skip: return current
case MenuSelectionType.Selection:
value = choice.single_value
if value == Audio.no_audio_text():
return None
else:
return AudioConfiguration(Audio[value])
return None
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return AudioConfiguration(audio=result.get_value())
case ResultType.Reset:
raise ValueError('Unhandled result type')
def select_language(preset: Optional[str] = None) -> Optional[str]:
@ -94,7 +122,8 @@ def select_language(preset: Optional[str] = None) -> Optional[str]:
# raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.")
# No need to translate this i feel, as it's a short lived message.
warn("select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version")
warn(
"select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version")
return select_kb_layout(preset)
@ -103,70 +132,112 @@ def select_archinstall_language(languages: List[Language], preset: Language) ->
# these are the displayed language names which can either be
# the english name of a language or, if present, the
# name of the language in its own language
options = {lang.display_name: lang for lang in languages}
items = [MenuItem(lang.display_name, lang) for lang in languages]
group = MenuItemGroup(items, sort_items=True)
group.set_focus_by_value(preset)
title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n'
title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n'
title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n'
choice = Menu(
title,
list(options.keys()),
default_option=preset.display_name,
preview_size=0.5
result = SelectMenu(
group,
header=title,
allow_skip=True,
allow_reset=False,
alignment=Alignment.CENTER,
frame=FrameProperties.min(header=str(_('Select language')))
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return options[choice.single_value]
raise ValueError('Language selection not handled')
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Language selection not handled')
def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]:
# 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(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.'))
header = str(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) + '\n'
header += str(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) + '\n'
header += str(_('Write additional packages to install (space separated, leave blank to skip)'))
def read_packages(p: list[str] = []) -> list[str]:
display = ' '.join(p)
input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip()
return input_packages.split() if input_packages else []
def validator(value: str) -> Optional[str]:
packages = value.split() if value else []
preset = preset if preset else []
packages = read_packages(preset)
if len(packages) == 0:
return None
if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']:
while True:
if len(packages):
# Verify packages that were given
print(_("Verifying that additional packages exist (this might take a few seconds)"))
valid, invalid = validate_package_list(packages)
if storage['arguments']['offline'] or storage['arguments']['no_pkg_lookups']:
return None
if invalid:
warn(f"Some packages could not be found in the repository: {invalid}")
packages = read_packages(valid)
continue
break
# Verify packages that were given
out = str(_("Verifying that additional packages exist (this might take a few seconds)"))
Tui.print(out, 0)
valid, invalid = validate_package_list(packages)
return packages
if invalid:
return f'{str(_("Some packages could not be found in the repository"))}: {invalid}'
return None
result = EditMenu(
str(_('Additional packages')),
alignment=Alignment.CENTER,
allow_skip=True,
allow_reset=True,
edit_width=100,
validator=validator,
default_text=' '.join(preset)
).input()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return []
case ResultType.Selection:
packages = result.text()
return packages.split(' ')
def add_number_of_parallel_downloads(input_number: Optional[int] = None) -> Optional[int]:
def add_number_of_parallel_downloads(preset: Optional[int] = None) -> Optional[int]:
max_recommended = 5
print(_("This option enables the number of parallel downloads that can occur during package downloads"))
print(_("Enter the number of parallel downloads to be enabled.\n\nNote:\n"))
print(str(_(" - Maximum recommended value : {} ( Allows {} parallel downloads at a time )")).format(max_recommended, max_recommended))
print(_(" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n"))
while True:
header = str(_('This option enables the number of parallel downloads that can occur during package downloads')) + '\n'
header += str(_('Enter the number of parallel downloads to be enabled.\n\nNote:\n'))
header += str(_(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )')).format(max_recommended, max_recommended) + '\n'
header += str(_(' - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n'))
def validator(s: str) -> Optional[str]:
try:
input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0)
if input_number <= 0:
input_number = 0
break
except:
print(str(_("Invalid input! Try again with a valid input [or 0 to disable]")).format(max_recommended))
value = int(s)
if value >= 0:
return None
except Exception:
pass
return str(_('Invalid download number'))
result = EditMenu(
str(_('Number downloads')),
header=header,
allow_skip=True,
allow_reset=True,
validator=validator,
default_text=str(preset) if preset is not None else None
).input()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return 0
case ResultType.Selection:
downloads: int = int(result.text())
pacman_conf_path = pathlib.Path("/etc/pacman.conf")
with pacman_conf_path.open() as f:
@ -175,11 +246,11 @@ def add_number_of_parallel_downloads(input_number: Optional[int] = None) -> Opti
with pacman_conf_path.open("w") as fwrite:
for line in pacman_conf:
if "ParallelDownloads" in line:
fwrite.write(f"ParallelDownloads = {input_number}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n")
fwrite.write(f"ParallelDownloads = {downloads}\n")
else:
fwrite.write(f"{line}\n")
return input_number
return downloads
def select_additional_repositories(preset: List[str]) -> List[str]:
@ -191,19 +262,55 @@ def select_additional_repositories(preset: List[str]) -> List[str]:
"""
repositories = ["multilib", "testing"]
items = [MenuItem(r, value=r) for r in repositories]
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(preset)
choice = Menu(
_('Choose which optional additional repositories to enable'),
repositories,
sort=False,
multi=True,
preset_values=preset,
allow_reset=True
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min('Additional repositories'),
allow_reset=True,
allow_skip=True,
multi=True
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Reset: return []
case MenuSelectionType.Selection: return choice.single_value
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return []
case ResultType.Selection:
return result.get_values()
return []
def ask_chroot() -> bool:
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) + '\n'
group = MenuItemGroup.yes_no()
result = SelectMenu(
group,
header=prompt,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
).run()
return result.item() == MenuItem.yes()
def ask_abort() -> None:
prompt = str(_('Do you really want to abort?')) + '\n'
group = MenuItemGroup.yes_no()
result = SelectMenu(
group,
header=prompt,
allow_skip=False,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL
).run()
if result.item() == MenuItem.yes():
exit(0)

View File

@ -3,19 +3,22 @@ from __future__ import annotations
import re
from typing import Any, TYPE_CHECKING, List, Optional
from .utils import get_password
from ..menu import Menu, ListManager
from ..utils.util import get_password
from ..menu import ListManager
from ..models.users import User
from ..general import secret
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
Alignment, EditMenu, Orientation,
ResultType
)
if TYPE_CHECKING:
_: Any
class UserList(ListManager):
"""
subclass of ListManager for the managing of user accounts
"""
def __init__(self, prompt: str, lusers: List[User]):
self._actions = [
str(_('Add a user')),
@ -37,8 +40,9 @@ class UserList(ListManager):
data = [d for d in data if d.username != new_user.username]
data += [new_user]
elif action == self._actions[1] and entry: # change password
prompt = str(_('Password for user "{}": ').format(entry.username))
new_password = get_password(prompt=prompt)
header = f'{str(_("User"))}: {entry.username}\n'
new_password = get_password(str(_('Password')), header=header)
if new_password:
user = next(filter(lambda x: x == entry, data))
user.password = new_password
@ -50,42 +54,55 @@ class UserList(ListManager):
return data
def _check_for_correct_username(self, username: str) -> bool:
def _check_for_correct_username(self, username: str) -> Optional[str]:
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
return True
return False
return None
return str(_("The username you entered is invalid"))
def _add_user(self) -> Optional[User]:
prompt = '\n\n' + str(_('Enter username (leave blank to skip): '))
editResult = EditMenu(
str(_('Username')),
allow_skip=True,
validator=self._check_for_correct_username
).input()
while True:
try:
username = input(prompt).strip(' ')
except (KeyboardInterrupt, EOFError):
match editResult.type_:
case ResultType.Skip:
return None
case ResultType.Selection:
username = editResult.text()
case _:
raise ValueError('Unhandled result type')
if not username:
return None
if not self._check_for_correct_username(username):
error_prompt = str(_("The username you entered is invalid. Try again"))
print(error_prompt)
else:
break
header = f'{str(_("Username"))}: {username}\n'
password = get_password(prompt=str(_('Password for user "{}": ').format(username)))
password = get_password(str(_('Password')), header=header, allow_skip=True)
if not password:
return None
choice = Menu(
str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(),
skip=False,
default_option=Menu.yes(),
clear_screen=False,
show_search_hint=False
header += f'{str(_("Password"))}: {secret(password)}\n\n'
header += str(_('Should "{}" be a superuser (sudo)?\n')).format(username)
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.yes()
result = SelectMenu(
group,
header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
search_enabled=False,
allow_skip=False
).run()
sudo = True if choice.value == Menu.yes() else False
match result.type_:
case ResultType.Selection:
sudo = result.item() == MenuItem.yes()
case _:
raise ValueError('Unhandled result type')
return User(username, password, sudo)

View File

@ -1,24 +1,23 @@
from __future__ import annotations
import ipaddress
from typing import Any, Optional, TYPE_CHECKING, List, Dict
from typing import Any, Optional, TYPE_CHECKING, List
from ..menu import MenuSelectionType, TextInput
from ..models.network_configuration import NetworkConfiguration, NicType, Nic
from ..networking import list_interfaces
from ..output import FormattedOutput, warn
from ..menu import ListManager, Menu
from ..menu import ListManager
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
EditMenu
)
if TYPE_CHECKING:
_: Any
class ManualNetworkConfig(ListManager):
"""
subclass of ListManager for the managing of network configurations
"""
def __init__(self, prompt: str, preset: List[Nic]):
self._actions = [
str(_('Add interface')),
@ -27,21 +26,6 @@ class ManualNetworkConfig(ListManager):
]
super().__init__(prompt, preset, [self._actions[0]], self._actions[1:])
def reformat(self, data: List[Nic]) -> Dict[str, Optional[Nic]]:
table = FormattedOutput.as_table(data)
rows = table.split('\n')
# these are the header rows of the table and do not map to any User obviously
# we're adding 2 spaces as prefix because the menu selector '> ' will be put before
# the selectable rows so the header has to be aligned
display_data: Dict[str, Optional[Nic]] = {f' {rows[0]}': None, f' {rows[1]}': None}
for row, iface in zip(rows[2:], data):
row = row.replace('|', '\\|')
display_data[row] = iface
return display_data
def selected_action_display(self, nic: Nic) -> str:
return nic.iface if nic.iface else ''
@ -69,56 +53,112 @@ class ManualNetworkConfig(ListManager):
if not available:
return None
choice = Menu(str(_('Select interface to add')), list(available), skip=True).run()
if choice.type_ == MenuSelectionType.Skip:
if not available:
return None
return choice.single_value
items = [MenuItem(i, value=i) for i in available]
group = MenuItemGroup(items, sort_items=True)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Interfaces'))),
allow_skip=True
).run()
match result.type_:
case ResultType.Skip:
return None
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
def _get_ip_address(
self,
title: str,
header: str,
allow_skip: bool,
multi: bool,
preset: Optional[str] = None
) -> Optional[str]:
def validator(ip: str) -> Optional[str]:
if multi:
ips = ip.split(' ')
else:
ips = [ip]
try:
for ip in ips:
ipaddress.ip_interface(ip)
return None
except ValueError:
return str(_('You need to enter a valid IP in IP-config mode'))
result = EditMenu(
title,
header=header,
validator=validator,
allow_skip=allow_skip,
default_text=preset
).input()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.text()
case ResultType.Reset:
raise ValueError('Unhandled result type')
def _edit_iface(self, edit_nic: Nic) -> Nic:
iface_name = edit_nic.iface
modes = ['DHCP (auto detect)', 'IP (static)']
default_mode = 'DHCP (auto detect)'
prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode)
mode = Menu(prompt, modes, default_option=default_mode, skip=False).run()
header = str(_('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode)) + '\n'
items = [MenuItem(m, value=m) for m in modes]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default_mode)
if mode.value == 'IP (static)':
while 1:
prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name)
ip = TextInput(prompt, edit_nic.ip).run().strip()
# Implemented new check for correct IP/subnet input
try:
ipaddress.ip_interface(ip)
break
except ValueError:
warn("You need to enter a valid IP in IP-config mode")
result = SelectMenu(
group,
header=header,
allow_skip=False,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Modes')))
).run()
# Implemented new check for correct gateway IP address
gateway = None
match result.type_:
case ResultType.Selection:
mode = result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
while 1:
gateway = TextInput(
_('Enter your gateway (router) IP address or leave blank for none: '),
edit_nic.gateway
).run().strip()
try:
if len(gateway) > 0:
ipaddress.ip_address(gateway)
break
except ValueError:
warn("You need to enter a valid gateway (router) IP address")
if mode == 'IP (static)':
header = str(_('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name)) + '\n'
ip = self._get_ip_address(str(_('IP address')), header, False, False)
header = str(_('Enter your gateway (router) IP address (leave blank for none)')) + '\n'
gateway = self._get_ip_address(str(_('Gateway address')), header, True, False)
if edit_nic.dns:
display_dns = ' '.join(edit_nic.dns)
else:
display_dns = None
dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip()
header = str(_('Enter your DNS servers with space separated (leave blank for none)')) + '\n'
dns_servers = self._get_ip_address(
str(_('DNS servers')),
header,
True,
True,
display_dns
)
dns = []
if len(dns_input):
dns = dns_input.split(' ')
if dns_servers is not None:
dns = dns_servers.split(' ')
return Nic(iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False)
else:
@ -128,35 +168,40 @@ class ManualNetworkConfig(ListManager):
def ask_to_configure_network(preset: Optional[NetworkConfiguration]) -> Optional[NetworkConfiguration]:
"""
Configure the network on the newly installed system
Configure the network on the newly installed system
"""
options = {n.display_msg(): n for n in NicType}
preset_val = preset.type.display_msg() if preset else None
warning = str(_('Are you sure you want to reset this setting?'))
choice = Menu(
_('Select one network interface to configure'),
list(options.keys()),
preset_values=preset_val,
sort=False,
items = [MenuItem(n.display_msg(), value=n) for n in NicType]
group = MenuItemGroup(items, sort_items=True)
if preset:
group.set_selected_by_value(preset.type)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Network configuration'))),
allow_reset=True,
allow_reset_warning_msg=warning
allow_skip=True
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Reset: return None
case MenuSelectionType.Selection:
nic_type = options[choice.single_value]
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
config = result.get_value()
match nic_type:
match config:
case NicType.ISO:
return NetworkConfiguration(NicType.ISO)
case NicType.NM:
return NetworkConfiguration(NicType.NM)
case NicType.MANUAL:
preset_nics = preset.nics if preset else []
nics = ManualNetworkConfig('Configure interfaces', preset_nics).run()
nics = ManualNetworkConfig(str(_('Configure interfaces')), preset_nics).run()
if nics:
return NetworkConfiguration(NicType.MANUAL, nics)

View File

@ -3,9 +3,14 @@ from __future__ import annotations
from typing import List, Any, TYPE_CHECKING, Optional
from ..hardware import SysInfo, GfxDriver
from ..menu import MenuSelectionType, Menu
from ..models.bootloader import Bootloader
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, FrameStyle, Alignment,
ResultType, Orientation, PreviewStyle
)
if TYPE_CHECKING:
_: Any
@ -17,71 +22,88 @@ def select_kernel(preset: List[str] = []) -> List[str]:
:return: The string as a selected kernel
:rtype: string
"""
kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"]
default_kernel = "linux"
warning = str(_('Are you sure you want to reset this setting?'))
items = [MenuItem(k, value=k) for k in kernels]
choice = Menu(
_('Choose which kernels to use or leave blank for default "{}"').format(default_kernel),
kernels,
sort=True,
multi=True,
preset_values=preset,
allow_reset_warning_msg=warning
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default_kernel)
group.set_focus_by_value(default_kernel)
group.set_selected_by_value(preset)
result = SelectMenu(
group,
allow_skip=True,
allow_reset=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Kernel'))),
multi=True
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return choice.single_value
return []
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return []
case ResultType.Selection:
return result.get_values()
def ask_for_bootloader(preset: Bootloader) -> Bootloader:
def ask_for_bootloader(preset: Optional[Bootloader]) -> Optional[Bootloader]:
# Systemd is UEFI only
if not SysInfo.has_uefi():
options = [Bootloader.Grub.value, Bootloader.Limine.value]
default = Bootloader.Grub.value
options = [Bootloader.Grub, Bootloader.Limine]
default = Bootloader.Grub
else:
options = Bootloader.values()
default = Bootloader.Systemd.value
options = [b for b in Bootloader]
default = Bootloader.Systemd
preset_value = preset.value if preset else None
items = [MenuItem(o.value, value=o) for o in options]
group = MenuItemGroup(items)
group.set_default_by_value(default)
group.set_focus_by_value(preset)
choice = Menu(
_('Choose a bootloader'),
options,
preset_values=preset_value,
sort=False,
default_option=default
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Bootloader'))),
allow_skip=True
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return Bootloader(choice.value)
return preset
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
def ask_for_uki(preset: bool = True) -> bool:
if preset:
preset_val = Menu.yes()
else:
preset_val = Menu.no()
prompt = str(_('Would you like to use unified kernel images?')) + '\n'
prompt = _('Would you like to use unified kernel images?')
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), preset_values=preset_val).run()
group = MenuItemGroup.yes_no()
group.set_focus_by_value(preset)
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True
result = SelectMenu(
group,
header=prompt,
columns=2,
orientation=Orientation.HORIZONTAL,
alignment=Alignment.CENTER,
allow_skip=True
).run()
return preset
match result.type_:
case ResultType.Skip: return preset
case ResultType.Selection:
return result.item() == MenuItem.yes()
case ResultType.Reset:
raise ValueError('Unhandled result type')
def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriver] = None) -> Optional[GfxDriver]:
def select_driver(options: List[GfxDriver] = [], preset: Optional[GfxDriver] = None) -> Optional[GfxDriver]:
"""
Some what convoluted function, whose job is simple.
Select a graphics driver from a pre-defined set of popular options.
@ -92,47 +114,65 @@ def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriv
if not options:
options = [driver for driver in GfxDriver]
drivers = sorted([o.value for o in options])
items = [MenuItem(o.value, value=o, preview_action=lambda x: x.value.packages_text()) for o in options]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(GfxDriver.AllOpenSource)
if drivers:
title = ''
if SysInfo.has_amd_graphics():
title += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n'
if SysInfo.has_intel_graphics():
title += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n'))
if SysInfo.has_nvidia_graphics():
title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n'))
if preset is not None:
group.set_focus_by_value(preset)
preset = current_value.value if current_value else None
header = ''
if SysInfo.has_amd_graphics():
header += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n'
if SysInfo.has_intel_graphics():
header += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n'))
if SysInfo.has_nvidia_graphics():
header += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n'))
choice = Menu(
title,
drivers,
preset_values=preset,
default_option=GfxDriver.AllOpenSource.value,
preview_command=lambda x: GfxDriver(x).packages_text(),
preview_size=0.3
).run()
result = SelectMenu(
group,
header=header,
allow_skip=True,
allow_reset=True,
preview_size='auto',
preview_style=PreviewStyle.BOTTOM,
preview_frame=FrameProperties(str(_('Info')), h_frame_style=FrameStyle.MIN)
).run()
if choice.type_ != MenuSelectionType.Selection:
return current_value
return GfxDriver(choice.single_value)
return current_value
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
return result.get_value()
def ask_for_swap(preset: bool = True) -> bool:
if preset:
preset_val = Menu.yes()
default_item = MenuItem.yes()
else:
preset_val = Menu.no()
default_item = MenuItem.no()
prompt = _('Would you like to use swap on zram?')
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run()
prompt = str(_('Would you like to use swap on zram?')) + '\n'
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True
group = MenuItemGroup.yes_no()
group.set_focus_by_value(default_item)
result = SelectMenu(
group,
header=prompt,
columns=2,
orientation=Orientation.HORIZONTAL,
alignment=Alignment.CENTER,
allow_skip=True
).run()
match result.type_:
case ResultType.Skip: return preset
case ResultType.Selection:
return result.item() == MenuItem.yes()
case ResultType.Reset:
raise ValueError('Unhandled result type')
return preset

View File

@ -1,39 +0,0 @@
from __future__ import annotations
import getpass
from typing import Any, Optional, TYPE_CHECKING
from ..models import PasswordStrength
from ..output import log, error
if TYPE_CHECKING:
_: Any
# used for signal handler
SIG_TRIGGER = None
def get_password(prompt: str = '') -> Optional[str]:
if not prompt:
prompt = _("Enter a password: ")
while True:
try:
password = getpass.getpass(prompt)
except (KeyboardInterrupt, EOFError):
break
if len(password.strip()) <= 0:
break
strength = PasswordStrength.strength(password)
log(f'Password strength: {strength.value}', fg=strength.color())
passwd_verification = getpass.getpass(prompt=_('And one more time for verification: '))
if password != passwd_verification:
error(' * Passwords did not match * ')
continue
return password
return None

View File

@ -1,8 +1,13 @@
from dataclasses import dataclass
from typing import Dict, Any, TYPE_CHECKING, Optional
from typing import Dict, Any, TYPE_CHECKING, Optional, List
from .utils import list_keyboard_languages, list_locales, set_kb_layout, get_kb_layout
from ..menu import Selector, AbstractSubMenu, MenuSelectionType, Menu
from ..menu import AbstractSubMenu
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType
)
if TYPE_CHECKING:
_: Any
@ -28,6 +33,12 @@ class LocaleConfiguration:
'sys_enc': self.sys_enc
}
def preview(self) -> str:
output = '{}: {}\n'.format(str(_('Keyboard layout')), self.kb_layout)
output += '{}: {}\n'.format(str(_('Locale language')), self.sys_lang)
output += '{}: {}'.format(str(_('Locale encoding')), self.sys_enc)
return output
@classmethod
def _load_config(cls, config: 'LocaleConfiguration', args: Dict[str, Any]) -> 'LocaleConfiguration':
if 'sys_lang' in args:
@ -54,34 +65,50 @@ class LocaleConfiguration:
class LocaleMenu(AbstractSubMenu):
def __init__(
self,
data_store: Dict[str, Any],
locale_conf: LocaleConfiguration
):
self._preset = locale_conf
super().__init__(data_store=data_store)
self._locale_conf = locale_conf
self._data_store: Dict[str, Any] = {}
menu_optioons = self._define_menu_options()
def setup_selection_menu_options(self) -> None:
self._menu_options['keyboard-layout'] = \
Selector(
_('Keyboard layout'),
lambda preset: self._select_kb_layout(preset),
default=self._preset.kb_layout,
enabled=True)
self._menu_options['sys-language'] = \
Selector(
_('Locale language'),
lambda preset: select_locale_lang(preset),
default=self._preset.sys_lang,
enabled=True)
self._menu_options['sys-encoding'] = \
Selector(
_('Locale encoding'),
lambda preset: select_locale_enc(preset),
default=self._preset.sys_enc,
enabled=True)
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
def run(self, allow_reset: bool = True) -> LocaleConfiguration:
super().run(allow_reset=allow_reset)
def _define_menu_options(self) -> List[MenuItem]:
return [
MenuItem(
text=str(_('Keyboard layout')),
action=lambda x: self._select_kb_layout(x),
value=self._locale_conf.kb_layout,
preview_action=self._prev_locale,
key='keyboard-layout'
),
MenuItem(
text=str(_('Locale language')),
action=lambda x: select_locale_lang(x),
value=self._locale_conf.sys_lang,
preview_action=self._prev_locale,
key='sys-language'
),
MenuItem(
text=str(_('Locale encoding')),
action=lambda x: select_locale_enc(x),
value=self._locale_conf.sys_enc,
preview_action=self._prev_locale,
key='sys-encoding'
)
]
def _prev_locale(self, item: MenuItem) -> Optional[str]:
temp_locale = LocaleConfiguration(
self._menu_item_group.find_by_key('keyboard-layout').get_value(),
self._menu_item_group.find_by_key('sys-language').get_value(),
self._menu_item_group.find_by_key('sys-encoding').get_value(),
)
return temp_locale.preview()
def run(self) -> LocaleConfiguration:
super().run()
if not self._data_store:
return LocaleConfiguration.default()
@ -103,59 +130,79 @@ def select_locale_lang(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales()
locale_lang = set([locale.split()[0] for locale in locales])
choice = Menu(
_('Choose which locale language to use'),
list(locale_lang),
sort=True,
preset_values=preset
items = [MenuItem(ll, value=ll) for ll in locale_lang]
group = MenuItemGroup(items, sort_items=True)
group.set_focus_by_value(preset)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Locale language'))),
allow_skip=True,
).run()
match choice.type_:
case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Skip: return preset
return None
match result.type_:
case ResultType.Selection:
return result.get_value()
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled return type')
def select_locale_enc(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales()
locale_enc = set([locale.split()[1] for locale in locales])
choice = Menu(
_('Choose which locale encoding to use'),
list(locale_enc),
sort=True,
preset_values=preset
items = [MenuItem(le, value=le) for le in locale_enc]
group = MenuItemGroup(items, sort_items=True)
group.set_focus_by_value(preset)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Locale encoding'))),
allow_skip=True,
).run()
match choice.type_:
case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Skip: return preset
return None
match result.type_:
case ResultType.Selection:
return result.get_value()
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled return type')
def select_kb_layout(preset: Optional[str] = None) -> Optional[str]:
"""
Asks the user to select a language
Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
Select keyboard layout
:return: The language/dictionary key of the selected language
:return: The keyboard layout shortcut for the selected layout
:rtype: str
"""
kb_lang = list_keyboard_languages()
# sort alphabetically and then by length
sorted_kb_lang = sorted(kb_lang, key=lambda x: (len(x), x))
choice = Menu(
_('Select keyboard layout'),
sorted_kb_lang,
preset_values=preset,
sort=False
items = [MenuItem(lang, value=lang) for lang in sorted_kb_lang]
group = MenuItemGroup(items, sort_items=False)
group.set_focus_by_value(preset)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Keyboard layout'))),
allow_skip=True,
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return choice.single_value
match result.type_:
case ResultType.Selection:
return result.get_value()
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled return type')
return None

View File

@ -1,9 +1,2 @@
from .abstract_menu import Selector, AbstractMenu, AbstractSubMenu
from .abstract_menu import AbstractMenu, AbstractSubMenu
from .list_manager import ListManager
from .menu import (
MenuSelectionType,
MenuSelection,
Menu,
)
from .table_selection_menu import TableMenu
from .text_input import TextInput

View File

@ -1,11 +1,14 @@
from __future__ import annotations
from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING
from typing import Callable, Any, List, Optional, Dict, TYPE_CHECKING
from .menu import Menu, MenuSelectionType
from ..output import error
from ..output import unicode_ljust
from ..translationhandler import TranslationHandler, Language
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
PreviewStyle, FrameProperties, FrameStyle,
ResultType, Chars, Tui
)
if TYPE_CHECKING:
_: Any
@ -144,41 +147,21 @@ class Selector:
class AbstractMenu:
def __init__(
self,
data_store: Dict[str, Any] = {},
auto_cursor: bool = False,
preview_size: float = 0.2
item_group: MenuItemGroup,
data_store: Dict[str, Any],
auto_cursor: bool = True,
allow_reset: bool = False,
reset_warning: Optional[str] = None
):
"""
Create a new selection menu.
:param data_store: Area (Dict) where the resulting data will be held. At least an entry for each option. Default area is self._data_store (not preset in the call, due to circular references
:type data_store: Dict
:param auto_cursor: Boolean which determines if the cursor stays on the first item (false) or steps each invocation of a selection entry (true)
:type auto_cursor: bool
:param preview_size. Size in fractions of screen size of the preview window
;type preview_size: float (range 0..1)
"""
self._enabled_order: List[str] = []
self._translation_handler = TranslationHandler()
self.is_context_mgr = False
self._menu_item_group = item_group
self._data_store = data_store
self.auto_cursor = auto_cursor
self._menu_options: Dict[str, Selector] = {}
self.preview_size = preview_size
self._last_choice = None
self._allow_reset = allow_reset
self._reset_warning = reset_warning
self.setup_selection_menu_options()
self._sync_all()
self._populate_default_values()
self.is_context_mgr = False
self.defined_text = str(_('Defined'))
@property
def last_choice(self):
return self._last_choice
self._sync_all_from_ds()
def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu:
self.is_context_mgr = True
@ -189,263 +172,86 @@ class AbstractMenu:
# TODO: skip processing when it comes from a planified exit
if len(args) >= 2 and args[1]:
error(args[1])
print(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")
Tui.print("Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")
raise args[1]
for key in self._menu_options:
selector = self._menu_options[key]
if key and key not in self._data_store:
self._data_store[key] = selector.current_selection
self._sync_all_to_ds()
self.exit_callback()
def _sync_all_from_ds(self) -> None:
for item in self._menu_item_group.menu_items:
if item.key is not None:
if (store_value := self._data_store.get(item.key, None)) is not None:
item.value = store_value
@property
def translation_handler(self) -> TranslationHandler:
return self._translation_handler
def _sync_all_to_ds(self) -> None:
for item in self._menu_item_group.menu_items:
if item.key:
self._data_store[item.key] = item.value
def _populate_default_values(self) -> None:
for config_key, selector in self._menu_options.items():
if selector.default is not None and config_key not in self._data_store:
self._data_store[config_key] = selector.default
def _sync(self, item: MenuItem) -> None:
if not item.key:
return
def _sync_all(self) -> None:
for key in self._menu_options.keys():
self._sync(key)
store_value = self._data_store.get(item.key, None)
def _sync(self, selector_name: str) -> None:
value = self._data_store.get(selector_name, None)
selector = self._menu_options.get(selector_name, None)
if store_value is not None:
item.value = store_value
elif item.value is not None:
self._data_store[item.key] = item.value
if value is not None:
self._menu_options[selector_name].set_current_selection(value)
elif selector is not None and selector.has_selection():
self._data_store[selector_name] = selector.current_selection
def set_enabled(self, key: str, enabled: bool) -> None:
if (item := self._menu_item_group.find_by_key(key)) is not None:
item.enabled = enabled
return None
def setup_selection_menu_options(self) -> None:
""" Define the menu options.
Menu options can be defined here in a subclass or done per program calling self.set_option()
"""
return
raise ValueError(f'No selector found: {key}')
def pre_callback(self, selector_name) -> None:
""" will be called before each action in the menu """
return
def disable_all(self) -> None:
for item in self._menu_item_group.items:
item.enabled = False
def post_callback(self, selection_name: Optional[str] = None, value: Any = None):
""" will be called after each action in the menu """
return True
def exit_callback(self) -> None:
""" will be called at the end of the processing of the menu """
return
def _update_enabled_order(self, selector_name: str) -> None:
self._enabled_order.append(selector_name)
def enable(self, selector_name: str, mandatory: bool = False) -> None:
""" activates menu options """
if self._menu_options.get(selector_name, None):
self._menu_options[selector_name].set_enabled(True)
self._update_enabled_order(selector_name)
self._menu_options[selector_name].set_mandatory(mandatory)
self._sync(selector_name)
else:
raise ValueError(f'No selector found: {selector_name}')
def _preview_display(self, selection_name: str) -> Optional[str]:
config_name, selector = self._find_selection(selection_name)
if preview := selector.preview_func:
return preview()
return None
def _get_menu_text_padding(self, entries: List[Selector]) -> int:
return max([len(str(selection.description)) for selection in entries])
def _find_selection(self, selection_name: str) -> Tuple[str, Selector]:
enabled_menus = self._menus_to_enable()
padding = self._get_menu_text_padding(list(enabled_menus.values()))
option = []
for k, v in self._menu_options.items():
if v.menu_text(padding).strip() == selection_name.strip():
option.append((k, v))
if len(option) != 1:
raise ValueError(f'Selection not found: {selection_name}')
config_name = option[0][0]
selector = option[0][1]
return config_name, selector
def run(self, allow_reset: bool = False):
self._sync_all()
self.post_callback()
cursor_pos = None
def run(self) -> Optional[Any]:
self._sync_all_from_ds()
while True:
enabled_menus = self._menus_to_enable()
padding = self._get_menu_text_padding(list(enabled_menus.values()))
menu_options = [m.menu_text(padding) for m in enabled_menus.values()]
warning_msg = str(_('All settings will be reset, are you sure?'))
selection = Menu(
_('Set/Modify the below options'),
menu_options,
sort=False,
cursor_index=cursor_pos,
preview_command=self._preview_display,
preview_size=self.preview_size,
skip_empty_entries=True,
skip=False,
allow_reset=allow_reset,
allow_reset_warning_msg=warning_msg
result = SelectMenu(
self._menu_item_group,
allow_skip=False,
allow_reset=self._allow_reset,
reset_warning_msg=self._reset_warning,
preview_style=PreviewStyle.RIGHT,
preview_size='auto',
preview_frame=FrameProperties('Info', FrameStyle.MAX),
).run()
match selection.type_:
case MenuSelectionType.Reset:
self._data_store = {}
return
case MenuSelectionType.Selection:
value: str = selection.value # type: ignore
match result.type_:
case ResultType.Selection:
item: MenuItem = result.item()
if self.auto_cursor:
cursor_pos = menu_options.index(value) + 1 # before the strip otherwise fails
# in case the new position lands on a "placeholder" we'll skip them as well
while True:
if cursor_pos >= len(menu_options):
cursor_pos = 0
if len(menu_options[cursor_pos]) > 0:
break
cursor_pos += 1
value = value.strip()
# if this calls returns false, we exit the menu
# we allow for an callback for special processing on releasing control
if not self._process_selection(value):
if item.action is None:
break
case ResultType.Reset:
self._data_store = {}
return None
# we get the last action key
actions = {str(v.description): k for k, v in self._menu_options.items()}
self._last_choice = actions[selection.value.strip()] # type: ignore
if not self.is_context_mgr:
self.__exit__()
def _process_selection(self, selection_name: str) -> bool:
""" determines and executes the selection y
Can / Should be extended to handle specific selection issues
Returns true if the menu shall continue, False if it has ended
"""
# find the selected option in our option list
config_name, selector = self._find_selection(selection_name)
return self.exec_option(config_name, selector)
def exec_option(self, config_name: str, p_selector: Optional[Selector] = None) -> bool:
""" processes the execution of a given menu entry
- pre process callback
- selection function
- post process callback
- exec action
returns True if the loop has to continue, false if the loop can be closed
"""
if not p_selector:
selector = self.option(config_name)
else:
selector = p_selector
self.pre_callback(config_name)
result = None
if selector.func is not None:
cur_value = self.option(config_name).get_selection()
result = selector.func(cur_value)
self._menu_options[config_name].set_current_selection(result)
if selector.do_store():
self._data_store[config_name] = result
exec_ret_val = selector.exec_func(config_name, result) if selector.exec_func else False
self.post_callback(config_name, result)
if exec_ret_val:
return False
return True
def _verify_selection_enabled(self, selection_name: str) -> bool:
if selection := self._menu_options.get(selection_name, None):
if not selection.enabled:
return False
if len(selection.dependencies) > 0:
for dep in selection.dependencies:
if isinstance(dep, str):
if not self._verify_selection_enabled(dep) or self._menu_options[dep].is_empty():
return False
elif callable(dep): # callable dependency eval
return dep()
else:
raise ValueError(f'Unsupported dependency: {selection_name}')
if len(selection.dependencies_not) > 0:
for dep in selection.dependencies_not:
if not self._menu_options[dep].is_empty():
return False
return True
raise ValueError(f'No selection found: {selection_name}')
def _menus_to_enable(self) -> dict:
""" general """
enabled_menus = {}
for name, selection in self._menu_options.items():
if self._verify_selection_enabled(name):
enabled_menus[name] = selection
# sort the enabled menu by the order we enabled them in
# we'll add the entries that have been enabled via the selector constructor at the top
enabled_keys = [i for i in enabled_menus.keys() if i not in self._enabled_order]
# and then we add the ones explicitly enabled by the enable function
enabled_keys += [i for i in self._enabled_order if i in enabled_menus.keys()]
ordered_menus = {k: enabled_menus[k] for k in enabled_keys}
return ordered_menus
def option(self, name: str) -> Selector:
# TODO check inexistent name
return self._menu_options[name]
def list_enabled_options(self) -> Iterator:
""" Iterator to retrieve the enabled menu options at a given time.
The results are dynamic (if between calls to the iterator some elements -still not retrieved- are (de)activated
"""
for item in self._menu_options:
if item in self._menus_to_enable():
yield item
def _select_archinstall_language(self, preset: Language) -> Language:
from ..interactions.general_conf import select_archinstall_language
language = select_archinstall_language(self.translation_handler.translated_languages, preset)
self._translation_handler.activate(language)
return language
self._sync_all_to_ds()
return None
class AbstractSubMenu(AbstractMenu):
def __init__(self, data_store: Dict[str, Any] = {}, preview_size: float = 0.2):
super().__init__(data_store=data_store, preview_size=preview_size)
def __init__(
self,
item_group: MenuItemGroup,
data_store: Dict[str, Any],
auto_cursor: bool = True,
allow_reset: bool = False
):
back_text = f'{Chars.Right_arrow} ' + str(_('Back'))
item_group.menu_items.append(MenuItem(text=back_text))
self._menu_options['__separator__'] = Selector('')
self._menu_options['back'] = \
Selector(
Menu.back(),
no_store=True,
enabled=True,
exec_func=lambda n, v: True,
)
super().__init__(
item_group,
data_store=data_store,
auto_cursor=auto_cursor,
allow_reset=allow_reset
)

View File

@ -1,10 +1,12 @@
import copy
from os import system
from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List
from .menu import Menu
from ..output import FormattedOutput
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
Alignment, ResultType
)
if TYPE_CHECKING:
_: Any
@ -63,31 +65,34 @@ class ListManager:
data_formatted = self.reformat(self._data)
options, header = self._prepare_selection(data_formatted)
system('clear')
items = [MenuItem(o, value=o) for o in options]
group = MenuItemGroup(items, sort_items=False)
choice = Menu(
self._prompt,
options,
sort=False,
clear_screen=False,
clear_menu_on_exit=False,
result = SelectMenu(
group,
header=header,
skip_empty_entries=True,
skip=False,
show_search_hint=False
search_enabled=False,
allow_skip=False,
alignment=Alignment.CENTER,
).run()
if choice.value in self._base_actions:
self._data = self.handle_action(choice.value, None, self._data)
elif choice.value in self._terminate_actions:
match result.type_:
case ResultType.Selection:
value = result.get_value()
case _:
raise ValueError('Unhandled return type')
if value in self._base_actions:
self._data = self.handle_action(value, None, self._data)
elif value in self._terminate_actions:
break
else: # an entry of the existing selection was chosen
selected_entry = data_formatted[choice.value] # type: ignore
selected_entry = result.get_value()
self._run_actions_on_entry(selected_entry)
self._last_choice = choice.value # type: ignore
self._last_choice = value
if choice.value == self._cancel_action:
if result.get_value() == self._cancel_action:
return self._original_data # return the original list
else:
return self._data
@ -110,23 +115,30 @@ class ListManager:
return options, header
def _run_actions_on_entry(self, entry: Any):
def _run_actions_on_entry(self, entry: Any) -> None:
options = self.filter_options(entry, self._sub_menu_actions) + [self._cancel_action]
display_value = self.selected_action_display(entry)
prompt = _("Select an action for '{}'").format(display_value)
items = [MenuItem(o, value=o) for o in options]
group = MenuItemGroup(items, sort_items=False)
choice = Menu(
prompt,
options,
sort=False,
clear_screen=False,
clear_menu_on_exit=False,
show_search_hint=False
header = f'{self.selected_action_display(entry)}\n'
result = SelectMenu(
group,
header=header,
search_enabled=False,
allow_skip=False,
alignment=Alignment.CENTER
).run()
if choice.value and choice.value != self._cancel_action:
self._data = self.handle_action(choice.value, entry, self._data)
match result.type_:
case ResultType.Selection:
value = result.get_value()
case _:
raise ValueError('Unhandled return type')
if value != self._cancel_action:
self._data = self.handle_action(value, entry, self._data)
def reformat(self, data: List[Any]) -> Dict[str, Optional[Any]]:
"""
@ -139,10 +151,9 @@ class ListManager:
# these are the header rows of the table and do not map to any User obviously
# we're adding 2 spaces as prefix because the menu selector '> ' will be put before
# the selectable rows so the header has to be aligned
display_data: Dict[str, Optional[Any]] = {f' {rows[0]}': None, f' {rows[1]}': None}
display_data: Dict[str, Optional[Any]] = {f'{rows[0]}': None, f'{rows[1]}': None}
for row, entry in zip(rows[2:], data):
row = row.replace('|', '\\|')
display_data[row] = entry
return display_data

View File

@ -1,350 +0,0 @@
from dataclasses import dataclass
from enum import Enum, auto
from os import system
from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable
from simple_term_menu import TerminalMenu
from ..exceptions import RequirementError
from ..output import debug
if TYPE_CHECKING:
_: Any
class MenuSelectionType(Enum):
Selection = auto()
Skip = auto()
Reset = auto()
@dataclass
class MenuSelection:
type_: MenuSelectionType
value: Optional[Union[str, List[str]]] = None
@property
def single_value(self) -> Any:
return self.value
@property
def multi_value(self) -> List[Any]:
return self.value # type: ignore
class Menu(TerminalMenu): # type: ignore[misc]
_menu_is_active: bool = False
@staticmethod
def is_menu_active() -> bool:
return Menu._menu_is_active
@classmethod
def back(cls) -> str:
return str(_('← Back'))
@classmethod
def yes(cls) -> str:
return str(_('yes'))
@classmethod
def no(cls) -> str:
return str(_('no'))
@classmethod
def yes_no(cls) -> List[str]:
return [cls.yes(), cls.no()]
def __init__(
self,
title: str,
p_options: Union[List[str], Dict[str, Any]],
skip: bool = True,
multi: bool = False,
default_option: Optional[str] = None,
sort: bool = True,
preset_values: Optional[Union[str, List[str]]] = None,
cursor_index: Optional[int] = None,
preview_command: Optional[Callable[[Any], str | None]] = None,
preview_size: float = 0.0,
preview_title: str = 'Info',
header: Union[List[str], str] = [],
allow_reset: bool = False,
allow_reset_warning_msg: Optional[str] = None,
clear_screen: bool = True,
show_search_hint: bool = True,
cycle_cursor: bool = True,
clear_menu_on_exit: bool = True,
skip_empty_entries: bool = False,
display_back_option: bool = False,
extra_bottom_space: bool = False
):
"""
Creates a new menu
:param title: Text that will be displayed above the menu
:type title: str
:param p_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 p_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
:param preset_values: Predefined value(s) of the menu. In a multi menu, it selects the options included therein. If the selection is simple, moves the cursor to the position of the value
:type preset_values: str or list
:param cursor_index: The position where the cursor will be located. If it is not in range (number of elements of the menu) it goes to the first position
:type cursor_index: int
:param preview_command: A function that should return a string that will be displayed in a preview window when a menu selection item is in focus
:type preview_command: Callable
:param preview_size: Size of the preview window in ratio to the full window
:type preview_size: float
:param preview_title: Title of the preview window
:type preview_title: str
:param header: one or more header lines for the menu
:type header: string or list
:param allow_reset: This will explicitly handle a ctrl+c instead and return that specific state
:type allow_reset: bool
param allow_reset_warning_msg: If raise_error_on_interrupt is True the warning is set, a user confirmation is displayed
type allow_reset_warning_msg: str
:param extra_bottom_space: Add an extra empty line at the end of the menu
:type extra_bottom_space: bool
"""
if isinstance(p_options, Dict):
options = list(p_options.keys())
else:
options = list(p_options)
if not options:
raise RequirementError('Menu.__init__() requires at least one option to proceed.')
if any([o for o in options if not isinstance(o, str)]):
raise RequirementError('Menu.__init__() requires the options to be of type string')
if sort:
options = sorted(options)
self._menu_options = options
self._skip = skip
self._default_option = default_option
self._multi = multi
self._raise_error_on_interrupt = allow_reset
self._raise_error_warning_msg = allow_reset_warning_msg
action_info = ''
if skip:
action_info += str(_('ESC to skip'))
if self._raise_error_on_interrupt:
action_info += ', ' if len(action_info) > 0 else ''
action_info += str(_('CTRL+C to reset'))
if multi:
action_info += ', ' if len(action_info) > 0 else ''
action_info += str(_('TAB to select'))
if action_info:
action_info += '\n\n'
menu_title = f'\n{action_info}{title}\n'
if header:
if not isinstance(header, (list, tuple)):
header = [header]
menu_title += '\n' + '\n'.join(header)
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
self._menu_options = [self._default_menu_value] + [o for o in self._menu_options if default_option != o]
if display_back_option and not multi and skip:
skip_empty_entries = True
self._menu_options += ['', self.back()]
if extra_bottom_space:
skip_empty_entries = True
self._menu_options += ['']
preset_list: Optional[List[str]] = None
if preset_values and isinstance(preset_values, str):
preset_list = [preset_values]
calc_cursor_idx = self._determine_cursor_pos(preset_list, cursor_index)
# when we're not in multi selection mode we don't care about
# passing the pre-selection list to the menu as the position
# of the cursor is the one determining the pre-selection
if not self._multi:
preset_values = None
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,
multi_select=multi,
preselected_entries=preset_values,
cursor_index=calc_cursor_idx,
preview_command=lambda x: self._show_preview(preview_command, x),
preview_size=preview_size,
preview_title=preview_title,
raise_error_on_interrupt=self._raise_error_on_interrupt,
multi_select_select_on_accept=False,
clear_screen=clear_screen,
show_search_hint=show_search_hint,
cycle_cursor=cycle_cursor,
clear_menu_on_exit=clear_menu_on_exit,
skip_empty_entries=skip_empty_entries
)
@property
def _default_menu_value(self) -> str:
default_str = str(_('(default)'))
return f'{self._default_option} {default_str}'
def _show_preview(
self,
preview_command: Optional[Callable[[Any], str | None]],
selection: str
) -> Optional[str]:
if selection == self.back():
return None
if preview_command:
if self._default_option is not None and self._default_menu_value == selection:
selection = self._default_option
if res := preview_command(selection):
return res.rstrip('\n')
return None
def _show(self) -> MenuSelection:
try:
idx = self.show()
except KeyboardInterrupt:
return MenuSelection(type_=MenuSelectionType.Reset)
def check_default(elem) -> str:
if self._default_option is not None and self._default_menu_value in elem:
return self._default_option
else:
return elem
if idx is not None:
if isinstance(idx, (list, tuple)): # on multi selection
results = []
for i in idx:
option = check_default(self._menu_options[i])
results.append(option)
return MenuSelection(type_=MenuSelectionType.Selection, value=results)
else: # on single selection
result = check_default(self._menu_options[idx])
return MenuSelection(type_=MenuSelectionType.Selection, value=result)
else:
return MenuSelection(type_=MenuSelectionType.Skip)
def run(self) -> MenuSelection:
Menu._menu_is_active = True
selection = self._show()
if selection.type_ == MenuSelectionType.Reset:
if self._raise_error_on_interrupt and self._raise_error_warning_msg is not None:
response = Menu(self._raise_error_warning_msg, Menu.yes_no(), skip=False).run()
if response.value == Menu.no():
return self.run()
elif selection.type_ is MenuSelectionType.Skip:
if not self._skip:
system('clear')
return self.run()
if selection.type_ == MenuSelectionType.Selection:
if selection.value == self.back():
selection.type_ = MenuSelectionType.Skip
selection.value = None
Menu._menu_is_active = False
return selection
def set_cursor_pos(self, pos: int) -> None:
if pos and 0 < pos < len(self._menu_entries):
self._view.active_menu_index = pos
else:
self._view.active_menu_index = 0 # we define a default
def set_cursor_pos_entry(self, value: str) -> None:
pos = self._menu_entries.index(value)
self.set_cursor_pos(pos)
def _determine_cursor_pos(
self,
preset: Optional[List[str]] = None,
cursor_index: Optional[int] = None
) -> Optional[int]:
"""
The priority order to determine the cursor position is:
1. A static cursor position was provided
2. Preset values have been provided so the cursor will be
positioned on those
3. A default value for a selection is given so the cursor
will be placed on such
"""
if cursor_index:
return cursor_index
if preset:
indexes = []
for p in preset:
try:
# the options of the table selection menu
# are already escaped so we have to escape
# the preset values as well for the comparison
if '|' in p:
p = p.replace('|', '\\|')
if p in self._menu_options:
idx = self._menu_options.index(p)
else:
idx = self._menu_options.index(self._default_menu_value)
indexes.append(idx)
except (IndexError, ValueError):
debug(f'Error finding index of {p}: {self._menu_options}')
if len(indexes) == 0:
indexes.append(0)
return indexes[0]
if self._default_option:
return self._menu_options.index(self._default_menu_value)
return None

View File

@ -0,0 +1,64 @@
from typing import Any, Tuple, List, Dict, Optional
from archinstall.lib.output import FormattedOutput
from archinstall.tui import (
MenuItemGroup, MenuItem
)
class MenuHelper:
@staticmethod
def create_table(
data: Optional[List[Any]] = None,
table_data: Optional[Tuple[List[Any], str]] = None,
) -> Tuple[MenuItemGroup, str]:
if data is not None:
table_text = FormattedOutput.as_table(data)
rows = table_text.split('\n')
table = MenuHelper._create_table(data, rows)
elif table_data is not None:
# we assume the table to be
# h1 | h2
# -----------
# r1 | r2
data = table_data[0]
rows = table_data[1].split('\n')
table = MenuHelper._create_table(data, rows)
else:
raise ValueError('Either "data" or "table_data" must be provided')
table, header = MenuHelper._prepare_selection(table)
items = [
MenuItem(text, value=entry)
for text, entry in table.items()
]
group = MenuItemGroup(items, sort_items=False)
return group, header
@staticmethod
def _create_table(data: List[Any], rows: List[str], header_padding: int = 2) -> Dict[str, Any]:
# these are the header rows of the table and do not map to any data obviously
# we're adding 2 spaces as prefix because the menu selector '> ' will be put before
# the selectable rows so the header has to be aligned
padding = ' ' * header_padding
display_data = {f'{padding}{rows[0]}': None, f'{padding}{rows[1]}': None}
for row, entry in zip(rows[2:], data):
display_data[row] = entry
return display_data
@staticmethod
def _prepare_selection(table: Dict[str, Any]) -> Tuple[Dict[str, Any], str]:
# header rows are mapped to None so make sure to exclude those from the selectable data
options = {key: val for key, val in table.items() if val is not None}
header = ''
if len(options) > 0:
table_header = [key for key, val in table.items() if val is None]
header = '\n'.join(table_header)
return options, header

View File

@ -1,153 +0,0 @@
from typing import Any, Tuple, List, Dict, Optional, Callable
from .menu import MenuSelectionType, MenuSelection, Menu
from ..output import FormattedOutput
class TableMenu(Menu):
def __init__(
self,
title: str,
data: Optional[List[Any]] = None,
table_data: Optional[Tuple[List[Any], str]] = None,
preset: List[Any] = [],
custom_menu_options: List[str] = [],
default: Any = None,
multi: bool = False,
preview_command: Optional[Callable] = None,
preview_title: str = 'Info',
preview_size: float = 0.0,
allow_reset: bool = True,
allow_reset_warning_msg: Optional[str] = None,
skip: bool = True
):
"""
param title: Text that will be displayed above the menu
:type title: str
param data: List of objects that will be displayed as rows
:type data: List
param table_data: Tuple containing a list of objects and the corresponding
Table representation of the data as string; this can be used in case the table
has to be crafted in a more sophisticated manner
:type table_data: Optional[Tuple[List[Any], str]]
param custom_options: List of custom options that will be displayed under the table
:type custom_menu_options: List
:param preview_command: A function that should return a string that will be displayed in a preview window when a menu selection item is in focus
:type preview_command: Callable
"""
self._custom_options = custom_menu_options
self._multi = multi
if multi:
header_padding = 7
else:
header_padding = 2
if data is not None:
table_text = FormattedOutput.as_table(data)
rows = table_text.split('\n')
table = self._create_table(data, rows, header_padding=header_padding)
elif table_data is not None:
# we assume the table to be
# h1 | h2
# -----------
# r1 | r2
data = table_data[0]
rows = table_data[1].split('\n')
table = self._create_table(data, rows, header_padding=header_padding)
else:
raise ValueError('Either "data" or "table_data" must be provided')
self._options, header = self._prepare_selection(table)
preset_values = self._preset_values(preset)
extra_bottom_space = True if preview_command else False
super().__init__(
title,
self._options,
preset_values=preset_values,
header=header,
skip_empty_entries=True,
show_search_hint=False,
multi=multi,
default_option=default,
preview_command=lambda x: self._table_show_preview(preview_command, x),
preview_size=preview_size,
preview_title=preview_title,
extra_bottom_space=extra_bottom_space,
allow_reset=allow_reset,
allow_reset_warning_msg=allow_reset_warning_msg,
skip=skip
)
def _preset_values(self, preset: List[Any]) -> List[str]:
# when we create the table of just the preset values it will
# be formatted a bit different due to spacing, so to determine
# correct rows lets remove all the spaces and compare apples with apples
preset_table = FormattedOutput.as_table(preset).strip()
data_rows = preset_table.split('\n')[2:] # get all data rows
pure_data_rows = [self._escape_row(row.replace(' ', '')) for row in data_rows]
# the actual preset value has to be in non-escaped form
pure_option_rows = {o.replace(' ', ''): self._unescape_row(o) for o in self._options.keys()}
preset_rows = [row for pure, row in pure_option_rows.items() if pure in pure_data_rows]
return preset_rows
def _table_show_preview(self, preview_command: Optional[Callable], selection: Any) -> Optional[str]:
if preview_command:
row = self._escape_row(selection)
obj = self._options[row]
return preview_command(obj)
return None
def run(self) -> MenuSelection:
choice = super().run()
match choice.type_:
case MenuSelectionType.Selection:
if self._multi:
choice.value = [self._options[val] for val in choice.value] # type: ignore
else:
choice.value = self._options[choice.value] # type: ignore
return choice
def _escape_row(self, row: str) -> str:
return row.replace('|', '\\|')
def _unescape_row(self, row: str) -> str:
return row.replace('\\|', '|')
def _create_table(self, data: List[Any], rows: List[str], header_padding: int = 2) -> Dict[str, Any]:
# these are the header rows of the table and do not map to any data obviously
# we're adding 2 spaces as prefix because the menu selector '> ' will be put before
# the selectable rows so the header has to be aligned
padding = ' ' * header_padding
display_data = {f'{padding}{rows[0]}': None, f'{padding}{rows[1]}': None}
for row, entry in zip(rows[2:], data):
row = self._escape_row(row)
display_data[row] = entry
return display_data
def _prepare_selection(self, table: Dict[str, Any]) -> Tuple[Dict[str, Any], str]:
# header rows are mapped to None so make sure to exclude those from the selectable data
options = {key: val for key, val in table.items() if val is not None}
header = ''
if len(options) > 0:
table_header = [key for key, val in table.items() if val is None]
header = '\n'.join(table_header)
custom = {key: None for key in self._custom_options}
options.update(custom)
return options, header

View File

@ -1,26 +0,0 @@
import readline
import sys
class TextInput:
def __init__(self, prompt: str, prefilled_text=''):
self._prompt = prompt
self._prefilled_text = prefilled_text
def _hook(self) -> None:
readline.insert_text(self._prefilled_text)
readline.redisplay()
def run(self) -> str:
readline.set_pre_input_hook(self._hook)
try:
result = input(self._prompt)
except (KeyboardInterrupt, EOFError):
# To make sure any output that may follow
# will be on the line after the prompt
sys.stdout.write('\n')
sys.stdout.flush()
result = ''
readline.set_pre_input_hook()
return result

View File

@ -1,16 +1,24 @@
import time
import json
import urllib.parse
from pathlib import Path
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Any, List, Optional, TYPE_CHECKING
from typing import Dict, Any, List, Optional, TYPE_CHECKING, Tuple
from .menu import AbstractSubMenu, Selector, MenuSelectionType, Menu, ListManager, TextInput
from .menu import AbstractSubMenu, ListManager
from .networking import fetch_data_from_url
from .output import FormattedOutput, debug
from .storage import storage
from .models.mirrors import MirrorStatusListV3, MirrorStatusEntryV3
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
EditMenu
)
if TYPE_CHECKING:
_: Any
@ -67,7 +75,7 @@ class CustomMirror:
@dataclass
class MirrorConfiguration:
mirror_regions: Dict[str, List[str]] = field(default_factory=dict)
mirror_regions: Dict[str, List[MirrorStatusEntryV3]] = field(default_factory=dict)
custom_mirrors: List[CustomMirror] = field(default_factory=list)
@property
@ -85,7 +93,7 @@ class MirrorConfiguration:
for region, mirrors in self.mirror_regions.items():
for mirror in mirrors:
config += f'\n\n## {region}\nServer = {mirror}\n'
config += f'\n\n## {region}\nServer = {mirror.url}$repo/os/$arch\n'
for cm in self.custom_mirrors:
config += f'\n\n## {cm.name}\nServer = {cm.url}\n'
@ -116,13 +124,18 @@ class MirrorConfiguration:
class CustomMirrorList(ListManager):
def __init__(self, prompt: str, custom_mirrors: List[CustomMirror]):
def __init__(self, custom_mirrors: List[CustomMirror]):
self._actions = [
str(_('Add a custom mirror')),
str(_('Change custom mirror')),
str(_('Delete custom mirror'))
]
super().__init__(prompt, custom_mirrors, [self._actions[0]], self._actions[1:])
super().__init__(
'',
custom_mirrors,
[self._actions[0]],
self._actions[1:]
)
def selected_action_display(self, mirror: CustomMirror) -> str:
return mirror.name
@ -148,164 +161,190 @@ class CustomMirrorList(ListManager):
return data
def _add_custom_mirror(self, mirror: Optional[CustomMirror] = None) -> Optional[CustomMirror]:
prompt = '\n\n' + str(_('Enter name (leave blank to skip): '))
existing_name = mirror.name if mirror else ''
def _add_custom_mirror(self, preset: Optional[CustomMirror] = None) -> Optional[CustomMirror]:
edit_result = EditMenu(
str(_('Mirror name')),
alignment=Alignment.CENTER,
allow_skip=True,
default_text=preset.name if preset else None
).input()
while True:
name = TextInput(prompt, existing_name).run()
if not name:
return mirror
break
match edit_result.type_:
case ResultType.Selection:
name = edit_result.text()
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled return type')
prompt = '\n' + str(_('Enter url (leave blank to skip): '))
existing_url = mirror.url if mirror else ''
header = f'{str(_("Name"))}: {name}'
while True:
url = TextInput(prompt, existing_url).run()
if not url:
return mirror
break
edit_result = EditMenu(
str(_('Url')),
header=header,
alignment=Alignment.CENTER,
allow_skip=True,
default_text=preset.url if preset else None
).input()
sign_check_choice = Menu(
str(_('Select signature check option')),
[s.value for s in SignCheck],
skip=False,
clear_screen=False,
preset_values=mirror.sign_check.value if mirror else None
match edit_result.type_:
case ResultType.Selection:
url = edit_result.text()
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled return type')
header += f'\n{str(_("Url"))}: {url}\n'
prompt = f'{header}\n' + str(_('Select signature check'))
sign_chk_items = [MenuItem(s.value, value=s.value) for s in SignCheck]
group = MenuItemGroup(sign_chk_items, sort_items=False)
if preset is not None:
group.set_selected_by_value(preset.sign_check.value)
result = SelectMenu(
group,
header=prompt,
alignment=Alignment.CENTER,
allow_skip=False
).run()
sign_option_choice = Menu(
str(_('Select signature option')),
[s.value for s in SignOption],
skip=False,
clear_screen=False,
preset_values=mirror.sign_option.value if mirror else None
match result.type_:
case ResultType.Selection:
sign_check = SignCheck(result.get_value())
case _:
raise ValueError('Unhandled return type')
header += f'{str(_("Signature check"))}: {sign_check.value}\n'
prompt = f'{header}\n' + 'Select signature option'
sign_opt_items = [MenuItem(s.value, value=s.value) for s in SignOption]
group = MenuItemGroup(sign_opt_items, sort_items=False)
if preset is not None:
group.set_selected_by_value(preset.sign_option.value)
result = SelectMenu(
group,
header=prompt,
alignment=Alignment.CENTER,
allow_skip=False
).run()
return CustomMirror(
name,
url,
SignCheck(sign_check_choice.single_value),
SignOption(sign_option_choice.single_value)
)
match result.type_:
case ResultType.Selection:
sign_opt = SignOption(result.get_value())
case _:
raise ValueError('Unhandled return type')
return CustomMirror(name, url, sign_check, sign_opt)
class MirrorMenu(AbstractSubMenu):
def __init__(
self,
data_store: Dict[str, Any],
preset: Optional[MirrorConfiguration] = None
):
if preset:
self._preset = preset
self._mirror_config = preset
else:
self._preset = MirrorConfiguration()
self._mirror_config = MirrorConfiguration()
super().__init__(data_store=data_store)
self._data_store: Dict[str, Any] = {}
def setup_selection_menu_options(self) -> None:
self._menu_options['mirror_regions'] = \
Selector(
_('Mirror region'),
lambda preset: select_mirror_regions(preset),
display_func=lambda x: ', '.join(x.keys()) if x else '',
default=self._preset.mirror_regions,
enabled=True)
self._menu_options['custom_mirrors'] = \
Selector(
_('Custom mirrors'),
lambda preset: select_custom_mirror(preset=preset),
display_func=lambda x: str(_('Defined')) if x else '',
preview_func=self._prev_custom_mirror,
default=self._preset.custom_mirrors,
enabled=True
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
def _define_menu_options(self) -> List[MenuItem]:
return [
MenuItem(
text=str(_('Mirror region')),
action=lambda x: select_mirror_regions(x),
value=self._mirror_config.mirror_regions,
preview_action=self._prev_regions,
key='mirror_regions'
),
MenuItem(
text=str(_('Custom mirrors')),
action=lambda x: select_custom_mirror(x),
value=self._mirror_config.custom_mirrors,
preview_action=self._prev_custom_mirror,
key='custom_mirrors'
)
]
def _prev_custom_mirror(self) -> Optional[str]:
selector = self._menu_options['custom_mirrors']
def _prev_regions(self, item: MenuItem) -> Optional[str]:
mirrors: Dict[str, List[MirrorStatusEntryV3]] = item.get_value()
if selector.has_selection():
custom_mirrors: List[CustomMirror] = selector.current_selection # type: ignore
output = FormattedOutput.as_table(custom_mirrors)
return output.strip()
output = ''
for name, status_list in mirrors.items():
output += f'{name}\n'
output += '-' * len(name) + '\n'
return None
for entry in status_list:
output += f'{entry.url}\n'
def run(self, allow_reset: bool = True) -> Optional[MirrorConfiguration]:
super().run(allow_reset=allow_reset)
output += '\n'
if self._data_store.get('mirror_regions', None) or self._data_store.get('custom_mirrors', None):
return MirrorConfiguration(
mirror_regions=self._data_store['mirror_regions'],
custom_mirrors=self._data_store['custom_mirrors'],
)
return output
return None
def _prev_custom_mirror(self, item: MenuItem) -> Optional[str]:
if not item.value:
return None
custom_mirrors: List[CustomMirror] = item.value
output = FormattedOutput.as_table(custom_mirrors)
return output.strip()
def run(self) -> MirrorConfiguration:
super().run()
if not self._data_store:
return MirrorConfiguration()
return MirrorConfiguration(
mirror_regions=self._data_store.get('mirror_regions', None),
custom_mirrors=self._data_store.get('custom_mirrors', None),
)
def select_mirror_regions(preset_values: Dict[str, List[str]] = {}) -> Dict[str, List[str]]:
"""
Asks the user to select a mirror or region
Usually this is combined with :ref:`archinstall.list_mirrors`.
def select_mirror_regions(preset: Dict[str, List[MirrorStatusEntryV3]]) -> Dict[str, List[MirrorStatusEntryV3]]:
mirrors: Dict[str, List[MirrorStatusEntryV3]] | None = list_mirrors_from_remote()
:return: The dictionary information about a mirror/region.
:rtype: dict
"""
if preset_values is None:
preselected = None
else:
preselected = list(preset_values.keys())
if not mirrors:
mirrors = list_mirrors_from_local()
remote_mirrors = list_mirrors_from_remote()
mirrors: Dict[str, list[str]] = {}
items = [MenuItem(name, value=(name, mirrors)) for name, mirrors in mirrors.items()]
group = MenuItemGroup(items, sort_items=True)
if remote_mirrors:
choice = Menu(
_('Select one of the regions to download packages from'),
list(remote_mirrors.keys()),
preset_values=preselected,
multi=True,
allow_reset=True
).run()
preset_values = [(name, mirror) for name, mirror in preset.items()]
group.set_selected_by_value(preset_values)
match choice.type_:
case MenuSelectionType.Reset:
return {}
case MenuSelectionType.Skip:
return preset_values
case MenuSelectionType.Selection:
for region in choice.multi_value:
mirrors.setdefault(region, [])
for mirror in _sort_mirrors_by_performance(remote_mirrors[region]):
mirrors[region].append(mirror.server_url)
return mirrors
else:
local_mirrors = list_mirrors_from_local()
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Mirror regions'))),
allow_reset=True,
allow_skip=True,
multi=True,
).run()
choice = Menu(
_('Select one of the regions to download packages from'),
list(local_mirrors.keys()),
preset_values=preselected,
multi=True,
allow_reset=True
).run()
match choice.type_:
case MenuSelectionType.Reset:
return {}
case MenuSelectionType.Skip:
return preset_values
case MenuSelectionType.Selection:
for region in choice.multi_value:
mirrors[region] = local_mirrors[region]
return mirrors
return mirrors
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return {}
case ResultType.Selection:
selected_mirrors: List[Tuple[str, List[MirrorStatusEntryV3]]] = result.get_values()
return {name: mirror for name, mirror in selected_mirrors}
def select_custom_mirror(prompt: str = '', preset: List[CustomMirror] = []) -> list[CustomMirror]:
custom_mirrors = CustomMirrorList(prompt, preset).run()
def select_custom_mirror(preset: List[CustomMirror] = []):
custom_mirrors = CustomMirrorList(preset).run()
return custom_mirrors
@ -327,7 +366,7 @@ def list_mirrors_from_remote() -> Optional[Dict[str, List[MirrorStatusEntryV3]]]
return None
def list_mirrors_from_local() -> Dict[str, list[str]]:
def list_mirrors_from_local() -> Dict[str, List[MirrorStatusEntryV3]]:
with Path('/etc/pacman.d/mirrorlist').open('r') as fp:
mirrorlist = fp.read()
return _parse_locale_mirrors(mirrorlist)
@ -370,13 +409,13 @@ def _parse_remote_mirror_list(mirrorlist: str) -> Dict[str, List[MirrorStatusEnt
return sorted_by_regions
def _parse_locale_mirrors(mirrorlist: str) -> Dict[str, List[str]]:
def _parse_locale_mirrors(mirrorlist: str) -> Dict[str, List[MirrorStatusEntryV3]]:
lines = mirrorlist.splitlines()
# remove empty lines
lines = [line for line in lines if line]
mirror_list: Dict[str, List[str]] = {}
mirror_list: Dict[str, List[MirrorStatusEntryV3]] = {}
current_region = ''
for idx, line in enumerate(lines):
@ -391,6 +430,20 @@ def _parse_locale_mirrors(mirrorlist: str) -> Dict[str, List[str]]:
break
url = line.removeprefix('Server = ')
mirror_list[current_region].append(url)
mirror_entry = MirrorStatusEntryV3(
url=url.rstrip('$repo/os/$arch'),
protocol=urllib.parse.urlparse(url).scheme,
active=True,
country=current_region or 'Worldwide',
# The following values are normally populated by
# archlinux.org mirror-list endpoint, and can't be known
# from just the local mirror-list file.
country_code='WW',
isos=True,
ipv4=True,
ipv6=True,
details='Locally defined mirror',
)
mirror_list[current_region].append(mirror_entry)
return mirror_list

View File

@ -12,13 +12,10 @@ if TYPE_CHECKING:
@dataclass
class Audio(Enum):
NoAudio = 'No audio server'
Pipewire = 'pipewire'
Pulseaudio = 'pulseaudio'
@staticmethod
def no_audio_text() -> str:
return str(_('No audio server'))
@dataclass
class AudioConfiguration:
@ -47,8 +44,9 @@ class AudioConfiguration:
case Audio.Pulseaudio:
installation.add_additional_packages("pulseaudio")
if SysInfo.requires_sof_fw():
installation.add_additional_packages('sof-firmware')
if self.audio != Audio.NoAudio:
if SysInfo.requires_sof_fw():
installation.add_additional_packages('sof-firmware')
if SysInfo.requires_alsa_fw():
installation.add_additional_packages('alsa-firmware')
if SysInfo.requires_alsa_fw():
installation.add_additional_packages('alsa-firmware')

View File

@ -1,19 +1,20 @@
from pydantic import BaseModel, field_validator, model_validator
import datetime
import pydantic
import http.client
import urllib.error
import urllib.parse
import urllib.request
from typing import (
Dict,
List
List,
Optional
)
from ..networking import ping, DownloadTimer
from ..output import debug
class MirrorStatusEntryV3(pydantic.BaseModel):
class MirrorStatusEntryV3(BaseModel):
url: str
protocol: str
active: bool
@ -91,15 +92,15 @@ class MirrorStatusEntryV3(pydantic.BaseModel):
return self._latency
@pydantic.field_validator('score', mode='before')
def validate_score(cls, value) -> int | None:
@field_validator('score', mode='before')
def validate_score(cls, value: int) -> Optional[int]:
if value is not None:
value = round(value)
debug(f" score: {value}")
return value
@pydantic.model_validator(mode='after')
@model_validator(mode='after')
def debug_output(self, validation_info) -> 'MirrorStatusEntryV3':
self._hostname, *_port = urllib.parse.urlparse(self.url).netloc.split(':', 1)
self._port = int(_port[0]) if _port and len(_port) >= 1 else None
@ -108,16 +109,19 @@ class MirrorStatusEntryV3(pydantic.BaseModel):
return self
class MirrorStatusListV3(pydantic.BaseModel):
class MirrorStatusListV3(BaseModel):
cutoff: int
last_check: datetime.datetime
num_checks: int
urls: List[MirrorStatusEntryV3]
version: int
@pydantic.model_validator(mode='before')
@model_validator(mode='before')
@classmethod
def check_model(cls, data: Dict[str, int | datetime.datetime | List[MirrorStatusEntryV3]]) -> Dict[str, int | datetime.datetime | List[MirrorStatusEntryV3]]:
def check_model(
cls,
data: Dict[str, int | datetime.datetime | List[MirrorStatusEntryV3]]
) -> Dict[str, int | datetime.datetime | List[MirrorStatusEntryV3]]:
if data.get('version') == 3:
return data

View File

@ -14,6 +14,7 @@ from urllib.request import urlopen
from .exceptions import SysCallError, DownloadTimeout
from .output import error, info
from .pacman import Pacman
from .output import debug
class DownloadTimer():
@ -191,7 +192,7 @@ def ping(hostname, timeout=5) -> int:
latency = round((time.time() - started) * 1000)
break
except socket.error as error:
print(f"Error: {error}")
debug(f"Error: {error}")
break
icmp_socket.close()

View File

@ -100,7 +100,7 @@ class FormattedOutput:
value = record.get(key, '')
if '!' in key:
value = '*' * width
value = '*' * len(value)
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
obj_data.append(unicode_rjust(str(value), width))
@ -322,14 +322,9 @@ def log(
Journald.log(text, level=level)
from .menu import Menu
if not Menu.is_menu_active():
# Finally, print the log unless we skipped it based on level.
# We use sys.stdout.write()+flush() instead of print() to try and
# fix issue #94
if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False):
sys.stdout.write(f"{text}\n")
sys.stdout.flush()
if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False):
from archinstall.tui import Tui
Tui.print(text)
def _count_wchars(string: str) -> int:

View File

@ -1,13 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional, Dict
from typing import TYPE_CHECKING, Any, Optional, Dict, List
from archinstall.default_profiles.profile import Profile, GreeterType
from .profile_model import ProfileConfiguration
from ..menu import Menu, MenuSelectionType, AbstractSubMenu, Selector
from ..menu import AbstractSubMenu
from ..interactions.system_conf import select_driver
from ..hardware import GfxDriver
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
Orientation
)
if TYPE_CHECKING:
_: Any
@ -15,7 +21,6 @@ if TYPE_CHECKING:
class ProfileMenu(AbstractSubMenu):
def __init__(
self,
data_store: Dict[str, Any],
preset: Optional[ProfileConfiguration] = None
):
if preset:
@ -23,45 +28,50 @@ class ProfileMenu(AbstractSubMenu):
else:
self._preset = ProfileConfiguration()
super().__init__(data_store=data_store)
self._data_store: Dict[str, Any] = {}
def setup_selection_menu_options(self) -> None:
self._menu_options['profile'] = Selector(
_('Type'),
lambda x: self._select_profile(x),
display_func=lambda x: x.name if x else None,
preview_func=self._preview_profile,
default=self._preset.profile,
enabled=True
)
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
self._menu_options['gfx_driver'] = Selector(
_('Graphics driver'),
lambda preset: self._select_gfx_driver(preset),
display_func=lambda x: x.value if x else None,
dependencies=['profile'],
preview_func=self._preview_gfx,
default=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None,
enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False
)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
self._menu_options['greeter'] = Selector(
_('Greeter'),
lambda preset: select_greeter(self._menu_options['profile'].current_selection, preset),
display_func=lambda x: x.value if x else None,
dependencies=['profile'],
default=self._preset.greeter if self._preset.profile and self._preset.profile.is_greeter_supported() else None,
enabled=self._preset.profile.is_greeter_supported() if self._preset.profile else False
)
def _define_menu_options(self) -> List[MenuItem]:
return [
MenuItem(
text=str(_('Type')),
action=lambda x: self._select_profile(x),
value=self._preset.profile,
preview_action=self._preview_profile,
key='profile'
),
MenuItem(
text=str(_('Graphics driver')),
action=lambda x: self._select_gfx_driver(x),
value=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None,
preview_action=self._prev_gfx,
enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False,
dependencies=['profile'],
key='gfx_driver',
),
MenuItem(
text=str(_('Greeter')),
action=lambda x: select_greeter(preset=x),
value=self._preset.greeter if self._preset.profile and self._preset.profile.is_greeter_supported() else None,
enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False,
preview_action=self._prev_greeter,
dependencies=['profile'],
key='greeter',
)
]
def run(self, allow_reset: bool = True) -> Optional[ProfileConfiguration]:
super().run(allow_reset=allow_reset)
def run(self) -> Optional[ProfileConfiguration]:
super().run()
if self._data_store.get('profile', None):
return ProfileConfiguration(
self._menu_options['profile'].current_selection,
self._menu_options['gfx_driver'].current_selection,
self._menu_options['greeter'].current_selection
self._data_store.get('profile', None),
self._data_store.get('gfx_driver', None),
self._data_store.get('greeter', None),
)
return None
@ -71,52 +81,69 @@ class ProfileMenu(AbstractSubMenu):
if profile is not None:
if not profile.is_graphic_driver_supported():
self._menu_options['gfx_driver'].set_enabled(False)
self._menu_options['gfx_driver'].set_current_selection(None)
self._item_group.find_by_key('gfx_driver').enabled = False
self._item_group.find_by_key('gfx_driver').value = None
else:
self._menu_options['gfx_driver'].set_enabled(True)
self._menu_options['gfx_driver'].set_current_selection(GfxDriver.AllOpenSource)
self._item_group.find_by_key('gfx_driver').enabled = True
self._item_group.find_by_key('gfx_driver').value = GfxDriver.AllOpenSource
if not profile.is_greeter_supported():
self._menu_options['greeter'].set_enabled(False)
self._menu_options['greeter'].set_current_selection(None)
self._item_group.find_by_key('greeter').enabled = False
self._item_group.find_by_key('greeter').value = None
else:
self._menu_options['greeter'].set_enabled(True)
self._menu_options['greeter'].set_current_selection(profile.default_greeter_type)
self._item_group.find_by_key('greeter').enabled = True
self._item_group.find_by_key('greeter').value = profile.default_greeter_type
else:
self._menu_options['gfx_driver'].set_current_selection(None)
self._menu_options['greeter'].set_current_selection(None)
self._item_group.find_by_key('gfx_driver').value = None
self._item_group.find_by_key('greeter').value = None
return profile
def _select_gfx_driver(self, preset: Optional[GfxDriver] = None) -> Optional[GfxDriver]:
driver = preset
profile: Optional[Profile] = self._menu_options['profile'].current_selection
profile: Optional[Profile] = self._item_group.find_by_key('profile').value
if profile:
if profile.is_graphic_driver_supported():
driver = select_driver(current_value=preset)
driver = select_driver(preset=preset)
if driver and 'Sway' in profile.current_selection_names():
if driver.is_nvidia():
prompt = str(_('The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?'))
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run()
header = str(_('The proprietary Nvidia driver is not supported by Sway.')) + '\n'
header += str(_('It is likely that you will run into issues, are you okay with that?')) + '\n'
if choice.value == Menu.no():
return None
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.no()
group.default_item = MenuItem.no()
result = SelectMenu(
group,
header=header,
allow_skip=False,
columns=2,
orientation=Orientation.HORIZONTAL,
alignment=Alignment.CENTER
).run()
if result.item() == MenuItem.no():
return preset
return driver
def _preview_gfx(self) -> Optional[str]:
driver: Optional[GfxDriver] = self._menu_options['gfx_driver'].current_selection
if driver:
return driver.packages_text()
def _prev_gfx(self, item: MenuItem) -> Optional[str]:
if item.value:
driver = item.get_value().value
packages = item.get_value().packages_text()
return f'Driver: {driver}\n{packages}'
return None
def _preview_profile(self) -> Optional[str]:
profile: Optional[Profile] = self._menu_options['profile'].current_selection
def _prev_greeter(self, item: MenuItem) -> Optional[str]:
if item.value:
return f'{str(_("Greeter"))}: {item.value.value}'
return None
def _preview_profile(self, item: MenuItem) -> Optional[str]:
profile: Optional[Profile] = item.value
text = ''
if profile:
@ -138,66 +165,71 @@ def select_greeter(
preset: Optional[GreeterType] = None
) -> Optional[GreeterType]:
if not profile or profile.is_greeter_supported():
title = str(_('Please chose which greeter to install'))
greeter_options = [greeter.value for greeter in GreeterType]
items = [MenuItem(greeter.value, value=greeter) for greeter in GreeterType]
group = MenuItemGroup(items, sort_items=True)
default: Optional[GreeterType] = None
if preset is not None:
default = preset
elif profile is not None:
default_greeter = profile.default_greeter_type
default = default_greeter if default_greeter else None
choice = Menu(
title,
greeter_options,
skip=True,
default_option=default.value if default else None
group.set_default_by_value(default)
result = SelectMenu(
group,
allow_skip=True,
frame=FrameProperties.min(str(_('Greeter'))),
alignment=Alignment.CENTER
).run()
match choice.type_:
case MenuSelectionType.Skip:
return default
return GreeterType(choice.single_value)
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
return None
def select_profile(
current_profile: Optional[Profile] = None,
title: Optional[str] = None,
header: Optional[str] = None,
allow_reset: bool = True,
multi: bool = False
) -> Optional[Profile]:
from archinstall.lib.profile.profiles_handler import profile_handler
top_level_profiles = profile_handler.get_top_level_profiles()
display_title = title
if not display_title:
display_title = str(_('This is a list of pre-programmed default_profiles'))
if header is None:
header = str(_('This is a list of pre-programmed default_profiles')) + '\n'
choice = profile_handler.select_profile(
top_level_profiles,
current_profile=current_profile,
title=display_title,
items = [MenuItem(p.name, value=p) for p in top_level_profiles]
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(current_profile)
result = SelectMenu(
group,
header=header,
allow_reset=allow_reset,
multi=multi
)
allow_skip=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Main profile')))
).run()
match choice.type_:
case MenuSelectionType.Selection:
profile_selection: Profile = choice.single_value
match result.type_:
case ResultType.Reset:
return None
case ResultType.Skip:
return current_profile
case ResultType.Selection:
profile_selection: Profile = result.get_value()
select_result = profile_selection.do_on_select()
if not select_result:
return select_profile(
current_profile=current_profile,
title=title,
allow_reset=allow_reset,
multi=multi
)
return None
# we're going to reset the currently selected profile(s) to avoid
# any stale data laying around
@ -212,7 +244,5 @@ def select_profile(
pass
return current_profile
case MenuSelectionType.Reset:
return None
case MenuSelectionType.Skip:
return current_profile
return None

View File

@ -13,11 +13,11 @@ from typing import List, TYPE_CHECKING, Any, Optional, Dict, Union
from ...default_profiles.profile import Profile, GreeterType
from .profile_model import ProfileConfiguration
from ..hardware import GfxDriver
from ..menu import MenuSelectionType, Menu, MenuSelection
from ..networking import list_interfaces, fetch_data_from_url
from ..output import error, debug, info
from ..storage import storage
if TYPE_CHECKING:
from ..installer import Installer
_: Any
@ -358,58 +358,5 @@ class ProfileHandler:
if profile.name not in excluded_profiles:
profile.reset()
def select_profile(
self,
selectable_profiles: List[Profile],
current_profile: Optional[Union[Profile, List[Profile]]] = None,
title: str = '',
allow_reset: bool = True,
multi: bool = False,
) -> MenuSelection:
"""
Helper function to perform a profile selection
"""
options = {p.name: p for p in selectable_profiles}
options = dict((k, v) for k, v in sorted(options.items(), key=lambda x: x[0].upper()))
warning = str(_('Are you sure you want to reset this setting?'))
preset_value: Optional[Union[str, List[str]]] = None
if current_profile is not None:
if isinstance(current_profile, list):
preset_value = [p.name for p in current_profile]
else:
preset_value = current_profile.name
choice = Menu(
title=title,
preset_values=preset_value,
p_options=options,
allow_reset=allow_reset,
allow_reset_warning_msg=warning,
multi=multi,
sort=False,
preview_command=self.preview_text,
preview_size=0.5
).run()
if choice.type_ == MenuSelectionType.Selection:
value = choice.value
if multi:
# this is quite dirty and should eb switched to a
# dedicated return type instead
choice.value = [options[val] for val in value] # type: ignore
else:
choice.value = options[value] # type: ignore
return choice
def preview_text(self, selection: str) -> Optional[str]:
"""
Callback for preview display on profile selection
"""
profile = self.get_profile_by_name(selection)
return profile.preview_text() if profile is not None else None
profile_handler = ProfileHandler()

View File

@ -2,22 +2,98 @@ from pathlib import Path
from typing import Any, TYPE_CHECKING, Optional, List
from ..output import FormattedOutput
from ..output import info
from ..general import secret
from archinstall.tui import (
Alignment, EditMenu
)
if TYPE_CHECKING:
_: Any
def prompt_dir(text: str, header: Optional[str] = None) -> Path:
if header:
print(header)
def get_password(
text: str,
header: Optional[str] = None,
allow_skip: bool = False,
preset: Optional[str] = None
) -> Optional[str]:
failure: Optional[str] = None
while True:
path = input(text).strip(' ')
user_hdr = None
if failure is not None:
user_hdr = f'{header}\n{failure}\n'
elif header is not None:
user_hdr = header
result = EditMenu(
text,
header=user_hdr,
alignment=Alignment.CENTER,
allow_skip=allow_skip,
default_text=preset,
hide_input=True
).input()
if allow_skip and not result.has_item():
return None
password = result.text()
hidden = secret(password)
if header is not None:
confirmation_header = f'{header}{str(_("Pssword"))}: {hidden}\n'
else:
confirmation_header = f'{str(_("Password"))}: {hidden}\n'
result = EditMenu(
str(_('Confirm password')),
header=confirmation_header,
alignment=Alignment.CENTER,
allow_skip=False,
hide_input=True
).input()
if password == result.text():
return password
failure = str(_('The confirmation password did not match, please try again'))
def prompt_dir(
text: str,
header: Optional[str] = None,
validate: bool = True,
allow_skip: bool = False,
preset: Optional[str] = None
) -> Optional[Path]:
def validate_path(path: str) -> Optional[str]:
dest_path = Path(path)
if dest_path.exists() and dest_path.is_dir():
return dest_path
info(_('Not a valid directory: {}').format(dest_path))
return None
return str(_('Not a valid directory'))
if validate:
validate_func = validate_path
else:
validate_func = None
result = EditMenu(
text,
header=header,
alignment=Alignment.CENTER,
allow_skip=allow_skip,
validator=validate_func,
default_text=preset
).input()
if allow_skip and not result.has_item():
return None
return Path(result.text())
def is_subpath(first: Path, second: Path) -> bool:
@ -48,4 +124,6 @@ def format_cols(items: List[str], header: Optional[str] = None) -> str:
col = 4
text += FormattedOutput.as_columns(items, col)
# remove whitespaces on each row
text = '\n'.join([t.strip() for t in text.split('\n')])
return text

View File

@ -9,10 +9,11 @@ from archinstall.lib import disk
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.installer import Installer
from archinstall.lib.menu import Menu
from archinstall.lib.models import AudioConfiguration, Bootloader
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.interactions.general_conf import ask_chroot
from archinstall.tui import Tui
if TYPE_CHECKING:
_: Any
@ -25,76 +26,18 @@ if archinstall.arguments.get('help'):
def ask_user_questions() -> None:
"""
First, we'll ask the user for a bunch of user input.
Not until we're satisfied with what we want to install
will we continue with the actual installation steps.
First, we'll ask the user for a bunch of user input.
Not until we're satisfied with what we want to install
will we continue with the actual installation steps.
"""
# ref: https://github.com/archlinux/archinstall/pull/831
# we'll set NTP to true by default since this is also
# the default value specified in the menu options; in
# case it will be changed by the user we'll also update
# the system immediately
global_menu = GlobalMenu(data_store=archinstall.arguments)
with Tui():
global_menu = GlobalMenu(data_store=archinstall.arguments)
global_menu.enable('archinstall-language')
if not archinstall.arguments.get('advanced', False):
global_menu.set_enabled('parallel downloads', False)
# Set which region to download packages from during the installation
global_menu.enable('mirror_config')
global_menu.enable('locale_config')
global_menu.enable('disk_config', mandatory=True)
# Specify disk encryption options
global_menu.enable('disk_encryption')
# Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB)
global_menu.enable('bootloader')
global_menu.enable('uki')
global_menu.enable('swap')
# Get the hostname for the machine
global_menu.enable('hostname')
# Ask for a root password (optional, but triggers requirement for super-user if skipped)
global_menu.enable('!root-password', mandatory=True)
global_menu.enable('!users', mandatory=True)
# Ask for archinstall-specific profiles_bck (such as desktop environments etc)
global_menu.enable('profile_config')
# Ask about audio server selection if one is not already set
global_menu.enable('audio_config')
# Ask for preferred kernel:
global_menu.enable('kernels', mandatory=True)
global_menu.enable('packages')
if archinstall.arguments.get('advanced', False):
# Enable parallel downloads
global_menu.enable('parallel downloads')
# Ask or Call the helper function that asks the user to optionally configure a network.
global_menu.enable('network_config')
global_menu.enable('timezone')
global_menu.enable('ntp')
global_menu.enable('additional-repositories')
global_menu.enable('__separator__')
global_menu.enable('save_config')
global_menu.enable('install')
global_menu.enable('abort')
global_menu.run()
global_menu.run()
def perform_installation(mountpoint: Path) -> None:
@ -209,9 +152,10 @@ def perform_installation(mountpoint: Path) -> None:
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
if not archinstall.arguments.get('silent'):
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?'))
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run()
if choice.value == Menu.yes():
with Tui():
chroot = ask_chroot()
if chroot:
try:
installation.drop_to_shell()
except:
@ -220,27 +164,30 @@ def perform_installation(mountpoint: Path) -> None:
debug(f"Disk states after installing: {disk.disk_layouts()}")
if not archinstall.arguments.get('silent'):
ask_user_questions()
def guided() -> None:
if not archinstall.arguments.get('silent'):
ask_user_questions()
config_output = ConfigurationOutput(archinstall.arguments)
config = ConfigurationOutput(archinstall.arguments)
config.write_debug()
config.save()
if not archinstall.arguments.get('silent'):
config_output.show()
if archinstall.arguments.get('dry_run'):
exit(0)
config_output.save()
if not archinstall.arguments.get('silent'):
with Tui():
if not config.confirm_config():
debug('Installation aborted')
guided()
if archinstall.arguments.get('dry_run'):
exit(0)
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if not archinstall.arguments.get('silent'):
input(str(_('Press Enter to continue.')))
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
guided()

View File

@ -2,13 +2,14 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any, List
import archinstall
from archinstall import info
from archinstall import info, debug
from archinstall import Installer, ConfigurationOutput
from archinstall.default_profiles.minimal import MinimalProfile
from archinstall.lib.interactions import suggest_single_disk_layout, select_devices
from archinstall.lib.models import Bootloader, User
from archinstall.lib.profile import ProfileConfiguration, profile_handler
from archinstall.lib import disk
from archinstall.tui import Tui
if TYPE_CHECKING:
_: Any
@ -88,19 +89,31 @@ def parse_disk_encryption() -> None:
)
prompt_disk_layout()
parse_disk_encryption()
def minimal() -> None:
with Tui():
prompt_disk_layout()
parse_disk_encryption()
config_output = ConfigurationOutput(archinstall.arguments)
config_output.show()
config = ConfigurationOutput(archinstall.arguments)
config.write_debug()
config.save()
input(str(_('Press Enter to continue.')))
if archinstall.arguments.get('dry_run'):
exit(0)
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if not archinstall.arguments.get('silent'):
with Tui():
if not config.confirm_config():
debug('Installation aborted')
minimal()
fs_handler.perform_filesystem_operations()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
minimal()

View File

@ -5,22 +5,23 @@ from archinstall import debug
from archinstall.lib.installer import Installer
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib import disk
from archinstall.tui import Tui
def ask_user_questions() -> None:
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
with Tui():
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
global_menu.disable_all()
global_menu.enable('archinstall-language')
global_menu.set_enabled('archinstall-language', True)
global_menu.set_enabled('disk_config', True)
global_menu.set_enabled('disk_encryption', True)
global_menu.set_enabled('swap', True)
global_menu.set_enabled('save_config', True)
global_menu.set_enabled('install', True)
global_menu.set_enabled('abort', True)
global_menu.enable('disk_config', mandatory=True)
global_menu.enable('disk_encryption')
global_menu.enable('swap')
global_menu.enable('save_config')
global_menu.enable('install')
global_menu.enable('abort')
global_menu.run()
global_menu.run()
def perform_installation(mountpoint: Path) -> None:
@ -52,27 +53,30 @@ def perform_installation(mountpoint: Path) -> None:
debug(f"Disk states after installing: {disk.disk_layouts()}")
if not archinstall.arguments.get('silent'):
ask_user_questions()
def only_hd() -> None:
if not archinstall.arguments.get('silent'):
ask_user_questions()
config_output = ConfigurationOutput(archinstall.arguments)
if not archinstall.arguments.get('silent'):
config_output.show()
config = ConfigurationOutput(archinstall.arguments)
config.write_debug()
config.save()
config_output.save()
if archinstall.arguments.get('dry_run'):
exit(0)
if archinstall.arguments.get('dry_run'):
exit(0)
if not archinstall.arguments.get('silent'):
with Tui():
if not config.confirm_config():
debug('Installation aborted')
only_hd()
if not archinstall.arguments.get('silent'):
input('Press Enter to continue.')
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
fs_handler.perform_filesystem_operations()
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
only_hd()

View File

@ -9,158 +9,122 @@ from archinstall.lib import disk
from archinstall.lib import locale
from archinstall.lib.models import AudioConfiguration
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib import menu
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.interactions.general_conf import ask_chroot
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
Tui
)
if TYPE_CHECKING:
_: Any
class ExecutionMode(Enum):
Full = 'full'
Guided = 'guided'
Lineal = 'lineal'
Only_HD = 'only-hd'
Only_OS = 'only-os'
Minimal = 'minimal'
def select_mode() -> ExecutionMode:
options = [str(e.value) for e in ExecutionMode]
choice = menu.Menu(
str(_('Select an execution mode')),
options,
default_option=ExecutionMode.Full.value,
skip=False
).run()
return ExecutionMode(choice.single_value)
class SetupMenu(GlobalMenu):
def __init__(self, storage_area: Dict[str, Any]):
super().__init__(data_store=storage_area)
def setup_selection_menu_options(self) -> None:
super().setup_selection_menu_options()
self._menu_options['mode'] = menu.Selector(
'Execution mode',
lambda x: select_mode(),
display_func=lambda x: x.value if x else '',
default=ExecutionMode.Full)
self._menu_options['continue'] = menu.Selector(
'Continue',
exec_func=lambda n, v: True)
self.enable('archinstall-language')
self.enable('ntp')
self.enable('mode')
self.enable('continue')
self.enable('abort')
def exit_callback(self) -> None:
if self._data_store.get('mode', None):
archinstall.arguments['mode'] = self._data_store['mode']
info(f"Archinstall will execute under {archinstall.arguments['mode']} mode")
class SwissMainMenu(GlobalMenu):
def __init__(
self,
data_store: Dict[str, Any],
exec_mode: ExecutionMode = ExecutionMode.Full
mode: ExecutionMode = ExecutionMode.Guided,
advanced: bool = False
):
self._execution_mode = exec_mode
self._execution_mode = mode
self._advanced = advanced
super().__init__(data_store)
def setup_selection_menu_options(self) -> None:
super().setup_selection_menu_options()
options_list = []
mandatory_list = []
def execute(self) -> None:
ignore = ['install', 'abort']
match self._execution_mode:
case ExecutionMode.Full | ExecutionMode.Lineal:
options_list = [
'mirror_config', 'disk_config',
'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password',
'!users', 'profile_config', 'audio_config', 'kernels', 'packages', 'additional-repositories', 'network_config',
'timezone', 'ntp'
]
if archinstall.arguments.get('advanced', False):
options_list.extend(['locale_config'])
mandatory_list = ['disk_config', 'bootloader', 'hostname']
case ExecutionMode.Guided:
from archinstall.scripts.guided import guided
guided()
case ExecutionMode.Only_HD:
options_list = ['disk_config', 'disk_encryption', 'swap']
mandatory_list = ['disk_config']
case ExecutionMode.Only_OS:
options_list = [
'mirror_config', 'bootloader', 'hostname',
'!root-password', '!users', 'profile_config', 'audio_config', 'kernels',
'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp'
]
from archinstall.scripts.only_hd import only_hd
only_hd()
case ExecutionMode.Minimal:
from archinstall.scripts.minimal import minimal
minimal()
case ExecutionMode.Lineal:
for item in self._menu_item_group.items:
if self._menu_item_group.should_enable_item(item):
if item.action is not None and item.key is not None:
if item.key not in ignore:
archinstall.arguments[item.key] = item.action(item.value)
perform_installation(
archinstall.storage.get('MOUNT_POINT', Path('/mnt')),
self._execution_mode
)
case ExecutionMode.Only_OS:
menu_items = [
'mirror_config', 'bootloader', 'hostname',
'!root-password', '!users', 'profile_config',
'audio_config', 'kernels', 'packages',
'additional-repositories', 'network_config', 'timezone', 'ntp'
]
mandatory_list = ['hostname']
if archinstall.arguments.get('advanced', False):
options_list += ['locale_config']
case ExecutionMode.Minimal:
pass
if self._advanced:
menu_items += ['locale_config']
for item in self._menu_item_group.items:
if self._menu_item_group.should_enable_item(item):
if item.action is not None and item.key is not None:
if item.key not in ignore and item.key in menu_items:
while True:
value = item.action(item.value)
if value not in mandatory_list or value is not None:
archinstall.arguments[item.key] = item.action(item.value)
break
perform_installation(
archinstall.storage.get('MOUNT_POINT', Path('/mnt')),
self._execution_mode
)
case _:
info(f' Execution mode {self._execution_mode} not supported')
exit(1)
if self._execution_mode != ExecutionMode.Lineal:
options_list += ['save_config', 'install']
if not archinstall.arguments.get('advanced', False):
options_list.append('archinstall-language')
def ask_user_questions(mode: ExecutionMode = ExecutionMode.Guided) -> None:
advanced = archinstall.arguments.get('advanced', False)
options_list += ['abort']
with Tui():
if advanced:
header = str(_('Select execution mode'))
items = [MenuItem(ex.name, value=ex) for ex in ExecutionMode]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(ExecutionMode.Guided)
for entry in mandatory_list:
self.enable(entry, mandatory=True)
result = SelectMenu(
group,
header=header,
allow_skip=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Modes')))
).run()
for entry in options_list:
self.enable(entry)
if result.type_ == ResultType.Skip:
exit(0)
mode = result.get_value()
def ask_user_questions(exec_mode: ExecutionMode = ExecutionMode.Full) -> None:
"""
First, we'll ask the user for a bunch of user input.
Not until we're satisfied with what we want to install
will we continue with the actual installation steps.
"""
if archinstall.arguments.get('advanced', None):
setup_area: Dict[str, Any] = {}
setup = SetupMenu(setup_area)
if exec_mode == ExecutionMode.Lineal:
for entry in setup.list_enabled_options():
if entry in ('continue', 'abort'):
continue
if not setup.option(entry).enabled:
continue
setup.exec_option(entry)
else:
setup.run()
archinstall.arguments['archinstall-language'] = setup_area.get('archinstall-language')
with SwissMainMenu(data_store=archinstall.arguments, exec_mode=exec_mode) as menu:
if mode == ExecutionMode.Lineal:
for entry in menu.list_enabled_options():
if entry in ('install', 'abort'):
continue
menu.exec_option(entry)
archinstall.arguments[entry] = menu.option(entry).get_selection()
else:
menu.run()
SwissMainMenu(
data_store=archinstall.arguments,
mode=mode,
advanced=advanced
).execute()
def perform_installation(mountpoint: Path, exec_mode: ExecutionMode) -> None:
@ -177,132 +141,134 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode) -> None:
disk_encryption=disk_encryption,
kernels=archinstall.arguments.get('kernels', ['linux'])
) as installation:
if exec_mode in [ExecutionMode.Full, ExecutionMode.Only_HD]:
installation.mount_ordered_layout()
installation.mount_ordered_layout()
installation.sanity_check()
installation.sanity_check()
if disk_config.config_type != disk.DiskLayoutType.Pre_mount:
if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption:
# generate encryption key files for the mounted luks devices
installation.generate_key_files()
if disk_config.config_type != disk.DiskLayoutType.Pre_mount:
if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption:
# generate encryption key files for the mounted luks devices
installation.generate_key_files()
if mirror_config := archinstall.arguments.get('mirror_config', None):
installation.set_mirrors(mirror_config)
if mirror_config := archinstall.arguments.get('mirror_config', None):
installation.set_mirrors(mirror_config)
installation.minimal_installation(
testing=enable_testing,
multilib=enable_multilib,
hostname=archinstall.arguments.get('hostname', 'archlinux'),
locale_config=locale_config
installation.minimal_installation(
testing=enable_testing,
multilib=enable_multilib,
hostname=archinstall.arguments.get('hostname', 'archlinux'),
locale_config=locale_config
)
if mirror_config := archinstall.arguments.get('mirror_config', None):
installation.set_mirrors(mirror_config, on_target=True)
if archinstall.arguments.get('swap'):
installation.setup_swap('zram')
if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi():
installation.add_additional_packages("grub")
installation.add_bootloader(archinstall.arguments["bootloader"])
# If user selected to copy the current ISO network configuration
# Perform a copy of the config
network_config = archinstall.arguments.get('network_config', None)
if network_config:
network_config.install_network_config(
installation,
archinstall.arguments.get('profile_config', None)
)
if mirror_config := archinstall.arguments.get('mirror_config', None):
installation.set_mirrors(mirror_config, on_target=True)
if users := archinstall.arguments.get('!users', None):
installation.create_users(users)
if archinstall.arguments.get('swap'):
installation.setup_swap('zram')
audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None)
if audio_config:
audio_config.install_audio_config(installation)
else:
info("No audio server will be installed")
if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi():
installation.add_additional_packages("grub")
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages(archinstall.arguments.get('packages', []))
installation.add_bootloader(archinstall.arguments["bootloader"])
if profile_config := archinstall.arguments.get('profile_config', None):
profile_handler.install_profile_config(installation, profile_config)
# If user selected to copy the current ISO network configuration
# Perform a copy of the config
network_config = archinstall.arguments.get('network_config', None)
if timezone := archinstall.arguments.get('timezone', None):
installation.set_timezone(timezone)
if network_config:
network_config.install_network_config(
installation,
archinstall.arguments.get('profile_config', None)
)
if archinstall.arguments.get('ntp', False):
installation.activate_time_synchronization()
if users := archinstall.arguments.get('!users', None):
installation.create_users(users)
if archinstall.accessibility_tools_in_use():
installation.enable_espeakup()
audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None)
if audio_config:
audio_config.install_audio_config(installation)
else:
info("No audio server will be installed")
if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw):
installation.user_set_pw('root', root_pw)
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages(archinstall.arguments.get('packages', []))
if profile_config := archinstall.arguments.get('profile_config', None):
profile_config.profile.post_install(installation)
if profile_config := archinstall.arguments.get('profile_config', None):
profile_handler.install_profile_config(installation, profile_config)
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
if archinstall.arguments.get('services', None):
installation.enable_service(archinstall.arguments.get('services', []))
if timezone := archinstall.arguments.get('timezone', None):
installation.set_timezone(timezone)
# If the user provided custom commands to be run post-installation, execute them now.
if archinstall.arguments.get('custom-commands', None):
archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation)
if archinstall.arguments.get('ntp', False):
installation.activate_time_synchronization()
installation.genfstab()
if archinstall.accessibility_tools_in_use():
installation.enable_espeakup()
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw):
installation.user_set_pw('root', root_pw)
if not archinstall.arguments.get('silent'):
with Tui():
chroot = ask_chroot()
if profile_config := archinstall.arguments.get('profile_config', None):
profile_config.profile.post_install(installation)
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
if archinstall.arguments.get('services', None):
installation.enable_service(archinstall.arguments.get('services', []))
# If the user provided custom commands to be run post-installation, execute them now.
if archinstall.arguments.get('custom-commands', None):
archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation)
installation.genfstab()
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
if not archinstall.arguments.get('silent'):
prompt = str(
_('Would you like to chroot into the newly created installation and perform post-installation configuration?'))
choice = menu.Menu(prompt, menu.Menu.yes_no(), default_option=menu.Menu.yes()).run()
if choice.value == menu.Menu.yes():
try:
installation.drop_to_shell()
except:
pass
if chroot:
try:
installation.drop_to_shell()
except:
pass
debug(f"Disk states after installing: {disk.disk_layouts()}")
param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower()
def swiss() -> None:
param_mode = archinstall.arguments.get('mode', ExecutionMode.Guided.value).lower()
try:
mode = ExecutionMode(param_mode)
except KeyError:
info(f'Mode "{param_mode}" is not supported')
exit(1)
try:
mode = ExecutionMode(param_mode)
except KeyError:
info(f'Mode "{param_mode}" is not supported')
exit(1)
if not archinstall.arguments.get('silent'):
ask_user_questions(mode)
if not archinstall.arguments.get('silent'):
ask_user_questions(mode)
config_output = ConfigurationOutput(archinstall.arguments)
if not archinstall.arguments.get('silent'):
config_output.show()
config = ConfigurationOutput(archinstall.arguments)
config.write_debug()
config.save()
config_output.save()
if archinstall.arguments.get('dry_run'):
exit(0)
if archinstall.arguments.get('dry_run'):
exit(0)
if not archinstall.arguments.get('silent'):
with Tui():
if not config.confirm_config():
debug('Installation aborted')
swiss()
if not archinstall.arguments.get('silent'):
input('Press Enter to continue.')
if mode in (ExecutionMode.Full, ExecutionMode.Only_HD):
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')), mode)
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')), mode)
swiss()

View File

@ -3,6 +3,8 @@ import time
import archinstall
from archinstall import info
from archinstall import profile
from archinstall.tui import Tui
for p in profile.profile_handler.get_mac_addr_profiles():
# Tailored means it's a match for this machine
@ -12,7 +14,7 @@ for p in profile.profile_handler.get_mac_addr_profiles():
print('Starting install in:')
for i in range(10, 0, -1):
print(f'{i}...')
Tui.print(f'{i}...')
time.sleep(1)
install_session = archinstall.storage['installation_session']

View File

@ -0,0 +1,12 @@
from .curses_menu import (
SelectMenu, EditMenu, Tui
)
from .menu_item import (
MenuItem, MenuItemGroup
)
from .types import (
PreviewStyle, FrameProperties, FrameStyle, Alignment,
Result, ResultType, Chars, Orientation
)

File diff suppressed because it is too large Load Diff

View File

@ -53,6 +53,8 @@ class Help:
selection = HelpGroup(
group_id=HelpTextGroupId.SELECTION,
group_entries=[
HelpText('Skip selction (if available)', ['Esc']),
HelpText('Reset selection (if available)', ['Ctrl+c']),
HelpText('Select on single select', ['Enter']),
HelpText('Select on select', ['Space', 'Tab']),
HelpText('Reset', ['Ctrl-C']),
@ -75,18 +77,14 @@ class Help:
max_desc_width = max([help.get_desc_width() for help in help_texts])
max_key_width = max([help.get_key_width() for help in help_texts])
margin = ' ' * 3
for help in help_texts:
help_output += f'{margin}{help.group_id.value}\n'
divider_len = max_desc_width + max_key_width + len(margin * 2)
help_output += margin + '-' * divider_len + '\n'
help_output += f'{help.group_id.value}\n'
divider_len = max_desc_width + max_key_width
help_output += '-' * divider_len + '\n'
for entry in help.group_entries:
help_output += (
margin +
entry.description.ljust(max_desc_width, ' ') +
margin +
', '.join(entry.keys) + '\n'
)

View File

@ -1,6 +1,6 @@
from dataclasses import dataclass, field
from typing import Any, Self, Optional, List, TYPE_CHECKING
from typing import Callable
from typing import Any, Optional, List, TYPE_CHECKING
from typing import Callable, ClassVar
from ..lib.output import unicode_ljust
@ -15,42 +15,51 @@ class MenuItem:
action: Optional[Callable[[Any], Any]] = None
enabled: bool = True
mandatory: bool = False
dependencies: List[Self] = field(default_factory=list)
dependencies_not: List[Self] = field(default_factory=list)
dependencies: List[str | Callable[[], bool]] = field(default_factory=list)
dependencies_not: List[str] = field(default_factory=list)
display_action: Optional[Callable[[Any], str]] = None
preview_action: Optional[Callable[[Any], Optional[str]]] = None
key: Optional[Any] = None
key: Optional[str] = None
_yes: ClassVar[Optional['MenuItem']] = None
_no: ClassVar[Optional['MenuItem']] = None
def get_value(self) -> Any:
assert self.value is not None
return self.value
@classmethod
def default_yes(cls) -> Self:
return cls(str(_('Yes')))
def yes(cls) -> 'MenuItem':
if cls._yes is None:
cls._yes = cls(str(_('Yes')), value=True)
return cls._yes
@classmethod
def default_no(cls) -> Self:
return cls(str(_('No')))
def no(cls) -> 'MenuItem':
if cls._no is None:
cls._no = cls(str(_('No')), value=True)
return cls._no
def is_empty(self) -> bool:
return self.text == '' or self.text is None
def get_text(self, spacing: int = 0, suffix: str = '') -> str:
if self.is_empty():
return ''
value_text = ''
if self.display_action:
value_text = self.display_action(self.value)
def has_value(self) -> bool:
if self.value is None:
return False
elif isinstance(self.value, list) and len(self.value) == 0:
return False
elif isinstance(self.value, dict) and len(self.value) == 0:
return False
else:
if self.value is not None:
value_text = str(self.value)
return True
if value_text:
spacing += 2
text = unicode_ljust(str(self.text), spacing, ' ')
else:
text = self.text
def get_display_value(self) -> Optional[str]:
if self.display_action is not None:
return self.display_action(self.value)
return f'{text} {value_text}{suffix}'.rstrip(' ')
return None
@dataclass
@ -59,11 +68,12 @@ class MenuItemGroup:
focus_item: Optional[MenuItem] = None
default_item: Optional[MenuItem] = None
selected_items: List[MenuItem] = field(default_factory=list)
sort_items: bool = True
sort_items: bool = False
checkmarks: bool = False
_filter_pattern: str = ''
def __post_init__(self) -> None:
def __post_init__(self):
if len(self.menu_items) < 1:
raise ValueError('Menu must have at least one item')
@ -79,18 +89,58 @@ class MenuItemGroup:
if self.focus_item not in self.menu_items:
raise ValueError('Selected item not in menu')
def find_by_key(self, key: str) -> MenuItem:
for item in self.menu_items:
if item.key == key:
return item
raise ValueError(f'No key found for: {key}')
@staticmethod
def default_confirm() -> 'MenuItemGroup':
def yes_no() -> 'MenuItemGroup':
return MenuItemGroup(
[MenuItem.default_yes(), MenuItem.default_no()],
sort_items=False
[MenuItem.yes(), MenuItem.no()],
sort_items=True
)
def index_of(self, item) -> int:
def set_preview_for_all(self, action: Callable[[Any], Optional[str]]) -> None:
for item in self.items:
item.preview_action = action
def set_focus_by_value(self, value: Any) -> None:
for item in self.menu_items:
if item.value == value:
self.focus_item = item
break
def set_default_by_value(self, value: Any) -> None:
for item in self.menu_items:
if item.value == value:
self.default_item = item
break
def set_selected_by_value(self, values: Optional[Any | List[Any]]) -> None:
if values is None:
return
if not isinstance(values, list):
values = [values]
for item in self.menu_items:
if item.value in values:
self.selected_items.append(item)
if values:
self.set_focus_by_value(values[0])
def index_of(self, item: MenuItem) -> int:
return self.items.index(item)
def index_focus(self) -> int:
return self.index_of(self.focus_item)
if self.focus_item:
return self.index_of(self.focus_item)
raise ValueError('No focus item set')
def index_last(self) -> int:
return self.index_of(self.items[-1])
@ -106,8 +156,43 @@ class MenuItemGroup:
def max_width(self) -> int:
# use the menu_items not the items here otherwise the preview
# will get resized all the time when a filter is applied
return max([len(self.get_item_text(item)) for item in self.menu_items])
def _max_item_width(self) -> int:
return max([len(item.text) for item in self.menu_items])
def get_item_text(self, item: MenuItem) -> str:
if item.is_empty():
return ''
max_width = self._max_item_width()
display_text = item.get_display_value()
default_text = self._default_suffix(item)
text = unicode_ljust(str(item.text), max_width, ' ')
spacing = ' ' * 4
if display_text:
text = f'{text}{spacing}{display_text}'
elif self.checkmarks:
from .types import Chars
if item.has_value():
if item.get_value() is not False:
text = f'{text}{spacing}{Chars.Check}'
else:
text = item.text
if default_text:
text = f'{text} {default_text}'
return text.rstrip(' ')
def _default_suffix(self, item: MenuItem) -> str:
if self.default_item == item:
return str(_(' (default)'))
return ''
@property
def items(self) -> List[MenuItem]:
f = self._filter_pattern.lower()
@ -115,14 +200,14 @@ class MenuItemGroup:
return list(items)
@property
def filter_pattern(self):
def filter_pattern(self) -> str:
return self._filter_pattern
def set_filter_pattern(self, pattern: str) -> None:
self._filter_pattern = pattern
self.reload_focus_itme()
def append_filter(self, pattern: str) -> None:
def append_filter(self, pattern: str):
self._filter_pattern += pattern
self.reload_focus_itme()
@ -189,7 +274,7 @@ class MenuItemGroup:
if last_item:
self.focus_item = last_item
def focus_prev(self, skip_empty: bool = True) -> None:
def focus_prev(self, skip_empty: bool = True):
items = self.items
if self.focus_item not in items:
@ -203,7 +288,7 @@ class MenuItemGroup:
if self.focus_item.is_empty() and skip_empty:
self.focus_prev(skip_empty)
def focus_next(self, skip_empty: bool = True) -> None:
def focus_next(self, skip_empty: bool = True):
items = self.items
if self.focus_item not in items:
@ -229,19 +314,21 @@ class MenuItemGroup:
return max(spaces)
return 0
def verify_item_enabled(self, item: MenuItem) -> bool:
def should_enable_item(self, item: MenuItem) -> bool:
if not item.enabled:
return False
if item in self.menu_items:
for dep in item.dependencies:
if not self.verify_item_enabled(dep):
for dep in item.dependencies:
if isinstance(dep, str):
item = self.find_by_key(dep)
if not item.value or not self.should_enable_item(item):
return False
else:
return dep()
for dep in item.dependencies_not:
if dep.value is not None:
return False
for dep_not in item.dependencies_not:
item = self.find_by_key(dep_not)
if item.value is not None:
return False
return True
return False
return True

View File

@ -1,12 +1,10 @@
import curses
from dataclasses import dataclass
from enum import Enum, auto
from typing import Optional, List, TypeVar, Generic
from typing import Optional, List, Any
from .menu_item import MenuItem
ItemType = TypeVar('ItemType', MenuItem, List[MenuItem], str)
SCROLL_INTERVAL = 10
@ -46,8 +44,8 @@ class MenuKeys(Enum):
ESC = {27}
# BACKSPACE (search)
BACKSPACE = {127, 263}
# Help view: CTRL+h
HELP = {8}
# Help view: ?
HELP = {63}
# Scroll up: CTRL+up, CTRL+k
SCROLL_UP = {581}
# Scroll down: CTRL+down, CTRL+j
@ -80,6 +78,22 @@ class FrameProperties:
w_frame_style: FrameStyle = FrameStyle.MAX
h_frame_style: FrameStyle = FrameStyle.MAX
@classmethod
def max(cls, header: str) -> 'FrameProperties':
return FrameProperties(
header,
FrameStyle.MAX,
FrameStyle.MAX,
)
@classmethod
def min(cls, header: str) -> 'FrameProperties':
return FrameProperties(
header,
FrameStyle.MIN,
FrameStyle.MIN,
)
class ResultType(Enum):
Selection = auto()
@ -87,7 +101,7 @@ class ResultType(Enum):
Reset = auto()
class MenuOrientation(Enum):
class Orientation(Enum):
VERTICAL = auto()
HORIZONTAL = auto()
@ -106,6 +120,7 @@ class PreviewStyle(Enum):
# https://www.compart.com/en/unicode/search?q=box+drawings#characters
# https://en.wikipedia.org/wiki/Box-drawing_characters
class Chars:
Horizontal = ""
Vertical = ""
@ -116,12 +131,36 @@ class Chars:
Block = ""
Triangle_up = ""
Triangle_down = ""
Check = "+"
Cross = "x"
Right_arrow = ""
@dataclass
class Result(Generic[ItemType]):
class Result:
type_: ResultType
value: Optional[ItemType]
_item: Optional[MenuItem | List[MenuItem] | str]
def has_item(self) -> bool:
return self._item is not None
def get_value(self) -> Any:
return self.item().get_value()
def get_values(self) -> List[Any]:
return [i.get_value() for i in self.items()]
def item(self) -> MenuItem:
assert self._item is not None and isinstance(self._item, MenuItem)
return self._item
def items(self) -> List[MenuItem]:
assert self._item is not None and isinstance(self._item, list)
return self._item
def text(self) -> str:
assert self._item is not None and isinstance(self._item, str)
return self._item
@dataclass

View File

@ -14,4 +14,4 @@ For other installation methods refer to the docs of the dependencies.
## Build
In `archinstall/docs`, run `make html` (or specify another target) to build locally. The build files will be in `archinstall/docs/_build`. Open `_build/html/index.html` with your browser to see your changes in action.
In `archinstall/docs`, run `make html` (or specify another target) to build locally. The build files will be in `archinstall/docs/_build`. Open `_build/html/index.html` with your browser to see your changes in action.

View File

@ -1,3 +1,3 @@
.wy-nav-content {
max-width: none;
}
}

View File

@ -1,4 +1,4 @@
{% extends "!layout.html" %}
{% block extrahead %}
<link href="{{ pathto("_static/style.css", True) }}" rel="stylesheet" type="text/css">
{% endblock %}
{% endblock %}

View File

@ -54,4 +54,4 @@ The simplest way currently is to look at a reference implementation or the commu
And search for `plugin.on_ <https://github.com/search?q=repo%3Aarchlinux%2Farchinstall+%22plugin.on_%22&type=code>`_ in the code base to find what ``archinstall`` will look for. PR's are welcome to widen the support for this.
.. _plugin discovery: https://packaging.python.org/en/latest/specifications/entry-points/
.. _entry points: https://docs.python.org/3/library/importlib.metadata.html#entry-points
.. _entry points: https://docs.python.org/3/library/importlib.metadata.html#entry-points

View File

@ -16,4 +16,4 @@ Disk encryption consists of a top level entry in the user configuration.
}
}
The ``UID`` in the ``partitions`` list is an internal reference to the ``obj_id`` in the :ref:`disk config` entries.
The ``UID`` in the ``partitions`` list is an internal reference to the ``obj_id`` in the :ref:`disk config` entries.

View File

@ -1,4 +1,4 @@
Key,Value(s),Description,Required
device,``str``,Which block-device to format,yes
partitions,[ {key: val} ],The data describing the change/addition in a partition,yes
wipe,``bool``,clear the disk before adding any partitions,No
wipe,``bool``,clear the disk before adding any partitions,No

1 Key Value(s) Description Required
2 device ``str`` Which block-device to format yes
3 partitions [ {key: val} ] The data describing the change/addition in a partition yes
4 wipe ``bool`` clear the disk before adding any partitions No

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1 +1 @@
<mxfile host="Electron" modified="2021-05-02T19:57:46.193Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.5.1 Chrome/89.0.4389.82 Electron/12.0.1 Safari/537.36" etag="WWkzNgJUxTiFme1f07FW" version="14.5.1" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7VvZdqM4EP2anHlKDpsxPMZ20sl0kl7S05meNwVkYCIjt5C3+fqRjFgFNnbwksQv3a5CCKG6dWsROdP7o/knAsb+PXYhOtMUd36mD840TVUsi/3HNYtYY5tC4ZHAFYMyxWPwH0zuFNpJ4MKoMJBijGgwLiodHIbQoQUdIATPisOGGBWfOgYelBSPDkCy9ilwqR9rLa2b6W9g4PnJk1XTjq+MQDJYvEnkAxfPcir96kzvE4xp/Gs070PENy/Zl6fbxRO6ezE//fkt+g3+6n3+8fDzPJ7sepNb0lcgMKRbT/3y2bLtaXf4oFrf7ga/rYfB/dW5IV6NLpL9gi7bPiFiQn3s4RCgq0zbI3gSupDPqjIpG3OH8Vgo/4WULgQWwIRipvLpCImr7C3I4m8mKBedRPzFxUQYzAvSQkhDHNI+RpgsV6obpmF3BkwfUYJfYO5K17IuL3V+R4BQTt/T+vZlbzmve80upetxLznSmOggEEWBs5wUECoGKYmcDAtxyHcieoHU8cWAeCf59pWAt8ZqYlyEJ8SBK8bpwnkA8eCq+cwUmsynIR5BtoXsPgIRoMG0uDggnMtLx2UAYj8EhjaAqljkFKCJeNIjRNytNaWHsPMygNPAYXRQRt3MDyh8HIPlDswYExURwy0v4KSyl+t53EypYbj1E9/mo1NHVSS01gFiBYRqQLeZdaeQUDjP7b1soOSqKehG8K0lxFlGXmrCSH6OuAzl9SatpAhNMukDPrFGBUHskwzMhmSgKtVga50NKqEjs8GvCt8/IHbUA2HnbUScCpBp3mx4N50TtftTuRl3H7/9Y4fnmrGS0c6Vi65qCCw0Bp6Y7isOQpobgofDiC2mjMz0qduHLlMC69VoTPl7xCEswKEEXeLj0fOELa23JoAVQhJH1TUYBYjD7AaiKaSBAyrCHECBF3KMMHtCUh3r2COD0GOSmUk/lm7Bkru3H/7UbsP4Z7UQ/6rRXcNiCp9WU7YKhs6ETJdGkTOUgodvQXWbU1OdieE8oDnOZFJKmex3xphcSAhz2yBd4LpqSizxpiDEjFeVjVGZJ8Bq02sNw2y3koprQa5cqIZmv44QxVRGp+QvZskP4rWLu/Ll4f6YtSsza+iQxZg//Tsvn0/VQB1SOnrJvPahywHlSMqBd5nWJ02ztSlXZ13G9XqCaZUXKqGkSlA6VHmwx0j3YXBbPbCG7vbTnVJl8rqf8FY0g5Gm9AZ/yOg7Zfi7zvC1piFtdxm+Kpn9naTwbbt5coCzLjxZDfPhPbm9HGjSrjSb8HmZgvbTig6ELvsXj3nFD/jkI2ZdeupZN3dpQ99jlloJQLul1KJd723dS+tzw2N20srVqHKVuFVh8arucCGRy/K62qbFG7G0ah6VqTuSpftsv0AQ8q7aV5bGBpx6T7nY7olb7xxbLqbKzL1/GtjskOjgNGC1TQO5gxvdFj3Qo2gjVKfvbQX7nab0FWd5pYq98lCwXOQfvjJ4FQS140o6LAk5X5CbrwoUH/Cg5LBdhKHcqP64EQkQR6xU0/cYoAz14AHKOkWjlVSgNv02pemh2WaRh1EoWOQGjHkcinYRd1T5i4G0rRAfaynXtwhGi4jCkQSaU/eg7oxLb+rjO+seaHKFcmzpRFt5gAsiP11Wy0yQ7FrDD4iOJitI1p0/JuAtwNXV6RqHLgGgnA9sdiQkM0Cd3fO8sS8H3ucnqyvxlDPgbcjS7eWd/DMunuCNKvK57W0o2crSnnXTbGIrtwMt1yjZquCXzA70C0/tKN9hY5e21I0js6Xe1inxqbrbnsiblncJERwLkcv13VMwhrH/96q+PPq4Bd3OCKVUwXWMTjNG2VkFpx9dMve+2aN9Uqj+FNJUS3WEYRenqPkUsq16MHnP/LkGgoDwnIN3lUzEdrP3zGTT47/iwtA9F50mZqWKuHYipNazVaMBIRntEBITs78PjXGW/ZWtfvU/</diagram></mxfile>
<mxfile host="Electron" modified="2021-05-02T19:57:46.193Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.5.1 Chrome/89.0.4389.82 Electron/12.0.1 Safari/537.36" etag="WWkzNgJUxTiFme1f07FW" version="14.5.1" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7VvZdqM4EP2anHlKDpsxPMZ20sl0kl7S05meNwVkYCIjt5C3+fqRjFgFNnbwksQv3a5CCKG6dWsROdP7o/knAsb+PXYhOtMUd36mD840TVUsi/3HNYtYY5tC4ZHAFYMyxWPwH0zuFNpJ4MKoMJBijGgwLiodHIbQoQUdIATPisOGGBWfOgYelBSPDkCy9ilwqR9rLa2b6W9g4PnJk1XTjq+MQDJYvEnkAxfPcir96kzvE4xp/Gs070PENy/Zl6fbxRO6ezE//fkt+g3+6n3+8fDzPJ7sepNb0lcgMKRbT/3y2bLtaXf4oFrf7ga/rYfB/dW5IV6NLpL9gi7bPiFiQn3s4RCgq0zbI3gSupDPqjIpG3OH8Vgo/4WULgQWwIRipvLpCImr7C3I4m8mKBedRPzFxUQYzAvSQkhDHNI+RpgsV6obpmF3BkwfUYJfYO5K17IuL3V+R4BQTt/T+vZlbzmve80upetxLznSmOggEEWBs5wUECoGKYmcDAtxyHcieoHU8cWAeCf59pWAt8ZqYlyEJ8SBK8bpwnkA8eCq+cwUmsynIR5BtoXsPgIRoMG0uDggnMtLx2UAYj8EhjaAqljkFKCJeNIjRNytNaWHsPMygNPAYXRQRt3MDyh8HIPlDswYExURwy0v4KSyl+t53EypYbj1E9/mo1NHVSS01gFiBYRqQLeZdaeQUDjP7b1soOSqKehG8K0lxFlGXmrCSH6OuAzl9SatpAhNMukDPrFGBUHskwzMhmSgKtVga50NKqEjs8GvCt8/IHbUA2HnbUScCpBp3mx4N50TtftTuRl3H7/9Y4fnmrGS0c6Vi65qCCw0Bp6Y7isOQpobgofDiC2mjMz0qduHLlMC69VoTPl7xCEswKEEXeLj0fOELa23JoAVQhJH1TUYBYjD7AaiKaSBAyrCHECBF3KMMHtCUh3r2COD0GOSmUk/lm7Bkru3H/7UbsP4Z7UQ/6rRXcNiCp9WU7YKhs6ETJdGkTOUgodvQXWbU1OdieE8oDnOZFJKmex3xphcSAhz2yBd4LpqSizxpiDEjFeVjVGZJ8Bq02sNw2y3koprQa5cqIZmv44QxVRGp+QvZskP4rWLu/Ll4f6YtSsza+iQxZg//Tsvn0/VQB1SOnrJvPahywHlSMqBd5nWJ02ztSlXZ13G9XqCaZUXKqGkSlA6VHmwx0j3YXBbPbCG7vbTnVJl8rqf8FY0g5Gm9AZ/yOg7Zfi7zvC1piFtdxm+Kpn9naTwbbt5coCzLjxZDfPhPbm9HGjSrjSb8HmZgvbTig6ELvsXj3nFD/jkI2ZdeupZN3dpQ99jlloJQLul1KJd723dS+tzw2N20srVqHKVuFVh8arucCGRy/K62qbFG7G0ah6VqTuSpftsv0AQ8q7aV5bGBpx6T7nY7olb7xxbLqbKzL1/GtjskOjgNGC1TQO5gxvdFj3Qo2gjVKfvbQX7nab0FWd5pYq98lCwXOQfvjJ4FQS140o6LAk5X5CbrwoUH/Cg5LBdhKHcqP64EQkQR6xU0/cYoAz14AHKOkWjlVSgNv02pemh2WaRh1EoWOQGjHkcinYRd1T5i4G0rRAfaynXtwhGi4jCkQSaU/eg7oxLb+rjO+seaHKFcmzpRFt5gAsiP11Wy0yQ7FrDD4iOJitI1p0/JuAtwNXV6RqHLgGgnA9sdiQkM0Cd3fO8sS8H3ucnqyvxlDPgbcjS7eWd/DMunuCNKvK57W0o2crSnnXTbGIrtwMt1yjZquCXzA70C0/tKN9hY5e21I0js6Xe1inxqbrbnsiblncJERwLkcv13VMwhrH/96q+PPq4Bd3OCKVUwXWMTjNG2VkFpx9dMve+2aN9Uqj+FNJUS3WEYRenqPkUsq16MHnP/LkGgoDwnIN3lUzEdrP3zGTT47/iwtA9F50mZqWKuHYipNazVaMBIRntEBITs78PjXGW/ZWtfvU/</diagram></mxfile>

View File

@ -269,7 +269,7 @@ Below is an example of how to set the root password and below that are descripti
{
"username": "<USERNAME>",
"!password": "<PASSWORD>",
"sudo": false
"sudo": false
}
- List of regular user credentials, see configuration for reference
- Maybe

View File

@ -6,7 +6,7 @@
## Tests and Checks
- [ ] I have tested the code!<br>
<!--
<!--
After submitting your PR, an ISO can be downloaded below the PR description. After testing it you can check the box
You can do manual tests too, like isolated function tests, just something!
-->

View File

@ -6,6 +6,7 @@ from archinstall.default_profiles.minimal import MinimalProfile
from archinstall import disk
from archinstall import models
# we're creating a new ext4 filesystem installation
fs_type = disk.FilesystemType('ext4')
device_path = Path('/dev/sda')

View File

@ -1,79 +1,40 @@
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Optional
from typing import Optional
import archinstall
from archinstall import Installer
from archinstall import profile
from archinstall import SysInfo
from archinstall import disk
from archinstall import menu
from archinstall import models
from archinstall import locale
from archinstall import info, debug
from archinstall import SysInfo
from archinstall.lib import locale
from archinstall.lib import disk
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.installer import Installer
from archinstall.lib.models import AudioConfiguration, Bootloader
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.interactions.general_conf import ask_chroot
from archinstall.tui import Tui
if TYPE_CHECKING:
_: Callable[[str], str]
if archinstall.arguments.get('help'):
print("See `man archinstall` for help.")
exit(0)
def ask_user_questions() -> None:
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
"""
First, we'll ask the user for a bunch of user input.
Not until we're satisfied with what we want to install
will we continue with the actual installation steps.
"""
global_menu.enable('archinstall-language')
with Tui():
global_menu = GlobalMenu(data_store=archinstall.arguments)
# Set which region to download packages from during the installation
global_menu.enable('mirror_config')
if not archinstall.arguments.get('advanced', False):
global_menu.set_enabled('parallel downloads', False)
global_menu.enable('locale_config')
global_menu.enable('disk_config', mandatory=True)
# Specify disk encryption options
global_menu.enable('disk_encryption')
# Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB)
global_menu.enable('bootloader')
global_menu.enable('swap')
# Get the hostname for the machine
global_menu.enable('hostname')
# Ask for a root password (optional, but triggers requirement for super-user if skipped)
global_menu.enable('!root-password', mandatory=True)
global_menu.enable('!users', mandatory=True)
# Ask for archinstall-specific profiles_bck (such as desktop environments etc)
global_menu.enable('profile_config')
# Ask about audio server selection if one is not already set
global_menu.enable('audio_config')
# Ask for preferred kernel:
global_menu.enable('kernels', mandatory=True)
global_menu.enable('packages')
if archinstall.arguments.get('advanced', False):
# Enable parallel downloads
global_menu.enable('parallel downloads')
# Ask or Call the helper function that asks the user to optionally configure a network.
global_menu.enable('network_config')
global_menu.enable('timezone')
global_menu.enable('ntp')
global_menu.enable('additional-repositories')
global_menu.enable('__separator__')
global_menu.enable('save_config')
global_menu.enable('install')
global_menu.enable('abort')
global_menu.run()
global_menu.run()
def perform_installation(mountpoint: Path) -> None:
@ -88,6 +49,7 @@ def perform_installation(mountpoint: Path) -> None:
# Retrieve list of additional repositories and set boolean values appropriately
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', [])
run_mkinitcpio = not archinstall.arguments.get('uki')
locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
@ -109,11 +71,12 @@ def perform_installation(mountpoint: Path) -> None:
installation.generate_key_files()
if mirror_config := archinstall.arguments.get('mirror_config', None):
installation.set_mirrors(mirror_config)
installation.set_mirrors(mirror_config, on_target=False)
installation.minimal_installation(
testing=enable_testing,
multilib=enable_multilib,
mkinitcpio=run_mkinitcpio,
hostname=archinstall.arguments.get('hostname', 'archlinux'),
locale_config=locale_config
)
@ -124,14 +87,17 @@ def perform_installation(mountpoint: Path) -> None:
if archinstall.arguments.get('swap'):
installation.setup_swap('zram')
if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi():
if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi():
installation.add_additional_packages("grub")
installation.add_bootloader(archinstall.arguments["bootloader"])
installation.add_bootloader(
archinstall.arguments["bootloader"],
archinstall.arguments.get('uki', False)
)
# If user selected to copy the current ISO network configuration
# Perform a copy of the config
network_config = archinstall.arguments.get('network_config', None)
network_config: Optional[NetworkConfiguration] = archinstall.arguments.get('network_config', None)
if network_config:
network_config.install_network_config(
@ -142,17 +108,17 @@ def perform_installation(mountpoint: Path) -> None:
if users := archinstall.arguments.get('!users', None):
installation.create_users(users)
audio_config: Optional[models.AudioConfiguration] = archinstall.arguments.get('audio_config', None)
audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None)
if audio_config:
audio_config.install_audio_config(installation)
else:
info("No audio server will be installed")
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages(archinstall.arguments.get('packages', []))
installation.add_additional_packages(archinstall.arguments.get('packages', None))
if profile_config := archinstall.arguments.get('profile_config', None):
profile.profile_handler.install_profile_config(installation, profile_config)
profile_handler.install_profile_config(installation, profile_config)
if timezone := archinstall.arguments.get('timezone', None):
installation.set_timezone(timezone)
@ -183,24 +149,42 @@ def perform_installation(mountpoint: Path) -> None:
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
if not archinstall.arguments.get('silent'):
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?'))
choice = menu.Menu(prompt, menu.Menu.yes_no(), default_option=menu.Menu.yes()).run()
if choice.value == menu.Menu.yes():
with Tui():
chroot = ask_chroot()
if chroot:
try:
installation.drop_to_shell()
except Exception:
except:
pass
debug(f"Disk states after installing: {disk.disk_layouts()}")
ask_user_questions()
def _guided() -> None:
if not archinstall.arguments.get('silent'):
ask_user_questions()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
config = ConfigurationOutput(archinstall.arguments)
config.write_debug()
config.save()
fs_handler.perform_filesystem_operations()
if archinstall.arguments.get('dry_run'):
exit(0)
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
if not archinstall.arguments.get('silent'):
with Tui():
if not config.confirm_config():
debug('Installation aborted')
_guided()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
_guided()

View File

@ -2,6 +2,8 @@ import time
import archinstall
from archinstall import profile, info
from archinstall.tui import Tui
for _profile in profile.profile_handler.get_mac_addr_profiles():
# Tailored means it's a match for this machine
@ -11,7 +13,7 @@ for _profile in profile.profile_handler.get_mac_addr_profiles():
print('Starting install in:')
for i in range(10, 0, -1):
print(f'{i}...')
Tui.print(f'{i}...')
time.sleep(1)
install_session = archinstall.storage['installation_session']

View File

@ -1,16 +1,24 @@
from pathlib import Path
from typing import TYPE_CHECKING, Callable, List
from typing import List
import archinstall
from archinstall import disk
from archinstall import Installer
from archinstall import profile
from archinstall import models
from archinstall import interactions
from archinstall import info, debug
from archinstall import Installer, ConfigurationOutput
from archinstall.default_profiles.minimal import MinimalProfile
from archinstall.lib.interactions import suggest_single_disk_layout, select_devices
from archinstall.lib.models import Bootloader, User
from archinstall.lib.profile import ProfileConfiguration, profile_handler
from archinstall.lib import disk
from archinstall.tui import Tui
if TYPE_CHECKING:
_: Callable[[str], str]
info("Minimal only supports:")
info(" * Being installed to a single disk")
if archinstall.arguments.get('help', None):
info(" - Optional disk encryption via --!encryption-password=<password>")
info(" - Optional filesystem type via --filesystem=<fs type>")
info(" - Optional systemd network via --network")
def perform_installation(mountpoint: Path) -> None:
@ -27,7 +35,7 @@ def perform_installation(mountpoint: Path) -> None:
# some other minor details as specified by this profile and user.
if installation.minimal_installation():
installation.set_hostname('minimal-arch')
installation.add_bootloader(models.Bootloader.Systemd)
installation.add_bootloader(Bootloader.Systemd)
# Optionally enable networking:
if archinstall.arguments.get('network', None):
@ -35,20 +43,26 @@ def perform_installation(mountpoint: Path) -> None:
installation.add_additional_packages(['nano', 'wget', 'git'])
profile_config = profile.ProfileConfiguration(MinimalProfile())
profile.profile_handler.install_profile_config(installation, profile_config)
profile_config = ProfileConfiguration(MinimalProfile())
profile_handler.install_profile_config(installation, profile_config)
user = models.User('devel', 'devel', False)
user = User('devel', 'devel', False)
installation.create_users(user)
# Once this is done, we output some useful information to the user
# And the installation is complete.
info("There are two new accounts in your installation after reboot:")
info(" * root (password: airoot)")
info(" * devel (password: devel)")
def prompt_disk_layout() -> None:
fs_type = None
if filesystem := archinstall.arguments.get('filesystem', None):
fs_type = disk.FilesystemType(filesystem)
devices = interactions.select_devices()
modifications = interactions.suggest_single_disk_layout(devices[0], filesystem_type=fs_type)
devices = select_devices()
modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type)
archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Default,
@ -72,15 +86,31 @@ def parse_disk_encryption() -> None:
)
prompt_disk_layout()
parse_disk_encryption()
def _minimal() -> None:
with Tui():
prompt_disk_layout()
parse_disk_encryption()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
config = ConfigurationOutput(archinstall.arguments)
config.write_debug()
config.save()
fs_handler.perform_filesystem_operations()
if archinstall.arguments.get('dry_run'):
exit(0)
mount_point = Path('/mnt')
perform_installation(mount_point)
if not archinstall.arguments.get('silent'):
with Tui():
if not config.confirm_config():
debug('Installation aborted')
_minimal()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
_minimal()

View File

@ -1,23 +1,27 @@
from pathlib import Path
import archinstall
from archinstall import Installer, disk, debug
from archinstall import debug
from archinstall.lib.installer import Installer
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib import disk
from archinstall.tui import Tui
def ask_user_questions() -> None:
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
with Tui():
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
global_menu.disable_all()
global_menu.enable('archinstall-language')
global_menu.set_enabled('archinstall-language', True)
global_menu.set_enabled('disk_config', True)
global_menu.set_enabled('disk_encryption', True)
global_menu.set_enabled('swap', True)
global_menu.set_enabled('save_config', True)
global_menu.set_enabled('install', True)
global_menu.set_enabled('abort', True)
global_menu.enable('disk_config', mandatory=True)
global_menu.enable('disk_encryption')
global_menu.enable('swap')
global_menu.enable('save_config')
global_menu.enable('install')
global_menu.enable('abort')
global_menu.run()
global_menu.run()
def perform_installation(mountpoint: Path) -> None:
@ -49,13 +53,30 @@ def perform_installation(mountpoint: Path) -> None:
debug(f"Disk states after installing: {disk.disk_layouts()}")
ask_user_questions()
def _only_hd() -> None:
if not archinstall.arguments.get('silent'):
ask_user_questions()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
config = ConfigurationOutput(archinstall.arguments)
config.write_debug()
config.save()
fs_handler.perform_filesystem_operations()
if archinstall.arguments.get('dry_run'):
exit(0)
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
if not archinstall.arguments.get('silent'):
with Tui():
if not config.confirm_config():
debug('Installation aborted')
_only_hd()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
_only_hd()

View File

@ -118,6 +118,11 @@ module = [
]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "archinstall.lib.models.mirrors"
disallow_untyped_decorators = false
disallow_subclassing_any = false
[tool.bandit]
targets = ["archinstall"]
exclude = ["/tests"]