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:
parent
591b8317ea
commit
88b91ae201
|
|
@ -24,7 +24,7 @@ body:
|
|||
- type: textarea
|
||||
id: bug-report
|
||||
attributes:
|
||||
label: The installation log
|
||||
label: The installation log
|
||||
description: 'note: located at `/var/log/archinstall/install.log`'
|
||||
placeholder: |
|
||||
Hardware model detected: Dell Inc. Precision 7670; UEFI mode: True
|
||||
|
|
@ -82,4 +82,4 @@ body:
|
|||
**Note**: Feel free to modify the textarea above as you wish.
|
||||
But it will grately help us in testing if we can generate the specific qemu command line,
|
||||
for instance via:
|
||||
`sudo virsh domxml-to-native qemu-argv --domain my-arch-machine`
|
||||
`sudo virsh domxml-to-native qemu-argv --domain my-arch-machine`
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ body:
|
|||
description: >
|
||||
Feel free to write any feature you think others might benefit from:
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ jobs:
|
|||
- run: python --version
|
||||
- run: mypy --version
|
||||
- name: run mypy
|
||||
run: mypy
|
||||
run: mypy --config-file pyproject.toml
|
||||
|
|
|
|||
|
|
@ -38,4 +38,4 @@ venv
|
|||
requirements.txt
|
||||
/.gitconfig
|
||||
/actions-runner
|
||||
/cmd_output.txt
|
||||
/cmd_output.txt
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ repos:
|
|||
rev: v1.13.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [
|
||||
'--config-file=pyproject.toml'
|
||||
]
|
||||
fail_fast: true
|
||||
additional_dependencies:
|
||||
- pydantic
|
||||
|
|
|
|||
2
.pypirc
2
.pypirc
|
|
@ -3,4 +3,4 @@ index-servers =
|
|||
pypi
|
||||
|
||||
[pypi]
|
||||
repository = https://upload.pypi.org/legacy/
|
||||
repository = https://upload.pypi.org/legacy/
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@ sphinx:
|
|||
build:
|
||||
os: "ubuntu-22.04"
|
||||
tools:
|
||||
python: "3.11"
|
||||
python: "3.11"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from pathlib import Path
|
|||
from typing import TYPE_CHECKING, Any, Dict, Union
|
||||
|
||||
from .lib import disk
|
||||
from .lib import menu
|
||||
from .lib import models
|
||||
from .lib import packages
|
||||
from .lib import exceptions
|
||||
|
|
@ -32,6 +31,7 @@ from .lib.boot import Boot
|
|||
from .lib.translationhandler import TranslationHandler, Language, DeferredTranslation
|
||||
from .lib.plugins import plugins, load_plugin
|
||||
from .lib.configuration import ConfigurationOutput
|
||||
from .tui import Tui
|
||||
|
||||
from .lib.general import (
|
||||
generate_password, locate_binary, clear_vt100_escape_codes,
|
||||
|
|
@ -330,24 +330,6 @@ def main() -> None:
|
|||
importlib.import_module(mod_name)
|
||||
|
||||
|
||||
def _shutdown_curses() -> None:
|
||||
try:
|
||||
curses.nocbreak()
|
||||
|
||||
try:
|
||||
from archinstall.tui.curses_menu import tui
|
||||
tui.screen.keypad(False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
curses.echo()
|
||||
curses.curs_set(True)
|
||||
curses.endwin()
|
||||
except Exception:
|
||||
# this may happen when curses has not been initialized
|
||||
pass
|
||||
|
||||
|
||||
def run_as_a_module() -> None:
|
||||
exc = None
|
||||
|
||||
|
|
@ -357,7 +339,7 @@ def run_as_a_module() -> None:
|
|||
exc = e
|
||||
finally:
|
||||
# restore the terminal to the original state
|
||||
_shutdown_curses()
|
||||
Tui.shutdown()
|
||||
|
||||
if exc:
|
||||
err = ''.join(traceback.format_exception(exc))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
from typing import Any, TYPE_CHECKING, List, Optional, Dict
|
||||
|
||||
from archinstall.lib import menu
|
||||
from archinstall.lib.output import info
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, GreeterType
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, ResultType, PreviewStyle
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
_: Any
|
||||
|
|
@ -52,22 +56,36 @@ class DesktopProfile(Profile):
|
|||
for profile in self.current_selection:
|
||||
profile.do_on_select()
|
||||
|
||||
def do_on_select(self) -> SelectResult:
|
||||
choice = profile_handler.select_profile(
|
||||
profile_handler.get_desktop_profiles(),
|
||||
self.current_selection,
|
||||
title=str(_('Select your desired desktop environment')),
|
||||
multi=True
|
||||
)
|
||||
def do_on_select(self) -> Optional[SelectResult]:
|
||||
items = [
|
||||
MenuItem(
|
||||
p.name,
|
||||
value=p,
|
||||
preview_action=lambda x: x.value.preview_text()
|
||||
) for p in profile_handler.get_desktop_profiles()
|
||||
]
|
||||
|
||||
match choice.type_:
|
||||
case menu.MenuSelectionType.Selection:
|
||||
self.current_selection = choice.value # type: ignore
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_selected_by_value(self.current_selection)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
multi=True,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
preview_style=PreviewStyle.RIGHT,
|
||||
preview_size='auto',
|
||||
preview_frame=FrameProperties.max('Info')
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
self.current_selection = result.get_values()
|
||||
self._do_on_select_profiles()
|
||||
return SelectResult.NewSelection
|
||||
case menu.MenuSelectionType.Skip:
|
||||
case ResultType.Skip:
|
||||
return SelectResult.SameSelection
|
||||
case menu.MenuSelectionType.Reset:
|
||||
case ResultType.Reset:
|
||||
return SelectResult.ResetCurrent
|
||||
|
||||
def post_install(self, install_session: 'Installer') -> None:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING, Any
|
||||
|
||||
from archinstall.default_profiles.profile import ProfileType, GreeterType
|
||||
from archinstall.default_profiles.profile import ProfileType, GreeterType, SelectResult
|
||||
from archinstall.default_profiles.xorg import XorgProfile
|
||||
from archinstall.lib.menu import Menu
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, ResultType, Alignment
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
|
@ -49,20 +52,30 @@ class HyprlandProfile(XorgProfile):
|
|||
|
||||
def _ask_seat_access(self) -> None:
|
||||
# need to activate seat service and add to seat group
|
||||
title = str(_('Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)'))
|
||||
title += str(_('\n\nChoose an option to give Hyprland access to your hardware'))
|
||||
header = str(_('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)'))
|
||||
header += '\n' + str(_('Choose an option to give Sway access to your hardware')) + '\n'
|
||||
|
||||
options = [e.value for e in SeatAccess]
|
||||
default = None
|
||||
items = [MenuItem(s.value, value=s) for s in SeatAccess]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
|
||||
if seat := self.custom_settings.get('seat_access', None):
|
||||
default = seat
|
||||
default = self.custom_settings.get('seat_access', None)
|
||||
group.set_default_by_value(default)
|
||||
|
||||
choice = Menu(title, options, skip=False, preset_values=default).run()
|
||||
self.custom_settings['seat_access'] = choice.single_value
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
frame=FrameProperties.min(str(_('Seat access'))),
|
||||
alignment=Alignment.CENTER
|
||||
).run()
|
||||
|
||||
def do_on_select(self):
|
||||
if result.type_ == ResultType.Selection:
|
||||
if result.item() is not None:
|
||||
self.custom_settings['seat_access'] = result.get_value()
|
||||
|
||||
def do_on_select(self) -> Optional[SelectResult]:
|
||||
self._ask_seat_access()
|
||||
return None
|
||||
|
||||
def install(self, install_session: 'Installer') -> None:
|
||||
super().install(install_session)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING, Any
|
||||
|
||||
from archinstall.default_profiles.profile import ProfileType, GreeterType
|
||||
from archinstall.default_profiles.profile import ProfileType, GreeterType, SelectResult
|
||||
from archinstall.default_profiles.xorg import XorgProfile
|
||||
from archinstall.lib.menu import Menu
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
|
@ -58,20 +62,30 @@ class SwayProfile(XorgProfile):
|
|||
|
||||
def _ask_seat_access(self) -> None:
|
||||
# need to activate seat service and add to seat group
|
||||
title = str(_('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)'))
|
||||
title += str(_('\n\nChoose an option to give Sway access to your hardware'))
|
||||
header = str(_('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)'))
|
||||
header += '\n' + str(_('Choose an option to give Sway access to your hardware')) + '\n'
|
||||
|
||||
options = [e.value for e in SeatAccess]
|
||||
default = None
|
||||
items = [MenuItem(s.value, value=s) for s in SeatAccess]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
|
||||
if seat := self.custom_settings.get('seat_access', None):
|
||||
default = seat
|
||||
default = self.custom_settings.get('seat_access', None)
|
||||
group.set_default_by_value(default)
|
||||
|
||||
choice = Menu(title, options, skip=False, preset_values=default).run()
|
||||
self.custom_settings['seat_access'] = choice.single_value
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
frame=FrameProperties.min(str(_('Seat access'))),
|
||||
alignment=Alignment.CENTER
|
||||
).run()
|
||||
|
||||
def do_on_select(self):
|
||||
if result.type_ == ResultType.Selection:
|
||||
if result.item() is not None:
|
||||
self.custom_settings['seat_access'] = result.get_value()
|
||||
|
||||
def do_on_select(self) -> Optional[SelectResult]:
|
||||
self._ask_seat_access()
|
||||
return None
|
||||
|
||||
def install(self, install_session: 'Installer') -> None:
|
||||
super().install(install_session)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import sys
|
|||
from enum import Enum, auto
|
||||
from typing import List, Optional, Any, Dict, TYPE_CHECKING
|
||||
|
||||
from ..lib.utils.util import format_cols
|
||||
from ..lib.storage import storage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -126,7 +125,7 @@ class Profile:
|
|||
"""
|
||||
return {}
|
||||
|
||||
def do_on_select(self) -> SelectResult:
|
||||
def do_on_select(self) -> Optional[SelectResult]:
|
||||
"""
|
||||
Hook that will be called when a profile is selected
|
||||
"""
|
||||
|
|
@ -187,24 +186,20 @@ class Profile:
|
|||
"""
|
||||
return self.packages_text()
|
||||
|
||||
def packages_text(self, include_sub_packages: bool = False) -> Optional[str]:
|
||||
header = str(_('Installed packages'))
|
||||
|
||||
text = ''
|
||||
packages = []
|
||||
def packages_text(self, include_sub_packages: bool = False) -> str:
|
||||
packages = set()
|
||||
|
||||
if self.packages:
|
||||
packages = self.packages
|
||||
packages = set(self.packages)
|
||||
|
||||
if include_sub_packages:
|
||||
for p in self.current_selection:
|
||||
if p.packages:
|
||||
packages += p.packages
|
||||
for sub_profile in self.current_selection:
|
||||
if sub_profile.packages:
|
||||
packages.update(sub_profile.packages)
|
||||
|
||||
text += format_cols(sorted(set(packages)))
|
||||
text = str(_('Installed packages')) + ':\n'
|
||||
|
||||
if text:
|
||||
text = f'{header}: \n{text}'
|
||||
return text
|
||||
for pkg in sorted(packages):
|
||||
text += f'\t- {pkg}\n'
|
||||
|
||||
return None
|
||||
return text
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
from typing import Any, TYPE_CHECKING, List
|
||||
from typing import Any, TYPE_CHECKING, List, Optional
|
||||
|
||||
from archinstall.lib.output import info
|
||||
from archinstall.lib.menu import MenuSelectionType
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.default_profiles.profile import ProfileType, Profile, SelectResult
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, ResultType, PreviewStyle
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
_: Any
|
||||
|
|
@ -19,23 +23,36 @@ class ServerProfile(Profile):
|
|||
current_selection=current_value
|
||||
)
|
||||
|
||||
def do_on_select(self) -> SelectResult:
|
||||
available_servers = profile_handler.get_server_profiles()
|
||||
def do_on_select(self) -> Optional[SelectResult]:
|
||||
items = [
|
||||
MenuItem(
|
||||
p.name,
|
||||
value=p,
|
||||
preview_action=lambda x: x.value.preview_text()
|
||||
) for p in profile_handler.get_server_profiles()
|
||||
]
|
||||
|
||||
choice = profile_handler.select_profile(
|
||||
available_servers,
|
||||
self.current_selection,
|
||||
title=str(_('Choose which servers to install, if none then a minimal installation will be done')),
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_selected_by_value(self.current_selection)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
preview_style=PreviewStyle.RIGHT,
|
||||
preview_size='auto',
|
||||
preview_frame=FrameProperties.max('Info'),
|
||||
multi=True
|
||||
)
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Selection:
|
||||
self.current_selection = choice.value # type: ignore
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
selections = result.get_values()
|
||||
self.current_selection = selections
|
||||
return SelectResult.NewSelection
|
||||
case MenuSelectionType.Skip:
|
||||
case ResultType.Skip:
|
||||
return SelectResult.SameSelection
|
||||
case MenuSelectionType.Reset:
|
||||
case ResultType.Reset:
|
||||
return SelectResult.ResetCurrent
|
||||
|
||||
def post_install(self, install_session: 'Installer') -> None:
|
||||
|
|
|
|||
|
|
@ -5,10 +5,16 @@ import readline
|
|||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, TYPE_CHECKING
|
||||
|
||||
from .menu import Menu, MenuSelectionType
|
||||
from .storage import storage
|
||||
from .general import JSON, UNSAFE_JSON
|
||||
from .output import debug, info, warn
|
||||
from .output import debug, warn
|
||||
from .utils.util import prompt_dir
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType,
|
||||
PreviewStyle, Orientation, Tui
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -68,12 +74,35 @@ class ConfigurationOutput:
|
|||
return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON)
|
||||
return None
|
||||
|
||||
def show(self) -> None:
|
||||
print(_('\nThis is your chosen configuration:'))
|
||||
def write_debug(self) -> None:
|
||||
debug(" -- Chosen configuration --")
|
||||
debug(self.user_config_to_json())
|
||||
|
||||
info(self.user_config_to_json())
|
||||
print()
|
||||
def confirm_config(self) -> bool:
|
||||
header = f'{str(_("The specified configuration will be applied"))}. '
|
||||
header += str(_('Would you like to continue?')) + '\n'
|
||||
|
||||
with Tui():
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.yes()
|
||||
group.set_preview_for_all(lambda x: self.user_config_to_json())
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
allow_skip=False,
|
||||
preview_size='auto',
|
||||
preview_style=PreviewStyle.BOTTOM,
|
||||
preview_frame=FrameProperties.max(str(_('Configuration')))
|
||||
).run()
|
||||
|
||||
if result.item() != MenuItem.yes():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _is_valid_path(self, dest_path: Path) -> bool:
|
||||
dest_path_ok = dest_path.exists() and dest_path.is_dir()
|
||||
|
|
@ -105,9 +134,9 @@ class ConfigurationOutput:
|
|||
self.save_user_creds(dest_path)
|
||||
|
||||
|
||||
def save_config(config: Dict) -> None:
|
||||
def preview(selection: str) -> Optional[str]:
|
||||
match options[selection]:
|
||||
def save_config(config: Dict[str, Any]) -> None:
|
||||
def preview(item: MenuItem) -> Optional[str]:
|
||||
match item.value:
|
||||
case "user_config":
|
||||
serialized = config_output.user_config_to_json()
|
||||
return f"{config_output.user_configuration_file}\n{serialized}"
|
||||
|
|
@ -122,60 +151,80 @@ def save_config(config: Dict) -> None:
|
|||
return '\n'.join(output)
|
||||
return None
|
||||
|
||||
try:
|
||||
config_output = ConfigurationOutput(config)
|
||||
config_output = ConfigurationOutput(config)
|
||||
|
||||
options = {
|
||||
str(_("Save user configuration (including disk layout)")): "user_config",
|
||||
str(_("Save user credentials")): "user_creds",
|
||||
str(_("Save all")): "all",
|
||||
}
|
||||
items = [
|
||||
MenuItem(
|
||||
str(_("Save user configuration (including disk layout)")),
|
||||
value="user_config",
|
||||
preview_action=lambda x: preview(x)
|
||||
),
|
||||
MenuItem(
|
||||
str(_("Save user credentials")),
|
||||
value="user_creds",
|
||||
preview_action=lambda x: preview(x)
|
||||
),
|
||||
MenuItem(
|
||||
str(_("Save all")),
|
||||
value="all",
|
||||
preview_action=lambda x: preview(x)
|
||||
)
|
||||
]
|
||||
|
||||
save_choice = Menu(
|
||||
_("Choose which configuration to save"),
|
||||
list(options),
|
||||
sort=False,
|
||||
skip=True,
|
||||
preview_size=0.75,
|
||||
preview_command=preview,
|
||||
).run()
|
||||
group = MenuItemGroup(items)
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_skip=True,
|
||||
preview_frame=FrameProperties.max(str(_('Configuration'))),
|
||||
preview_size='auto',
|
||||
preview_style=PreviewStyle.RIGHT
|
||||
).run()
|
||||
|
||||
if save_choice.type_ == MenuSelectionType.Skip:
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return
|
||||
case ResultType.Selection:
|
||||
save_option = result.get_value()
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
readline.set_completer_delims("\t\n=")
|
||||
readline.parse_and_bind("tab: complete")
|
||||
while True:
|
||||
path = input(
|
||||
_(
|
||||
"Enter a directory for the configuration(s) to be saved (tab completion enabled)\nSave directory: "
|
||||
)
|
||||
).strip(" ")
|
||||
dest_path = Path(path)
|
||||
if dest_path.exists() and dest_path.is_dir():
|
||||
break
|
||||
info(_("Not a valid directory: {}").format(dest_path), fg="red")
|
||||
readline.set_completer_delims("\t\n=")
|
||||
readline.parse_and_bind("tab: complete")
|
||||
|
||||
if not path:
|
||||
return
|
||||
dest_path = prompt_dir(
|
||||
str(_('Directory')),
|
||||
str(_('Enter a directory for the configuration(s) to be saved (tab completion enabled)')) + '\n',
|
||||
allow_skip=True
|
||||
)
|
||||
|
||||
prompt = _(
|
||||
"Do you want to save {} configuration file(s) in the following location?\n\n{}"
|
||||
).format(options[str(save_choice.value)], dest_path.absolute())
|
||||
|
||||
save_confirmation = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run()
|
||||
if save_confirmation == Menu.no():
|
||||
return
|
||||
|
||||
debug("Saving {} configuration files to {}".format(options[str(save_choice.value)], dest_path.absolute()))
|
||||
|
||||
match options[str(save_choice.value)]:
|
||||
case "user_config":
|
||||
config_output.save_user_config(dest_path)
|
||||
case "user_creds":
|
||||
config_output.save_user_creds(dest_path)
|
||||
case "all":
|
||||
config_output.save(dest_path)
|
||||
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
if not dest_path:
|
||||
return
|
||||
|
||||
header = str(_("Do you want to save the configuration file(s) to {}?")).format(dest_path)
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.yes()
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
if result.item() == MenuItem.no():
|
||||
return
|
||||
|
||||
debug("Saving configuration files to {}".format(dest_path.absolute()))
|
||||
|
||||
match save_option:
|
||||
case "user_config":
|
||||
config_output.save_user_config(dest_path)
|
||||
case "user_creds":
|
||||
config_output.save_user_creds(dest_path)
|
||||
case "all":
|
||||
config_output.save(dest_path)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ from ..disk import (
|
|||
)
|
||||
from ..interactions import select_disk_config
|
||||
from ..interactions.disk_conf import select_lvm_config
|
||||
from ..menu import (
|
||||
Selector,
|
||||
AbstractSubMenu
|
||||
)
|
||||
from ..output import FormattedOutput
|
||||
from ..menu import AbstractSubMenu
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -21,37 +22,38 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
|
|||
def __init__(
|
||||
self,
|
||||
disk_layout_config: Optional[DiskLayoutConfiguration],
|
||||
data_store: Dict[str, Any],
|
||||
advanced: bool = False
|
||||
):
|
||||
self._disk_layout_config = disk_layout_config
|
||||
self._advanced = advanced
|
||||
self._data_store: Dict[str, Any] = {}
|
||||
|
||||
super().__init__(data_store=data_store, preview_size=0.5)
|
||||
menu_optioons = self._define_menu_options()
|
||||
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
self._menu_options['disk_config'] = \
|
||||
Selector(
|
||||
_('Partitioning'),
|
||||
lambda x: self._select_disk_layout_config(x),
|
||||
display_func=lambda x: self._display_disk_layout(x),
|
||||
preview_func=self._prev_disk_layouts,
|
||||
default=self._disk_layout_config,
|
||||
enabled=True
|
||||
)
|
||||
self._menu_options['lvm_config'] = \
|
||||
Selector(
|
||||
f'{_('LVM - Logical Volume Management')} (BETA)',
|
||||
lambda x: self._select_lvm_config(x),
|
||||
display_func=lambda x: self.defined_text if x else '',
|
||||
preview_func=self._prev_lvm_config,
|
||||
default=self._disk_layout_config.lvm_config if self._disk_layout_config else None,
|
||||
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
|
||||
|
||||
def _define_menu_options(self) -> List[MenuItem]:
|
||||
return [
|
||||
MenuItem(
|
||||
text=str(_('Partitioning')),
|
||||
action=lambda x: self._select_disk_layout_config(x),
|
||||
value=self._disk_layout_config,
|
||||
preview_action=self._prev_disk_layouts,
|
||||
key='disk_config'
|
||||
),
|
||||
MenuItem(
|
||||
text='LVM (BETA)',
|
||||
action=lambda x: self._select_lvm_config(x),
|
||||
value=self._disk_layout_config.lvm_config if self._disk_layout_config else None,
|
||||
preview_action=self._prev_lvm_config,
|
||||
dependencies=[self._check_dep_lvm],
|
||||
enabled=True
|
||||
)
|
||||
key='lvm_config'
|
||||
),
|
||||
]
|
||||
|
||||
def run(self, allow_reset: bool = True) -> Optional[DiskLayoutConfiguration]:
|
||||
super().run(allow_reset=allow_reset)
|
||||
def run(self) -> Optional[DiskLayoutConfiguration]:
|
||||
super().run()
|
||||
|
||||
disk_layout_config: Optional[DiskLayoutConfiguration] = self._data_store.get('disk_config', None)
|
||||
|
||||
|
|
@ -61,7 +63,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
|
|||
return disk_layout_config
|
||||
|
||||
def _check_dep_lvm(self) -> bool:
|
||||
disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
|
||||
disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_item_group.find_by_key('disk_config').value
|
||||
|
||||
if disk_layout_conf and disk_layout_conf.config_type == DiskLayoutType.Default:
|
||||
return True
|
||||
|
|
@ -75,66 +77,72 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
|
|||
disk_config = select_disk_config(preset, advanced_option=self._advanced)
|
||||
|
||||
if disk_config != preset:
|
||||
self._menu_options['lvm_config'].set_current_selection(None)
|
||||
self._menu_item_group.find_by_key('lvm_config').value = None
|
||||
|
||||
return disk_config
|
||||
|
||||
def _select_lvm_config(self, preset: Optional[LvmConfiguration]) -> Optional[LvmConfiguration]:
|
||||
disk_config: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
|
||||
disk_config: Optional[DiskLayoutConfiguration] = self._item_group.find_by_key('disk_config').value
|
||||
|
||||
if disk_config:
|
||||
return select_lvm_config(disk_config, preset=preset)
|
||||
|
||||
return preset
|
||||
|
||||
def _display_disk_layout(self, current_value: Optional[DiskLayoutConfiguration] = None) -> str:
|
||||
if current_value:
|
||||
return current_value.config_type.display_msg()
|
||||
return ''
|
||||
def _prev_disk_layouts(self, item: MenuItem) -> Optional[str]:
|
||||
if not item.value:
|
||||
return None
|
||||
|
||||
def _prev_disk_layouts(self) -> Optional[str]:
|
||||
disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
|
||||
disk_layout_conf: DiskLayoutConfiguration = item.get_value()
|
||||
|
||||
if disk_layout_conf:
|
||||
device_mods: List[DeviceModification] = \
|
||||
list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications))
|
||||
if disk_layout_conf.config_type == DiskLayoutType.Pre_mount:
|
||||
msg = str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) + '\n'
|
||||
msg += str(_('Mountpoint')) + ': ' + str(disk_layout_conf.mountpoint)
|
||||
return msg
|
||||
|
||||
if device_mods:
|
||||
output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg())
|
||||
output_btrfs = ''
|
||||
device_mods: List[DeviceModification] = \
|
||||
list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications))
|
||||
|
||||
for mod in device_mods:
|
||||
# create partition table
|
||||
partition_table = FormattedOutput.as_table(mod.partitions)
|
||||
if device_mods:
|
||||
output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg())
|
||||
output_btrfs = ''
|
||||
|
||||
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
|
||||
output_partition += partition_table + '\n'
|
||||
for mod in device_mods:
|
||||
# create partition table
|
||||
partition_table = FormattedOutput.as_table(mod.partitions)
|
||||
|
||||
# create btrfs table
|
||||
btrfs_partitions = list(
|
||||
filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions)
|
||||
)
|
||||
for partition in btrfs_partitions:
|
||||
output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
|
||||
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
|
||||
output_partition += partition_table + '\n'
|
||||
|
||||
output = output_partition + output_btrfs
|
||||
return output.rstrip()
|
||||
# create btrfs table
|
||||
btrfs_partitions = list(
|
||||
filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions)
|
||||
)
|
||||
for partition in btrfs_partitions:
|
||||
output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
|
||||
|
||||
output = output_partition + output_btrfs
|
||||
return output.rstrip()
|
||||
|
||||
return None
|
||||
|
||||
def _prev_lvm_config(self) -> Optional[str]:
|
||||
lvm_config: Optional[LvmConfiguration] = self._menu_options['lvm_config'].current_selection
|
||||
def _prev_lvm_config(self, item: MenuItem) -> Optional[str]:
|
||||
if not item.value:
|
||||
return None
|
||||
|
||||
if lvm_config:
|
||||
output = '{}: {}\n'.format(str(_('Configuration')), lvm_config.config_type.display_msg())
|
||||
lvm_config: LvmConfiguration = item.value
|
||||
|
||||
for vol_gp in lvm_config.vol_groups:
|
||||
pv_table = FormattedOutput.as_table(vol_gp.pvs)
|
||||
output += '{}:\n{}'.format(str(_('Physical volumes')), pv_table)
|
||||
output = '{}: {}\n'.format(str(_('Configuration')), lvm_config.config_type.display_msg())
|
||||
|
||||
output += f'\nVolume Group: {vol_gp.name}'
|
||||
for vol_gp in lvm_config.vol_groups:
|
||||
pv_table = FormattedOutput.as_table(vol_gp.pvs)
|
||||
output += '{}:\n{}'.format(str(_('Physical volumes')), pv_table)
|
||||
|
||||
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
|
||||
output += '\n\n{}:\n{}'.format(str(_('Volumes')), lvm_volumes)
|
||||
output += f'\nVolume Group: {vol_gp.name}'
|
||||
|
||||
return output
|
||||
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
|
||||
output += '\n\n{}:\n{}'.format(str(_('Volumes')), lvm_volumes)
|
||||
|
||||
return output
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ from ..disk import (
|
|||
DiskEncryption,
|
||||
EncryptionType
|
||||
)
|
||||
from ..menu import (
|
||||
Selector,
|
||||
AbstractSubMenu,
|
||||
MenuSelectionType,
|
||||
TableMenu
|
||||
)
|
||||
from ..interactions.utils import get_password
|
||||
from ..menu import Menu
|
||||
from ..general import secret
|
||||
from ..menu import AbstractSubMenu
|
||||
from .fido import Fido2Device, Fido2
|
||||
from ..output import FormattedOutput
|
||||
from ..utils.util import get_password
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType
|
||||
)
|
||||
from archinstall.lib.menu.menu_helper import MenuHelper
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -29,7 +29,6 @@ class DiskEncryptionMenu(AbstractSubMenu):
|
|||
def __init__(
|
||||
self,
|
||||
disk_config: DiskLayoutConfiguration,
|
||||
data_store: Dict[str, Any],
|
||||
preset: Optional[DiskEncryption] = None
|
||||
):
|
||||
if preset:
|
||||
|
|
@ -37,57 +36,56 @@ class DiskEncryptionMenu(AbstractSubMenu):
|
|||
else:
|
||||
self._preset = DiskEncryption()
|
||||
|
||||
self._data_store: Dict[str, Any] = {}
|
||||
self._disk_config = disk_config
|
||||
super().__init__(data_store=data_store)
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
self._menu_options['encryption_type'] = \
|
||||
Selector(
|
||||
_('Encryption type'),
|
||||
func=lambda preset: select_encryption_type(self._disk_config, preset),
|
||||
display_func=lambda x: EncryptionType.type_to_text(x) if x else None,
|
||||
default=self._preset.encryption_type,
|
||||
enabled=True,
|
||||
)
|
||||
self._menu_options['encryption_password'] = \
|
||||
Selector(
|
||||
_('Encryption password'),
|
||||
lambda x: select_encrypted_password(),
|
||||
menu_optioons = self._define_menu_options()
|
||||
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
|
||||
|
||||
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
|
||||
|
||||
def _define_menu_options(self) -> List[MenuItem]:
|
||||
return [
|
||||
MenuItem(
|
||||
text=str(_('Encryption type')),
|
||||
action=lambda x: select_encryption_type(self._disk_config, x),
|
||||
value=self._preset.encryption_type,
|
||||
preview_action=self._preview,
|
||||
key='encryption_type'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Encryption password')),
|
||||
action=lambda x: select_encrypted_password(),
|
||||
value=self._preset.encryption_password,
|
||||
dependencies=[self._check_dep_enc_type],
|
||||
display_func=lambda x: secret(x) if x else '',
|
||||
default=self._preset.encryption_password,
|
||||
enabled=True
|
||||
)
|
||||
self._menu_options['partitions'] = \
|
||||
Selector(
|
||||
_('Partitions'),
|
||||
func=lambda preset: select_partitions_to_encrypt(self._disk_config.device_modifications, preset),
|
||||
display_func=lambda x: f'{len(x)} {_("Partitions")}' if x else None,
|
||||
preview_action=self._preview,
|
||||
key='encryption_password'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Partitions')),
|
||||
action=lambda x: select_partitions_to_encrypt(self._disk_config.device_modifications, x),
|
||||
value=self._preset.partitions,
|
||||
dependencies=[self._check_dep_partitions],
|
||||
default=self._preset.partitions,
|
||||
preview_func=self._prev_partitions,
|
||||
enabled=True
|
||||
)
|
||||
self._menu_options['lvm_vols'] = \
|
||||
Selector(
|
||||
_('LVM volumes'),
|
||||
func=lambda preset: self._select_lvm_vols(preset),
|
||||
display_func=lambda x: f'{len(x)} {_("LVM volumes")}' if x else None,
|
||||
preview_action=self._preview,
|
||||
key='partitions'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('LVM volumes')),
|
||||
action=lambda x: self._select_lvm_vols(x),
|
||||
value=self._preset.lvm_volumes,
|
||||
dependencies=[self._check_dep_lvm_vols],
|
||||
default=self._preset.lvm_volumes,
|
||||
preview_func=self._prev_lvm_vols,
|
||||
enabled=True
|
||||
)
|
||||
self._menu_options['HSM'] = \
|
||||
Selector(
|
||||
description=_('Use HSM to unlock encrypted drive'),
|
||||
func=lambda preset: select_hsm(preset),
|
||||
display_func=lambda x: self._display_hsm(x),
|
||||
preview_func=self._prev_hsm,
|
||||
preview_action=self._preview,
|
||||
key='lvm_vols'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('HSM')),
|
||||
action=lambda x: select_hsm(x),
|
||||
value=self._preset.hsm_device,
|
||||
dependencies=[self._check_dep_enc_type],
|
||||
default=self._preset.hsm_device,
|
||||
enabled=True
|
||||
)
|
||||
preview_action=self._preview,
|
||||
key='HSM'
|
||||
),
|
||||
]
|
||||
|
||||
def _select_lvm_vols(self, preset: List[LvmVolume]) -> List[LvmVolume]:
|
||||
if self._disk_config.lvm_config:
|
||||
|
|
@ -95,30 +93,34 @@ class DiskEncryptionMenu(AbstractSubMenu):
|
|||
return []
|
||||
|
||||
def _check_dep_enc_type(self) -> bool:
|
||||
enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection
|
||||
enc_type: Optional[EncryptionType] = self._item_group.find_by_key('encryption_type').value
|
||||
if enc_type and enc_type != EncryptionType.NoEncryption:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_dep_partitions(self) -> bool:
|
||||
enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection
|
||||
enc_type: Optional[EncryptionType] = self._item_group.find_by_key('encryption_type').value
|
||||
if enc_type and enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_dep_lvm_vols(self) -> bool:
|
||||
enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection
|
||||
enc_type: Optional[EncryptionType] = self._item_group.find_by_key('encryption_type').value
|
||||
if enc_type and enc_type == EncryptionType.LuksOnLvm:
|
||||
return True
|
||||
return False
|
||||
|
||||
def run(self, allow_reset: bool = True) -> Optional[DiskEncryption]:
|
||||
super().run(allow_reset=allow_reset)
|
||||
def run(self) -> Optional[DiskEncryption]:
|
||||
super().run()
|
||||
|
||||
enc_type = self._data_store.get('encryption_type', None)
|
||||
enc_password = self._data_store.get('encryption_password', None)
|
||||
enc_partitions = self._data_store.get('partitions', None)
|
||||
enc_lvm_vols = self._data_store.get('lvm_vols', None)
|
||||
enc_type: Optional[EncryptionType] = self._item_group.find_by_key('encryption_type').value
|
||||
enc_password: Optional[str] = self._item_group.find_by_key('encryption_password').value
|
||||
enc_partitions = self._item_group.find_by_key('partitions').value
|
||||
enc_lvm_vols = self._item_group.find_by_key('lvm_vols').value
|
||||
|
||||
assert enc_type is not None
|
||||
assert enc_partitions is not None
|
||||
assert enc_lvm_vols is not None
|
||||
|
||||
if enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and enc_partitions:
|
||||
enc_lvm_vols = []
|
||||
|
|
@ -137,14 +139,50 @@ class DiskEncryptionMenu(AbstractSubMenu):
|
|||
|
||||
return None
|
||||
|
||||
def _display_hsm(self, device: Optional[Fido2Device]) -> Optional[str]:
|
||||
if device:
|
||||
return device.manufacturer
|
||||
def _preview(self, item: MenuItem) -> Optional[str]:
|
||||
output = ''
|
||||
|
||||
if (enc_type := self._prev_type()) is not None:
|
||||
output += enc_type
|
||||
|
||||
if (enc_pwd := self._prev_password()) is not None:
|
||||
output += f'\n{enc_pwd}'
|
||||
|
||||
if (fido_device := self._prev_hsm()) is not None:
|
||||
output += f'\n{fido_device}'
|
||||
|
||||
if (partitions := self._prev_partitions()) is not None:
|
||||
output += f'\n\n{partitions}'
|
||||
|
||||
if (lvm := self._prev_lvm_vols()) is not None:
|
||||
output += f'\n\n{lvm}'
|
||||
|
||||
if not output:
|
||||
return None
|
||||
|
||||
return output
|
||||
|
||||
def _prev_type(self) -> Optional[str]:
|
||||
enc_type = self._item_group.find_by_key('encryption_type').value
|
||||
|
||||
if enc_type:
|
||||
enc_text = EncryptionType.type_to_text(enc_type)
|
||||
return f'{str(_("Encryption type"))}: {enc_text}'
|
||||
|
||||
return None
|
||||
|
||||
def _prev_password(self) -> Optional[str]:
|
||||
enc_pwd = self._item_group.find_by_key('encryption_password').value
|
||||
|
||||
if enc_pwd:
|
||||
pwd_text = '*' * len(enc_pwd)
|
||||
return f'{str(_("Encryption password"))}: {pwd_text}'
|
||||
|
||||
return None
|
||||
|
||||
def _prev_partitions(self) -> Optional[str]:
|
||||
partitions: Optional[List[PartitionModification]] = self._menu_options['partitions'].current_selection
|
||||
partitions: Optional[List[PartitionModification]] = self._item_group.find_by_key('partitions').value
|
||||
|
||||
if partitions:
|
||||
output = str(_('Partitions to be encrypted')) + '\n'
|
||||
output += FormattedOutput.as_table(partitions)
|
||||
|
|
@ -153,7 +191,8 @@ class DiskEncryptionMenu(AbstractSubMenu):
|
|||
return None
|
||||
|
||||
def _prev_lvm_vols(self) -> Optional[str]:
|
||||
volumes: Optional[List[PartitionModification]] = self._menu_options['lvm_vols'].current_selection
|
||||
volumes: Optional[List[PartitionModification]] = self._item_group.find_by_key('lvm_vols').value
|
||||
|
||||
if volumes:
|
||||
output = str(_('LVM volumes to be encrypted')) + '\n'
|
||||
output += FormattedOutput.as_table(volumes)
|
||||
|
|
@ -162,51 +201,57 @@ class DiskEncryptionMenu(AbstractSubMenu):
|
|||
return None
|
||||
|
||||
def _prev_hsm(self) -> Optional[str]:
|
||||
try:
|
||||
Fido2.get_fido2_devices()
|
||||
except ValueError:
|
||||
return str(_('Unable to determine fido2 devices. Is libfido2 installed?'))
|
||||
fido_device: Optional[Fido2Device] = self._item_group.find_by_key('HSM').value
|
||||
|
||||
fido_device: Optional[Fido2Device] = self._menu_options['HSM'].current_selection
|
||||
if not fido_device:
|
||||
return None
|
||||
|
||||
if fido_device:
|
||||
output = '{}: {}'.format(str(_('Path')), fido_device.path)
|
||||
output += '{}: {}'.format(str(_('Manufacturer')), fido_device.manufacturer)
|
||||
output += '{}: {}'.format(str(_('Product')), fido_device.product)
|
||||
return output
|
||||
|
||||
return None
|
||||
output = str(fido_device.path)
|
||||
output += f' ({fido_device.manufacturer}, {fido_device.product})'
|
||||
return f'{str(_("HSM device"))}: {output}'
|
||||
|
||||
|
||||
def select_encryption_type(disk_config: DiskLayoutConfiguration, preset: EncryptionType) -> Optional[EncryptionType]:
|
||||
title = str(_('Select disk encryption option'))
|
||||
|
||||
if disk_config.lvm_config:
|
||||
options = [
|
||||
EncryptionType.type_to_text(EncryptionType.LvmOnLuks),
|
||||
EncryptionType.type_to_text(EncryptionType.LuksOnLvm)
|
||||
]
|
||||
else:
|
||||
options = [EncryptionType.type_to_text(EncryptionType.Luks)]
|
||||
|
||||
options: List[EncryptionType] = []
|
||||
preset_value = EncryptionType.type_to_text(preset)
|
||||
|
||||
choice = Menu(title, options, preset_values=preset_value).run()
|
||||
if disk_config.lvm_config:
|
||||
options = [EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm]
|
||||
else:
|
||||
options = [EncryptionType.Luks]
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Reset: return None
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection: return EncryptionType.text_to_type(choice.value) # type: ignore
|
||||
items = [MenuItem(EncryptionType.type_to_text(o), value=o) for o in options]
|
||||
group = MenuItemGroup(items)
|
||||
group.set_focus_by_value(preset_value)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Encryption type')))
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Reset: return None
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
|
||||
|
||||
def select_encrypted_password() -> Optional[str]:
|
||||
if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))):
|
||||
return passwd
|
||||
return None
|
||||
header = str(_('Enter disk encryption password (leave blank for no encryption)')) + '\n'
|
||||
password = get_password(
|
||||
text=str(_('Disk encryption password')),
|
||||
header=header,
|
||||
allow_skip=True
|
||||
)
|
||||
|
||||
return password
|
||||
|
||||
|
||||
def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]:
|
||||
title = _('Select a FIDO2 device to use for HSM')
|
||||
header = str(_('Select a FIDO2 device to use for HSM'))
|
||||
|
||||
try:
|
||||
fido_devices = Fido2.get_fido2_devices()
|
||||
|
|
@ -214,14 +259,20 @@ def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]:
|
|||
return None
|
||||
|
||||
if fido_devices:
|
||||
choice = TableMenu(title, data=fido_devices).run()
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Reset:
|
||||
return None
|
||||
case MenuSelectionType.Skip:
|
||||
return preset
|
||||
case MenuSelectionType.Selection:
|
||||
return choice.value # type: ignore
|
||||
group, table_header = MenuHelper.create_table(data=fido_devices)
|
||||
header = f'{header}\n\n{table_header}'
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Reset: return None
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -240,23 +291,22 @@ def select_partitions_to_encrypt(
|
|||
avail_partitions = list(filter(lambda x: not x.exists(), partitions))
|
||||
|
||||
if avail_partitions:
|
||||
title = str(_('Select which partitions to encrypt'))
|
||||
partition_table = FormattedOutput.as_table(avail_partitions)
|
||||
group, header = MenuHelper.create_table(data=avail_partitions)
|
||||
|
||||
choice = TableMenu(
|
||||
title,
|
||||
table_data=(avail_partitions, partition_table),
|
||||
preset=preset,
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
multi=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Reset:
|
||||
return []
|
||||
case MenuSelectionType.Skip:
|
||||
return preset
|
||||
case MenuSelectionType.Selection:
|
||||
return choice.multi_value
|
||||
match result.type_:
|
||||
case ResultType.Reset: return []
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Selection:
|
||||
partitions = result.get_values()
|
||||
return partitions
|
||||
|
||||
return []
|
||||
|
||||
|
||||
|
|
@ -267,22 +317,20 @@ def select_lvm_vols_to_encrypt(
|
|||
volumes: List[LvmVolume] = lvm_config.get_all_volumes()
|
||||
|
||||
if volumes:
|
||||
title = str(_('Select which LVM volumes to encrypt'))
|
||||
partition_table = FormattedOutput.as_table(volumes)
|
||||
group, header = MenuHelper.create_table(data=volumes)
|
||||
|
||||
choice = TableMenu(
|
||||
title,
|
||||
table_data=(volumes, partition_table),
|
||||
preset=preset,
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
multi=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Reset:
|
||||
return []
|
||||
case MenuSelectionType.Skip:
|
||||
return preset
|
||||
case MenuSelectionType.Selection:
|
||||
return choice.multi_value
|
||||
match result.type_:
|
||||
case ResultType.Reset: return []
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Selection:
|
||||
volumes = result.get_values()
|
||||
return volumes
|
||||
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, TYPE_CHECKING, List, Dict, Set
|
||||
|
||||
from ..interactions.general_conf import ask_abort
|
||||
from .device_handler import device_handler
|
||||
from .device_model import (
|
||||
DiskLayoutConfiguration, DiskLayoutType, PartitionTable,
|
||||
|
|
@ -15,8 +14,11 @@ from .device_model import (
|
|||
)
|
||||
from ..hardware import SysInfo
|
||||
from ..luks import Luks2
|
||||
from ..menu import Menu
|
||||
from ..output import debug, info
|
||||
from archinstall.tui import (
|
||||
Tui
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -44,12 +46,8 @@ class FilesystemHandler:
|
|||
|
||||
device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods])
|
||||
|
||||
# Issue a final warning before we continue with something un-revertable.
|
||||
# We mention the drive one last time, and count from 5 to 0.
|
||||
print(str(_(' ! Formatting {} in ')).format(device_paths))
|
||||
|
||||
if show_countdown:
|
||||
self._do_countdown()
|
||||
self._final_warning(device_paths)
|
||||
|
||||
# Setup the blockdevice, filesystem (and optionally encryption).
|
||||
# Once that's done, we'll hand over to perform_installation()
|
||||
|
|
@ -339,40 +337,19 @@ class FilesystemHandler:
|
|||
Size(256, Unit.MiB, SectorSize.default())
|
||||
)
|
||||
|
||||
def _do_countdown(self) -> bool:
|
||||
SIG_TRIGGER = False
|
||||
def _final_warning(self, device_paths: str) -> bool:
|
||||
# Issue a final warning before we continue with something un-revertable.
|
||||
# We mention the drive one last time, and count from 5 to 0.
|
||||
out = str(_(' ! Formatting {} in ')).format(device_paths)
|
||||
Tui.print(out, row=0, endl='', clear_screen=True)
|
||||
|
||||
def kill_handler(sig: int, frame: Any) -> None:
|
||||
print()
|
||||
exit(0)
|
||||
|
||||
def sig_handler(sig: int, frame: Any) -> None:
|
||||
signal.signal(signal.SIGINT, kill_handler)
|
||||
|
||||
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
||||
signal.signal(signal.SIGINT, sig_handler)
|
||||
|
||||
for i in range(5, 0, -1):
|
||||
print(f"{i}", end='')
|
||||
|
||||
for x in range(4):
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
countdown = '\n5...4...3...2...1'
|
||||
for c in countdown:
|
||||
Tui.print(c, row=0, endl='')
|
||||
time.sleep(0.25)
|
||||
print(".", end='')
|
||||
|
||||
if SIG_TRIGGER:
|
||||
prompt = _('Do you really want to abort?')
|
||||
choice = Menu(prompt, Menu.yes_no(), skip=False).run()
|
||||
if choice.value == Menu.yes():
|
||||
exit(0)
|
||||
|
||||
if SIG_TRIGGER is False:
|
||||
sys.stdin.read()
|
||||
|
||||
SIG_TRIGGER = False
|
||||
signal.signal(signal.SIGINT, sig_handler)
|
||||
|
||||
print()
|
||||
signal.signal(signal.SIGINT, original_sigint_handler)
|
||||
except KeyboardInterrupt:
|
||||
with Tui():
|
||||
ask_abort()
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -5,16 +5,23 @@ from pathlib import Path
|
|||
from typing import Any, TYPE_CHECKING, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..utils.util import prompt_dir
|
||||
from .device_model import (
|
||||
PartitionModification, FilesystemType, BDevice,
|
||||
Size, Unit, PartitionType, PartitionFlag,
|
||||
ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption
|
||||
)
|
||||
from ..hardware import SysInfo
|
||||
from ..menu import Menu, ListManager, MenuSelection, TextInput
|
||||
from ..output import FormattedOutput, warn
|
||||
from ..menu import ListManager
|
||||
from ..output import FormattedOutput
|
||||
from .subvolume_menu import SubvolumeMenu
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, EditMenu,
|
||||
Orientation, ResultType
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
|
@ -26,9 +33,6 @@ class DefaultFreeSector:
|
|||
|
||||
|
||||
class PartitioningList(ListManager):
|
||||
"""
|
||||
subclass of ListManager for the managing of user accounts
|
||||
"""
|
||||
def __init__(self, prompt: str, device: BDevice, device_partitions: List[PartitionModification]):
|
||||
self._device = device
|
||||
self._actions = {
|
||||
|
|
@ -49,7 +53,10 @@ class PartitioningList(ListManager):
|
|||
super().__init__(prompt, device_partitions, display_actions[:2], display_actions[3:])
|
||||
|
||||
def selected_action_display(self, partition: PartitionModification) -> str:
|
||||
return str(_('Partition'))
|
||||
if partition.status == ModificationStatus.Create:
|
||||
return str(_('Partition - New'))
|
||||
else:
|
||||
return str(partition.dev_path)
|
||||
|
||||
def filter_options(self, selection: PartitionModification, options: List[str]) -> List[str]:
|
||||
not_filter = []
|
||||
|
|
@ -100,8 +107,7 @@ class PartitioningList(ListManager):
|
|||
if len(new_partitions) > 0:
|
||||
data = new_partitions
|
||||
case 'remove_added_partitions':
|
||||
choice = self._reset_confirmation()
|
||||
if choice.value == Menu.yes():
|
||||
if self._reset_confirmation():
|
||||
data = [part for part in data if part.is_exists_or_modify()]
|
||||
case 'assign_mountpoint' if entry:
|
||||
entry.mountpoint = self._prompt_mountpoint()
|
||||
|
|
@ -169,7 +175,7 @@ class PartitioningList(ListManager):
|
|||
|
||||
def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None:
|
||||
partition.btrfs_subvols = SubvolumeMenu(
|
||||
_("Manage btrfs subvolumes for current partition"),
|
||||
str(_("Manage btrfs subvolumes for current partition")),
|
||||
partition.btrfs_subvols
|
||||
).run()
|
||||
|
||||
|
|
@ -185,7 +191,7 @@ class PartitioningList(ListManager):
|
|||
# without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set,
|
||||
# it's safe to change the filesystem for this partition.
|
||||
if partition.fs_type == FilesystemType.Crypto_luks:
|
||||
prompt = str(_('This partition is currently encrypted, to format it a filesystem has to be specified'))
|
||||
prompt = str(_('This partition is currently encrypted, to format it a filesystem has to be specified')) + '\n'
|
||||
fs_type = self._prompt_partition_fs_type(prompt)
|
||||
partition.fs_type = fs_type
|
||||
|
||||
|
|
@ -195,25 +201,31 @@ class PartitioningList(ListManager):
|
|||
def _prompt_mountpoint(self) -> Path:
|
||||
header = str(_('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.')) + '\n'
|
||||
header += str(_('If mountpoint /boot is set, then the partition will also be marked as bootable.')) + '\n'
|
||||
prompt = str(_('Mountpoint: '))
|
||||
prompt = str(_('Mountpoint'))
|
||||
|
||||
print(header)
|
||||
|
||||
while True:
|
||||
value = TextInput(prompt).run().strip()
|
||||
|
||||
if value:
|
||||
mountpoint = Path(value)
|
||||
break
|
||||
mountpoint = prompt_dir(prompt, header, allow_skip=False)
|
||||
assert mountpoint
|
||||
|
||||
return mountpoint
|
||||
|
||||
def _prompt_partition_fs_type(self, prompt: str = '') -> FilesystemType:
|
||||
options = {fs.value: fs for fs in FilesystemType if fs != FilesystemType.Crypto_luks}
|
||||
def _prompt_partition_fs_type(self, prompt: Optional[str] = None) -> FilesystemType:
|
||||
fs_types = filter(lambda fs: fs != FilesystemType.Crypto_luks, FilesystemType)
|
||||
items = [MenuItem(fs.value, value=fs) for fs in fs_types]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
|
||||
prompt = prompt + '\n' + str(_('Enter a desired filesystem type for the partition'))
|
||||
choice = Menu(prompt, options, sort=False, skip=False).run()
|
||||
return options[choice.single_value]
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Filesystem'))),
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
def _validate_value(
|
||||
self,
|
||||
|
|
@ -246,22 +258,34 @@ class PartitioningList(ListManager):
|
|||
self,
|
||||
sector_size: SectorSize,
|
||||
total_size: Size,
|
||||
prompt: str,
|
||||
text: str,
|
||||
header: str,
|
||||
default: Size,
|
||||
start: Optional[Size],
|
||||
) -> Size:
|
||||
while True:
|
||||
value = TextInput(prompt).run().strip()
|
||||
size: Optional[Size] = None
|
||||
if not value:
|
||||
size = default
|
||||
else:
|
||||
size = self._validate_value(sector_size, total_size, value, start)
|
||||
def validate(value: str) -> Optional[str]:
|
||||
size = self._validate_value(sector_size, total_size, value, start)
|
||||
if not size:
|
||||
return str(_('Invalid size'))
|
||||
return None
|
||||
|
||||
if size:
|
||||
return size
|
||||
result = EditMenu(
|
||||
text,
|
||||
header=f'{header}\b',
|
||||
allow_skip=True,
|
||||
validator=validate
|
||||
).input()
|
||||
|
||||
warn(f'Invalid value: {value}')
|
||||
size: Optional[Size] = None
|
||||
value = result.text()
|
||||
|
||||
if value is None:
|
||||
size = default
|
||||
else:
|
||||
size = self._validate_value(sector_size, total_size, value, start)
|
||||
|
||||
assert size
|
||||
return size
|
||||
|
||||
def _prompt_size(self) -> Tuple[Size, Size]:
|
||||
device_info = self._device.device_info
|
||||
|
|
@ -276,7 +300,6 @@ class PartitioningList(ListManager):
|
|||
prompt += str(_('Total: {} / {}')).format(total_sectors, total_bytes) + '\n\n'
|
||||
prompt += str(_('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...')) + '\n'
|
||||
prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n'
|
||||
print(prompt)
|
||||
|
||||
default_free_sector = self._find_default_free_space()
|
||||
|
||||
|
|
@ -287,27 +310,32 @@ class PartitioningList(ListManager):
|
|||
)
|
||||
|
||||
# prompt until a valid start sector was entered
|
||||
start_prompt = str(_('Enter start (default: sector {}): ')).format(default_free_sector.start.value)
|
||||
start_text = str(_('Start (default: sector {}): ')).format(default_free_sector.start.value)
|
||||
|
||||
start_size = self._enter_size(
|
||||
device_info.sector_size,
|
||||
device_info.total_size,
|
||||
start_prompt,
|
||||
start_text,
|
||||
prompt,
|
||||
default_free_sector.start,
|
||||
None
|
||||
)
|
||||
|
||||
prompt += f'\nStart: {start_size.as_text()}\n'
|
||||
|
||||
if start_size.value == default_free_sector.start.value and default_free_sector.end.value != 0:
|
||||
end_size = default_free_sector.end
|
||||
else:
|
||||
end_size = device_info.total_size
|
||||
|
||||
# prompt until valid end sector was entered
|
||||
end_prompt = str(_('Enter end (default: {}): ')).format(end_size.as_text())
|
||||
end_text = str(_('End (default: {}): ')).format(end_size.as_text())
|
||||
|
||||
end_size = self._enter_size(
|
||||
device_info.sector_size,
|
||||
device_info.total_size,
|
||||
end_prompt,
|
||||
end_text,
|
||||
prompt,
|
||||
end_size,
|
||||
start_size
|
||||
)
|
||||
|
|
@ -358,9 +386,6 @@ class PartitioningList(ListManager):
|
|||
start_size, end_size = self._prompt_size()
|
||||
length = end_size - start_size
|
||||
|
||||
# new line for the next prompt
|
||||
print()
|
||||
|
||||
mountpoint = None
|
||||
if fs_type != FilesystemType.Btrfs:
|
||||
mountpoint = self._prompt_mountpoint()
|
||||
|
|
@ -381,17 +406,26 @@ class PartitioningList(ListManager):
|
|||
|
||||
return partition
|
||||
|
||||
def _reset_confirmation(self) -> MenuSelection:
|
||||
prompt = str(_('This will remove all newly added partitions, continue?'))
|
||||
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run()
|
||||
return choice
|
||||
def _reset_confirmation(self) -> bool:
|
||||
prompt = str(_('This will remove all newly added partitions, continue?')) + '\n'
|
||||
|
||||
result = SelectMenu(
|
||||
MenuItemGroup.yes_no(),
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
columns=2,
|
||||
reset_warning_msg=prompt,
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
return result.item() == MenuItem.yes()
|
||||
|
||||
def _suggest_partition_layout(self, data: List[PartitionModification]) -> List[PartitionModification]:
|
||||
# if modifications have been done already, inform the user
|
||||
# that this operation will erase those modifications
|
||||
if any([not entry.exists() for entry in data]):
|
||||
choice = self._reset_confirmation()
|
||||
if choice.value == Menu.no():
|
||||
if not self._reset_confirmation():
|
||||
return []
|
||||
|
||||
from ..interactions.disk_conf import suggest_single_disk_layout
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ from pathlib import Path
|
|||
from typing import List, Optional, Any, TYPE_CHECKING
|
||||
|
||||
from .device_model import SubvolumeModification
|
||||
from ..menu import TextInput, ListManager
|
||||
from ..menu import ListManager
|
||||
from ..utils.util import prompt_dir
|
||||
|
||||
from archinstall.tui import (
|
||||
Alignment, EditMenu, ResultType
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -20,18 +25,34 @@ class SubvolumeMenu(ListManager):
|
|||
def selected_action_display(self, subvolume: SubvolumeModification) -> str:
|
||||
return str(subvolume.name)
|
||||
|
||||
def _add_subvolume(self, editing: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]:
|
||||
name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run()
|
||||
def _add_subvolume(self, preset: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]:
|
||||
result = EditMenu(
|
||||
str(_('Subvolume name')),
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
default_text=str(preset.name) if preset else None
|
||||
).input()
|
||||
|
||||
if not name:
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
name = result.text()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
header = f"{str(_('Subvolume name'))}: {name}\n"
|
||||
|
||||
path = prompt_dir(
|
||||
str(_("Subvolume mountpoint")),
|
||||
header=header,
|
||||
allow_skip=True
|
||||
)
|
||||
|
||||
if not path:
|
||||
return None
|
||||
|
||||
mountpoint = TextInput(f'{_("Subvolume mountpoint")}: ', str(editing.mountpoint) if editing else '').run()
|
||||
|
||||
if not mountpoint:
|
||||
return None
|
||||
|
||||
return SubvolumeModification(Path(name), Path(mountpoint))
|
||||
return SubvolumeModification(Path(name), path)
|
||||
|
||||
def handle_action(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -6,29 +6,32 @@ from . import disk
|
|||
from .general import secret
|
||||
from .hardware import SysInfo
|
||||
from .locale.locale_menu import LocaleConfiguration, LocaleMenu
|
||||
from .menu import Selector, AbstractMenu
|
||||
from .menu import AbstractMenu
|
||||
from .mirrors import MirrorConfiguration, MirrorMenu
|
||||
from .models import NetworkConfiguration, NicType
|
||||
from .models.bootloader import Bootloader
|
||||
from .models.audio_configuration import Audio, AudioConfiguration
|
||||
from .models.audio_configuration import AudioConfiguration
|
||||
from .models.users import User
|
||||
from .output import FormattedOutput
|
||||
from .profile.profile_menu import ProfileConfiguration
|
||||
from .configuration import save_config
|
||||
from .interactions import add_number_of_parallel_downloads
|
||||
from .interactions import ask_additional_packages_to_install
|
||||
from .interactions import ask_for_additional_users
|
||||
from .interactions import ask_for_audio_selection
|
||||
from .interactions import ask_for_bootloader
|
||||
from .interactions import ask_for_uki
|
||||
from .interactions import ask_for_swap
|
||||
from .interactions import ask_hostname
|
||||
from .interactions import ask_to_configure_network
|
||||
from .interactions import get_password, ask_for_a_timezone
|
||||
from .interactions import select_additional_repositories
|
||||
from .interactions import select_kernel
|
||||
from .interactions import (
|
||||
ask_for_audio_selection, ask_for_swap,
|
||||
ask_for_bootloader, ask_for_uki, ask_hostname,
|
||||
add_number_of_parallel_downloads, select_kernel,
|
||||
ask_additional_packages_to_install, select_additional_repositories,
|
||||
ask_for_a_timezone, ask_ntp, ask_to_configure_network
|
||||
)
|
||||
from .utils.util import get_password
|
||||
from .utils.util import format_cols
|
||||
from .interactions import ask_ntp
|
||||
from .configuration import save_config
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem
|
||||
)
|
||||
|
||||
|
||||
from .translationhandler import Language, TranslationHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -36,175 +39,211 @@ if TYPE_CHECKING:
|
|||
|
||||
class GlobalMenu(AbstractMenu):
|
||||
def __init__(self, data_store: Dict[str, Any]):
|
||||
super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3)
|
||||
self._data_store = data_store
|
||||
self._translation_handler = TranslationHandler()
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
# archinstall.Language will not use preset values
|
||||
self._menu_options['archinstall-language'] = \
|
||||
Selector(
|
||||
_('Archinstall language'),
|
||||
lambda x: self._select_archinstall_language(x),
|
||||
display_func=lambda x: x.display_name,
|
||||
default=self.translation_handler.get_language_by_abbr('en'))
|
||||
self._menu_options['locale_config'] = \
|
||||
Selector(
|
||||
_('Locales'),
|
||||
lambda preset: self._locale_selection(preset),
|
||||
preview_func=self._prev_locale,
|
||||
display_func=lambda x: self.defined_text if x else '')
|
||||
self._menu_options['mirror_config'] = \
|
||||
Selector(
|
||||
_('Mirrors'),
|
||||
lambda preset: self._mirror_configuration(preset),
|
||||
display_func=lambda x: self.defined_text if x else '',
|
||||
preview_func=self._prev_mirror_config
|
||||
)
|
||||
self._menu_options['disk_config'] = \
|
||||
Selector(
|
||||
_('Disk configuration'),
|
||||
lambda preset: self._select_disk_config(preset),
|
||||
preview_func=self._prev_disk_config,
|
||||
display_func=lambda x: self.defined_text if x else '',
|
||||
)
|
||||
self._menu_options['disk_encryption'] = \
|
||||
Selector(
|
||||
_('Disk encryption'),
|
||||
lambda preset: self._disk_encryption(preset),
|
||||
preview_func=self._prev_disk_encryption,
|
||||
display_func=lambda x: self._display_disk_encryption(x),
|
||||
if 'archinstall-language' not in data_store:
|
||||
data_store['archinstall-language'] = self._translation_handler.get_language_by_abbr('en')
|
||||
|
||||
menu_optioons = self._get_menu_options(data_store)
|
||||
self._item_group = MenuItemGroup(
|
||||
menu_optioons,
|
||||
sort_items=False,
|
||||
checkmarks=True
|
||||
)
|
||||
|
||||
super().__init__(self._item_group, data_store)
|
||||
|
||||
def _get_menu_options(self, data_store: Dict[str, Any]) -> List[MenuItem]:
|
||||
return [
|
||||
MenuItem(
|
||||
text=str(_('Archinstall language')),
|
||||
action=lambda x: self._select_archinstall_language(x),
|
||||
display_action=lambda x: x.display_name if x else '',
|
||||
key='archinstall-language'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Locales')),
|
||||
action=lambda x: self._locale_selection(x),
|
||||
preview_action=self._prev_locale,
|
||||
key='locale_config'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Mirrors')),
|
||||
action=lambda x: self._mirror_configuration(x),
|
||||
preview_action=self._prev_mirror_config,
|
||||
key='mirror_config'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Disk configuration')),
|
||||
action=lambda x: self._select_disk_config(x),
|
||||
preview_action=self._prev_disk_config,
|
||||
mandatory=True,
|
||||
key='disk_config'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Disk encryption')),
|
||||
action=lambda x: self._disk_encryption(x),
|
||||
preview_action=self._prev_disk_encryption,
|
||||
key='disk_encryption',
|
||||
dependencies=['disk_config']
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Swap')),
|
||||
value=True,
|
||||
action=lambda x: ask_for_swap(x),
|
||||
preview_action=self._prev_swap,
|
||||
key='swap',
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Bootloader')),
|
||||
value=Bootloader.get_default(),
|
||||
action=lambda x: self._select_bootloader(x),
|
||||
preview_action=self._prev_bootloader,
|
||||
mandatory=True,
|
||||
key='bootloader',
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Unified kernel images')),
|
||||
value=False,
|
||||
action=lambda x: ask_for_uki(x),
|
||||
preview_action=self._prev_uki,
|
||||
key='uki',
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Hostname')),
|
||||
value='archlinux',
|
||||
action=lambda x: ask_hostname(x),
|
||||
preview_action=self._prev_hostname,
|
||||
key='hostname',
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Root password')),
|
||||
action=lambda x: self._set_root_password(x),
|
||||
preview_action=self._prev_root_pwd,
|
||||
key='!root-password',
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('User account')),
|
||||
action=lambda x: self._create_user_account(x),
|
||||
preview_action=self._prev_users,
|
||||
key='!users'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Profile')),
|
||||
action=lambda x: self._select_profile(x),
|
||||
preview_action=self._prev_profile,
|
||||
key='profile_config'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Audio')),
|
||||
action=lambda x: ask_for_audio_selection(x),
|
||||
preview_action=self._prev_audio,
|
||||
key='audio_config'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Kernels')),
|
||||
value=['linux'],
|
||||
action=lambda x: select_kernel(x),
|
||||
preview_action=self._prev_kernel,
|
||||
mandatory=True,
|
||||
key='kernels'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Network configuration')),
|
||||
action=lambda x: ask_to_configure_network(x),
|
||||
value={},
|
||||
preview_action=self._prev_network_config,
|
||||
key='network_config'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Parallel Downloads')),
|
||||
action=lambda x: add_number_of_parallel_downloads(x),
|
||||
value=0,
|
||||
preview_action=self._prev_parallel_dw,
|
||||
key='parallel downloads'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Additional packages')),
|
||||
action=lambda x: ask_additional_packages_to_install(x),
|
||||
value=[],
|
||||
preview_action=self._prev_additional_pkgs,
|
||||
key='packages'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Optional repositories')),
|
||||
action=lambda x: select_additional_repositories(x),
|
||||
value=[],
|
||||
preview_action=self._prev_additional_repos,
|
||||
key='additional-repositories'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Timezone')),
|
||||
action=lambda x: ask_for_a_timezone(x),
|
||||
value='UTC',
|
||||
preview_action=self._prev_tz,
|
||||
key='timezone'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Automatic time sync (NTP)')),
|
||||
action=lambda x: ask_ntp(x),
|
||||
value=True,
|
||||
preview_action=self._prev_ntp,
|
||||
key='ntp'
|
||||
),
|
||||
MenuItem(
|
||||
text=''
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Save configuration')),
|
||||
action=lambda x: self._safe_config(),
|
||||
key='save_config'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Install')),
|
||||
preview_action=self._prev_install_invalid_config,
|
||||
key='install'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Abort')),
|
||||
action=lambda x: exit(1),
|
||||
key='abort'
|
||||
)
|
||||
self._menu_options['swap'] = \
|
||||
Selector(
|
||||
_('Swap'),
|
||||
lambda preset: ask_for_swap(preset),
|
||||
default=True)
|
||||
self._menu_options['bootloader'] = \
|
||||
Selector(
|
||||
_('Bootloader'),
|
||||
lambda preset: ask_for_bootloader(preset),
|
||||
display_func=lambda x: x.value,
|
||||
default=Bootloader.get_default())
|
||||
self._menu_options['uki'] = \
|
||||
Selector(
|
||||
_('Unified kernel images'),
|
||||
lambda preset: ask_for_uki(preset),
|
||||
default=False)
|
||||
self._menu_options['hostname'] = \
|
||||
Selector(
|
||||
_('Hostname'),
|
||||
lambda preset: ask_hostname(preset),
|
||||
default='archlinux')
|
||||
# root password won't have preset value
|
||||
self._menu_options['!root-password'] = \
|
||||
Selector(
|
||||
_('Root password'),
|
||||
lambda preset: self._set_root_password(),
|
||||
display_func=lambda x: secret(x) if x else '')
|
||||
self._menu_options['!users'] = \
|
||||
Selector(
|
||||
_('User account'),
|
||||
lambda x: self._create_user_account(x),
|
||||
default=[],
|
||||
display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else '',
|
||||
preview_func=self._prev_users)
|
||||
self._menu_options['profile_config'] = \
|
||||
Selector(
|
||||
_('Profile'),
|
||||
lambda preset: self._select_profile(preset),
|
||||
display_func=lambda x: x.profile.name if x else '',
|
||||
preview_func=self._prev_profile
|
||||
)
|
||||
self._menu_options['audio_config'] = \
|
||||
Selector(
|
||||
_('Audio'),
|
||||
lambda preset: self._select_audio(preset),
|
||||
display_func=lambda x: self._display_audio(x)
|
||||
)
|
||||
self._menu_options['parallel downloads'] = \
|
||||
Selector(
|
||||
_('Parallel Downloads'),
|
||||
lambda preset: add_number_of_parallel_downloads(preset),
|
||||
display_func=lambda x: x if x else '0',
|
||||
default=0
|
||||
)
|
||||
self._menu_options['kernels'] = \
|
||||
Selector(
|
||||
_('Kernels'),
|
||||
lambda preset: select_kernel(preset),
|
||||
display_func=lambda x: ', '.join(x) if x else None,
|
||||
default=['linux'])
|
||||
self._menu_options['packages'] = \
|
||||
Selector(
|
||||
_('Additional packages'),
|
||||
lambda preset: ask_additional_packages_to_install(preset),
|
||||
display_func=lambda x: self.defined_text if x else '',
|
||||
preview_func=self._prev_additional_pkgs,
|
||||
default=[])
|
||||
self._menu_options['additional-repositories'] = \
|
||||
Selector(
|
||||
_('Optional repositories'),
|
||||
lambda preset: select_additional_repositories(preset),
|
||||
display_func=lambda x: ', '.join(x) if x else None,
|
||||
default=[])
|
||||
self._menu_options['network_config'] = \
|
||||
Selector(
|
||||
_('Network configuration'),
|
||||
lambda preset: ask_to_configure_network(preset),
|
||||
display_func=lambda x: self._display_network_conf(x),
|
||||
preview_func=self._prev_network_config,
|
||||
default={})
|
||||
self._menu_options['timezone'] = \
|
||||
Selector(
|
||||
_('Timezone'),
|
||||
lambda preset: ask_for_a_timezone(preset),
|
||||
default='UTC')
|
||||
self._menu_options['ntp'] = \
|
||||
Selector(
|
||||
_('Automatic time sync (NTP)'),
|
||||
lambda preset: ask_ntp(preset),
|
||||
default=True)
|
||||
self._menu_options['__separator__'] = \
|
||||
Selector('')
|
||||
self._menu_options['save_config'] = \
|
||||
Selector(
|
||||
_('Save configuration'),
|
||||
lambda preset: save_config(self._data_store),
|
||||
no_store=True)
|
||||
self._menu_options['install'] = \
|
||||
Selector(
|
||||
self._install_text(),
|
||||
exec_func=lambda n, v: self._is_config_valid(),
|
||||
preview_func=self._prev_install_invalid_config,
|
||||
no_store=True)
|
||||
]
|
||||
|
||||
self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n, v: exit(1))
|
||||
def _safe_config(self) -> None:
|
||||
data: Dict[str, Any] = {}
|
||||
for item in self._item_group.items:
|
||||
if item.key is not None:
|
||||
data[item.key] = item.value
|
||||
|
||||
save_config(data)
|
||||
|
||||
def _missing_configs(self) -> List[str]:
|
||||
def check(s: str) -> bool:
|
||||
obj = self._menu_options.get(s)
|
||||
if obj and obj.has_selection():
|
||||
return True
|
||||
return False
|
||||
def check(s) -> bool:
|
||||
item = self._item_group.find_by_key(s)
|
||||
return item.has_value()
|
||||
|
||||
def has_superuser() -> bool:
|
||||
sel = self._menu_options['!users']
|
||||
if sel.current_selection:
|
||||
return any([u.sudo for u in sel.current_selection])
|
||||
item = self._item_group.find_by_key('!users')
|
||||
|
||||
if item.has_value():
|
||||
users = item.value
|
||||
if users:
|
||||
return any([u.sudo for u in users])
|
||||
return False
|
||||
|
||||
mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items()))
|
||||
missing = set()
|
||||
|
||||
for key, selector in mandatory_fields.items():
|
||||
if key in ['!root-password', '!users']:
|
||||
for item in self._item_group.items:
|
||||
if item.key in ['!root-password', '!users']:
|
||||
if not check('!root-password') and not has_superuser():
|
||||
missing.add(
|
||||
str(_('Either root-password or at least 1 user with sudo privileges must be specified'))
|
||||
)
|
||||
elif key == 'disk_config':
|
||||
if not check('disk_config'):
|
||||
missing.add(self._menu_options['disk_config'].description)
|
||||
elif item.mandatory:
|
||||
if not check(item.key):
|
||||
missing.add(item.text)
|
||||
|
||||
return list(missing)
|
||||
|
||||
|
|
@ -216,36 +255,28 @@ class GlobalMenu(AbstractMenu):
|
|||
return False
|
||||
return self._validate_bootloader() is None
|
||||
|
||||
def _update_uki_display(self, name: Optional[str] = None) -> None:
|
||||
if bootloader := self._menu_options['bootloader'].current_selection:
|
||||
if not SysInfo.has_uefi() or not bootloader.has_uki_support():
|
||||
self._menu_options['uki'].set_current_selection(False)
|
||||
self._menu_options['uki'].set_enabled(False)
|
||||
elif name and name == 'bootloader':
|
||||
self._menu_options['uki'].set_enabled(True)
|
||||
def _select_archinstall_language(self, preset: Language) -> Language:
|
||||
from .interactions.general_conf import select_archinstall_language
|
||||
language = select_archinstall_language(self._translation_handler.translated_languages, preset)
|
||||
self._translation_handler.activate(language)
|
||||
|
||||
def _update_install_text(self, name: Optional[str] = None, value: Any = None) -> None:
|
||||
text = self._install_text()
|
||||
self._menu_options['install'].update_description(text)
|
||||
self._upate_lang_text()
|
||||
|
||||
def post_callback(self, name: Optional[str] = None, value: Any = None) -> None:
|
||||
self._update_uki_display(name)
|
||||
self._update_install_text(name, value)
|
||||
return language
|
||||
|
||||
def _install_text(self) -> str:
|
||||
missing = len(self._missing_configs())
|
||||
if missing > 0:
|
||||
return _('Install ({} config(s) missing)').format(missing)
|
||||
return _('Install')
|
||||
def _upate_lang_text(self) -> None:
|
||||
"""
|
||||
The options for the global menu are generated with a static text;
|
||||
each entry of the menu needs to be updated with the new translation
|
||||
"""
|
||||
new_options = self._get_menu_options(self._data_store)
|
||||
|
||||
def _display_network_conf(self, config: Optional[NetworkConfiguration]) -> str:
|
||||
if not config:
|
||||
return str(_('Not configured, unavailable unless setup manually'))
|
||||
|
||||
return config.type.display_msg()
|
||||
for o in new_options:
|
||||
if o.key is not None:
|
||||
self._item_group.find_by_key(o.key).text = o.text
|
||||
|
||||
def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]:
|
||||
disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
|
||||
disk_config: Optional[disk.DiskLayoutConfiguration] = self._item_group.find_by_key('disk_config').value
|
||||
|
||||
if not disk_config:
|
||||
# this should not happen as the encryption menu has the disk_config as dependency
|
||||
|
|
@ -254,91 +285,140 @@ class GlobalMenu(AbstractMenu):
|
|||
if not disk.DiskEncryption.validate_enc(disk_config):
|
||||
return None
|
||||
|
||||
data_store: Dict[str, Any] = {}
|
||||
disk_encryption = disk.DiskEncryptionMenu(disk_config, data_store, preset=preset).run()
|
||||
disk_encryption = disk.DiskEncryptionMenu(disk_config, preset=preset).run()
|
||||
return disk_encryption
|
||||
|
||||
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
|
||||
data_store: Dict[str, Any] = {}
|
||||
locale_config = LocaleMenu(data_store, preset).run()
|
||||
locale_config = LocaleMenu(preset).run()
|
||||
return locale_config
|
||||
|
||||
def _prev_locale(self) -> Optional[str]:
|
||||
selector = self._menu_options['locale_config']
|
||||
if selector.has_selection():
|
||||
config: LocaleConfiguration = selector.current_selection # type: ignore
|
||||
output = '{}: {}\n'.format(str(_('Keyboard layout')), config.kb_layout)
|
||||
output += '{}: {}\n'.format(str(_('Locale language')), config.sys_lang)
|
||||
output += '{}: {}'.format(str(_('Locale encoding')), config.sys_enc)
|
||||
def _prev_locale(self, item: MenuItem) -> Optional[str]:
|
||||
if not item.value:
|
||||
return None
|
||||
|
||||
config: LocaleConfiguration = item.value
|
||||
return config.preview()
|
||||
|
||||
def _prev_network_config(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value:
|
||||
network_config: NetworkConfiguration = item.value
|
||||
if network_config.type == NicType.MANUAL:
|
||||
output = FormattedOutput.as_table(network_config.nics)
|
||||
else:
|
||||
output = f'{str(_('Network configuration'))}:\n{network_config.type.display_msg()}'
|
||||
|
||||
return output
|
||||
return None
|
||||
|
||||
def _prev_network_config(self) -> Optional[str]:
|
||||
selector: Optional[NetworkConfiguration] = self._menu_options['network_config'].current_selection
|
||||
if selector:
|
||||
if selector.type == NicType.MANUAL:
|
||||
output = FormattedOutput.as_table(selector.nics)
|
||||
return output
|
||||
def _prev_additional_pkgs(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value:
|
||||
return format_cols(item.value, None)
|
||||
return None
|
||||
|
||||
def _prev_additional_pkgs(self) -> Optional[str]:
|
||||
selector = self._menu_options['packages']
|
||||
if selector.current_selection:
|
||||
packages: List[str] = selector.current_selection
|
||||
return format_cols(packages, None)
|
||||
def _prev_additional_repos(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value:
|
||||
repos = ', '.join(item.value)
|
||||
return f'{str(_("Additional repositories"))}: {repos}'
|
||||
return None
|
||||
|
||||
def _prev_disk_config(self) -> Optional[str]:
|
||||
selector = self._menu_options['disk_config']
|
||||
disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection
|
||||
def _prev_tz(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value:
|
||||
return f'{str(_("Timezone"))}: {item.value}'
|
||||
return None
|
||||
|
||||
def _prev_ntp(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value is not None:
|
||||
output = f'{str(_("NTP"))}: '
|
||||
output += str(_('Enabled')) if item.value else str(_('Disabled'))
|
||||
return output
|
||||
return None
|
||||
|
||||
def _prev_disk_config(self, item: MenuItem) -> Optional[str]:
|
||||
disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = item.value
|
||||
|
||||
output = ''
|
||||
if disk_layout_conf:
|
||||
output += str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg())
|
||||
output = str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) + '\n'
|
||||
|
||||
if disk_layout_conf.config_type == disk.DiskLayoutType.Pre_mount:
|
||||
output += str(_('Mountpoint')) + ': ' + str(disk_layout_conf.mountpoint)
|
||||
|
||||
if disk_layout_conf.lvm_config:
|
||||
output += '\n{}: {}'.format(str(_('LVM configuration type')), disk_layout_conf.lvm_config.config_type.display_msg())
|
||||
output += '{}: {}'.format(str(_('LVM configuration type')), disk_layout_conf.lvm_config.config_type.display_msg())
|
||||
|
||||
if output:
|
||||
return output
|
||||
|
||||
return None
|
||||
|
||||
def _display_disk_config(self, current_value: Optional[disk.DiskLayoutConfiguration] = None) -> str:
|
||||
if current_value:
|
||||
return current_value.config_type.display_msg()
|
||||
return ''
|
||||
def _prev_swap(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value is not None:
|
||||
output = f'{str(_("Swap on zram"))}: '
|
||||
output += str(_('Enabled')) if item.value else str(_('Disabled'))
|
||||
return output
|
||||
return None
|
||||
|
||||
def _prev_disk_encryption(self) -> Optional[str]:
|
||||
disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection
|
||||
def _prev_uki(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value is not None:
|
||||
output = f'{str(_('Unified kernel images'))}: '
|
||||
output += str(_('Enabled')) if item.value else str(_('Disabled'))
|
||||
return output
|
||||
return None
|
||||
|
||||
def _prev_hostname(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value is not None:
|
||||
return f'{str(_("Hostname"))}: {item.value}'
|
||||
return None
|
||||
|
||||
def _prev_root_pwd(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value is not None:
|
||||
return f'{str(_("Root password"))}: {secret(item.value)}'
|
||||
return None
|
||||
|
||||
def _prev_audio(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value is not None:
|
||||
config: AudioConfiguration = item.value
|
||||
return f'{str(_("Audio"))}: {config.audio.value}'
|
||||
return None
|
||||
|
||||
def _prev_parallel_dw(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value is not None:
|
||||
return f'{str(_("Parallel Downloads"))}: {item.value}'
|
||||
return None
|
||||
|
||||
def _prev_kernel(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value:
|
||||
kernel = ', '.join(item.value)
|
||||
return f'{str(_("Kernel"))}: {kernel}'
|
||||
return None
|
||||
|
||||
def _prev_bootloader(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value is not None:
|
||||
return f'{str(_("Bootloader"))}: {item.value.value}'
|
||||
return None
|
||||
|
||||
def _prev_disk_encryption(self, item: MenuItem) -> Optional[str]:
|
||||
disk_config: Optional[disk.DiskLayoutConfiguration] = self._item_group.find_by_key('disk_config').value
|
||||
enc_config: Optional[disk.DiskEncryption] = item.value
|
||||
|
||||
if disk_config and not disk.DiskEncryption.validate_enc(disk_config):
|
||||
return str(_('LVM disk encryption with more than 2 partitions is currently not supported'))
|
||||
|
||||
encryption: Optional[disk.DiskEncryption] = self._menu_options['disk_encryption'].current_selection
|
||||
|
||||
if encryption:
|
||||
enc_type = disk.EncryptionType.type_to_text(encryption.encryption_type)
|
||||
if enc_config:
|
||||
enc_type = disk.EncryptionType.type_to_text(enc_config.encryption_type)
|
||||
output = str(_('Encryption type')) + f': {enc_type}\n'
|
||||
output += str(_('Password')) + f': {secret(encryption.encryption_password)}\n'
|
||||
output += str(_('Password')) + f': {secret(enc_config.encryption_password)}\n'
|
||||
|
||||
if encryption.partitions:
|
||||
output += 'Partitions: {} selected'.format(len(encryption.partitions)) + '\n'
|
||||
elif encryption.lvm_volumes:
|
||||
output += 'LVM volumes: {} selected'.format(len(encryption.lvm_volumes)) + '\n'
|
||||
if enc_config.partitions:
|
||||
output += 'Partitions: {} selected'.format(len(enc_config.partitions)) + '\n'
|
||||
elif enc_config.lvm_volumes:
|
||||
output += 'LVM volumes: {} selected'.format(len(enc_config.lvm_volumes)) + '\n'
|
||||
|
||||
if encryption.hsm_device:
|
||||
output += f'HSM: {encryption.hsm_device.manufacturer}'
|
||||
if enc_config.hsm_device:
|
||||
output += f'HSM: {enc_config.hsm_device.manufacturer}'
|
||||
|
||||
return output
|
||||
|
||||
return None
|
||||
|
||||
def _display_disk_encryption(self, current_value: Optional[disk.DiskEncryption]) -> str:
|
||||
if current_value:
|
||||
return disk.EncryptionType.type_to_text(current_value.encryption_type)
|
||||
return ''
|
||||
|
||||
def _validate_bootloader(self) -> Optional[str]:
|
||||
"""
|
||||
Checks the selected bootloader is valid for the selected filesystem
|
||||
|
|
@ -350,10 +430,10 @@ class GlobalMenu(AbstractMenu):
|
|||
XXX: The caller is responsible for wrapping the string with the translation
|
||||
shim if necessary.
|
||||
"""
|
||||
bootloader = self._menu_options['bootloader'].current_selection
|
||||
bootloader = self._item_group.find_by_key('bootloader').value
|
||||
boot_partition: Optional[disk.PartitionModification] = None
|
||||
|
||||
if disk_config := self._menu_options['disk_config'].current_selection:
|
||||
if disk_config := self._item_group.find_by_key('disk_config').value:
|
||||
for layout in disk_config.device_modifications:
|
||||
if boot_partition := layout.get_boot_partition():
|
||||
break
|
||||
|
|
@ -369,7 +449,7 @@ class GlobalMenu(AbstractMenu):
|
|||
|
||||
return None
|
||||
|
||||
def _prev_install_invalid_config(self) -> Optional[str]:
|
||||
def _prev_install_invalid_config(self, item: MenuItem) -> Optional[str]:
|
||||
if missing := self._missing_configs():
|
||||
text = str(_('Missing configurations:\n'))
|
||||
for m in missing:
|
||||
|
|
@ -381,17 +461,15 @@ class GlobalMenu(AbstractMenu):
|
|||
|
||||
return None
|
||||
|
||||
def _prev_users(self) -> Optional[str]:
|
||||
selector = self._menu_options['!users']
|
||||
users: Optional[List[User]] = selector.current_selection
|
||||
def _prev_users(self, item: MenuItem) -> Optional[str]:
|
||||
users: Optional[List[User]] = item.value
|
||||
|
||||
if users:
|
||||
return FormattedOutput.as_table(users)
|
||||
return None
|
||||
|
||||
def _prev_profile(self) -> Optional[str]:
|
||||
selector = self._menu_options['profile_config']
|
||||
profile_config: Optional[ProfileConfiguration] = selector.current_selection
|
||||
def _prev_profile(self, item: MenuItem) -> Optional[str]:
|
||||
profile_config: Optional[ProfileConfiguration] = item.value
|
||||
|
||||
if profile_config and profile_config.profile:
|
||||
output = str(_('Profiles')) + ': '
|
||||
|
|
@ -410,63 +488,59 @@ class GlobalMenu(AbstractMenu):
|
|||
|
||||
return None
|
||||
|
||||
def _set_root_password(self) -> Optional[str]:
|
||||
prompt = str(_('Enter root password (leave blank to disable root): '))
|
||||
password = get_password(prompt=prompt)
|
||||
def _set_root_password(self, preset: Optional[str] = None) -> Optional[str]:
|
||||
password = get_password(text=str(_('Root password')), allow_skip=True)
|
||||
return password
|
||||
|
||||
def _select_disk_config(
|
||||
self,
|
||||
preset: Optional[disk.DiskLayoutConfiguration] = None
|
||||
) -> Optional[disk.DiskLayoutConfiguration]:
|
||||
data_store: Dict[str, Any] = {}
|
||||
disk_config = disk.DiskLayoutConfigurationMenu(preset, data_store).run()
|
||||
disk_config = disk.DiskLayoutConfigurationMenu(preset).run()
|
||||
|
||||
if disk_config != preset:
|
||||
self._menu_options['disk_encryption'].set_current_selection(None)
|
||||
self._menu_item_group.find_by_key('disk_encryption').value = None
|
||||
|
||||
return disk_config
|
||||
|
||||
def _select_bootloader(self, preset: Optional[Bootloader]) -> Optional[Bootloader]:
|
||||
bootloader = ask_for_bootloader(preset)
|
||||
|
||||
if bootloader:
|
||||
uki = self._item_group.find_by_key('uki')
|
||||
if not SysInfo.has_uefi() or not bootloader.has_uki_support():
|
||||
uki.value = False
|
||||
uki.enabled = False
|
||||
else:
|
||||
uki.enabled = True
|
||||
|
||||
return bootloader
|
||||
|
||||
def _select_profile(self, current_profile: Optional[ProfileConfiguration]):
|
||||
from .profile.profile_menu import ProfileMenu
|
||||
store: Dict[str, Any] = {}
|
||||
profile_config = ProfileMenu(store, preset=current_profile).run()
|
||||
profile_config = ProfileMenu(preset=current_profile).run()
|
||||
return profile_config
|
||||
|
||||
def _select_audio(
|
||||
self,
|
||||
current: Optional[AudioConfiguration] = None
|
||||
) -> Optional[AudioConfiguration]:
|
||||
selection = ask_for_audio_selection(current)
|
||||
return selection
|
||||
|
||||
def _display_audio(self, current: Optional[AudioConfiguration]) -> str:
|
||||
if not current:
|
||||
return Audio.no_audio_text()
|
||||
else:
|
||||
return current.audio.name
|
||||
|
||||
def _create_user_account(self, defined_users: List[User]) -> List[User]:
|
||||
users = ask_for_additional_users(defined_users=defined_users)
|
||||
def _create_user_account(self, preset: Optional[List[User]] = None) -> List[User]:
|
||||
preset = [] if preset is None else preset
|
||||
users = ask_for_additional_users(defined_users=preset)
|
||||
return users
|
||||
|
||||
def _mirror_configuration(self, preset: Optional[MirrorConfiguration] = None) -> Optional[MirrorConfiguration]:
|
||||
data_store: Dict[str, Any] = {}
|
||||
mirror_configuration = MirrorMenu(data_store, preset=preset).run()
|
||||
mirror_configuration = MirrorMenu(preset=preset).run()
|
||||
return mirror_configuration
|
||||
|
||||
def _prev_mirror_config(self) -> Optional[str]:
|
||||
selector = self._menu_options['mirror_config']
|
||||
def _prev_mirror_config(self, item: MenuItem) -> Optional[str]:
|
||||
if not item.value:
|
||||
return None
|
||||
|
||||
if selector.has_selection():
|
||||
mirror_config: MirrorConfiguration = selector.current_selection # type: ignore
|
||||
output = ''
|
||||
if mirror_config.regions:
|
||||
output += '{}: {}\n\n'.format(str(_('Mirror regions')), mirror_config.regions)
|
||||
if mirror_config.custom_mirrors:
|
||||
table = FormattedOutput.as_table(mirror_config.custom_mirrors)
|
||||
output += '{}\n{}'.format(str(_('Custom mirrors')), table)
|
||||
mirror_config: MirrorConfiguration = item.value
|
||||
|
||||
return output.strip()
|
||||
output = ''
|
||||
if mirror_config.regions:
|
||||
output += '{}: {}\n\n'.format(str(_('Mirror regions')), mirror_config.regions)
|
||||
if mirror_config.custom_mirrors:
|
||||
table = FormattedOutput.as_table(mirror_config.custom_mirrors)
|
||||
output += '{}\n{}'.format(str(_('Custom mirrors')), table)
|
||||
|
||||
return None
|
||||
return output.strip()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from .exceptions import SysCallError
|
|||
from .general import SysCommand
|
||||
from .networking import list_interfaces, enrich_iface_types
|
||||
from .output import debug
|
||||
from .utils.util import format_cols
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -78,9 +77,12 @@ class GfxDriver(Enum):
|
|||
return False
|
||||
|
||||
def packages_text(self) -> str:
|
||||
text = str(_('Installed packages')) + ':\n'
|
||||
pkg_names = [p.value for p in self.gfx_packages()]
|
||||
text += format_cols(sorted(pkg_names))
|
||||
text = str(_('Installed packages')) + ':\n'
|
||||
|
||||
for p in sorted(pkg_names):
|
||||
text += f'\t- {p}\n'
|
||||
|
||||
return text
|
||||
|
||||
def gfx_packages(self) -> List[GfxPackage]:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from . import pacman
|
|||
from .pacman import Pacman
|
||||
from .plugins import plugins
|
||||
from .storage import storage
|
||||
from archinstall.tui.curses_menu import Tui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -105,9 +106,9 @@ class Installer:
|
|||
|
||||
# We avoid printing /mnt/<log path> because that might confuse people if they note it down
|
||||
# and then reboot, and a identical log file will be found in the ISO medium anyway.
|
||||
print(_("[!] A log file has been created here: {}").format(
|
||||
os.path.join(storage['LOG_PATH'], storage['LOG_FILE'])))
|
||||
print(_(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues"))
|
||||
log_file = os.path.join(storage['LOG_PATH'], storage['LOG_FILE'])
|
||||
Tui.print(str(_("[!] A log file has been created here: {}").format(log_file)))
|
||||
Tui.print(str(_('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues')))
|
||||
raise exc_val
|
||||
|
||||
if not (missing_steps := self.post_install_check()):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from .manage_users_conf import UserList, ask_for_additional_users
|
||||
from .network_menu import ManualNetworkConfig, ask_to_configure_network
|
||||
from .utils import get_password
|
||||
|
||||
from .disk_conf import (
|
||||
select_devices, select_disk_config, get_default_partition_layout,
|
||||
|
|
|
|||
|
|
@ -7,25 +7,22 @@ from typing import Optional, List, Tuple
|
|||
from .. import disk
|
||||
from ..disk.device_model import BtrfsMountOption
|
||||
from ..hardware import SysInfo
|
||||
from ..menu import Menu
|
||||
from ..menu import TableMenu
|
||||
from ..menu.menu import MenuSelectionType
|
||||
from ..output import FormattedOutput, debug
|
||||
from ..utils.util import prompt_dir
|
||||
from ..storage import storage
|
||||
|
||||
from archinstall.lib.menu.menu_helper import MenuHelper
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType,
|
||||
Orientation
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
||||
def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]:
|
||||
"""
|
||||
Asks the user to select one or multiple devices
|
||||
|
||||
:return: List of selected devices
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
def select_devices(preset: Optional[List[disk.BDevice]] = []) -> List[disk.BDevice]:
|
||||
def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]:
|
||||
dev = disk.device_handler.get_device(selection.path)
|
||||
if dev and dev.partition_infos:
|
||||
|
|
@ -35,30 +32,25 @@ def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]:
|
|||
if preset is None:
|
||||
preset = []
|
||||
|
||||
title = str(_('Select one or more devices to use and configure'))
|
||||
warning = str(_('If you reset the device selection this will also reset the current disk layout. Are you sure?'))
|
||||
|
||||
devices = disk.device_handler.devices
|
||||
options = [d.device_info for d in devices]
|
||||
preset_value = [p.device_info for p in preset]
|
||||
presets = [p.device_info for p in preset]
|
||||
|
||||
choice = TableMenu(
|
||||
title,
|
||||
data=options,
|
||||
multi=True,
|
||||
preset=preset_value,
|
||||
preview_command=_preview_device_selection,
|
||||
preview_title=str(_('Existing Partitions')),
|
||||
preview_size=0.2,
|
||||
allow_reset=True,
|
||||
allow_reset_warning_msg=warning
|
||||
group, header = MenuHelper.create_table(data=options)
|
||||
group.set_selected_by_value(presets)
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
search_enabled=False,
|
||||
multi=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Reset: return []
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection:
|
||||
selected_device_info: List[disk._DeviceInfo] = choice.single_value
|
||||
match result.type_:
|
||||
case ResultType.Reset: return []
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Selection:
|
||||
selected_device_info: List[disk._DeviceInfo] = result.get_values()
|
||||
selected_devices = []
|
||||
|
||||
for device in devices:
|
||||
|
|
@ -113,35 +105,40 @@ def select_disk_config(
|
|||
manual_mode = disk.DiskLayoutType.Manual.display_msg()
|
||||
pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg()
|
||||
|
||||
options = [default_layout, manual_mode, pre_mount_mode]
|
||||
preset_value = preset.config_type.display_msg() if preset else None
|
||||
warning = str(_('Are you sure you want to reset this setting?'))
|
||||
items = [
|
||||
MenuItem(default_layout, value=default_layout),
|
||||
MenuItem(manual_mode, value=manual_mode),
|
||||
MenuItem(pre_mount_mode, value=pre_mount_mode)
|
||||
]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
|
||||
choice = Menu(
|
||||
_('Select a partitioning option'),
|
||||
options,
|
||||
allow_reset=True,
|
||||
allow_reset_warning_msg=warning,
|
||||
sort=False,
|
||||
preview_size=0.2,
|
||||
preset_values=preset_value
|
||||
if preset:
|
||||
group.set_selected_by_value(preset.config_type.display_msg())
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Disk configuration type'))),
|
||||
allow_reset=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Reset: return None
|
||||
case MenuSelectionType.Selection:
|
||||
if choice.single_value == pre_mount_mode:
|
||||
match result.type_:
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Reset: return None
|
||||
case ResultType.Selection:
|
||||
selection = result.get_value()
|
||||
|
||||
if selection == pre_mount_mode:
|
||||
output = 'You will use whatever drive-setup is mounted at the specified directory\n'
|
||||
output += "WARNING: Archinstall won't check the suitability of this setup\n"
|
||||
|
||||
try:
|
||||
path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return preset
|
||||
path = prompt_dir(str(_('Root mount directory')), output, allow_skip=False)
|
||||
assert path is not None
|
||||
|
||||
mods = disk.device_handler.detect_pre_mounted_mods(path)
|
||||
|
||||
storage['MOUNT_POINT'] = Path(path)
|
||||
storage['MOUNT_POINT'] = path
|
||||
|
||||
return disk.DiskLayoutConfiguration(
|
||||
config_type=disk.DiskLayoutType.Pre_mount,
|
||||
|
|
@ -155,14 +152,14 @@ def select_disk_config(
|
|||
if not devices:
|
||||
return None
|
||||
|
||||
if choice.value == default_layout:
|
||||
if result.get_value() == default_layout:
|
||||
modifications = get_default_partition_layout(devices, advanced_option=advanced_option)
|
||||
if modifications:
|
||||
return disk.DiskLayoutConfiguration(
|
||||
config_type=disk.DiskLayoutType.Default,
|
||||
device_modifications=modifications
|
||||
)
|
||||
elif choice.value == manual_mode:
|
||||
elif result.get_value() == manual_mode:
|
||||
preset_mods = preset.device_modifications if preset else []
|
||||
modifications = _manual_partitioning(preset_mods, devices)
|
||||
|
||||
|
|
@ -179,30 +176,29 @@ def select_lvm_config(
|
|||
disk_config: disk.DiskLayoutConfiguration,
|
||||
preset: Optional[disk.LvmConfiguration] = None,
|
||||
) -> Optional[disk.LvmConfiguration]:
|
||||
preset_value = preset.config_type.display_msg() if preset else None
|
||||
default_mode = disk.LvmLayoutType.Default.display_msg()
|
||||
|
||||
options = [default_mode]
|
||||
items = [MenuItem(default_mode, value=default_mode)]
|
||||
group = MenuItemGroup(items)
|
||||
group.set_focus_by_value(preset_value)
|
||||
|
||||
preset_value = preset.config_type.display_msg() if preset else None
|
||||
warning = str(_('Are you sure you want to reset this setting?'))
|
||||
|
||||
choice = Menu(
|
||||
_('Select a LVM option'),
|
||||
options,
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_reset=True,
|
||||
allow_reset_warning_msg=warning,
|
||||
sort=False,
|
||||
preview_size=0.2,
|
||||
preset_values=preset_value
|
||||
allow_skip=True,
|
||||
frame=FrameProperties.min(str(_('LVM configuration type'))),
|
||||
alignment=Alignment.CENTER
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Reset: return None
|
||||
case MenuSelectionType.Selection:
|
||||
if choice.single_value == default_mode:
|
||||
match result.type_:
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Reset: return None
|
||||
case ResultType.Selection:
|
||||
if result.get_value() == default_mode:
|
||||
return suggest_lvm_layout(disk_config)
|
||||
return preset
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.PartitionModification:
|
||||
|
|
@ -227,33 +223,56 @@ def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.Parti
|
|||
|
||||
|
||||
def select_main_filesystem_format(advanced_options: bool = False) -> disk.FilesystemType:
|
||||
options = {
|
||||
'btrfs': disk.FilesystemType.Btrfs,
|
||||
'ext4': disk.FilesystemType.Ext4,
|
||||
'xfs': disk.FilesystemType.Xfs,
|
||||
'f2fs': disk.FilesystemType.F2fs
|
||||
}
|
||||
items = [
|
||||
MenuItem('btrfs', value=disk.FilesystemType.Btrfs),
|
||||
MenuItem('ext4', value=disk.FilesystemType.Ext4),
|
||||
MenuItem('xfs', value=disk.FilesystemType.Xfs),
|
||||
MenuItem('f2fs', value=disk.FilesystemType.F2fs)
|
||||
]
|
||||
|
||||
if advanced_options:
|
||||
options.update({'ntfs': disk.FilesystemType.Ntfs})
|
||||
items.append(MenuItem('ntfs', value=disk.FilesystemType.Ntfs))
|
||||
|
||||
prompt = _('Select which filesystem your main partition should use')
|
||||
choice = Menu(prompt, options, skip=False, sort=False).run()
|
||||
return options[choice.single_value]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min('Filesystem'),
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
def select_mount_options() -> List[str]:
|
||||
prompt = str(_('Would you like to use compression or disable CoW?'))
|
||||
options = [str(_('Use compression')), str(_('Disable Copy-on-Write'))]
|
||||
choice = Menu(prompt, options, sort=False).run()
|
||||
prompt = str(_('Would you like to use compression or disable CoW?')) + '\n'
|
||||
compression = str(_('Use compression'))
|
||||
disable_cow = str(_('Disable Copy-on-Write'))
|
||||
|
||||
if choice.type_ == MenuSelectionType.Selection:
|
||||
if choice.single_value == options[0]:
|
||||
return [BtrfsMountOption.compress.value]
|
||||
else:
|
||||
return [BtrfsMountOption.nodatacow.value]
|
||||
items = [
|
||||
MenuItem(compression, value=BtrfsMountOption.compress.value),
|
||||
MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value),
|
||||
]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
search_enabled=False,
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
return []
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
return [result.get_value()]
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
def process_root_partition_size(total_size: disk.Size, sector_size: disk.SectorSize) -> disk.Size:
|
||||
|
|
@ -286,9 +305,19 @@ def suggest_single_disk_layout(
|
|||
min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB, sector_size)
|
||||
|
||||
if filesystem_type == disk.FilesystemType.Btrfs:
|
||||
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?'))
|
||||
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
|
||||
using_subvolumes = choice.value == Menu.yes()
|
||||
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(MenuItem.yes().value)
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
using_subvolumes = result.item() == MenuItem.yes()
|
||||
mount_options = select_mount_options()
|
||||
else:
|
||||
using_subvolumes = False
|
||||
|
|
@ -325,9 +354,19 @@ def suggest_single_disk_layout(
|
|||
elif separate_home:
|
||||
using_home_partition = True
|
||||
else:
|
||||
prompt = str(_('Would you like to create a separate partition for /home?'))
|
||||
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
|
||||
using_home_partition = choice.value == Menu.yes()
|
||||
prompt = str(_('Would you like to create a separate partition for /home?')) + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(MenuItem.yes().value)
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
columns=2,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
using_home_partition = result.item() == MenuItem.yes()
|
||||
|
||||
# root partition
|
||||
root_start = boot_partition.start + boot_partition.length
|
||||
|
|
@ -417,10 +456,14 @@ def suggest_multi_disk_layout(
|
|||
root_device: Optional[disk.BDevice] = sorted_delta[0][0]
|
||||
|
||||
if home_device is None or root_device is None:
|
||||
text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n')
|
||||
text += _('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB))
|
||||
text += _('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB))
|
||||
Menu(str(text), [str(_('Continue'))], skip=False).run()
|
||||
text = str(_('The selected drives do not have the minimum capacity required for an automatic suggestion\n'))
|
||||
text += str(_('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)))
|
||||
text += str(_('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)))
|
||||
|
||||
items = [MenuItem(str(_('Continue')))]
|
||||
group = MenuItemGroup(items)
|
||||
SelectMenu(group).run()
|
||||
|
||||
return []
|
||||
|
||||
if filesystem_type == disk.FilesystemType.Btrfs:
|
||||
|
|
@ -503,10 +546,21 @@ def suggest_lvm_layout(
|
|||
filesystem_type = select_main_filesystem_format()
|
||||
|
||||
if filesystem_type == disk.FilesystemType.Btrfs:
|
||||
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?'))
|
||||
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
|
||||
using_subvolumes = choice.value == Menu.yes()
|
||||
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(MenuItem.yes().value)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
search_enabled=False,
|
||||
allow_skip=False,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
columns=2,
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
|
||||
using_subvolumes = MenuItem.yes() == result.item()
|
||||
mount_options = select_mount_options()
|
||||
|
||||
if using_subvolumes:
|
||||
|
|
|
|||
|
|
@ -4,86 +4,114 @@ import pathlib
|
|||
from typing import List, Any, Optional, TYPE_CHECKING
|
||||
|
||||
from ..locale import list_timezones
|
||||
from ..menu import MenuSelectionType, Menu, TextInput
|
||||
from ..models.audio_configuration import Audio, AudioConfiguration
|
||||
from ..output import warn
|
||||
from ..packages.packages import validate_package_list
|
||||
from ..storage import storage
|
||||
from ..translationhandler import Language
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType,
|
||||
EditMenu, Orientation, Tui
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
||||
def ask_ntp(preset: bool = True) -> bool:
|
||||
prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n'))
|
||||
prompt += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki'))
|
||||
if preset:
|
||||
preset_val = Menu.yes()
|
||||
else:
|
||||
preset_val = Menu.no()
|
||||
choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run()
|
||||
header = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) + '\n'
|
||||
header += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki')) + '\n'
|
||||
|
||||
return False if choice.value == Menu.no() else True
|
||||
preset_val = MenuItem.yes() if preset else MenuItem.no()
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = preset_val
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return result.item() == MenuItem.yes()
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
|
||||
def ask_hostname(preset: str = '') -> str:
|
||||
hostname = TextInput(
|
||||
str(_('Desired hostname for the installation: ')),
|
||||
preset
|
||||
).run().strip()
|
||||
def ask_hostname(preset: Optional[str] = None) -> Optional[str]:
|
||||
result = EditMenu(
|
||||
str(_('Hostname')),
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
default_text=preset
|
||||
).input()
|
||||
|
||||
if not hostname:
|
||||
return preset
|
||||
|
||||
return hostname
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
hostname = result.text()
|
||||
if len(hostname) < 1:
|
||||
return None
|
||||
return hostname
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]:
|
||||
timezones = list_timezones()
|
||||
default = 'UTC'
|
||||
timezones = list_timezones()
|
||||
|
||||
choice = Menu(
|
||||
_('Select a timezone'),
|
||||
timezones,
|
||||
preset_values=preset,
|
||||
default_option=default
|
||||
items = [MenuItem(tz, value=tz) for tz in timezones]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_selected_by_value(preset)
|
||||
group.set_default_by_value(default)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
frame=FrameProperties.min(str(_('Timezone'))),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection: return choice.single_value
|
||||
|
||||
return None
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return default
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
|
||||
|
||||
def ask_for_audio_selection(
|
||||
current: Optional[AudioConfiguration] = None
|
||||
) -> Optional[AudioConfiguration]:
|
||||
choices = [
|
||||
Audio.Pipewire.name, # pylint: disable=no-member
|
||||
Audio.Pulseaudio.name, # pylint: disable=no-member
|
||||
Audio.no_audio_text()
|
||||
]
|
||||
def ask_for_audio_selection(preset: Optional[AudioConfiguration] = None) -> Optional[AudioConfiguration]:
|
||||
items = [MenuItem(a.value, value=a) for a in Audio]
|
||||
group = MenuItemGroup(items)
|
||||
|
||||
preset = current.audio.name if current else None
|
||||
if preset:
|
||||
group.set_focus_by_value(preset.audio)
|
||||
|
||||
choice = Menu(
|
||||
_('Choose an audio server'),
|
||||
choices,
|
||||
preset_values=preset
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Audio')))
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return current
|
||||
case MenuSelectionType.Selection:
|
||||
value = choice.single_value
|
||||
if value == Audio.no_audio_text():
|
||||
return None
|
||||
else:
|
||||
return AudioConfiguration(Audio[value])
|
||||
|
||||
return None
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return AudioConfiguration(audio=result.get_value())
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
def select_language(preset: Optional[str] = None) -> Optional[str]:
|
||||
|
|
@ -94,7 +122,8 @@ def select_language(preset: Optional[str] = None) -> Optional[str]:
|
|||
# raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.")
|
||||
|
||||
# No need to translate this i feel, as it's a short lived message.
|
||||
warn("select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version")
|
||||
warn(
|
||||
"select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version")
|
||||
|
||||
return select_kb_layout(preset)
|
||||
|
||||
|
|
@ -103,70 +132,112 @@ def select_archinstall_language(languages: List[Language], preset: Language) ->
|
|||
# these are the displayed language names which can either be
|
||||
# the english name of a language or, if present, the
|
||||
# name of the language in its own language
|
||||
options = {lang.display_name: lang for lang in languages}
|
||||
|
||||
items = [MenuItem(lang.display_name, lang) for lang in languages]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n'
|
||||
title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n'
|
||||
title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n'
|
||||
|
||||
choice = Menu(
|
||||
title,
|
||||
list(options.keys()),
|
||||
default_option=preset.display_name,
|
||||
preview_size=0.5
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=title,
|
||||
allow_skip=True,
|
||||
allow_reset=False,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(header=str(_('Select language')))
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection: return options[choice.single_value]
|
||||
|
||||
raise ValueError('Language selection not handled')
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Language selection not handled')
|
||||
|
||||
|
||||
def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]:
|
||||
# Additional packages (with some light weight error handling for invalid package names)
|
||||
print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.'))
|
||||
print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.'))
|
||||
header = str(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) + '\n'
|
||||
header += str(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) + '\n'
|
||||
header += str(_('Write additional packages to install (space separated, leave blank to skip)'))
|
||||
|
||||
def read_packages(p: list[str] = []) -> list[str]:
|
||||
display = ' '.join(p)
|
||||
input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip()
|
||||
return input_packages.split() if input_packages else []
|
||||
def validator(value: str) -> Optional[str]:
|
||||
packages = value.split() if value else []
|
||||
|
||||
preset = preset if preset else []
|
||||
packages = read_packages(preset)
|
||||
if len(packages) == 0:
|
||||
return None
|
||||
|
||||
if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']:
|
||||
while True:
|
||||
if len(packages):
|
||||
# Verify packages that were given
|
||||
print(_("Verifying that additional packages exist (this might take a few seconds)"))
|
||||
valid, invalid = validate_package_list(packages)
|
||||
if storage['arguments']['offline'] or storage['arguments']['no_pkg_lookups']:
|
||||
return None
|
||||
|
||||
if invalid:
|
||||
warn(f"Some packages could not be found in the repository: {invalid}")
|
||||
packages = read_packages(valid)
|
||||
continue
|
||||
break
|
||||
# Verify packages that were given
|
||||
out = str(_("Verifying that additional packages exist (this might take a few seconds)"))
|
||||
Tui.print(out, 0)
|
||||
valid, invalid = validate_package_list(packages)
|
||||
|
||||
return packages
|
||||
if invalid:
|
||||
return f'{str(_("Some packages could not be found in the repository"))}: {invalid}'
|
||||
|
||||
return None
|
||||
|
||||
result = EditMenu(
|
||||
str(_('Additional packages')),
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
edit_width=100,
|
||||
validator=validator,
|
||||
default_text=' '.join(preset)
|
||||
).input()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return []
|
||||
case ResultType.Selection:
|
||||
packages = result.text()
|
||||
return packages.split(' ')
|
||||
|
||||
|
||||
def add_number_of_parallel_downloads(input_number: Optional[int] = None) -> Optional[int]:
|
||||
def add_number_of_parallel_downloads(preset: Optional[int] = None) -> Optional[int]:
|
||||
max_recommended = 5
|
||||
print(_("This option enables the number of parallel downloads that can occur during package downloads"))
|
||||
print(_("Enter the number of parallel downloads to be enabled.\n\nNote:\n"))
|
||||
print(str(_(" - Maximum recommended value : {} ( Allows {} parallel downloads at a time )")).format(max_recommended, max_recommended))
|
||||
print(_(" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n"))
|
||||
|
||||
while True:
|
||||
header = str(_('This option enables the number of parallel downloads that can occur during package downloads')) + '\n'
|
||||
header += str(_('Enter the number of parallel downloads to be enabled.\n\nNote:\n'))
|
||||
header += str(_(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )')).format(max_recommended, max_recommended) + '\n'
|
||||
header += str(_(' - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n'))
|
||||
|
||||
def validator(s: str) -> Optional[str]:
|
||||
try:
|
||||
input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0)
|
||||
if input_number <= 0:
|
||||
input_number = 0
|
||||
break
|
||||
except:
|
||||
print(str(_("Invalid input! Try again with a valid input [or 0 to disable]")).format(max_recommended))
|
||||
value = int(s)
|
||||
if value >= 0:
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return str(_('Invalid download number'))
|
||||
|
||||
result = EditMenu(
|
||||
str(_('Number downloads')),
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
validator=validator,
|
||||
default_text=str(preset) if preset is not None else None
|
||||
).input()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return 0
|
||||
case ResultType.Selection:
|
||||
downloads: int = int(result.text())
|
||||
|
||||
pacman_conf_path = pathlib.Path("/etc/pacman.conf")
|
||||
with pacman_conf_path.open() as f:
|
||||
|
|
@ -175,11 +246,11 @@ def add_number_of_parallel_downloads(input_number: Optional[int] = None) -> Opti
|
|||
with pacman_conf_path.open("w") as fwrite:
|
||||
for line in pacman_conf:
|
||||
if "ParallelDownloads" in line:
|
||||
fwrite.write(f"ParallelDownloads = {input_number}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n")
|
||||
fwrite.write(f"ParallelDownloads = {downloads}\n")
|
||||
else:
|
||||
fwrite.write(f"{line}\n")
|
||||
|
||||
return input_number
|
||||
return downloads
|
||||
|
||||
|
||||
def select_additional_repositories(preset: List[str]) -> List[str]:
|
||||
|
|
@ -191,19 +262,55 @@ def select_additional_repositories(preset: List[str]) -> List[str]:
|
|||
"""
|
||||
|
||||
repositories = ["multilib", "testing"]
|
||||
items = [MenuItem(r, value=r) for r in repositories]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_selected_by_value(preset)
|
||||
|
||||
choice = Menu(
|
||||
_('Choose which optional additional repositories to enable'),
|
||||
repositories,
|
||||
sort=False,
|
||||
multi=True,
|
||||
preset_values=preset,
|
||||
allow_reset=True
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min('Additional repositories'),
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
multi=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Reset: return []
|
||||
case MenuSelectionType.Selection: return choice.single_value
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return []
|
||||
case ResultType.Selection:
|
||||
return result.get_values()
|
||||
|
||||
return []
|
||||
|
||||
def ask_chroot() -> bool:
|
||||
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
).run()
|
||||
|
||||
return result.item() == MenuItem.yes()
|
||||
|
||||
|
||||
def ask_abort() -> None:
|
||||
prompt = str(_('Do you really want to abort?')) + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL
|
||||
).run()
|
||||
|
||||
if result.item() == MenuItem.yes():
|
||||
exit(0)
|
||||
|
|
|
|||
|
|
@ -3,19 +3,22 @@ from __future__ import annotations
|
|||
import re
|
||||
from typing import Any, TYPE_CHECKING, List, Optional
|
||||
|
||||
from .utils import get_password
|
||||
from ..menu import Menu, ListManager
|
||||
from ..utils.util import get_password
|
||||
from ..menu import ListManager
|
||||
from ..models.users import User
|
||||
from ..general import secret
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
Alignment, EditMenu, Orientation,
|
||||
ResultType
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
||||
class UserList(ListManager):
|
||||
"""
|
||||
subclass of ListManager for the managing of user accounts
|
||||
"""
|
||||
|
||||
def __init__(self, prompt: str, lusers: List[User]):
|
||||
self._actions = [
|
||||
str(_('Add a user')),
|
||||
|
|
@ -37,8 +40,9 @@ class UserList(ListManager):
|
|||
data = [d for d in data if d.username != new_user.username]
|
||||
data += [new_user]
|
||||
elif action == self._actions[1] and entry: # change password
|
||||
prompt = str(_('Password for user "{}": ').format(entry.username))
|
||||
new_password = get_password(prompt=prompt)
|
||||
header = f'{str(_("User"))}: {entry.username}\n'
|
||||
new_password = get_password(str(_('Password')), header=header)
|
||||
|
||||
if new_password:
|
||||
user = next(filter(lambda x: x == entry, data))
|
||||
user.password = new_password
|
||||
|
|
@ -50,42 +54,55 @@ class UserList(ListManager):
|
|||
|
||||
return data
|
||||
|
||||
def _check_for_correct_username(self, username: str) -> bool:
|
||||
def _check_for_correct_username(self, username: str) -> Optional[str]:
|
||||
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
|
||||
return True
|
||||
return False
|
||||
return None
|
||||
return str(_("The username you entered is invalid"))
|
||||
|
||||
def _add_user(self) -> Optional[User]:
|
||||
prompt = '\n\n' + str(_('Enter username (leave blank to skip): '))
|
||||
editResult = EditMenu(
|
||||
str(_('Username')),
|
||||
allow_skip=True,
|
||||
validator=self._check_for_correct_username
|
||||
).input()
|
||||
|
||||
while True:
|
||||
try:
|
||||
username = input(prompt).strip(' ')
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
match editResult.type_:
|
||||
case ResultType.Skip:
|
||||
return None
|
||||
case ResultType.Selection:
|
||||
username = editResult.text()
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
if not username:
|
||||
return None
|
||||
if not self._check_for_correct_username(username):
|
||||
error_prompt = str(_("The username you entered is invalid. Try again"))
|
||||
print(error_prompt)
|
||||
else:
|
||||
break
|
||||
header = f'{str(_("Username"))}: {username}\n'
|
||||
|
||||
password = get_password(prompt=str(_('Password for user "{}": ').format(username)))
|
||||
password = get_password(str(_('Password')), header=header, allow_skip=True)
|
||||
|
||||
if not password:
|
||||
return None
|
||||
|
||||
choice = Menu(
|
||||
str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(),
|
||||
skip=False,
|
||||
default_option=Menu.yes(),
|
||||
clear_screen=False,
|
||||
show_search_hint=False
|
||||
header += f'{str(_("Password"))}: {secret(password)}\n\n'
|
||||
header += str(_('Should "{}" be a superuser (sudo)?\n')).format(username)
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.yes()
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
search_enabled=False,
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
sudo = True if choice.value == Menu.yes() else False
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
sudo = result.item() == MenuItem.yes()
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
return User(username, password, sudo)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any, Optional, TYPE_CHECKING, List, Dict
|
||||
from typing import Any, Optional, TYPE_CHECKING, List
|
||||
|
||||
from ..menu import MenuSelectionType, TextInput
|
||||
from ..models.network_configuration import NetworkConfiguration, NicType, Nic
|
||||
|
||||
from ..networking import list_interfaces
|
||||
from ..output import FormattedOutput, warn
|
||||
from ..menu import ListManager, Menu
|
||||
from ..menu import ListManager
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType,
|
||||
EditMenu
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
||||
class ManualNetworkConfig(ListManager):
|
||||
"""
|
||||
subclass of ListManager for the managing of network configurations
|
||||
"""
|
||||
|
||||
def __init__(self, prompt: str, preset: List[Nic]):
|
||||
self._actions = [
|
||||
str(_('Add interface')),
|
||||
|
|
@ -27,21 +26,6 @@ class ManualNetworkConfig(ListManager):
|
|||
]
|
||||
super().__init__(prompt, preset, [self._actions[0]], self._actions[1:])
|
||||
|
||||
def reformat(self, data: List[Nic]) -> Dict[str, Optional[Nic]]:
|
||||
table = FormattedOutput.as_table(data)
|
||||
rows = table.split('\n')
|
||||
|
||||
# these are the header rows of the table and do not map to any User obviously
|
||||
# we're adding 2 spaces as prefix because the menu selector '> ' will be put before
|
||||
# the selectable rows so the header has to be aligned
|
||||
display_data: Dict[str, Optional[Nic]] = {f' {rows[0]}': None, f' {rows[1]}': None}
|
||||
|
||||
for row, iface in zip(rows[2:], data):
|
||||
row = row.replace('|', '\\|')
|
||||
display_data[row] = iface
|
||||
|
||||
return display_data
|
||||
|
||||
def selected_action_display(self, nic: Nic) -> str:
|
||||
return nic.iface if nic.iface else ''
|
||||
|
||||
|
|
@ -69,56 +53,112 @@ class ManualNetworkConfig(ListManager):
|
|||
if not available:
|
||||
return None
|
||||
|
||||
choice = Menu(str(_('Select interface to add')), list(available), skip=True).run()
|
||||
|
||||
if choice.type_ == MenuSelectionType.Skip:
|
||||
if not available:
|
||||
return None
|
||||
|
||||
return choice.single_value
|
||||
items = [MenuItem(i, value=i) for i in available]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Interfaces'))),
|
||||
allow_skip=True
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return None
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
def _get_ip_address(
|
||||
self,
|
||||
title: str,
|
||||
header: str,
|
||||
allow_skip: bool,
|
||||
multi: bool,
|
||||
preset: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
def validator(ip: str) -> Optional[str]:
|
||||
if multi:
|
||||
ips = ip.split(' ')
|
||||
else:
|
||||
ips = [ip]
|
||||
|
||||
try:
|
||||
for ip in ips:
|
||||
ipaddress.ip_interface(ip)
|
||||
return None
|
||||
except ValueError:
|
||||
return str(_('You need to enter a valid IP in IP-config mode'))
|
||||
|
||||
result = EditMenu(
|
||||
title,
|
||||
header=header,
|
||||
validator=validator,
|
||||
allow_skip=allow_skip,
|
||||
default_text=preset
|
||||
).input()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return result.text()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
def _edit_iface(self, edit_nic: Nic) -> Nic:
|
||||
iface_name = edit_nic.iface
|
||||
modes = ['DHCP (auto detect)', 'IP (static)']
|
||||
default_mode = 'DHCP (auto detect)'
|
||||
|
||||
prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode)
|
||||
mode = Menu(prompt, modes, default_option=default_mode, skip=False).run()
|
||||
header = str(_('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode)) + '\n'
|
||||
items = [MenuItem(m, value=m) for m in modes]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_default_by_value(default_mode)
|
||||
|
||||
if mode.value == 'IP (static)':
|
||||
while 1:
|
||||
prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name)
|
||||
ip = TextInput(prompt, edit_nic.ip).run().strip()
|
||||
# Implemented new check for correct IP/subnet input
|
||||
try:
|
||||
ipaddress.ip_interface(ip)
|
||||
break
|
||||
except ValueError:
|
||||
warn("You need to enter a valid IP in IP-config mode")
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Modes')))
|
||||
).run()
|
||||
|
||||
# Implemented new check for correct gateway IP address
|
||||
gateway = None
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
mode = result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
while 1:
|
||||
gateway = TextInput(
|
||||
_('Enter your gateway (router) IP address or leave blank for none: '),
|
||||
edit_nic.gateway
|
||||
).run().strip()
|
||||
try:
|
||||
if len(gateway) > 0:
|
||||
ipaddress.ip_address(gateway)
|
||||
break
|
||||
except ValueError:
|
||||
warn("You need to enter a valid gateway (router) IP address")
|
||||
if mode == 'IP (static)':
|
||||
header = str(_('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name)) + '\n'
|
||||
ip = self._get_ip_address(str(_('IP address')), header, False, False)
|
||||
|
||||
header = str(_('Enter your gateway (router) IP address (leave blank for none)')) + '\n'
|
||||
gateway = self._get_ip_address(str(_('Gateway address')), header, True, False)
|
||||
|
||||
if edit_nic.dns:
|
||||
display_dns = ' '.join(edit_nic.dns)
|
||||
else:
|
||||
display_dns = None
|
||||
dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip()
|
||||
|
||||
header = str(_('Enter your DNS servers with space separated (leave blank for none)')) + '\n'
|
||||
dns_servers = self._get_ip_address(
|
||||
str(_('DNS servers')),
|
||||
header,
|
||||
True,
|
||||
True,
|
||||
display_dns
|
||||
)
|
||||
|
||||
dns = []
|
||||
if len(dns_input):
|
||||
dns = dns_input.split(' ')
|
||||
if dns_servers is not None:
|
||||
dns = dns_servers.split(' ')
|
||||
|
||||
return Nic(iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False)
|
||||
else:
|
||||
|
|
@ -128,35 +168,40 @@ class ManualNetworkConfig(ListManager):
|
|||
|
||||
def ask_to_configure_network(preset: Optional[NetworkConfiguration]) -> Optional[NetworkConfiguration]:
|
||||
"""
|
||||
Configure the network on the newly installed system
|
||||
Configure the network on the newly installed system
|
||||
"""
|
||||
options = {n.display_msg(): n for n in NicType}
|
||||
preset_val = preset.type.display_msg() if preset else None
|
||||
warning = str(_('Are you sure you want to reset this setting?'))
|
||||
|
||||
choice = Menu(
|
||||
_('Select one network interface to configure'),
|
||||
list(options.keys()),
|
||||
preset_values=preset_val,
|
||||
sort=False,
|
||||
items = [MenuItem(n.display_msg(), value=n) for n in NicType]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
|
||||
if preset:
|
||||
group.set_selected_by_value(preset.type)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Network configuration'))),
|
||||
allow_reset=True,
|
||||
allow_reset_warning_msg=warning
|
||||
allow_skip=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Reset: return None
|
||||
case MenuSelectionType.Selection:
|
||||
nic_type = options[choice.single_value]
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return None
|
||||
case ResultType.Selection:
|
||||
config = result.get_value()
|
||||
|
||||
match nic_type:
|
||||
match config:
|
||||
case NicType.ISO:
|
||||
return NetworkConfiguration(NicType.ISO)
|
||||
case NicType.NM:
|
||||
return NetworkConfiguration(NicType.NM)
|
||||
case NicType.MANUAL:
|
||||
preset_nics = preset.nics if preset else []
|
||||
nics = ManualNetworkConfig('Configure interfaces', preset_nics).run()
|
||||
nics = ManualNetworkConfig(str(_('Configure interfaces')), preset_nics).run()
|
||||
|
||||
if nics:
|
||||
return NetworkConfiguration(NicType.MANUAL, nics)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,14 @@ from __future__ import annotations
|
|||
from typing import List, Any, TYPE_CHECKING, Optional
|
||||
|
||||
from ..hardware import SysInfo, GfxDriver
|
||||
from ..menu import MenuSelectionType, Menu
|
||||
from ..models.bootloader import Bootloader
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, FrameStyle, Alignment,
|
||||
ResultType, Orientation, PreviewStyle
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
|
@ -17,71 +22,88 @@ def select_kernel(preset: List[str] = []) -> List[str]:
|
|||
:return: The string as a selected kernel
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"]
|
||||
default_kernel = "linux"
|
||||
|
||||
warning = str(_('Are you sure you want to reset this setting?'))
|
||||
items = [MenuItem(k, value=k) for k in kernels]
|
||||
|
||||
choice = Menu(
|
||||
_('Choose which kernels to use or leave blank for default "{}"').format(default_kernel),
|
||||
kernels,
|
||||
sort=True,
|
||||
multi=True,
|
||||
preset_values=preset,
|
||||
allow_reset_warning_msg=warning
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_default_by_value(default_kernel)
|
||||
group.set_focus_by_value(default_kernel)
|
||||
group.set_selected_by_value(preset)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Kernel'))),
|
||||
multi=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection: return choice.single_value
|
||||
|
||||
return []
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return []
|
||||
case ResultType.Selection:
|
||||
return result.get_values()
|
||||
|
||||
|
||||
def ask_for_bootloader(preset: Bootloader) -> Bootloader:
|
||||
def ask_for_bootloader(preset: Optional[Bootloader]) -> Optional[Bootloader]:
|
||||
# Systemd is UEFI only
|
||||
if not SysInfo.has_uefi():
|
||||
options = [Bootloader.Grub.value, Bootloader.Limine.value]
|
||||
default = Bootloader.Grub.value
|
||||
options = [Bootloader.Grub, Bootloader.Limine]
|
||||
default = Bootloader.Grub
|
||||
else:
|
||||
options = Bootloader.values()
|
||||
default = Bootloader.Systemd.value
|
||||
options = [b for b in Bootloader]
|
||||
default = Bootloader.Systemd
|
||||
|
||||
preset_value = preset.value if preset else None
|
||||
items = [MenuItem(o.value, value=o) for o in options]
|
||||
group = MenuItemGroup(items)
|
||||
group.set_default_by_value(default)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
choice = Menu(
|
||||
_('Choose a bootloader'),
|
||||
options,
|
||||
preset_values=preset_value,
|
||||
sort=False,
|
||||
default_option=default
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Bootloader'))),
|
||||
allow_skip=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection: return Bootloader(choice.value)
|
||||
|
||||
return preset
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
def ask_for_uki(preset: bool = True) -> bool:
|
||||
if preset:
|
||||
preset_val = Menu.yes()
|
||||
else:
|
||||
preset_val = Menu.no()
|
||||
prompt = str(_('Would you like to use unified kernel images?')) + '\n'
|
||||
|
||||
prompt = _('Would you like to use unified kernel images?')
|
||||
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), preset_values=preset_val).run()
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True
|
||||
).run()
|
||||
|
||||
return preset
|
||||
match result.type_:
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Selection:
|
||||
return result.item() == MenuItem.yes()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriver] = None) -> Optional[GfxDriver]:
|
||||
def select_driver(options: List[GfxDriver] = [], preset: Optional[GfxDriver] = None) -> Optional[GfxDriver]:
|
||||
"""
|
||||
Some what convoluted function, whose job is simple.
|
||||
Select a graphics driver from a pre-defined set of popular options.
|
||||
|
|
@ -92,47 +114,65 @@ def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriv
|
|||
if not options:
|
||||
options = [driver for driver in GfxDriver]
|
||||
|
||||
drivers = sorted([o.value for o in options])
|
||||
items = [MenuItem(o.value, value=o, preview_action=lambda x: x.value.packages_text()) for o in options]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_default_by_value(GfxDriver.AllOpenSource)
|
||||
|
||||
if drivers:
|
||||
title = ''
|
||||
if SysInfo.has_amd_graphics():
|
||||
title += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n'
|
||||
if SysInfo.has_intel_graphics():
|
||||
title += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n'))
|
||||
if SysInfo.has_nvidia_graphics():
|
||||
title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n'))
|
||||
if preset is not None:
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
preset = current_value.value if current_value else None
|
||||
header = ''
|
||||
if SysInfo.has_amd_graphics():
|
||||
header += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n'
|
||||
if SysInfo.has_intel_graphics():
|
||||
header += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n'))
|
||||
if SysInfo.has_nvidia_graphics():
|
||||
header += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n'))
|
||||
|
||||
choice = Menu(
|
||||
title,
|
||||
drivers,
|
||||
preset_values=preset,
|
||||
default_option=GfxDriver.AllOpenSource.value,
|
||||
preview_command=lambda x: GfxDriver(x).packages_text(),
|
||||
preview_size=0.3
|
||||
).run()
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
preview_size='auto',
|
||||
preview_style=PreviewStyle.BOTTOM,
|
||||
preview_frame=FrameProperties(str(_('Info')), h_frame_style=FrameStyle.MIN)
|
||||
).run()
|
||||
|
||||
if choice.type_ != MenuSelectionType.Selection:
|
||||
return current_value
|
||||
|
||||
return GfxDriver(choice.single_value)
|
||||
|
||||
return current_value
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return None
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
|
||||
|
||||
def ask_for_swap(preset: bool = True) -> bool:
|
||||
if preset:
|
||||
preset_val = Menu.yes()
|
||||
default_item = MenuItem.yes()
|
||||
else:
|
||||
preset_val = Menu.no()
|
||||
default_item = MenuItem.no()
|
||||
|
||||
prompt = _('Would you like to use swap on zram?')
|
||||
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run()
|
||||
prompt = str(_('Would you like to use swap on zram?')) + '\n'
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(default_item)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip: return preset
|
||||
case ResultType.Selection:
|
||||
return result.item() == MenuItem.yes()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
return preset
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Dict, Any, TYPE_CHECKING, Optional
|
||||
from typing import Dict, Any, TYPE_CHECKING, Optional, List
|
||||
|
||||
from .utils import list_keyboard_languages, list_locales, set_kb_layout, get_kb_layout
|
||||
from ..menu import Selector, AbstractSubMenu, MenuSelectionType, Menu
|
||||
from ..menu import AbstractSubMenu
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -28,6 +33,12 @@ class LocaleConfiguration:
|
|||
'sys_enc': self.sys_enc
|
||||
}
|
||||
|
||||
def preview(self) -> str:
|
||||
output = '{}: {}\n'.format(str(_('Keyboard layout')), self.kb_layout)
|
||||
output += '{}: {}\n'.format(str(_('Locale language')), self.sys_lang)
|
||||
output += '{}: {}'.format(str(_('Locale encoding')), self.sys_enc)
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def _load_config(cls, config: 'LocaleConfiguration', args: Dict[str, Any]) -> 'LocaleConfiguration':
|
||||
if 'sys_lang' in args:
|
||||
|
|
@ -54,34 +65,50 @@ class LocaleConfiguration:
|
|||
class LocaleMenu(AbstractSubMenu):
|
||||
def __init__(
|
||||
self,
|
||||
data_store: Dict[str, Any],
|
||||
locale_conf: LocaleConfiguration
|
||||
):
|
||||
self._preset = locale_conf
|
||||
super().__init__(data_store=data_store)
|
||||
self._locale_conf = locale_conf
|
||||
self._data_store: Dict[str, Any] = {}
|
||||
menu_optioons = self._define_menu_options()
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
self._menu_options['keyboard-layout'] = \
|
||||
Selector(
|
||||
_('Keyboard layout'),
|
||||
lambda preset: self._select_kb_layout(preset),
|
||||
default=self._preset.kb_layout,
|
||||
enabled=True)
|
||||
self._menu_options['sys-language'] = \
|
||||
Selector(
|
||||
_('Locale language'),
|
||||
lambda preset: select_locale_lang(preset),
|
||||
default=self._preset.sys_lang,
|
||||
enabled=True)
|
||||
self._menu_options['sys-encoding'] = \
|
||||
Selector(
|
||||
_('Locale encoding'),
|
||||
lambda preset: select_locale_enc(preset),
|
||||
default=self._preset.sys_enc,
|
||||
enabled=True)
|
||||
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
|
||||
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
|
||||
|
||||
def run(self, allow_reset: bool = True) -> LocaleConfiguration:
|
||||
super().run(allow_reset=allow_reset)
|
||||
def _define_menu_options(self) -> List[MenuItem]:
|
||||
return [
|
||||
MenuItem(
|
||||
text=str(_('Keyboard layout')),
|
||||
action=lambda x: self._select_kb_layout(x),
|
||||
value=self._locale_conf.kb_layout,
|
||||
preview_action=self._prev_locale,
|
||||
key='keyboard-layout'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Locale language')),
|
||||
action=lambda x: select_locale_lang(x),
|
||||
value=self._locale_conf.sys_lang,
|
||||
preview_action=self._prev_locale,
|
||||
key='sys-language'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Locale encoding')),
|
||||
action=lambda x: select_locale_enc(x),
|
||||
value=self._locale_conf.sys_enc,
|
||||
preview_action=self._prev_locale,
|
||||
key='sys-encoding'
|
||||
)
|
||||
]
|
||||
|
||||
def _prev_locale(self, item: MenuItem) -> Optional[str]:
|
||||
temp_locale = LocaleConfiguration(
|
||||
self._menu_item_group.find_by_key('keyboard-layout').get_value(),
|
||||
self._menu_item_group.find_by_key('sys-language').get_value(),
|
||||
self._menu_item_group.find_by_key('sys-encoding').get_value(),
|
||||
)
|
||||
return temp_locale.preview()
|
||||
|
||||
def run(self) -> LocaleConfiguration:
|
||||
super().run()
|
||||
|
||||
if not self._data_store:
|
||||
return LocaleConfiguration.default()
|
||||
|
|
@ -103,59 +130,79 @@ def select_locale_lang(preset: Optional[str] = None) -> Optional[str]:
|
|||
locales = list_locales()
|
||||
locale_lang = set([locale.split()[0] for locale in locales])
|
||||
|
||||
choice = Menu(
|
||||
_('Choose which locale language to use'),
|
||||
list(locale_lang),
|
||||
sort=True,
|
||||
preset_values=preset
|
||||
items = [MenuItem(ll, value=ll) for ll in locale_lang]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Locale language'))),
|
||||
allow_skip=True,
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Selection: return choice.single_value
|
||||
case MenuSelectionType.Skip: return preset
|
||||
|
||||
return None
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
|
||||
def select_locale_enc(preset: Optional[str] = None) -> Optional[str]:
|
||||
locales = list_locales()
|
||||
locale_enc = set([locale.split()[1] for locale in locales])
|
||||
|
||||
choice = Menu(
|
||||
_('Choose which locale encoding to use'),
|
||||
list(locale_enc),
|
||||
sort=True,
|
||||
preset_values=preset
|
||||
items = [MenuItem(le, value=le) for le in locale_enc]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Locale encoding'))),
|
||||
allow_skip=True,
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Selection: return choice.single_value
|
||||
case MenuSelectionType.Skip: return preset
|
||||
|
||||
return None
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
|
||||
def select_kb_layout(preset: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Asks the user to select a language
|
||||
Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
|
||||
Select keyboard layout
|
||||
|
||||
:return: The language/dictionary key of the selected language
|
||||
:return: The keyboard layout shortcut for the selected layout
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
kb_lang = list_keyboard_languages()
|
||||
# sort alphabetically and then by length
|
||||
sorted_kb_lang = sorted(kb_lang, key=lambda x: (len(x), x))
|
||||
|
||||
choice = Menu(
|
||||
_('Select keyboard layout'),
|
||||
sorted_kb_lang,
|
||||
preset_values=preset,
|
||||
sort=False
|
||||
items = [MenuItem(lang, value=lang) for lang in sorted_kb_lang]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Keyboard layout'))),
|
||||
allow_skip=True,
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip: return preset
|
||||
case MenuSelectionType.Selection: return choice.single_value
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,9 +1,2 @@
|
|||
from .abstract_menu import Selector, AbstractMenu, AbstractSubMenu
|
||||
from .abstract_menu import AbstractMenu, AbstractSubMenu
|
||||
from .list_manager import ListManager
|
||||
from .menu import (
|
||||
MenuSelectionType,
|
||||
MenuSelection,
|
||||
Menu,
|
||||
)
|
||||
from .table_selection_menu import TableMenu
|
||||
from .text_input import TextInput
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING
|
||||
from typing import Callable, Any, List, Optional, Dict, TYPE_CHECKING
|
||||
|
||||
from .menu import Menu, MenuSelectionType
|
||||
from ..output import error
|
||||
from ..output import unicode_ljust
|
||||
from ..translationhandler import TranslationHandler, Language
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
PreviewStyle, FrameProperties, FrameStyle,
|
||||
ResultType, Chars, Tui
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -144,41 +147,21 @@ class Selector:
|
|||
class AbstractMenu:
|
||||
def __init__(
|
||||
self,
|
||||
data_store: Dict[str, Any] = {},
|
||||
auto_cursor: bool = False,
|
||||
preview_size: float = 0.2
|
||||
item_group: MenuItemGroup,
|
||||
data_store: Dict[str, Any],
|
||||
auto_cursor: bool = True,
|
||||
allow_reset: bool = False,
|
||||
reset_warning: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Create a new selection menu.
|
||||
|
||||
:param data_store: Area (Dict) where the resulting data will be held. At least an entry for each option. Default area is self._data_store (not preset in the call, due to circular references
|
||||
:type data_store: Dict
|
||||
|
||||
:param auto_cursor: Boolean which determines if the cursor stays on the first item (false) or steps each invocation of a selection entry (true)
|
||||
:type auto_cursor: bool
|
||||
|
||||
:param preview_size. Size in fractions of screen size of the preview window
|
||||
;type preview_size: float (range 0..1)
|
||||
|
||||
"""
|
||||
self._enabled_order: List[str] = []
|
||||
self._translation_handler = TranslationHandler()
|
||||
self.is_context_mgr = False
|
||||
self._menu_item_group = item_group
|
||||
self._data_store = data_store
|
||||
self.auto_cursor = auto_cursor
|
||||
self._menu_options: Dict[str, Selector] = {}
|
||||
self.preview_size = preview_size
|
||||
self._last_choice = None
|
||||
self._allow_reset = allow_reset
|
||||
self._reset_warning = reset_warning
|
||||
|
||||
self.setup_selection_menu_options()
|
||||
self._sync_all()
|
||||
self._populate_default_values()
|
||||
self.is_context_mgr = False
|
||||
|
||||
self.defined_text = str(_('Defined'))
|
||||
|
||||
@property
|
||||
def last_choice(self):
|
||||
return self._last_choice
|
||||
self._sync_all_from_ds()
|
||||
|
||||
def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu:
|
||||
self.is_context_mgr = True
|
||||
|
|
@ -189,263 +172,86 @@ class AbstractMenu:
|
|||
# TODO: skip processing when it comes from a planified exit
|
||||
if len(args) >= 2 and args[1]:
|
||||
error(args[1])
|
||||
print(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")
|
||||
Tui.print("Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")
|
||||
raise args[1]
|
||||
|
||||
for key in self._menu_options:
|
||||
selector = self._menu_options[key]
|
||||
if key and key not in self._data_store:
|
||||
self._data_store[key] = selector.current_selection
|
||||
self._sync_all_to_ds()
|
||||
|
||||
self.exit_callback()
|
||||
def _sync_all_from_ds(self) -> None:
|
||||
for item in self._menu_item_group.menu_items:
|
||||
if item.key is not None:
|
||||
if (store_value := self._data_store.get(item.key, None)) is not None:
|
||||
item.value = store_value
|
||||
|
||||
@property
|
||||
def translation_handler(self) -> TranslationHandler:
|
||||
return self._translation_handler
|
||||
def _sync_all_to_ds(self) -> None:
|
||||
for item in self._menu_item_group.menu_items:
|
||||
if item.key:
|
||||
self._data_store[item.key] = item.value
|
||||
|
||||
def _populate_default_values(self) -> None:
|
||||
for config_key, selector in self._menu_options.items():
|
||||
if selector.default is not None and config_key not in self._data_store:
|
||||
self._data_store[config_key] = selector.default
|
||||
def _sync(self, item: MenuItem) -> None:
|
||||
if not item.key:
|
||||
return
|
||||
|
||||
def _sync_all(self) -> None:
|
||||
for key in self._menu_options.keys():
|
||||
self._sync(key)
|
||||
store_value = self._data_store.get(item.key, None)
|
||||
|
||||
def _sync(self, selector_name: str) -> None:
|
||||
value = self._data_store.get(selector_name, None)
|
||||
selector = self._menu_options.get(selector_name, None)
|
||||
if store_value is not None:
|
||||
item.value = store_value
|
||||
elif item.value is not None:
|
||||
self._data_store[item.key] = item.value
|
||||
|
||||
if value is not None:
|
||||
self._menu_options[selector_name].set_current_selection(value)
|
||||
elif selector is not None and selector.has_selection():
|
||||
self._data_store[selector_name] = selector.current_selection
|
||||
def set_enabled(self, key: str, enabled: bool) -> None:
|
||||
if (item := self._menu_item_group.find_by_key(key)) is not None:
|
||||
item.enabled = enabled
|
||||
return None
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
""" Define the menu options.
|
||||
Menu options can be defined here in a subclass or done per program calling self.set_option()
|
||||
"""
|
||||
return
|
||||
raise ValueError(f'No selector found: {key}')
|
||||
|
||||
def pre_callback(self, selector_name) -> None:
|
||||
""" will be called before each action in the menu """
|
||||
return
|
||||
def disable_all(self) -> None:
|
||||
for item in self._menu_item_group.items:
|
||||
item.enabled = False
|
||||
|
||||
def post_callback(self, selection_name: Optional[str] = None, value: Any = None):
|
||||
""" will be called after each action in the menu """
|
||||
return True
|
||||
|
||||
def exit_callback(self) -> None:
|
||||
""" will be called at the end of the processing of the menu """
|
||||
return
|
||||
|
||||
def _update_enabled_order(self, selector_name: str) -> None:
|
||||
self._enabled_order.append(selector_name)
|
||||
|
||||
def enable(self, selector_name: str, mandatory: bool = False) -> None:
|
||||
""" activates menu options """
|
||||
if self._menu_options.get(selector_name, None):
|
||||
self._menu_options[selector_name].set_enabled(True)
|
||||
self._update_enabled_order(selector_name)
|
||||
self._menu_options[selector_name].set_mandatory(mandatory)
|
||||
self._sync(selector_name)
|
||||
else:
|
||||
raise ValueError(f'No selector found: {selector_name}')
|
||||
|
||||
def _preview_display(self, selection_name: str) -> Optional[str]:
|
||||
config_name, selector = self._find_selection(selection_name)
|
||||
if preview := selector.preview_func:
|
||||
return preview()
|
||||
return None
|
||||
|
||||
def _get_menu_text_padding(self, entries: List[Selector]) -> int:
|
||||
return max([len(str(selection.description)) for selection in entries])
|
||||
|
||||
def _find_selection(self, selection_name: str) -> Tuple[str, Selector]:
|
||||
enabled_menus = self._menus_to_enable()
|
||||
padding = self._get_menu_text_padding(list(enabled_menus.values()))
|
||||
|
||||
option = []
|
||||
for k, v in self._menu_options.items():
|
||||
if v.menu_text(padding).strip() == selection_name.strip():
|
||||
option.append((k, v))
|
||||
|
||||
if len(option) != 1:
|
||||
raise ValueError(f'Selection not found: {selection_name}')
|
||||
config_name = option[0][0]
|
||||
selector = option[0][1]
|
||||
return config_name, selector
|
||||
|
||||
def run(self, allow_reset: bool = False):
|
||||
self._sync_all()
|
||||
self.post_callback()
|
||||
cursor_pos = None
|
||||
def run(self) -> Optional[Any]:
|
||||
self._sync_all_from_ds()
|
||||
|
||||
while True:
|
||||
enabled_menus = self._menus_to_enable()
|
||||
|
||||
padding = self._get_menu_text_padding(list(enabled_menus.values()))
|
||||
menu_options = [m.menu_text(padding) for m in enabled_menus.values()]
|
||||
|
||||
warning_msg = str(_('All settings will be reset, are you sure?'))
|
||||
|
||||
selection = Menu(
|
||||
_('Set/Modify the below options'),
|
||||
menu_options,
|
||||
sort=False,
|
||||
cursor_index=cursor_pos,
|
||||
preview_command=self._preview_display,
|
||||
preview_size=self.preview_size,
|
||||
skip_empty_entries=True,
|
||||
skip=False,
|
||||
allow_reset=allow_reset,
|
||||
allow_reset_warning_msg=warning_msg
|
||||
result = SelectMenu(
|
||||
self._menu_item_group,
|
||||
allow_skip=False,
|
||||
allow_reset=self._allow_reset,
|
||||
reset_warning_msg=self._reset_warning,
|
||||
preview_style=PreviewStyle.RIGHT,
|
||||
preview_size='auto',
|
||||
preview_frame=FrameProperties('Info', FrameStyle.MAX),
|
||||
).run()
|
||||
|
||||
match selection.type_:
|
||||
case MenuSelectionType.Reset:
|
||||
self._data_store = {}
|
||||
return
|
||||
case MenuSelectionType.Selection:
|
||||
value: str = selection.value # type: ignore
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
item: MenuItem = result.item()
|
||||
|
||||
if self.auto_cursor:
|
||||
cursor_pos = menu_options.index(value) + 1 # before the strip otherwise fails
|
||||
|
||||
# in case the new position lands on a "placeholder" we'll skip them as well
|
||||
while True:
|
||||
if cursor_pos >= len(menu_options):
|
||||
cursor_pos = 0
|
||||
if len(menu_options[cursor_pos]) > 0:
|
||||
break
|
||||
cursor_pos += 1
|
||||
|
||||
value = value.strip()
|
||||
|
||||
# if this calls returns false, we exit the menu
|
||||
# we allow for an callback for special processing on releasing control
|
||||
if not self._process_selection(value):
|
||||
if item.action is None:
|
||||
break
|
||||
case ResultType.Reset:
|
||||
self._data_store = {}
|
||||
return None
|
||||
|
||||
# we get the last action key
|
||||
actions = {str(v.description): k for k, v in self._menu_options.items()}
|
||||
self._last_choice = actions[selection.value.strip()] # type: ignore
|
||||
|
||||
if not self.is_context_mgr:
|
||||
self.__exit__()
|
||||
|
||||
def _process_selection(self, selection_name: str) -> bool:
|
||||
""" determines and executes the selection y
|
||||
Can / Should be extended to handle specific selection issues
|
||||
Returns true if the menu shall continue, False if it has ended
|
||||
"""
|
||||
# find the selected option in our option list
|
||||
config_name, selector = self._find_selection(selection_name)
|
||||
return self.exec_option(config_name, selector)
|
||||
|
||||
def exec_option(self, config_name: str, p_selector: Optional[Selector] = None) -> bool:
|
||||
""" processes the execution of a given menu entry
|
||||
- pre process callback
|
||||
- selection function
|
||||
- post process callback
|
||||
- exec action
|
||||
returns True if the loop has to continue, false if the loop can be closed
|
||||
"""
|
||||
if not p_selector:
|
||||
selector = self.option(config_name)
|
||||
else:
|
||||
selector = p_selector
|
||||
|
||||
self.pre_callback(config_name)
|
||||
|
||||
result = None
|
||||
|
||||
if selector.func is not None:
|
||||
cur_value = self.option(config_name).get_selection()
|
||||
result = selector.func(cur_value)
|
||||
self._menu_options[config_name].set_current_selection(result)
|
||||
|
||||
if selector.do_store():
|
||||
self._data_store[config_name] = result
|
||||
|
||||
exec_ret_val = selector.exec_func(config_name, result) if selector.exec_func else False
|
||||
|
||||
self.post_callback(config_name, result)
|
||||
|
||||
if exec_ret_val:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _verify_selection_enabled(self, selection_name: str) -> bool:
|
||||
if selection := self._menu_options.get(selection_name, None):
|
||||
if not selection.enabled:
|
||||
return False
|
||||
|
||||
if len(selection.dependencies) > 0:
|
||||
for dep in selection.dependencies:
|
||||
if isinstance(dep, str):
|
||||
if not self._verify_selection_enabled(dep) or self._menu_options[dep].is_empty():
|
||||
return False
|
||||
elif callable(dep): # callable dependency eval
|
||||
return dep()
|
||||
else:
|
||||
raise ValueError(f'Unsupported dependency: {selection_name}')
|
||||
|
||||
if len(selection.dependencies_not) > 0:
|
||||
for dep in selection.dependencies_not:
|
||||
if not self._menu_options[dep].is_empty():
|
||||
return False
|
||||
return True
|
||||
|
||||
raise ValueError(f'No selection found: {selection_name}')
|
||||
|
||||
def _menus_to_enable(self) -> dict:
|
||||
""" general """
|
||||
enabled_menus = {}
|
||||
|
||||
for name, selection in self._menu_options.items():
|
||||
if self._verify_selection_enabled(name):
|
||||
enabled_menus[name] = selection
|
||||
|
||||
# sort the enabled menu by the order we enabled them in
|
||||
# we'll add the entries that have been enabled via the selector constructor at the top
|
||||
enabled_keys = [i for i in enabled_menus.keys() if i not in self._enabled_order]
|
||||
# and then we add the ones explicitly enabled by the enable function
|
||||
enabled_keys += [i for i in self._enabled_order if i in enabled_menus.keys()]
|
||||
|
||||
ordered_menus = {k: enabled_menus[k] for k in enabled_keys}
|
||||
|
||||
return ordered_menus
|
||||
|
||||
def option(self, name: str) -> Selector:
|
||||
# TODO check inexistent name
|
||||
return self._menu_options[name]
|
||||
|
||||
def list_enabled_options(self) -> Iterator:
|
||||
""" Iterator to retrieve the enabled menu options at a given time.
|
||||
The results are dynamic (if between calls to the iterator some elements -still not retrieved- are (de)activated
|
||||
"""
|
||||
for item in self._menu_options:
|
||||
if item in self._menus_to_enable():
|
||||
yield item
|
||||
|
||||
def _select_archinstall_language(self, preset: Language) -> Language:
|
||||
from ..interactions.general_conf import select_archinstall_language
|
||||
language = select_archinstall_language(self.translation_handler.translated_languages, preset)
|
||||
self._translation_handler.activate(language)
|
||||
return language
|
||||
self._sync_all_to_ds()
|
||||
return None
|
||||
|
||||
|
||||
class AbstractSubMenu(AbstractMenu):
|
||||
def __init__(self, data_store: Dict[str, Any] = {}, preview_size: float = 0.2):
|
||||
super().__init__(data_store=data_store, preview_size=preview_size)
|
||||
def __init__(
|
||||
self,
|
||||
item_group: MenuItemGroup,
|
||||
data_store: Dict[str, Any],
|
||||
auto_cursor: bool = True,
|
||||
allow_reset: bool = False
|
||||
):
|
||||
back_text = f'{Chars.Right_arrow} ' + str(_('Back'))
|
||||
item_group.menu_items.append(MenuItem(text=back_text))
|
||||
|
||||
self._menu_options['__separator__'] = Selector('')
|
||||
self._menu_options['back'] = \
|
||||
Selector(
|
||||
Menu.back(),
|
||||
no_store=True,
|
||||
enabled=True,
|
||||
exec_func=lambda n, v: True,
|
||||
)
|
||||
super().__init__(
|
||||
item_group,
|
||||
data_store=data_store,
|
||||
auto_cursor=auto_cursor,
|
||||
allow_reset=allow_reset
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import copy
|
||||
from os import system
|
||||
from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List
|
||||
|
||||
from .menu import Menu
|
||||
from ..output import FormattedOutput
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
Alignment, ResultType
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
|
@ -63,31 +65,34 @@ class ListManager:
|
|||
data_formatted = self.reformat(self._data)
|
||||
options, header = self._prepare_selection(data_formatted)
|
||||
|
||||
system('clear')
|
||||
items = [MenuItem(o, value=o) for o in options]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
|
||||
choice = Menu(
|
||||
self._prompt,
|
||||
options,
|
||||
sort=False,
|
||||
clear_screen=False,
|
||||
clear_menu_on_exit=False,
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
skip_empty_entries=True,
|
||||
skip=False,
|
||||
show_search_hint=False
|
||||
search_enabled=False,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
|
||||
if choice.value in self._base_actions:
|
||||
self._data = self.handle_action(choice.value, None, self._data)
|
||||
elif choice.value in self._terminate_actions:
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
value = result.get_value()
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
if value in self._base_actions:
|
||||
self._data = self.handle_action(value, None, self._data)
|
||||
elif value in self._terminate_actions:
|
||||
break
|
||||
else: # an entry of the existing selection was chosen
|
||||
selected_entry = data_formatted[choice.value] # type: ignore
|
||||
selected_entry = result.get_value()
|
||||
self._run_actions_on_entry(selected_entry)
|
||||
|
||||
self._last_choice = choice.value # type: ignore
|
||||
self._last_choice = value
|
||||
|
||||
if choice.value == self._cancel_action:
|
||||
if result.get_value() == self._cancel_action:
|
||||
return self._original_data # return the original list
|
||||
else:
|
||||
return self._data
|
||||
|
|
@ -110,23 +115,30 @@ class ListManager:
|
|||
|
||||
return options, header
|
||||
|
||||
def _run_actions_on_entry(self, entry: Any):
|
||||
def _run_actions_on_entry(self, entry: Any) -> None:
|
||||
options = self.filter_options(entry, self._sub_menu_actions) + [self._cancel_action]
|
||||
display_value = self.selected_action_display(entry)
|
||||
|
||||
prompt = _("Select an action for '{}'").format(display_value)
|
||||
items = [MenuItem(o, value=o) for o in options]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
|
||||
choice = Menu(
|
||||
prompt,
|
||||
options,
|
||||
sort=False,
|
||||
clear_screen=False,
|
||||
clear_menu_on_exit=False,
|
||||
show_search_hint=False
|
||||
header = f'{self.selected_action_display(entry)}\n'
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
search_enabled=False,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER
|
||||
).run()
|
||||
|
||||
if choice.value and choice.value != self._cancel_action:
|
||||
self._data = self.handle_action(choice.value, entry, self._data)
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
value = result.get_value()
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
if value != self._cancel_action:
|
||||
self._data = self.handle_action(value, entry, self._data)
|
||||
|
||||
def reformat(self, data: List[Any]) -> Dict[str, Optional[Any]]:
|
||||
"""
|
||||
|
|
@ -139,10 +151,9 @@ class ListManager:
|
|||
# these are the header rows of the table and do not map to any User obviously
|
||||
# we're adding 2 spaces as prefix because the menu selector '> ' will be put before
|
||||
# the selectable rows so the header has to be aligned
|
||||
display_data: Dict[str, Optional[Any]] = {f' {rows[0]}': None, f' {rows[1]}': None}
|
||||
display_data: Dict[str, Optional[Any]] = {f'{rows[0]}': None, f'{rows[1]}': None}
|
||||
|
||||
for row, entry in zip(rows[2:], data):
|
||||
row = row.replace('|', '\\|')
|
||||
display_data[row] = entry
|
||||
|
||||
return display_data
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,16 +1,24 @@
|
|||
import time
|
||||
import json
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, List, Optional, TYPE_CHECKING
|
||||
from typing import Dict, Any, List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from .menu import AbstractSubMenu, Selector, MenuSelectionType, Menu, ListManager, TextInput
|
||||
from .menu import AbstractSubMenu, ListManager
|
||||
from .networking import fetch_data_from_url
|
||||
from .output import FormattedOutput, debug
|
||||
from .storage import storage
|
||||
from .models.mirrors import MirrorStatusListV3, MirrorStatusEntryV3
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType,
|
||||
EditMenu
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
|
@ -67,7 +75,7 @@ class CustomMirror:
|
|||
|
||||
@dataclass
|
||||
class MirrorConfiguration:
|
||||
mirror_regions: Dict[str, List[str]] = field(default_factory=dict)
|
||||
mirror_regions: Dict[str, List[MirrorStatusEntryV3]] = field(default_factory=dict)
|
||||
custom_mirrors: List[CustomMirror] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
|
|
@ -85,7 +93,7 @@ class MirrorConfiguration:
|
|||
|
||||
for region, mirrors in self.mirror_regions.items():
|
||||
for mirror in mirrors:
|
||||
config += f'\n\n## {region}\nServer = {mirror}\n'
|
||||
config += f'\n\n## {region}\nServer = {mirror.url}$repo/os/$arch\n'
|
||||
|
||||
for cm in self.custom_mirrors:
|
||||
config += f'\n\n## {cm.name}\nServer = {cm.url}\n'
|
||||
|
|
@ -116,13 +124,18 @@ class MirrorConfiguration:
|
|||
|
||||
|
||||
class CustomMirrorList(ListManager):
|
||||
def __init__(self, prompt: str, custom_mirrors: List[CustomMirror]):
|
||||
def __init__(self, custom_mirrors: List[CustomMirror]):
|
||||
self._actions = [
|
||||
str(_('Add a custom mirror')),
|
||||
str(_('Change custom mirror')),
|
||||
str(_('Delete custom mirror'))
|
||||
]
|
||||
super().__init__(prompt, custom_mirrors, [self._actions[0]], self._actions[1:])
|
||||
super().__init__(
|
||||
'',
|
||||
custom_mirrors,
|
||||
[self._actions[0]],
|
||||
self._actions[1:]
|
||||
)
|
||||
|
||||
def selected_action_display(self, mirror: CustomMirror) -> str:
|
||||
return mirror.name
|
||||
|
|
@ -148,164 +161,190 @@ class CustomMirrorList(ListManager):
|
|||
|
||||
return data
|
||||
|
||||
def _add_custom_mirror(self, mirror: Optional[CustomMirror] = None) -> Optional[CustomMirror]:
|
||||
prompt = '\n\n' + str(_('Enter name (leave blank to skip): '))
|
||||
existing_name = mirror.name if mirror else ''
|
||||
def _add_custom_mirror(self, preset: Optional[CustomMirror] = None) -> Optional[CustomMirror]:
|
||||
edit_result = EditMenu(
|
||||
str(_('Mirror name')),
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
default_text=preset.name if preset else None
|
||||
).input()
|
||||
|
||||
while True:
|
||||
name = TextInput(prompt, existing_name).run()
|
||||
if not name:
|
||||
return mirror
|
||||
break
|
||||
match edit_result.type_:
|
||||
case ResultType.Selection:
|
||||
name = edit_result.text()
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
prompt = '\n' + str(_('Enter url (leave blank to skip): '))
|
||||
existing_url = mirror.url if mirror else ''
|
||||
header = f'{str(_("Name"))}: {name}'
|
||||
|
||||
while True:
|
||||
url = TextInput(prompt, existing_url).run()
|
||||
if not url:
|
||||
return mirror
|
||||
break
|
||||
edit_result = EditMenu(
|
||||
str(_('Url')),
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
default_text=preset.url if preset else None
|
||||
).input()
|
||||
|
||||
sign_check_choice = Menu(
|
||||
str(_('Select signature check option')),
|
||||
[s.value for s in SignCheck],
|
||||
skip=False,
|
||||
clear_screen=False,
|
||||
preset_values=mirror.sign_check.value if mirror else None
|
||||
match edit_result.type_:
|
||||
case ResultType.Selection:
|
||||
url = edit_result.text()
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
header += f'\n{str(_("Url"))}: {url}\n'
|
||||
prompt = f'{header}\n' + str(_('Select signature check'))
|
||||
|
||||
sign_chk_items = [MenuItem(s.value, value=s.value) for s in SignCheck]
|
||||
group = MenuItemGroup(sign_chk_items, sort_items=False)
|
||||
|
||||
if preset is not None:
|
||||
group.set_selected_by_value(preset.sign_check.value)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
sign_option_choice = Menu(
|
||||
str(_('Select signature option')),
|
||||
[s.value for s in SignOption],
|
||||
skip=False,
|
||||
clear_screen=False,
|
||||
preset_values=mirror.sign_option.value if mirror else None
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
sign_check = SignCheck(result.get_value())
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
header += f'{str(_("Signature check"))}: {sign_check.value}\n'
|
||||
prompt = f'{header}\n' + 'Select signature option'
|
||||
|
||||
sign_opt_items = [MenuItem(s.value, value=s.value) for s in SignOption]
|
||||
group = MenuItemGroup(sign_opt_items, sort_items=False)
|
||||
|
||||
if preset is not None:
|
||||
group.set_selected_by_value(preset.sign_option.value)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=False
|
||||
).run()
|
||||
|
||||
return CustomMirror(
|
||||
name,
|
||||
url,
|
||||
SignCheck(sign_check_choice.single_value),
|
||||
SignOption(sign_option_choice.single_value)
|
||||
)
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
sign_opt = SignOption(result.get_value())
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
return CustomMirror(name, url, sign_check, sign_opt)
|
||||
|
||||
|
||||
class MirrorMenu(AbstractSubMenu):
|
||||
def __init__(
|
||||
self,
|
||||
data_store: Dict[str, Any],
|
||||
preset: Optional[MirrorConfiguration] = None
|
||||
):
|
||||
if preset:
|
||||
self._preset = preset
|
||||
self._mirror_config = preset
|
||||
else:
|
||||
self._preset = MirrorConfiguration()
|
||||
self._mirror_config = MirrorConfiguration()
|
||||
|
||||
super().__init__(data_store=data_store)
|
||||
self._data_store: Dict[str, Any] = {}
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
self._menu_options['mirror_regions'] = \
|
||||
Selector(
|
||||
_('Mirror region'),
|
||||
lambda preset: select_mirror_regions(preset),
|
||||
display_func=lambda x: ', '.join(x.keys()) if x else '',
|
||||
default=self._preset.mirror_regions,
|
||||
enabled=True)
|
||||
self._menu_options['custom_mirrors'] = \
|
||||
Selector(
|
||||
_('Custom mirrors'),
|
||||
lambda preset: select_custom_mirror(preset=preset),
|
||||
display_func=lambda x: str(_('Defined')) if x else '',
|
||||
preview_func=self._prev_custom_mirror,
|
||||
default=self._preset.custom_mirrors,
|
||||
enabled=True
|
||||
menu_optioons = self._define_menu_options()
|
||||
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
|
||||
|
||||
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
|
||||
|
||||
def _define_menu_options(self) -> List[MenuItem]:
|
||||
return [
|
||||
MenuItem(
|
||||
text=str(_('Mirror region')),
|
||||
action=lambda x: select_mirror_regions(x),
|
||||
value=self._mirror_config.mirror_regions,
|
||||
preview_action=self._prev_regions,
|
||||
key='mirror_regions'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Custom mirrors')),
|
||||
action=lambda x: select_custom_mirror(x),
|
||||
value=self._mirror_config.custom_mirrors,
|
||||
preview_action=self._prev_custom_mirror,
|
||||
key='custom_mirrors'
|
||||
)
|
||||
]
|
||||
|
||||
def _prev_custom_mirror(self) -> Optional[str]:
|
||||
selector = self._menu_options['custom_mirrors']
|
||||
def _prev_regions(self, item: MenuItem) -> Optional[str]:
|
||||
mirrors: Dict[str, List[MirrorStatusEntryV3]] = item.get_value()
|
||||
|
||||
if selector.has_selection():
|
||||
custom_mirrors: List[CustomMirror] = selector.current_selection # type: ignore
|
||||
output = FormattedOutput.as_table(custom_mirrors)
|
||||
return output.strip()
|
||||
output = ''
|
||||
for name, status_list in mirrors.items():
|
||||
output += f'{name}\n'
|
||||
output += '-' * len(name) + '\n'
|
||||
|
||||
return None
|
||||
for entry in status_list:
|
||||
output += f'{entry.url}\n'
|
||||
|
||||
def run(self, allow_reset: bool = True) -> Optional[MirrorConfiguration]:
|
||||
super().run(allow_reset=allow_reset)
|
||||
output += '\n'
|
||||
|
||||
if self._data_store.get('mirror_regions', None) or self._data_store.get('custom_mirrors', None):
|
||||
return MirrorConfiguration(
|
||||
mirror_regions=self._data_store['mirror_regions'],
|
||||
custom_mirrors=self._data_store['custom_mirrors'],
|
||||
)
|
||||
return output
|
||||
|
||||
return None
|
||||
def _prev_custom_mirror(self, item: MenuItem) -> Optional[str]:
|
||||
if not item.value:
|
||||
return None
|
||||
|
||||
custom_mirrors: List[CustomMirror] = item.value
|
||||
output = FormattedOutput.as_table(custom_mirrors)
|
||||
return output.strip()
|
||||
|
||||
def run(self) -> MirrorConfiguration:
|
||||
super().run()
|
||||
|
||||
if not self._data_store:
|
||||
return MirrorConfiguration()
|
||||
|
||||
return MirrorConfiguration(
|
||||
mirror_regions=self._data_store.get('mirror_regions', None),
|
||||
custom_mirrors=self._data_store.get('custom_mirrors', None),
|
||||
)
|
||||
|
||||
|
||||
def select_mirror_regions(preset_values: Dict[str, List[str]] = {}) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Asks the user to select a mirror or region
|
||||
Usually this is combined with :ref:`archinstall.list_mirrors`.
|
||||
def select_mirror_regions(preset: Dict[str, List[MirrorStatusEntryV3]]) -> Dict[str, List[MirrorStatusEntryV3]]:
|
||||
mirrors: Dict[str, List[MirrorStatusEntryV3]] | None = list_mirrors_from_remote()
|
||||
|
||||
:return: The dictionary information about a mirror/region.
|
||||
:rtype: dict
|
||||
"""
|
||||
if preset_values is None:
|
||||
preselected = None
|
||||
else:
|
||||
preselected = list(preset_values.keys())
|
||||
if not mirrors:
|
||||
mirrors = list_mirrors_from_local()
|
||||
|
||||
remote_mirrors = list_mirrors_from_remote()
|
||||
mirrors: Dict[str, list[str]] = {}
|
||||
items = [MenuItem(name, value=(name, mirrors)) for name, mirrors in mirrors.items()]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
|
||||
if remote_mirrors:
|
||||
choice = Menu(
|
||||
_('Select one of the regions to download packages from'),
|
||||
list(remote_mirrors.keys()),
|
||||
preset_values=preselected,
|
||||
multi=True,
|
||||
allow_reset=True
|
||||
).run()
|
||||
preset_values = [(name, mirror) for name, mirror in preset.items()]
|
||||
group.set_selected_by_value(preset_values)
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Reset:
|
||||
return {}
|
||||
case MenuSelectionType.Skip:
|
||||
return preset_values
|
||||
case MenuSelectionType.Selection:
|
||||
for region in choice.multi_value:
|
||||
mirrors.setdefault(region, [])
|
||||
for mirror in _sort_mirrors_by_performance(remote_mirrors[region]):
|
||||
mirrors[region].append(mirror.server_url)
|
||||
return mirrors
|
||||
else:
|
||||
local_mirrors = list_mirrors_from_local()
|
||||
result = SelectMenu(
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Mirror regions'))),
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
multi=True,
|
||||
).run()
|
||||
|
||||
choice = Menu(
|
||||
_('Select one of the regions to download packages from'),
|
||||
list(local_mirrors.keys()),
|
||||
preset_values=preselected,
|
||||
multi=True,
|
||||
allow_reset=True
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Reset:
|
||||
return {}
|
||||
case MenuSelectionType.Skip:
|
||||
return preset_values
|
||||
case MenuSelectionType.Selection:
|
||||
for region in choice.multi_value:
|
||||
mirrors[region] = local_mirrors[region]
|
||||
return mirrors
|
||||
|
||||
return mirrors
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return {}
|
||||
case ResultType.Selection:
|
||||
selected_mirrors: List[Tuple[str, List[MirrorStatusEntryV3]]] = result.get_values()
|
||||
return {name: mirror for name, mirror in selected_mirrors}
|
||||
|
||||
|
||||
def select_custom_mirror(prompt: str = '', preset: List[CustomMirror] = []) -> list[CustomMirror]:
|
||||
custom_mirrors = CustomMirrorList(prompt, preset).run()
|
||||
def select_custom_mirror(preset: List[CustomMirror] = []):
|
||||
custom_mirrors = CustomMirrorList(preset).run()
|
||||
return custom_mirrors
|
||||
|
||||
|
||||
|
|
@ -327,7 +366,7 @@ def list_mirrors_from_remote() -> Optional[Dict[str, List[MirrorStatusEntryV3]]]
|
|||
return None
|
||||
|
||||
|
||||
def list_mirrors_from_local() -> Dict[str, list[str]]:
|
||||
def list_mirrors_from_local() -> Dict[str, List[MirrorStatusEntryV3]]:
|
||||
with Path('/etc/pacman.d/mirrorlist').open('r') as fp:
|
||||
mirrorlist = fp.read()
|
||||
return _parse_locale_mirrors(mirrorlist)
|
||||
|
|
@ -370,13 +409,13 @@ def _parse_remote_mirror_list(mirrorlist: str) -> Dict[str, List[MirrorStatusEnt
|
|||
return sorted_by_regions
|
||||
|
||||
|
||||
def _parse_locale_mirrors(mirrorlist: str) -> Dict[str, List[str]]:
|
||||
def _parse_locale_mirrors(mirrorlist: str) -> Dict[str, List[MirrorStatusEntryV3]]:
|
||||
lines = mirrorlist.splitlines()
|
||||
|
||||
# remove empty lines
|
||||
lines = [line for line in lines if line]
|
||||
|
||||
mirror_list: Dict[str, List[str]] = {}
|
||||
mirror_list: Dict[str, List[MirrorStatusEntryV3]] = {}
|
||||
|
||||
current_region = ''
|
||||
for idx, line in enumerate(lines):
|
||||
|
|
@ -391,6 +430,20 @@ def _parse_locale_mirrors(mirrorlist: str) -> Dict[str, List[str]]:
|
|||
break
|
||||
|
||||
url = line.removeprefix('Server = ')
|
||||
mirror_list[current_region].append(url)
|
||||
mirror_entry = MirrorStatusEntryV3(
|
||||
url=url.rstrip('$repo/os/$arch'),
|
||||
protocol=urllib.parse.urlparse(url).scheme,
|
||||
active=True,
|
||||
country=current_region or 'Worldwide',
|
||||
# The following values are normally populated by
|
||||
# archlinux.org mirror-list endpoint, and can't be known
|
||||
# from just the local mirror-list file.
|
||||
country_code='WW',
|
||||
isos=True,
|
||||
ipv4=True,
|
||||
ipv6=True,
|
||||
details='Locally defined mirror',
|
||||
)
|
||||
mirror_list[current_region].append(mirror_entry)
|
||||
|
||||
return mirror_list
|
||||
|
|
|
|||
|
|
@ -12,13 +12,10 @@ if TYPE_CHECKING:
|
|||
|
||||
@dataclass
|
||||
class Audio(Enum):
|
||||
NoAudio = 'No audio server'
|
||||
Pipewire = 'pipewire'
|
||||
Pulseaudio = 'pulseaudio'
|
||||
|
||||
@staticmethod
|
||||
def no_audio_text() -> str:
|
||||
return str(_('No audio server'))
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioConfiguration:
|
||||
|
|
@ -47,8 +44,9 @@ class AudioConfiguration:
|
|||
case Audio.Pulseaudio:
|
||||
installation.add_additional_packages("pulseaudio")
|
||||
|
||||
if SysInfo.requires_sof_fw():
|
||||
installation.add_additional_packages('sof-firmware')
|
||||
if self.audio != Audio.NoAudio:
|
||||
if SysInfo.requires_sof_fw():
|
||||
installation.add_additional_packages('sof-firmware')
|
||||
|
||||
if SysInfo.requires_alsa_fw():
|
||||
installation.add_additional_packages('alsa-firmware')
|
||||
if SysInfo.requires_alsa_fw():
|
||||
installation.add_additional_packages('alsa-firmware')
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
from pydantic import BaseModel, field_validator, model_validator
|
||||
import datetime
|
||||
import pydantic
|
||||
import http.client
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import (
|
||||
Dict,
|
||||
List
|
||||
List,
|
||||
Optional
|
||||
)
|
||||
|
||||
from ..networking import ping, DownloadTimer
|
||||
from ..output import debug
|
||||
|
||||
|
||||
class MirrorStatusEntryV3(pydantic.BaseModel):
|
||||
class MirrorStatusEntryV3(BaseModel):
|
||||
url: str
|
||||
protocol: str
|
||||
active: bool
|
||||
|
|
@ -91,15 +92,15 @@ class MirrorStatusEntryV3(pydantic.BaseModel):
|
|||
|
||||
return self._latency
|
||||
|
||||
@pydantic.field_validator('score', mode='before')
|
||||
def validate_score(cls, value) -> int | None:
|
||||
@field_validator('score', mode='before')
|
||||
def validate_score(cls, value: int) -> Optional[int]:
|
||||
if value is not None:
|
||||
value = round(value)
|
||||
debug(f" score: {value}")
|
||||
|
||||
return value
|
||||
|
||||
@pydantic.model_validator(mode='after')
|
||||
@model_validator(mode='after')
|
||||
def debug_output(self, validation_info) -> 'MirrorStatusEntryV3':
|
||||
self._hostname, *_port = urllib.parse.urlparse(self.url).netloc.split(':', 1)
|
||||
self._port = int(_port[0]) if _port and len(_port) >= 1 else None
|
||||
|
|
@ -108,16 +109,19 @@ class MirrorStatusEntryV3(pydantic.BaseModel):
|
|||
return self
|
||||
|
||||
|
||||
class MirrorStatusListV3(pydantic.BaseModel):
|
||||
class MirrorStatusListV3(BaseModel):
|
||||
cutoff: int
|
||||
last_check: datetime.datetime
|
||||
num_checks: int
|
||||
urls: List[MirrorStatusEntryV3]
|
||||
version: int
|
||||
|
||||
@pydantic.model_validator(mode='before')
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def check_model(cls, data: Dict[str, int | datetime.datetime | List[MirrorStatusEntryV3]]) -> Dict[str, int | datetime.datetime | List[MirrorStatusEntryV3]]:
|
||||
def check_model(
|
||||
cls,
|
||||
data: Dict[str, int | datetime.datetime | List[MirrorStatusEntryV3]]
|
||||
) -> Dict[str, int | datetime.datetime | List[MirrorStatusEntryV3]]:
|
||||
if data.get('version') == 3:
|
||||
return data
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from urllib.request import urlopen
|
|||
from .exceptions import SysCallError, DownloadTimeout
|
||||
from .output import error, info
|
||||
from .pacman import Pacman
|
||||
from .output import debug
|
||||
|
||||
|
||||
class DownloadTimer():
|
||||
|
|
@ -191,7 +192,7 @@ def ping(hostname, timeout=5) -> int:
|
|||
latency = round((time.time() - started) * 1000)
|
||||
break
|
||||
except socket.error as error:
|
||||
print(f"Error: {error}")
|
||||
debug(f"Error: {error}")
|
||||
break
|
||||
|
||||
icmp_socket.close()
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class FormattedOutput:
|
|||
value = record.get(key, '')
|
||||
|
||||
if '!' in key:
|
||||
value = '*' * width
|
||||
value = '*' * len(value)
|
||||
|
||||
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
|
||||
obj_data.append(unicode_rjust(str(value), width))
|
||||
|
|
@ -322,14 +322,9 @@ def log(
|
|||
|
||||
Journald.log(text, level=level)
|
||||
|
||||
from .menu import Menu
|
||||
if not Menu.is_menu_active():
|
||||
# Finally, print the log unless we skipped it based on level.
|
||||
# We use sys.stdout.write()+flush() instead of print() to try and
|
||||
# fix issue #94
|
||||
if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False):
|
||||
sys.stdout.write(f"{text}\n")
|
||||
sys.stdout.flush()
|
||||
if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False):
|
||||
from archinstall.tui import Tui
|
||||
Tui.print(text)
|
||||
|
||||
|
||||
def _count_wchars(string: str) -> int:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional, Dict
|
||||
from typing import TYPE_CHECKING, Any, Optional, Dict, List
|
||||
|
||||
from archinstall.default_profiles.profile import Profile, GreeterType
|
||||
from .profile_model import ProfileConfiguration
|
||||
from ..menu import Menu, MenuSelectionType, AbstractSubMenu, Selector
|
||||
from ..menu import AbstractSubMenu
|
||||
from ..interactions.system_conf import select_driver
|
||||
from ..hardware import GfxDriver
|
||||
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType,
|
||||
Orientation
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
|
@ -15,7 +21,6 @@ if TYPE_CHECKING:
|
|||
class ProfileMenu(AbstractSubMenu):
|
||||
def __init__(
|
||||
self,
|
||||
data_store: Dict[str, Any],
|
||||
preset: Optional[ProfileConfiguration] = None
|
||||
):
|
||||
if preset:
|
||||
|
|
@ -23,45 +28,50 @@ class ProfileMenu(AbstractSubMenu):
|
|||
else:
|
||||
self._preset = ProfileConfiguration()
|
||||
|
||||
super().__init__(data_store=data_store)
|
||||
self._data_store: Dict[str, Any] = {}
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
self._menu_options['profile'] = Selector(
|
||||
_('Type'),
|
||||
lambda x: self._select_profile(x),
|
||||
display_func=lambda x: x.name if x else None,
|
||||
preview_func=self._preview_profile,
|
||||
default=self._preset.profile,
|
||||
enabled=True
|
||||
)
|
||||
menu_optioons = self._define_menu_options()
|
||||
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
|
||||
|
||||
self._menu_options['gfx_driver'] = Selector(
|
||||
_('Graphics driver'),
|
||||
lambda preset: self._select_gfx_driver(preset),
|
||||
display_func=lambda x: x.value if x else None,
|
||||
dependencies=['profile'],
|
||||
preview_func=self._preview_gfx,
|
||||
default=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None,
|
||||
enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False
|
||||
)
|
||||
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
|
||||
|
||||
self._menu_options['greeter'] = Selector(
|
||||
_('Greeter'),
|
||||
lambda preset: select_greeter(self._menu_options['profile'].current_selection, preset),
|
||||
display_func=lambda x: x.value if x else None,
|
||||
dependencies=['profile'],
|
||||
default=self._preset.greeter if self._preset.profile and self._preset.profile.is_greeter_supported() else None,
|
||||
enabled=self._preset.profile.is_greeter_supported() if self._preset.profile else False
|
||||
)
|
||||
def _define_menu_options(self) -> List[MenuItem]:
|
||||
return [
|
||||
MenuItem(
|
||||
text=str(_('Type')),
|
||||
action=lambda x: self._select_profile(x),
|
||||
value=self._preset.profile,
|
||||
preview_action=self._preview_profile,
|
||||
key='profile'
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Graphics driver')),
|
||||
action=lambda x: self._select_gfx_driver(x),
|
||||
value=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None,
|
||||
preview_action=self._prev_gfx,
|
||||
enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False,
|
||||
dependencies=['profile'],
|
||||
key='gfx_driver',
|
||||
),
|
||||
MenuItem(
|
||||
text=str(_('Greeter')),
|
||||
action=lambda x: select_greeter(preset=x),
|
||||
value=self._preset.greeter if self._preset.profile and self._preset.profile.is_greeter_supported() else None,
|
||||
enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False,
|
||||
preview_action=self._prev_greeter,
|
||||
dependencies=['profile'],
|
||||
key='greeter',
|
||||
)
|
||||
]
|
||||
|
||||
def run(self, allow_reset: bool = True) -> Optional[ProfileConfiguration]:
|
||||
super().run(allow_reset=allow_reset)
|
||||
def run(self) -> Optional[ProfileConfiguration]:
|
||||
super().run()
|
||||
|
||||
if self._data_store.get('profile', None):
|
||||
return ProfileConfiguration(
|
||||
self._menu_options['profile'].current_selection,
|
||||
self._menu_options['gfx_driver'].current_selection,
|
||||
self._menu_options['greeter'].current_selection
|
||||
self._data_store.get('profile', None),
|
||||
self._data_store.get('gfx_driver', None),
|
||||
self._data_store.get('greeter', None),
|
||||
)
|
||||
|
||||
return None
|
||||
|
|
@ -71,52 +81,69 @@ class ProfileMenu(AbstractSubMenu):
|
|||
|
||||
if profile is not None:
|
||||
if not profile.is_graphic_driver_supported():
|
||||
self._menu_options['gfx_driver'].set_enabled(False)
|
||||
self._menu_options['gfx_driver'].set_current_selection(None)
|
||||
self._item_group.find_by_key('gfx_driver').enabled = False
|
||||
self._item_group.find_by_key('gfx_driver').value = None
|
||||
else:
|
||||
self._menu_options['gfx_driver'].set_enabled(True)
|
||||
self._menu_options['gfx_driver'].set_current_selection(GfxDriver.AllOpenSource)
|
||||
self._item_group.find_by_key('gfx_driver').enabled = True
|
||||
self._item_group.find_by_key('gfx_driver').value = GfxDriver.AllOpenSource
|
||||
|
||||
if not profile.is_greeter_supported():
|
||||
self._menu_options['greeter'].set_enabled(False)
|
||||
self._menu_options['greeter'].set_current_selection(None)
|
||||
self._item_group.find_by_key('greeter').enabled = False
|
||||
self._item_group.find_by_key('greeter').value = None
|
||||
else:
|
||||
self._menu_options['greeter'].set_enabled(True)
|
||||
self._menu_options['greeter'].set_current_selection(profile.default_greeter_type)
|
||||
self._item_group.find_by_key('greeter').enabled = True
|
||||
self._item_group.find_by_key('greeter').value = profile.default_greeter_type
|
||||
else:
|
||||
self._menu_options['gfx_driver'].set_current_selection(None)
|
||||
self._menu_options['greeter'].set_current_selection(None)
|
||||
self._item_group.find_by_key('gfx_driver').value = None
|
||||
self._item_group.find_by_key('greeter').value = None
|
||||
|
||||
return profile
|
||||
|
||||
def _select_gfx_driver(self, preset: Optional[GfxDriver] = None) -> Optional[GfxDriver]:
|
||||
driver = preset
|
||||
profile: Optional[Profile] = self._menu_options['profile'].current_selection
|
||||
profile: Optional[Profile] = self._item_group.find_by_key('profile').value
|
||||
|
||||
if profile:
|
||||
if profile.is_graphic_driver_supported():
|
||||
driver = select_driver(current_value=preset)
|
||||
driver = select_driver(preset=preset)
|
||||
|
||||
if driver and 'Sway' in profile.current_selection_names():
|
||||
if driver.is_nvidia():
|
||||
prompt = str(_('The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?'))
|
||||
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run()
|
||||
header = str(_('The proprietary Nvidia driver is not supported by Sway.')) + '\n'
|
||||
header += str(_('It is likely that you will run into issues, are you okay with that?')) + '\n'
|
||||
|
||||
if choice.value == Menu.no():
|
||||
return None
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.no()
|
||||
group.default_item = MenuItem.no()
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
alignment=Alignment.CENTER
|
||||
).run()
|
||||
|
||||
if result.item() == MenuItem.no():
|
||||
return preset
|
||||
|
||||
return driver
|
||||
|
||||
def _preview_gfx(self) -> Optional[str]:
|
||||
driver: Optional[GfxDriver] = self._menu_options['gfx_driver'].current_selection
|
||||
|
||||
if driver:
|
||||
return driver.packages_text()
|
||||
|
||||
def _prev_gfx(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value:
|
||||
driver = item.get_value().value
|
||||
packages = item.get_value().packages_text()
|
||||
return f'Driver: {driver}\n{packages}'
|
||||
return None
|
||||
|
||||
def _preview_profile(self) -> Optional[str]:
|
||||
profile: Optional[Profile] = self._menu_options['profile'].current_selection
|
||||
def _prev_greeter(self, item: MenuItem) -> Optional[str]:
|
||||
if item.value:
|
||||
return f'{str(_("Greeter"))}: {item.value.value}'
|
||||
return None
|
||||
|
||||
def _preview_profile(self, item: MenuItem) -> Optional[str]:
|
||||
profile: Optional[Profile] = item.value
|
||||
text = ''
|
||||
|
||||
if profile:
|
||||
|
|
@ -138,66 +165,71 @@ def select_greeter(
|
|||
preset: Optional[GreeterType] = None
|
||||
) -> Optional[GreeterType]:
|
||||
if not profile or profile.is_greeter_supported():
|
||||
title = str(_('Please chose which greeter to install'))
|
||||
greeter_options = [greeter.value for greeter in GreeterType]
|
||||
items = [MenuItem(greeter.value, value=greeter) for greeter in GreeterType]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
|
||||
default: Optional[GreeterType] = None
|
||||
|
||||
if preset is not None:
|
||||
default = preset
|
||||
elif profile is not None:
|
||||
default_greeter = profile.default_greeter_type
|
||||
default = default_greeter if default_greeter else None
|
||||
|
||||
choice = Menu(
|
||||
title,
|
||||
greeter_options,
|
||||
skip=True,
|
||||
default_option=default.value if default else None
|
||||
group.set_default_by_value(default)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
allow_skip=True,
|
||||
frame=FrameProperties.min(str(_('Greeter'))),
|
||||
alignment=Alignment.CENTER
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Skip:
|
||||
return default
|
||||
|
||||
return GreeterType(choice.single_value)
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def select_profile(
|
||||
current_profile: Optional[Profile] = None,
|
||||
title: Optional[str] = None,
|
||||
header: Optional[str] = None,
|
||||
allow_reset: bool = True,
|
||||
multi: bool = False
|
||||
) -> Optional[Profile]:
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
top_level_profiles = profile_handler.get_top_level_profiles()
|
||||
|
||||
display_title = title
|
||||
if not display_title:
|
||||
display_title = str(_('This is a list of pre-programmed default_profiles'))
|
||||
if header is None:
|
||||
header = str(_('This is a list of pre-programmed default_profiles')) + '\n'
|
||||
|
||||
choice = profile_handler.select_profile(
|
||||
top_level_profiles,
|
||||
current_profile=current_profile,
|
||||
title=display_title,
|
||||
items = [MenuItem(p.name, value=p) for p in top_level_profiles]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_selected_by_value(current_profile)
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_reset=allow_reset,
|
||||
multi=multi
|
||||
)
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Main profile')))
|
||||
).run()
|
||||
|
||||
match choice.type_:
|
||||
case MenuSelectionType.Selection:
|
||||
profile_selection: Profile = choice.single_value
|
||||
match result.type_:
|
||||
case ResultType.Reset:
|
||||
return None
|
||||
case ResultType.Skip:
|
||||
return current_profile
|
||||
case ResultType.Selection:
|
||||
profile_selection: Profile = result.get_value()
|
||||
select_result = profile_selection.do_on_select()
|
||||
|
||||
if not select_result:
|
||||
return select_profile(
|
||||
current_profile=current_profile,
|
||||
title=title,
|
||||
allow_reset=allow_reset,
|
||||
multi=multi
|
||||
)
|
||||
return None
|
||||
|
||||
# we're going to reset the currently selected profile(s) to avoid
|
||||
# any stale data laying around
|
||||
|
|
@ -212,7 +244,5 @@ def select_profile(
|
|||
pass
|
||||
|
||||
return current_profile
|
||||
case MenuSelectionType.Reset:
|
||||
return None
|
||||
case MenuSelectionType.Skip:
|
||||
return current_profile
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ from typing import List, TYPE_CHECKING, Any, Optional, Dict, Union
|
|||
from ...default_profiles.profile import Profile, GreeterType
|
||||
from .profile_model import ProfileConfiguration
|
||||
from ..hardware import GfxDriver
|
||||
from ..menu import MenuSelectionType, Menu, MenuSelection
|
||||
from ..networking import list_interfaces, fetch_data_from_url
|
||||
from ..output import error, debug, info
|
||||
from ..storage import storage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..installer import Installer
|
||||
_: Any
|
||||
|
|
@ -358,58 +358,5 @@ class ProfileHandler:
|
|||
if profile.name not in excluded_profiles:
|
||||
profile.reset()
|
||||
|
||||
def select_profile(
|
||||
self,
|
||||
selectable_profiles: List[Profile],
|
||||
current_profile: Optional[Union[Profile, List[Profile]]] = None,
|
||||
title: str = '',
|
||||
allow_reset: bool = True,
|
||||
multi: bool = False,
|
||||
) -> MenuSelection:
|
||||
"""
|
||||
Helper function to perform a profile selection
|
||||
"""
|
||||
options = {p.name: p for p in selectable_profiles}
|
||||
options = dict((k, v) for k, v in sorted(options.items(), key=lambda x: x[0].upper()))
|
||||
|
||||
warning = str(_('Are you sure you want to reset this setting?'))
|
||||
|
||||
preset_value: Optional[Union[str, List[str]]] = None
|
||||
if current_profile is not None:
|
||||
if isinstance(current_profile, list):
|
||||
preset_value = [p.name for p in current_profile]
|
||||
else:
|
||||
preset_value = current_profile.name
|
||||
|
||||
choice = Menu(
|
||||
title=title,
|
||||
preset_values=preset_value,
|
||||
p_options=options,
|
||||
allow_reset=allow_reset,
|
||||
allow_reset_warning_msg=warning,
|
||||
multi=multi,
|
||||
sort=False,
|
||||
preview_command=self.preview_text,
|
||||
preview_size=0.5
|
||||
).run()
|
||||
|
||||
if choice.type_ == MenuSelectionType.Selection:
|
||||
value = choice.value
|
||||
if multi:
|
||||
# this is quite dirty and should eb switched to a
|
||||
# dedicated return type instead
|
||||
choice.value = [options[val] for val in value] # type: ignore
|
||||
else:
|
||||
choice.value = options[value] # type: ignore
|
||||
|
||||
return choice
|
||||
|
||||
def preview_text(self, selection: str) -> Optional[str]:
|
||||
"""
|
||||
Callback for preview display on profile selection
|
||||
"""
|
||||
profile = self.get_profile_by_name(selection)
|
||||
return profile.preview_text() if profile is not None else None
|
||||
|
||||
|
||||
profile_handler = ProfileHandler()
|
||||
|
|
|
|||
|
|
@ -2,22 +2,98 @@ from pathlib import Path
|
|||
from typing import Any, TYPE_CHECKING, Optional, List
|
||||
|
||||
from ..output import FormattedOutput
|
||||
from ..output import info
|
||||
from ..general import secret
|
||||
|
||||
from archinstall.tui import (
|
||||
Alignment, EditMenu
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
||||
def prompt_dir(text: str, header: Optional[str] = None) -> Path:
|
||||
if header:
|
||||
print(header)
|
||||
def get_password(
|
||||
text: str,
|
||||
header: Optional[str] = None,
|
||||
allow_skip: bool = False,
|
||||
preset: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
failure: Optional[str] = None
|
||||
|
||||
while True:
|
||||
path = input(text).strip(' ')
|
||||
user_hdr = None
|
||||
if failure is not None:
|
||||
user_hdr = f'{header}\n{failure}\n'
|
||||
elif header is not None:
|
||||
user_hdr = header
|
||||
|
||||
result = EditMenu(
|
||||
text,
|
||||
header=user_hdr,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=allow_skip,
|
||||
default_text=preset,
|
||||
hide_input=True
|
||||
).input()
|
||||
|
||||
if allow_skip and not result.has_item():
|
||||
return None
|
||||
|
||||
password = result.text()
|
||||
hidden = secret(password)
|
||||
|
||||
if header is not None:
|
||||
confirmation_header = f'{header}{str(_("Pssword"))}: {hidden}\n'
|
||||
else:
|
||||
confirmation_header = f'{str(_("Password"))}: {hidden}\n'
|
||||
|
||||
result = EditMenu(
|
||||
str(_('Confirm password')),
|
||||
header=confirmation_header,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=False,
|
||||
hide_input=True
|
||||
).input()
|
||||
|
||||
if password == result.text():
|
||||
return password
|
||||
|
||||
failure = str(_('The confirmation password did not match, please try again'))
|
||||
|
||||
|
||||
def prompt_dir(
|
||||
text: str,
|
||||
header: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
allow_skip: bool = False,
|
||||
preset: Optional[str] = None
|
||||
) -> Optional[Path]:
|
||||
def validate_path(path: str) -> Optional[str]:
|
||||
dest_path = Path(path)
|
||||
|
||||
if dest_path.exists() and dest_path.is_dir():
|
||||
return dest_path
|
||||
info(_('Not a valid directory: {}').format(dest_path))
|
||||
return None
|
||||
|
||||
return str(_('Not a valid directory'))
|
||||
|
||||
if validate:
|
||||
validate_func = validate_path
|
||||
else:
|
||||
validate_func = None
|
||||
|
||||
result = EditMenu(
|
||||
text,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=allow_skip,
|
||||
validator=validate_func,
|
||||
default_text=preset
|
||||
).input()
|
||||
|
||||
if allow_skip and not result.has_item():
|
||||
return None
|
||||
|
||||
return Path(result.text())
|
||||
|
||||
|
||||
def is_subpath(first: Path, second: Path) -> bool:
|
||||
|
|
@ -48,4 +124,6 @@ def format_cols(items: List[str], header: Optional[str] = None) -> str:
|
|||
col = 4
|
||||
|
||||
text += FormattedOutput.as_columns(items, col)
|
||||
# remove whitespaces on each row
|
||||
text = '\n'.join([t.strip() for t in text.split('\n')])
|
||||
return text
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ from archinstall.lib import disk
|
|||
from archinstall.lib.global_menu import GlobalMenu
|
||||
from archinstall.lib.configuration import ConfigurationOutput
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.menu import Menu
|
||||
from archinstall.lib.models import AudioConfiguration, Bootloader
|
||||
from archinstall.lib.models.network_configuration import NetworkConfiguration
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.lib.interactions.general_conf import ask_chroot
|
||||
from archinstall.tui import Tui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -25,76 +26,18 @@ if archinstall.arguments.get('help'):
|
|||
|
||||
def ask_user_questions() -> None:
|
||||
"""
|
||||
First, we'll ask the user for a bunch of user input.
|
||||
Not until we're satisfied with what we want to install
|
||||
will we continue with the actual installation steps.
|
||||
First, we'll ask the user for a bunch of user input.
|
||||
Not until we're satisfied with what we want to install
|
||||
will we continue with the actual installation steps.
|
||||
"""
|
||||
|
||||
# ref: https://github.com/archlinux/archinstall/pull/831
|
||||
# we'll set NTP to true by default since this is also
|
||||
# the default value specified in the menu options; in
|
||||
# case it will be changed by the user we'll also update
|
||||
# the system immediately
|
||||
global_menu = GlobalMenu(data_store=archinstall.arguments)
|
||||
with Tui():
|
||||
global_menu = GlobalMenu(data_store=archinstall.arguments)
|
||||
|
||||
global_menu.enable('archinstall-language')
|
||||
if not archinstall.arguments.get('advanced', False):
|
||||
global_menu.set_enabled('parallel downloads', False)
|
||||
|
||||
# Set which region to download packages from during the installation
|
||||
global_menu.enable('mirror_config')
|
||||
|
||||
global_menu.enable('locale_config')
|
||||
|
||||
global_menu.enable('disk_config', mandatory=True)
|
||||
|
||||
# Specify disk encryption options
|
||||
global_menu.enable('disk_encryption')
|
||||
|
||||
# Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB)
|
||||
global_menu.enable('bootloader')
|
||||
|
||||
global_menu.enable('uki')
|
||||
|
||||
global_menu.enable('swap')
|
||||
|
||||
# Get the hostname for the machine
|
||||
global_menu.enable('hostname')
|
||||
|
||||
# Ask for a root password (optional, but triggers requirement for super-user if skipped)
|
||||
global_menu.enable('!root-password', mandatory=True)
|
||||
|
||||
global_menu.enable('!users', mandatory=True)
|
||||
|
||||
# Ask for archinstall-specific profiles_bck (such as desktop environments etc)
|
||||
global_menu.enable('profile_config')
|
||||
|
||||
# Ask about audio server selection if one is not already set
|
||||
global_menu.enable('audio_config')
|
||||
|
||||
# Ask for preferred kernel:
|
||||
global_menu.enable('kernels', mandatory=True)
|
||||
|
||||
global_menu.enable('packages')
|
||||
|
||||
if archinstall.arguments.get('advanced', False):
|
||||
# Enable parallel downloads
|
||||
global_menu.enable('parallel downloads')
|
||||
|
||||
# Ask or Call the helper function that asks the user to optionally configure a network.
|
||||
global_menu.enable('network_config')
|
||||
|
||||
global_menu.enable('timezone')
|
||||
|
||||
global_menu.enable('ntp')
|
||||
|
||||
global_menu.enable('additional-repositories')
|
||||
|
||||
global_menu.enable('__separator__')
|
||||
|
||||
global_menu.enable('save_config')
|
||||
global_menu.enable('install')
|
||||
global_menu.enable('abort')
|
||||
|
||||
global_menu.run()
|
||||
global_menu.run()
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path) -> None:
|
||||
|
|
@ -209,9 +152,10 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?'))
|
||||
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run()
|
||||
if choice.value == Menu.yes():
|
||||
with Tui():
|
||||
chroot = ask_chroot()
|
||||
|
||||
if chroot:
|
||||
try:
|
||||
installation.drop_to_shell()
|
||||
except:
|
||||
|
|
@ -220,27 +164,30 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
debug(f"Disk states after installing: {disk.disk_layouts()}")
|
||||
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
ask_user_questions()
|
||||
def guided() -> None:
|
||||
if not archinstall.arguments.get('silent'):
|
||||
ask_user_questions()
|
||||
|
||||
config_output = ConfigurationOutput(archinstall.arguments)
|
||||
config = ConfigurationOutput(archinstall.arguments)
|
||||
config.write_debug()
|
||||
config.save()
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
config_output.show()
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
|
||||
config_output.save()
|
||||
if not archinstall.arguments.get('silent'):
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
guided()
|
||||
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
input(str(_('Press Enter to continue.')))
|
||||
fs_handler.perform_filesystem_operations()
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
guided()
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ from pathlib import Path
|
|||
from typing import TYPE_CHECKING, Any, List
|
||||
|
||||
import archinstall
|
||||
from archinstall import info
|
||||
from archinstall import info, debug
|
||||
from archinstall import Installer, ConfigurationOutput
|
||||
from archinstall.default_profiles.minimal import MinimalProfile
|
||||
from archinstall.lib.interactions import suggest_single_disk_layout, select_devices
|
||||
from archinstall.lib.models import Bootloader, User
|
||||
from archinstall.lib.profile import ProfileConfiguration, profile_handler
|
||||
from archinstall.lib import disk
|
||||
from archinstall.tui import Tui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
|
@ -88,19 +89,31 @@ def parse_disk_encryption() -> None:
|
|||
)
|
||||
|
||||
|
||||
prompt_disk_layout()
|
||||
parse_disk_encryption()
|
||||
def minimal() -> None:
|
||||
with Tui():
|
||||
prompt_disk_layout()
|
||||
parse_disk_encryption()
|
||||
|
||||
config_output = ConfigurationOutput(archinstall.arguments)
|
||||
config_output.show()
|
||||
config = ConfigurationOutput(archinstall.arguments)
|
||||
config.write_debug()
|
||||
config.save()
|
||||
|
||||
input(str(_('Press Enter to continue.')))
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
if not archinstall.arguments.get('silent'):
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
minimal()
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
fs_handler.perform_filesystem_operations()
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
|
||||
|
||||
minimal()
|
||||
|
|
|
|||
|
|
@ -5,22 +5,23 @@ from archinstall import debug
|
|||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.configuration import ConfigurationOutput
|
||||
from archinstall.lib import disk
|
||||
from archinstall.tui import Tui
|
||||
|
||||
|
||||
def ask_user_questions() -> None:
|
||||
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
|
||||
with Tui():
|
||||
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
|
||||
global_menu.disable_all()
|
||||
|
||||
global_menu.enable('archinstall-language')
|
||||
global_menu.set_enabled('archinstall-language', True)
|
||||
global_menu.set_enabled('disk_config', True)
|
||||
global_menu.set_enabled('disk_encryption', True)
|
||||
global_menu.set_enabled('swap', True)
|
||||
global_menu.set_enabled('save_config', True)
|
||||
global_menu.set_enabled('install', True)
|
||||
global_menu.set_enabled('abort', True)
|
||||
|
||||
global_menu.enable('disk_config', mandatory=True)
|
||||
global_menu.enable('disk_encryption')
|
||||
global_menu.enable('swap')
|
||||
|
||||
global_menu.enable('save_config')
|
||||
global_menu.enable('install')
|
||||
global_menu.enable('abort')
|
||||
|
||||
global_menu.run()
|
||||
global_menu.run()
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path) -> None:
|
||||
|
|
@ -52,27 +53,30 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
debug(f"Disk states after installing: {disk.disk_layouts()}")
|
||||
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
ask_user_questions()
|
||||
def only_hd() -> None:
|
||||
if not archinstall.arguments.get('silent'):
|
||||
ask_user_questions()
|
||||
|
||||
config_output = ConfigurationOutput(archinstall.arguments)
|
||||
if not archinstall.arguments.get('silent'):
|
||||
config_output.show()
|
||||
config = ConfigurationOutput(archinstall.arguments)
|
||||
config.write_debug()
|
||||
config.save()
|
||||
|
||||
config_output.save()
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
if not archinstall.arguments.get('silent'):
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
only_hd()
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
input('Press Enter to continue.')
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
fs_handler.perform_filesystem_operations()
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
|
||||
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
only_hd()
|
||||
|
|
|
|||
|
|
@ -9,158 +9,122 @@ from archinstall.lib import disk
|
|||
from archinstall.lib import locale
|
||||
from archinstall.lib.models import AudioConfiguration
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.lib import menu
|
||||
from archinstall.lib.global_menu import GlobalMenu
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.configuration import ConfigurationOutput
|
||||
from archinstall.lib.interactions.general_conf import ask_chroot
|
||||
from archinstall.tui import (
|
||||
MenuItemGroup, MenuItem, SelectMenu,
|
||||
FrameProperties, Alignment, ResultType,
|
||||
Tui
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Any
|
||||
|
||||
|
||||
class ExecutionMode(Enum):
|
||||
Full = 'full'
|
||||
Guided = 'guided'
|
||||
Lineal = 'lineal'
|
||||
Only_HD = 'only-hd'
|
||||
Only_OS = 'only-os'
|
||||
Minimal = 'minimal'
|
||||
|
||||
|
||||
def select_mode() -> ExecutionMode:
|
||||
options = [str(e.value) for e in ExecutionMode]
|
||||
choice = menu.Menu(
|
||||
str(_('Select an execution mode')),
|
||||
options,
|
||||
default_option=ExecutionMode.Full.value,
|
||||
skip=False
|
||||
).run()
|
||||
|
||||
return ExecutionMode(choice.single_value)
|
||||
|
||||
|
||||
class SetupMenu(GlobalMenu):
|
||||
def __init__(self, storage_area: Dict[str, Any]):
|
||||
super().__init__(data_store=storage_area)
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
super().setup_selection_menu_options()
|
||||
|
||||
self._menu_options['mode'] = menu.Selector(
|
||||
'Execution mode',
|
||||
lambda x: select_mode(),
|
||||
display_func=lambda x: x.value if x else '',
|
||||
default=ExecutionMode.Full)
|
||||
|
||||
self._menu_options['continue'] = menu.Selector(
|
||||
'Continue',
|
||||
exec_func=lambda n, v: True)
|
||||
|
||||
self.enable('archinstall-language')
|
||||
self.enable('ntp')
|
||||
self.enable('mode')
|
||||
self.enable('continue')
|
||||
self.enable('abort')
|
||||
|
||||
def exit_callback(self) -> None:
|
||||
if self._data_store.get('mode', None):
|
||||
archinstall.arguments['mode'] = self._data_store['mode']
|
||||
info(f"Archinstall will execute under {archinstall.arguments['mode']} mode")
|
||||
|
||||
|
||||
class SwissMainMenu(GlobalMenu):
|
||||
def __init__(
|
||||
self,
|
||||
data_store: Dict[str, Any],
|
||||
exec_mode: ExecutionMode = ExecutionMode.Full
|
||||
mode: ExecutionMode = ExecutionMode.Guided,
|
||||
advanced: bool = False
|
||||
):
|
||||
self._execution_mode = exec_mode
|
||||
self._execution_mode = mode
|
||||
self._advanced = advanced
|
||||
super().__init__(data_store)
|
||||
|
||||
def setup_selection_menu_options(self) -> None:
|
||||
super().setup_selection_menu_options()
|
||||
|
||||
options_list = []
|
||||
mandatory_list = []
|
||||
def execute(self) -> None:
|
||||
ignore = ['install', 'abort']
|
||||
|
||||
match self._execution_mode:
|
||||
case ExecutionMode.Full | ExecutionMode.Lineal:
|
||||
options_list = [
|
||||
'mirror_config', 'disk_config',
|
||||
'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password',
|
||||
'!users', 'profile_config', 'audio_config', 'kernels', 'packages', 'additional-repositories', 'network_config',
|
||||
'timezone', 'ntp'
|
||||
]
|
||||
|
||||
if archinstall.arguments.get('advanced', False):
|
||||
options_list.extend(['locale_config'])
|
||||
|
||||
mandatory_list = ['disk_config', 'bootloader', 'hostname']
|
||||
case ExecutionMode.Guided:
|
||||
from archinstall.scripts.guided import guided
|
||||
guided()
|
||||
case ExecutionMode.Only_HD:
|
||||
options_list = ['disk_config', 'disk_encryption', 'swap']
|
||||
mandatory_list = ['disk_config']
|
||||
case ExecutionMode.Only_OS:
|
||||
options_list = [
|
||||
'mirror_config', 'bootloader', 'hostname',
|
||||
'!root-password', '!users', 'profile_config', 'audio_config', 'kernels',
|
||||
'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp'
|
||||
]
|
||||
from archinstall.scripts.only_hd import only_hd
|
||||
only_hd()
|
||||
case ExecutionMode.Minimal:
|
||||
from archinstall.scripts.minimal import minimal
|
||||
minimal()
|
||||
case ExecutionMode.Lineal:
|
||||
for item in self._menu_item_group.items:
|
||||
if self._menu_item_group.should_enable_item(item):
|
||||
if item.action is not None and item.key is not None:
|
||||
if item.key not in ignore:
|
||||
archinstall.arguments[item.key] = item.action(item.value)
|
||||
|
||||
perform_installation(
|
||||
archinstall.storage.get('MOUNT_POINT', Path('/mnt')),
|
||||
self._execution_mode
|
||||
)
|
||||
case ExecutionMode.Only_OS:
|
||||
menu_items = [
|
||||
'mirror_config', 'bootloader', 'hostname',
|
||||
'!root-password', '!users', 'profile_config',
|
||||
'audio_config', 'kernels', 'packages',
|
||||
'additional-repositories', 'network_config', 'timezone', 'ntp'
|
||||
]
|
||||
mandatory_list = ['hostname']
|
||||
|
||||
if archinstall.arguments.get('advanced', False):
|
||||
options_list += ['locale_config']
|
||||
case ExecutionMode.Minimal:
|
||||
pass
|
||||
if self._advanced:
|
||||
menu_items += ['locale_config']
|
||||
|
||||
for item in self._menu_item_group.items:
|
||||
if self._menu_item_group.should_enable_item(item):
|
||||
if item.action is not None and item.key is not None:
|
||||
if item.key not in ignore and item.key in menu_items:
|
||||
while True:
|
||||
value = item.action(item.value)
|
||||
if value not in mandatory_list or value is not None:
|
||||
archinstall.arguments[item.key] = item.action(item.value)
|
||||
break
|
||||
|
||||
perform_installation(
|
||||
archinstall.storage.get('MOUNT_POINT', Path('/mnt')),
|
||||
self._execution_mode
|
||||
)
|
||||
case _:
|
||||
info(f' Execution mode {self._execution_mode} not supported')
|
||||
exit(1)
|
||||
|
||||
if self._execution_mode != ExecutionMode.Lineal:
|
||||
options_list += ['save_config', 'install']
|
||||
|
||||
if not archinstall.arguments.get('advanced', False):
|
||||
options_list.append('archinstall-language')
|
||||
def ask_user_questions(mode: ExecutionMode = ExecutionMode.Guided) -> None:
|
||||
advanced = archinstall.arguments.get('advanced', False)
|
||||
|
||||
options_list += ['abort']
|
||||
with Tui():
|
||||
if advanced:
|
||||
header = str(_('Select execution mode'))
|
||||
items = [MenuItem(ex.name, value=ex) for ex in ExecutionMode]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_default_by_value(ExecutionMode.Guided)
|
||||
|
||||
for entry in mandatory_list:
|
||||
self.enable(entry, mandatory=True)
|
||||
result = SelectMenu(
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(str(_('Modes')))
|
||||
).run()
|
||||
|
||||
for entry in options_list:
|
||||
self.enable(entry)
|
||||
if result.type_ == ResultType.Skip:
|
||||
exit(0)
|
||||
|
||||
mode = result.get_value()
|
||||
|
||||
def ask_user_questions(exec_mode: ExecutionMode = ExecutionMode.Full) -> None:
|
||||
"""
|
||||
First, we'll ask the user for a bunch of user input.
|
||||
Not until we're satisfied with what we want to install
|
||||
will we continue with the actual installation steps.
|
||||
"""
|
||||
if archinstall.arguments.get('advanced', None):
|
||||
setup_area: Dict[str, Any] = {}
|
||||
setup = SetupMenu(setup_area)
|
||||
|
||||
if exec_mode == ExecutionMode.Lineal:
|
||||
for entry in setup.list_enabled_options():
|
||||
if entry in ('continue', 'abort'):
|
||||
continue
|
||||
if not setup.option(entry).enabled:
|
||||
continue
|
||||
setup.exec_option(entry)
|
||||
else:
|
||||
setup.run()
|
||||
|
||||
archinstall.arguments['archinstall-language'] = setup_area.get('archinstall-language')
|
||||
|
||||
with SwissMainMenu(data_store=archinstall.arguments, exec_mode=exec_mode) as menu:
|
||||
if mode == ExecutionMode.Lineal:
|
||||
for entry in menu.list_enabled_options():
|
||||
if entry in ('install', 'abort'):
|
||||
continue
|
||||
menu.exec_option(entry)
|
||||
archinstall.arguments[entry] = menu.option(entry).get_selection()
|
||||
else:
|
||||
menu.run()
|
||||
SwissMainMenu(
|
||||
data_store=archinstall.arguments,
|
||||
mode=mode,
|
||||
advanced=advanced
|
||||
).execute()
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path, exec_mode: ExecutionMode) -> None:
|
||||
|
|
@ -177,132 +141,134 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode) -> None:
|
|||
disk_encryption=disk_encryption,
|
||||
kernels=archinstall.arguments.get('kernels', ['linux'])
|
||||
) as installation:
|
||||
if exec_mode in [ExecutionMode.Full, ExecutionMode.Only_HD]:
|
||||
installation.mount_ordered_layout()
|
||||
installation.mount_ordered_layout()
|
||||
|
||||
installation.sanity_check()
|
||||
installation.sanity_check()
|
||||
|
||||
if disk_config.config_type != disk.DiskLayoutType.Pre_mount:
|
||||
if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption:
|
||||
# generate encryption key files for the mounted luks devices
|
||||
installation.generate_key_files()
|
||||
if disk_config.config_type != disk.DiskLayoutType.Pre_mount:
|
||||
if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption:
|
||||
# generate encryption key files for the mounted luks devices
|
||||
installation.generate_key_files()
|
||||
|
||||
if mirror_config := archinstall.arguments.get('mirror_config', None):
|
||||
installation.set_mirrors(mirror_config)
|
||||
if mirror_config := archinstall.arguments.get('mirror_config', None):
|
||||
installation.set_mirrors(mirror_config)
|
||||
|
||||
installation.minimal_installation(
|
||||
testing=enable_testing,
|
||||
multilib=enable_multilib,
|
||||
hostname=archinstall.arguments.get('hostname', 'archlinux'),
|
||||
locale_config=locale_config
|
||||
installation.minimal_installation(
|
||||
testing=enable_testing,
|
||||
multilib=enable_multilib,
|
||||
hostname=archinstall.arguments.get('hostname', 'archlinux'),
|
||||
locale_config=locale_config
|
||||
)
|
||||
|
||||
if mirror_config := archinstall.arguments.get('mirror_config', None):
|
||||
installation.set_mirrors(mirror_config, on_target=True)
|
||||
|
||||
if archinstall.arguments.get('swap'):
|
||||
installation.setup_swap('zram')
|
||||
|
||||
if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi():
|
||||
installation.add_additional_packages("grub")
|
||||
|
||||
installation.add_bootloader(archinstall.arguments["bootloader"])
|
||||
|
||||
# If user selected to copy the current ISO network configuration
|
||||
# Perform a copy of the config
|
||||
network_config = archinstall.arguments.get('network_config', None)
|
||||
|
||||
if network_config:
|
||||
network_config.install_network_config(
|
||||
installation,
|
||||
archinstall.arguments.get('profile_config', None)
|
||||
)
|
||||
|
||||
if mirror_config := archinstall.arguments.get('mirror_config', None):
|
||||
installation.set_mirrors(mirror_config, on_target=True)
|
||||
if users := archinstall.arguments.get('!users', None):
|
||||
installation.create_users(users)
|
||||
|
||||
if archinstall.arguments.get('swap'):
|
||||
installation.setup_swap('zram')
|
||||
audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None)
|
||||
if audio_config:
|
||||
audio_config.install_audio_config(installation)
|
||||
else:
|
||||
info("No audio server will be installed")
|
||||
|
||||
if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi():
|
||||
installation.add_additional_packages("grub")
|
||||
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
|
||||
installation.add_additional_packages(archinstall.arguments.get('packages', []))
|
||||
|
||||
installation.add_bootloader(archinstall.arguments["bootloader"])
|
||||
if profile_config := archinstall.arguments.get('profile_config', None):
|
||||
profile_handler.install_profile_config(installation, profile_config)
|
||||
|
||||
# If user selected to copy the current ISO network configuration
|
||||
# Perform a copy of the config
|
||||
network_config = archinstall.arguments.get('network_config', None)
|
||||
if timezone := archinstall.arguments.get('timezone', None):
|
||||
installation.set_timezone(timezone)
|
||||
|
||||
if network_config:
|
||||
network_config.install_network_config(
|
||||
installation,
|
||||
archinstall.arguments.get('profile_config', None)
|
||||
)
|
||||
if archinstall.arguments.get('ntp', False):
|
||||
installation.activate_time_synchronization()
|
||||
|
||||
if users := archinstall.arguments.get('!users', None):
|
||||
installation.create_users(users)
|
||||
if archinstall.accessibility_tools_in_use():
|
||||
installation.enable_espeakup()
|
||||
|
||||
audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None)
|
||||
if audio_config:
|
||||
audio_config.install_audio_config(installation)
|
||||
else:
|
||||
info("No audio server will be installed")
|
||||
if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw):
|
||||
installation.user_set_pw('root', root_pw)
|
||||
|
||||
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
|
||||
installation.add_additional_packages(archinstall.arguments.get('packages', []))
|
||||
if profile_config := archinstall.arguments.get('profile_config', None):
|
||||
profile_config.profile.post_install(installation)
|
||||
|
||||
if profile_config := archinstall.arguments.get('profile_config', None):
|
||||
profile_handler.install_profile_config(installation, profile_config)
|
||||
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
|
||||
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
|
||||
if archinstall.arguments.get('services', None):
|
||||
installation.enable_service(archinstall.arguments.get('services', []))
|
||||
|
||||
if timezone := archinstall.arguments.get('timezone', None):
|
||||
installation.set_timezone(timezone)
|
||||
# If the user provided custom commands to be run post-installation, execute them now.
|
||||
if archinstall.arguments.get('custom-commands', None):
|
||||
archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation)
|
||||
|
||||
if archinstall.arguments.get('ntp', False):
|
||||
installation.activate_time_synchronization()
|
||||
installation.genfstab()
|
||||
|
||||
if archinstall.accessibility_tools_in_use():
|
||||
installation.enable_espeakup()
|
||||
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
|
||||
|
||||
if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw):
|
||||
installation.user_set_pw('root', root_pw)
|
||||
if not archinstall.arguments.get('silent'):
|
||||
with Tui():
|
||||
chroot = ask_chroot()
|
||||
|
||||
if profile_config := archinstall.arguments.get('profile_config', None):
|
||||
profile_config.profile.post_install(installation)
|
||||
|
||||
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
|
||||
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
|
||||
if archinstall.arguments.get('services', None):
|
||||
installation.enable_service(archinstall.arguments.get('services', []))
|
||||
|
||||
# If the user provided custom commands to be run post-installation, execute them now.
|
||||
if archinstall.arguments.get('custom-commands', None):
|
||||
archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation)
|
||||
|
||||
installation.genfstab()
|
||||
|
||||
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
prompt = str(
|
||||
_('Would you like to chroot into the newly created installation and perform post-installation configuration?'))
|
||||
choice = menu.Menu(prompt, menu.Menu.yes_no(), default_option=menu.Menu.yes()).run()
|
||||
if choice.value == menu.Menu.yes():
|
||||
try:
|
||||
installation.drop_to_shell()
|
||||
except:
|
||||
pass
|
||||
if chroot:
|
||||
try:
|
||||
installation.drop_to_shell()
|
||||
except:
|
||||
pass
|
||||
|
||||
debug(f"Disk states after installing: {disk.disk_layouts()}")
|
||||
|
||||
|
||||
param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower()
|
||||
def swiss() -> None:
|
||||
param_mode = archinstall.arguments.get('mode', ExecutionMode.Guided.value).lower()
|
||||
|
||||
try:
|
||||
mode = ExecutionMode(param_mode)
|
||||
except KeyError:
|
||||
info(f'Mode "{param_mode}" is not supported')
|
||||
exit(1)
|
||||
try:
|
||||
mode = ExecutionMode(param_mode)
|
||||
except KeyError:
|
||||
info(f'Mode "{param_mode}" is not supported')
|
||||
exit(1)
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
ask_user_questions(mode)
|
||||
if not archinstall.arguments.get('silent'):
|
||||
ask_user_questions(mode)
|
||||
|
||||
config_output = ConfigurationOutput(archinstall.arguments)
|
||||
if not archinstall.arguments.get('silent'):
|
||||
config_output.show()
|
||||
config = ConfigurationOutput(archinstall.arguments)
|
||||
config.write_debug()
|
||||
config.save()
|
||||
|
||||
config_output.save()
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
if not archinstall.arguments.get('silent'):
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
swiss()
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
input('Press Enter to continue.')
|
||||
|
||||
if mode in (ExecutionMode.Full, ExecutionMode.Only_HD):
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')), mode)
|
||||
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')), mode)
|
||||
|
||||
swiss()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import time
|
|||
import archinstall
|
||||
from archinstall import info
|
||||
from archinstall import profile
|
||||
from archinstall.tui import Tui
|
||||
|
||||
|
||||
for p in profile.profile_handler.get_mac_addr_profiles():
|
||||
# Tailored means it's a match for this machine
|
||||
|
|
@ -12,7 +14,7 @@ for p in profile.profile_handler.get_mac_addr_profiles():
|
|||
|
||||
print('Starting install in:')
|
||||
for i in range(10, 0, -1):
|
||||
print(f'{i}...')
|
||||
Tui.print(f'{i}...')
|
||||
time.sleep(1)
|
||||
|
||||
install_session = archinstall.storage['installation_session']
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -53,6 +53,8 @@ class Help:
|
|||
selection = HelpGroup(
|
||||
group_id=HelpTextGroupId.SELECTION,
|
||||
group_entries=[
|
||||
HelpText('Skip selction (if available)', ['Esc']),
|
||||
HelpText('Reset selection (if available)', ['Ctrl+c']),
|
||||
HelpText('Select on single select', ['Enter']),
|
||||
HelpText('Select on select', ['Space', 'Tab']),
|
||||
HelpText('Reset', ['Ctrl-C']),
|
||||
|
|
@ -75,18 +77,14 @@ class Help:
|
|||
max_desc_width = max([help.get_desc_width() for help in help_texts])
|
||||
max_key_width = max([help.get_key_width() for help in help_texts])
|
||||
|
||||
margin = ' ' * 3
|
||||
|
||||
for help in help_texts:
|
||||
help_output += f'{margin}{help.group_id.value}\n'
|
||||
divider_len = max_desc_width + max_key_width + len(margin * 2)
|
||||
help_output += margin + '-' * divider_len + '\n'
|
||||
help_output += f'{help.group_id.value}\n'
|
||||
divider_len = max_desc_width + max_key_width
|
||||
help_output += '-' * divider_len + '\n'
|
||||
|
||||
for entry in help.group_entries:
|
||||
help_output += (
|
||||
margin +
|
||||
entry.description.ljust(max_desc_width, ' ') +
|
||||
margin +
|
||||
', '.join(entry.keys) + '\n'
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Any, Self, Optional, List, TYPE_CHECKING
|
||||
from typing import Callable
|
||||
from typing import Any, Optional, List, TYPE_CHECKING
|
||||
from typing import Callable, ClassVar
|
||||
|
||||
from ..lib.output import unicode_ljust
|
||||
|
||||
|
|
@ -15,42 +15,51 @@ class MenuItem:
|
|||
action: Optional[Callable[[Any], Any]] = None
|
||||
enabled: bool = True
|
||||
mandatory: bool = False
|
||||
dependencies: List[Self] = field(default_factory=list)
|
||||
dependencies_not: List[Self] = field(default_factory=list)
|
||||
dependencies: List[str | Callable[[], bool]] = field(default_factory=list)
|
||||
dependencies_not: List[str] = field(default_factory=list)
|
||||
display_action: Optional[Callable[[Any], str]] = None
|
||||
preview_action: Optional[Callable[[Any], Optional[str]]] = None
|
||||
key: Optional[Any] = None
|
||||
key: Optional[str] = None
|
||||
|
||||
_yes: ClassVar[Optional['MenuItem']] = None
|
||||
_no: ClassVar[Optional['MenuItem']] = None
|
||||
|
||||
def get_value(self) -> Any:
|
||||
assert self.value is not None
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def default_yes(cls) -> Self:
|
||||
return cls(str(_('Yes')))
|
||||
def yes(cls) -> 'MenuItem':
|
||||
if cls._yes is None:
|
||||
cls._yes = cls(str(_('Yes')), value=True)
|
||||
|
||||
return cls._yes
|
||||
|
||||
@classmethod
|
||||
def default_no(cls) -> Self:
|
||||
return cls(str(_('No')))
|
||||
def no(cls) -> 'MenuItem':
|
||||
if cls._no is None:
|
||||
cls._no = cls(str(_('No')), value=True)
|
||||
|
||||
return cls._no
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return self.text == '' or self.text is None
|
||||
|
||||
def get_text(self, spacing: int = 0, suffix: str = '') -> str:
|
||||
if self.is_empty():
|
||||
return ''
|
||||
|
||||
value_text = ''
|
||||
|
||||
if self.display_action:
|
||||
value_text = self.display_action(self.value)
|
||||
def has_value(self) -> bool:
|
||||
if self.value is None:
|
||||
return False
|
||||
elif isinstance(self.value, list) and len(self.value) == 0:
|
||||
return False
|
||||
elif isinstance(self.value, dict) and len(self.value) == 0:
|
||||
return False
|
||||
else:
|
||||
if self.value is not None:
|
||||
value_text = str(self.value)
|
||||
return True
|
||||
|
||||
if value_text:
|
||||
spacing += 2
|
||||
text = unicode_ljust(str(self.text), spacing, ' ')
|
||||
else:
|
||||
text = self.text
|
||||
def get_display_value(self) -> Optional[str]:
|
||||
if self.display_action is not None:
|
||||
return self.display_action(self.value)
|
||||
|
||||
return f'{text} {value_text}{suffix}'.rstrip(' ')
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -59,11 +68,12 @@ class MenuItemGroup:
|
|||
focus_item: Optional[MenuItem] = None
|
||||
default_item: Optional[MenuItem] = None
|
||||
selected_items: List[MenuItem] = field(default_factory=list)
|
||||
sort_items: bool = True
|
||||
sort_items: bool = False
|
||||
checkmarks: bool = False
|
||||
|
||||
_filter_pattern: str = ''
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
def __post_init__(self):
|
||||
if len(self.menu_items) < 1:
|
||||
raise ValueError('Menu must have at least one item')
|
||||
|
||||
|
|
@ -79,18 +89,58 @@ class MenuItemGroup:
|
|||
if self.focus_item not in self.menu_items:
|
||||
raise ValueError('Selected item not in menu')
|
||||
|
||||
def find_by_key(self, key: str) -> MenuItem:
|
||||
for item in self.menu_items:
|
||||
if item.key == key:
|
||||
return item
|
||||
|
||||
raise ValueError(f'No key found for: {key}')
|
||||
|
||||
@staticmethod
|
||||
def default_confirm() -> 'MenuItemGroup':
|
||||
def yes_no() -> 'MenuItemGroup':
|
||||
return MenuItemGroup(
|
||||
[MenuItem.default_yes(), MenuItem.default_no()],
|
||||
sort_items=False
|
||||
[MenuItem.yes(), MenuItem.no()],
|
||||
sort_items=True
|
||||
)
|
||||
|
||||
def index_of(self, item) -> int:
|
||||
def set_preview_for_all(self, action: Callable[[Any], Optional[str]]) -> None:
|
||||
for item in self.items:
|
||||
item.preview_action = action
|
||||
|
||||
def set_focus_by_value(self, value: Any) -> None:
|
||||
for item in self.menu_items:
|
||||
if item.value == value:
|
||||
self.focus_item = item
|
||||
break
|
||||
|
||||
def set_default_by_value(self, value: Any) -> None:
|
||||
for item in self.menu_items:
|
||||
if item.value == value:
|
||||
self.default_item = item
|
||||
break
|
||||
|
||||
def set_selected_by_value(self, values: Optional[Any | List[Any]]) -> None:
|
||||
if values is None:
|
||||
return
|
||||
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
|
||||
for item in self.menu_items:
|
||||
if item.value in values:
|
||||
self.selected_items.append(item)
|
||||
|
||||
if values:
|
||||
self.set_focus_by_value(values[0])
|
||||
|
||||
def index_of(self, item: MenuItem) -> int:
|
||||
return self.items.index(item)
|
||||
|
||||
def index_focus(self) -> int:
|
||||
return self.index_of(self.focus_item)
|
||||
if self.focus_item:
|
||||
return self.index_of(self.focus_item)
|
||||
|
||||
raise ValueError('No focus item set')
|
||||
|
||||
def index_last(self) -> int:
|
||||
return self.index_of(self.items[-1])
|
||||
|
|
@ -106,8 +156,43 @@ class MenuItemGroup:
|
|||
def max_width(self) -> int:
|
||||
# use the menu_items not the items here otherwise the preview
|
||||
# will get resized all the time when a filter is applied
|
||||
return max([len(self.get_item_text(item)) for item in self.menu_items])
|
||||
|
||||
def _max_item_width(self) -> int:
|
||||
return max([len(item.text) for item in self.menu_items])
|
||||
|
||||
def get_item_text(self, item: MenuItem) -> str:
|
||||
if item.is_empty():
|
||||
return ''
|
||||
|
||||
max_width = self._max_item_width()
|
||||
display_text = item.get_display_value()
|
||||
default_text = self._default_suffix(item)
|
||||
|
||||
text = unicode_ljust(str(item.text), max_width, ' ')
|
||||
spacing = ' ' * 4
|
||||
|
||||
if display_text:
|
||||
text = f'{text}{spacing}{display_text}'
|
||||
elif self.checkmarks:
|
||||
from .types import Chars
|
||||
|
||||
if item.has_value():
|
||||
if item.get_value() is not False:
|
||||
text = f'{text}{spacing}{Chars.Check}'
|
||||
else:
|
||||
text = item.text
|
||||
|
||||
if default_text:
|
||||
text = f'{text} {default_text}'
|
||||
|
||||
return text.rstrip(' ')
|
||||
|
||||
def _default_suffix(self, item: MenuItem) -> str:
|
||||
if self.default_item == item:
|
||||
return str(_(' (default)'))
|
||||
return ''
|
||||
|
||||
@property
|
||||
def items(self) -> List[MenuItem]:
|
||||
f = self._filter_pattern.lower()
|
||||
|
|
@ -115,14 +200,14 @@ class MenuItemGroup:
|
|||
return list(items)
|
||||
|
||||
@property
|
||||
def filter_pattern(self):
|
||||
def filter_pattern(self) -> str:
|
||||
return self._filter_pattern
|
||||
|
||||
def set_filter_pattern(self, pattern: str) -> None:
|
||||
self._filter_pattern = pattern
|
||||
self.reload_focus_itme()
|
||||
|
||||
def append_filter(self, pattern: str) -> None:
|
||||
def append_filter(self, pattern: str):
|
||||
self._filter_pattern += pattern
|
||||
self.reload_focus_itme()
|
||||
|
||||
|
|
@ -189,7 +274,7 @@ class MenuItemGroup:
|
|||
if last_item:
|
||||
self.focus_item = last_item
|
||||
|
||||
def focus_prev(self, skip_empty: bool = True) -> None:
|
||||
def focus_prev(self, skip_empty: bool = True):
|
||||
items = self.items
|
||||
|
||||
if self.focus_item not in items:
|
||||
|
|
@ -203,7 +288,7 @@ class MenuItemGroup:
|
|||
if self.focus_item.is_empty() and skip_empty:
|
||||
self.focus_prev(skip_empty)
|
||||
|
||||
def focus_next(self, skip_empty: bool = True) -> None:
|
||||
def focus_next(self, skip_empty: bool = True):
|
||||
items = self.items
|
||||
|
||||
if self.focus_item not in items:
|
||||
|
|
@ -229,19 +314,21 @@ class MenuItemGroup:
|
|||
return max(spaces)
|
||||
return 0
|
||||
|
||||
def verify_item_enabled(self, item: MenuItem) -> bool:
|
||||
def should_enable_item(self, item: MenuItem) -> bool:
|
||||
if not item.enabled:
|
||||
return False
|
||||
|
||||
if item in self.menu_items:
|
||||
for dep in item.dependencies:
|
||||
if not self.verify_item_enabled(dep):
|
||||
for dep in item.dependencies:
|
||||
if isinstance(dep, str):
|
||||
item = self.find_by_key(dep)
|
||||
if not item.value or not self.should_enable_item(item):
|
||||
return False
|
||||
else:
|
||||
return dep()
|
||||
|
||||
for dep in item.dependencies_not:
|
||||
if dep.value is not None:
|
||||
return False
|
||||
for dep_not in item.dependencies_not:
|
||||
item = self.find_by_key(dep_not)
|
||||
if item.value is not None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import curses
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, List, TypeVar, Generic
|
||||
from typing import Optional, List, Any
|
||||
|
||||
from .menu_item import MenuItem
|
||||
|
||||
ItemType = TypeVar('ItemType', MenuItem, List[MenuItem], str)
|
||||
|
||||
|
||||
SCROLL_INTERVAL = 10
|
||||
|
||||
|
|
@ -46,8 +44,8 @@ class MenuKeys(Enum):
|
|||
ESC = {27}
|
||||
# BACKSPACE (search)
|
||||
BACKSPACE = {127, 263}
|
||||
# Help view: CTRL+h
|
||||
HELP = {8}
|
||||
# Help view: ?
|
||||
HELP = {63}
|
||||
# Scroll up: CTRL+up, CTRL+k
|
||||
SCROLL_UP = {581}
|
||||
# Scroll down: CTRL+down, CTRL+j
|
||||
|
|
@ -80,6 +78,22 @@ class FrameProperties:
|
|||
w_frame_style: FrameStyle = FrameStyle.MAX
|
||||
h_frame_style: FrameStyle = FrameStyle.MAX
|
||||
|
||||
@classmethod
|
||||
def max(cls, header: str) -> 'FrameProperties':
|
||||
return FrameProperties(
|
||||
header,
|
||||
FrameStyle.MAX,
|
||||
FrameStyle.MAX,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def min(cls, header: str) -> 'FrameProperties':
|
||||
return FrameProperties(
|
||||
header,
|
||||
FrameStyle.MIN,
|
||||
FrameStyle.MIN,
|
||||
)
|
||||
|
||||
|
||||
class ResultType(Enum):
|
||||
Selection = auto()
|
||||
|
|
@ -87,7 +101,7 @@ class ResultType(Enum):
|
|||
Reset = auto()
|
||||
|
||||
|
||||
class MenuOrientation(Enum):
|
||||
class Orientation(Enum):
|
||||
VERTICAL = auto()
|
||||
HORIZONTAL = auto()
|
||||
|
||||
|
|
@ -106,6 +120,7 @@ class PreviewStyle(Enum):
|
|||
|
||||
|
||||
# https://www.compart.com/en/unicode/search?q=box+drawings#characters
|
||||
# https://en.wikipedia.org/wiki/Box-drawing_characters
|
||||
class Chars:
|
||||
Horizontal = "─"
|
||||
Vertical = "│"
|
||||
|
|
@ -116,12 +131,36 @@ class Chars:
|
|||
Block = "█"
|
||||
Triangle_up = "▲"
|
||||
Triangle_down = "▼"
|
||||
Check = "+"
|
||||
Cross = "x"
|
||||
Right_arrow = "←"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result(Generic[ItemType]):
|
||||
class Result:
|
||||
type_: ResultType
|
||||
value: Optional[ItemType]
|
||||
_item: Optional[MenuItem | List[MenuItem] | str]
|
||||
|
||||
def has_item(self) -> bool:
|
||||
return self._item is not None
|
||||
|
||||
def get_value(self) -> Any:
|
||||
return self.item().get_value()
|
||||
|
||||
def get_values(self) -> List[Any]:
|
||||
return [i.get_value() for i in self.items()]
|
||||
|
||||
def item(self) -> MenuItem:
|
||||
assert self._item is not None and isinstance(self._item, MenuItem)
|
||||
return self._item
|
||||
|
||||
def items(self) -> List[MenuItem]:
|
||||
assert self._item is not None and isinstance(self._item, list)
|
||||
return self._item
|
||||
|
||||
def text(self) -> str:
|
||||
assert self._item is not None and isinstance(self._item, str)
|
||||
return self._item
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ For other installation methods refer to the docs of the dependencies.
|
|||
|
||||
## Build
|
||||
|
||||
In `archinstall/docs`, run `make html` (or specify another target) to build locally. The build files will be in `archinstall/docs/_build`. Open `_build/html/index.html` with your browser to see your changes in action.
|
||||
In `archinstall/docs`, run `make html` (or specify another target) to build locally. The build files will be in `archinstall/docs/_build`. Open `_build/html/index.html` with your browser to see your changes in action.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
.wy-nav-content {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "!layout.html" %}
|
||||
{% block extrahead %}
|
||||
<link href="{{ pathto("_static/style.css", True) }}" rel="stylesheet" type="text/css">
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -54,4 +54,4 @@ The simplest way currently is to look at a reference implementation or the commu
|
|||
And search for `plugin.on_ <https://github.com/search?q=repo%3Aarchlinux%2Farchinstall+%22plugin.on_%22&type=code>`_ in the code base to find what ``archinstall`` will look for. PR's are welcome to widen the support for this.
|
||||
|
||||
.. _plugin discovery: https://packaging.python.org/en/latest/specifications/entry-points/
|
||||
.. _entry points: https://docs.python.org/3/library/importlib.metadata.html#entry-points
|
||||
.. _entry points: https://docs.python.org/3/library/importlib.metadata.html#entry-points
|
||||
|
|
|
|||
|
|
@ -16,4 +16,4 @@ Disk encryption consists of a top level entry in the user configuration.
|
|||
}
|
||||
}
|
||||
|
||||
The ``UID`` in the ``partitions`` list is an internal reference to the ``obj_id`` in the :ref:`disk config` entries.
|
||||
The ``UID`` in the ``partitions`` list is an internal reference to the ``obj_id`` in the :ref:`disk config` entries.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Key,Value(s),Description,Required
|
||||
device,``str``,Which block-device to format,yes
|
||||
partitions,[ {key: val} ],The data describing the change/addition in a partition,yes
|
||||
wipe,``bool``,clear the disk before adding any partitions,No
|
||||
wipe,``bool``,clear the disk before adding any partitions,No
|
||||
|
|
|
|||
|
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
|
@ -1 +1 @@
|
|||
<mxfile host="Electron" modified="2021-05-02T19:57:46.193Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.5.1 Chrome/89.0.4389.82 Electron/12.0.1 Safari/537.36" etag="WWkzNgJUxTiFme1f07FW" version="14.5.1" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7VvZdqM4EP2anHlKDpsxPMZ20sl0kl7S05meNwVkYCIjt5C3+fqRjFgFNnbwksQv3a5CCKG6dWsROdP7o/knAsb+PXYhOtMUd36mD840TVUsi/3HNYtYY5tC4ZHAFYMyxWPwH0zuFNpJ4MKoMJBijGgwLiodHIbQoQUdIATPisOGGBWfOgYelBSPDkCy9ilwqR9rLa2b6W9g4PnJk1XTjq+MQDJYvEnkAxfPcir96kzvE4xp/Gs070PENy/Zl6fbxRO6ezE//fkt+g3+6n3+8fDzPJ7sepNb0lcgMKRbT/3y2bLtaXf4oFrf7ga/rYfB/dW5IV6NLpL9gi7bPiFiQn3s4RCgq0zbI3gSupDPqjIpG3OH8Vgo/4WULgQWwIRipvLpCImr7C3I4m8mKBedRPzFxUQYzAvSQkhDHNI+RpgsV6obpmF3BkwfUYJfYO5K17IuL3V+R4BQTt/T+vZlbzmve80upetxLznSmOggEEWBs5wUECoGKYmcDAtxyHcieoHU8cWAeCf59pWAt8ZqYlyEJ8SBK8bpwnkA8eCq+cwUmsynIR5BtoXsPgIRoMG0uDggnMtLx2UAYj8EhjaAqljkFKCJeNIjRNytNaWHsPMygNPAYXRQRt3MDyh8HIPlDswYExURwy0v4KSyl+t53EypYbj1E9/mo1NHVSS01gFiBYRqQLeZdaeQUDjP7b1soOSqKehG8K0lxFlGXmrCSH6OuAzl9SatpAhNMukDPrFGBUHskwzMhmSgKtVga50NKqEjs8GvCt8/IHbUA2HnbUScCpBp3mx4N50TtftTuRl3H7/9Y4fnmrGS0c6Vi65qCCw0Bp6Y7isOQpobgofDiC2mjMz0qduHLlMC69VoTPl7xCEswKEEXeLj0fOELa23JoAVQhJH1TUYBYjD7AaiKaSBAyrCHECBF3KMMHtCUh3r2COD0GOSmUk/lm7Bkru3H/7UbsP4Z7UQ/6rRXcNiCp9WU7YKhs6ETJdGkTOUgodvQXWbU1OdieE8oDnOZFJKmex3xphcSAhz2yBd4LpqSizxpiDEjFeVjVGZJ8Bq02sNw2y3koprQa5cqIZmv44QxVRGp+QvZskP4rWLu/Ll4f6YtSsza+iQxZg//Tsvn0/VQB1SOnrJvPahywHlSMqBd5nWJ02ztSlXZ13G9XqCaZUXKqGkSlA6VHmwx0j3YXBbPbCG7vbTnVJl8rqf8FY0g5Gm9AZ/yOg7Zfi7zvC1piFtdxm+Kpn9naTwbbt5coCzLjxZDfPhPbm9HGjSrjSb8HmZgvbTig6ELvsXj3nFD/jkI2ZdeupZN3dpQ99jlloJQLul1KJd723dS+tzw2N20srVqHKVuFVh8arucCGRy/K62qbFG7G0ah6VqTuSpftsv0AQ8q7aV5bGBpx6T7nY7olb7xxbLqbKzL1/GtjskOjgNGC1TQO5gxvdFj3Qo2gjVKfvbQX7nab0FWd5pYq98lCwXOQfvjJ4FQS140o6LAk5X5CbrwoUH/Cg5LBdhKHcqP64EQkQR6xU0/cYoAz14AHKOkWjlVSgNv02pemh2WaRh1EoWOQGjHkcinYRd1T5i4G0rRAfaynXtwhGi4jCkQSaU/eg7oxLb+rjO+seaHKFcmzpRFt5gAsiP11Wy0yQ7FrDD4iOJitI1p0/JuAtwNXV6RqHLgGgnA9sdiQkM0Cd3fO8sS8H3ucnqyvxlDPgbcjS7eWd/DMunuCNKvK57W0o2crSnnXTbGIrtwMt1yjZquCXzA70C0/tKN9hY5e21I0js6Xe1inxqbrbnsiblncJERwLkcv13VMwhrH/96q+PPq4Bd3OCKVUwXWMTjNG2VkFpx9dMve+2aN9Uqj+FNJUS3WEYRenqPkUsq16MHnP/LkGgoDwnIN3lUzEdrP3zGTT47/iwtA9F50mZqWKuHYipNazVaMBIRntEBITs78PjXGW/ZWtfvU/</diagram></mxfile>
|
||||
<mxfile host="Electron" modified="2021-05-02T19:57:46.193Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.5.1 Chrome/89.0.4389.82 Electron/12.0.1 Safari/537.36" etag="WWkzNgJUxTiFme1f07FW" version="14.5.1" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7VvZdqM4EP2anHlKDpsxPMZ20sl0kl7S05meNwVkYCIjt5C3+fqRjFgFNnbwksQv3a5CCKG6dWsROdP7o/knAsb+PXYhOtMUd36mD840TVUsi/3HNYtYY5tC4ZHAFYMyxWPwH0zuFNpJ4MKoMJBijGgwLiodHIbQoQUdIATPisOGGBWfOgYelBSPDkCy9ilwqR9rLa2b6W9g4PnJk1XTjq+MQDJYvEnkAxfPcir96kzvE4xp/Gs070PENy/Zl6fbxRO6ezE//fkt+g3+6n3+8fDzPJ7sepNb0lcgMKRbT/3y2bLtaXf4oFrf7ga/rYfB/dW5IV6NLpL9gi7bPiFiQn3s4RCgq0zbI3gSupDPqjIpG3OH8Vgo/4WULgQWwIRipvLpCImr7C3I4m8mKBedRPzFxUQYzAvSQkhDHNI+RpgsV6obpmF3BkwfUYJfYO5K17IuL3V+R4BQTt/T+vZlbzmve80upetxLznSmOggEEWBs5wUECoGKYmcDAtxyHcieoHU8cWAeCf59pWAt8ZqYlyEJ8SBK8bpwnkA8eCq+cwUmsynIR5BtoXsPgIRoMG0uDggnMtLx2UAYj8EhjaAqljkFKCJeNIjRNytNaWHsPMygNPAYXRQRt3MDyh8HIPlDswYExURwy0v4KSyl+t53EypYbj1E9/mo1NHVSS01gFiBYRqQLeZdaeQUDjP7b1soOSqKehG8K0lxFlGXmrCSH6OuAzl9SatpAhNMukDPrFGBUHskwzMhmSgKtVga50NKqEjs8GvCt8/IHbUA2HnbUScCpBp3mx4N50TtftTuRl3H7/9Y4fnmrGS0c6Vi65qCCw0Bp6Y7isOQpobgofDiC2mjMz0qduHLlMC69VoTPl7xCEswKEEXeLj0fOELa23JoAVQhJH1TUYBYjD7AaiKaSBAyrCHECBF3KMMHtCUh3r2COD0GOSmUk/lm7Bkru3H/7UbsP4Z7UQ/6rRXcNiCp9WU7YKhs6ETJdGkTOUgodvQXWbU1OdieE8oDnOZFJKmex3xphcSAhz2yBd4LpqSizxpiDEjFeVjVGZJ8Bq02sNw2y3koprQa5cqIZmv44QxVRGp+QvZskP4rWLu/Ll4f6YtSsza+iQxZg//Tsvn0/VQB1SOnrJvPahywHlSMqBd5nWJ02ztSlXZ13G9XqCaZUXKqGkSlA6VHmwx0j3YXBbPbCG7vbTnVJl8rqf8FY0g5Gm9AZ/yOg7Zfi7zvC1piFtdxm+Kpn9naTwbbt5coCzLjxZDfPhPbm9HGjSrjSb8HmZgvbTig6ELvsXj3nFD/jkI2ZdeupZN3dpQ99jlloJQLul1KJd723dS+tzw2N20srVqHKVuFVh8arucCGRy/K62qbFG7G0ah6VqTuSpftsv0AQ8q7aV5bGBpx6T7nY7olb7xxbLqbKzL1/GtjskOjgNGC1TQO5gxvdFj3Qo2gjVKfvbQX7nab0FWd5pYq98lCwXOQfvjJ4FQS140o6LAk5X5CbrwoUH/Cg5LBdhKHcqP64EQkQR6xU0/cYoAz14AHKOkWjlVSgNv02pemh2WaRh1EoWOQGjHkcinYRd1T5i4G0rRAfaynXtwhGi4jCkQSaU/eg7oxLb+rjO+seaHKFcmzpRFt5gAsiP11Wy0yQ7FrDD4iOJitI1p0/JuAtwNXV6RqHLgGgnA9sdiQkM0Cd3fO8sS8H3ucnqyvxlDPgbcjS7eWd/DMunuCNKvK57W0o2crSnnXTbGIrtwMt1yjZquCXzA70C0/tKN9hY5e21I0js6Xe1inxqbrbnsiblncJERwLkcv13VMwhrH/96q+PPq4Bd3OCKVUwXWMTjNG2VkFpx9dMve+2aN9Uqj+FNJUS3WEYRenqPkUsq16MHnP/LkGgoDwnIN3lUzEdrP3zGTT47/iwtA9F50mZqWKuHYipNazVaMBIRntEBITs78PjXGW/ZWtfvU/</diagram></mxfile>
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ Below is an example of how to set the root password and below that are descripti
|
|||
{
|
||||
"username": "<USERNAME>",
|
||||
"!password": "<PASSWORD>",
|
||||
"sudo": false
|
||||
"sudo": false
|
||||
}
|
||||
- List of regular user credentials, see configuration for reference
|
||||
- Maybe
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
## Tests and Checks
|
||||
- [ ] I have tested the code!<br>
|
||||
<!--
|
||||
<!--
|
||||
After submitting your PR, an ISO can be downloaded below the PR description. After testing it you can check the box
|
||||
You can do manual tests too, like isolated function tests, just something!
|
||||
-->
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from archinstall.default_profiles.minimal import MinimalProfile
|
|||
from archinstall import disk
|
||||
from archinstall import models
|
||||
|
||||
|
||||
# we're creating a new ext4 filesystem installation
|
||||
fs_type = disk.FilesystemType('ext4')
|
||||
device_path = Path('/dev/sda')
|
||||
|
|
|
|||
|
|
@ -1,79 +1,40 @@
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
from typing import Optional
|
||||
|
||||
import archinstall
|
||||
from archinstall import Installer
|
||||
from archinstall import profile
|
||||
from archinstall import SysInfo
|
||||
from archinstall import disk
|
||||
from archinstall import menu
|
||||
from archinstall import models
|
||||
from archinstall import locale
|
||||
from archinstall import info, debug
|
||||
from archinstall import SysInfo
|
||||
from archinstall.lib import locale
|
||||
from archinstall.lib import disk
|
||||
from archinstall.lib.global_menu import GlobalMenu
|
||||
from archinstall.lib.configuration import ConfigurationOutput
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.models import AudioConfiguration, Bootloader
|
||||
from archinstall.lib.models.network_configuration import NetworkConfiguration
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.lib.interactions.general_conf import ask_chroot
|
||||
from archinstall.tui import Tui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Callable[[str], str]
|
||||
|
||||
if archinstall.arguments.get('help'):
|
||||
print("See `man archinstall` for help.")
|
||||
exit(0)
|
||||
|
||||
|
||||
def ask_user_questions() -> None:
|
||||
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
|
||||
"""
|
||||
First, we'll ask the user for a bunch of user input.
|
||||
Not until we're satisfied with what we want to install
|
||||
will we continue with the actual installation steps.
|
||||
"""
|
||||
|
||||
global_menu.enable('archinstall-language')
|
||||
with Tui():
|
||||
global_menu = GlobalMenu(data_store=archinstall.arguments)
|
||||
|
||||
# Set which region to download packages from during the installation
|
||||
global_menu.enable('mirror_config')
|
||||
if not archinstall.arguments.get('advanced', False):
|
||||
global_menu.set_enabled('parallel downloads', False)
|
||||
|
||||
global_menu.enable('locale_config')
|
||||
|
||||
global_menu.enable('disk_config', mandatory=True)
|
||||
|
||||
# Specify disk encryption options
|
||||
global_menu.enable('disk_encryption')
|
||||
|
||||
# Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB)
|
||||
global_menu.enable('bootloader')
|
||||
|
||||
global_menu.enable('swap')
|
||||
|
||||
# Get the hostname for the machine
|
||||
global_menu.enable('hostname')
|
||||
|
||||
# Ask for a root password (optional, but triggers requirement for super-user if skipped)
|
||||
global_menu.enable('!root-password', mandatory=True)
|
||||
|
||||
global_menu.enable('!users', mandatory=True)
|
||||
|
||||
# Ask for archinstall-specific profiles_bck (such as desktop environments etc)
|
||||
global_menu.enable('profile_config')
|
||||
|
||||
# Ask about audio server selection if one is not already set
|
||||
global_menu.enable('audio_config')
|
||||
|
||||
# Ask for preferred kernel:
|
||||
global_menu.enable('kernels', mandatory=True)
|
||||
|
||||
global_menu.enable('packages')
|
||||
|
||||
if archinstall.arguments.get('advanced', False):
|
||||
# Enable parallel downloads
|
||||
global_menu.enable('parallel downloads')
|
||||
|
||||
# Ask or Call the helper function that asks the user to optionally configure a network.
|
||||
global_menu.enable('network_config')
|
||||
|
||||
global_menu.enable('timezone')
|
||||
|
||||
global_menu.enable('ntp')
|
||||
|
||||
global_menu.enable('additional-repositories')
|
||||
|
||||
global_menu.enable('__separator__')
|
||||
|
||||
global_menu.enable('save_config')
|
||||
global_menu.enable('install')
|
||||
global_menu.enable('abort')
|
||||
|
||||
global_menu.run()
|
||||
global_menu.run()
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path) -> None:
|
||||
|
|
@ -88,6 +49,7 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
# Retrieve list of additional repositories and set boolean values appropriately
|
||||
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
|
||||
enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', [])
|
||||
run_mkinitcpio = not archinstall.arguments.get('uki')
|
||||
locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
|
||||
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
|
||||
|
||||
|
|
@ -109,11 +71,12 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
installation.generate_key_files()
|
||||
|
||||
if mirror_config := archinstall.arguments.get('mirror_config', None):
|
||||
installation.set_mirrors(mirror_config)
|
||||
installation.set_mirrors(mirror_config, on_target=False)
|
||||
|
||||
installation.minimal_installation(
|
||||
testing=enable_testing,
|
||||
multilib=enable_multilib,
|
||||
mkinitcpio=run_mkinitcpio,
|
||||
hostname=archinstall.arguments.get('hostname', 'archlinux'),
|
||||
locale_config=locale_config
|
||||
)
|
||||
|
|
@ -124,14 +87,17 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
if archinstall.arguments.get('swap'):
|
||||
installation.setup_swap('zram')
|
||||
|
||||
if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi():
|
||||
if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi():
|
||||
installation.add_additional_packages("grub")
|
||||
|
||||
installation.add_bootloader(archinstall.arguments["bootloader"])
|
||||
installation.add_bootloader(
|
||||
archinstall.arguments["bootloader"],
|
||||
archinstall.arguments.get('uki', False)
|
||||
)
|
||||
|
||||
# If user selected to copy the current ISO network configuration
|
||||
# Perform a copy of the config
|
||||
network_config = archinstall.arguments.get('network_config', None)
|
||||
network_config: Optional[NetworkConfiguration] = archinstall.arguments.get('network_config', None)
|
||||
|
||||
if network_config:
|
||||
network_config.install_network_config(
|
||||
|
|
@ -142,17 +108,17 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
if users := archinstall.arguments.get('!users', None):
|
||||
installation.create_users(users)
|
||||
|
||||
audio_config: Optional[models.AudioConfiguration] = archinstall.arguments.get('audio_config', None)
|
||||
audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None)
|
||||
if audio_config:
|
||||
audio_config.install_audio_config(installation)
|
||||
else:
|
||||
info("No audio server will be installed")
|
||||
|
||||
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
|
||||
installation.add_additional_packages(archinstall.arguments.get('packages', []))
|
||||
installation.add_additional_packages(archinstall.arguments.get('packages', None))
|
||||
|
||||
if profile_config := archinstall.arguments.get('profile_config', None):
|
||||
profile.profile_handler.install_profile_config(installation, profile_config)
|
||||
profile_handler.install_profile_config(installation, profile_config)
|
||||
|
||||
if timezone := archinstall.arguments.get('timezone', None):
|
||||
installation.set_timezone(timezone)
|
||||
|
|
@ -183,24 +149,42 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
|
||||
|
||||
if not archinstall.arguments.get('silent'):
|
||||
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?'))
|
||||
choice = menu.Menu(prompt, menu.Menu.yes_no(), default_option=menu.Menu.yes()).run()
|
||||
if choice.value == menu.Menu.yes():
|
||||
with Tui():
|
||||
chroot = ask_chroot()
|
||||
|
||||
if chroot:
|
||||
try:
|
||||
installation.drop_to_shell()
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
|
||||
debug(f"Disk states after installing: {disk.disk_layouts()}")
|
||||
|
||||
|
||||
ask_user_questions()
|
||||
def _guided() -> None:
|
||||
if not archinstall.arguments.get('silent'):
|
||||
ask_user_questions()
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
config = ConfigurationOutput(archinstall.arguments)
|
||||
config.write_debug()
|
||||
config.save()
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
if not archinstall.arguments.get('silent'):
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
_guided()
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
|
||||
|
||||
_guided()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import time
|
|||
|
||||
import archinstall
|
||||
from archinstall import profile, info
|
||||
from archinstall.tui import Tui
|
||||
|
||||
|
||||
for _profile in profile.profile_handler.get_mac_addr_profiles():
|
||||
# Tailored means it's a match for this machine
|
||||
|
|
@ -11,7 +13,7 @@ for _profile in profile.profile_handler.get_mac_addr_profiles():
|
|||
|
||||
print('Starting install in:')
|
||||
for i in range(10, 0, -1):
|
||||
print(f'{i}...')
|
||||
Tui.print(f'{i}...')
|
||||
time.sleep(1)
|
||||
|
||||
install_session = archinstall.storage['installation_session']
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, List
|
||||
from typing import List
|
||||
|
||||
import archinstall
|
||||
from archinstall import disk
|
||||
from archinstall import Installer
|
||||
from archinstall import profile
|
||||
from archinstall import models
|
||||
from archinstall import interactions
|
||||
from archinstall import info, debug
|
||||
from archinstall import Installer, ConfigurationOutput
|
||||
from archinstall.default_profiles.minimal import MinimalProfile
|
||||
from archinstall.lib.interactions import suggest_single_disk_layout, select_devices
|
||||
from archinstall.lib.models import Bootloader, User
|
||||
from archinstall.lib.profile import ProfileConfiguration, profile_handler
|
||||
from archinstall.lib import disk
|
||||
from archinstall.tui import Tui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_: Callable[[str], str]
|
||||
|
||||
info("Minimal only supports:")
|
||||
info(" * Being installed to a single disk")
|
||||
|
||||
if archinstall.arguments.get('help', None):
|
||||
info(" - Optional disk encryption via --!encryption-password=<password>")
|
||||
info(" - Optional filesystem type via --filesystem=<fs type>")
|
||||
info(" - Optional systemd network via --network")
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path) -> None:
|
||||
|
|
@ -27,7 +35,7 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
# some other minor details as specified by this profile and user.
|
||||
if installation.minimal_installation():
|
||||
installation.set_hostname('minimal-arch')
|
||||
installation.add_bootloader(models.Bootloader.Systemd)
|
||||
installation.add_bootloader(Bootloader.Systemd)
|
||||
|
||||
# Optionally enable networking:
|
||||
if archinstall.arguments.get('network', None):
|
||||
|
|
@ -35,20 +43,26 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
|
||||
installation.add_additional_packages(['nano', 'wget', 'git'])
|
||||
|
||||
profile_config = profile.ProfileConfiguration(MinimalProfile())
|
||||
profile.profile_handler.install_profile_config(installation, profile_config)
|
||||
profile_config = ProfileConfiguration(MinimalProfile())
|
||||
profile_handler.install_profile_config(installation, profile_config)
|
||||
|
||||
user = models.User('devel', 'devel', False)
|
||||
user = User('devel', 'devel', False)
|
||||
installation.create_users(user)
|
||||
|
||||
# Once this is done, we output some useful information to the user
|
||||
# And the installation is complete.
|
||||
info("There are two new accounts in your installation after reboot:")
|
||||
info(" * root (password: airoot)")
|
||||
info(" * devel (password: devel)")
|
||||
|
||||
|
||||
def prompt_disk_layout() -> None:
|
||||
fs_type = None
|
||||
if filesystem := archinstall.arguments.get('filesystem', None):
|
||||
fs_type = disk.FilesystemType(filesystem)
|
||||
|
||||
devices = interactions.select_devices()
|
||||
modifications = interactions.suggest_single_disk_layout(devices[0], filesystem_type=fs_type)
|
||||
devices = select_devices()
|
||||
modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type)
|
||||
|
||||
archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration(
|
||||
config_type=disk.DiskLayoutType.Default,
|
||||
|
|
@ -72,15 +86,31 @@ def parse_disk_encryption() -> None:
|
|||
)
|
||||
|
||||
|
||||
prompt_disk_layout()
|
||||
parse_disk_encryption()
|
||||
def _minimal() -> None:
|
||||
with Tui():
|
||||
prompt_disk_layout()
|
||||
parse_disk_encryption()
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
config = ConfigurationOutput(archinstall.arguments)
|
||||
config.write_debug()
|
||||
config.save()
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
|
||||
mount_point = Path('/mnt')
|
||||
perform_installation(mount_point)
|
||||
if not archinstall.arguments.get('silent'):
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
_minimal()
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
|
||||
|
||||
_minimal()
|
||||
|
|
|
|||
|
|
@ -1,23 +1,27 @@
|
|||
from pathlib import Path
|
||||
|
||||
import archinstall
|
||||
from archinstall import Installer, disk, debug
|
||||
from archinstall import debug
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.configuration import ConfigurationOutput
|
||||
from archinstall.lib import disk
|
||||
from archinstall.tui import Tui
|
||||
|
||||
|
||||
def ask_user_questions() -> None:
|
||||
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
|
||||
with Tui():
|
||||
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
|
||||
global_menu.disable_all()
|
||||
|
||||
global_menu.enable('archinstall-language')
|
||||
global_menu.set_enabled('archinstall-language', True)
|
||||
global_menu.set_enabled('disk_config', True)
|
||||
global_menu.set_enabled('disk_encryption', True)
|
||||
global_menu.set_enabled('swap', True)
|
||||
global_menu.set_enabled('save_config', True)
|
||||
global_menu.set_enabled('install', True)
|
||||
global_menu.set_enabled('abort', True)
|
||||
|
||||
global_menu.enable('disk_config', mandatory=True)
|
||||
global_menu.enable('disk_encryption')
|
||||
global_menu.enable('swap')
|
||||
|
||||
global_menu.enable('save_config')
|
||||
global_menu.enable('install')
|
||||
global_menu.enable('abort')
|
||||
|
||||
global_menu.run()
|
||||
global_menu.run()
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path) -> None:
|
||||
|
|
@ -49,13 +53,30 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
debug(f"Disk states after installing: {disk.disk_layouts()}")
|
||||
|
||||
|
||||
ask_user_questions()
|
||||
def _only_hd() -> None:
|
||||
if not archinstall.arguments.get('silent'):
|
||||
ask_user_questions()
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
config = ConfigurationOutput(archinstall.arguments)
|
||||
config.write_debug()
|
||||
config.save()
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
if archinstall.arguments.get('dry_run'):
|
||||
exit(0)
|
||||
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
if not archinstall.arguments.get('silent'):
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
_only_hd()
|
||||
|
||||
fs_handler = disk.FilesystemHandler(
|
||||
archinstall.arguments['disk_config'],
|
||||
archinstall.arguments.get('disk_encryption', None)
|
||||
)
|
||||
|
||||
fs_handler.perform_filesystem_operations()
|
||||
perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')))
|
||||
|
||||
|
||||
_only_hd()
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ module = [
|
|||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "archinstall.lib.models.mirrors"
|
||||
disallow_untyped_decorators = false
|
||||
disallow_subclassing_any = false
|
||||
|
||||
[tool.bandit]
|
||||
targets = ["archinstall"]
|
||||
exclude = ["/tests"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue