diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml
index ab40754f..92061f64 100644
--- a/.github/ISSUE_TEMPLATE/01_bug.yml
+++ b/.github/ISSUE_TEMPLATE/01_bug.yml
@@ -41,8 +41,8 @@ body:
attributes:
value: >
**Note**: Assuming you have network connectivity,
- you can easily post the installation log using the following command:
- `curl -F'file=@/var/log/archinstall/install.log' https://0x0.st`
+ you can easily upload the installation log and get a shareable URL by running:
+ `archinstall share-log`
- type: textarea
id: freeform
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7ee79056..c573ae04 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -31,7 +31,7 @@ repos:
args: [--config=.flake8]
fail_fast: true
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.20.2
+ rev: v2.1.0
hooks:
- id: mypy
args: [
diff --git a/README.md b/README.md
index 95d15702..4e6847f4 100644
--- a/README.md
+++ b/README.md
@@ -101,9 +101,9 @@ If you come across any issues, kindly submit your issue here on GitHub or post y
When submitting an issue, please:
* Provide the stacktrace of the output if applicable
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
- * To extract the log from the ISO image, one way is to use
+ * To upload the log from the ISO image and get a shareable URL, run
```shell
- curl -F'file=@/var/log/archinstall/install.log' https://0x0.st
+ archinstall share-log
```
diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py
index 19681bc7..daf70831 100644
--- a/archinstall/default_profiles/desktop.py
+++ b/archinstall/default_profiles/desktop.py
@@ -9,6 +9,7 @@ from archinstall.tui.result import ResultType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
+ from archinstall.lib.models.users import User
class DesktopProfile(Profile):
@@ -88,6 +89,11 @@ class DesktopProfile(Profile):
for profile in self.current_selection:
profile.post_install(install_session)
+ @override
+ def provision(self, install_session: Installer, users: list[User]) -> None:
+ for profile in self.current_selection:
+ profile.provision(install_session, users)
+
@override
def install(self, install_session: Installer) -> None:
# Install common packages for all desktop environments
diff --git a/archinstall/default_profiles/desktops/bspwm.py b/archinstall/default_profiles/desktops/bspwm.py
index f8c2a7b2..8886298a 100644
--- a/archinstall/default_profiles/desktops/bspwm.py
+++ b/archinstall/default_profiles/desktops/bspwm.py
@@ -1,6 +1,8 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
+from archinstall.lib.installer import Installer
+from archinstall.lib.models.users import User
class BspwmProfile(Profile):
@@ -27,3 +29,11 @@ class BspwmProfile(Profile):
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm
+
+ @override
+ def provision(self, install_session: Installer, users: list[User]) -> None:
+ for user in users:
+ install_session.arch_chroot('mkdir -p ~/.config/bspwm ~/.config/sxhkd', run_as=user.username)
+ install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/bspwmrc ~/.config/bspwm/', run_as=user.username)
+ install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/sxhkdrc ~/.config/sxhkd/', run_as=user.username)
+ install_session.arch_chroot('chmod +x ~/.config/bspwm/bspwmrc', run_as=user.username)
diff --git a/archinstall/default_profiles/desktops/budgie.py b/archinstall/default_profiles/desktops/budgie.py
index f156a53b..da147851 100644
--- a/archinstall/default_profiles/desktops/budgie.py
+++ b/archinstall/default_profiles/desktops/budgie.py
@@ -18,12 +18,13 @@ class BudgieProfile(Profile):
return [
'materia-gtk-theme',
'budgie',
- 'konsole',
- 'dolphin',
+ 'mate-terminal',
+ 'nemo',
+ 'nemo-fileroller',
'papirus-icon-theme',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
- return GreeterType.Sddm
+ return GreeterType.LightdmSlick
diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py
index 2d9b4489..b65024d3 100644
--- a/archinstall/lib/args.py
+++ b/archinstall/lib/args.py
@@ -6,6 +6,7 @@ import urllib.error
import urllib.parse
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass, field
+from enum import StrEnum
from pathlib import Path
from typing import Any, Self
from urllib.request import Request, urlopen
@@ -17,6 +18,7 @@ from archinstall.lib.menu.util import get_password
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration
@@ -57,121 +59,222 @@ class Arguments:
verbose: bool = False
+class ArchConfigType(StrEnum):
+ VERSION = 'version'
+ SCRIPT = 'script'
+ LOCALE_CONFIG = 'locale_config'
+ ARCHINSTALL_LANGUAGE = 'archinstall_language'
+ DISK_CONFIG = 'disk_config'
+ PROFILE_CONFIG = 'profile_config'
+ MIRROR_CONFIG = 'mirror_config'
+ NETWORK_CONFIG = 'network_config'
+ BOOTLOADER_CONFIG = 'bootloader_config'
+ APP_CONFIG = 'app_config'
+ AUTH_CONFIG = 'auth_config'
+ SWAP = 'swap'
+ USERS = 'users'
+ ROOT_ENC_PASSWORD = 'root_enc_password'
+ ENCRYPTION_PASSWORD = 'encryption_password'
+ HOSTNAME = 'hostname'
+ KERNELS = 'kernels'
+ NTP = 'ntp'
+ TIMEZONE = 'timezone'
+ SERVICES = 'services'
+ PACKAGES = 'packages'
+ PACMAN_CONFIG = 'pacman_config'
+ CUSTOM_COMMANDS = 'custom_commands'
+
+ def text(self) -> str:
+ match self:
+ case ArchConfigType.ARCHINSTALL_LANGUAGE:
+ return tr('ArchInstall Language')
+ case ArchConfigType.VERSION:
+ return tr('Version')
+ case ArchConfigType.SCRIPT:
+ return tr('Installation Script')
+ case ArchConfigType.LOCALE_CONFIG:
+ return tr('Locales')
+ case ArchConfigType.DISK_CONFIG:
+ return tr('Disk configuration')
+ case ArchConfigType.PROFILE_CONFIG:
+ return tr('Profile')
+ case ArchConfigType.MIRROR_CONFIG:
+ return tr('Mirrors and repositories')
+ case ArchConfigType.NETWORK_CONFIG:
+ return tr('Network')
+ case ArchConfigType.BOOTLOADER_CONFIG:
+ return tr('Bootloader')
+ case ArchConfigType.APP_CONFIG:
+ return tr('Application')
+ case ArchConfigType.AUTH_CONFIG:
+ return tr('Authentication')
+ case ArchConfigType.SWAP:
+ return tr('Swap')
+ case ArchConfigType.HOSTNAME:
+ return tr('Hostname')
+ case ArchConfigType.KERNELS:
+ return tr('Kernels')
+ case ArchConfigType.NTP:
+ return tr('Automatic time sync (NTP)')
+ case ArchConfigType.TIMEZONE:
+ return tr('Timezone')
+ case ArchConfigType.SERVICES:
+ return tr('Services')
+ case ArchConfigType.PACKAGES:
+ return tr('Additional packages')
+ case ArchConfigType.PACMAN_CONFIG:
+ return tr('Pacman')
+ case ArchConfigType.CUSTOM_COMMANDS:
+ return tr('Custom commands')
+ case ArchConfigType.USERS:
+ return tr('Users')
+ case ArchConfigType.ROOT_ENC_PASSWORD:
+ return tr('Root encrypted password')
+ case ArchConfigType.ENCRYPTION_PASSWORD:
+ return tr('Disk encryption password')
+
+
@dataclass
class ArchConfig:
- version: str | None = None
- script: str | None = None
- locale_config: LocaleConfiguration | None = None
- archinstall_language: Language = field(default_factory=lambda: translation_handler.get_language_by_abbr('en'))
- disk_config: DiskLayoutConfiguration | None = None
- profile_config: ProfileConfiguration | None = None
- mirror_config: MirrorConfiguration | None = None
- network_config: NetworkConfiguration | None = None
- bootloader_config: BootloaderConfiguration | None = None
- app_config: ApplicationConfiguration | None = None
- auth_config: AuthenticationConfiguration | None = None
- swap: ZramConfiguration | None = None
- hostname: str = 'archlinux'
- kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
- ntp: bool = True
- packages: list[str] = field(default_factory=list)
- pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
- timezone: str = 'UTC'
- services: list[str] = field(default_factory=list)
- custom_commands: list[str] = field(default_factory=list)
+ version: str | None = None
+ script: str | None = None
+ locale_config: LocaleConfiguration | None = None
+ archinstall_language: Language = field(default_factory=lambda: translation_handler.get_language_by_abbr('en'))
+ disk_config: DiskLayoutConfiguration | None = None
+ profile_config: ProfileConfiguration | None = None
+ mirror_config: MirrorConfiguration | None = None
+ network_config: NetworkConfiguration | None = None
+ bootloader_config: BootloaderConfiguration | None = None
+ app_config: ApplicationConfiguration | None = None
+ auth_config: AuthenticationConfiguration | None = None
+ swap: ZramConfiguration | None = None
+ hostname: str = 'archlinux'
+ kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
+ ntp: bool = True
+ packages: list[str] = field(default_factory=list)
+ pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
+ timezone: str = 'UTC'
+ services: list[str] = field(default_factory=list)
+ custom_commands: list[str] = field(default_factory=list)
- def unsafe_config(self) -> dict[str, Any]:
- config: dict[str, list[UserSerialization] | str | None] = {}
+ def unsafe_config(self) -> dict[ArchConfigType, Any]:
+ config: dict[ArchConfigType, list[UserSerialization] | str | None] = {}
- if self.auth_config:
- if self.auth_config.users:
- config['users'] = [user.json() for user in self.auth_config.users]
+ if self.auth_config:
+ if self.auth_config.users:
+ config[ArchConfigType.USERS] = [user.json() for user in self.auth_config.users]
- if self.auth_config.root_enc_password:
- config['root_enc_password'] = self.auth_config.root_enc_password.enc_password
+ if self.auth_config.root_enc_password:
+ config[ArchConfigType.ROOT_ENC_PASSWORD] = self.auth_config.root_enc_password.enc_password
- if self.disk_config:
- disk_encryption = self.disk_config.disk_encryption
- if disk_encryption and disk_encryption.encryption_password:
- config['encryption_password'] = disk_encryption.encryption_password.plaintext
+ if self.disk_config:
+ disk_encryption = self.disk_config.disk_encryption
+ if disk_encryption and disk_encryption.encryption_password:
+ config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext
- return config
+ return config
- def safe_config(self) -> dict[str, Any]:
- config: Any = {
- 'version': self.version,
- 'script': self.script,
- 'archinstall-language': self.archinstall_language.json(),
- 'hostname': self.hostname,
- 'kernels': self.kernels,
- 'ntp': self.ntp,
- 'packages': self.packages,
- 'pacman_config': self.pacman_config.json(),
- 'swap': self.swap,
- 'timezone': self.timezone,
- 'services': self.services,
- 'custom_commands': self.custom_commands,
- 'bootloader_config': self.bootloader_config.json() if self.bootloader_config else None,
- 'app_config': self.app_config.json() if self.app_config else None,
- 'auth_config': self.auth_config.json() if self.auth_config else None,
- }
+ def safe_config(self) -> dict[ArchConfigType, Any]:
+ base_config: dict[ArchConfigType, Any] = {
+ ArchConfigType.VERSION: self.version,
+ ArchConfigType.SCRIPT: self.script,
+ ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(),
+ }
- if self.locale_config:
- config['locale_config'] = self.locale_config.json()
+ base_config.update(self.plain_cfg())
+ sub_config = self.sub_cfg()
- if self.disk_config:
- config['disk_config'] = self.disk_config.json()
+ for config_type, value in sub_config.items():
+ if not hasattr(value, 'json'):
+ raise ValueError(f'Config value for {config_type} must implement json() method')
+ base_config[config_type] = value.json()
- if self.profile_config:
- config['profile_config'] = self.profile_config.json()
+ return base_config
- if self.mirror_config:
- config['mirror_config'] = self.mirror_config.json()
+ def plain_cfg(self) -> dict[ArchConfigType, str | list[str] | bool]:
+ return {
+ ArchConfigType.HOSTNAME: self.hostname,
+ ArchConfigType.KERNELS: self.kernels,
+ ArchConfigType.NTP: self.ntp,
+ ArchConfigType.TIMEZONE: self.timezone,
+ ArchConfigType.SERVICES: self.services,
+ ArchConfigType.PACKAGES: self.packages,
+ ArchConfigType.CUSTOM_COMMANDS: self.custom_commands,
+ }
- if self.network_config:
- config['network_config'] = self.network_config.json()
+ def sub_cfg(self) -> dict[ArchConfigType, SubConfig]:
+ cfg: dict[ArchConfigType, SubConfig] = {
+ ArchConfigType.PACMAN_CONFIG: self.pacman_config,
+ }
- return config
+ if self.mirror_config:
+ cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config
- @classmethod
- def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
- arch_config = cls()
+ if self.bootloader_config:
+ cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config
- arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
+ if self.disk_config:
+ cfg[ArchConfigType.DISK_CONFIG] = self.disk_config
- if script := args_config.get('script', None):
- arch_config.script = script
+ if self.swap:
+ cfg[ArchConfigType.SWAP] = self.swap
- if archinstall_lang := args_config.get('archinstall-language', None):
- arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
- translation_handler.activate(arch_config.archinstall_language, set_font=False)
+ if self.auth_config:
+ cfg[ArchConfigType.AUTH_CONFIG] = self.auth_config
- if disk_config := args_config.get('disk_config', {}):
- enc_password = args_config.get('encryption_password', '')
- password = Password(plaintext=enc_password) if enc_password else None
- arch_config.disk_config = DiskLayoutConfiguration.parse_arg(disk_config, password)
+ if self.locale_config:
+ cfg[ArchConfigType.LOCALE_CONFIG] = self.locale_config
- # DEPRECATED
- # backwards compatibility for main level disk_encryption entry
- disk_encryption: DiskEncryption | None = None
+ if self.profile_config:
+ cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config
- if args_config.get('disk_encryption', None) is not None and arch_config.disk_config is not None:
- disk_encryption = DiskEncryption.parse_arg(
- arch_config.disk_config,
- args_config['disk_encryption'],
- Password(plaintext=args_config.get('encryption_password', '')),
- )
+ if self.network_config:
+ cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config
- if disk_encryption:
- arch_config.disk_config.disk_encryption = disk_encryption
+ if self.app_config:
+ cfg[ArchConfigType.APP_CONFIG] = self.app_config
- if profile_config := args_config.get('profile_config', None):
- arch_config.profile_config = ProfileConfiguration.parse_arg(profile_config)
+ return cfg
- if mirror_config := args_config.get('mirror_config', None):
- backwards_compatible_repo = []
- if additional_repositories := args_config.get('additional-repositories', []):
- backwards_compatible_repo = [Repository(r) for r in additional_repositories]
+ @classmethod
+ def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
+ arch_config = cls()
+
+ arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
+
+ if script := args_config.get('script', None):
+ arch_config.script = script
+
+ if archinstall_lang := args_config.get('archinstall-language', None):
+ arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
+ translation_handler.activate(arch_config.archinstall_language, set_font=False)
+
+ if disk_config := args_config.get('disk_config', {}):
+ enc_password = args_config.get('encryption_password', '')
+ password = Password(plaintext=enc_password) if enc_password else None
+ arch_config.disk_config = DiskLayoutConfiguration.parse_arg(disk_config, password)
+
+ # DEPRECATED
+ # backwards compatibility for main level disk_encryption entry
+ disk_encryption: DiskEncryption | None = None
+
+ if args_config.get('disk_encryption', None) is not None and arch_config.disk_config is not None:
+ disk_encryption = DiskEncryption.parse_arg(
+ arch_config.disk_config,
+ args_config['disk_encryption'],
+ Password(plaintext=args_config.get('encryption_password', '')),
+ )
+
+ if disk_encryption:
+ arch_config.disk_config.disk_encryption = disk_encryption
+
+ if profile_config := args_config.get('profile_config', None):
+ arch_config.profile_config = ProfileConfiguration.parse_arg(profile_config)
+
+ if mirror_config := args_config.get('mirror_config', None):
+ backwards_compatible_repo = []
+ if additional_repositories := args_config.get('additional-repositories', []):
+ backwards_compatible_repo = [Repository(r) for r in additional_repositories]
arch_config.mirror_config = MirrorConfiguration.parse_args(
mirror_config,
@@ -260,305 +363,298 @@ class ArchConfig:
class ArchConfigHandler:
- def __init__(self) -> None:
- self._parser: ArgumentParser = self._define_arguments()
- args: Arguments = self._parse_args()
- self._args = args
+ def __init__(self) -> None:
+ self._parser: ArgumentParser = self._define_arguments()
+ args: Arguments = self._parse_args()
+ self._args = args
- config = self._parse_config()
+ config = self._parse_config()
- try:
- self._config = ArchConfig.from_config(config, args)
- self._config.version = get_version()
- except ValueError as err:
- warn(str(err))
- sys.exit(1)
+ try:
+ self._config = ArchConfig.from_config(config, args)
+ self._config.version = get_version()
+ except ValueError as err:
+ warn(str(err))
+ sys.exit(1)
- @property
- def config(self) -> ArchConfig:
- return self._config
+ @property
+ def config(self) -> ArchConfig:
+ return self._config
- @property
- def args(self) -> Arguments:
- return self._args
+ @property
+ def args(self) -> Arguments:
+ return self._args
- def get_script(self) -> str:
- if script := self.args.script:
- return script
+ def get_script(self) -> str:
+ if script := self.args.script:
+ return script
- if script := self.config.script:
- return script
+ if script := self.config.script:
+ return script
- return 'guided'
+ return 'guided'
- def print_help(self) -> None:
- self._parser.print_help()
+ def print_help(self) -> None:
+ self._parser.print_help()
- def _define_arguments(self) -> ArgumentParser:
- parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- parser.add_argument(
- '-v',
- '--version',
- action='version',
- default=False,
- version='%(prog)s ' + get_version(),
- )
- parser.add_argument(
- '--config',
- type=Path,
- nargs='?',
- default=None,
- help='JSON configuration file',
- )
- parser.add_argument(
- '--config-url',
- type=str,
- nargs='?',
- default=None,
- help='Url to a JSON configuration file',
- )
- parser.add_argument(
- '--creds',
- type=Path,
- nargs='?',
- default=None,
- help='JSON credentials configuration file',
- )
- parser.add_argument(
- '--creds-url',
- type=str,
- nargs='?',
- default=None,
- help='Url to a JSON credentials configuration file',
- )
- parser.add_argument(
- '--creds-decryption-key',
- type=str,
- nargs='?',
- default=None,
- help='Decryption key for credentials file',
- )
- parser.add_argument(
- '--silent',
- action='store_true',
- default=False,
- help='WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored',
- )
- parser.add_argument(
- '--dry-run',
- '--dry_run',
- action='store_true',
- default=False,
- help='Generates a configuration file and then exits instead of performing an installation',
- )
- parser.add_argument(
- '--script',
- nargs='?',
- help='Script to run for installation',
- type=str,
- )
- parser.add_argument(
- '--mountpoint',
- type=Path,
- nargs='?',
- default=Path('/mnt'),
- help='Define an alternate mount point for installation',
- )
- parser.add_argument(
- '--skip-ntp',
- action='store_true',
- help='Disables NTP checks during installation',
- default=False,
- )
- parser.add_argument(
- '--skip-wkd',
- action='store_true',
- help='Disables checking if archlinux keyring wkd sync is complete.',
- default=False,
- )
- parser.add_argument(
- '--skip-boot',
- action='store_true',
- help='Disables installation of a boot loader (note: only use this when problems arise with the boot loader step).',
- default=False,
- )
- parser.add_argument(
- '--debug',
- action='store_true',
- default=False,
- help='Adds debug info into the log',
- )
- parser.add_argument(
- '--offline',
- action='store_true',
- default=False,
- help='Disabled online upstream services such as package search and key-ring auto update.',
- )
- parser.add_argument(
- '--no-pkg-lookups',
- action='store_true',
- default=False,
- help='Disabled package validation specifically prior to starting installation.',
- )
- parser.add_argument(
- '--plugin',
- nargs='?',
- type=str,
- default=None,
- help='File path to a plugin to load',
- )
- parser.add_argument(
- '--skip-version-check',
- action='store_true',
- default=False,
- help='Skip the version check when running archinstall',
- )
- parser.add_argument(
- '--skip-wifi-check',
- action='store_true',
- default=False,
- help='Skip wifi check when running archinstall',
- )
- parser.add_argument(
- '--advanced',
- action='store_true',
- default=False,
- help='Enabled advanced options',
- )
- parser.add_argument(
- '--verbose',
- action='store_true',
- default=False,
- help='Enabled verbose options',
- )
+ def _define_arguments(self) -> ArgumentParser:
+ parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument(
+ '-v',
+ '--version',
+ action='version',
+ default=False,
+ version='%(prog)s ' + get_version(),
+ )
+ parser.add_argument(
+ '--config',
+ type=Path,
+ nargs='?',
+ default=None,
+ help='JSON configuration file',
+ )
+ parser.add_argument(
+ '--config-url',
+ type=str,
+ nargs='?',
+ default=None,
+ help='Url to a JSON configuration file',
+ )
+ parser.add_argument(
+ '--creds',
+ type=Path,
+ nargs='?',
+ default=None,
+ help='JSON credentials configuration file',
+ )
+ parser.add_argument(
+ '--creds-url',
+ type=str,
+ nargs='?',
+ default=None,
+ help='Url to a JSON credentials configuration file',
+ )
+ parser.add_argument(
+ '--creds-decryption-key',
+ type=str,
+ nargs='?',
+ default=None,
+ help='Decryption key for credentials file',
+ )
+ parser.add_argument(
+ '--silent',
+ action='store_true',
+ default=False,
+ help='WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored',
+ )
+ parser.add_argument(
+ '--dry-run',
+ '--dry_run',
+ action='store_true',
+ default=False,
+ help='Generates a configuration file and then exits instead of performing an installation',
+ )
+ parser.add_argument(
+ '--script',
+ nargs='?',
+ help='Script to run for installation',
+ type=str,
+ )
+ parser.add_argument(
+ '--mountpoint',
+ type=Path,
+ nargs='?',
+ default=Path('/mnt'),
+ help='Define an alternate mount point for installation',
+ )
+ parser.add_argument(
+ '--skip-ntp',
+ action='store_true',
+ help='Disables NTP checks during installation',
+ default=False,
+ )
+ parser.add_argument(
+ '--skip-wkd',
+ action='store_true',
+ help='Disables checking if archlinux keyring wkd sync is complete.',
+ default=False,
+ )
+ parser.add_argument(
+ '--skip-boot',
+ action='store_true',
+ help='Disables installation of a boot loader (note: only use this when problems arise with the boot loader step).',
+ default=False,
+ )
+ parser.add_argument(
+ '--debug',
+ action='store_true',
+ default=False,
+ help='Adds debug info into the log',
+ )
+ parser.add_argument(
+ '--offline',
+ action='store_true',
+ default=False,
+ help='Disabled online upstream services such as package search and key-ring auto update.',
+ )
+ parser.add_argument(
+ '--no-pkg-lookups',
+ action='store_true',
+ default=False,
+ help='Disabled package validation specifically prior to starting installation.',
+ )
+ parser.add_argument(
+ '--plugin',
+ nargs='?',
+ type=str,
+ default=None,
+ help='File path to a plugin to load',
+ )
+ parser.add_argument(
+ '--skip-version-check',
+ action='store_true',
+ default=False,
+ help='Skip the version check when running archinstall',
+ )
+ parser.add_argument(
+ '--skip-wifi-check',
+ action='store_true',
+ default=False,
+ help='Skip wifi check when running archinstall',
+ )
+ parser.add_argument(
+ '--advanced',
+ action='store_true',
+ default=False,
+ help='Enabled advanced options',
+ )
+ parser.add_argument(
+ '--verbose',
+ action='store_true',
+ default=False,
+ help='Enabled verbose options',
+ )
+ return parser
- return parser
+ def _parse_args(self) -> Arguments:
+ argparse_args = vars(self._parser.parse_args())
+ args: Arguments = Arguments(**argparse_args)
- def _parse_args(self) -> Arguments:
- argparse_args = vars(self._parser.parse_args())
- args: Arguments = Arguments(**argparse_args)
+ # amend the parameters (check internal consistency)
+ # Installation can't be silent if config is not passed
+ if args.config is None and args.config_url is None:
+ args.silent = False
- # amend the parameters (check internal consistency)
- # Installation can't be silent if config is not passed
- if args.config is None and args.config_url is None:
- args.silent = False
+ if args.debug:
+ warn(f'Warning: --debug mode will write certain credentials to {logger.path}!')
- if args.debug:
- warn(f'Warning: --debug mode will write certain credentials to {logger.path}!')
+ if args.plugin:
+ plugin_path = Path(args.plugin)
+ load_plugin(plugin_path)
- if args.plugin:
- # Pass plugin as string to preserve URL format (avoid Path normalization)
- load_plugin(args.plugin)
+ if args.creds_decryption_key is None:
+ if os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY'):
+ args.creds_decryption_key = os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY')
- if args.creds_decryption_key is None:
- if os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY'):
- args.creds_decryption_key = os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY')
+ return args
- return args
+ def _parse_config(self) -> dict[str, Any]:
+ config: dict[str, Any] = {}
+ config_data: str | None = None
+ creds_data: str | None = None
- def _parse_config(self) -> dict[str, Any]:
- config: dict[str, Any] = {}
- config_data: str | None = None
- creds_data: str | None = None
+ if self._args.config is not None:
+ config_data = self._read_file(self._args.config)
+ elif self._args.config_url is not None:
+ config_data = self._fetch_from_url(self._args.config_url)
- if self._args.config is not None:
- config_data = self._read_file(self._args.config)
- elif self._args.config_url is not None:
- config_data = self._fetch_from_url(self._args.config_url)
+ if config_data is not None:
+ config.update(json.loads(config_data))
- if config_data is not None:
- config.update(json.loads(config_data))
+ if self._args.creds is not None:
+ creds_data = self._read_file(self._args.creds)
+ elif self._args.creds_url is not None:
+ creds_data = self._fetch_from_url(self._args.creds_url)
- if self._args.creds is not None:
- creds_data = self._read_file(self._args.creds)
- elif self._args.creds_url is not None:
- creds_data = self._fetch_from_url(self._args.creds_url)
+ if creds_data is not None:
+ json_data = self._process_creds_data(creds_data)
+ if json_data is not None:
+ config.update(json_data)
- if creds_data is not None:
- json_data = self._process_creds_data(creds_data)
- if json_data is not None:
- config.update(json_data)
+ config = self._cleanup_config(config)
- config = self._cleanup_config(config)
+ return config
- return config
+ def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None:
+ if creds_data.startswith('$'): # encrypted data
+ if self._args.creds_decryption_key is not None:
+ try:
+ creds_data = decrypt(creds_data, self._args.creds_decryption_key)
+ return json.loads(creds_data)
+ except ValueError as err:
+ if 'Invalid password' in str(err):
+ error(tr('Incorrect credentials file decryption password'))
+ sys.exit(1)
+ else:
+ debug(f'Error decrypting credentials file: {err}')
+ raise err from err
+ else:
+ header = tr('Enter credentials file decryption password')
+ wrong_pwd_text = tr('Incorrect password')
+ prompt = header
- def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None:
- if creds_data.startswith('$'): # encrypted data
- if self._args.creds_decryption_key is not None:
- try:
- creds_data = decrypt(creds_data, self._args.creds_decryption_key)
- return json.loads(creds_data)
- except ValueError as err:
- if 'Invalid password' in str(err):
- error(tr('Incorrect credentials file decryption password'))
- sys.exit(1)
- else:
- debug(f'Error decrypting credentials file: {err}')
- raise err from err
- else:
- header = tr('Enter credentials file decryption password')
- wrong_pwd_text = tr('Incorrect password')
- prompt = header
+ while True:
+ decryption_pwd: Password | None = tui.run(
+ lambda p=prompt: get_password( # type: ignore[misc]
+ header=p,
+ allow_skip=False,
+ no_confirmation=True,
+ )
+ )
- while True:
- decryption_pwd: Password | None = tui.run(
- lambda p=prompt: get_password( # type: ignore[misc]
- header=p,
- allow_skip=False,
- no_confirmation=True,
- )
- )
+ if not decryption_pwd:
+ return None
- if not decryption_pwd:
- return None
+ try:
+ creds_data = decrypt(creds_data, decryption_pwd.plaintext)
+ break
+ except ValueError as err:
+ if 'Invalid password' in str(err):
+ debug('Incorrect credentials file decryption password')
+ prompt = f'{header}' + f'\n\n{wrong_pwd_text}'
+ else:
+ debug(f'Error decrypting credentials file: {err}')
+ raise err from err
- try:
- creds_data = decrypt(creds_data, decryption_pwd.plaintext)
- break
- except ValueError as err:
- if 'Invalid password' in str(err):
- debug('Incorrect credentials file decryption password')
- prompt = f'{header}' + f'\n\n{wrong_pwd_text}'
- else:
- debug(f'Error decrypting credentials file: {err}')
- raise err from err
+ return json.loads(creds_data)
- return json.loads(creds_data)
+ def _fetch_from_url(self, url: str) -> str:
+ if urllib.parse.urlparse(url).scheme:
+ try:
+ req = Request(url, headers={'User-Agent': 'ArchInstall'})
+ with urlopen(req) as resp:
+ return resp.read().decode('utf-8')
+ except urllib.error.HTTPError as err:
+ error(f'Could not fetch JSON from {url}: {err}')
+ else:
+ error('Not a valid url')
- def _fetch_from_url(self, url: str) -> str:
- if urllib.parse.urlparse(url).scheme:
- try:
- req = Request(url, headers={'User-Agent': 'ArchInstall'})
- # FIXED: Added a 15-second timeout to prevent infinite hanging
- with urlopen(req, timeout=15) as resp:
- return resp.read().decode('utf-8')
- except urllib.error.HTTPError as err:
- error(f'Could not fetch JSON from {url}: {err}')
- else:
- error('Not a valid url')
+ sys.exit(1)
- sys.exit(1)
+ def _read_file(self, path: Path) -> str:
+ if not path.exists():
+ error(f'Could not find file {path}')
+ sys.exit(1)
- def _read_file(self, path: Path) -> str:
- if not path.exists():
- error(f'Could not find file {path}')
- sys.exit(1)
+ return path.read_text()
- return path.read_text()
+ def _cleanup_config(self, config: Namespace | dict[str, Any]) -> dict[str, Any]:
+ clean_args = {}
+ for key, val in config.items():
+ if isinstance(val, dict):
+ val = self._cleanup_config(val)
- # FIXED: Added safe handling to support both ArgumentParser Namespace objects AND dictionaries,
- # as calling .items() on a Namespace object without using `vars()` throws an AttributeError.
- def _cleanup_config(self, config: Namespace | dict[str, Any]) -> dict[str, Any]:
- clean_args = {}
- # Convert Namespace to dict if necessary before iterating
- config_dict = vars(config) if isinstance(config, Namespace) else config
-
- for key, val in config_dict.items():
- if isinstance(val, dict):
- val = self._cleanup_config(val)
+ if val is not None:
+ clean_args[key] = val
- if val is not None:
- clean_args[key] = val
-
- return clean_args
\ No newline at end of file
+ return clean_args
diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py
index aba835d9..c40ef161 100644
--- a/archinstall/lib/configuration.py
+++ b/archinstall/lib/configuration.py
@@ -6,13 +6,12 @@ from typing import Any
from pydantic import TypeAdapter
-from archinstall.lib.args import ArchConfig
+from archinstall.lib.args import ArchConfig, ArchConfigType
from archinstall.lib.crypt import encrypt
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password, prompt_dir
-from archinstall.lib.models.bootloader import Bootloader
from archinstall.lib.models.network import NetworkConfiguration
-from archinstall.lib.output import debug, logger, warn
+from archinstall.lib.output import as_key_value_pair, debug, logger, warn
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@@ -45,15 +44,15 @@ class ConfigurationOutput:
def user_config_to_json(self) -> str:
config = self._config.safe_config()
- adapter = TypeAdapter(dict[str, Any])
+ adapter = TypeAdapter(dict[ArchConfigType, Any])
python_dict = adapter.dump_python(config)
return json.dumps(python_dict, indent=4, sort_keys=True)
def user_credentials_to_json(self) -> str:
- config = self._config.unsafe_config()
+ cfg = self._config.unsafe_config()
- adapter = TypeAdapter(dict[str, Any])
- python_dict = adapter.dump_python(config)
+ adapter = TypeAdapter(dict[ArchConfigType, Any])
+ python_dict = adapter.dump_python(cfg)
return json.dumps(python_dict, indent=4, sort_keys=True)
def write_debug(self) -> None:
@@ -64,65 +63,24 @@ class ConfigurationOutput:
"""
Render a concise two-column summary of the current configuration.
- The left column holds section labels, the right column holds values.
- Column width adapts to the longest translated label so translations
- do not break the alignment. Rows whose underlying config is not set
- are skipped.
-
Returns an empty string if nothing meaningful to show.
"""
- rows: list[tuple[str, str]] = []
+ cfg: dict[str, str | list[str] | bool] = {}
- disk_config = self._config.disk_config
- if disk_config and disk_config.device_modifications:
- disk_parts: list[str] = []
- for mod in disk_config.device_modifications:
- path = str(mod.device_path)
- root_part = mod.get_root_partition()
- flags: list[str] = []
- if root_part and root_part.fs_type:
- flags.append(root_part.fs_type.value)
- if disk_config.disk_encryption:
- flags.append(tr('LUKS'))
- disk_parts.append(f'{path} ({" + ".join(flags)})' if flags else path)
- rows.append((tr('Disks'), ', '.join(disk_parts)))
+ for key, value in self._config.plain_cfg().items():
+ cfg[key.text()] = value
- bl_config = self._config.bootloader_config
- if bl_config and bl_config.bootloader != Bootloader.NO_BOOTLOADER:
- rows.append((tr('Bootloader'), bl_config.bootloader.value))
+ for config_type, obj in self._config.sub_cfg().items():
+ if not hasattr(obj, 'summary'):
+ continue
- kernels = self._config.kernels
- if kernels:
- rows.append((tr('Kernel'), ', '.join(kernels)))
+ summary = obj.summary()
+ if summary:
+ cfg[config_type.text()] = summary
- profile_config = self._config.profile_config
- if profile_config and profile_config.profile:
- names = profile_config.profile.current_selection_names()
- rows.append((tr('Profile'), ', '.join(names) if names else profile_config.profile.name))
- if profile_config.greeter:
- rows.append((tr('Greeter'), profile_config.greeter.value))
+ simple_summary = as_key_value_pair(cfg, ignore_empty=True)
- packages = self._config.packages
- if packages:
- rows.append((tr('Packages'), str(len(packages))))
-
- net_config = self._config.network_config
- if isinstance(net_config, NetworkConfiguration):
- rows.append((tr('Network'), net_config.type.display_msg()))
-
- locale_config = self._config.locale_config
- if locale_config:
- rows.append((tr('Locale'), locale_config.sys_lang))
-
- tz = self._config.timezone
- if tz:
- rows.append((tr('Timezone'), tz))
-
- if not rows:
- return ''
-
- label_width = max(len(label) for label, _ in rows) + 2
- return '\n'.join(f'{label:<{label_width}}{value}' for label, value in rows)
+ return simple_summary
async def confirm_config(self, show_install_warnings: bool = False) -> bool:
header = f'{tr("The specified configuration will be applied")}. '
diff --git a/archinstall/lib/menu/helpers.py b/archinstall/lib/menu/helpers.py
index b512c956..947f3c55 100644
--- a/archinstall/lib/menu/helpers.py
+++ b/archinstall/lib/menu/helpers.py
@@ -20,6 +20,7 @@ class Selection[ValueT]:
preview_location: Literal['right', 'bottom'] | None = None,
multi: bool = False,
enable_filter: bool = False,
+ wrap_preview: bool = False,
):
self._header = header
self._title = title
@@ -29,6 +30,7 @@ class Selection[ValueT]:
self._preview_location = preview_location
self._multi = multi
self._enable_filter = enable_filter
+ self._wrap_preview = wrap_preview
async def show(self) -> Result[ValueT]:
if self._multi:
@@ -39,6 +41,7 @@ class Selection[ValueT]:
allow_reset=self._allow_reset,
preview_location=self._preview_location,
enable_filter=self._enable_filter,
+ wrap_preview=self._wrap_preview,
).run()
else:
result = await OptionListScreen[ValueT](
@@ -49,6 +52,7 @@ class Selection[ValueT]:
allow_reset=self._allow_reset,
preview_location=self._preview_location,
enable_filter=self._enable_filter,
+ wrap_preview=self._wrap_preview,
).run()
if result.type_ == ResultType.Reset:
diff --git a/archinstall/lib/models/application.py b/archinstall/lib/models/application.py
index 04d1a342..1e97a784 100644
--- a/archinstall/lib/models/application.py
+++ b/archinstall/lib/models/application.py
@@ -1,7 +1,8 @@
from dataclasses import dataclass
from enum import StrEnum, auto
-from typing import Any, NotRequired, Self, TypedDict
+from typing import Any, NotRequired, Self, TypedDict, override
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
@@ -168,7 +169,7 @@ class FontsConfiguration:
@dataclass(frozen=True)
-class ZramConfiguration:
+class ZramConfiguration(SubConfig):
enabled: bool
algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD
@@ -181,9 +182,28 @@ class ZramConfiguration:
algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value))
return cls(enabled=enabled, algorithm=ZramAlgorithm(algo))
+ @override
+ def json(self) -> dict[str, bool | str]:
+ return {
+ 'enabled': self.enabled,
+ 'algorithm': self.algorithm.value,
+ }
+
+ @override
+ def summary(self) -> list[str] | None:
+ out: list[str] = []
+
+ if self.enabled:
+ out.append(tr('Zram enabled'))
+
+ out.append(tr('Zram algorithm {}').format(self.algorithm))
+ return out
+
+ return None
+
@dataclass
-class ApplicationConfiguration:
+class ApplicationConfiguration(SubConfig):
bluetooth_config: BluetoothConfiguration | None = None
audio_config: AudioConfiguration | None = None
power_management_config: PowerManagementConfiguration | None = None
@@ -223,6 +243,7 @@ class ApplicationConfiguration:
return app_config
+ @override
def json(self) -> ApplicationSerialization:
config: ApplicationSerialization = {}
@@ -245,3 +266,28 @@ class ApplicationConfiguration:
config['fonts_config'] = self.fonts_config.json()
return config
+
+ @override
+ def summary(self) -> list[str]:
+ out: list[str] = []
+
+ if self.bluetooth_config and self.bluetooth_config.enabled:
+ out.append(tr('Bluetooth enabled'))
+
+ if self.audio_config:
+ out.append(tr('Audio server "{}"').format(self.audio_config.audio))
+
+ if self.power_management_config:
+ out.append(tr('Power management "{}"').format(self.power_management_config.power_management))
+
+ if self.print_service_config and self.print_service_config.enabled:
+ out.append(tr('Print service enabled'))
+
+ if self.firewall_config:
+ out.append(tr('Firewall "{}"').format(self.firewall_config.firewall))
+
+ if self.fonts_config and self.fonts_config.fonts:
+ fonts = ', '.join(f.value for f in self.fonts_config.fonts)
+ out.append(tr('Extra fonts "{}"').format(fonts))
+
+ return out
diff --git a/archinstall/lib/models/authentication.py b/archinstall/lib/models/authentication.py
index 51120479..37261e7c 100644
--- a/archinstall/lib/models/authentication.py
+++ b/archinstall/lib/models/authentication.py
@@ -1,7 +1,8 @@
from dataclasses import dataclass, field
from enum import Enum
-from typing import Any, NotRequired, Self, TypedDict
+from typing import Any, NotRequired, Self, TypedDict, override
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.users import Password, User
from archinstall.lib.translationhandler import tr
@@ -58,7 +59,7 @@ class U2FLoginConfiguration:
@dataclass
-class AuthenticationConfiguration:
+class AuthenticationConfiguration(SubConfig):
root_enc_password: Password | None = None
users: list[User] = field(default_factory=list)
u2f_config: U2FLoginConfiguration | None = None
@@ -75,6 +76,7 @@ class AuthenticationConfiguration:
return auth_config
+ @override
def json(self) -> AuthenticationSerialization:
config: AuthenticationSerialization = {}
@@ -83,6 +85,21 @@ class AuthenticationConfiguration:
return config
+ @override
+ def summary(self) -> list[str]:
+ out: list[str] = []
+
+ if self.root_enc_password:
+ out.append(tr('Root password set'))
+
+ if self.users:
+ out.append(tr('Configured {} user(s)').format(len(self.users)))
+
+ if self.u2f_config:
+ out.append(tr('U2F set up'))
+
+ return out
+
def has_superuser(self) -> bool:
return any(u.sudo for u in self.users)
diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py
index 5d8d0d80..0dbd4268 100644
--- a/archinstall/lib/models/bootloader.py
+++ b/archinstall/lib/models/bootloader.py
@@ -1,8 +1,9 @@
import sys
from dataclasses import dataclass
from enum import Enum
-from typing import Any, Self
+from typing import Any, Self, override
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.output import warn
from archinstall.lib.translationhandler import tr
@@ -60,14 +61,26 @@ class Bootloader(Enum):
@dataclass
-class BootloaderConfiguration:
+class BootloaderConfiguration(SubConfig):
bootloader: Bootloader
uki: bool = False
removable: bool = True
+ @override
def json(self) -> dict[str, Any]:
return {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable}
+ @override
+ def summary(self) -> list[str]:
+ out = [tr('Bootloader "{}"').format(self.bootloader.value)]
+
+ if self.uki:
+ out.append(tr('UKI enabled'))
+ if self.removable:
+ out.append(tr('Removable'))
+
+ return out
+
@classmethod
def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self:
bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot)
diff --git a/archinstall/lib/models/config.py b/archinstall/lib/models/config.py
new file mode 100644
index 00000000..45835bc0
--- /dev/null
+++ b/archinstall/lib/models/config.py
@@ -0,0 +1,12 @@
+from abc import ABC, abstractmethod
+from typing import Any
+
+
+class SubConfig(ABC):
+ @abstractmethod
+ def json(self) -> Any:
+ pass
+
+ @abstractmethod
+ def summary(self) -> str | list[str] | None:
+ pass
diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py
index 7fa05407..a21f3c6f 100644
--- a/archinstall/lib/models/device.py
+++ b/archinstall/lib/models/device.py
@@ -12,6 +12,7 @@ from parted import Disk, Geometry, Partition
from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator
from archinstall.lib.hardware import SysInfo
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.users import Password
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
@@ -34,6 +35,15 @@ class DiskLayoutType(Enum):
case DiskLayoutType.Pre_mount:
return tr('Pre-mounted configuration')
+ def short_msg(self) -> str:
+ match self:
+ case DiskLayoutType.Default:
+ return tr('Default')
+ case DiskLayoutType.Manual:
+ return tr('Manual')
+ case DiskLayoutType.Pre_mount:
+ return tr('Pre-mount')
+
class _DiskLayoutConfigurationSerialization(TypedDict):
config_type: str
@@ -45,7 +55,7 @@ class _DiskLayoutConfigurationSerialization(TypedDict):
@dataclass
-class DiskLayoutConfiguration:
+class DiskLayoutConfiguration(SubConfig):
config_type: DiskLayoutType
device_modifications: list[DeviceModification] = field(default_factory=list)
lvm_config: LvmConfiguration | None = None
@@ -55,6 +65,7 @@ class DiskLayoutConfiguration:
# used for pre-mounted config
mountpoint: Path | None = None
+ @override
def json(self) -> _DiskLayoutConfigurationSerialization:
if self.config_type == DiskLayoutType.Pre_mount:
return {
@@ -78,6 +89,28 @@ class DiskLayoutConfiguration:
return config
+ @override
+ def summary(self) -> list[str]:
+ out = [tr('{} layout').format(self.config_type.short_msg())]
+
+ devices = set(mod.device_path for mod in self.device_modifications)
+
+ if devices:
+ dev_str = ', '.join(str(d) for d in devices)
+ out.append(tr('Devices {}').format(dev_str))
+
+ if self.lvm_config is not None:
+ out.append(tr('LVM set up'))
+
+ if self.disk_encryption is not None:
+ out.append(tr('{} encryption').format(self.disk_encryption.encryption_type.type_to_text()))
+
+ if self.btrfs_options is not None:
+ if self.btrfs_options.snapshot_config:
+ out.append(tr('Btrfs snapshot "{}"').format(self.btrfs_options.snapshot_config.snapshot_type))
+
+ return out
+
@classmethod
def parse_arg(
cls,
@@ -1321,7 +1354,7 @@ class _SnapshotConfigSerialization(TypedDict):
type: str
-class SnapshotType(Enum):
+class SnapshotType(StrEnum):
Snapper = 'Snapper'
Timeshift = 'Timeshift'
diff --git a/archinstall/lib/models/locale.py b/archinstall/lib/models/locale.py
index 8f580989..26ab3cc0 100644
--- a/archinstall/lib/models/locale.py
+++ b/archinstall/lib/models/locale.py
@@ -1,12 +1,13 @@
from dataclasses import dataclass
-from typing import Any, Self
+from typing import Any, Self, override
from archinstall.lib.locale.utils import get_kb_layout
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
@dataclass
-class LocaleConfiguration:
+class LocaleConfiguration(SubConfig):
kb_layout: str
sys_lang: str
sys_enc: str
@@ -23,6 +24,7 @@ class LocaleConfiguration:
layout = 'us'
return cls(layout, 'en_US.UTF-8', 'UTF-8')
+ @override
def json(self) -> dict[str, str]:
return {
'kb_layout': self.kb_layout,
@@ -31,6 +33,15 @@ class LocaleConfiguration:
'console_font': self.console_font,
}
+ @override
+ def summary(self) -> list[str]:
+ return [
+ tr('Keyboard layout "{}"').format(self.kb_layout),
+ tr('Locale language "{}"').format(self.sys_lang),
+ tr('Locale encoding "{}"').format(self.sys_enc),
+ tr('Console font "{}"').format(self.console_font),
+ ]
+
def preview(self) -> str:
output = '{}: {}\n'.format(tr('Keyboard layout'), self.kb_layout)
output += '{}: {}\n'.format(tr('Locale language'), self.sys_lang)
diff --git a/archinstall/lib/models/mirrors.py b/archinstall/lib/models/mirrors.py
index 0a596c13..3ded2a9a 100644
--- a/archinstall/lib/models/mirrors.py
+++ b/archinstall/lib/models/mirrors.py
@@ -9,9 +9,11 @@ from typing import TYPE_CHECKING, Any, Self, TypedDict, override
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.packages import Repository
from archinstall.lib.networking import DownloadTimer, ping
from archinstall.lib.output import debug
+from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
@@ -236,7 +238,7 @@ class _MirrorConfigurationSerialization(TypedDict):
@dataclass
-class MirrorConfiguration:
+class MirrorConfiguration(SubConfig):
mirror_regions: list[MirrorRegion] = field(default_factory=list)
custom_servers: list[CustomServer] = field(default_factory=list)
optional_repositories: list[Repository] = field(default_factory=list)
@@ -250,6 +252,7 @@ class MirrorConfiguration:
def custom_server_urls(self) -> str:
return '\n'.join(s.url for s in self.custom_servers)
+ @override
def json(self) -> _MirrorConfigurationSerialization:
regions = {}
for m in self.mirror_regions:
@@ -262,6 +265,24 @@ class MirrorConfiguration:
'custom_repositories': [c.json() for c in self.custom_repositories],
}
+ @override
+ def summary(self) -> list[str]:
+ out: list[str] = []
+
+ if self.mirror_regions:
+ out.append(tr('Mirror regions "{}"').format(', '.join(m.name for m in self.mirror_regions)))
+
+ if self.optional_repositories:
+ out.append(tr('Optional repositories "{}"').format(', '.join(r.value for r in self.optional_repositories)))
+
+ if self.custom_servers:
+ out.append(tr('Custom servers set up'))
+
+ if self.custom_repositories:
+ out.append(tr('Custom repositories set up'))
+
+ return out
+
def custom_servers_config(self) -> str:
config = ''
diff --git a/archinstall/lib/models/network.py b/archinstall/lib/models/network.py
index 71f3dca2..9e61cb56 100644
--- a/archinstall/lib/models/network.py
+++ b/archinstall/lib/models/network.py
@@ -3,6 +3,7 @@ from dataclasses import dataclass, field
from enum import Enum
from typing import NotRequired, Self, TypedDict, override
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
@@ -103,10 +104,11 @@ class _NetworkConfigurationSerialization(TypedDict):
@dataclass
-class NetworkConfiguration:
+class NetworkConfiguration(SubConfig):
type: NicType
nics: list[Nic] = field(default_factory=list)
+ @override
def json(self) -> _NetworkConfigurationSerialization:
config: _NetworkConfigurationSerialization = {'type': self.type.value}
if self.nics:
@@ -114,6 +116,10 @@ class NetworkConfiguration:
return config
+ @override
+ def summary(self) -> str:
+ return self.type.display_msg()
+
@classmethod
def parse_arg(cls, config: _NetworkConfigurationSerialization) -> Self | None:
nic_type = config.get('type', None)
diff --git a/archinstall/lib/models/packages.py b/archinstall/lib/models/packages.py
index 6ba6e134..205d653e 100644
--- a/archinstall/lib/models/packages.py
+++ b/archinstall/lib/models/packages.py
@@ -132,7 +132,6 @@ class AvailablePackage(BaseModel):
def longest_key(self) -> int:
return max(len(key) for key in self.model_dump().keys())
- # return all package info line by line
def info(self) -> str:
output = ''
for key, value in self.model_dump().items():
diff --git a/archinstall/lib/models/pacman.py b/archinstall/lib/models/pacman.py
index 5d271ea9..004dfbc3 100644
--- a/archinstall/lib/models/pacman.py
+++ b/archinstall/lib/models/pacman.py
@@ -1,6 +1,7 @@
from dataclasses import dataclass
-from typing import Self, TypedDict
+from typing import Self, TypedDict, override
+from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
@@ -10,7 +11,7 @@ class PacmanConfigSerialization(TypedDict):
@dataclass
-class PacmanConfiguration:
+class PacmanConfiguration(SubConfig):
parallel_downloads: int = 5
color: bool = True
@@ -18,12 +19,19 @@ class PacmanConfiguration:
def default(cls) -> Self:
return cls()
+ @override
def json(self) -> PacmanConfigSerialization:
return {
'parallel_downloads': self.parallel_downloads,
'color': self.color,
}
+ @override
+ def summary(self) -> str | None:
+ if self.color:
+ return tr('Color enabled')
+ return None
+
def preview(self) -> str:
color_str = str(self.color)
output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads)
diff --git a/archinstall/lib/models/profile.py b/archinstall/lib/models/profile.py
index e7ae8c8c..fa890a1e 100644
--- a/archinstall/lib/models/profile.py
+++ b/archinstall/lib/models/profile.py
@@ -1,8 +1,10 @@
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Self, TypedDict
+from typing import TYPE_CHECKING, Self, TypedDict, override
from archinstall.default_profiles.profile import GreeterType, Profile
from archinstall.lib.hardware import GfxDriver
+from archinstall.lib.models.config import SubConfig
+from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
from archinstall.lib.profile.profiles_handler import ProfileSerialization
@@ -15,11 +17,12 @@ class _ProfileConfigurationSerialization(TypedDict):
@dataclass
-class ProfileConfiguration:
+class ProfileConfiguration(SubConfig):
profile: Profile | None = None
gfx_driver: GfxDriver | None = None
greeter: GreeterType | None = None
+ @override
def json(self) -> _ProfileConfigurationSerialization:
from archinstall.lib.profile.profiles_handler import profile_handler
@@ -29,6 +32,23 @@ class ProfileConfiguration:
'greeter': self.greeter.value if self.greeter else None,
}
+ @override
+ def summary(self) -> list[str] | None:
+ out: list[str] = []
+
+ if self.profile:
+ out.append(self.profile.name)
+
+ if self.gfx_driver:
+ out.append(tr('{} grphics driver').format(self.gfx_driver.value))
+
+ if self.greeter:
+ out.append(tr('{} greeter').format(self.greeter.value))
+
+ return out
+
+ return None
+
@classmethod
def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self:
from archinstall.lib.profile.profiles_handler import profile_handler
diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py
index bc4bb113..86a47ed1 100644
--- a/archinstall/lib/output.py
+++ b/archinstall/lib/output.py
@@ -1,6 +1,8 @@
import logging
import os
import sys
+import urllib.error
+import urllib.request
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
from datetime import UTC, datetime
@@ -9,6 +11,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any
from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust
+from archinstall.tui.rich import BaseRichTable
if TYPE_CHECKING:
from _typeshed import DataclassInstance
@@ -18,7 +21,7 @@ class FormattedOutput:
@staticmethod
def _get_values(
o: DataclassInstance,
- class_formatter: str | Callable | None = None, # type: ignore[type-arg]
+ class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]
filter_list: list[str] = [],
) -> dict[str, Any]:
"""
@@ -127,6 +130,35 @@ class FormattedOutput:
return output
+def as_key_value_pair(
+ entries: dict[str, str | list[str] | bool],
+ ignore_empty: bool = True,
+) -> str:
+ """
+ Formats key-values as a Rich Table:
+ key1 : value1
+ key2 : value2
+ ...
+ """
+ table = BaseRichTable()
+ table.add_column('key', style='bold', no_wrap=True)
+ table.add_column('value', style='white', max_width=70)
+
+ for label, value in entries.items():
+ if ignore_empty and not value:
+ continue
+
+ if isinstance(value, bool):
+ value = 'Yes' if value else 'No'
+
+ if isinstance(value, list):
+ value = '\n '.join(str(val) for val in value)
+
+ table.add_row(label.title(), f': {value}')
+
+ return table.stringify()
+
+
class Journald:
@staticmethod
def log(message: str, level: int = logging.DEBUG) -> None:
@@ -333,3 +365,51 @@ def log(
if level != logging.DEBUG:
print(text)
+
+
+def share_install_log(
+ paste_url: str = 'https://paste.rs',
+ max_size: int = 10 * 1024 * 1024,
+ confirm: Callable[[str], bool] = lambda _: True,
+) -> int:
+ log_path = logger.path
+
+ if not log_path.exists():
+ info(f'Log file not found: {log_path}')
+ return 1
+
+ size = log_path.stat().st_size
+ if size == 0:
+ info(f'Log file is empty: {log_path}')
+ return 1
+
+ if size > max_size:
+ info(f'Log file exceeds {max_size} bytes, uploading last {max_size} bytes')
+ content = log_path.read_bytes()[-max_size:]
+ else:
+ content = log_path.read_bytes()
+
+ header = f'About to upload {log_path} ({len(content)} bytes) to {paste_url}\n\n'
+ header += 'The log may contain hostname, mirror URLs, package list and partition layout.\n'
+ header += 'The uploaded paste is public.\n\n'
+ header += 'Continue?'
+
+ if not confirm(header):
+ info('Cancelled.')
+ return 1
+
+ try:
+ req = urllib.request.Request(paste_url, data=content)
+ with urllib.request.urlopen(req) as response:
+ url = response.read().decode().strip()
+ except urllib.error.URLError as e:
+ info(f'Upload failed: {e}')
+ return 1
+
+ if not url.startswith('http'):
+ info(f'Unexpected response from {paste_url}: {url[:200]!r}')
+ return 1
+
+ # raw print so the URL is pipe-friendly (no ANSI colors, no log prefix)
+ print(url)
+ return 0
diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py
index dcc4da9e..a7422986 100644
--- a/archinstall/lib/packages/packages.py
+++ b/archinstall/lib/packages/packages.py
@@ -14,7 +14,7 @@ def installed_package(package: str) -> LocalPackage | None:
try:
package_info = []
for line in Pacman.run(f'-Q --info {package}'):
- package_info.append(line.decode().strip())
+ package_info.append(line.decode().rstrip())
return _parse_package_output(package_info, LocalPackage)
except SysCallError:
@@ -53,7 +53,7 @@ def available_package(package: str) -> AvailablePackage | None:
try:
package_info: list[str] = []
for line in Pacman.run(f'-S --info {package}'):
- package_info.append(line.decode().strip())
+ package_info.append(line.decode().rstrip())
return _parse_package_output(package_info, AvailablePackage)
except SysCallError:
@@ -79,7 +79,7 @@ def list_available_packages(
debug(f'Failed to sync Arch Linux package database: {e}')
for line in Pacman.run('-S --info'):
- dec_line = line.decode().strip()
+ dec_line = line.decode().rstrip()
current_package.append(dec_line)
if dec_line.startswith('Validated'):
@@ -187,6 +187,7 @@ async def select_additional_packages(
multi=True,
preview_location='right',
enable_filter=True,
+ wrap_preview=True,
).show()
match pck_result.type_:
diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot
index 31f5e196..0b08df29 100644
--- a/archinstall/locales/base.pot
+++ b/archinstall/locales/base.pot
@@ -2293,3 +2293,14 @@ msgstr ""
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr ""
+
+#, python-brace-format
+msgid "{} needs access to your seat"
+msgstr ""
+
+msgid "collection of hardware devices i.e. keyboard, mouse"
+msgstr ""
+
+#, python-brace-format
+msgid "Choose an option how to give {} access to your hardware"
+msgstr ""
diff --git a/archinstall/locales/pl/LC_MESSAGES/base.mo b/archinstall/locales/pl/LC_MESSAGES/base.mo
index 4a57d37c..0ab34f9e 100644
Binary files a/archinstall/locales/pl/LC_MESSAGES/base.mo and b/archinstall/locales/pl/LC_MESSAGES/base.mo differ
diff --git a/archinstall/locales/pl/LC_MESSAGES/base.po b/archinstall/locales/pl/LC_MESSAGES/base.po
index ab73ad85..d01f1012 100644
--- a/archinstall/locales/pl/LC_MESSAGES/base.po
+++ b/archinstall/locales/pl/LC_MESSAGES/base.po
@@ -256,6 +256,9 @@ msgstr "Język"
msgid "Locale encoding"
msgstr "Kodowanie"
+msgid "Console font"
+msgstr "Czcionka konsoli"
+
msgid "Drive(s)"
msgstr "Dysk(i)"
@@ -820,6 +823,19 @@ msgstr "Nieprawidłowa wartość! Spróbuj jeszcze raz z prawidłową wartości
msgid "Parallel Downloads"
msgstr "Pobieranie kilku plików jednocześnie"
+msgid "Pacman"
+msgstr "Pacman"
+
+msgid "Color"
+msgstr "Kolor"
+
+#, python-brace-format
+msgid "Enter the number of parallel downloads (1-{})"
+msgstr "Wprowadź maksymalną liczbę plików pobieranych jednocześnie (1-{})"
+
+msgid "Enable colored output for pacman"
+msgstr "Włącz kolory w pacmanie"
+
msgid "ESC to skip"
msgstr "Naciśnij ESC, aby pominąć"
@@ -1203,6 +1219,21 @@ msgstr "Produkt"
msgid "Invalid configuration: {error}"
msgstr "Niepoprawna konfiguracja: {error}"
+msgid "Ready to install"
+msgstr "Gotowe do zainstalowania"
+
+msgid "Disks"
+msgstr "Dyski"
+
+msgid "Packages"
+msgstr "Pakiety"
+
+msgid "Network"
+msgstr "Sieć"
+
+msgid "Locale"
+msgstr "Ustawienia regionalne (locale)"
+
msgid "Type"
msgstr "Typ"
@@ -1660,6 +1691,123 @@ msgstr "Wejdź w tryb wyszukiwania"
msgid "Exit search mode"
msgstr "Wyjdź z trybu wyszukiwania"
+msgid "General"
+msgstr "Ogólne"
+
+msgid "Navigation"
+msgstr "Nawigacja"
+
+msgid "Selection"
+msgstr "Wybór"
+
+msgid "Search"
+msgstr "Szukaj"
+
+msgid "Down"
+msgstr "W dół"
+
+msgid "Up"
+msgstr "W górę"
+
+msgid "Confirm"
+msgstr "Potwierdź"
+
+msgid "Focus right"
+msgstr "W prawo"
+
+msgid "Focus left"
+msgstr "W lewo"
+
+msgid "Toggle"
+msgstr "Zaznacz/Odznacz"
+
+msgid "Show/Hide help"
+msgstr "Pokaż/Ukryj pomoc"
+
+msgid "Quit"
+msgstr "Wyjdź"
+
+msgid "First"
+msgstr "Pierwszy"
+
+msgid "Last"
+msgstr "Ostatni"
+
+msgid "Select"
+msgstr "Wybierz"
+
+msgid "Page Up"
+msgstr "Strona w górę"
+
+msgid "Page Down"
+msgstr "Strona w dół"
+
+msgid "Page up"
+msgstr "Strona w górę"
+
+msgid "Page down"
+msgstr "Strona w dół"
+
+msgid "Page Left"
+msgstr "Strona w lewo"
+
+msgid "Page Right"
+msgstr "Strona w prawo"
+
+msgid "Cursor up"
+msgstr "Kursor w górę"
+
+msgid "Cursor down"
+msgstr "Kursor w dół"
+
+msgid "Cursor right"
+msgstr "Kursor w prawo"
+
+msgid "Cursor left"
+msgstr "Kursor w lewo"
+
+msgid "Top"
+msgstr "Góra"
+
+msgid "Bottom"
+msgstr "Dół"
+
+msgid "Home"
+msgstr "Początek"
+
+msgid "End"
+msgstr "Koniec"
+
+msgid "Toggle option"
+msgstr "Zaznacz/Odznacz opcję"
+
+msgid "Scroll Up"
+msgstr "Przewiń w górę"
+
+msgid "Scroll Down"
+msgstr "Przewiń w dół"
+
+msgid "Scroll Left"
+msgstr "Przewiń w lewo"
+
+msgid "Scroll Right"
+msgstr "Przewiń w prawo"
+
+msgid "Scroll Home"
+msgstr "Przewiń do początku"
+
+msgid "Scroll End"
+msgstr "Przewiń do końca"
+
+msgid "Focus Next"
+msgstr "Następny"
+
+msgid "Focus Previous"
+msgstr "Poprzedni"
+
+msgid "Copy selected text"
+msgstr "Skopiuj zaznaczony tekst"
+
msgid "labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr "labwc potrzebuje dostępu do twojego seat'a (sprzętu, np. klawiatury, myszki)"
@@ -1878,6 +2026,21 @@ msgstr "Użyj NetworkManager'a (backend iwd)"
msgid "Firewall"
msgstr "Zapora sieciowa"
+msgid "Additional fonts"
+msgstr "Dodatkowe czcionki"
+
+msgid "Select font packages to install"
+msgstr "Wybierz pakiety czcionek do zainstalowania"
+
+msgid "Unicode font coverage for most languages"
+msgstr "Wsparcie Unicode dla większości języków"
+
+msgid "color emoji for browsers and apps"
+msgstr "kolorowe emoji dla przeglądarek i aplikacji"
+
+msgid "Chinese, Japanese, Korean characters"
+msgstr "Chińskie, Japońskie, Koreańskie znaki"
+
msgid "Select audio configuration"
msgstr "Wybierz konfigurację audio"
@@ -1965,7 +2128,13 @@ msgid "Select an interface"
msgstr "Wybierz interfejs"
msgid "Choose network configuration"
-msgstr "Wybierz konfigurację sieciową"
+msgstr "Wybierz konfigurację sieci"
+
+msgid "Recommended: Network Manager for desktop, Manual for server"
+msgstr "Rekomendowane: Network Manager dla desktopów, ręcznie dla serwerów"
+
+msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system."
+msgstr "Uwaga: nie wybrano konfiguracji sieci. Sieć musi zostać skonfigurowana ręcznie w zainstalowanym systemie."
msgid "No packages found"
msgstr "Nie znaleziono pakietów"
@@ -2004,6 +2173,72 @@ msgstr "Wybrany profil pulpitu wymaga logowania normalnego użytkownika przez gr
msgid "Environment type: {} {}"
msgstr "Typ środowiska: {} {}"
+msgid "Input cannot be empty"
+msgstr "Podana wartość nie może być pusta"
+
+msgid "Recommended"
+msgstr "Rekomendowane"
+
+msgid "Package"
+msgstr "Pakiet"
+
+msgid "Curated selection of KDE Plasma packages"
+msgstr "Wyselekcjonowany zestaw pakietów KDE Plasma"
+
+msgid "Dependencies"
+msgstr "Zależności"
+
+msgid "Package group"
+msgstr "Grupa pakietów"
+
+msgid "Extensive KDE Plasma installation"
+msgstr "Obszerna instalacja KDE Plasma"
+
+msgid "Packages in group"
+msgstr "Pakiety w grupie"
+
+msgid "Minimal KDE Plasma installation"
+msgstr "Minimalistyczna instalacja KDE Plasma"
+
+msgid "Description"
+msgstr "Opis"
+
+msgid "Select a flavor of KDE Plasma to install"
+msgstr "Wybierz rodzaj KDE Plasma do zainstalowania"
+
+msgid "Arial/Times/Courier replacement, Cyrillic support for Steam/games"
+msgstr "Zamiennik Arial/Times/Courier, obsługa Cyrylicy dla Steam/gier"
+
+msgid "wide Unicode coverage, good fallback font"
+msgstr "szerokie wsparcie Unicode, dobra czczionka zastępcza"
+
+#, python-brace-format
+msgid "Setting up U2F login: {u2f_config.u2f_login_method.value}"
+msgstr "Ustawianie logowania U2F {u2f_config.u2f_login_method.value}"
+
+#, python-brace-format
+msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000"
+msgstr "Domyślnie: {DEFAULT_ITER_TIME}ms, Rekomendowany przedział: 1000-60000"
+
+#, python-brace-format
+msgid "Setting up U2F login: {}"
+msgstr "Konfigurowanie logowania U2F: {}"
+
+#, python-brace-format
+msgid "Default: {}ms, Recommended range: 1000-60000"
+msgstr "Domyślnie: {}ms, Rekomendowany przedział: 1000-60000"
+
+#, python-brace-format
+msgid "{} needs access to your seat"
+msgstr "{} potrzebuje dostępu do twojego seat'a"
+
+msgid "collection of hardware devices i.e. keyboard, mouse"
+msgstr "zbiór urządzeń, t.j. klawiatura, myszka"
+
+#, python-brace-format
+msgid "Choose an option how to give {} access to your hardware"
+msgstr "Wybierz opcję, aby nadać {} dostęp do twojego sprzętu"
+
#~ msgid "When picking a directory to save configuration files to, by default we will ignore the following folders: "
#~ msgstr "Podczas wybierania katalogu do zapisywania plików konfiguracyjnych, domyślnie ignorowane są następujące foldery: "
diff --git a/archinstall/main.py b/archinstall/main.py
index 505652a4..9b91681b 100644
--- a/archinstall/main.py
+++ b/archinstall/main.py
@@ -11,14 +11,16 @@ from pathlib import Path
from archinstall.lib.args import ArchConfigHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.hardware import SysInfo
+from archinstall.lib.menu.helpers import Confirmation
from archinstall.lib.network.wifi_handler import WifiHandler
from archinstall.lib.networking import ping
-from archinstall.lib.output import debug, error, info, warn
+from archinstall.lib.output import debug, error, info, share_install_log, warn
from archinstall.lib.packages.util import check_version_upgrade
from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr, translation_handler
from archinstall.lib.utils.util import running_from_iso
from archinstall.tui.components import tui
+from archinstall.tui.menu_item import MenuItemGroup
def _log_sys_info() -> None:
@@ -73,12 +75,28 @@ def _list_scripts() -> str:
return '\n'.join(lines)
+def _tui_confirm(header: str) -> bool:
+ async def _ask() -> bool:
+ result = await Confirmation(
+ group=MenuItemGroup.yes_no(),
+ header=header,
+ allow_skip=False,
+ preset=False,
+ ).show()
+ return result.get_value()
+
+ return tui.run(_ask)
+
+
def run() -> int:
"""
This can either be run as the compiled and installed application: python setup.py install
OR straight as a module: python -m archinstall
In any case we will be attempting to load the provided script to be run from the scripts/ folder
"""
+ if 'share-log' in sys.argv:
+ return share_install_log(confirm=_tui_confirm)
+
arch_config_handler = ArchConfigHandler()
if '--help' in sys.argv or '-h' in sys.argv:
@@ -141,8 +159,8 @@ def _error_message(exc: Exception) -> None:
Archinstall experienced the above error. If you think this is a bug, please report it to
https://github.com/archlinux/archinstall and include the log file "/var/log/archinstall/install.log".
- Hint: To extract the log from a live ISO
- curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st
+ Hint: To upload the log and get a shareable URL, run
+ archinstall share-log
"""
)
warn(text)
diff --git a/archinstall/tui/components.py b/archinstall/tui/components.py
index f92364bb..9fecf3c6 100644
--- a/archinstall/tui/components.py
+++ b/archinstall/tui/components.py
@@ -200,6 +200,11 @@ class OptionListScreen(BaseScreen[ValueT]):
color: white;
text-style: bold;
}
+
+ .wrap-preview {
+ width: 100%;
+ height: auto;
+ }
"""
def __init__(
@@ -211,6 +216,7 @@ class OptionListScreen(BaseScreen[ValueT]):
allow_reset: bool = False,
preview_location: Literal['right', 'bottom'] | None = None,
enable_filter: bool = False,
+ wrap_preview: bool = False,
):
super().__init__(allow_skip, allow_reset)
self._group = group
@@ -218,6 +224,7 @@ class OptionListScreen(BaseScreen[ValueT]):
self._title = title
self._preview_location = preview_location
self._filter = enable_filter
+ self._wrap_preview = wrap_preview
self._show_frame = False
self._options = self._get_options()
@@ -280,7 +287,10 @@ class OptionListScreen(BaseScreen[ValueT]):
with Container():
yield option_list
yield Rule(orientation=rule_orientation)
- yield ScrollableContainer(Label('', id='preview_content', markup=False))
+ preview_label = Label('', id='preview_content', markup=False)
+ if self._wrap_preview:
+ preview_label.add_class('wrap-preview')
+ yield ScrollableContainer(preview_label)
if self._filter:
yield Input(placeholder='/filter', id='filter-input')
@@ -340,11 +350,6 @@ class OptionListScreen(BaseScreen[ValueT]):
]
)
- # debug(f'Index: {index}')
- # debug(f'Region: {option_list.region}')
- # debug(f'Scroll offset: {option_list.scroll_offset}')
- # debug(f'Target_Y: {target_y}')
-
self.app.cursor_position = Offset(option_list.region.x, target_y)
self.app.refresh()
@@ -433,6 +438,11 @@ class SelectListScreen(BaseScreen[ValueT]):
color: white;
text-style: bold;
}
+
+ .wrap-preview {
+ width: 100%;
+ height: auto;
+ }
"""
def __init__(
@@ -443,6 +453,7 @@ class SelectListScreen(BaseScreen[ValueT]):
allow_reset: bool = False,
preview_location: Literal['right', 'bottom'] | None = None,
enable_filter: bool = False,
+ wrap_preview: bool = False,
):
super().__init__(allow_skip, allow_reset)
self._group = group
@@ -450,6 +461,7 @@ class SelectListScreen(BaseScreen[ValueT]):
self._preview_location = preview_location
self._show_frame = False
self._filter = enable_filter
+ self._wrap_preview = wrap_preview
self._selected_items: list[MenuItem] = self._group.selected_items
self._options: list[Selection[MenuItem]] = self._get_selections()
@@ -510,7 +522,10 @@ class SelectListScreen(BaseScreen[ValueT]):
with Container():
yield selection_list
yield Rule(orientation=rule_orientation)
- yield ScrollableContainer(Label('', id='preview_content', markup=False))
+ preview_label = Label('', id='preview_content', markup=False)
+ if self._wrap_preview:
+ preview_label.add_class('wrap-preview')
+ yield ScrollableContainer(preview_label)
if self._filter:
yield Input(placeholder='/filter', id='filter-input')
@@ -1143,8 +1158,6 @@ class TableSelectionScreen(BaseScreen[ValueT]):
]
)
- debug(f'Setting cursor to target_y: {target_y}')
-
self.app.cursor_position = Offset(data_table.region.x, target_y)
self.app.refresh()
diff --git a/archinstall/tui/rich.py b/archinstall/tui/rich.py
new file mode 100644
index 00000000..055ec22c
--- /dev/null
+++ b/archinstall/tui/rich.py
@@ -0,0 +1,25 @@
+from io import StringIO
+
+from rich import box
+from rich.console import Console
+from rich.table import Table as RichTable
+
+
+class BaseRichTable(RichTable):
+ def __init__(self) -> None:
+ super().__init__(
+ box=box.SIMPLE,
+ show_header=False,
+ pad_edge=False,
+ show_edge=False,
+ )
+
+ def stringify(self) -> str:
+ string_io = StringIO()
+ buf = Console(file=string_io, highlight=False)
+ buf.print(self)
+
+ _ = string_io.seek(0)
+ output = string_io.read()
+
+ return output
diff --git a/docs/help/report_bug.rst b/docs/help/report_bug.rst
index bacaeb4c..b11027ab 100644
--- a/docs/help/report_bug.rst
+++ b/docs/help/report_bug.rst
@@ -15,7 +15,7 @@ When submitting a help ticket, please include the :code:`/var/log/archinstall/in
It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in.
.. tip::
- | An easy way to submit logs is ``curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st``.
+ | An easy way to submit logs is ``archinstall share-log``, which uploads ``install.log`` to paste.rs and prints a shareable URL.
| Use caution when submitting other log files, but ``archinstall`` pledges to keep ``install.log`` safe for posting publicly!
There are additional log files under ``/var/log/archinstall/`` that can be useful:
diff --git a/pyproject.toml b/pyproject.toml
index 28e91c2a..13e0933f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,7 +19,7 @@ classifiers = [
]
dependencies = [
"pyparted==3.13.0",
- "pydantic==2.12.5",
+ "pydantic==2.13.4",
"cryptography==48.0.0",
"textual==8.2.5",
"markdown-it-py==4.0.0",
@@ -34,7 +34,7 @@ Source = "https://github.com/archlinux/archinstall"
[project.optional-dependencies]
log = ["systemd_python==235"]
dev = [
- "mypy==1.20.2",
+ "mypy==2.1.0",
"flake8==7.3.0",
"pre-commit==4.6.0",
"ruff==0.15.12",
diff --git a/tests/test_share_log.py b/tests/test_share_log.py
new file mode 100644
index 00000000..c1c3ca84
--- /dev/null
+++ b/tests/test_share_log.py
@@ -0,0 +1,94 @@
+# pylint: disable=redefined-outer-name
+import urllib.error
+from io import BytesIO
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from archinstall.lib.output import share_install_log
+
+
+@pytest.fixture()
+def log_file(tmp_path: Path) -> Path:
+ log_dir = tmp_path / 'archinstall'
+ log_dir.mkdir()
+ return log_dir / 'install.log'
+
+
+def _fake_logger(log_file: Path) -> MagicMock:
+ mock = MagicMock()
+ mock.path = log_file
+ return mock
+
+
+def test_file_not_found(tmp_path: Path) -> None:
+ missing = tmp_path / 'no-such' / 'install.log'
+ with patch('archinstall.lib.output.logger', _fake_logger(missing)):
+ assert share_install_log() == 1
+
+
+def test_empty_file(log_file: Path) -> None:
+ log_file.write_bytes(b'')
+ with patch('archinstall.lib.output.logger', _fake_logger(log_file)):
+ assert share_install_log() == 1
+
+
+def test_user_cancels(log_file: Path) -> None:
+ log_file.write_text('some log content')
+ with patch('archinstall.lib.output.logger', _fake_logger(log_file)):
+ assert share_install_log(confirm=lambda _: False) == 1
+
+
+def test_successful_upload(log_file: Path) -> None:
+ log_file.write_text('some log content')
+ fake_response = BytesIO(b'https://paste.rs/abc.def')
+
+ with (
+ patch('archinstall.lib.output.logger', _fake_logger(log_file)),
+ patch('urllib.request.urlopen', return_value=fake_response) as mock_open,
+ ):
+ result = share_install_log()
+
+ assert result == 0
+ req = mock_open.call_args[0][0]
+ assert req.data == b'some log content'
+
+
+def test_truncation(log_file: Path) -> None:
+ max_size = 100
+ content = b'A' * 50 + b'B' * 80
+ log_file.write_bytes(content)
+ fake_response = BytesIO(b'https://paste.rs/abc.def')
+
+ with (
+ patch('archinstall.lib.output.logger', _fake_logger(log_file)),
+ patch('urllib.request.urlopen', return_value=fake_response) as mock_open,
+ ):
+ result = share_install_log(max_size=max_size)
+
+ assert result == 0
+ req = mock_open.call_args[0][0]
+ assert len(req.data) == max_size
+ assert req.data == content[-max_size:]
+
+
+def test_network_error(log_file: Path) -> None:
+ log_file.write_text('some log content')
+
+ with (
+ patch('archinstall.lib.output.logger', _fake_logger(log_file)),
+ patch('urllib.request.urlopen', side_effect=urllib.error.URLError('no network')),
+ ):
+ assert share_install_log() == 1
+
+
+def test_unexpected_response(log_file: Path) -> None:
+ log_file.write_text('some log content')
+ fake_response = BytesIO(b'ERROR: something went wrong')
+
+ with (
+ patch('archinstall.lib.output.logger', _fake_logger(log_file)),
+ patch('urllib.request.urlopen', return_value=fake_response),
+ ):
+ assert share_install_log() == 1