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

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

View File

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

View File

@ -10,7 +10,6 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Union from typing import TYPE_CHECKING, Any, Dict, Union
from .lib import disk from .lib import disk
from .lib import menu
from .lib import models from .lib import models
from .lib import packages from .lib import packages
from .lib import exceptions from .lib import exceptions
@ -32,6 +31,7 @@ from .lib.boot import Boot
from .lib.translationhandler import TranslationHandler, Language, DeferredTranslation from .lib.translationhandler import TranslationHandler, Language, DeferredTranslation
from .lib.plugins import plugins, load_plugin from .lib.plugins import plugins, load_plugin
from .lib.configuration import ConfigurationOutput from .lib.configuration import ConfigurationOutput
from .tui import Tui
from .lib.general import ( from .lib.general import (
generate_password, locate_binary, clear_vt100_escape_codes, generate_password, locate_binary, clear_vt100_escape_codes,
@ -330,24 +330,6 @@ def main() -> None:
importlib.import_module(mod_name) 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: def run_as_a_module() -> None:
exc = None exc = None
@ -357,7 +339,7 @@ def run_as_a_module() -> None:
exc = e exc = e
finally: finally:
# restore the terminal to the original state # restore the terminal to the original state
_shutdown_curses() Tui.shutdown()
if exc: if exc:
err = ''.join(traceback.format_exception(exc)) err = ''.join(traceback.format_exception(exc))

View File

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

View File

@ -1,9 +1,12 @@
from enum import Enum from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Any 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.default_profiles.xorg import XorgProfile
from archinstall.lib.menu import Menu from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, ResultType, Alignment
)
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
@ -49,20 +52,30 @@ class HyprlandProfile(XorgProfile):
def _ask_seat_access(self) -> None: def _ask_seat_access(self) -> None:
# need to activate seat service and add to seat group # 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)')) header = str(_('Sway 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 += '\n' + str(_('Choose an option to give Sway access to your hardware')) + '\n'
options = [e.value for e in SeatAccess] items = [MenuItem(s.value, value=s) for s in SeatAccess]
default = None group = MenuItemGroup(items, sort_items=True)
if seat := self.custom_settings.get('seat_access', None): default = self.custom_settings.get('seat_access', None)
default = seat group.set_default_by_value(default)
choice = Menu(title, options, skip=False, preset_values=default).run() result = SelectMenu(
self.custom_settings['seat_access'] = choice.single_value 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() self._ask_seat_access()
return None
def install(self, install_session: 'Installer') -> None: def install(self, install_session: 'Installer') -> None:
super().install(install_session) super().install(install_session)

View File

@ -1,9 +1,13 @@
from enum import Enum from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Any 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.default_profiles.xorg import XorgProfile
from archinstall.lib.menu import Menu
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType
)
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
@ -58,20 +62,30 @@ class SwayProfile(XorgProfile):
def _ask_seat_access(self) -> None: def _ask_seat_access(self) -> None:
# need to activate seat service and add to seat group # 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)')) header = 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 += '\n' + str(_('Choose an option to give Sway access to your hardware')) + '\n'
options = [e.value for e in SeatAccess] items = [MenuItem(s.value, value=s) for s in SeatAccess]
default = None group = MenuItemGroup(items, sort_items=True)
if seat := self.custom_settings.get('seat_access', None): default = self.custom_settings.get('seat_access', None)
default = seat group.set_default_by_value(default)
choice = Menu(title, options, skip=False, preset_values=default).run() result = SelectMenu(
self.custom_settings['seat_access'] = choice.single_value 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() self._ask_seat_access()
return None
def install(self, install_session: 'Installer') -> None: def install(self, install_session: 'Installer') -> None:
super().install(install_session) super().install(install_session)

View File

@ -4,7 +4,6 @@ import sys
from enum import Enum, auto from enum import Enum, auto
from typing import List, Optional, Any, Dict, TYPE_CHECKING from typing import List, Optional, Any, Dict, TYPE_CHECKING
from ..lib.utils.util import format_cols
from ..lib.storage import storage from ..lib.storage import storage
if TYPE_CHECKING: if TYPE_CHECKING:
@ -126,7 +125,7 @@ class Profile:
""" """
return {} return {}
def do_on_select(self) -> SelectResult: def do_on_select(self) -> Optional[SelectResult]:
""" """
Hook that will be called when a profile is selected Hook that will be called when a profile is selected
""" """
@ -187,24 +186,20 @@ class Profile:
""" """
return self.packages_text() return self.packages_text()
def packages_text(self, include_sub_packages: bool = False) -> Optional[str]: def packages_text(self, include_sub_packages: bool = False) -> str:
header = str(_('Installed packages')) packages = set()
text = ''
packages = []
if self.packages: if self.packages:
packages = self.packages packages = set(self.packages)
if include_sub_packages: if include_sub_packages:
for p in self.current_selection: for sub_profile in self.current_selection:
if p.packages: if sub_profile.packages:
packages += p.packages packages.update(sub_profile.packages)
text += format_cols(sorted(set(packages))) text = str(_('Installed packages')) + ':\n'
if text: for pkg in sorted(packages):
text = f'{header}: \n{text}' text += f'\t- {pkg}\n'
return text
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.output import info
from archinstall.lib.menu import MenuSelectionType
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.default_profiles.profile import ProfileType, Profile, SelectResult from archinstall.default_profiles.profile import ProfileType, Profile, SelectResult
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, ResultType, PreviewStyle
)
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
_: Any _: Any
@ -19,23 +23,36 @@ class ServerProfile(Profile):
current_selection=current_value current_selection=current_value
) )
def do_on_select(self) -> SelectResult: def do_on_select(self) -> Optional[SelectResult]:
available_servers = profile_handler.get_server_profiles() 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( group = MenuItemGroup(items, sort_items=True)
available_servers, group.set_selected_by_value(self.current_selection)
self.current_selection,
title=str(_('Choose which servers to install, if none then a minimal installation will be done')), result = SelectMenu(
group,
allow_reset=True,
allow_skip=True,
preview_style=PreviewStyle.RIGHT,
preview_size='auto',
preview_frame=FrameProperties.max('Info'),
multi=True multi=True
) ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Selection: case ResultType.Selection:
self.current_selection = choice.value # type: ignore selections = result.get_values()
self.current_selection = selections
return SelectResult.NewSelection return SelectResult.NewSelection
case MenuSelectionType.Skip: case ResultType.Skip:
return SelectResult.SameSelection return SelectResult.SameSelection
case MenuSelectionType.Reset: case ResultType.Reset:
return SelectResult.ResetCurrent return SelectResult.ResetCurrent
def post_install(self, install_session: 'Installer') -> None: def post_install(self, install_session: 'Installer') -> None:

View File

@ -5,10 +5,16 @@ import readline
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, TYPE_CHECKING from typing import Optional, Dict, Any, TYPE_CHECKING
from .menu import Menu, MenuSelectionType
from .storage import storage from .storage import storage
from .general import JSON, UNSAFE_JSON 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: if TYPE_CHECKING:
_: Any _: Any
@ -68,12 +74,35 @@ class ConfigurationOutput:
return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON) return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON)
return None return None
def show(self) -> None: def write_debug(self) -> None:
print(_('\nThis is your chosen configuration:'))
debug(" -- Chosen configuration --") debug(" -- Chosen configuration --")
debug(self.user_config_to_json())
info(self.user_config_to_json()) def confirm_config(self) -> bool:
print() 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: def _is_valid_path(self, dest_path: Path) -> bool:
dest_path_ok = dest_path.exists() and dest_path.is_dir() dest_path_ok = dest_path.exists() and dest_path.is_dir()
@ -105,9 +134,9 @@ class ConfigurationOutput:
self.save_user_creds(dest_path) self.save_user_creds(dest_path)
def save_config(config: Dict) -> None: def save_config(config: Dict[str, Any]) -> None:
def preview(selection: str) -> Optional[str]: def preview(item: MenuItem) -> Optional[str]:
match options[selection]: match item.value:
case "user_config": case "user_config":
serialized = config_output.user_config_to_json() serialized = config_output.user_config_to_json()
return f"{config_output.user_configuration_file}\n{serialized}" return f"{config_output.user_configuration_file}\n{serialized}"
@ -122,60 +151,80 @@ def save_config(config: Dict) -> None:
return '\n'.join(output) return '\n'.join(output)
return None return None
try: config_output = ConfigurationOutput(config)
config_output = ConfigurationOutput(config)
options = { items = [
str(_("Save user configuration (including disk layout)")): "user_config", MenuItem(
str(_("Save user credentials")): "user_creds", str(_("Save user configuration (including disk layout)")),
str(_("Save all")): "all", 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( group = MenuItemGroup(items)
_("Choose which configuration to save"), result = SelectMenu(
list(options), group,
sort=False, allow_skip=True,
skip=True, preview_frame=FrameProperties.max(str(_('Configuration'))),
preview_size=0.75, preview_size='auto',
preview_command=preview, preview_style=PreviewStyle.RIGHT
).run() ).run()
if save_choice.type_ == MenuSelectionType.Skip: match result.type_:
case ResultType.Skip:
return return
case ResultType.Selection:
save_option = result.get_value()
case _:
raise ValueError('Unhandled return type')
readline.set_completer_delims("\t\n=") readline.set_completer_delims("\t\n=")
readline.parse_and_bind("tab: complete") 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")
if not path: dest_path = prompt_dir(
return str(_('Directory')),
str(_('Enter a directory for the configuration(s) to be saved (tab completion enabled)')) + '\n',
allow_skip=True
)
prompt = _( if not dest_path:
"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):
return 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 import select_disk_config
from ..interactions.disk_conf import select_lvm_config from ..interactions.disk_conf import select_lvm_config
from ..menu import (
Selector,
AbstractSubMenu
)
from ..output import FormattedOutput from ..output import FormattedOutput
from ..menu import AbstractSubMenu
from archinstall.tui import (
MenuItemGroup, MenuItem
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -21,37 +22,38 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
def __init__( def __init__(
self, self,
disk_layout_config: Optional[DiskLayoutConfiguration], disk_layout_config: Optional[DiskLayoutConfiguration],
data_store: Dict[str, Any],
advanced: bool = False advanced: bool = False
): ):
self._disk_layout_config = disk_layout_config self._disk_layout_config = disk_layout_config
self._advanced = advanced 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: super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
self._menu_options['disk_config'] = \
Selector( def _define_menu_options(self) -> List[MenuItem]:
_('Partitioning'), return [
lambda x: self._select_disk_layout_config(x), MenuItem(
display_func=lambda x: self._display_disk_layout(x), text=str(_('Partitioning')),
preview_func=self._prev_disk_layouts, action=lambda x: self._select_disk_layout_config(x),
default=self._disk_layout_config, value=self._disk_layout_config,
enabled=True preview_action=self._prev_disk_layouts,
) key='disk_config'
self._menu_options['lvm_config'] = \ ),
Selector( MenuItem(
f'{_('LVM - Logical Volume Management')} (BETA)', text='LVM (BETA)',
lambda x: self._select_lvm_config(x), action=lambda x: self._select_lvm_config(x),
display_func=lambda x: self.defined_text if x else '', value=self._disk_layout_config.lvm_config if self._disk_layout_config else None,
preview_func=self._prev_lvm_config, preview_action=self._prev_lvm_config,
default=self._disk_layout_config.lvm_config if self._disk_layout_config else None,
dependencies=[self._check_dep_lvm], dependencies=[self._check_dep_lvm],
enabled=True key='lvm_config'
) ),
]
def run(self, allow_reset: bool = True) -> Optional[DiskLayoutConfiguration]: def run(self) -> Optional[DiskLayoutConfiguration]:
super().run(allow_reset=allow_reset) super().run()
disk_layout_config: Optional[DiskLayoutConfiguration] = self._data_store.get('disk_config', None) disk_layout_config: Optional[DiskLayoutConfiguration] = self._data_store.get('disk_config', None)
@ -61,7 +63,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
return disk_layout_config return disk_layout_config
def _check_dep_lvm(self) -> bool: 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: if disk_layout_conf and disk_layout_conf.config_type == DiskLayoutType.Default:
return True return True
@ -75,66 +77,72 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
disk_config = select_disk_config(preset, advanced_option=self._advanced) disk_config = select_disk_config(preset, advanced_option=self._advanced)
if disk_config != preset: 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 return disk_config
def _select_lvm_config(self, preset: Optional[LvmConfiguration]) -> Optional[LvmConfiguration]: 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: if disk_config:
return select_lvm_config(disk_config, preset=preset) return select_lvm_config(disk_config, preset=preset)
return preset return preset
def _display_disk_layout(self, current_value: Optional[DiskLayoutConfiguration] = None) -> str: def _prev_disk_layouts(self, item: MenuItem) -> Optional[str]:
if current_value: if not item.value:
return current_value.config_type.display_msg() return None
return ''
def _prev_disk_layouts(self) -> Optional[str]: disk_layout_conf: DiskLayoutConfiguration = item.get_value()
disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
if disk_layout_conf: if disk_layout_conf.config_type == DiskLayoutType.Pre_mount:
device_mods: List[DeviceModification] = \ msg = str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) + '\n'
list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications)) msg += str(_('Mountpoint')) + ': ' + str(disk_layout_conf.mountpoint)
return msg
if device_mods: device_mods: List[DeviceModification] = \
output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg()) list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications))
output_btrfs = ''
for mod in device_mods: if device_mods:
# create partition table output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg())
partition_table = FormattedOutput.as_table(mod.partitions) output_btrfs = ''
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' for mod in device_mods:
output_partition += partition_table + '\n' # create partition table
partition_table = FormattedOutput.as_table(mod.partitions)
# create btrfs table output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
btrfs_partitions = list( output_partition += partition_table + '\n'
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 # create btrfs table
return output.rstrip() 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 return None
def _prev_lvm_config(self) -> Optional[str]: def _prev_lvm_config(self, item: MenuItem) -> Optional[str]:
lvm_config: Optional[LvmConfiguration] = self._menu_options['lvm_config'].current_selection if not item.value:
return None
if lvm_config: lvm_config: LvmConfiguration = item.value
output = '{}: {}\n'.format(str(_('Configuration')), lvm_config.config_type.display_msg())
for vol_gp in lvm_config.vol_groups: output = '{}: {}\n'.format(str(_('Configuration')), lvm_config.config_type.display_msg())
pv_table = FormattedOutput.as_table(vol_gp.pvs)
output += '{}:\n{}'.format(str(_('Physical volumes')), pv_table)
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 += f'\nVolume Group: {vol_gp.name}'
output += '\n\n{}:\n{}'.format(str(_('Volumes')), lvm_volumes)
return output lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(str(_('Volumes')), lvm_volumes)
return output
return None return None

View File

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

View File

@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import signal
import sys
import time import time
from pathlib import Path from pathlib import Path
from typing import Any, Optional, TYPE_CHECKING, List, Dict, Set 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_handler import device_handler
from .device_model import ( from .device_model import (
DiskLayoutConfiguration, DiskLayoutType, PartitionTable, DiskLayoutConfiguration, DiskLayoutType, PartitionTable,
@ -15,8 +14,11 @@ from .device_model import (
) )
from ..hardware import SysInfo from ..hardware import SysInfo
from ..luks import Luks2 from ..luks import Luks2
from ..menu import Menu
from ..output import debug, info from ..output import debug, info
from archinstall.tui import (
Tui
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -44,12 +46,8 @@ class FilesystemHandler:
device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods]) 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: if show_countdown:
self._do_countdown() self._final_warning(device_paths)
# Setup the blockdevice, filesystem (and optionally encryption). # Setup the blockdevice, filesystem (and optionally encryption).
# Once that's done, we'll hand over to perform_installation() # Once that's done, we'll hand over to perform_installation()
@ -339,40 +337,19 @@ class FilesystemHandler:
Size(256, Unit.MiB, SectorSize.default()) Size(256, Unit.MiB, SectorSize.default())
) )
def _do_countdown(self) -> bool: def _final_warning(self, device_paths: str) -> bool:
SIG_TRIGGER = False # 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: try:
print() countdown = '\n5...4...3...2...1'
exit(0) for c in countdown:
Tui.print(c, row=0, endl='')
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()
time.sleep(0.25) time.sleep(0.25)
print(".", end='') except KeyboardInterrupt:
with Tui():
if SIG_TRIGGER: ask_abort()
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)
return True return True

View File

@ -5,16 +5,23 @@ from pathlib import Path
from typing import Any, TYPE_CHECKING, List, Optional, Tuple from typing import Any, TYPE_CHECKING, List, Optional, Tuple
from dataclasses import dataclass from dataclasses import dataclass
from ..utils.util import prompt_dir
from .device_model import ( from .device_model import (
PartitionModification, FilesystemType, BDevice, PartitionModification, FilesystemType, BDevice,
Size, Unit, PartitionType, PartitionFlag, Size, Unit, PartitionType, PartitionFlag,
ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption
) )
from ..hardware import SysInfo from ..hardware import SysInfo
from ..menu import Menu, ListManager, MenuSelection, TextInput from ..menu import ListManager
from ..output import FormattedOutput, warn from ..output import FormattedOutput
from .subvolume_menu import SubvolumeMenu from .subvolume_menu import SubvolumeMenu
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, EditMenu,
Orientation, ResultType
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -26,9 +33,6 @@ class DefaultFreeSector:
class PartitioningList(ListManager): class PartitioningList(ListManager):
"""
subclass of ListManager for the managing of user accounts
"""
def __init__(self, prompt: str, device: BDevice, device_partitions: List[PartitionModification]): def __init__(self, prompt: str, device: BDevice, device_partitions: List[PartitionModification]):
self._device = device self._device = device
self._actions = { self._actions = {
@ -49,7 +53,10 @@ class PartitioningList(ListManager):
super().__init__(prompt, device_partitions, display_actions[:2], display_actions[3:]) super().__init__(prompt, device_partitions, display_actions[:2], display_actions[3:])
def selected_action_display(self, partition: PartitionModification) -> str: 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]: def filter_options(self, selection: PartitionModification, options: List[str]) -> List[str]:
not_filter = [] not_filter = []
@ -100,8 +107,7 @@ class PartitioningList(ListManager):
if len(new_partitions) > 0: if len(new_partitions) > 0:
data = new_partitions data = new_partitions
case 'remove_added_partitions': case 'remove_added_partitions':
choice = self._reset_confirmation() if self._reset_confirmation():
if choice.value == Menu.yes():
data = [part for part in data if part.is_exists_or_modify()] data = [part for part in data if part.is_exists_or_modify()]
case 'assign_mountpoint' if entry: case 'assign_mountpoint' if entry:
entry.mountpoint = self._prompt_mountpoint() entry.mountpoint = self._prompt_mountpoint()
@ -169,7 +175,7 @@ class PartitioningList(ListManager):
def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None: def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None:
partition.btrfs_subvols = SubvolumeMenu( partition.btrfs_subvols = SubvolumeMenu(
_("Manage btrfs subvolumes for current partition"), str(_("Manage btrfs subvolumes for current partition")),
partition.btrfs_subvols partition.btrfs_subvols
).run() ).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, # without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set,
# it's safe to change the filesystem for this partition. # it's safe to change the filesystem for this partition.
if partition.fs_type == FilesystemType.Crypto_luks: 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) fs_type = self._prompt_partition_fs_type(prompt)
partition.fs_type = fs_type partition.fs_type = fs_type
@ -195,25 +201,31 @@ class PartitioningList(ListManager):
def _prompt_mountpoint(self) -> Path: 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(_('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' 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) mountpoint = prompt_dir(prompt, header, allow_skip=False)
assert mountpoint
while True:
value = TextInput(prompt).run().strip()
if value:
mountpoint = Path(value)
break
return mountpoint return mountpoint
def _prompt_partition_fs_type(self, prompt: str = '') -> FilesystemType: def _prompt_partition_fs_type(self, prompt: Optional[str] = None) -> FilesystemType:
options = {fs.value: fs for fs in FilesystemType if fs != FilesystemType.Crypto_luks} 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')) result = SelectMenu(
choice = Menu(prompt, options, sort=False, skip=False).run() group,
return options[choice.single_value] 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( def _validate_value(
self, self,
@ -246,22 +258,34 @@ class PartitioningList(ListManager):
self, self,
sector_size: SectorSize, sector_size: SectorSize,
total_size: Size, total_size: Size,
prompt: str, text: str,
header: str,
default: Size, default: Size,
start: Optional[Size], start: Optional[Size],
) -> Size: ) -> Size:
while True: def validate(value: str) -> Optional[str]:
value = TextInput(prompt).run().strip() size = self._validate_value(sector_size, total_size, value, start)
size: Optional[Size] = None if not size:
if not value: return str(_('Invalid size'))
size = default return None
else:
size = self._validate_value(sector_size, total_size, value, start)
if size: result = EditMenu(
return size 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]: def _prompt_size(self) -> Tuple[Size, Size]:
device_info = self._device.device_info 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(_('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(_('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' prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n'
print(prompt)
default_free_sector = self._find_default_free_space() default_free_sector = self._find_default_free_space()
@ -287,27 +310,32 @@ class PartitioningList(ListManager):
) )
# prompt until a valid start sector was entered # 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( start_size = self._enter_size(
device_info.sector_size, device_info.sector_size,
device_info.total_size, device_info.total_size,
start_prompt, start_text,
prompt,
default_free_sector.start, default_free_sector.start,
None 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: if start_size.value == default_free_sector.start.value and default_free_sector.end.value != 0:
end_size = default_free_sector.end end_size = default_free_sector.end
else: else:
end_size = device_info.total_size end_size = device_info.total_size
# prompt until valid end sector was entered # 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( end_size = self._enter_size(
device_info.sector_size, device_info.sector_size,
device_info.total_size, device_info.total_size,
end_prompt, end_text,
prompt,
end_size, end_size,
start_size start_size
) )
@ -358,9 +386,6 @@ class PartitioningList(ListManager):
start_size, end_size = self._prompt_size() start_size, end_size = self._prompt_size()
length = end_size - start_size length = end_size - start_size
# new line for the next prompt
print()
mountpoint = None mountpoint = None
if fs_type != FilesystemType.Btrfs: if fs_type != FilesystemType.Btrfs:
mountpoint = self._prompt_mountpoint() mountpoint = self._prompt_mountpoint()
@ -381,17 +406,26 @@ class PartitioningList(ListManager):
return partition return partition
def _reset_confirmation(self) -> MenuSelection: def _reset_confirmation(self) -> bool:
prompt = str(_('This will remove all newly added partitions, continue?')) prompt = str(_('This will remove all newly added partitions, continue?')) + '\n'
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run()
return choice 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]: def _suggest_partition_layout(self, data: List[PartitionModification]) -> List[PartitionModification]:
# if modifications have been done already, inform the user # if modifications have been done already, inform the user
# that this operation will erase those modifications # that this operation will erase those modifications
if any([not entry.exists() for entry in data]): if any([not entry.exists() for entry in data]):
choice = self._reset_confirmation() if not self._reset_confirmation():
if choice.value == Menu.no():
return [] return []
from ..interactions.disk_conf import suggest_single_disk_layout 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 typing import List, Optional, Any, TYPE_CHECKING
from .device_model import SubvolumeModification 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: if TYPE_CHECKING:
_: Any _: Any
@ -20,18 +25,34 @@ class SubvolumeMenu(ListManager):
def selected_action_display(self, subvolume: SubvolumeModification) -> str: def selected_action_display(self, subvolume: SubvolumeModification) -> str:
return str(subvolume.name) return str(subvolume.name)
def _add_subvolume(self, editing: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]: def _add_subvolume(self, preset: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]:
name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() 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 return None
mountpoint = TextInput(f'{_("Subvolume mountpoint")}: ', str(editing.mountpoint) if editing else '').run() return SubvolumeModification(Path(name), path)
if not mountpoint:
return None
return SubvolumeModification(Path(name), Path(mountpoint))
def handle_action( def handle_action(
self, self,

View File

@ -6,29 +6,32 @@ from . import disk
from .general import secret from .general import secret
from .hardware import SysInfo from .hardware import SysInfo
from .locale.locale_menu import LocaleConfiguration, LocaleMenu from .locale.locale_menu import LocaleConfiguration, LocaleMenu
from .menu import Selector, AbstractMenu from .menu import AbstractMenu
from .mirrors import MirrorConfiguration, MirrorMenu from .mirrors import MirrorConfiguration, MirrorMenu
from .models import NetworkConfiguration, NicType from .models import NetworkConfiguration, NicType
from .models.bootloader import Bootloader from .models.bootloader import Bootloader
from .models.audio_configuration import Audio, AudioConfiguration from .models.audio_configuration import AudioConfiguration
from .models.users import User from .models.users import User
from .output import FormattedOutput from .output import FormattedOutput
from .profile.profile_menu import ProfileConfiguration 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_additional_users
from .interactions import ask_for_audio_selection from .interactions import (
from .interactions import ask_for_bootloader ask_for_audio_selection, ask_for_swap,
from .interactions import ask_for_uki ask_for_bootloader, ask_for_uki, ask_hostname,
from .interactions import ask_for_swap add_number_of_parallel_downloads, select_kernel,
from .interactions import ask_hostname ask_additional_packages_to_install, select_additional_repositories,
from .interactions import ask_to_configure_network ask_for_a_timezone, ask_ntp, ask_to_configure_network
from .interactions import get_password, ask_for_a_timezone )
from .interactions import select_additional_repositories from .utils.util import get_password
from .interactions import select_kernel
from .utils.util import format_cols 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: if TYPE_CHECKING:
_: Any _: Any
@ -36,175 +39,211 @@ if TYPE_CHECKING:
class GlobalMenu(AbstractMenu): class GlobalMenu(AbstractMenu):
def __init__(self, data_store: Dict[str, Any]): 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: if 'archinstall-language' not in data_store:
# archinstall.Language will not use preset values data_store['archinstall-language'] = self._translation_handler.get_language_by_abbr('en')
self._menu_options['archinstall-language'] = \
Selector( menu_optioons = self._get_menu_options(data_store)
_('Archinstall language'), self._item_group = MenuItemGroup(
lambda x: self._select_archinstall_language(x), menu_optioons,
display_func=lambda x: x.display_name, sort_items=False,
default=self.translation_handler.get_language_by_abbr('en')) checkmarks=True
self._menu_options['locale_config'] = \ )
Selector(
_('Locales'), super().__init__(self._item_group, data_store)
lambda preset: self._locale_selection(preset),
preview_func=self._prev_locale, def _get_menu_options(self, data_store: Dict[str, Any]) -> List[MenuItem]:
display_func=lambda x: self.defined_text if x else '') return [
self._menu_options['mirror_config'] = \ MenuItem(
Selector( text=str(_('Archinstall language')),
_('Mirrors'), action=lambda x: self._select_archinstall_language(x),
lambda preset: self._mirror_configuration(preset), display_action=lambda x: x.display_name if x else '',
display_func=lambda x: self.defined_text if x else '', key='archinstall-language'
preview_func=self._prev_mirror_config ),
) MenuItem(
self._menu_options['disk_config'] = \ text=str(_('Locales')),
Selector( action=lambda x: self._locale_selection(x),
_('Disk configuration'), preview_action=self._prev_locale,
lambda preset: self._select_disk_config(preset), key='locale_config'
preview_func=self._prev_disk_config, ),
display_func=lambda x: self.defined_text if x else '', MenuItem(
) text=str(_('Mirrors')),
self._menu_options['disk_encryption'] = \ action=lambda x: self._mirror_configuration(x),
Selector( preview_action=self._prev_mirror_config,
_('Disk encryption'), key='mirror_config'
lambda preset: self._disk_encryption(preset), ),
preview_func=self._prev_disk_encryption, MenuItem(
display_func=lambda x: self._display_disk_encryption(x), 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'] 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 _missing_configs(self) -> List[str]:
def check(s: str) -> bool: def check(s) -> bool:
obj = self._menu_options.get(s) item = self._item_group.find_by_key(s)
if obj and obj.has_selection(): return item.has_value()
return True
return False
def has_superuser() -> bool: def has_superuser() -> bool:
sel = self._menu_options['!users'] item = self._item_group.find_by_key('!users')
if sel.current_selection:
return any([u.sudo for u in sel.current_selection]) if item.has_value():
users = item.value
if users:
return any([u.sudo for u in users])
return False return False
mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items()))
missing = set() missing = set()
for key, selector in mandatory_fields.items(): for item in self._item_group.items:
if key in ['!root-password', '!users']: if item.key in ['!root-password', '!users']:
if not check('!root-password') and not has_superuser(): if not check('!root-password') and not has_superuser():
missing.add( missing.add(
str(_('Either root-password or at least 1 user with sudo privileges must be specified')) str(_('Either root-password or at least 1 user with sudo privileges must be specified'))
) )
elif key == 'disk_config': elif item.mandatory:
if not check('disk_config'): if not check(item.key):
missing.add(self._menu_options['disk_config'].description) missing.add(item.text)
return list(missing) return list(missing)
@ -216,36 +255,28 @@ class GlobalMenu(AbstractMenu):
return False return False
return self._validate_bootloader() is None return self._validate_bootloader() is None
def _update_uki_display(self, name: Optional[str] = None) -> None: def _select_archinstall_language(self, preset: Language) -> Language:
if bootloader := self._menu_options['bootloader'].current_selection: from .interactions.general_conf import select_archinstall_language
if not SysInfo.has_uefi() or not bootloader.has_uki_support(): language = select_archinstall_language(self._translation_handler.translated_languages, preset)
self._menu_options['uki'].set_current_selection(False) self._translation_handler.activate(language)
self._menu_options['uki'].set_enabled(False)
elif name and name == 'bootloader':
self._menu_options['uki'].set_enabled(True)
def _update_install_text(self, name: Optional[str] = None, value: Any = None) -> None: self._upate_lang_text()
text = self._install_text()
self._menu_options['install'].update_description(text)
def post_callback(self, name: Optional[str] = None, value: Any = None) -> None: return language
self._update_uki_display(name)
self._update_install_text(name, value)
def _install_text(self) -> str: def _upate_lang_text(self) -> None:
missing = len(self._missing_configs()) """
if missing > 0: The options for the global menu are generated with a static text;
return _('Install ({} config(s) missing)').format(missing) each entry of the menu needs to be updated with the new translation
return _('Install') """
new_options = self._get_menu_options(self._data_store)
def _display_network_conf(self, config: Optional[NetworkConfiguration]) -> str: for o in new_options:
if not config: if o.key is not None:
return str(_('Not configured, unavailable unless setup manually')) self._item_group.find_by_key(o.key).text = o.text
return config.type.display_msg()
def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]: 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: if not disk_config:
# this should not happen as the encryption menu has the disk_config as dependency # 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): if not disk.DiskEncryption.validate_enc(disk_config):
return None return None
data_store: Dict[str, Any] = {} disk_encryption = disk.DiskEncryptionMenu(disk_config, preset=preset).run()
disk_encryption = disk.DiskEncryptionMenu(disk_config, data_store, preset=preset).run()
return disk_encryption return disk_encryption
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration: def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
data_store: Dict[str, Any] = {} locale_config = LocaleMenu(preset).run()
locale_config = LocaleMenu(data_store, preset).run()
return locale_config return locale_config
def _prev_locale(self) -> Optional[str]: def _prev_locale(self, item: MenuItem) -> Optional[str]:
selector = self._menu_options['locale_config'] if not item.value:
if selector.has_selection(): return None
config: LocaleConfiguration = selector.current_selection # type: ignore
output = '{}: {}\n'.format(str(_('Keyboard layout')), config.kb_layout) config: LocaleConfiguration = item.value
output += '{}: {}\n'.format(str(_('Locale language')), config.sys_lang) return config.preview()
output += '{}: {}'.format(str(_('Locale encoding')), config.sys_enc)
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 output
return None return None
def _prev_network_config(self) -> Optional[str]: def _prev_additional_pkgs(self, item: MenuItem) -> Optional[str]:
selector: Optional[NetworkConfiguration] = self._menu_options['network_config'].current_selection if item.value:
if selector: return format_cols(item.value, None)
if selector.type == NicType.MANUAL:
output = FormattedOutput.as_table(selector.nics)
return output
return None return None
def _prev_additional_pkgs(self) -> Optional[str]: def _prev_additional_repos(self, item: MenuItem) -> Optional[str]:
selector = self._menu_options['packages'] if item.value:
if selector.current_selection: repos = ', '.join(item.value)
packages: List[str] = selector.current_selection return f'{str(_("Additional repositories"))}: {repos}'
return format_cols(packages, None)
return None return None
def _prev_disk_config(self) -> Optional[str]: def _prev_tz(self, item: MenuItem) -> Optional[str]:
selector = self._menu_options['disk_config'] if item.value:
disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection 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: 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: 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 output
return None return None
def _display_disk_config(self, current_value: Optional[disk.DiskLayoutConfiguration] = None) -> str: def _prev_swap(self, item: MenuItem) -> Optional[str]:
if current_value: if item.value is not None:
return current_value.config_type.display_msg() output = f'{str(_("Swap on zram"))}: '
return '' output += str(_('Enabled')) if item.value else str(_('Disabled'))
return output
return None
def _prev_disk_encryption(self) -> Optional[str]: def _prev_uki(self, item: MenuItem) -> Optional[str]:
disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection 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): 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')) 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 enc_config:
enc_type = disk.EncryptionType.type_to_text(enc_config.encryption_type)
if encryption:
enc_type = disk.EncryptionType.type_to_text(encryption.encryption_type)
output = str(_('Encryption type')) + f': {enc_type}\n' 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: if enc_config.partitions:
output += 'Partitions: {} selected'.format(len(encryption.partitions)) + '\n' output += 'Partitions: {} selected'.format(len(enc_config.partitions)) + '\n'
elif encryption.lvm_volumes: elif enc_config.lvm_volumes:
output += 'LVM volumes: {} selected'.format(len(encryption.lvm_volumes)) + '\n' output += 'LVM volumes: {} selected'.format(len(enc_config.lvm_volumes)) + '\n'
if encryption.hsm_device: if enc_config.hsm_device:
output += f'HSM: {encryption.hsm_device.manufacturer}' output += f'HSM: {enc_config.hsm_device.manufacturer}'
return output return output
return None 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]: def _validate_bootloader(self) -> Optional[str]:
""" """
Checks the selected bootloader is valid for the selected filesystem 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 XXX: The caller is responsible for wrapping the string with the translation
shim if necessary. 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 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: for layout in disk_config.device_modifications:
if boot_partition := layout.get_boot_partition(): if boot_partition := layout.get_boot_partition():
break break
@ -369,7 +449,7 @@ class GlobalMenu(AbstractMenu):
return None 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(): if missing := self._missing_configs():
text = str(_('Missing configurations:\n')) text = str(_('Missing configurations:\n'))
for m in missing: for m in missing:
@ -381,17 +461,15 @@ class GlobalMenu(AbstractMenu):
return None return None
def _prev_users(self) -> Optional[str]: def _prev_users(self, item: MenuItem) -> Optional[str]:
selector = self._menu_options['!users'] users: Optional[List[User]] = item.value
users: Optional[List[User]] = selector.current_selection
if users: if users:
return FormattedOutput.as_table(users) return FormattedOutput.as_table(users)
return None return None
def _prev_profile(self) -> Optional[str]: def _prev_profile(self, item: MenuItem) -> Optional[str]:
selector = self._menu_options['profile_config'] profile_config: Optional[ProfileConfiguration] = item.value
profile_config: Optional[ProfileConfiguration] = selector.current_selection
if profile_config and profile_config.profile: if profile_config and profile_config.profile:
output = str(_('Profiles')) + ': ' output = str(_('Profiles')) + ': '
@ -410,63 +488,59 @@ class GlobalMenu(AbstractMenu):
return None return None
def _set_root_password(self) -> Optional[str]: def _set_root_password(self, preset: Optional[str] = None) -> Optional[str]:
prompt = str(_('Enter root password (leave blank to disable root): ')) password = get_password(text=str(_('Root password')), allow_skip=True)
password = get_password(prompt=prompt)
return password return password
def _select_disk_config( def _select_disk_config(
self, self,
preset: Optional[disk.DiskLayoutConfiguration] = None preset: Optional[disk.DiskLayoutConfiguration] = None
) -> Optional[disk.DiskLayoutConfiguration]: ) -> Optional[disk.DiskLayoutConfiguration]:
data_store: Dict[str, Any] = {} disk_config = disk.DiskLayoutConfigurationMenu(preset).run()
disk_config = disk.DiskLayoutConfigurationMenu(preset, data_store).run()
if disk_config != preset: 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 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]): def _select_profile(self, current_profile: Optional[ProfileConfiguration]):
from .profile.profile_menu import ProfileMenu from .profile.profile_menu import ProfileMenu
store: Dict[str, Any] = {} profile_config = ProfileMenu(preset=current_profile).run()
profile_config = ProfileMenu(store, preset=current_profile).run()
return profile_config return profile_config
def _select_audio( def _create_user_account(self, preset: Optional[List[User]] = None) -> List[User]:
self, preset = [] if preset is None else preset
current: Optional[AudioConfiguration] = None users = ask_for_additional_users(defined_users=preset)
) -> 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)
return users return users
def _mirror_configuration(self, preset: Optional[MirrorConfiguration] = None) -> Optional[MirrorConfiguration]: def _mirror_configuration(self, preset: Optional[MirrorConfiguration] = None) -> Optional[MirrorConfiguration]:
data_store: Dict[str, Any] = {} mirror_configuration = MirrorMenu(preset=preset).run()
mirror_configuration = MirrorMenu(data_store, preset=preset).run()
return mirror_configuration return mirror_configuration
def _prev_mirror_config(self) -> Optional[str]: def _prev_mirror_config(self, item: MenuItem) -> Optional[str]:
selector = self._menu_options['mirror_config'] if not item.value:
return None
if selector.has_selection(): mirror_config: MirrorConfiguration = item.value
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)
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 .general import SysCommand
from .networking import list_interfaces, enrich_iface_types from .networking import list_interfaces, enrich_iface_types
from .output import debug from .output import debug
from .utils.util import format_cols
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -78,9 +77,12 @@ class GfxDriver(Enum):
return False return False
def packages_text(self) -> str: def packages_text(self) -> str:
text = str(_('Installed packages')) + ':\n'
pkg_names = [p.value for p in self.gfx_packages()] 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 return text
def gfx_packages(self) -> List[GfxPackage]: def gfx_packages(self) -> List[GfxPackage]:

View File

@ -24,6 +24,7 @@ from . import pacman
from .pacman import Pacman from .pacman import Pacman
from .plugins import plugins from .plugins import plugins
from .storage import storage from .storage import storage
from archinstall.tui.curses_menu import Tui
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -105,9 +106,9 @@ class Installer:
# We avoid printing /mnt/<log path> because that might confuse people if they note it down # 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. # 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( log_file = os.path.join(storage['LOG_PATH'], storage['LOG_FILE'])
os.path.join(storage['LOG_PATH'], storage['LOG_FILE']))) Tui.print(str(_("[!] A log file has been created here: {}").format(log_file)))
print(_(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")) Tui.print(str(_('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues')))
raise exc_val raise exc_val
if not (missing_steps := self.post_install_check()): 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 .manage_users_conf import UserList, ask_for_additional_users
from .network_menu import ManualNetworkConfig, ask_to_configure_network from .network_menu import ManualNetworkConfig, ask_to_configure_network
from .utils import get_password
from .disk_conf import ( from .disk_conf import (
select_devices, select_disk_config, get_default_partition_layout, 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 .. import disk
from ..disk.device_model import BtrfsMountOption from ..disk.device_model import BtrfsMountOption
from ..hardware import SysInfo from ..hardware import SysInfo
from ..menu import Menu
from ..menu import TableMenu
from ..menu.menu import MenuSelectionType
from ..output import FormattedOutput, debug from ..output import FormattedOutput, debug
from ..utils.util import prompt_dir from ..utils.util import prompt_dir
from ..storage import storage 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: if TYPE_CHECKING:
_: Any _: Any
def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: def select_devices(preset: Optional[List[disk.BDevice]] = []) -> List[disk.BDevice]:
"""
Asks the user to select one or multiple devices
:return: List of selected devices
:rtype: list
"""
def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]: def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]:
dev = disk.device_handler.get_device(selection.path) dev = disk.device_handler.get_device(selection.path)
if dev and dev.partition_infos: if dev and dev.partition_infos:
@ -35,30 +32,25 @@ def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]:
if preset is None: if preset is None:
preset = [] 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 devices = disk.device_handler.devices
options = [d.device_info for d in 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( group, header = MenuHelper.create_table(data=options)
title, group.set_selected_by_value(presets)
data=options, result = SelectMenu(
multi=True, group,
preset=preset_value, header=header,
preview_command=_preview_device_selection, alignment=Alignment.CENTER,
preview_title=str(_('Existing Partitions')), search_enabled=False,
preview_size=0.2, multi=True
allow_reset=True,
allow_reset_warning_msg=warning
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Reset: return [] case ResultType.Reset: return []
case MenuSelectionType.Skip: return preset case ResultType.Skip: return preset
case MenuSelectionType.Selection: case ResultType.Selection:
selected_device_info: List[disk._DeviceInfo] = choice.single_value selected_device_info: List[disk._DeviceInfo] = result.get_values()
selected_devices = [] selected_devices = []
for device in devices: for device in devices:
@ -113,35 +105,40 @@ def select_disk_config(
manual_mode = disk.DiskLayoutType.Manual.display_msg() manual_mode = disk.DiskLayoutType.Manual.display_msg()
pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg() pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg()
options = [default_layout, manual_mode, pre_mount_mode] items = [
preset_value = preset.config_type.display_msg() if preset else None MenuItem(default_layout, value=default_layout),
warning = str(_('Are you sure you want to reset this setting?')) MenuItem(manual_mode, value=manual_mode),
MenuItem(pre_mount_mode, value=pre_mount_mode)
]
group = MenuItemGroup(items, sort_items=False)
choice = Menu( if preset:
_('Select a partitioning option'), group.set_selected_by_value(preset.config_type.display_msg())
options,
allow_reset=True, result = SelectMenu(
allow_reset_warning_msg=warning, group,
sort=False, allow_skip=True,
preview_size=0.2, alignment=Alignment.CENTER,
preset_values=preset_value frame=FrameProperties.min(str(_('Disk configuration type'))),
allow_reset=True
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Skip: return preset
case MenuSelectionType.Reset: return None case ResultType.Reset: return None
case MenuSelectionType.Selection: case ResultType.Selection:
if choice.single_value == pre_mount_mode: selection = result.get_value()
if selection == pre_mount_mode:
output = 'You will use whatever drive-setup is mounted at the specified directory\n' 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" output += "WARNING: Archinstall won't check the suitability of this setup\n"
try: path = prompt_dir(str(_('Root mount directory')), output, allow_skip=False)
path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) assert path is not None
except (KeyboardInterrupt, EOFError):
return preset
mods = disk.device_handler.detect_pre_mounted_mods(path) mods = disk.device_handler.detect_pre_mounted_mods(path)
storage['MOUNT_POINT'] = Path(path) storage['MOUNT_POINT'] = path
return disk.DiskLayoutConfiguration( return disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Pre_mount, config_type=disk.DiskLayoutType.Pre_mount,
@ -155,14 +152,14 @@ def select_disk_config(
if not devices: if not devices:
return None return None
if choice.value == default_layout: if result.get_value() == default_layout:
modifications = get_default_partition_layout(devices, advanced_option=advanced_option) modifications = get_default_partition_layout(devices, advanced_option=advanced_option)
if modifications: if modifications:
return disk.DiskLayoutConfiguration( return disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Default, config_type=disk.DiskLayoutType.Default,
device_modifications=modifications device_modifications=modifications
) )
elif choice.value == manual_mode: elif result.get_value() == manual_mode:
preset_mods = preset.device_modifications if preset else [] preset_mods = preset.device_modifications if preset else []
modifications = _manual_partitioning(preset_mods, devices) modifications = _manual_partitioning(preset_mods, devices)
@ -179,30 +176,29 @@ def select_lvm_config(
disk_config: disk.DiskLayoutConfiguration, disk_config: disk.DiskLayoutConfiguration,
preset: Optional[disk.LvmConfiguration] = None, preset: Optional[disk.LvmConfiguration] = None,
) -> Optional[disk.LvmConfiguration]: ) -> Optional[disk.LvmConfiguration]:
preset_value = preset.config_type.display_msg() if preset else None
default_mode = disk.LvmLayoutType.Default.display_msg() 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 result = SelectMenu(
warning = str(_('Are you sure you want to reset this setting?')) group,
choice = Menu(
_('Select a LVM option'),
options,
allow_reset=True, allow_reset=True,
allow_reset_warning_msg=warning, allow_skip=True,
sort=False, frame=FrameProperties.min(str(_('LVM configuration type'))),
preview_size=0.2, alignment=Alignment.CENTER
preset_values=preset_value
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Skip: return preset
case MenuSelectionType.Reset: return None case ResultType.Reset: return None
case MenuSelectionType.Selection: case ResultType.Selection:
if choice.single_value == default_mode: if result.get_value() == default_mode:
return suggest_lvm_layout(disk_config) return suggest_lvm_layout(disk_config)
return preset
return None
def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.PartitionModification: 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: def select_main_filesystem_format(advanced_options: bool = False) -> disk.FilesystemType:
options = { items = [
'btrfs': disk.FilesystemType.Btrfs, MenuItem('btrfs', value=disk.FilesystemType.Btrfs),
'ext4': disk.FilesystemType.Ext4, MenuItem('ext4', value=disk.FilesystemType.Ext4),
'xfs': disk.FilesystemType.Xfs, MenuItem('xfs', value=disk.FilesystemType.Xfs),
'f2fs': disk.FilesystemType.F2fs MenuItem('f2fs', value=disk.FilesystemType.F2fs)
} ]
if advanced_options: 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') group = MenuItemGroup(items, sort_items=False)
choice = Menu(prompt, options, skip=False, sort=False).run() result = SelectMenu(
return options[choice.single_value] 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]: def select_mount_options() -> List[str]:
prompt = str(_('Would you like to use compression or disable CoW?')) prompt = str(_('Would you like to use compression or disable CoW?')) + '\n'
options = [str(_('Use compression')), str(_('Disable Copy-on-Write'))] compression = str(_('Use compression'))
choice = Menu(prompt, options, sort=False).run() disable_cow = str(_('Disable Copy-on-Write'))
if choice.type_ == MenuSelectionType.Selection: items = [
if choice.single_value == options[0]: MenuItem(compression, value=BtrfsMountOption.compress.value),
return [BtrfsMountOption.compress.value] MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value),
else: ]
return [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: 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) min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB, sector_size)
if filesystem_type == disk.FilesystemType.Btrfs: if filesystem_type == disk.FilesystemType.Btrfs:
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n'
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() group = MenuItemGroup.yes_no()
using_subvolumes = choice.value == Menu.yes() 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() mount_options = select_mount_options()
else: else:
using_subvolumes = False using_subvolumes = False
@ -325,9 +354,19 @@ def suggest_single_disk_layout(
elif separate_home: elif separate_home:
using_home_partition = True using_home_partition = True
else: else:
prompt = str(_('Would you like to create a separate partition for /home?')) prompt = str(_('Would you like to create a separate partition for /home?')) + '\n'
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() group = MenuItemGroup.yes_no()
using_home_partition = choice.value == Menu.yes() 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 partition
root_start = boot_partition.start + boot_partition.length 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] root_device: Optional[disk.BDevice] = sorted_delta[0][0]
if home_device is None or root_device is None: 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 = str(_('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 += str(_('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)) text += str(_('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()
items = [MenuItem(str(_('Continue')))]
group = MenuItemGroup(items)
SelectMenu(group).run()
return [] return []
if filesystem_type == disk.FilesystemType.Btrfs: if filesystem_type == disk.FilesystemType.Btrfs:
@ -503,10 +546,21 @@ def suggest_lvm_layout(
filesystem_type = select_main_filesystem_format() filesystem_type = select_main_filesystem_format()
if filesystem_type == disk.FilesystemType.Btrfs: if filesystem_type == disk.FilesystemType.Btrfs:
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n'
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() group = MenuItemGroup.yes_no()
using_subvolumes = choice.value == Menu.yes() 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() mount_options = select_mount_options()
if using_subvolumes: if using_subvolumes:

View File

@ -4,86 +4,114 @@ import pathlib
from typing import List, Any, Optional, TYPE_CHECKING from typing import List, Any, Optional, TYPE_CHECKING
from ..locale import list_timezones from ..locale import list_timezones
from ..menu import MenuSelectionType, Menu, TextInput
from ..models.audio_configuration import Audio, AudioConfiguration from ..models.audio_configuration import Audio, AudioConfiguration
from ..output import warn from ..output import warn
from ..packages.packages import validate_package_list from ..packages.packages import validate_package_list
from ..storage import storage from ..storage import storage
from ..translationhandler import Language from ..translationhandler import Language
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
EditMenu, Orientation, Tui
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
def ask_ntp(preset: bool = True) -> bool: def ask_ntp(preset: bool = True) -> bool:
prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) header = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) + '\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')) 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'
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()
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: def ask_hostname(preset: Optional[str] = None) -> Optional[str]:
hostname = TextInput( result = EditMenu(
str(_('Desired hostname for the installation: ')), str(_('Hostname')),
preset alignment=Alignment.CENTER,
).run().strip() allow_skip=True,
default_text=preset
).input()
if not hostname: match result.type_:
return preset case ResultType.Skip:
return preset
return hostname 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]: def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]:
timezones = list_timezones()
default = 'UTC' default = 'UTC'
timezones = list_timezones()
choice = Menu( items = [MenuItem(tz, value=tz) for tz in timezones]
_('Select a timezone'), group = MenuItemGroup(items, sort_items=True)
timezones, group.set_selected_by_value(preset)
preset_values=preset, group.set_default_by_value(default)
default_option=default
result = SelectMenu(
group,
allow_reset=True,
allow_skip=True,
frame=FrameProperties.min(str(_('Timezone'))),
alignment=Alignment.CENTER,
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Skip:
case MenuSelectionType.Selection: return choice.single_value return preset
case ResultType.Reset:
return None return default
case ResultType.Selection:
return result.get_value()
def ask_for_audio_selection( def ask_for_audio_selection(preset: Optional[AudioConfiguration] = None) -> Optional[AudioConfiguration]:
current: Optional[AudioConfiguration] = None items = [MenuItem(a.value, value=a) for a in Audio]
) -> Optional[AudioConfiguration]: group = MenuItemGroup(items)
choices = [
Audio.Pipewire.name, # pylint: disable=no-member
Audio.Pulseaudio.name, # pylint: disable=no-member
Audio.no_audio_text()
]
preset = current.audio.name if current else None if preset:
group.set_focus_by_value(preset.audio)
choice = Menu( result = SelectMenu(
_('Choose an audio server'), group,
choices, allow_skip=True,
preset_values=preset alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Audio')))
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return current case ResultType.Skip:
case MenuSelectionType.Selection: return preset
value = choice.single_value case ResultType.Selection:
if value == Audio.no_audio_text(): return AudioConfiguration(audio=result.get_value())
return None case ResultType.Reset:
else: raise ValueError('Unhandled result type')
return AudioConfiguration(Audio[value])
return None
def select_language(preset: Optional[str] = None) -> Optional[str]: 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.") # 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. # 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) 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 # these are the displayed language names which can either be
# the english name of a language or, if present, the # the english name of a language or, if present, the
# name of the language in its own language # 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 = '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 += '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' title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n'
choice = Menu( result = SelectMenu(
title, group,
list(options.keys()), header=title,
default_option=preset.display_name, allow_skip=True,
preview_size=0.5 allow_reset=False,
alignment=Alignment.CENTER,
frame=FrameProperties.min(header=str(_('Select language')))
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Skip:
case MenuSelectionType.Selection: return options[choice.single_value] return preset
case ResultType.Selection:
raise ValueError('Language selection not handled') return result.get_value()
case ResultType.Reset:
raise ValueError('Language selection not handled')
def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]: def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]:
# Additional packages (with some light weight error handling for invalid package names) # Additional packages (with some light weight error handling for invalid package names)
print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) header = str(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) + '\n'
print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) 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]: def validator(value: str) -> Optional[str]:
display = ' '.join(p) packages = value.split() if value else []
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 []
preset = preset if preset else [] if len(packages) == 0:
packages = read_packages(preset) return None
if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: if storage['arguments']['offline'] or storage['arguments']['no_pkg_lookups']:
while True: return None
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 invalid: # Verify packages that were given
warn(f"Some packages could not be found in the repository: {invalid}") out = str(_("Verifying that additional packages exist (this might take a few seconds)"))
packages = read_packages(valid) Tui.print(out, 0)
continue valid, invalid = validate_package_list(packages)
break
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 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: try:
input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0) value = int(s)
if input_number <= 0: if value >= 0:
input_number = 0 return None
break except Exception:
except: pass
print(str(_("Invalid input! Try again with a valid input [or 0 to disable]")).format(max_recommended))
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") pacman_conf_path = pathlib.Path("/etc/pacman.conf")
with pacman_conf_path.open() as f: 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: with pacman_conf_path.open("w") as fwrite:
for line in pacman_conf: for line in pacman_conf:
if "ParallelDownloads" in line: 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: else:
fwrite.write(f"{line}\n") fwrite.write(f"{line}\n")
return input_number return downloads
def select_additional_repositories(preset: List[str]) -> List[str]: 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"] 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( result = SelectMenu(
_('Choose which optional additional repositories to enable'), group,
repositories, alignment=Alignment.CENTER,
sort=False, frame=FrameProperties.min('Additional repositories'),
multi=True, allow_reset=True,
preset_values=preset, allow_skip=True,
allow_reset=True multi=True
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Skip:
case MenuSelectionType.Reset: return [] return preset
case MenuSelectionType.Selection: return choice.single_value 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 import re
from typing import Any, TYPE_CHECKING, List, Optional from typing import Any, TYPE_CHECKING, List, Optional
from .utils import get_password from ..utils.util import get_password
from ..menu import Menu, ListManager from ..menu import ListManager
from ..models.users import User from ..models.users import User
from ..general import secret
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
Alignment, EditMenu, Orientation,
ResultType
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
class UserList(ListManager): class UserList(ListManager):
"""
subclass of ListManager for the managing of user accounts
"""
def __init__(self, prompt: str, lusers: List[User]): def __init__(self, prompt: str, lusers: List[User]):
self._actions = [ self._actions = [
str(_('Add a user')), str(_('Add a user')),
@ -37,8 +40,9 @@ class UserList(ListManager):
data = [d for d in data if d.username != new_user.username] data = [d for d in data if d.username != new_user.username]
data += [new_user] data += [new_user]
elif action == self._actions[1] and entry: # change password elif action == self._actions[1] and entry: # change password
prompt = str(_('Password for user "{}": ').format(entry.username)) header = f'{str(_("User"))}: {entry.username}\n'
new_password = get_password(prompt=prompt) new_password = get_password(str(_('Password')), header=header)
if new_password: if new_password:
user = next(filter(lambda x: x == entry, data)) user = next(filter(lambda x: x == entry, data))
user.password = new_password user.password = new_password
@ -50,42 +54,55 @@ class UserList(ListManager):
return data 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: if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
return True return None
return False return str(_("The username you entered is invalid"))
def _add_user(self) -> Optional[User]: 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: match editResult.type_:
try: case ResultType.Skip:
username = input(prompt).strip(' ')
except (KeyboardInterrupt, EOFError):
return None return None
case ResultType.Selection:
username = editResult.text()
case _:
raise ValueError('Unhandled result type')
if not username: header = f'{str(_("Username"))}: {username}\n'
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
password = get_password(prompt=str(_('Password for user "{}": ').format(username))) password = get_password(str(_('Password')), header=header, allow_skip=True)
if not password: if not password:
return None return None
choice = Menu( header += f'{str(_("Password"))}: {secret(password)}\n\n'
str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), header += str(_('Should "{}" be a superuser (sudo)?\n')).format(username)
skip=False,
default_option=Menu.yes(), group = MenuItemGroup.yes_no()
clear_screen=False, group.focus_item = MenuItem.yes()
show_search_hint=False
result = SelectMenu(
group,
header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
search_enabled=False,
allow_skip=False
).run() ).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) return User(username, password, sudo)

View File

@ -1,24 +1,23 @@
from __future__ import annotations from __future__ import annotations
import ipaddress 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 ..models.network_configuration import NetworkConfiguration, NicType, Nic
from ..networking import list_interfaces from ..networking import list_interfaces
from ..output import FormattedOutput, warn from ..menu import ListManager
from ..menu import ListManager, Menu from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
EditMenu
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
class ManualNetworkConfig(ListManager): class ManualNetworkConfig(ListManager):
"""
subclass of ListManager for the managing of network configurations
"""
def __init__(self, prompt: str, preset: List[Nic]): def __init__(self, prompt: str, preset: List[Nic]):
self._actions = [ self._actions = [
str(_('Add interface')), str(_('Add interface')),
@ -27,21 +26,6 @@ class ManualNetworkConfig(ListManager):
] ]
super().__init__(prompt, preset, [self._actions[0]], self._actions[1:]) 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: def selected_action_display(self, nic: Nic) -> str:
return nic.iface if nic.iface else '' return nic.iface if nic.iface else ''
@ -69,56 +53,112 @@ class ManualNetworkConfig(ListManager):
if not available: if not available:
return None return None
choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() if not available:
if choice.type_ == MenuSelectionType.Skip:
return None 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: def _edit_iface(self, edit_nic: Nic) -> Nic:
iface_name = edit_nic.iface iface_name = edit_nic.iface
modes = ['DHCP (auto detect)', 'IP (static)'] modes = ['DHCP (auto detect)', 'IP (static)']
default_mode = 'DHCP (auto detect)' default_mode = 'DHCP (auto detect)'
prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) header = str(_('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode)) + '\n'
mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() 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)': result = SelectMenu(
while 1: group,
prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) header=header,
ip = TextInput(prompt, edit_nic.ip).run().strip() allow_skip=False,
# Implemented new check for correct IP/subnet input alignment=Alignment.CENTER,
try: frame=FrameProperties.min(str(_('Modes')))
ipaddress.ip_interface(ip) ).run()
break
except ValueError:
warn("You need to enter a valid IP in IP-config mode")
# Implemented new check for correct gateway IP address match result.type_:
gateway = None case ResultType.Selection:
mode = result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
while 1: if mode == 'IP (static)':
gateway = TextInput( header = str(_('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name)) + '\n'
_('Enter your gateway (router) IP address or leave blank for none: '), ip = self._get_ip_address(str(_('IP address')), header, False, False)
edit_nic.gateway
).run().strip() header = str(_('Enter your gateway (router) IP address (leave blank for none)')) + '\n'
try: gateway = self._get_ip_address(str(_('Gateway address')), header, True, False)
if len(gateway) > 0:
ipaddress.ip_address(gateway)
break
except ValueError:
warn("You need to enter a valid gateway (router) IP address")
if edit_nic.dns: if edit_nic.dns:
display_dns = ' '.join(edit_nic.dns) display_dns = ' '.join(edit_nic.dns)
else: else:
display_dns = None 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 = [] dns = []
if len(dns_input): if dns_servers is not None:
dns = dns_input.split(' ') dns = dns_servers.split(' ')
return Nic(iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) return Nic(iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False)
else: else:
@ -128,35 +168,40 @@ class ManualNetworkConfig(ListManager):
def ask_to_configure_network(preset: Optional[NetworkConfiguration]) -> Optional[NetworkConfiguration]: 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( items = [MenuItem(n.display_msg(), value=n) for n in NicType]
_('Select one network interface to configure'), group = MenuItemGroup(items, sort_items=True)
list(options.keys()),
preset_values=preset_val, if preset:
sort=False, group.set_selected_by_value(preset.type)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Network configuration'))),
allow_reset=True, allow_reset=True,
allow_reset_warning_msg=warning allow_skip=True
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Skip:
case MenuSelectionType.Reset: return None return preset
case MenuSelectionType.Selection: case ResultType.Reset:
nic_type = options[choice.single_value] return None
case ResultType.Selection:
config = result.get_value()
match nic_type: match config:
case NicType.ISO: case NicType.ISO:
return NetworkConfiguration(NicType.ISO) return NetworkConfiguration(NicType.ISO)
case NicType.NM: case NicType.NM:
return NetworkConfiguration(NicType.NM) return NetworkConfiguration(NicType.NM)
case NicType.MANUAL: case NicType.MANUAL:
preset_nics = preset.nics if preset else [] preset_nics = preset.nics if preset else []
nics = ManualNetworkConfig('Configure interfaces', preset_nics).run() nics = ManualNetworkConfig(str(_('Configure interfaces')), preset_nics).run()
if nics: if nics:
return NetworkConfiguration(NicType.MANUAL, 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 typing import List, Any, TYPE_CHECKING, Optional
from ..hardware import SysInfo, GfxDriver from ..hardware import SysInfo, GfxDriver
from ..menu import MenuSelectionType, Menu
from ..models.bootloader import Bootloader from ..models.bootloader import Bootloader
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, FrameStyle, Alignment,
ResultType, Orientation, PreviewStyle
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -17,71 +22,88 @@ def select_kernel(preset: List[str] = []) -> List[str]:
:return: The string as a selected kernel :return: The string as a selected kernel
:rtype: string :rtype: string
""" """
kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"]
default_kernel = "linux" 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( group = MenuItemGroup(items, sort_items=True)
_('Choose which kernels to use or leave blank for default "{}"').format(default_kernel), group.set_default_by_value(default_kernel)
kernels, group.set_focus_by_value(default_kernel)
sort=True, group.set_selected_by_value(preset)
multi=True,
preset_values=preset, result = SelectMenu(
allow_reset_warning_msg=warning group,
allow_skip=True,
allow_reset=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Kernel'))),
multi=True
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Skip:
case MenuSelectionType.Selection: return choice.single_value return preset
case ResultType.Reset:
return [] 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 # Systemd is UEFI only
if not SysInfo.has_uefi(): if not SysInfo.has_uefi():
options = [Bootloader.Grub.value, Bootloader.Limine.value] options = [Bootloader.Grub, Bootloader.Limine]
default = Bootloader.Grub.value default = Bootloader.Grub
else: else:
options = Bootloader.values() options = [b for b in Bootloader]
default = Bootloader.Systemd.value 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( result = SelectMenu(
_('Choose a bootloader'), group,
options, alignment=Alignment.CENTER,
preset_values=preset_value, frame=FrameProperties.min(str(_('Bootloader'))),
sort=False, allow_skip=True
default_option=default
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Skip:
case MenuSelectionType.Selection: return Bootloader(choice.value) return preset
case ResultType.Selection:
return preset return result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
def ask_for_uki(preset: bool = True) -> bool: def ask_for_uki(preset: bool = True) -> bool:
if preset: prompt = str(_('Would you like to use unified kernel images?')) + '\n'
preset_val = Menu.yes()
else:
preset_val = Menu.no()
prompt = _('Would you like to use unified kernel images?') group = MenuItemGroup.yes_no()
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), preset_values=preset_val).run() group.set_focus_by_value(preset)
match choice.type_: result = SelectMenu(
case MenuSelectionType.Skip: return preset group,
case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True 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. Some what convoluted function, whose job is simple.
Select a graphics driver from a pre-defined set of popular options. 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: if not options:
options = [driver for driver in GfxDriver] 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: if preset is not None:
title = '' group.set_focus_by_value(preset)
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'))
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( result = SelectMenu(
title, group,
drivers, header=header,
preset_values=preset, allow_skip=True,
default_option=GfxDriver.AllOpenSource.value, allow_reset=True,
preview_command=lambda x: GfxDriver(x).packages_text(), preview_size='auto',
preview_size=0.3 preview_style=PreviewStyle.BOTTOM,
).run() preview_frame=FrameProperties(str(_('Info')), h_frame_style=FrameStyle.MIN)
).run()
if choice.type_ != MenuSelectionType.Selection: match result.type_:
return current_value case ResultType.Skip:
return preset
return GfxDriver(choice.single_value) case ResultType.Reset:
return None
return current_value case ResultType.Selection:
return result.get_value()
def ask_for_swap(preset: bool = True) -> bool: def ask_for_swap(preset: bool = True) -> bool:
if preset: if preset:
preset_val = Menu.yes() default_item = MenuItem.yes()
else: else:
preset_val = Menu.no() default_item = MenuItem.no()
prompt = _('Would you like to use swap on zram?') prompt = str(_('Would you like to use swap on zram?')) + '\n'
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run()
match choice.type_: group = MenuItemGroup.yes_no()
case MenuSelectionType.Skip: return preset group.set_focus_by_value(default_item)
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()
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 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 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 .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: if TYPE_CHECKING:
_: Any _: Any
@ -28,6 +33,12 @@ class LocaleConfiguration:
'sys_enc': self.sys_enc '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 @classmethod
def _load_config(cls, config: 'LocaleConfiguration', args: Dict[str, Any]) -> 'LocaleConfiguration': def _load_config(cls, config: 'LocaleConfiguration', args: Dict[str, Any]) -> 'LocaleConfiguration':
if 'sys_lang' in args: if 'sys_lang' in args:
@ -54,34 +65,50 @@ class LocaleConfiguration:
class LocaleMenu(AbstractSubMenu): class LocaleMenu(AbstractSubMenu):
def __init__( def __init__(
self, self,
data_store: Dict[str, Any],
locale_conf: LocaleConfiguration locale_conf: LocaleConfiguration
): ):
self._preset = locale_conf self._locale_conf = locale_conf
super().__init__(data_store=data_store) self._data_store: Dict[str, Any] = {}
menu_optioons = self._define_menu_options()
def setup_selection_menu_options(self) -> None: self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
self._menu_options['keyboard-layout'] = \ super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
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)
def run(self, allow_reset: bool = True) -> LocaleConfiguration: def _define_menu_options(self) -> List[MenuItem]:
super().run(allow_reset=allow_reset) 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: if not self._data_store:
return LocaleConfiguration.default() return LocaleConfiguration.default()
@ -103,59 +130,79 @@ def select_locale_lang(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales() locales = list_locales()
locale_lang = set([locale.split()[0] for locale in locales]) locale_lang = set([locale.split()[0] for locale in locales])
choice = Menu( items = [MenuItem(ll, value=ll) for ll in locale_lang]
_('Choose which locale language to use'), group = MenuItemGroup(items, sort_items=True)
list(locale_lang), group.set_focus_by_value(preset)
sort=True,
preset_values=preset result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Locale language'))),
allow_skip=True,
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Selection: return choice.single_value case ResultType.Selection:
case MenuSelectionType.Skip: return preset return result.get_value()
case ResultType.Skip:
return None return preset
case _:
raise ValueError('Unhandled return type')
def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: def select_locale_enc(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales() locales = list_locales()
locale_enc = set([locale.split()[1] for locale in locales]) locale_enc = set([locale.split()[1] for locale in locales])
choice = Menu( items = [MenuItem(le, value=le) for le in locale_enc]
_('Choose which locale encoding to use'), group = MenuItemGroup(items, sort_items=True)
list(locale_enc), group.set_focus_by_value(preset)
sort=True,
preset_values=preset result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Locale encoding'))),
allow_skip=True,
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Selection: return choice.single_value case ResultType.Selection:
case MenuSelectionType.Skip: return preset return result.get_value()
case ResultType.Skip:
return None return preset
case _:
raise ValueError('Unhandled return type')
def select_kb_layout(preset: Optional[str] = None) -> Optional[str]: def select_kb_layout(preset: Optional[str] = None) -> Optional[str]:
""" """
Asks the user to select a language Select keyboard layout
Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
:return: The language/dictionary key of the selected language :return: The keyboard layout shortcut for the selected layout
:rtype: str :rtype: str
""" """
kb_lang = list_keyboard_languages() kb_lang = list_keyboard_languages()
# sort alphabetically and then by length # sort alphabetically and then by length
sorted_kb_lang = sorted(kb_lang, key=lambda x: (len(x), x)) sorted_kb_lang = sorted(kb_lang, key=lambda x: (len(x), x))
choice = Menu( items = [MenuItem(lang, value=lang) for lang in sorted_kb_lang]
_('Select keyboard layout'), group = MenuItemGroup(items, sort_items=False)
sorted_kb_lang, group.set_focus_by_value(preset)
preset_values=preset,
sort=False result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Keyboard layout'))),
allow_skip=True,
).run() ).run()
match choice.type_: match result.type_:
case MenuSelectionType.Skip: return preset case ResultType.Selection:
case MenuSelectionType.Selection: return choice.single_value return result.get_value()
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled return type')
return None 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 .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 __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 error
from ..output import unicode_ljust 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: if TYPE_CHECKING:
_: Any _: Any
@ -144,41 +147,21 @@ class Selector:
class AbstractMenu: class AbstractMenu:
def __init__( def __init__(
self, self,
data_store: Dict[str, Any] = {}, item_group: MenuItemGroup,
auto_cursor: bool = False, data_store: Dict[str, Any],
preview_size: float = 0.2 auto_cursor: bool = True,
allow_reset: bool = False,
reset_warning: Optional[str] = None
): ):
""" self._menu_item_group = item_group
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._data_store = data_store self._data_store = data_store
self.auto_cursor = auto_cursor self.auto_cursor = auto_cursor
self._menu_options: Dict[str, Selector] = {} self._allow_reset = allow_reset
self.preview_size = preview_size self._reset_warning = reset_warning
self._last_choice = None
self.setup_selection_menu_options() self.is_context_mgr = False
self._sync_all()
self._populate_default_values()
self.defined_text = str(_('Defined')) self._sync_all_from_ds()
@property
def last_choice(self):
return self._last_choice
def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu: def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu:
self.is_context_mgr = True self.is_context_mgr = True
@ -189,263 +172,86 @@ class AbstractMenu:
# TODO: skip processing when it comes from a planified exit # TODO: skip processing when it comes from a planified exit
if len(args) >= 2 and args[1]: if len(args) >= 2 and args[1]:
error(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] raise args[1]
for key in self._menu_options: self._sync_all_to_ds()
selector = self._menu_options[key]
if key and key not in self._data_store:
self._data_store[key] = selector.current_selection
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 _sync_all_to_ds(self) -> None:
def translation_handler(self) -> TranslationHandler: for item in self._menu_item_group.menu_items:
return self._translation_handler if item.key:
self._data_store[item.key] = item.value
def _populate_default_values(self) -> None: def _sync(self, item: MenuItem) -> None:
for config_key, selector in self._menu_options.items(): if not item.key:
if selector.default is not None and config_key not in self._data_store: return
self._data_store[config_key] = selector.default
def _sync_all(self) -> None: store_value = self._data_store.get(item.key, None)
for key in self._menu_options.keys():
self._sync(key)
def _sync(self, selector_name: str) -> None: if store_value is not None:
value = self._data_store.get(selector_name, None) item.value = store_value
selector = self._menu_options.get(selector_name, None) elif item.value is not None:
self._data_store[item.key] = item.value
if value is not None: def set_enabled(self, key: str, enabled: bool) -> None:
self._menu_options[selector_name].set_current_selection(value) if (item := self._menu_item_group.find_by_key(key)) is not None:
elif selector is not None and selector.has_selection(): item.enabled = enabled
self._data_store[selector_name] = selector.current_selection return None
def setup_selection_menu_options(self) -> None: raise ValueError(f'No selector found: {key}')
""" Define the menu options.
Menu options can be defined here in a subclass or done per program calling self.set_option()
"""
return
def pre_callback(self, selector_name) -> None: def disable_all(self) -> None:
""" will be called before each action in the menu """ for item in self._menu_item_group.items:
return item.enabled = False
def post_callback(self, selection_name: Optional[str] = None, value: Any = None): def run(self) -> Optional[Any]:
""" will be called after each action in the menu """ self._sync_all_from_ds()
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
while True: while True:
enabled_menus = self._menus_to_enable() result = SelectMenu(
self._menu_item_group,
padding = self._get_menu_text_padding(list(enabled_menus.values())) allow_skip=False,
menu_options = [m.menu_text(padding) for m in enabled_menus.values()] allow_reset=self._allow_reset,
reset_warning_msg=self._reset_warning,
warning_msg = str(_('All settings will be reset, are you sure?')) preview_style=PreviewStyle.RIGHT,
preview_size='auto',
selection = Menu( preview_frame=FrameProperties('Info', FrameStyle.MAX),
_('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
).run() ).run()
match selection.type_: match result.type_:
case MenuSelectionType.Reset: case ResultType.Selection:
self._data_store = {} item: MenuItem = result.item()
return
case MenuSelectionType.Selection:
value: str = selection.value # type: ignore
if self.auto_cursor: if item.action is None:
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):
break break
case ResultType.Reset:
self._data_store = {}
return None
# we get the last action key self._sync_all_to_ds()
actions = {str(v.description): k for k, v in self._menu_options.items()} return None
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
class AbstractSubMenu(AbstractMenu): class AbstractSubMenu(AbstractMenu):
def __init__(self, data_store: Dict[str, Any] = {}, preview_size: float = 0.2): def __init__(
super().__init__(data_store=data_store, preview_size=preview_size) 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('') super().__init__(
self._menu_options['back'] = \ item_group,
Selector( data_store=data_store,
Menu.back(), auto_cursor=auto_cursor,
no_store=True, allow_reset=allow_reset
enabled=True, )
exec_func=lambda n, v: True,
)

View File

@ -1,10 +1,12 @@
import copy import copy
from os import system
from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List
from .menu import Menu
from ..output import FormattedOutput from ..output import FormattedOutput
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
Alignment, ResultType
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -63,31 +65,34 @@ class ListManager:
data_formatted = self.reformat(self._data) data_formatted = self.reformat(self._data)
options, header = self._prepare_selection(data_formatted) 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( result = SelectMenu(
self._prompt, group,
options,
sort=False,
clear_screen=False,
clear_menu_on_exit=False,
header=header, header=header,
skip_empty_entries=True, search_enabled=False,
skip=False, allow_skip=False,
show_search_hint=False alignment=Alignment.CENTER,
).run() ).run()
if choice.value in self._base_actions: match result.type_:
self._data = self.handle_action(choice.value, None, self._data) case ResultType.Selection:
elif choice.value in self._terminate_actions: 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 break
else: # an entry of the existing selection was chosen 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._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 return self._original_data # return the original list
else: else:
return self._data return self._data
@ -110,23 +115,30 @@ class ListManager:
return options, header 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] 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( header = f'{self.selected_action_display(entry)}\n'
prompt,
options, result = SelectMenu(
sort=False, group,
clear_screen=False, header=header,
clear_menu_on_exit=False, search_enabled=False,
show_search_hint=False allow_skip=False,
alignment=Alignment.CENTER
).run() ).run()
if choice.value and choice.value != self._cancel_action: match result.type_:
self._data = self.handle_action(choice.value, entry, self._data) 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]]: 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 # 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 # 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 # 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): for row, entry in zip(rows[2:], data):
row = row.replace('|', '\\|')
display_data[row] = entry display_data[row] = entry
return display_data 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 time
import json import json
import urllib.parse
from pathlib import Path from pathlib import Path
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum 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 .networking import fetch_data_from_url
from .output import FormattedOutput, debug from .output import FormattedOutput, debug
from .storage import storage from .storage import storage
from .models.mirrors import MirrorStatusListV3, MirrorStatusEntryV3 from .models.mirrors import MirrorStatusListV3, MirrorStatusEntryV3
from archinstall.tui import (
MenuItemGroup, MenuItem, SelectMenu,
FrameProperties, Alignment, ResultType,
EditMenu
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -67,7 +75,7 @@ class CustomMirror:
@dataclass @dataclass
class MirrorConfiguration: 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) custom_mirrors: List[CustomMirror] = field(default_factory=list)
@property @property
@ -85,7 +93,7 @@ class MirrorConfiguration:
for region, mirrors in self.mirror_regions.items(): for region, mirrors in self.mirror_regions.items():
for mirror in mirrors: 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: for cm in self.custom_mirrors:
config += f'\n\n## {cm.name}\nServer = {cm.url}\n' config += f'\n\n## {cm.name}\nServer = {cm.url}\n'
@ -116,13 +124,18 @@ class MirrorConfiguration:
class CustomMirrorList(ListManager): class CustomMirrorList(ListManager):
def __init__(self, prompt: str, custom_mirrors: List[CustomMirror]): def __init__(self, custom_mirrors: List[CustomMirror]):
self._actions = [ self._actions = [
str(_('Add a custom mirror')), str(_('Add a custom mirror')),
str(_('Change custom mirror')), str(_('Change custom mirror')),
str(_('Delete 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: def selected_action_display(self, mirror: CustomMirror) -> str:
return mirror.name return mirror.name
@ -148,164 +161,190 @@ class CustomMirrorList(ListManager):
return data return data
def _add_custom_mirror(self, mirror: Optional[CustomMirror] = None) -> Optional[CustomMirror]: def _add_custom_mirror(self, preset: Optional[CustomMirror] = None) -> Optional[CustomMirror]:
prompt = '\n\n' + str(_('Enter name (leave blank to skip): ')) edit_result = EditMenu(
existing_name = mirror.name if mirror else '' str(_('Mirror name')),
alignment=Alignment.CENTER,
allow_skip=True,
default_text=preset.name if preset else None
).input()
while True: match edit_result.type_:
name = TextInput(prompt, existing_name).run() case ResultType.Selection:
if not name: name = edit_result.text()
return mirror case ResultType.Skip:
break return preset
case _:
raise ValueError('Unhandled return type')
prompt = '\n' + str(_('Enter url (leave blank to skip): ')) header = f'{str(_("Name"))}: {name}'
existing_url = mirror.url if mirror else ''
while True: edit_result = EditMenu(
url = TextInput(prompt, existing_url).run() str(_('Url')),
if not url: header=header,
return mirror alignment=Alignment.CENTER,
break allow_skip=True,
default_text=preset.url if preset else None
).input()
sign_check_choice = Menu( match edit_result.type_:
str(_('Select signature check option')), case ResultType.Selection:
[s.value for s in SignCheck], url = edit_result.text()
skip=False, case ResultType.Skip:
clear_screen=False, return preset
preset_values=mirror.sign_check.value if mirror else None 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() ).run()
sign_option_choice = Menu( match result.type_:
str(_('Select signature option')), case ResultType.Selection:
[s.value for s in SignOption], sign_check = SignCheck(result.get_value())
skip=False, case _:
clear_screen=False, raise ValueError('Unhandled return type')
preset_values=mirror.sign_option.value if mirror else None
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() ).run()
return CustomMirror( match result.type_:
name, case ResultType.Selection:
url, sign_opt = SignOption(result.get_value())
SignCheck(sign_check_choice.single_value), case _:
SignOption(sign_option_choice.single_value) raise ValueError('Unhandled return type')
)
return CustomMirror(name, url, sign_check, sign_opt)
class MirrorMenu(AbstractSubMenu): class MirrorMenu(AbstractSubMenu):
def __init__( def __init__(
self, self,
data_store: Dict[str, Any],
preset: Optional[MirrorConfiguration] = None preset: Optional[MirrorConfiguration] = None
): ):
if preset: if preset:
self._preset = preset self._mirror_config = preset
else: 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: menu_optioons = self._define_menu_options()
self._menu_options['mirror_regions'] = \ self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
Selector(
_('Mirror region'), super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
lambda preset: select_mirror_regions(preset),
display_func=lambda x: ', '.join(x.keys()) if x else '', def _define_menu_options(self) -> List[MenuItem]:
default=self._preset.mirror_regions, return [
enabled=True) MenuItem(
self._menu_options['custom_mirrors'] = \ text=str(_('Mirror region')),
Selector( action=lambda x: select_mirror_regions(x),
_('Custom mirrors'), value=self._mirror_config.mirror_regions,
lambda preset: select_custom_mirror(preset=preset), preview_action=self._prev_regions,
display_func=lambda x: str(_('Defined')) if x else '', key='mirror_regions'
preview_func=self._prev_custom_mirror, ),
default=self._preset.custom_mirrors, MenuItem(
enabled=True 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]: def _prev_regions(self, item: MenuItem) -> Optional[str]:
selector = self._menu_options['custom_mirrors'] mirrors: Dict[str, List[MirrorStatusEntryV3]] = item.get_value()
if selector.has_selection(): output = ''
custom_mirrors: List[CustomMirror] = selector.current_selection # type: ignore for name, status_list in mirrors.items():
output = FormattedOutput.as_table(custom_mirrors) output += f'{name}\n'
return output.strip() 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]: output += '\n'
super().run(allow_reset=allow_reset)
if self._data_store.get('mirror_regions', None) or self._data_store.get('custom_mirrors', None): return output
return MirrorConfiguration(
mirror_regions=self._data_store['mirror_regions'],
custom_mirrors=self._data_store['custom_mirrors'],
)
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]]: def select_mirror_regions(preset: Dict[str, List[MirrorStatusEntryV3]]) -> Dict[str, List[MirrorStatusEntryV3]]:
""" mirrors: Dict[str, List[MirrorStatusEntryV3]] | None = list_mirrors_from_remote()
Asks the user to select a mirror or region
Usually this is combined with :ref:`archinstall.list_mirrors`.
:return: The dictionary information about a mirror/region. if not mirrors:
:rtype: dict mirrors = list_mirrors_from_local()
"""
if preset_values is None:
preselected = None
else:
preselected = list(preset_values.keys())
remote_mirrors = list_mirrors_from_remote() items = [MenuItem(name, value=(name, mirrors)) for name, mirrors in mirrors.items()]
mirrors: Dict[str, list[str]] = {} group = MenuItemGroup(items, sort_items=True)
if remote_mirrors: preset_values = [(name, mirror) for name, mirror in preset.items()]
choice = Menu( group.set_selected_by_value(preset_values)
_('Select one of the regions to download packages from'),
list(remote_mirrors.keys()),
preset_values=preselected,
multi=True,
allow_reset=True
).run()
match choice.type_: result = SelectMenu(
case MenuSelectionType.Reset: group,
return {} alignment=Alignment.CENTER,
case MenuSelectionType.Skip: frame=FrameProperties.min(str(_('Mirror regions'))),
return preset_values allow_reset=True,
case MenuSelectionType.Selection: allow_skip=True,
for region in choice.multi_value: multi=True,
mirrors.setdefault(region, []) ).run()
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()
choice = Menu( match result.type_:
_('Select one of the regions to download packages from'), case ResultType.Skip:
list(local_mirrors.keys()), return preset
preset_values=preselected, case ResultType.Reset:
multi=True, return {}
allow_reset=True case ResultType.Selection:
).run() selected_mirrors: List[Tuple[str, List[MirrorStatusEntryV3]]] = result.get_values()
return {name: mirror for name, mirror in selected_mirrors}
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
def select_custom_mirror(prompt: str = '', preset: List[CustomMirror] = []) -> list[CustomMirror]: def select_custom_mirror(preset: List[CustomMirror] = []):
custom_mirrors = CustomMirrorList(prompt, preset).run() custom_mirrors = CustomMirrorList(preset).run()
return custom_mirrors return custom_mirrors
@ -327,7 +366,7 @@ def list_mirrors_from_remote() -> Optional[Dict[str, List[MirrorStatusEntryV3]]]
return None 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: with Path('/etc/pacman.d/mirrorlist').open('r') as fp:
mirrorlist = fp.read() mirrorlist = fp.read()
return _parse_locale_mirrors(mirrorlist) return _parse_locale_mirrors(mirrorlist)
@ -370,13 +409,13 @@ def _parse_remote_mirror_list(mirrorlist: str) -> Dict[str, List[MirrorStatusEnt
return sorted_by_regions 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() lines = mirrorlist.splitlines()
# remove empty lines # remove empty lines
lines = [line for line in lines if line] lines = [line for line in lines if line]
mirror_list: Dict[str, List[str]] = {} mirror_list: Dict[str, List[MirrorStatusEntryV3]] = {}
current_region = '' current_region = ''
for idx, line in enumerate(lines): for idx, line in enumerate(lines):
@ -391,6 +430,20 @@ def _parse_locale_mirrors(mirrorlist: str) -> Dict[str, List[str]]:
break break
url = line.removeprefix('Server = ') 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 return mirror_list

View File

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

View File

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

View File

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

View File

@ -100,7 +100,7 @@ class FormattedOutput:
value = record.get(key, '') value = record.get(key, '')
if '!' in key: if '!' in key:
value = '*' * width value = '*' * len(value)
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()): if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
obj_data.append(unicode_rjust(str(value), width)) obj_data.append(unicode_rjust(str(value), width))
@ -322,14 +322,9 @@ def log(
Journald.log(text, level=level) Journald.log(text, level=level)
from .menu import Menu if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False):
if not Menu.is_menu_active(): from archinstall.tui import Tui
# Finally, print the log unless we skipped it based on level. Tui.print(text)
# 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()
def _count_wchars(string: str) -> int: def _count_wchars(string: str) -> int:

View File

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

View File

@ -13,11 +13,11 @@ from typing import List, TYPE_CHECKING, Any, Optional, Dict, Union
from ...default_profiles.profile import Profile, GreeterType from ...default_profiles.profile import Profile, GreeterType
from .profile_model import ProfileConfiguration from .profile_model import ProfileConfiguration
from ..hardware import GfxDriver from ..hardware import GfxDriver
from ..menu import MenuSelectionType, Menu, MenuSelection
from ..networking import list_interfaces, fetch_data_from_url from ..networking import list_interfaces, fetch_data_from_url
from ..output import error, debug, info from ..output import error, debug, info
from ..storage import storage from ..storage import storage
if TYPE_CHECKING: if TYPE_CHECKING:
from ..installer import Installer from ..installer import Installer
_: Any _: Any
@ -358,58 +358,5 @@ class ProfileHandler:
if profile.name not in excluded_profiles: if profile.name not in excluded_profiles:
profile.reset() 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() profile_handler = ProfileHandler()

View File

@ -2,22 +2,98 @@ from pathlib import Path
from typing import Any, TYPE_CHECKING, Optional, List from typing import Any, TYPE_CHECKING, Optional, List
from ..output import FormattedOutput from ..output import FormattedOutput
from ..output import info from ..general import secret
from archinstall.tui import (
Alignment, EditMenu
)
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
def prompt_dir(text: str, header: Optional[str] = None) -> Path: def get_password(
if header: text: str,
print(header) header: Optional[str] = None,
allow_skip: bool = False,
preset: Optional[str] = None
) -> Optional[str]:
failure: Optional[str] = None
while True: 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) dest_path = Path(path)
if dest_path.exists() and dest_path.is_dir(): if dest_path.exists() and dest_path.is_dir():
return dest_path return None
info(_('Not a valid directory: {}').format(dest_path))
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: 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 col = 4
text += FormattedOutput.as_columns(items, col) text += FormattedOutput.as_columns(items, col)
# remove whitespaces on each row
text = '\n'.join([t.strip() for t in text.split('\n')])
return text return text

View File

@ -9,10 +9,11 @@ from archinstall.lib import disk
from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
from archinstall.lib.menu import Menu
from archinstall.lib.models import AudioConfiguration, Bootloader from archinstall.lib.models import AudioConfiguration, Bootloader
from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.profile.profiles_handler import profile_handler 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: if TYPE_CHECKING:
_: Any _: Any
@ -25,76 +26,18 @@ if archinstall.arguments.get('help'):
def ask_user_questions() -> None: def ask_user_questions() -> None:
""" """
First, we'll ask the user for a bunch of user input. First, we'll ask the user for a bunch of user input.
Not until we're satisfied with what we want to install Not until we're satisfied with what we want to install
will we continue with the actual installation steps. will we continue with the actual installation steps.
""" """
# ref: https://github.com/archlinux/archinstall/pull/831 with Tui():
# we'll set NTP to true by default since this is also global_menu = GlobalMenu(data_store=archinstall.arguments)
# 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)
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.run()
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()
def perform_installation(mountpoint: Path) -> None: 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") info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
if not archinstall.arguments.get('silent'): if not archinstall.arguments.get('silent'):
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) with Tui():
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() chroot = ask_chroot()
if choice.value == Menu.yes():
if chroot:
try: try:
installation.drop_to_shell() installation.drop_to_shell()
except: except:
@ -220,27 +164,30 @@ def perform_installation(mountpoint: Path) -> None:
debug(f"Disk states after installing: {disk.disk_layouts()}") debug(f"Disk states after installing: {disk.disk_layouts()}")
if not archinstall.arguments.get('silent'): def guided() -> None:
ask_user_questions() 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'): if archinstall.arguments.get('dry_run'):
config_output.show() 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'): fs_handler = disk.FilesystemHandler(
exit(0) archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if not archinstall.arguments.get('silent'): fs_handler.perform_filesystem_operations()
input(str(_('Press Enter to continue.'))) 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() guided()
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))

View File

@ -2,13 +2,14 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any, List from typing import TYPE_CHECKING, Any, List
import archinstall import archinstall
from archinstall import info from archinstall import info, debug
from archinstall import Installer, ConfigurationOutput from archinstall import Installer, ConfigurationOutput
from archinstall.default_profiles.minimal import MinimalProfile from archinstall.default_profiles.minimal import MinimalProfile
from archinstall.lib.interactions import suggest_single_disk_layout, select_devices from archinstall.lib.interactions import suggest_single_disk_layout, select_devices
from archinstall.lib.models import Bootloader, User from archinstall.lib.models import Bootloader, User
from archinstall.lib.profile import ProfileConfiguration, profile_handler from archinstall.lib.profile import ProfileConfiguration, profile_handler
from archinstall.lib import disk from archinstall.lib import disk
from archinstall.tui import Tui
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -88,19 +89,31 @@ def parse_disk_encryption() -> None:
) )
prompt_disk_layout() def minimal() -> None:
parse_disk_encryption() with Tui():
prompt_disk_layout()
parse_disk_encryption()
config_output = ConfigurationOutput(archinstall.arguments) config = ConfigurationOutput(archinstall.arguments)
config_output.show() config.write_debug()
config.save()
input(str(_('Press Enter to continue.'))) if archinstall.arguments.get('dry_run'):
exit(0)
fs_handler = disk.FilesystemHandler( if not archinstall.arguments.get('silent'):
archinstall.arguments['disk_config'], with Tui():
archinstall.arguments.get('disk_encryption', None) 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.installer import Installer
from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib import disk from archinstall.lib import disk
from archinstall.tui import Tui
def ask_user_questions() -> None: 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.run()
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()
def perform_installation(mountpoint: Path) -> None: 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()}") debug(f"Disk states after installing: {disk.disk_layouts()}")
if not archinstall.arguments.get('silent'): def only_hd() -> None:
ask_user_questions() if not archinstall.arguments.get('silent'):
ask_user_questions()
config_output = ConfigurationOutput(archinstall.arguments) config = ConfigurationOutput(archinstall.arguments)
if not archinstall.arguments.get('silent'): config.write_debug()
config_output.show() config.save()
config_output.save() if archinstall.arguments.get('dry_run'):
exit(0)
if archinstall.arguments.get('dry_run'): if not archinstall.arguments.get('silent'):
exit(0) with Tui():
if not config.confirm_config():
debug('Installation aborted')
only_hd()
if not archinstall.arguments.get('silent'): fs_handler = disk.FilesystemHandler(
input('Press Enter to continue.') archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
fs_handler = disk.FilesystemHandler( fs_handler.perform_filesystem_operations()
archinstall.arguments['disk_config'], perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
archinstall.arguments.get('disk_encryption', None)
)
fs_handler.perform_filesystem_operations()
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 import locale
from archinstall.lib.models import AudioConfiguration from archinstall.lib.models import AudioConfiguration
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib import menu
from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
from archinstall.lib.configuration import ConfigurationOutput 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: if TYPE_CHECKING:
_: Any _: Any
class ExecutionMode(Enum): class ExecutionMode(Enum):
Full = 'full' Guided = 'guided'
Lineal = 'lineal' Lineal = 'lineal'
Only_HD = 'only-hd' Only_HD = 'only-hd'
Only_OS = 'only-os' Only_OS = 'only-os'
Minimal = 'minimal' 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): class SwissMainMenu(GlobalMenu):
def __init__( def __init__(
self, self,
data_store: Dict[str, Any], 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) super().__init__(data_store)
def setup_selection_menu_options(self) -> None: def execute(self) -> None:
super().setup_selection_menu_options() ignore = ['install', 'abort']
options_list = []
mandatory_list = []
match self._execution_mode: match self._execution_mode:
case ExecutionMode.Full | ExecutionMode.Lineal: case ExecutionMode.Guided:
options_list = [ from archinstall.scripts.guided import guided
'mirror_config', 'disk_config', guided()
'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.Only_HD: case ExecutionMode.Only_HD:
options_list = ['disk_config', 'disk_encryption', 'swap'] from archinstall.scripts.only_hd import only_hd
mandatory_list = ['disk_config'] only_hd()
case ExecutionMode.Only_OS: case ExecutionMode.Minimal:
options_list = [ from archinstall.scripts.minimal import minimal
'mirror_config', 'bootloader', 'hostname', minimal()
'!root-password', '!users', 'profile_config', 'audio_config', 'kernels', case ExecutionMode.Lineal:
'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' 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'] mandatory_list = ['hostname']
if archinstall.arguments.get('advanced', False): if self._advanced:
options_list += ['locale_config'] menu_items += ['locale_config']
case ExecutionMode.Minimal:
pass 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 _: case _:
info(f' Execution mode {self._execution_mode} not supported') info(f' Execution mode {self._execution_mode} not supported')
exit(1) exit(1)
if self._execution_mode != ExecutionMode.Lineal:
options_list += ['save_config', 'install']
if not archinstall.arguments.get('advanced', False): def ask_user_questions(mode: ExecutionMode = ExecutionMode.Guided) -> None:
options_list.append('archinstall-language') 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: result = SelectMenu(
self.enable(entry, mandatory=True) group,
header=header,
allow_skip=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(str(_('Modes')))
).run()
for entry in options_list: if result.type_ == ResultType.Skip:
self.enable(entry) exit(0)
mode = result.get_value()
def ask_user_questions(exec_mode: ExecutionMode = ExecutionMode.Full) -> None: SwissMainMenu(
""" data_store=archinstall.arguments,
First, we'll ask the user for a bunch of user input. mode=mode,
Not until we're satisfied with what we want to install advanced=advanced
will we continue with the actual installation steps. ).execute()
"""
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()
def perform_installation(mountpoint: Path, exec_mode: ExecutionMode) -> None: 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, disk_encryption=disk_encryption,
kernels=archinstall.arguments.get('kernels', ['linux']) kernels=archinstall.arguments.get('kernels', ['linux'])
) as installation: ) 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_config.config_type != disk.DiskLayoutType.Pre_mount:
if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption: if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption:
# generate encryption key files for the mounted luks devices # generate encryption key files for the mounted luks devices
installation.generate_key_files() installation.generate_key_files()
if mirror_config := archinstall.arguments.get('mirror_config', None): if mirror_config := archinstall.arguments.get('mirror_config', None):
installation.set_mirrors(mirror_config) installation.set_mirrors(mirror_config)
installation.minimal_installation( installation.minimal_installation(
testing=enable_testing, testing=enable_testing,
multilib=enable_multilib, multilib=enable_multilib,
hostname=archinstall.arguments.get('hostname', 'archlinux'), hostname=archinstall.arguments.get('hostname', 'archlinux'),
locale_config=locale_config 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): if users := archinstall.arguments.get('!users', None):
installation.set_mirrors(mirror_config, on_target=True) installation.create_users(users)
if archinstall.arguments.get('swap'): audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None)
installation.setup_swap('zram') 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(): if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages("grub") 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 if timezone := archinstall.arguments.get('timezone', None):
# Perform a copy of the config installation.set_timezone(timezone)
network_config = archinstall.arguments.get('network_config', None)
if network_config: if archinstall.arguments.get('ntp', False):
network_config.install_network_config( installation.activate_time_synchronization()
installation,
archinstall.arguments.get('profile_config', None)
)
if users := archinstall.arguments.get('!users', None): if archinstall.accessibility_tools_in_use():
installation.create_users(users) installation.enable_espeakup()
audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None) if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw):
if audio_config: installation.user_set_pw('root', root_pw)
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] != '': if profile_config := archinstall.arguments.get('profile_config', None):
installation.add_additional_packages(archinstall.arguments.get('packages', [])) profile_config.profile.post_install(installation)
if profile_config := archinstall.arguments.get('profile_config', None): # If the user provided a list of services to be enabled, pass the list to the enable_service function.
profile_handler.install_profile_config(installation, profile_config) # 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): # If the user provided custom commands to be run post-installation, execute them now.
installation.set_timezone(timezone) if archinstall.arguments.get('custom-commands', None):
archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation)
if archinstall.arguments.get('ntp', False): installation.genfstab()
installation.activate_time_synchronization()
if archinstall.accessibility_tools_in_use(): info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
installation.enable_espeakup()
if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): if not archinstall.arguments.get('silent'):
installation.user_set_pw('root', root_pw) with Tui():
chroot = ask_chroot()
if profile_config := archinstall.arguments.get('profile_config', None): if chroot:
profile_config.profile.post_install(installation) try:
installation.drop_to_shell()
# If the user provided a list of services to be enabled, pass the list to the enable_service function. except:
# Note that while it's called enable_service, it can actually take a list of services and iterate it. pass
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
debug(f"Disk states after installing: {disk.disk_layouts()}") 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: try:
mode = ExecutionMode(param_mode) mode = ExecutionMode(param_mode)
except KeyError: except KeyError:
info(f'Mode "{param_mode}" is not supported') info(f'Mode "{param_mode}" is not supported')
exit(1) exit(1)
if not archinstall.arguments.get('silent'): if not archinstall.arguments.get('silent'):
ask_user_questions(mode) ask_user_questions(mode)
config_output = ConfigurationOutput(archinstall.arguments) config = ConfigurationOutput(archinstall.arguments)
if not archinstall.arguments.get('silent'): config.write_debug()
config_output.show() config.save()
config_output.save() if archinstall.arguments.get('dry_run'):
exit(0)
if archinstall.arguments.get('dry_run'): if not archinstall.arguments.get('silent'):
exit(0) 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( fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'], archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None) 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')), mode)
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')), mode)
swiss()

View File

@ -3,6 +3,8 @@ import time
import archinstall import archinstall
from archinstall import info from archinstall import info
from archinstall import profile from archinstall import profile
from archinstall.tui import Tui
for p in profile.profile_handler.get_mac_addr_profiles(): for p in profile.profile_handler.get_mac_addr_profiles():
# Tailored means it's a match for this machine # 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:') print('Starting install in:')
for i in range(10, 0, -1): for i in range(10, 0, -1):
print(f'{i}...') Tui.print(f'{i}...')
time.sleep(1) time.sleep(1)
install_session = archinstall.storage['installation_session'] 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( selection = HelpGroup(
group_id=HelpTextGroupId.SELECTION, group_id=HelpTextGroupId.SELECTION,
group_entries=[ group_entries=[
HelpText('Skip selction (if available)', ['Esc']),
HelpText('Reset selection (if available)', ['Ctrl+c']),
HelpText('Select on single select', ['Enter']), HelpText('Select on single select', ['Enter']),
HelpText('Select on select', ['Space', 'Tab']), HelpText('Select on select', ['Space', 'Tab']),
HelpText('Reset', ['Ctrl-C']), HelpText('Reset', ['Ctrl-C']),
@ -75,18 +77,14 @@ class Help:
max_desc_width = max([help.get_desc_width() for help in help_texts]) 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]) max_key_width = max([help.get_key_width() for help in help_texts])
margin = ' ' * 3
for help in help_texts: for help in help_texts:
help_output += f'{margin}{help.group_id.value}\n' help_output += f'{help.group_id.value}\n'
divider_len = max_desc_width + max_key_width + len(margin * 2) divider_len = max_desc_width + max_key_width
help_output += margin + '-' * divider_len + '\n' help_output += '-' * divider_len + '\n'
for entry in help.group_entries: for entry in help.group_entries:
help_output += ( help_output += (
margin +
entry.description.ljust(max_desc_width, ' ') + entry.description.ljust(max_desc_width, ' ') +
margin +
', '.join(entry.keys) + '\n' ', '.join(entry.keys) + '\n'
) )

View File

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

View File

@ -1,12 +1,10 @@
import curses import curses
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from typing import Optional, List, TypeVar, Generic from typing import Optional, List, Any
from .menu_item import MenuItem from .menu_item import MenuItem
ItemType = TypeVar('ItemType', MenuItem, List[MenuItem], str)
SCROLL_INTERVAL = 10 SCROLL_INTERVAL = 10
@ -46,8 +44,8 @@ class MenuKeys(Enum):
ESC = {27} ESC = {27}
# BACKSPACE (search) # BACKSPACE (search)
BACKSPACE = {127, 263} BACKSPACE = {127, 263}
# Help view: CTRL+h # Help view: ?
HELP = {8} HELP = {63}
# Scroll up: CTRL+up, CTRL+k # Scroll up: CTRL+up, CTRL+k
SCROLL_UP = {581} SCROLL_UP = {581}
# Scroll down: CTRL+down, CTRL+j # Scroll down: CTRL+down, CTRL+j
@ -80,6 +78,22 @@ class FrameProperties:
w_frame_style: FrameStyle = FrameStyle.MAX w_frame_style: FrameStyle = FrameStyle.MAX
h_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): class ResultType(Enum):
Selection = auto() Selection = auto()
@ -87,7 +101,7 @@ class ResultType(Enum):
Reset = auto() Reset = auto()
class MenuOrientation(Enum): class Orientation(Enum):
VERTICAL = auto() VERTICAL = auto()
HORIZONTAL = auto() HORIZONTAL = auto()
@ -106,6 +120,7 @@ class PreviewStyle(Enum):
# https://www.compart.com/en/unicode/search?q=box+drawings#characters # https://www.compart.com/en/unicode/search?q=box+drawings#characters
# https://en.wikipedia.org/wiki/Box-drawing_characters
class Chars: class Chars:
Horizontal = "" Horizontal = ""
Vertical = "" Vertical = ""
@ -116,12 +131,36 @@ class Chars:
Block = "" Block = ""
Triangle_up = "" Triangle_up = ""
Triangle_down = "" Triangle_down = ""
Check = "+"
Cross = "x"
Right_arrow = ""
@dataclass @dataclass
class Result(Generic[ItemType]): class Result:
type_: ResultType 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 @dataclass

View File

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

View File

@ -1,79 +1,40 @@
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Callable, Optional from typing import Optional
import archinstall 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 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: 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 if not archinstall.arguments.get('advanced', False):
global_menu.enable('mirror_config') global_menu.set_enabled('parallel downloads', False)
global_menu.enable('locale_config') global_menu.run()
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()
def perform_installation(mountpoint: Path) -> None: 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 # Retrieve list of additional repositories and set boolean values appropriately
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
enable_multilib = 'multilib' 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'] locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
@ -109,11 +71,12 @@ def perform_installation(mountpoint: Path) -> None:
installation.generate_key_files() installation.generate_key_files()
if mirror_config := archinstall.arguments.get('mirror_config', None): 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( installation.minimal_installation(
testing=enable_testing, testing=enable_testing,
multilib=enable_multilib, multilib=enable_multilib,
mkinitcpio=run_mkinitcpio,
hostname=archinstall.arguments.get('hostname', 'archlinux'), hostname=archinstall.arguments.get('hostname', 'archlinux'),
locale_config=locale_config locale_config=locale_config
) )
@ -124,14 +87,17 @@ def perform_installation(mountpoint: Path) -> None:
if archinstall.arguments.get('swap'): if archinstall.arguments.get('swap'):
installation.setup_swap('zram') 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_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 # If user selected to copy the current ISO network configuration
# Perform a copy of the config # 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: if network_config:
network_config.install_network_config( network_config.install_network_config(
@ -142,17 +108,17 @@ def perform_installation(mountpoint: Path) -> None:
if users := archinstall.arguments.get('!users', None): if users := archinstall.arguments.get('!users', None):
installation.create_users(users) 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: if audio_config:
audio_config.install_audio_config(installation) audio_config.install_audio_config(installation)
else: else:
info("No audio server will be installed") info("No audio server will be installed")
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': 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): 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): if timezone := archinstall.arguments.get('timezone', None):
installation.set_timezone(timezone) 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") info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
if not archinstall.arguments.get('silent'): if not archinstall.arguments.get('silent'):
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) with Tui():
choice = menu.Menu(prompt, menu.Menu.yes_no(), default_option=menu.Menu.yes()).run() chroot = ask_chroot()
if choice.value == menu.Menu.yes():
if chroot:
try: try:
installation.drop_to_shell() installation.drop_to_shell()
except Exception: except:
pass pass
debug(f"Disk states after installing: {disk.disk_layouts()}") 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( config = ConfigurationOutput(archinstall.arguments)
archinstall.arguments['disk_config'], config.write_debug()
archinstall.arguments.get('disk_encryption', None) 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 import archinstall
from archinstall import profile, info from archinstall import profile, info
from archinstall.tui import Tui
for _profile in profile.profile_handler.get_mac_addr_profiles(): for _profile in profile.profile_handler.get_mac_addr_profiles():
# Tailored means it's a match for this machine # 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:') print('Starting install in:')
for i in range(10, 0, -1): for i in range(10, 0, -1):
print(f'{i}...') Tui.print(f'{i}...')
time.sleep(1) time.sleep(1)
install_session = archinstall.storage['installation_session'] install_session = archinstall.storage['installation_session']

View File

@ -1,16 +1,24 @@
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Callable, List from typing import List
import archinstall import archinstall
from archinstall import disk from archinstall import info, debug
from archinstall import Installer from archinstall import Installer, ConfigurationOutput
from archinstall import profile
from archinstall import models
from archinstall import interactions
from archinstall.default_profiles.minimal import MinimalProfile 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: 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. # some other minor details as specified by this profile and user.
if installation.minimal_installation(): if installation.minimal_installation():
installation.set_hostname('minimal-arch') installation.set_hostname('minimal-arch')
installation.add_bootloader(models.Bootloader.Systemd) installation.add_bootloader(Bootloader.Systemd)
# Optionally enable networking: # Optionally enable networking:
if archinstall.arguments.get('network', None): if archinstall.arguments.get('network', None):
@ -35,20 +43,26 @@ def perform_installation(mountpoint: Path) -> None:
installation.add_additional_packages(['nano', 'wget', 'git']) installation.add_additional_packages(['nano', 'wget', 'git'])
profile_config = profile.ProfileConfiguration(MinimalProfile()) profile_config = ProfileConfiguration(MinimalProfile())
profile.profile_handler.install_profile_config(installation, profile_config) profile_handler.install_profile_config(installation, profile_config)
user = models.User('devel', 'devel', False) user = User('devel', 'devel', False)
installation.create_users(user) 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: def prompt_disk_layout() -> None:
fs_type = None fs_type = None
if filesystem := archinstall.arguments.get('filesystem', None): if filesystem := archinstall.arguments.get('filesystem', None):
fs_type = disk.FilesystemType(filesystem) fs_type = disk.FilesystemType(filesystem)
devices = interactions.select_devices() devices = select_devices()
modifications = interactions.suggest_single_disk_layout(devices[0], filesystem_type=fs_type) modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type)
archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration( archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Default, config_type=disk.DiskLayoutType.Default,
@ -72,15 +86,31 @@ def parse_disk_encryption() -> None:
) )
prompt_disk_layout() def _minimal() -> None:
parse_disk_encryption() with Tui():
prompt_disk_layout()
parse_disk_encryption()
fs_handler = disk.FilesystemHandler( config = ConfigurationOutput(archinstall.arguments)
archinstall.arguments['disk_config'], config.write_debug()
archinstall.arguments.get('disk_encryption', None) config.save()
)
fs_handler.perform_filesystem_operations() if archinstall.arguments.get('dry_run'):
exit(0)
mount_point = Path('/mnt') if not archinstall.arguments.get('silent'):
perform_installation(mount_point) 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 from pathlib import Path
import archinstall 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: 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.run()
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()
def perform_installation(mountpoint: Path) -> None: 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()}") 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( config = ConfigurationOutput(archinstall.arguments)
archinstall.arguments['disk_config'], config.write_debug()
archinstall.arguments.get('disk_encryption', None) 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 ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "archinstall.lib.models.mirrors"
disallow_untyped_decorators = false
disallow_subclassing_any = false
[tool.bandit] [tool.bandit]
targets = ["archinstall"] targets = ["archinstall"]
exclude = ["/tests"] exclude = ["/tests"]