archinstall/archinstall/lib/bootloader/bootloader_menu.py

230 lines
6.4 KiB
Python

import textwrap
from typing import override
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation
from ..args import arch_config_handler
from ..hardware import SysInfo
from ..menu.abstract_menu import AbstractSubMenu
from ..models.bootloader import Bootloader, BootloaderConfiguration
class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
def __init__(
self,
bootloader_conf: BootloaderConfiguration,
):
self._bootloader_conf = bootloader_conf
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
super().__init__(
self._item_group,
config=self._bootloader_conf,
allow_reset=False,
)
def _define_menu_options(self) -> list[MenuItem]:
bootloader = self._bootloader_conf.bootloader
has_uefi = SysInfo.has_uefi()
# UKI availability
uki_enabled = has_uefi and bootloader.has_uki_support()
if not uki_enabled:
self._bootloader_conf.uki = False
# Removable availability
removable_enabled = has_uefi and bootloader.has_removable_support()
if not removable_enabled:
self._bootloader_conf.removable = False
return [
MenuItem(
text=tr('Bootloader'),
action=self._select_bootloader,
value=self._bootloader_conf.bootloader,
preview_action=self._prev_bootloader,
mandatory=True,
key='bootloader',
),
MenuItem(
text=tr('Unified kernel images'),
action=self._select_uki,
value=self._bootloader_conf.uki,
preview_action=self._prev_uki,
key='uki',
enabled=uki_enabled,
),
MenuItem(
text=tr('Install to removable location'),
action=self._select_removable,
value=self._bootloader_conf.removable,
preview_action=self._prev_removable,
key='removable',
enabled=removable_enabled,
),
]
def _prev_bootloader(self, item: MenuItem) -> str | None:
if item.value:
return f'{tr("Bootloader")}: {item.value.value}'
return None
def _prev_uki(self, item: MenuItem) -> str | None:
uki_text = f'{tr("Unified kernel images")}'
if item.value:
return f'{uki_text}: {tr("Enabled")}'
else:
return f'{uki_text}: {tr("Disabled")}'
def _prev_removable(self, item: MenuItem) -> str | None:
if item.value:
return tr('Will install to /EFI/BOOT/ (removable location)')
return tr('Will install to standard location with NVRAM entry')
@override
def run(
self,
additional_title: str | None = None,
) -> BootloaderConfiguration:
super().run(additional_title=additional_title)
return self._bootloader_conf
def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None:
bootloader = ask_for_bootloader(preset)
if bootloader:
# Update UKI option based on bootloader
uki_item = self._menu_item_group.find_by_key('uki')
if not SysInfo.has_uefi() or not bootloader.has_uki_support():
uki_item.enabled = False
uki_item.value = False
self._bootloader_conf.uki = False
else:
uki_item.enabled = True
# Update removable option based on bootloader
removable_item = self._menu_item_group.find_by_key('removable')
if not SysInfo.has_uefi() or not bootloader.has_removable_support():
removable_item.enabled = False
removable_item.value = False
self._bootloader_conf.removable = False
else:
removable_item.enabled = True
return bootloader
def _select_uki(self, preset: bool) -> bool:
prompt = tr('Would you like to use unified kernel images?') + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(preset)
result = SelectMenu[bool](
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')
def _select_removable(self, preset: bool) -> bool:
prompt = (
tr('Would you like to install the bootloader to the default removable media search location?')
+ '\n\n'
+ tr('This installs the bootloader to /EFI/BOOT/BOOTX64.EFI (or similar) which is useful for:')
+ '\n\n'
+ tr('USB drives or other portable external media.')
+ '\n'
+ tr('Systems where you want the disk to be bootable on any computer.')
+ '\n'
+ tr('Firmware that does not properly support NVRAM boot entries.')
+ '\n\n'
+ tr(
textwrap.dedent(
"""\
This is NOT recommended if none of the above apply, as it makes installing multiple
EFI bootloaders on the same disk more challenging, and it overwrites whatever bootloader
was previously installed on the default removable media search location, if any.
"""
)
)
+ '\n'
)
group = MenuItemGroup.yes_no()
group.set_focus_by_value(preset)
result = SelectMenu[bool](
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')
def ask_for_bootloader(preset: Bootloader | None) -> Bootloader | None:
options = []
hidden_options = []
default = None
header = None
if arch_config_handler.args.skip_boot:
default = Bootloader.NO_BOOTLOADER
else:
hidden_options += [Bootloader.NO_BOOTLOADER]
if not SysInfo.has_uefi():
options += [Bootloader.Grub, Bootloader.Limine]
if not default:
default = Bootloader.Grub
header = tr('UEFI is not detected and some options are disabled')
else:
options += [b for b in Bootloader if b not in hidden_options]
if not default:
default = Bootloader.Systemd
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)
result = SelectMenu[Bootloader](
group,
header=header,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Bootloader')),
allow_skip=True,
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')