Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 62 additions & 4 deletions archinstall/lib/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import readline
import stat
from pathlib import Path
from typing import Any

from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu, Tui
Expand All @@ -16,7 +17,10 @@
from .utils.util import get_password, prompt_dir


class ConfigurationOutput:
class ConfigurationHandler:
_USER_CONFIG_FILENAME = 'user_configuration.json'
_USER_CREDS_FILENAME = 'user_credentials.json'

def __init__(self, config: ArchConfig):
"""
Configuration output handler to parse the existing
Expand All @@ -29,8 +33,8 @@ def __init__(self, config: ArchConfig):

self._config = config
self._default_save_path = logger.directory
self._user_config_file = Path('user_configuration.json')
self._user_creds_file = Path('user_credentials.json')
self._user_config_file = Path(self._USER_CONFIG_FILENAME)
self._user_creds_file = Path(self._USER_CREDS_FILENAME)

@property
def user_configuration_file(self) -> Path:
Expand Down Expand Up @@ -121,6 +125,60 @@ def save(
if creds:
self.save_user_creds(save_path, password=password)

@classmethod
def has_saved_config(cls) -> bool:
config_file = logger.directory / cls._USER_CONFIG_FILENAME
return config_file.exists()

@classmethod
def load_saved_config(cls) -> dict[str, Any] | None:
try:
config_data: dict[str, Any] = {}

# Load main config
config_file = logger.directory / cls._USER_CONFIG_FILENAME
if config_file.exists():
with open(config_file) as f:
config_data.update(json.load(f))

# Load credentials
creds_file = logger.directory / cls._USER_CREDS_FILENAME
if creds_file.exists():
with open(creds_file) as f:
creds_data = json.load(f)
config_data.update(creds_data)

return config_data if config_data else None

except Exception as e:
warn(f'Failed to load saved config: {e}')
return None

def auto_save_config(self) -> tuple[bool, list[str]]:
"""Automatically save config to /var/log/archinstall without prompts

Returns:
tuple[bool, list[str]]: (success, list of saved files)
"""
try:
save_path = logger.directory
save_path.mkdir(exist_ok=True, parents=True)

saved_files: list[str] = []

# Save configuration
self.save_user_config(save_path)
saved_files.append(str(save_path / self._user_config_file))

# Save credentials
self.save_user_creds(save_path, password=None)
saved_files.append(str(save_path / self._user_creds_file))

return True, saved_files
except Exception as e:
debug(f'Failed to auto-save config: {e}')
return False, []


def save_config(config: ArchConfig) -> None:
def preview(item: MenuItem) -> str | None:
Expand All @@ -139,7 +197,7 @@ def preview(item: MenuItem) -> str | None:
return '\n'.join(output)
return None

config_output = ConfigurationOutput(config)
config_output = ConfigurationHandler(config)

items = [
MenuItem(
Expand Down
57 changes: 54 additions & 3 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification
from archinstall.lib.packages import list_available_packages
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment

from .applications.application_menu import ApplicationMenu
from .args import ArchConfig
from .authentication.authentication_menu import AuthenticationMenu
from .bootloader.bootloader_menu import BootloaderMenu
from .configuration import save_config
from .configuration import ConfigurationHandler, save_config
from .hardware import SysInfo
from .interactions.general_conf import (
add_number_of_parallel_downloads,
Expand All @@ -33,7 +36,7 @@
from .models.network import NetworkConfiguration, NicType
from .models.packages import Repository
from .models.profile import ProfileConfiguration
from .output import FormattedOutput
from .output import FormattedOutput, info, warn
from .pacman.config import PacmanConfig
from .translationhandler import Language, tr, translation_handler

Expand Down Expand Up @@ -176,7 +179,7 @@ def _get_menu_options(self) -> list[MenuItem]:
),
MenuItem(
text=tr('Abort'),
action=lambda x: exit(1),
action=self._handle_abort,
key=f'{CONFIG_KEY}_abort',
),
]
Expand Down Expand Up @@ -570,3 +573,51 @@ def _prev_mirror_config(self, item: MenuItem) -> str | None:
output += f'{title}:\n\n{table}'

return output.strip()

def _handle_abort(self, preset: None) -> None:
"""Handle abort with option to save selections"""
from .args import arch_config_handler

items = []

# Only show save option in debug mode
if arch_config_handler.args.debug:
items.append(MenuItem(text=tr('Save selections and abort'), value='save_abort'))
items.append(MenuItem(text=tr('Abort without saving'), value='abort_only'))
else:
items.append(MenuItem(text=tr('Abort'), value='abort_only'))

items.append(MenuItem(text=tr('Cancel'), value='cancel'))

group = MenuItemGroup(items)
group.focus_item = group.items[0] # Focus on first option

result = SelectMenu[str](
group,
header=tr('Abort the installation? \n'),
alignment=Alignment.CENTER,
allow_skip=False,
).run()

if result.type_ == ResultType.Selection:
choice = result.get_value()

if choice == 'save_abort':
# Sync current selections to config before saving
self.sync_all_to_config()
config_output = ConfigurationHandler(self._arch_config)
success, _ = config_output.auto_save_config()
if success:
# Check if credentials are actually present (not just empty JSON)
creds_json = config_output.user_credentials_to_json()
has_creds = creds_json and creds_json.strip() != '{}'
creds_status = 'saved' if has_creds else 'empty'
info(f'Configuration saved: user_configuration.json, user_credentials.json ({creds_status}).')
else:
warn('Failed to save selections.')
exit(1)
elif choice == 'abort_only':
exit(1)
# If 'cancel', just return to menu

return None
59 changes: 55 additions & 4 deletions archinstall/scripts/guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from archinstall import SysInfo
from archinstall.lib.applications.application_handler import application_handler
from archinstall.lib.args import arch_config_handler
from archinstall.lib.args import ArchConfig, arch_config_handler
from archinstall.lib.authentication.authentication_handler import auth_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.configuration import ConfigurationHandler
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.global_menu import GlobalMenu
Expand All @@ -18,11 +18,61 @@
EncryptionType,
)
from archinstall.lib.models.users import User
from archinstall.lib.output import debug, error, info
from archinstall.lib.output import debug, error, info, logger
from archinstall.lib.packages.packages import check_package_upgrade
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.translationhandler import tr
from archinstall.tui import Tui
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment


def _check_for_saved_config() -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want this feature it should be usable in all the provided scripts that ship, so this should go into a generic file so it can be re-used by any script.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it only makes sense for guided ? I integrated it to the Class and renamed as you asked
that already makes it re-usable. There are also safeguards for silent and check that this only happens in --debug

Altho it does change the exit behavior slightly with an extra Abort / Cancel which isn't necessarily a bad thing

"""Check for saved config and offer to resume"""
if not arch_config_handler.args.debug:
return
if not ConfigurationHandler.has_saved_config() or arch_config_handler.args.silent:
return

with Tui():
items = [
MenuItem(text=('Resume from saved selections'), value='resume'),
MenuItem(text=('Start fresh'), value='fresh'),
]

group = MenuItemGroup(items)
group.focus_item = group.items[0] # Focus on resume

result = SelectMenu[str](
group,
header=('Saved configuration found: \n'),
alignment=Alignment.CENTER,
allow_skip=False,
).run()

if result.type_ == ResultType.Selection:
choice = result.get_value()

if choice == 'resume':
cached_config = ConfigurationHandler.load_saved_config()
if cached_config:
try:
new_config = ArchConfig.from_config(cached_config, arch_config_handler.args)
arch_config_handler._config = new_config
info('Saved selections loaded successfully')
except Exception as e:
error(f'Failed to load saved selections: {e}')
elif choice == 'fresh':
# Remove both saved config files
config_file = logger.directory / 'user_configuration.json'
creds_file = logger.directory / 'user_credentials.json'

if config_file.exists():
config_file.unlink()
if creds_file.exists():
creds_file.unlink()


def ask_user_questions() -> None:
Expand Down Expand Up @@ -187,9 +237,10 @@ def perform_installation(mountpoint: Path) -> None:

def guided() -> None:
if not arch_config_handler.args.silent:
_check_for_saved_config()
ask_user_questions()

config = ConfigurationOutput(arch_config_handler.config)
config = ConfigurationHandler(arch_config_handler.config)
config.write_debug()
config.save()

Expand Down
4 changes: 2 additions & 2 deletions archinstall/scripts/minimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from archinstall.default_profiles.minimal import MinimalProfile
from archinstall.lib.args import arch_config_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.configuration import ConfigurationHandler
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.installer import Installer
Expand Down Expand Up @@ -64,7 +64,7 @@ def _minimal() -> None:
disk_config = DiskLayoutConfigurationMenu(disk_layout_config=None).run()
arch_config_handler.config.disk_config = disk_config

config = ConfigurationOutput(arch_config_handler.config)
config = ConfigurationHandler(arch_config_handler.config)
config.write_debug()
config.save()

Expand Down
4 changes: 2 additions & 2 deletions archinstall/scripts/only_hd.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from archinstall import debug, error
from archinstall.lib.args import arch_config_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.configuration import ConfigurationHandler
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.global_menu import GlobalMenu
Expand Down Expand Up @@ -60,7 +60,7 @@ def _only_hd() -> None:
if not arch_config_handler.args.silent:
ask_user_questions()

config = ConfigurationOutput(arch_config_handler.config)
config = ConfigurationHandler(arch_config_handler.config)
config.write_debug()
config.save()

Expand Down
6 changes: 3 additions & 3 deletions tests/test_configuration_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pytest import MonkeyPatch

from archinstall.lib.args import ArchConfigHandler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.configuration import ConfigurationHandler


def test_user_config_roundtrip(
Expand All @@ -20,7 +20,7 @@ def test_user_config_roundtrip(
# as there is no version present in the test environment we'll set it manually
arch_config.version = '3.0.2'

config_output = ConfigurationOutput(arch_config)
config_output = ConfigurationHandler(arch_config)

test_out_dir = Path('/tmp/')
test_out_file = test_out_dir / config_output.user_configuration_file
Expand Down Expand Up @@ -55,7 +55,7 @@ def test_creds_roundtrip(
handler = ArchConfigHandler()
arch_config = handler.config

config_output = ConfigurationOutput(arch_config)
config_output = ConfigurationHandler(arch_config)

test_out_dir = Path('/tmp/')
test_out_file = test_out_dir / config_output.user_credentials_file
Expand Down