diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index 63b68b9ce4..e99d7c0d23 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -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 @@ -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 @@ -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: @@ -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: @@ -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( diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 9f7ec10398..00467262d5 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -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, @@ -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 @@ -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', ), ] @@ -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 diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index a91aa4272d..6ca3bddc25 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -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 @@ -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: + """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: @@ -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() diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index 2c0c26c2f0..f549dfd148 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -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 @@ -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() diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py index b63ee94072..998895031a 100644 --- a/archinstall/scripts/only_hd.py +++ b/archinstall/scripts/only_hd.py @@ -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 @@ -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() diff --git a/tests/test_configuration_output.py b/tests/test_configuration_output.py index 7d9f1f97cf..08e2628261 100644 --- a/tests/test_configuration_output.py +++ b/tests/test_configuration_output.py @@ -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( @@ -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 @@ -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