Compare commits

..

No commits in common. "master" and "4.2" have entirely different histories.
master ... 4.2

164 changed files with 6126 additions and 10666 deletions

View File

@ -41,8 +41,8 @@ body:
attributes: attributes:
value: > value: >
**Note**: Assuming you have network connectivity, **Note**: Assuming you have network connectivity,
you can easily upload the installation log and get a shareable URL by running: you can easily post the installation log using the following command:
`archinstall share-log` `curl -F'file=@/var/log/archinstall/install.log' https://0x0.st`
- type: textarea - type: textarea
id: freeform id: freeform

View File

@ -32,7 +32,7 @@ jobs:
- run: cat /etc/os-release - run: cat /etc/os-release
- run: pacman-key --init - run: pacman-key --init
- run: pacman --noconfirm -Sy archlinux-keyring - run: pacman --noconfirm -Sy archlinux-keyring
- run: ./test_tooling/mkarchiso/build_iso.sh - run: ./build_iso.sh
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: Arch Live ISO name: Arch Live ISO

View File

@ -1,22 +1,28 @@
name: Translation validation #on:
on: # push:
push: # paths:
paths: # - 'archinstall/locales/**'
- 'archinstall/**/*.py' # pull_request:
- 'archinstall/locales/**' # paths:
- '.github/workflows/translation-check.yaml' # - 'archinstall/locales/**'
pull_request: #name: Verify local_generate script was run on translation changes
paths: #jobs:
- 'archinstall/**/*.py' # translation-check:
- 'archinstall/locales/**' # runs-on: ubuntu-latest
- '.github/workflows/translation-check.yaml' # container:
jobs: # image: archlinux/archlinux:latest
translations: # steps:
name: Validate translations # - uses: actions/checkout@v4
runs-on: ubuntu-latest # - run: pacman --noconfirm -Syu python git diffutils
steps: # - name: Verify all translation scripts are up to date
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # run: |
- name: Install gettext # cd ..
run: sudo apt-get update && sudo apt-get install -y gettext # cp -r archinstall archinstall_orig
- name: Run translation checks # cd archinstall/archinstall/locales
run: bash archinstall/locales/locales_generator.sh check # bash locales_generator.sh 1> /dev/null
# cd ../../..
# git diff \
# --quiet --no-index --name-only \
# archinstall_orig/archinstall/locales \
# archinstall/archinstall/locales \
# || (echo "Translation files have not been updated after translation, please run ./locales_generator.sh once more and commit" && exit 1)

View File

@ -1,40 +0,0 @@
# This workflow will build an Arch Linux UKI file with the commit on it
name: Build Arch UKI with ArchInstall Commit
on:
push:
branches:
- master
- main # In case we adopt this convention in the future
pull_request:
paths-ignore:
- 'docs/**'
- '**.editorconfig'
- '**.gitignore'
- '**.md'
- 'LICENSE'
- 'PKGBUILD'
release:
types:
- created
jobs:
build:
runs-on: ubuntu-latest
container:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- run: pwd
- run: find .
- run: cat /etc/os-release
- run: pacman-key --init
- run: pacman --noconfirm -Sy archlinux-keyring
- run: pacman --noconfirm -Sy mkosi
- run: (cd test_tooling/mkosi/ && mkosi build -B)
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: Arch Live UKI
path: test_tooling/mkosi/mkosi.output/*.efi

4
.gitignore vendored
View File

@ -41,7 +41,3 @@ requirements.txt
/cmd_output.txt /cmd_output.txt
node_modules/ node_modules/
uv.lock uv.lock
test_tooling/mkosi/mkosi.output/*image*
test_tooling/mkosi/mkosi.cache/**
test_tooling/mkosi/mkosi.tools/**
test_tooling/mkosi/mkosi.tools.manifest

View File

@ -1,7 +1,7 @@
default_stages: ['pre-commit'] default_stages: ['pre-commit']
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.14 rev: v0.15.10
hooks: hooks:
# fix unused imports and sort them # fix unused imports and sort them
- id: ruff - id: ruff
@ -31,7 +31,7 @@ repos:
args: [--config=.flake8] args: [--config=.flake8]
fail_fast: true fail_fast: true
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0 rev: v1.20.1
hooks: hooks:
- id: mypy - id: mypy
args: [ args: [
@ -41,7 +41,6 @@ repos:
additional_dependencies: additional_dependencies:
- pydantic - pydantic
- pytest - pytest
- hypothesis
- cryptography - cryptography
- textual - textual
- repo: local - repo: local

View File

@ -5,7 +5,7 @@
# Contributor: demostanis worlds <demostanis@protonmail.com> # Contributor: demostanis worlds <demostanis@protonmail.com>
pkgname=archinstall pkgname=archinstall
pkgver=4.3 pkgver=4.2
pkgrel=1 pkgrel=1
pkgdesc="Just another guided/automated Arch Linux installer with a twist" pkgdesc="Just another guided/automated Arch Linux installer with a twist"
arch=(any) arch=(any)

View File

@ -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: When submitting an issue, please:
* Provide the stacktrace of the output if applicable * Provide the stacktrace of the output if applicable
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you! * Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
* To upload the log from the ISO image and get a shareable URL, run<br> * To extract the log from the ISO image, one way is to use<br>
```shell ```shell
archinstall share-log curl -F'file=@/var/log/archinstall/install.log' https://0x0.st
``` ```
@ -179,7 +179,7 @@ This can be done by installing `pacman -S arch-install-scripts util-linux` local
# losetup --partscan --show ./testimage.img # losetup --partscan --show ./testimage.img
# pip install --upgrade archinstall # pip install --upgrade archinstall
# python -m archinstall --script guided # python -m archinstall --script guided
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd # qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd
This will create a *20 GB* `testimage.img` and create a loop device which we can use to format and install to.<br> This will create a *20 GB* `testimage.img` and create a loop device which we can use to format and install to.<br>
`archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br> `archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br>
@ -199,8 +199,8 @@ You may want to boot an ISO image in a VM to test `archinstall` in there.
qemu-system-x86_64 -enable-kvm \ qemu-system-x86_64 -enable-kvm \
-machine q35,accel=kvm -device intel-iommu \ -machine q35,accel=kvm -device intel-iommu \
-cpu host -m 4096 -boot order=d \ -cpu host -m 4096 -boot order=d \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \ -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \ -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw -drive file=./archlinux-2025.12.01-x86_64.iso,format=raw
``` ```
@ -209,8 +209,8 @@ HINT: For espeakup support
qemu-system-x86_64 -enable-kvm \ qemu-system-x86_64 -enable-kvm \
-machine q35,accel=kvm -device intel-iommu \ -machine q35,accel=kvm -device intel-iommu \
-cpu host -m 4096 -boot order=d \ -cpu host -m 4096 -boot order=d \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \ -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \ -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw \ -drive file=./archlinux-2025.12.01-x86_64.iso,format=raw \
-device intel-hda -device hda-duplex,audiodev=snd0 \ -device intel-hda -device hda-duplex,audiodev=snd0 \
-audiodev pa,id=snd0,server=/run/user/1000/pulse/native -audiodev pa,id=snd0,server=/run/user/1000/pulse/native
@ -219,10 +219,6 @@ qemu-system-x86_64 -enable-kvm \
# FAQ # FAQ
## AUR
`archinstall` will not offer or bundle AUR helpers or AUR packages due to a current consensus. This is not any individual developers decision. The reasons and discussions for this stance on the topic can be found on our mailing list thread: [(optional) AUR helper in archinstall](https://lists.archlinux.org/archives/list/arch-dev-public@lists.archlinux.org/thread/VYOULH2GOJLFM2BXOFLWH3D754YXFPSL/).
## Keyring out-of-date ## Keyring out-of-date
For a description of the problem see https://archinstall.archlinux.page/help/known_issues.html#keyring-is-out-of-date-2213 and discussion in issue https://github.com/archlinux/archinstall/issues/2213. For a description of the problem see https://archinstall.archlinux.page/help/known_issues.html#keyring-is-out-of-date-2213 and discussion in issue https://github.com/archlinux/archinstall/issues/2213.

View File

@ -1,9 +1,9 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from archinstall.lib.hardware import SysInfo from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug
from archinstall.lib.models.application import Audio, AudioConfiguration from archinstall.lib.models.application import Audio, AudioConfiguration
from archinstall.lib.models.users import User from archinstall.lib.models.users import User
from archinstall.lib.output import debug
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer

View File

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from archinstall.lib.log import debug from archinstall.lib.output import debug
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer

View File

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import Firewall, FirewallConfiguration from archinstall.lib.models.application import Firewall, FirewallConfiguration
from archinstall.lib.output import debug
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer

View File

@ -1,14 +0,0 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import FontsConfiguration
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class FontsApp:
def install(self, install_session: Installer, fonts_config: FontsConfiguration) -> None:
packages = [f.value for f in fonts_config.fonts]
debug(f'Installing fonts: {packages}')
install_session.add_additional_packages(packages)

View File

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration
from archinstall.lib.output import debug
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
@ -21,18 +21,6 @@ class PowerManagementApp:
'tuned-ppd', 'tuned-ppd',
] ]
@property
def ppd_services(self) -> list[str]:
return [
'power-profiles-daemon.service',
]
@property
def tuned_services(self) -> list[str]:
return [
'tuned.service',
]
def install( def install(
self, self,
install_session: Installer, install_session: Installer,
@ -43,7 +31,5 @@ class PowerManagementApp:
match power_management_config.power_management: match power_management_config.power_management:
case PowerManagement.POWER_PROFILES_DAEMON: case PowerManagement.POWER_PROFILES_DAEMON:
install_session.add_additional_packages(self.ppd_packages) install_session.add_additional_packages(self.ppd_packages)
install_session.enable_service(self.ppd_services)
case PowerManagement.TUNED: case PowerManagement.TUNED:
install_session.add_additional_packages(self.tuned_packages) install_session.add_additional_packages(self.tuned_packages)
install_session.enable_service(self.tuned_services)

View File

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from archinstall.lib.log import debug from archinstall.lib.output import debug
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer

View File

@ -1,15 +1,14 @@
from typing import TYPE_CHECKING, Self, override from typing import TYPE_CHECKING, Self, override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType, SelectResult from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType, SelectResult
from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import info
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class DesktopProfile(Profile): class DesktopProfile(Profile):
@ -89,11 +88,6 @@ class DesktopProfile(Profile):
for profile in self.current_selection: for profile in self.current_selection:
profile.post_install(install_session) 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 @override
def install(self, install_session: Installer) -> None: def install(self, install_session: Installer) -> None:
# Install common packages for all desktop environments # Install common packages for all desktop environments

View File

@ -0,0 +1,6 @@
from enum import Enum
class SeatAccess(Enum):
seatd = 'seatd'
polkit = 'polkit'

View File

@ -1,8 +1,6 @@
from typing import override from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType 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): class BspwmProfile(Profile):
@ -29,11 +27,3 @@ class BspwmProfile(Profile):
@override @override
def default_greeter_type(self) -> GreeterType: def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm 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)

View File

@ -9,7 +9,7 @@ class BudgieProfile(Profile):
'Budgie', 'Budgie',
ProfileType.DesktopEnv, ProfileType.DesktopEnv,
support_gfx_driver=True, support_gfx_driver=True,
display_server=DisplayServerType.Wayland, display_server=DisplayServerType.Xorg,
) )
@property @property
@ -20,7 +20,6 @@ class BudgieProfile(Profile):
'budgie', 'budgie',
'mate-terminal', 'mate-terminal',
'nemo', 'nemo',
'nemo-fileroller',
'papirus-icon-theme', 'papirus-icon-theme',
] ]

View File

@ -0,0 +1,26 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class CutefishProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Cutefish',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'cutefish',
'noto-fonts',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Sddm

View File

@ -1,7 +1,11 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops.utils import select_seat_access from archinstall.default_profiles.desktops import SeatAccess
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class HyprlandProfile(Profile): class HyprlandProfile(Profile):
@ -45,8 +49,26 @@ class HyprlandProfile(Profile):
return [pref] return [pref]
return [] return []
async def _select_seat_access(self) -> None:
# need to activate seat service and add to seat group
header = tr('Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
header += '\n' + tr('Choose an option to give Hyprland access to your hardware') + '\n'
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
group.set_default_by_value(default)
result = await Selection[SeatAccess](
group,
header=header,
allow_skip=False,
).show()
if result.type_ == ResultType.Selection:
self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value
@override @override
async def do_on_select(self) -> None: async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None) await self._select_seat_access()
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,7 +1,11 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops.utils import select_seat_access from archinstall.default_profiles.desktops import SeatAccess
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class LabwcProfile(Profile): class LabwcProfile(Profile):
@ -39,8 +43,26 @@ class LabwcProfile(Profile):
return [pref] return [pref]
return [] return []
async def _select_seat_access(self) -> None:
# need to activate seat service and add to seat group
header = tr('labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
header += '\n' + tr('Choose an option to give labwc access to your hardware') + '\n'
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
group.set_default_by_value(default)
result = await Selection[SeatAccess](
group,
header=header,
allow_skip=False,
).show()
if result.type_ == ResultType.Selection:
self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value
@override @override
async def do_on_select(self) -> None: async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None) await self._select_seat_access()
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,13 +1,17 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops.utils import select_seat_access from archinstall.default_profiles.desktops import SeatAccess
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class NiriProfile(Profile): class NiriProfile(Profile):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
'niri', 'Niri',
ProfileType.WindowMgr, ProfileType.WindowMgr,
support_gfx_driver=True, support_gfx_driver=True,
display_server=DisplayServerType.Wayland, display_server=DisplayServerType.Wayland,
@ -47,8 +51,26 @@ class NiriProfile(Profile):
return [pref] return [pref]
return [] return []
async def _select_seat_access(self) -> None:
# need to activate seat service and add to seat group
header = tr('niri needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
header += '\n' + tr('Choose an option to give niri access to your hardware') + '\n'
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
group.set_default_by_value(default)
result = await Selection[SeatAccess](
group,
header=header,
allow_skip=False,
).show()
if result.type_ == ResultType.Selection:
self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value
@override @override
async def do_on_select(self) -> None: async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None) await self._select_seat_access()
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,66 +0,0 @@
import shutil
from pathlib import Path
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
_TERMINAL = 'alacritty'
_ASSETS_DIR = Path(__file__).parent / 'niri_dms_assets'
class NiriDmsProfile(Profile):
def __init__(self) -> None:
super().__init__(
'niri - DankMaterialShell',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property
@override
def packages(self) -> list[str]:
return [
'niri',
'dms-shell-niri',
'polkit',
'xdg-desktop-portal-gnome',
'xorg-xwayland',
'matugen',
'cava',
'kimageformats',
'cups-pk-helper',
'tuned-ppd',
_TERMINAL,
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.GreetdDms
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
binds = (_ASSETS_DIR / 'dms/binds.kdl').read_text().replace('{{TERMINAL_COMMAND}}', _TERMINAL)
for user in users:
home = install_session.target / 'home' / user.username
niri_dir = home / '.config/niri'
dms_dir = niri_dir / 'dms'
dms_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(_ASSETS_DIR / 'niri.kdl', niri_dir / 'config.kdl')
for name in ('colors.kdl', 'layout.kdl', 'alttab.kdl', 'outputs.kdl', 'cursor.kdl'):
shutil.copy(_ASSETS_DIR / 'dms' / name, dms_dir / name)
(dms_dir / 'binds.kdl').write_text(binds)
niri_unit_dropin = home / '.config/systemd/user/niri.service.d'
niri_unit_dropin.mkdir(parents=True, exist_ok=True)
(niri_unit_dropin / 'dms.conf').write_text('[Unit]\nWants=dms.service\n')
install_session.arch_chroot(f'chown -R {user.username}:{user.username} /home/{user.username}/.config')

View File

@ -1,10 +0,0 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
recent-windows {
highlight {
corner-radius 12
}
}

View File

@ -1,221 +0,0 @@
binds {
// === System & Overview ===
Mod+D repeat=false { toggle-overview; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
XF86AudioPause allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPlay allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPrev allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "previous";
}
XF86AudioNext allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "next";
}
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "increment" "3";
}
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Workspace Management ===
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
spawn "dms" "ipc" "call" "workspace-rename" "open";
}
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
}

View File

@ -1,39 +0,0 @@
// ! Auto-generated file. Do not edit directly.
// Remove `include "dms/colors.kdl"` from your config to override.
layout {
background-color "transparent"
focus-ring {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
border {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
shadow {
color "#00000070"
}
tab-indicator {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
insert-hint {
color "#d0bcff80"
}
}
recent-windows {
highlight {
active-color "#4f378b"
urgent-color "#f2b8b5"
}
}

View File

@ -1,6 +0,0 @@
// Place cursor configuration here.
// Example:
// cursor {
// xcursor-theme "Adwaita"
// xcursor-size 24
// }

View File

@ -1,22 +0,0 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout {
gaps 4
border {
width 2
}
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius 12
clip-to-geometry true
tiled-state true
draw-border-with-background false
}

View File

@ -1,7 +0,0 @@
// Place per-output configuration here.
// Example:
// output "DP-1" {
// mode "2560x1440@165"
// position x=0 y=0
// scale 1
// }

View File

@ -1,279 +0,0 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
config-notification {
disable-failed
}
gestures {
hot-corners {
off
}
}
// Input device configuration.
// Find the full list of options on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
input {
keyboard {
xkb {
// You can set rules, model, layout, variant and options.
// For more information, see xkeyboard-config(7).
// For example:
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
// If this section is empty, niri will fetch xkb settings
// from org.freedesktop.locale1. You can control these using
// localectl set-x11-keymap.
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// scroll-button-lock
// middle-emulation
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-2" {
mode "2560x1600@239.998993"
position x=2560 y=0
variable-refresh-rate
}
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
background-color "transparent"
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
// or right edge of the screen.
// - "always", the focused column will always be centered.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
center-focused-column "never"
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.33333
proportion 0.5
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decorations can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
//
// Alternatively, you can override it with a window rule called
// `draw-border-with-background`.
border {
off
width 4
active-color "#707070" // Neutral gray
inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red
}
shadow {
softness 30
spread 5
offset x=0 y=5
color "#0007"
}
struts {
}
}
layer-rule {
match namespace="^quickshell$"
place-within-backdrop true
}
overview {
workspace-shadow {
off
}
}
// Add lines like this to spawn processes at startup.
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors.
environment {
XDG_CURRENT_DESKTOP "niri"
}
hotkey-overlay {
skip-at-startup
}
prefer-no-csd
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
animations {
workspace-switch {
spring damping-ratio=0.80 stiffness=523 epsilon=0.0001
}
window-open {
duration-ms 150
curve "ease-out-expo"
}
window-close {
duration-ms 150
curve "ease-out-quad"
}
horizontal-view-movement {
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
}
window-movement {
spring damping-ratio=0.75 stiffness=323 epsilon=0.0001
}
window-resize {
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
}
config-notification-open-close {
spring damping-ratio=0.65 stiffness=923 epsilon=0.001
}
screenshot-ui-open {
duration-ms 200
curve "ease-out-quad"
}
overview-open-close {
spring damping-ratio=0.85 stiffness=800 epsilon=0.0001
}
}
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
window-rule {
// This regular expression is intentionally made as specific as possible,
// since this is the default config, and we want no false positives.
// You can get away with just app-id="wezterm" if you want.
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
window-rule {
match app-id=r#"^org\.gnome\."#
draw-border-with-background false
geometry-corner-radius 12
clip-to-geometry true
}
window-rule {
match app-id=r#"^gnome-control-center$"#
match app-id=r#"^pavucontrol$"#
match app-id=r#"^nm-connection-editor$"#
default-column-width { proportion 0.5; }
open-floating false
}
window-rule {
match app-id=r#"^org\.gnome\.Calculator$"#
match app-id=r#"^gnome-calculator$"#
match app-id=r#"^galculator$"#
match app-id=r#"^blueman-manager$"#
match app-id=r#"^org\.gnome\.Nautilus$"#
match app-id=r#"^xdg-desktop-portal$"#
open-floating true
}
window-rule {
match app-id=r#"^steam$"# title=r#"^notificationtoasts_\d+_desktop$"#
default-floating-position x=10 y=10 relative-to="bottom-right"
open-focused false
}
window-rule {
match app-id=r#"^org\.wezfurlong\.wezterm$"#
match app-id="Alacritty"
match app-id="zen"
match app-id="com.mitchellh.ghostty"
match app-id="kitty"
draw-border-with-background false
}
window-rule {
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
match app-id="zoom"
open-floating true
}
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
match app-id=r#"com.danklinux.dms$"#
open-floating true
}
debug {
honor-xdg-activation-with-invalid-serial
}
// Override to disable super+tab
recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
Alt+grave { next-window filter="app-id"; }
Alt+Shift+grave { previous-window filter="app-id"; }
}
}
// Include dms files
include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"
include "dms/outputs.kdl"
include "dms/cursor.kdl"

View File

@ -5,8 +5,8 @@ from archinstall.default_profiles.profile import CustomSetting, DisplayServerTyp
from archinstall.lib.menu.helpers import Selection from archinstall.lib.menu.helpers import Selection
from archinstall.lib.packages.packages import available_package, package_group_info from archinstall.lib.packages.packages import available_package, package_group_info
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class PlasmaFlavor(StrEnum): class PlasmaFlavor(StrEnum):

View File

@ -1,7 +1,11 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops.utils import select_seat_access from archinstall.default_profiles.desktops import SeatAccess
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class SwayProfile(Profile): class SwayProfile(Profile):
@ -49,8 +53,26 @@ class SwayProfile(Profile):
return [pref] return [pref]
return [] return []
async def _select_seat_access(self) -> None:
# need to activate seat service and add to seat group
header = tr('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
header += '\n' + tr('Choose an option to give Sway access to your hardware') + '\n'
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
group.set_default_by_value(default)
result = await Selection[SeatAccess](
group,
header=header,
allow_skip=False,
).show()
if result.type_ == ResultType.Selection:
self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value
@override @override
async def do_on_select(self) -> None: async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None) await self._select_seat_access()
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,33 +0,0 @@
from enum import Enum
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class SeatAccess(Enum):
seatd = 'seatd'
polkit = 'polkit'
async def select_seat_access(profile_name: str, default: str | None) -> SeatAccess:
header = tr('{} needs access to your seat').format(profile_name)
header += f' ({tr("collection of hardware devices i.e. keyboard, mouse")})' + '\n'
header += tr('Choose an option how to give {} access to your hardware').format(profile_name)
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default)
result = await Selection[SeatAccess](
group,
header=header,
allow_skip=False,
).show()
if result.type_ == ResultType.Selection:
return result.get_value()
else:
raise ValueError('Unexpected result type from seat access selection')

View File

@ -37,7 +37,6 @@ class GreeterType(Enum):
Ly = 'ly' Ly = 'ly'
CosmicSession = 'cosmic-greeter' CosmicSession = 'cosmic-greeter'
PlasmaLoginManager = 'plasma-login-manager' PlasmaLoginManager = 'plasma-login-manager'
GreetdDms = 'dms-greeter'
class SelectResult(Enum): class SelectResult(Enum):

View File

@ -1,11 +1,11 @@
from typing import TYPE_CHECKING, Self, override from typing import TYPE_CHECKING, Self, override
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult
from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import info
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer

View File

@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
from archinstall.applications.audio import AudioApp from archinstall.applications.audio import AudioApp
from archinstall.applications.bluetooth import BluetoothApp from archinstall.applications.bluetooth import BluetoothApp
from archinstall.applications.firewall import FirewallApp from archinstall.applications.firewall import FirewallApp
from archinstall.applications.fonts import FontsApp
from archinstall.applications.power_management import PowerManagementApp from archinstall.applications.power_management import PowerManagementApp
from archinstall.applications.print_service import PrintServiceApp from archinstall.applications.print_service import PrintServiceApp
from archinstall.lib.models import Audio from archinstall.lib.models import Audio
@ -43,9 +42,3 @@ class ApplicationHandler:
install_session, install_session,
app_config.firewall_config, app_config.firewall_config,
) )
if app_config.fonts_config:
FontsApp().install(
install_session,
app_config.fonts_config,
)

View File

@ -10,15 +10,13 @@ from archinstall.lib.models.application import (
BluetoothConfiguration, BluetoothConfiguration,
Firewall, Firewall,
FirewallConfiguration, FirewallConfiguration,
FontPackage,
FontsConfiguration,
PowerManagement, PowerManagement,
PowerManagementConfiguration, PowerManagementConfiguration,
PrintServiceConfiguration, PrintServiceConfiguration,
) )
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]): class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
@ -79,13 +77,6 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
preview_action=self._prev_firewall, preview_action=self._prev_firewall,
key='firewall_config', key='firewall_config',
), ),
MenuItem(
text=tr('Additional fonts'),
action=select_fonts,
value=self._app_config.fonts_config,
preview_action=self._prev_fonts,
key='fonts_config',
),
] ]
def _prev_power_management(self, item: MenuItem) -> str | None: def _prev_power_management(self, item: MenuItem) -> str | None:
@ -124,13 +115,6 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
return f'{tr("Firewall")}: {config.firewall.value}' return f'{tr("Firewall")}: {config.firewall.value}'
return None return None
def _prev_fonts(self, item: MenuItem) -> str | None:
if item.value is not None:
config: FontsConfiguration = item.value
packages = ', '.join(f.value for f in config.fonts)
return f'{tr("Additional fonts")}: {packages}'
return None
async def select_power_management(preset: PowerManagementConfiguration | None = None) -> PowerManagementConfiguration | None: async def select_power_management(preset: PowerManagementConfiguration | None = None) -> PowerManagementConfiguration | None:
group = MenuItemGroup.from_enum(PowerManagement) group = MenuItemGroup.from_enum(PowerManagement)
@ -233,31 +217,3 @@ async def select_firewall(preset: FirewallConfiguration | None = None) -> Firewa
return FirewallConfiguration(firewall=result.get_value()) return FirewallConfiguration(firewall=result.get_value())
case ResultType.Reset: case ResultType.Reset:
return None return None
async def select_fonts(preset: FontsConfiguration | None = None) -> FontsConfiguration | None:
items = [MenuItem(f'{f.value} ({f.description()})', value=f) for f in FontPackage]
group = MenuItemGroup(items)
if preset:
for f in preset.fonts:
group.set_selected_by_value(f)
result = await Selection[FontPackage](
group,
header=tr('Select font packages to install'),
allow_skip=True,
allow_reset=True,
multi=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
selected = result.get_values()
if selected:
return FontsConfiguration(fonts=selected)
return None
case ResultType.Reset:
return None

View File

@ -6,7 +6,6 @@ import urllib.error
import urllib.parse import urllib.parse
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, StrEnum
from pathlib import Path from pathlib import Path
from typing import Any, Self from typing import Any, Self
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
@ -14,29 +13,23 @@ from urllib.request import Request, urlopen
from pydantic.dataclasses import dataclass as p_dataclass from pydantic.dataclasses import dataclass as p_dataclass
from archinstall.lib.crypt import decrypt from archinstall.lib.crypt import decrypt
from archinstall.lib.log import debug, error, logger, warn
from archinstall.lib.menu.util import get_password from archinstall.lib.menu.util import get_password
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration 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.device import DiskEncryption, DiskLayoutConfiguration
from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration
from archinstall.lib.models.network import NetworkConfiguration from archinstall.lib.models.network import NetworkConfiguration
from archinstall.lib.models.package_types import DEFAULT_KERNEL
from archinstall.lib.models.packages import Repository from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.models.users import Password, User, UserSerialization from archinstall.lib.models.users import Password, User, UserSerialization
from archinstall.lib.output import debug, error, logger, warn
from archinstall.lib.plugins import load_plugin from archinstall.lib.plugins import load_plugin
from archinstall.lib.translationhandler import Language, tr, translation_handler from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.version import get_version from archinstall.lib.version import get_version
from archinstall.tui.components import tui from archinstall.tui.ui.components import tui
class SubCommand(Enum):
SHARE_LOG = 'share-log'
@p_dataclass @p_dataclass
@ -62,83 +55,6 @@ class Arguments:
advanced: bool = False advanced: bool = False
verbose: bool = False verbose: bool = False
command: SubCommand | None = None
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 @dataclass
class ArchConfig: class ArchConfig:
@ -155,7 +71,7 @@ class ArchConfig:
auth_config: AuthenticationConfiguration | None = None auth_config: AuthenticationConfiguration | None = None
swap: ZramConfiguration | None = None swap: ZramConfiguration | None = None
hostname: str = 'archlinux' hostname: str = 'archlinux'
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value]) kernels: list[str] = field(default_factory=lambda: ['linux'])
ntp: bool = True ntp: bool = True
packages: list[str] = field(default_factory=list) packages: list[str] = field(default_factory=list)
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default) pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
@ -163,84 +79,58 @@ class ArchConfig:
services: list[str] = field(default_factory=list) services: list[str] = field(default_factory=list)
custom_commands: list[str] = field(default_factory=list) custom_commands: list[str] = field(default_factory=list)
def unsafe_config(self) -> dict[ArchConfigType, Any]: def unsafe_config(self) -> dict[str, Any]:
config: dict[ArchConfigType, list[UserSerialization] | str | None] = {} config: dict[str, list[UserSerialization] | str | None] = {}
if self.auth_config: if self.auth_config:
if self.auth_config.users: if self.auth_config.users:
config[ArchConfigType.USERS] = [user.json() for user in self.auth_config.users] config['users'] = [user.json() for user in self.auth_config.users]
if self.auth_config.root_enc_password: if self.auth_config.root_enc_password:
config[ArchConfigType.ROOT_ENC_PASSWORD] = self.auth_config.root_enc_password.enc_password config['root_enc_password'] = self.auth_config.root_enc_password.enc_password
if self.disk_config: if self.disk_config:
disk_encryption = self.disk_config.disk_encryption disk_encryption = self.disk_config.disk_encryption
if disk_encryption and disk_encryption.encryption_password: if disk_encryption and disk_encryption.encryption_password:
config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext config['encryption_password'] = disk_encryption.encryption_password.plaintext
return config return config
def safe_config(self) -> dict[ArchConfigType, Any]: def safe_config(self) -> dict[str, Any]:
base_config: dict[ArchConfigType, Any] = { config: Any = {
ArchConfigType.VERSION: self.version, 'version': self.version,
ArchConfigType.SCRIPT: self.script, 'script': self.script,
ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(), '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,
} }
base_config.update(self.plain_cfg())
sub_config = self.sub_cfg()
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()
return base_config
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,
}
def sub_cfg(self) -> dict[ArchConfigType, SubConfig]:
cfg: dict[ArchConfigType, SubConfig] = {
ArchConfigType.PACMAN_CONFIG: self.pacman_config,
}
if self.mirror_config:
cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config
if self.bootloader_config:
cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config
if self.disk_config:
cfg[ArchConfigType.DISK_CONFIG] = self.disk_config
if self.swap:
cfg[ArchConfigType.SWAP] = self.swap
if self.auth_config:
cfg[ArchConfigType.AUTH_CONFIG] = self.auth_config
if self.locale_config: if self.locale_config:
cfg[ArchConfigType.LOCALE_CONFIG] = self.locale_config config['locale_config'] = self.locale_config.json()
if self.disk_config:
config['disk_config'] = self.disk_config.json()
if self.profile_config: if self.profile_config:
cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config config['profile_config'] = self.profile_config.json()
if self.mirror_config:
config['mirror_config'] = self.mirror_config.json()
if self.network_config: if self.network_config:
cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config config['network_config'] = self.network_config.json()
if self.app_config: return config
cfg[ArchConfigType.APP_CONFIG] = self.app_config
return cfg
@classmethod @classmethod
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self: def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
@ -253,7 +143,6 @@ class ArchConfig:
if archinstall_lang := args_config.get('archinstall-language', None): if archinstall_lang := args_config.get('archinstall-language', None):
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang) 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', {}): if disk_config := args_config.get('disk_config', {}):
enc_password = args_config.get('encryption_password', '') enc_password = args_config.get('encryption_password', '')
@ -371,13 +260,13 @@ class ArchConfig:
class ArchConfigHandler: class ArchConfigHandler:
def __init__(self) -> None: def __init__(self) -> None:
self._parser: ArgumentParser = self._define_arguments() self._parser: ArgumentParser = self._define_arguments()
self._add_sub_parsers() args: Arguments = self._parse_args()
self._args = args
self._args: Arguments = self._parse_args()
config = self._parse_config() config = self._parse_config()
try: try:
self._config = ArchConfig.from_config(config, self._args) self._config = ArchConfig.from_config(config, args)
self._config.version = get_version() self._config.version = get_version()
except ValueError as err: except ValueError as err:
warn(str(err)) warn(str(err))
@ -403,13 +292,8 @@ class ArchConfigHandler:
def print_help(self) -> None: def print_help(self) -> None:
self._parser.print_help() self._parser.print_help()
def _add_sub_parsers(self) -> None:
subparsers = self._parser.add_subparsers(dest='command', help='Available subcommands')
_ = subparsers.add_parser(SubCommand.SHARE_LOG.value, help='Upload log file to public server')
def _define_arguments(self) -> ArgumentParser: def _define_arguments(self) -> ArgumentParser:
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument( parser.add_argument(
'-v', '-v',
'--version', '--version',
@ -545,6 +429,7 @@ class ArchConfigHandler:
default=False, default=False,
help='Enabled verbose options', help='Enabled verbose options',
) )
return parser return parser
def _parse_args(self) -> Arguments: def _parse_args(self) -> Arguments:

View File

@ -3,9 +3,9 @@ from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from archinstall.lib.command import SysCommandWorker from archinstall.lib.command import SysCommandWorker
from archinstall.lib.log import debug, info
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import User from archinstall.lib.models.users import User
from archinstall.lib.output import debug, info
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
if TYPE_CHECKING: if TYPE_CHECKING:
@ -53,7 +53,7 @@ class AuthenticationHandler:
def _add_u2f_entry(self, file: Path, entry: str) -> None: def _add_u2f_entry(self, file: Path, entry: str) -> None:
if not file.exists(): if not file.exists():
debug(f'File does not exist: {file}') debug(f'File does not exist: {file}')
return return None
content = file.read_text().splitlines() content = file.read_text().splitlines()
@ -81,7 +81,7 @@ class AuthenticationHandler:
install_session.pacman.strap('pam-u2f') install_session.pacman.strap('pam-u2f')
print(tr('Setting up U2F login: {}').format(u2f_config.u2f_login_method.value)) print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}'))
# https://developers.yubico.com/pam-u2f/ # https://developers.yubico.com/pam-u2f/
u2f_auth_file = install_session.target / 'etc/u2f_mappings' u2f_auth_file = install_session.target / 'etc/u2f_mappings'

View File

@ -6,11 +6,11 @@ from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password from archinstall.lib.menu.util import get_password
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import Password, User from archinstall.lib.models.users import Password, User
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.lib.user.user_menu import select_users from archinstall.lib.user.user_menu import select_users
from archinstall.lib.utils.format import as_table from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType
from archinstall.tui.result import ResultType
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]): class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
@ -65,7 +65,7 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
users: list[User] | None = item.value users: list[User] | None = item.value
if users: if users:
return as_table(users) return FormattedOutput.as_table(users)
return None return None
def _prev_root_pwd(self, item: MenuItem) -> str | None: def _prev_root_pwd(self, item: MenuItem) -> str | None:

View File

@ -6,7 +6,7 @@ from typing import ClassVar, Self
from archinstall.lib.command import SysCommand, SysCommandWorker from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.exceptions import SysCallError from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import error from archinstall.lib.output import error
class Boot: class Boot:

View File

@ -5,8 +5,8 @@ from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]): class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):

View File

@ -1,86 +0,0 @@
from dataclasses import dataclass
from enum import Enum, auto
from pathlib import Path
from archinstall.lib.hardware import SysInfo
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration
class BootloaderValidationFailureKind(Enum):
LimineNonFatBoot = auto()
LimineLayout = auto()
BootloaderRequiresUefi = auto()
EfistubNonFatBoot = auto()
@dataclass(frozen=True)
class BootloaderValidationFailure:
kind: BootloaderValidationFailureKind
description: str
def validate_bootloader_layout(
bootloader_config: BootloaderConfiguration | None,
disk_config: DiskLayoutConfiguration | None,
) -> BootloaderValidationFailure | None:
"""Validate bootloader configuration against disk layout.
Returns a failure with a human-readable description if the configuration
would produce an unbootable system, or None if it is valid.
"""
if not (bootloader_config and disk_config):
return None
bootloader = bootloader_config.bootloader
if bootloader == Bootloader.NO_BOOTLOADER:
return None
if bootloader.is_uefi_only() and not SysInfo.has_uefi():
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.BootloaderRequiresUefi,
description=f'{bootloader.value} requires a UEFI system.',
)
boot_part = next(
(p for m in disk_config.device_modifications if (p := m.get_boot_partition())),
None,
)
if bootloader == Bootloader.Efistub:
# The UEFI firmware reads the kernel directly from the boot partition,
# which must be FAT.
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.EfistubNonFatBoot,
description='Efistub does not support booting with a non-FAT boot partition.',
)
if bootloader == Bootloader.Limine:
# Limine reads its config and kernels from the boot partition, which
# must be FAT.
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.LimineNonFatBoot,
description='Limine does not support booting with a non-FAT boot partition.',
)
# When the ESP is the boot partition but mounted outside /boot and
# UKI is disabled, kernels end up on the root filesystem which
# Limine cannot access.
if not bootloader_config.uki:
efi_part = next(
(p for m in disk_config.device_modifications if (p := m.get_efi_partition())),
None,
)
if efi_part and efi_part == boot_part and efi_part.mountpoint != Path('/boot'):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.LimineLayout,
description=(
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_part.mountpoint}, '
'enable UKI or add a separate /boot partition to install Limine.'
),
)
return None

View File

@ -11,7 +11,7 @@ from types import TracebackType
from typing import Any, Self, override from typing import Any, Self, override
from archinstall.lib.exceptions import RequirementError, SysCallError from archinstall.lib.exceptions import RequirementError, SysCallError
from archinstall.lib.log import debug, error, logger from archinstall.lib.output import debug, error, logger
from archinstall.lib.utils.encoding import clear_vt100_escape_codes from archinstall.lib.utils.encoding import clear_vt100_escape_codes
@ -44,8 +44,8 @@ class SysCommandWorker:
self._trace_log_pos = 0 self._trace_log_pos = 0
self.poll_object = epoll() self.poll_object = epoll()
self.child_fd: int | None = None self.child_fd: int | None = None
self.started = False self.started: float | None = None
self.ended = False self.ended: float | None = None
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
def __contains__(self, key: bytes) -> bool: def __contains__(self, key: bytes) -> bool:
@ -117,7 +117,7 @@ class SysCommandWorker:
def is_alive(self) -> bool: def is_alive(self) -> bool:
self.poll() self.poll()
if self.started and not self.ended: if self.started and self.ended is None:
return True return True
return False return False
@ -173,11 +173,11 @@ class SysCommandWorker:
self.peak(output) self.peak(output)
self._trace_log += output self._trace_log += output
except OSError: except OSError:
self.ended = True self.ended = time.time()
break break
if self.ended or (not got_output and not _pid_exists(self.pid)): if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = True self.ended = time.time()
try: try:
wait_status = os.waitpid(self.pid, 0)[1] wait_status = os.waitpid(self.pid, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status) self.exit_code = os.waitstatus_to_exitcode(wait_status)
@ -215,7 +215,7 @@ class SysCommandWorker:
# Only parent process moves back to the original working directory # Only parent process moves back to the original working directory
os.chdir(old_dir) os.chdir(old_dir)
self.started = True self.started = time.time()
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP) self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True return True

View File

@ -6,16 +6,14 @@ from typing import Any
from pydantic import TypeAdapter from pydantic import TypeAdapter
from archinstall.lib.args import ArchConfig, ArchConfigType from archinstall.lib.args import ArchConfig
from archinstall.lib.crypt import encrypt from archinstall.lib.crypt import encrypt
from archinstall.lib.log import debug, logger, warn
from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password, prompt_dir from archinstall.lib.menu.util import get_password, prompt_dir
from archinstall.lib.models.network import NetworkConfiguration from archinstall.lib.output import debug, logger, warn
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_key_value_pair from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType
from archinstall.tui.result import ResultType
class ConfigurationOutput: class ConfigurationOutput:
@ -45,51 +43,25 @@ class ConfigurationOutput:
def user_config_to_json(self) -> str: def user_config_to_json(self) -> str:
config = self._config.safe_config() config = self._config.safe_config()
adapter = TypeAdapter(dict[ArchConfigType, Any]) adapter = TypeAdapter(dict[str, Any])
python_dict = adapter.dump_python(config) python_dict = adapter.dump_python(config)
return json.dumps(python_dict, indent=4, sort_keys=True) return json.dumps(python_dict, indent=4, sort_keys=True)
def user_credentials_to_json(self) -> str: def user_credentials_to_json(self) -> str:
cfg = self._config.unsafe_config() config = self._config.unsafe_config()
adapter = TypeAdapter(dict[ArchConfigType, Any]) adapter = TypeAdapter(dict[str, Any])
python_dict = adapter.dump_python(cfg) python_dict = adapter.dump_python(config)
return json.dumps(python_dict, indent=4, sort_keys=True) return json.dumps(python_dict, indent=4, sort_keys=True)
def write_debug(self) -> None: def write_debug(self) -> None:
debug(' -- Chosen configuration --') debug(' -- Chosen configuration --')
debug(self.user_config_to_json()) debug(self.user_config_to_json())
def as_summary(self) -> str: async def confirm_config(self) -> bool:
"""
Render a concise two-column summary of the current configuration.
Returns an empty string if nothing meaningful to show.
"""
cfg: dict[str, str | list[str] | bool] = {}
for key, value in self._config.plain_cfg().items():
cfg[key.text()] = value
for config_type, obj in self._config.sub_cfg().items():
if not hasattr(obj, 'summary'):
continue
summary = obj.summary()
if summary:
cfg[config_type.text()] = summary
simple_summary = as_key_value_pair(cfg, ignore_empty=True)
return simple_summary
async def confirm_config(self, show_install_warnings: bool = False) -> bool:
header = f'{tr("The specified configuration will be applied")}. ' header = f'{tr("The specified configuration will be applied")}. '
header += tr('Would you like to continue?') + '\n' header += tr('Would you like to continue?') + '\n'
if show_install_warnings:
header += self._render_install_warnings()
group = MenuItemGroup.yes_no() group = MenuItemGroup.yes_no()
group.set_preview_for_all(lambda x: self.user_config_to_json()) group.set_preview_for_all(lambda x: self.user_config_to_json())
@ -107,22 +79,6 @@ class ConfigurationOutput:
return True return True
def get_install_warnings(self) -> list[str]:
warnings: list[str] = []
if not isinstance(self._config.network_config, NetworkConfiguration):
warnings.append(tr('Warning: no network configuration selected. Network will need to be set up manually on the installed system.'))
return warnings
def _render_install_warnings(self) -> str:
warnings = self.get_install_warnings()
if not warnings:
return ''
return '\n' + '\n'.join(f'[yellow]{w}[/]' for w in warnings) + '\n'
def _is_valid_path(self, dest_path: Path) -> bool: def _is_valid_path(self, dest_path: Path) -> bool:
dest_path_ok = dest_path.exists() and dest_path.is_dir() dest_path_ok = dest_path.exists() and dest_path.is_dir()
if not dest_path_ok: if not dest_path_ok:

View File

@ -6,7 +6,7 @@ from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
from archinstall.lib.log import debug from archinstall.lib.output import debug
libcrypt = ctypes.CDLL('libcrypt.so') libcrypt = ctypes.CDLL('libcrypt.so')

View File

@ -15,7 +15,6 @@ from archinstall.lib.disk.utils import (
umount, umount,
) )
from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from archinstall.lib.log import debug, error, info, log
from archinstall.lib.models.device import ( from archinstall.lib.models.device import (
DEFAULT_ITER_TIME, DEFAULT_ITER_TIME,
BDevice, BDevice,
@ -36,6 +35,7 @@ from archinstall.lib.models.device import (
_PartitionInfo, _PartitionInfo,
) )
from archinstall.lib.models.users import Password from archinstall.lib.models.users import Password
from archinstall.lib.output import debug, error, info, log
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
@ -250,7 +250,7 @@ class DeviceHandler:
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4: case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
# Force create # Force create
options.append('-F') options.append('-F')
case _ if fs_type.is_fat(): case FilesystemType.FAT12 | FilesystemType.FAT16 | FilesystemType.FAT32:
mkfs_type = 'fat' mkfs_type = 'fat'
# Set FAT size # Set FAT size
options.extend(('-F', fs_type.value.removeprefix(mkfs_type))) options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))

View File

@ -5,7 +5,6 @@ from typing import override
from archinstall.lib.disk.device_handler import device_handler from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
from archinstall.lib.disk.partitioning_menu import manual_partitioning from archinstall.lib.disk.partitioning_menu import manual_partitioning
from archinstall.lib.log import debug
from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Notify, Selection, Table from archinstall.lib.menu.helpers import Confirmation, Notify, Selection, Table
from archinstall.lib.menu.util import prompt_dir from archinstall.lib.menu.util import prompt_dir
@ -36,10 +35,10 @@ from archinstall.lib.models.device import (
Unit, Unit,
_DeviceInfo, _DeviceInfo,
) )
from archinstall.lib.output import FormattedOutput, debug
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType
from archinstall.tui.result import ResultType
@dataclass @dataclass
@ -222,7 +221,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
for mod in device_mods: for mod in device_mods:
# create partition table # create partition table
partition_table = as_table(mod.partitions) partition_table = FormattedOutput.as_table(mod.partitions)
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe) output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
@ -231,7 +230,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
# create btrfs table # create btrfs table
btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols] btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols]
for partition in btrfs_partitions: for partition in btrfs_partitions:
output_btrfs += as_table(partition.btrfs_subvols) + '\n' output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
output = output_partition + output_btrfs output = output_partition + output_btrfs
return output.rstrip() return output.rstrip()
@ -247,12 +246,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg()) output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg())
for vol_gp in lvm_config.vol_groups: for vol_gp in lvm_config.vol_groups:
pv_table = as_table(vol_gp.pvs) pv_table = FormattedOutput.as_table(vol_gp.pvs)
output += '{}:\n{}'.format(tr('Physical volumes'), pv_table) output += '{}:\n{}'.format(tr('Physical volumes'), pv_table)
output += f'\nVolume Group: {vol_gp.name}' output += f'\nVolume Group: {vol_gp.name}'
lvm_volumes = as_table(vol_gp.volumes) lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes) output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes)
return output return output
@ -281,7 +280,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
if enc_config.encryption_password: if enc_config.encryption_password:
output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n' output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n'
if enc_type != EncryptionType.NO_ENCRYPTION: if enc_type != EncryptionType.NoEncryption:
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n' output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
if enc_config.partitions: if enc_config.partitions:
@ -303,7 +302,7 @@ async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | N
dev = device_handler.get_device(device.path) dev = device_handler.get_device(device.path)
if dev and dev.partition_infos: if dev and dev.partition_infos:
return as_table(dev.partition_infos) return FormattedOutput.as_table(dev.partition_infos)
return None return None
if preset is None: if preset is None:
@ -504,7 +503,7 @@ def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModifi
# boot partition # boot partition
return PartitionModification( return PartitionModification(
status=ModificationStatus.CREATE, status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY, type=PartitionType.Primary,
start=start, start=start,
length=size, length=size,
mountpoint=Path('/boot'), mountpoint=Path('/boot'),
@ -656,7 +655,7 @@ async def suggest_single_disk_layout(
root_partition = PartitionModification( root_partition = PartitionModification(
status=ModificationStatus.CREATE, status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY, type=PartitionType.Primary,
start=root_start, start=root_start,
length=root_length, length=root_length,
mountpoint=Path('/') if not using_subvolumes else None, mountpoint=Path('/') if not using_subvolumes else None,
@ -681,7 +680,7 @@ async def suggest_single_disk_layout(
home_partition = PartitionModification( home_partition = PartitionModification(
status=ModificationStatus.CREATE, status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY, type=PartitionType.Primary,
start=home_start, start=home_start,
length=home_length, length=home_length,
mountpoint=Path('/home'), mountpoint=Path('/home'),
@ -766,7 +765,7 @@ async def suggest_multi_disk_layout(
# add root partition to the root device # add root partition to the root device
root_partition = PartitionModification( root_partition = PartitionModification(
status=ModificationStatus.CREATE, status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY, type=PartitionType.Primary,
start=root_start, start=root_start,
length=root_length, length=root_length,
mountpoint=Path('/'), mountpoint=Path('/'),
@ -788,7 +787,7 @@ async def suggest_multi_disk_layout(
# add home partition to home device # add home partition to home device
home_partition = PartitionModification( home_partition = PartitionModification(
status=ModificationStatus.CREATE, status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY, type=PartitionType.Primary,
start=home_start, start=home_start,
length=home_length, length=home_length,
mountpoint=Path('/home'), mountpoint=Path('/home'),

View File

@ -17,10 +17,10 @@ from archinstall.lib.models.device import (
PartitionModification, PartitionModification,
) )
from archinstall.lib.models.users import Password from archinstall.lib.models.users import Password
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType
from archinstall.tui.result import ResultType
class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]): class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
@ -105,19 +105,19 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _check_dep_enc_type(self) -> bool: def _check_dep_enc_type(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type != EncryptionType.NO_ENCRYPTION: if enc_type and enc_type != EncryptionType.NoEncryption:
return True return True
return False return False
def _check_dep_partitions(self) -> bool: def _check_dep_partitions(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS]: if enc_type and enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks]:
return True return True
return False return False
def _check_dep_lvm_vols(self) -> bool: def _check_dep_lvm_vols(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type == EncryptionType.LUKS_ON_LVM: if enc_type and enc_type == EncryptionType.LuksOnLvm:
return True return True
return False return False
@ -137,13 +137,13 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
assert enc_partitions is not None assert enc_partitions is not None
assert enc_lvm_vols is not None assert enc_lvm_vols is not None
if enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and enc_partitions: if enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and enc_partitions:
enc_lvm_vols = [] enc_lvm_vols = []
if enc_type == EncryptionType.LUKS_ON_LVM: if enc_type == EncryptionType.LuksOnLvm:
enc_partitions = [] enc_partitions = []
if enc_type != EncryptionType.NO_ENCRYPTION and enc_password and (enc_partitions or enc_lvm_vols): if enc_type != EncryptionType.NoEncryption and enc_password and (enc_partitions or enc_lvm_vols):
return DiskEncryption( return DiskEncryption(
encryption_password=enc_password, encryption_password=enc_password,
encryption_type=enc_type, encryption_type=enc_type,
@ -199,7 +199,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _prev_partitions(self, item: MenuItem) -> str | None: def _prev_partitions(self, item: MenuItem) -> str | None:
if item.value: if item.value:
output = tr('Partitions to be encrypted') + '\n' output = tr('Partitions to be encrypted') + '\n'
output += as_table(item.value) output += FormattedOutput.as_table(item.value)
return output.rstrip() return output.rstrip()
return None return None
@ -207,7 +207,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _prev_lvm_vols(self, item: MenuItem) -> str | None: def _prev_lvm_vols(self, item: MenuItem) -> str | None:
if item.value: if item.value:
output = tr('LVM volumes to be encrypted') + '\n' output = tr('LVM volumes to be encrypted') + '\n'
output += as_table(item.value) output += FormattedOutput.as_table(item.value)
return output.rstrip() return output.rstrip()
return None return None
@ -227,7 +227,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
iter_time = item.value iter_time = item.value
enc_type = self._item_group.find_by_key('encryption_type').value enc_type = self._item_group.find_by_key('encryption_type').value
if iter_time and enc_type != EncryptionType.NO_ENCRYPTION: if iter_time and enc_type != EncryptionType.NoEncryption:
return f'{tr("Iteration time")}: {iter_time}ms' return f'{tr("Iteration time")}: {iter_time}ms'
return None return None
@ -240,9 +240,9 @@ async def select_encryption_type(
options: list[EncryptionType] = [] options: list[EncryptionType] = []
if lvm_config: if lvm_config:
options = [EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM] options = [EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm]
else: else:
options = [EncryptionType.LUKS] options = [EncryptionType.Luks]
if not preset: if not preset:
preset = options[0] preset = options[0]
@ -321,7 +321,7 @@ async def select_partitions_to_encrypt(
avail_partitions = [p for p in partitions if not p.exists()] avail_partitions = [p for p in partitions if not p.exists()]
if avail_partitions: if avail_partitions:
group = MenuItemGroup.from_objects(avail_partitions) group = MenuItemGroup.from_objects(partitions)
group.set_selected_by_value(preset) group.set_selected_by_value(preset)
result = await Table[PartitionModification]( result = await Table[PartitionModification](
@ -375,7 +375,7 @@ async def select_lvm_vols_to_encrypt(
async def select_iteration_time(preset: int | None = None) -> int | None: async def select_iteration_time(preset: int | None = None) -> int | None:
header = tr('Enter iteration time for LUKS encryption (in milliseconds)') + '\n' header = tr('Enter iteration time for LUKS encryption (in milliseconds)') + '\n'
header += tr('Higher values increase security but slow down boot time') + '\n' header += tr('Higher values increase security but slow down boot time') + '\n'
header += tr('Default: {}ms, Recommended range: 1000-60000').format(DEFAULT_ITER_TIME) + '\n' header += tr(f'Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000') + '\n'
def validate_iter_time(value: str) -> str | None: def validate_iter_time(value: str) -> str | None:
try: try:

View File

@ -4,9 +4,9 @@ from typing import ClassVar
from archinstall.lib.command import SysCommand, SysCommandWorker from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.exceptions import SysCallError from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import error, info
from archinstall.lib.models.device import Fido2Device from archinstall.lib.models.device import Fido2Device
from archinstall.lib.models.users import Password from archinstall.lib.models.users import Password
from archinstall.lib.output import error, info
from archinstall.lib.utils.encoding import clear_vt100_escape_codes_from_str from archinstall.lib.utils.encoding import clear_vt100_escape_codes_from_str

View File

@ -13,7 +13,6 @@ from archinstall.lib.disk.lvm import (
lvm_vol_reduce, lvm_vol_reduce,
) )
from archinstall.lib.disk.utils import udev_sync from archinstall.lib.disk.utils import udev_sync
from archinstall.lib.log import debug, info
from archinstall.lib.models.device import ( from archinstall.lib.models.device import (
DiskEncryption, DiskEncryption,
DiskLayoutConfiguration, DiskLayoutConfiguration,
@ -28,6 +27,7 @@ from archinstall.lib.models.device import (
Size, Size,
Unit, Unit,
) )
from archinstall.lib.output import debug, info
class FilesystemHandler: class FilesystemHandler:
@ -139,7 +139,7 @@ class FilesystemHandler:
self._format_lvm_vols(self._disk_config.lvm_config) self._format_lvm_vols(self._disk_config.lvm_config)
def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None: def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None:
if enc_config.encryption_type == EncryptionType.LVM_ON_LUKS: if enc_config.encryption_type == EncryptionType.LvmOnLuks:
enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False) enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False)
self._setup_lvm(lvm_config, enc_mods) self._setup_lvm(lvm_config, enc_mods)
@ -148,7 +148,7 @@ class FilesystemHandler:
# Don't close LVM or LUKS during setup - keep everything active # Don't close LVM or LUKS during setup - keep everything active
# The installation phase will handle unlocking and mounting # The installation phase will handle unlocking and mounting
# Closing causes "parent leaked" and lvchange errors # Closing causes "parent leaked" and lvchange errors
elif enc_config.encryption_type == EncryptionType.LUKS_ON_LVM: elif enc_config.encryption_type == EncryptionType.LuksOnLvm:
self._setup_lvm(lvm_config) self._setup_lvm(lvm_config)
enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False) enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False)
self._format_lvm_vols(lvm_config, enc_vols) self._format_lvm_vols(lvm_config, enc_vols)

View File

@ -7,9 +7,9 @@ from types import TracebackType
from archinstall.lib.command import SysCommand, SysCommandWorker, run from archinstall.lib.command import SysCommand, SysCommandWorker, run
from archinstall.lib.disk.utils import get_lsblk_info, umount from archinstall.lib.disk.utils import get_lsblk_info, umount
from archinstall.lib.exceptions import DiskError, SysCallError from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info
from archinstall.lib.models.device import DEFAULT_ITER_TIME from archinstall.lib.models.device import DEFAULT_ITER_TIME
from archinstall.lib.models.users import Password from archinstall.lib.models.users import Password
from archinstall.lib.output import debug, info
from archinstall.lib.utils.util import generate_password from archinstall.lib.utils.util import generate_password

View File

@ -7,7 +7,6 @@ from typing import Literal, overload
from archinstall.lib.command import SysCommand, SysCommandWorker from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.disk.utils import udev_sync from archinstall.lib.disk.utils import udev_sync
from archinstall.lib.exceptions import SysCallError from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.device import ( from archinstall.lib.models.device import (
LvmGroupInfo, LvmGroupInfo,
LvmPVInfo, LvmPVInfo,
@ -18,6 +17,7 @@ from archinstall.lib.models.device import (
Size, Size,
Unit, Unit,
) )
from archinstall.lib.output import debug
def _lvm_info( def _lvm_info(

View File

@ -19,10 +19,10 @@ from archinstall.lib.models.device import (
Size, Size,
Unit, Unit,
) )
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType
from archinstall.tui.result import ResultType
class FreeSpace: class FreeSpace:
@ -58,7 +58,7 @@ class DiskSegment:
part_mod = PartitionModification( part_mod = PartitionModification(
status=ModificationStatus.CREATE, status=ModificationStatus.CREATE,
type=PartitionType._UNKNOWN, type=PartitionType._Unknown,
start=self.segment.start, start=self.segment.start,
length=self.segment.length, length=self.segment.length,
) )
@ -479,7 +479,7 @@ class PartitioningList(ListManager[DiskSegment]):
sector_size = device_info.sector_size sector_size = device_info.sector_size
text = tr('Selected free space segment on device {}:').format(device_info.path) + '\n\n' text = tr('Selected free space segment on device {}:').format(device_info.path) + '\n\n'
free_space_table = as_table([free_space]) free_space_table = FormattedOutput.as_table([free_space])
prompt = text + free_space_table + '\n' prompt = text + free_space_table + '\n'
max_sectors = free_space.length.format_size(Unit.sectors, sector_size) max_sectors = free_space.length.format_size(Unit.sectors, sector_size)
@ -527,7 +527,7 @@ class PartitioningList(ListManager[DiskSegment]):
partition = PartitionModification( partition = PartitionModification(
status=ModificationStatus.CREATE, status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY, type=PartitionType.Primary,
start=free_space.start, start=free_space.start,
length=length, length=length,
fs_type=fs_type, fs_type=fs_type,

View File

@ -6,7 +6,7 @@ from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import prompt_dir from archinstall.lib.menu.util import prompt_dir
from archinstall.lib.models.device import SubvolumeModification from archinstall.lib.models.device import SubvolumeModification
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class SubvolumeMenu(ListManager[SubvolumeModification]): class SubvolumeMenu(ListManager[SubvolumeModification]):

View File

@ -4,8 +4,8 @@ from pydantic import BaseModel
from archinstall.lib.command import SysCommand from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import DiskError, SysCallError from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info, warn
from archinstall.lib.models.device import LsblkInfo from archinstall.lib.models.device import LsblkInfo
from archinstall.lib.output import debug, info, warn
class LsblkOutput(BaseModel): class LsblkOutput(BaseModel):

View File

@ -1,11 +1,11 @@
from enum import Enum from enum import Enum
from archinstall.lib.locale.utils import list_timezones from archinstall.lib.locale.utils import list_timezones
from archinstall.lib.log import warn
from archinstall.lib.menu.helpers import Confirmation, Input, Selection from archinstall.lib.menu.helpers import Confirmation, Input, Selection
from archinstall.lib.output import warn
from archinstall.lib.translationhandler import Language, tr from archinstall.lib.translationhandler import Language, tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class PostInstallationAction(Enum): class PostInstallationAction(Enum):
@ -105,9 +105,9 @@ async def select_archinstall_language(languages: list[Language], preset: Languag
group = MenuItemGroup(items, sort_items=True) group = MenuItemGroup(items, sort_items=True)
group.set_focus_by_value(preset) group.set_focus_by_value(preset)
title = 'NOTE: Console font will be set automatically for supported languages.\n' title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n'
title += 'For other languages, fonts can be found in "/usr/share/kbd/consolefonts"\n' title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n'
title += 'and set manually with: setfont <fontname>\n' title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n'
result = await Selection[Language]( result = await Selection[Language](
header=title, header=title,

View File

@ -3,24 +3,29 @@ from typing import assert_never
from archinstall.lib.hardware import GfxDriver, SysInfo from archinstall.lib.hardware import GfxDriver, SysInfo
from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.application import ZramAlgorithm, ZramConfiguration from archinstall.lib.models.application import ZramAlgorithm, ZramConfiguration
from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
async def select_kernel(preset: list[Kernel] = []) -> list[Kernel]: async def select_kernel(preset: list[str] = []) -> list[str]:
""" """
Asks the user to select a kernel for system. Asks the user to select a kernel for system.
:return: The string as a selected kernel :return: The string as a selected kernel
:rtype: string :rtype: string
""" """
group = MenuItemGroup.from_enum(Kernel, sort_items=True, preset=preset) kernels = ['linux', 'linux-lts', 'linux-zen', 'linux-hardened']
group.set_default_by_value(DEFAULT_KERNEL) default_kernel = 'linux'
group.set_focus_by_value(DEFAULT_KERNEL)
result = await Selection[Kernel]( items = [MenuItem(k, value=k) for k in kernels]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default_kernel)
group.set_focus_by_value(default_kernel)
group.set_selected_by_value(preset)
result = await Selection[str](
group, group,
header=tr('Select which kernel(s) to install'), header=tr('Select which kernel(s) to install'),
allow_skip=True, allow_skip=True,

View File

@ -5,8 +5,7 @@ from archinstall.lib.applications.application_menu import ApplicationMenu
from archinstall.lib.args import ArchConfig from archinstall.lib.args import ArchConfig
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
from archinstall.lib.bootloader.utils import validate_bootloader_layout from archinstall.lib.configuration import save_config
from archinstall.lib.configuration import ConfigurationOutput, save_config
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone
from archinstall.lib.general.system_menu import select_kernel, select_swap from archinstall.lib.general.system_menu import select_kernel, select_swap
@ -18,22 +17,21 @@ from archinstall.lib.mirror.mirror_menu import MirrorMenu
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, PartitionModification from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, FilesystemType, PartitionModification
from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration
from archinstall.lib.models.network import NetworkConfiguration, NicType from archinstall.lib.models.network import NetworkConfiguration, NicType
from archinstall.lib.models.package_types import DEFAULT_KERNEL
from archinstall.lib.models.packages import Repository from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.network.network_menu import select_network from archinstall.lib.network.network_menu import select_network
from archinstall.lib.output import FormattedOutput
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
from archinstall.lib.pacman.config import PacmanConfig from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman_menu import PacmanMenu from archinstall.lib.pacman.pacman_menu import PacmanMenu
from archinstall.lib.translationhandler import Language, tr, translation_handler from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_table from archinstall.tui.ui.components import tui
from archinstall.tui.components import tui from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
class GlobalMenu(AbstractMenu[None]): class GlobalMenu(AbstractMenu[None]):
@ -104,7 +102,7 @@ class GlobalMenu(AbstractMenu[None]):
), ),
MenuItem( MenuItem(
text=tr('Kernels'), text=tr('Kernels'),
value=[DEFAULT_KERNEL], value=['linux'],
action=select_kernel, action=select_kernel,
preview_action=self._prev_kernel, preview_action=self._prev_kernel,
mandatory=True, mandatory=True,
@ -299,7 +297,7 @@ class GlobalMenu(AbstractMenu[None]):
if item.value: if item.value:
network_config: NetworkConfiguration = item.value network_config: NetworkConfiguration = item.value
if network_config.type == NicType.MANUAL: if network_config.type == NicType.MANUAL:
output = as_table(network_config.nics) output = FormattedOutput.as_table(network_config.nics)
else: else:
output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}' output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'
@ -321,7 +319,7 @@ class GlobalMenu(AbstractMenu[None]):
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n' output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
if auth_config.users: if auth_config.users:
output += as_table(auth_config.users) + '\n' output += FormattedOutput.as_table(auth_config.users) + '\n'
if auth_config.u2f_config: if auth_config.u2f_config:
u2f_config = auth_config.u2f_config u2f_config = auth_config.u2f_config
@ -462,6 +460,8 @@ class GlobalMenu(AbstractMenu[None]):
if not bootloader_config or bootloader_config.bootloader == Bootloader.NO_BOOTLOADER: if not bootloader_config or bootloader_config.bootloader == Bootloader.NO_BOOTLOADER:
return None return None
bootloader = bootloader_config.bootloader
if disk_config := self._item_group.find_by_key('disk_config').value: if disk_config := self._item_group.find_by_key('disk_config').value:
for layout in disk_config.device_modifications: for layout in disk_config.device_modifications:
if root_partition := layout.get_root_partition(): if root_partition := layout.get_root_partition():
@ -486,11 +486,16 @@ class GlobalMenu(AbstractMenu[None]):
if efi_partition is None: if efi_partition is None:
return 'EFI system partition (ESP) not found' return 'EFI system partition (ESP) not found'
if efi_partition.fs_type is None or not efi_partition.fs_type.is_fat(): if efi_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
return 'ESP must be formatted as a FAT filesystem' return 'ESP must be formatted as a FAT filesystem'
if failure := validate_bootloader_layout(bootloader_config, disk_config): if bootloader == Bootloader.Limine:
return failure.description if boot_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
return 'Limine does not support booting with a non-FAT boot partition'
elif bootloader == Bootloader.Refind:
if not self._uefi:
return 'rEFInd can only be used on UEFI systems'
return None return None
@ -502,13 +507,9 @@ class GlobalMenu(AbstractMenu[None]):
return text[:-1] # remove last new line return text[:-1] # remove last new line
if error := self._validate_bootloader(): if error := self._validate_bootloader():
return tr('Invalid configuration: {}').format(error) return tr(f'Invalid configuration: {error}')
self.sync_all_to_config() return None
summary = ConfigurationOutput(self._arch_config).as_summary()
if summary:
return f'{tr("Ready to install")}\n\n{summary}'
return tr('Ready to install')
def _prev_profile(self, item: MenuItem) -> str | None: def _prev_profile(self, item: MenuItem) -> str | None:
profile_config: ProfileConfiguration | None = item.value profile_config: ProfileConfiguration | None = item.value
@ -612,7 +613,7 @@ class GlobalMenu(AbstractMenu[None]):
if mirror_config.custom_repositories: if mirror_config.custom_repositories:
title = tr('Custom repositories') title = tr('Custom repositories')
table = as_table(mirror_config.custom_repositories) table = FormattedOutput.as_table(mirror_config.custom_repositories)
output += f'{title}:\n\n{table}' output += f'{title}:\n\n{table}'
return output.strip() return output.strip()

View File

@ -6,8 +6,8 @@ from typing import Self
from archinstall.lib.command import SysCommand from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.networking import enrich_iface_types, list_interfaces from archinstall.lib.networking import enrich_iface_types, list_interfaces
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -68,19 +68,6 @@ class GfxDriver(Enum):
case _: case _:
return False return False
def is_nvidia_proprietary(self) -> bool:
"""
True for Nvidia drivers that ship proprietary userspace components.
Currently only NvidiaOpenKernel (nvidia-open-dkms): open kernel module
paired with proprietary userspace. NvidiaOpenSource (nouveau) is fully
open and works with Sway, so it is excluded.
"""
match self:
case GfxDriver.NvidiaOpenKernel:
return True
case _:
return False
def packages_text(self) -> str: def packages_text(self) -> str:
pkg_names = [p.value for p in self.gfx_packages()] pkg_names = [p.value for p in self.gfx_packages()]
text = tr('Installed packages') + ':\n' text = tr('Installed packages') + ':\n'

View File

@ -12,7 +12,6 @@ from types import TracebackType
from typing import Any, Self from typing import Any, Self
from archinstall.lib.boot import Boot from archinstall.lib.boot import Boot
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.command import SysCommand, run from archinstall.lib.command import SysCommand, run
from archinstall.lib.disk.fido import Fido2 from archinstall.lib.disk.fido import Fido2
from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev
@ -29,10 +28,9 @@ from archinstall.lib.exceptions import DiskError, HardwareIncompatibilityError,
from archinstall.lib.hardware import SysInfo from archinstall.lib.hardware import SysInfo
from archinstall.lib.linux_path import LPath from archinstall.lib.linux_path import LPath
from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout
from archinstall.lib.log import debug, error, info, log, logger, warn
from archinstall.lib.mirror.mirror_handler import MirrorListHandler from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.models.application import ZramAlgorithm from archinstall.lib.models.application import ZramAlgorithm
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration from archinstall.lib.models.bootloader import Bootloader
from archinstall.lib.models.device import ( from archinstall.lib.models.device import (
DiskEncryption, DiskEncryption,
DiskLayoutConfiguration, DiskLayoutConfiguration,
@ -49,10 +47,10 @@ from archinstall.lib.models.device import (
from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration
from archinstall.lib.models.network import Nic from archinstall.lib.models.network import Nic
from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel
from archinstall.lib.models.packages import Repository from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.users import User from archinstall.lib.models.users import User
from archinstall.lib.output import debug, error, info, log, logger, warn
from archinstall.lib.packages.packages import installed_package from archinstall.lib.packages.packages import installed_package
from archinstall.lib.pacman.config import PacmanConfig from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.pacman.pacman import Pacman
@ -61,12 +59,7 @@ from archinstall.lib.plugins import plugins
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
# Any package that the Installer() is responsible for (optional and the default ones) # Any package that the Installer() is responsible for (optional and the default ones)
# https://github.com/archlinux/archinstall/issues/4368 __packages__ = ['base', 'sudo', 'linux-firmware', 'linux', 'linux-lts', 'linux-zen', 'linux-hardened']
# mkinitcpio is listed explicitly so pacstrap installs it deterministically. Otherwise
# pacman picks the first initramfs provider from the host's pacman.conf, which on non-Arch
# hosts (EndeavourOS prefers dracut, etc.) breaks the installer's mkinitcpio() and
# _config_uki() methods that assume mkinitcpio is present in the chroot.
__packages__ = ['base', 'sudo', 'linux-firmware', 'mkinitcpio'] + [k.value for k in Kernel]
# Additional packages that are installed if the user is running the Live ISO with accessibility tools enabled # Additional packages that are installed if the user is running the Live ISO with accessibility tools enabled
__accessibility_packages__ = ['brltty', 'espeakup', 'alsa-utils'] __accessibility_packages__ = ['brltty', 'espeakup', 'alsa-utils']
@ -85,11 +78,11 @@ class Installer:
`Installer()` is the wrapper for most basic installation steps. `Installer()` is the wrapper for most basic installation steps.
It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things. It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things.
""" """
self._base_packages = base_packages or __packages__[:4] self._base_packages = base_packages or __packages__[:3]
self.kernels = kernels or [DEFAULT_KERNEL.value] self.kernels = kernels or ['linux']
self._disk_config = disk_config self._disk_config = disk_config
self._disk_encryption = disk_config.disk_encryption or DiskEncryption(EncryptionType.NO_ENCRYPTION) self._disk_encryption = disk_config.disk_encryption or DiskEncryption(EncryptionType.NoEncryption)
self.target: Path = target self.target: Path = target
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
@ -189,10 +182,10 @@ class Installer:
if not skip_ntp: if not skip_ntp:
info(tr('Waiting for time sync (timedatectl show) to complete.')) info(tr('Waiting for time sync (timedatectl show) to complete.'))
started_wait = time.monotonic() started_wait = time.time()
notified = False notified = False
while True: while True:
if not notified and time.monotonic() - started_wait > 5: if not notified and time.time() - started_wait > 5:
notified = True notified = True
warn(tr('Time synchronization not completing, while you wait - check the docs for workarounds: https://archinstall.readthedocs.io/')) warn(tr('Time synchronization not completing, while you wait - check the docs for workarounds: https://archinstall.readthedocs.io/'))
@ -261,16 +254,16 @@ class Installer:
luks_handlers: dict[Any, Luks2] = {} luks_handlers: dict[Any, Luks2] = {}
match self._disk_encryption.encryption_type: match self._disk_encryption.encryption_type:
case EncryptionType.NO_ENCRYPTION: case EncryptionType.NoEncryption:
self._import_lvm() self._import_lvm()
self._mount_lvm_layout() self._mount_lvm_layout()
case EncryptionType.LUKS: case EncryptionType.Luks:
luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions) luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions)
case EncryptionType.LVM_ON_LUKS: case EncryptionType.LvmOnLuks:
luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions) luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions)
self._import_lvm() self._import_lvm()
self._mount_lvm_layout(luks_handlers) self._mount_lvm_layout(luks_handlers)
case EncryptionType.LUKS_ON_LVM: case EncryptionType.LuksOnLvm:
self._import_lvm() self._import_lvm()
luks_handlers = self._prepare_luks_lvm(self._disk_encryption.lvm_volumes) luks_handlers = self._prepare_luks_lvm(self._disk_encryption.lvm_volumes)
self._mount_lvm_layout(luks_handlers) self._mount_lvm_layout(luks_handlers)
@ -375,12 +368,7 @@ class Installer:
# it would be none if it's btrfs as the subvolumes will have the mountpoints defined # it would be none if it's btrfs as the subvolumes will have the mountpoints defined
if part_mod.mountpoint: if part_mod.mountpoint:
target = self.target / part_mod.relative_mountpoint target = self.target / part_mod.relative_mountpoint
options = part_mod.mount_options mount(part_mod.dev_path, target, options=part_mod.mount_options)
if part_mod.is_efi():
options = list(dict.fromkeys(options + ['fmask=0077', 'dmask=0077']))
mount(part_mod.dev_path, target, options=options)
elif part_mod.fs_type == FilesystemType.BTRFS: elif part_mod.fs_type == FilesystemType.BTRFS:
# Only mount BTRFS subvolumes that have mountpoints specified # Only mount BTRFS subvolumes that have mountpoints specified
subvols_with_mountpoints = [sv for sv in part_mod.btrfs_subvols if sv.mountpoint is not None] subvols_with_mountpoints = [sv for sv in part_mod.btrfs_subvols if sv.mountpoint is not None]
@ -407,7 +395,7 @@ class Installer:
def _mount_luks_partition(self, part_mod: PartitionModification, luks_handler: Luks2) -> None: def _mount_luks_partition(self, part_mod: PartitionModification, luks_handler: Luks2) -> None:
if not luks_handler.mapper_dev: if not luks_handler.mapper_dev:
return return None
if part_mod.fs_type == FilesystemType.BTRFS and part_mod.btrfs_subvols: if part_mod.fs_type == FilesystemType.BTRFS and part_mod.btrfs_subvols:
# Only mount BTRFS subvolumes that have mountpoints specified # Only mount BTRFS subvolumes that have mountpoints specified
@ -445,11 +433,11 @@ class Installer:
def generate_key_files(self) -> None: def generate_key_files(self) -> None:
match self._disk_encryption.encryption_type: match self._disk_encryption.encryption_type:
case EncryptionType.LUKS: case EncryptionType.Luks:
self._generate_key_files_partitions() self._generate_key_files_partitions()
case EncryptionType.LUKS_ON_LVM: case EncryptionType.LuksOnLvm:
self._generate_key_file_lvm_volumes() self._generate_key_file_lvm_volumes()
case EncryptionType.LVM_ON_LUKS: case EncryptionType.LvmOnLuks:
# currently LvmOnLuks only supports a single # currently LvmOnLuks only supports a single
# partitioning layout (boot + partition) # partitioning layout (boot + partition)
# so we won't need any keyfile generation atm # so we won't need any keyfile generation atm
@ -517,9 +505,10 @@ class Installer:
# Copy over the install log (if there is one) to the install medium if # Copy over the install log (if there is one) to the install medium if
# at least the base has been strapped in, otherwise we won't have a filesystem/structure to copy to. # at least the base has been strapped in, otherwise we won't have a filesystem/structure to copy to.
if self._helper_flags.get('base-strapped', False) is True: if self._helper_flags.get('base-strapped', False) is True:
logfile_target = self.target / LPath(logger.directory).relative_to_root() absolute_logfile = logger.path
logfile_target.mkdir(parents=True, exist_ok=True) logfile_target = self.target / absolute_logfile
logger.path.copy_into(logfile_target, preserve_metadata=True) logfile_target.parent.mkdir(parents=True, exist_ok=True)
absolute_logfile.copy(logfile_target, preserve_metadata=True)
return True return True
@ -750,9 +739,6 @@ class Installer:
return self.run_command(cmd, peek_output=peek_output) return self.run_command(cmd, peek_output=peek_output)
def _chroot_argv(self, *args: str) -> list[str]:
return ['arch-chroot', '-S', str(self.target), *args]
def drop_to_shell(self) -> None: def drop_to_shell(self) -> None:
subprocess.check_call(f'arch-chroot {self.target}', shell=True) subprocess.check_call(f'arch-chroot {self.target}', shell=True)
@ -913,7 +899,7 @@ class Installer:
if vol.fs_type is not None: if vol.fs_type is not None:
self._prepare_fs_type(vol.fs_type, vol.mountpoint) self._prepare_fs_type(vol.fs_type, vol.mountpoint)
types = (EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM) types = (EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm)
if self._disk_encryption.encryption_type in types: if self._disk_encryption.encryption_type in types:
self._prepare_encrypt(lvm) self._prepare_encrypt(lvm)
else: else:
@ -1001,7 +987,17 @@ class Installer:
} }
for config_name, mountpoint in snapper.items(): for config_name, mountpoint in snapper.items():
command = self._chroot_argv('snapper', '--no-dbus', '-c', config_name, 'create-config', mountpoint) command = [
'arch-chroot',
'-S',
str(self.target),
'snapper',
'--no-dbus',
'-c',
config_name,
'create-config',
mountpoint,
]
try: try:
SysCommand(command, peek_output=True) SysCommand(command, peek_output=True)
@ -1141,7 +1137,7 @@ class Installer:
kernel_parameters = [] kernel_parameters = []
match self._disk_encryption.encryption_type: match self._disk_encryption.encryption_type:
case EncryptionType.LVM_ON_LUKS: case EncryptionType.LvmOnLuks:
if not lvm.vg_name: if not lvm.vg_name:
raise ValueError(f'Unable to determine VG name for {lvm.name}') raise ValueError(f'Unable to determine VG name for {lvm.name}')
@ -1158,7 +1154,7 @@ class Installer:
else: else:
debug(f'LvmOnLuks, encrypted root partition, identifying by UUID: {uuid}') debug(f'LvmOnLuks, encrypted root partition, identifying by UUID: {uuid}')
kernel_parameters.append(f'cryptdevice=UUID={uuid}:cryptlvm root={lvm.safe_dev_path}') kernel_parameters.append(f'cryptdevice=UUID={uuid}:cryptlvm root={lvm.safe_dev_path}')
case EncryptionType.LUKS_ON_LVM: case EncryptionType.LuksOnLvm:
uuid = self._get_luks_uuid_from_mapper_dev(lvm.mapper_path) uuid = self._get_luks_uuid_from_mapper_dev(lvm.mapper_path)
if self._disk_encryption.hsm_device: if self._disk_encryption.hsm_device:
@ -1167,7 +1163,7 @@ class Installer:
else: else:
debug(f'LuksOnLvm, encrypted root partition, identifying by UUID: {uuid}') debug(f'LuksOnLvm, encrypted root partition, identifying by UUID: {uuid}')
kernel_parameters.append(f'cryptdevice=UUID={uuid}:root root=/dev/mapper/root') kernel_parameters.append(f'cryptdevice=UUID={uuid}:root root=/dev/mapper/root')
case EncryptionType.NO_ENCRYPTION: case EncryptionType.NoEncryption:
debug(f'Identifying root lvm by mapper device: {lvm.dev_path}') debug(f'Identifying root lvm by mapper device: {lvm.dev_path}')
kernel_parameters.append(f'root={lvm.safe_dev_path}') kernel_parameters.append(f'root={lvm.safe_dev_path}')
@ -1340,7 +1336,13 @@ class Installer:
boot_dir = Path('/boot') boot_dir = Path('/boot')
command = self._chroot_argv('grub-install', '--debug') command = [
'arch-chroot',
'-S',
str(self.target),
'grub-install',
'--debug',
]
if SysInfo.has_uefi(): if SysInfo.has_uefi():
if not efi_partition: if not efi_partition:
@ -1465,14 +1467,6 @@ class Installer:
elif not efi_partition.mountpoint: elif not efi_partition.mountpoint:
raise ValueError('EFI partition is not mounted') raise ValueError('EFI partition is not mounted')
# Safety net for programmatic callers that bypass GlobalMenu and
# guided.py validation.
if failure := validate_bootloader_layout(
BootloaderConfiguration(bootloader=Bootloader.Limine, uki=uki_enabled),
self._disk_config,
):
raise DiskError(failure.description)
info(f'Limine EFI partition: {efi_partition.dev_path}') info(f'Limine EFI partition: {efi_partition.dev_path}')
parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path) parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path)
@ -1483,16 +1477,20 @@ class Installer:
if bootloader_removable: if bootloader_removable:
efi_dir_path = efi_dir_path / 'BOOT' efi_dir_path = efi_dir_path / 'BOOT'
efi_dir_path_target = efi_dir_path_target / 'BOOT' efi_dir_path_target = efi_dir_path_target / 'BOOT'
boot_limine_path = self.target / 'boot' / 'limine'
boot_limine_path.mkdir(parents=True, exist_ok=True)
config_path = boot_limine_path / 'limine.conf'
else: else:
efi_dir_path = efi_dir_path / 'arch-limine' efi_dir_path = efi_dir_path / 'arch-limine'
efi_dir_path_target = efi_dir_path_target / 'arch-limine' efi_dir_path_target = efi_dir_path_target / 'arch-limine'
config_path = efi_dir_path / 'limine.conf' config_path = efi_dir_path / 'limine.conf'
efi_dir_path.mkdir(parents=True, exist_ok=True) efi_dir_path.mkdir(parents=True, exist_ok=True)
for file in ('BOOTIA32.EFI', 'BOOTX64.EFI'): for file in ('BOOTIA32.EFI', 'BOOTX64.EFI'):
(limine_path / file).copy_into(efi_dir_path) (limine_path / file).copy(efi_dir_path)
except Exception as err: except Exception as err:
raise DiskError(f'Failed to install Limine in {self.target}{efi_partition.mountpoint}: {err}') raise DiskError(f'Failed to install Limine in {self.target}{efi_partition.mountpoint}: {err}')
@ -1541,7 +1539,7 @@ class Installer:
try: try:
# The `limine-bios.sys` file contains stage 3 code. # The `limine-bios.sys` file contains stage 3 code.
(limine_path / 'limine-bios.sys').copy_into(boot_limine_path) (limine_path / 'limine-bios.sys').copy(boot_limine_path)
# `limine bios-install` deploys the stage 1 and 2 to the # `limine bios-install` deploys the stage 1 and 2 to the
self.arch_chroot(f'limine bios-install {parent_dev_path}', peek_output=True) self.arch_chroot(f'limine bios-install {parent_dev_path}', peek_output=True)
@ -1759,7 +1757,6 @@ class Installer:
self, self,
root: PartitionModification | LvmVolume, root: PartitionModification | LvmVolume,
efi_partition: PartitionModification | None, efi_partition: PartitionModification | None,
keep_initramfs: bool = False,
) -> None: ) -> None:
if not efi_partition or not efi_partition.mountpoint: if not efi_partition or not efi_partition.mountpoint:
raise ValueError(f'Could not detect ESP at mountpoint {self.target}') raise ValueError(f'Could not detect ESP at mountpoint {self.target}')
@ -1783,11 +1780,11 @@ class Installer:
config = preset.read_text().splitlines(True) config = preset.read_text().splitlines(True)
for index, line in enumerate(config): for index, line in enumerate(config):
# Avoid storing redundant image file
if m := image_re.match(line): if m := image_re.match(line):
if not keep_initramfs: image = self.target / m.group(2)
image = self.target / m.group(2) image.unlink(missing_ok=True)
image.unlink(missing_ok=True) config[index] = '#' + m.group(1)
config[index] = '#' + m.group(1)
elif m := uki_re.match(line): elif m := uki_re.match(line):
if diff_mountpoint: if diff_mountpoint:
config[index] = m.group(2) + diff_mountpoint + m.group(3) config[index] = m.group(2) + diff_mountpoint + m.group(3)
@ -1806,12 +1803,7 @@ class Installer:
if not self.mkinitcpio(['-P']): if not self.mkinitcpio(['-P']):
error('Error generating initramfs (continuing anyway)') error('Error generating initramfs (continuing anyway)')
def add_bootloader( def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False, bootloader_removable: bool = False) -> None:
self,
bootloader: Bootloader,
uki_enabled: bool = False,
bootloader_removable: bool = False,
) -> None:
""" """
Adds a bootloader to the installation instance. Adds a bootloader to the installation instance.
Archinstall supports one of five types: Archinstall supports one of five types:
@ -1860,13 +1852,7 @@ class Installer:
bootloader_removable = False bootloader_removable = False
if uki_enabled: if uki_enabled:
keep_initramfs = ( self._config_uki(root, efi_partition)
bootloader == Bootloader.Grub
and self._disk_config.has_default_btrfs_vols()
and self._disk_config.btrfs_options is not None
and self._disk_config.btrfs_options.snapshot_config is not None
)
self._config_uki(root, efi_partition, keep_initramfs)
match bootloader: match bootloader:
case Bootloader.Systemd: case Bootloader.Systemd:
@ -1932,17 +1918,16 @@ class Installer:
if not handled_by_plugin: if not handled_by_plugin:
info(f'Creating user {user.username}') info(f'Creating user {user.username}')
cmd = self._chroot_argv('useradd', '-m') cmd = 'useradd -m'
if user.sudo: if user.sudo:
cmd += ['-G', 'wheel'] cmd += ' -G wheel'
cmd += ['--', user.username] cmd += f' {user.username}'
try: try:
run(cmd) self.arch_chroot(cmd)
except CalledProcessError as err: except SysCallError as err:
debug(f'Error creating user {user.username}: {err}')
raise SystemError(f'Could not create user inside installation: {err}') raise SystemError(f'Could not create user inside installation: {err}')
for plugin in plugins.values(): for plugin in plugins.values():
@ -1953,11 +1938,7 @@ class Installer:
self.set_user_password(user) self.set_user_password(user)
for group in user.groups: for group in user.groups:
cmd = self._chroot_argv('gpasswd', '-a', user.username, group) self.arch_chroot(f'gpasswd -a {user.username} {group}')
try:
run(cmd)
except CalledProcessError as err:
warn(f'Failed to add {user.username} to group {group}: {err}')
if user.sudo: if user.sudo:
self.enable_sudo(user) self.enable_sudo(user)
@ -1972,7 +1953,7 @@ class Installer:
return False return False
input_data = f'{user.username}:{enc_password}'.encode() input_data = f'{user.username}:{enc_password}'.encode()
cmd = self._chroot_argv('chpasswd', '--encrypted') cmd = ['arch-chroot', '-S', str(self.target), 'chpasswd', '--encrypted']
try: try:
run(cmd, input_data=input_data) run(cmd, input_data=input_data)
@ -1984,31 +1965,29 @@ class Installer:
def user_set_shell(self, user: str, shell: str) -> bool: def user_set_shell(self, user: str, shell: str) -> bool:
info(f'Setting shell for {user} to {shell}') info(f'Setting shell for {user} to {shell}')
cmd = self._chroot_argv('chsh', '-s', shell, user)
try: try:
run(cmd) self.arch_chroot(f'sh -c "chsh -s {shell} {user}"')
return True return True
except CalledProcessError as err: except SysCallError:
debug(f'Error setting user shell: {err}')
return False return False
def chown(self, owner: str, path: str, options: list[str] | None = None) -> bool: def chown(self, owner: str, path: str, options: list[str] = []) -> bool:
options = options or [] cleaned_path = path.replace("'", "\\'")
cmd = self._chroot_argv('chown', *options, '--', owner, path)
try: try:
run(cmd) self.arch_chroot(f"sh -c 'chown {' '.join(options)} {owner} {cleaned_path}'")
return True return True
except CalledProcessError as err: except SysCallError:
debug(f'Error changing ownership of {path}: {err}')
return False return False
def set_vconsole(self, locale_config: LocaleConfiguration) -> None: def set_vconsole(self, locale_config: LocaleConfiguration) -> None:
# use the already set kb layout # use the already set kb layout
kb_vconsole: str = locale_config.kb_layout kb_vconsole: str = locale_config.kb_layout
font_vconsole = locale_config.console_font # this is the default used in ISO other option for hdpi screens TER16x32
# can be checked using
# zgrep "CONFIG_FONT" /proc/config.gz
# https://wiki.archlinux.org/title/Linux_console#Fonts
if font_vconsole.startswith('ter-'): font_vconsole = 'default8x16'
self.pacman.strap(['terminus-font'])
# Ensure /etc exists # Ensure /etc exists
vconsole_dir: Path = self.target / 'etc' vconsole_dir: Path = self.target / 'etc'

View File

@ -1,12 +1,12 @@
from typing import override from typing import override
from archinstall.lib.locale.utils import list_console_fonts, list_keyboard_languages, list_locales, set_kb_layout from archinstall.lib.locale.utils import list_keyboard_languages, list_locales, set_kb_layout
from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Selection from archinstall.lib.menu.helpers import Selection
from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class LocaleMenu(AbstractSubMenu[LocaleConfiguration]): class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
@ -47,13 +47,6 @@ class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
preview_action=lambda item: item.get_value(), preview_action=lambda item: item.get_value(),
key='sys_enc', key='sys_enc',
), ),
MenuItem(
text=tr('Console font'),
action=select_console_font,
value=self._locale_conf.console_font,
preview_action=lambda item: item.get_value(),
key='console_font',
),
] ]
@override @override
@ -147,25 +140,3 @@ async def select_kb_layout(preset: str | None = None) -> str | None:
return preset return preset
case _: case _:
raise ValueError('Unhandled return type') raise ValueError('Unhandled return type')
async def select_console_font(preset: str | None = None) -> str | None:
fonts = list_console_fonts()
items = [MenuItem(f, value=f) for f in fonts]
group = MenuItemGroup(items, sort_items=False)
group.set_focus_by_value(preset)
result = await Selection[str](
header=tr('Console font'),
group=group,
enable_filter=True,
).show()
match result.type_:
case ResultType.Selection:
return result.get_value()
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled return type')

View File

@ -1,9 +1,6 @@
from functools import lru_cache
from pathlib import Path
from archinstall.lib.command import SysCommand from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import ServiceException, SysCallError from archinstall.lib.exceptions import ServiceException, SysCallError
from archinstall.lib.log import error from archinstall.lib.output import error
from archinstall.lib.utils.util import running_from_iso from archinstall.lib.utils.util import running_from_iso
@ -29,13 +26,6 @@ def list_locales() -> list[str]:
return locales return locales
@lru_cache
def list_console_fonts() -> list[str]:
directory = Path('/usr/share/kbd/consolefonts')
fonts = {path.name.split('.')[0] for path in directory.glob('*.gz')}
return sorted(fonts)
def list_x11_keyboard_languages() -> list[str]: def list_x11_keyboard_languages() -> list[str]:
return ( return (
SysCommand( SysCommand(

View File

@ -1,259 +0,0 @@
import logging
import os
import sys
import urllib.error
import urllib.request
from enum import Enum
from pathlib import Path
from archinstall.lib.utils.util import timestamp
class Logger:
def __init__(self, path: Path | None = None) -> None:
if path is None:
path = Path('/var/log/archinstall')
self._path: Path = path
@property
def path(self) -> Path:
return self._path / 'install.log'
@path.setter
def path(self, value: Path) -> None:
self._path = value
@property
def directory(self) -> Path:
return self._path
def _check_permissions(self) -> None:
log_file = self.path
try:
self._path.mkdir(exist_ok=True, parents=True)
log_file.touch(exist_ok=True)
with log_file.open('a') as f:
f.write('')
except PermissionError:
# Fallback to creating the log file in the current folder
logger._path = Path('./').absolute()
warn(f'Not enough permission to place log file at {log_file}, creating it in {logger.path} instead')
def log(self, level: int, content: str) -> None:
self._check_permissions()
with self.path.open('a') as f:
ts = timestamp()
level_name = logging.getLevelName(level)
f.write(f'[{ts}] - {level_name} - {content}\n')
def get_content(self, max_bytes: int | None = None) -> bytes:
content = self.path.read_bytes()
if max_bytes is not None:
size = self.path.stat().st_size
if size > max_bytes:
content = content[-max_bytes:]
return content
logger = Logger()
def _supports_color() -> bool:
"""
Found first reference here:
https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python
And re-used this:
https://github.com/django/django/blob/master/django/core/management/color.py#L12
Return True if the running system's terminal supports color,
and False otherwise.
"""
supported_platform = sys.platform != 'win32' or 'ANSICON' in os.environ
# isatty is not always implemented, #6223.
is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
return supported_platform and is_a_tty
class Font(Enum):
bold = '1'
italic = '3'
underscore = '4'
blink = '5'
reverse = '7'
conceal = '8'
def _stylize_output(
text: str,
fg: str,
bg: str | None,
reset: bool,
font: list[Font] = [],
) -> str:
"""
Heavily influenced by:
https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13
Color options here:
https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i
Adds styling to a text given a set of color arguments.
"""
colors = {
'black': '0',
'red': '1',
'green': '2',
'yellow': '3',
'blue': '4',
'magenta': '5',
'cyan': '6',
'white': '7',
'teal': '8;5;109', # Extended 256-bit colors (not always supported)
'orange': '8;5;208', # https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html#256-colors
'darkorange': '8;5;202',
'gray': '8;5;246',
'grey': '8;5;246',
'darkgray': '8;5;240',
'lightgray': '8;5;256',
}
foreground = {key: f'3{colors[key]}' for key in colors}
background = {key: f'4{colors[key]}' for key in colors}
code_list = []
if text == '' and reset:
return '\x1b[0m'
code_list.append(foreground[str(fg)])
if bg:
code_list.append(background[str(bg)])
for o in font:
code_list.append(o.value)
ansi = ';'.join(code_list)
return f'\033[{ansi}m{text}\033[0m'
def journal_log(message: str, level: int = logging.DEBUG) -> None:
try:
import systemd.journal # type: ignore[import-not-found]
except ModuleNotFoundError:
return
log_adapter = logging.getLogger('archinstall')
log_fmt = logging.Formatter('[%(levelname)s]: %(message)s')
log_ch = systemd.journal.JournalHandler()
log_ch.setFormatter(log_fmt)
log_adapter.addHandler(log_ch)
log_adapter.setLevel(logging.DEBUG)
log_adapter.log(level, message)
def info(
*msgs: str,
level: int = logging.INFO,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def debug(
*msgs: str,
level: int = logging.DEBUG,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def error(
*msgs: str,
level: int = logging.ERROR,
fg: str = 'red',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def warn(
*msgs: str,
level: int = logging.WARNING,
fg: str = 'yellow',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def log(
*msgs: str,
level: int = logging.INFO,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
text = ' '.join(str(x) for x in msgs)
logger.log(level, text)
# Attempt to colorize the output if supported
# Insert default colors and override with **kwargs
if _supports_color():
text = _stylize_output(text, fg, bg, reset, font)
journal_log(text, level=level)
if level != logging.DEBUG:
print(text)
def share_install_log(
paste_url: str,
max_bytes: int | None = None,
) -> str | None:
log_path = logger.path
if not log_path.exists():
info(f'Log file not found: {log_path}')
return None
content = logger.get_content(max_bytes=max_bytes)
if len(content) == 0:
info(f'Log file is empty: {log_path}')
return None
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 None
if not url.startswith('http'):
info(f'Unexpected response from {paste_url}: {url[:200]!r}')
return None
return url

View File

@ -2,12 +2,13 @@ from enum import Enum
from types import TracebackType from types import TracebackType
from typing import Any, Self, override from typing import Any, Self, override
from archinstall.lib.log import error
from archinstall.lib.menu.helpers import Selection from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import error
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.components import InstanceRunnable from archinstall.tui.types import Chars
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.components import InstanceRunnable
from archinstall.tui.result import ResultType from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
CONFIG_KEY = '__config__' CONFIG_KEY = '__config__'
@ -153,7 +154,7 @@ class AbstractSubMenu[ValueT](AbstractMenu[ValueT]):
auto_cursor: bool = True, auto_cursor: bool = True,
allow_reset: bool = False, allow_reset: bool = False,
): ):
back_text = ' ' + tr('Back') back_text = f'{Chars.Right_arrow} ' + tr('Back')
item_group.add_item(MenuItem(text=back_text)) item_group.add_item(MenuItem(text=back_text))
super().__init__( super().__init__(

View File

@ -4,9 +4,9 @@ from typing import Any, Literal, override
from textual.validation import ValidationResult, Validator from textual.validation import ValidationResult, Validator
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.components import InputInfo, InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen from archinstall.tui.ui.components import InputInfo, InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen
from archinstall.tui.menu_item import MenuItemGroup from archinstall.tui.ui.menu_item import MenuItemGroup
from archinstall.tui.result import Result, ResultType from archinstall.tui.ui.result import Result, ResultType
class Selection[ValueT]: class Selection[ValueT]:
@ -20,7 +20,6 @@ class Selection[ValueT]:
preview_location: Literal['right', 'bottom'] | None = None, preview_location: Literal['right', 'bottom'] | None = None,
multi: bool = False, multi: bool = False,
enable_filter: bool = False, enable_filter: bool = False,
wrap_preview: bool = False,
): ):
self._header = header self._header = header
self._title = title self._title = title
@ -30,7 +29,6 @@ class Selection[ValueT]:
self._preview_location = preview_location self._preview_location = preview_location
self._multi = multi self._multi = multi
self._enable_filter = enable_filter self._enable_filter = enable_filter
self._wrap_preview = wrap_preview
async def show(self) -> Result[ValueT]: async def show(self) -> Result[ValueT]:
if self._multi: if self._multi:
@ -41,7 +39,6 @@ class Selection[ValueT]:
allow_reset=self._allow_reset, allow_reset=self._allow_reset,
preview_location=self._preview_location, preview_location=self._preview_location,
enable_filter=self._enable_filter, enable_filter=self._enable_filter,
wrap_preview=self._wrap_preview,
).run() ).run()
else: else:
result = await OptionListScreen[ValueT]( result = await OptionListScreen[ValueT](
@ -52,7 +49,6 @@ class Selection[ValueT]:
allow_reset=self._allow_reset, allow_reset=self._allow_reset,
preview_location=self._preview_location, preview_location=self._preview_location,
enable_filter=self._enable_filter, enable_filter=self._enable_filter,
wrap_preview=self._wrap_preview,
).run() ).run()
if result.type_ == ResultType.Reset: if result.type_ == ResultType.Reset:

View File

@ -4,8 +4,8 @@ from typing import cast
from archinstall.lib.menu.helpers import Selection from archinstall.lib.menu.helpers import Selection
from archinstall.lib.menu.menu_helper import MenuHelper from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class ListManager[ValueT]: class ListManager[ValueT]:

View File

@ -1,5 +1,5 @@
from archinstall.lib.utils.format import as_table from archinstall.lib.output import FormattedOutput
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
class MenuHelper[ValueT]: class MenuHelper[ValueT]:
@ -32,7 +32,7 @@ class MenuHelper[ValueT]:
display_data: dict[str, ValueT | str | None] = {} display_data: dict[str, ValueT | str | None] = {}
if data: if data:
table = as_table(data) table = FormattedOutput.as_table(data)
rows = table.split('\n') rows = table.split('\n')
# these are the header rows of the table # these are the header rows of the table

View File

@ -5,8 +5,8 @@ from pathlib import Path
from archinstall.lib.menu.helpers import Confirmation, Input from archinstall.lib.menu.helpers import Confirmation, Input
from archinstall.lib.models.users import Password, PasswordStrength from archinstall.lib.models.users import Password, PasswordStrength
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.components import InputInfo, InputInfoType, tui from archinstall.tui.ui.components import InputInfo, InputInfoType, tui
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
async def get_password( async def get_password(

View File

@ -2,10 +2,10 @@ import time
import urllib.parse import urllib.parse
from pathlib import Path from pathlib import Path
from archinstall.lib.log import debug, info
from archinstall.lib.models import MirrorRegion from archinstall.lib.models import MirrorRegion
from archinstall.lib.models.mirrors import MirrorStatusEntryV3, MirrorStatusListV3 from archinstall.lib.models.mirrors import MirrorStatusEntryV3, MirrorStatusListV3
from archinstall.lib.networking import fetch_data_from_url from archinstall.lib.networking import fetch_data_from_url
from archinstall.lib.output import debug, info
from archinstall.lib.pathnames import MIRRORLIST from archinstall.lib.pathnames import MIRRORLIST

View File

@ -13,10 +13,10 @@ from archinstall.lib.models.mirrors import (
SignOption, SignOption,
) )
from archinstall.lib.models.packages import Repository from archinstall.lib.models.packages import Repository
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType
from archinstall.tui.result import ResultType
class CustomMirrorRepositoriesList(ListManager[CustomRepository]): class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
@ -65,7 +65,7 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
async def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None: async def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None:
edit_result = await Input( edit_result = await Input(
header=tr('Enter a repository name'), header=tr('Enter a respository name'),
allow_skip=True, allow_skip=True,
default_value=preset.name if preset else None, default_value=preset.name if preset else None,
).show() ).show()
@ -281,7 +281,7 @@ class MirrorMenu(AbstractSubMenu[MirrorConfiguration]):
return None return None
custom_mirrors: list[CustomRepository] = item.value custom_mirrors: list[CustomRepository] = item.value
output = as_table(custom_mirrors) output = FormattedOutput.as_table(custom_mirrors)
return output.strip() return output.strip()
def _prev_custom_servers(self, item: MenuItem) -> str | None: def _prev_custom_servers(self, item: MenuItem) -> str | None:

View File

@ -1,9 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum, auto from enum import StrEnum, auto
from typing import Any, NotRequired, Self, TypedDict, override from typing import Any, NotRequired, Self, TypedDict
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
class PowerManagement(StrEnum): class PowerManagement(StrEnum):
@ -42,31 +39,6 @@ class FirewallConfigSerialization(TypedDict):
firewall: str firewall: str
class FontPackage(StrEnum):
NOTO = 'noto-fonts'
EMOJI = 'noto-fonts-emoji'
CJK = 'noto-fonts-cjk'
LIBERATION = 'ttf-liberation'
DEJAVU = 'ttf-dejavu'
def description(self) -> str:
match self:
case FontPackage.NOTO:
return tr('Unicode font coverage for most languages')
case FontPackage.EMOJI:
return tr('color emoji for browsers and apps')
case FontPackage.CJK:
return tr('Chinese, Japanese, Korean characters')
case FontPackage.LIBERATION:
return tr('Arial/Times/Courier replacement, Cyrillic support for Steam/games')
case FontPackage.DEJAVU:
return tr('wide Unicode coverage, good fallback font')
class FontsConfigSerialization(TypedDict):
fonts: list[str]
class ZramAlgorithm(StrEnum): class ZramAlgorithm(StrEnum):
ZSTD = auto() ZSTD = auto()
LZO_RLE = 'lzo-rle' LZO_RLE = 'lzo-rle'
@ -81,7 +53,6 @@ class ApplicationSerialization(TypedDict):
power_management_config: NotRequired[PowerManagementConfigSerialization] power_management_config: NotRequired[PowerManagementConfigSerialization]
print_service_config: NotRequired[PrintServiceConfigSerialization] print_service_config: NotRequired[PrintServiceConfigSerialization]
firewall_config: NotRequired[FirewallConfigSerialization] firewall_config: NotRequired[FirewallConfigSerialization]
fonts_config: NotRequired[FontsConfigSerialization]
@dataclass @dataclass
@ -156,20 +127,8 @@ class FirewallConfiguration:
) )
@dataclass
class FontsConfiguration:
fonts: list[FontPackage]
def json(self) -> FontsConfigSerialization:
return {'fonts': [f.value for f in self.fonts]}
@classmethod
def parse_arg(cls, arg: FontsConfigSerialization) -> Self:
return cls(fonts=[FontPackage(f) for f in arg['fonts']])
@dataclass(frozen=True) @dataclass(frozen=True)
class ZramConfiguration(SubConfig): class ZramConfiguration:
enabled: bool enabled: bool
algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD
@ -182,34 +141,14 @@ class ZramConfiguration(SubConfig):
algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value)) algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value))
return cls(enabled=enabled, algorithm=ZramAlgorithm(algo)) 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 @dataclass
class ApplicationConfiguration(SubConfig): class ApplicationConfiguration:
bluetooth_config: BluetoothConfiguration | None = None bluetooth_config: BluetoothConfiguration | None = None
audio_config: AudioConfiguration | None = None audio_config: AudioConfiguration | None = None
power_management_config: PowerManagementConfiguration | None = None power_management_config: PowerManagementConfiguration | None = None
print_service_config: PrintServiceConfiguration | None = None print_service_config: PrintServiceConfiguration | None = None
firewall_config: FirewallConfiguration | None = None firewall_config: FirewallConfiguration | None = None
fonts_config: FontsConfiguration | None = None
@classmethod @classmethod
def parse_arg( def parse_arg(
@ -238,12 +177,8 @@ class ApplicationConfiguration(SubConfig):
if args and (firewall_config := args.get('firewall_config')) is not None: if args and (firewall_config := args.get('firewall_config')) is not None:
app_config.firewall_config = FirewallConfiguration.parse_arg(firewall_config) app_config.firewall_config = FirewallConfiguration.parse_arg(firewall_config)
if args and (fonts_config := args.get('fonts_config')) is not None:
app_config.fonts_config = FontsConfiguration.parse_arg(fonts_config)
return app_config return app_config
@override
def json(self) -> ApplicationSerialization: def json(self) -> ApplicationSerialization:
config: ApplicationSerialization = {} config: ApplicationSerialization = {}
@ -262,32 +197,4 @@ class ApplicationConfiguration(SubConfig):
if self.firewall_config: if self.firewall_config:
config['firewall_config'] = self.firewall_config.json() config['firewall_config'] = self.firewall_config.json()
if self.fonts_config:
config['fonts_config'] = self.fonts_config.json()
return config 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

View File

@ -1,8 +1,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Any, NotRequired, Self, TypedDict, override from typing import Any, NotRequired, Self, TypedDict
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.users import Password, User from archinstall.lib.models.users import Password, User
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -59,7 +58,7 @@ class U2FLoginConfiguration:
@dataclass @dataclass
class AuthenticationConfiguration(SubConfig): class AuthenticationConfiguration:
root_enc_password: Password | None = None root_enc_password: Password | None = None
users: list[User] = field(default_factory=list) users: list[User] = field(default_factory=list)
u2f_config: U2FLoginConfiguration | None = None u2f_config: U2FLoginConfiguration | None = None
@ -76,7 +75,6 @@ class AuthenticationConfiguration(SubConfig):
return auth_config return auth_config
@override
def json(self) -> AuthenticationSerialization: def json(self) -> AuthenticationSerialization:
config: AuthenticationSerialization = {} config: AuthenticationSerialization = {}
@ -85,21 +83,6 @@ class AuthenticationConfiguration(SubConfig):
return config 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: def has_superuser(self) -> bool:
return any(u.sudo for u in self.users) return any(u.sudo for u in self.users)

View File

@ -1,10 +1,9 @@
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Any, Self, override from typing import Any, Self
from archinstall.lib.log import warn from archinstall.lib.output import warn
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -26,13 +25,6 @@ class Bootloader(Enum):
case _: case _:
return False return False
def is_uefi_only(self) -> bool:
match self:
case Bootloader.Systemd | Bootloader.Efistub | Bootloader.Refind:
return True
case _:
return False
def json(self) -> str: def json(self) -> str:
return self.value return self.value
@ -61,26 +53,14 @@ class Bootloader(Enum):
@dataclass @dataclass
class BootloaderConfiguration(SubConfig): class BootloaderConfiguration:
bootloader: Bootloader bootloader: Bootloader
uki: bool = False uki: bool = False
removable: bool = True removable: bool = True
@override
def json(self) -> dict[str, Any]: def json(self) -> dict[str, Any]:
return {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable} 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 @classmethod
def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self: def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self:
bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot) bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot)

View File

@ -1,12 +0,0 @@
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

View File

@ -4,7 +4,7 @@ import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, StrEnum, auto from enum import Enum, StrEnum, auto
from pathlib import Path from pathlib import Path
from typing import Any, NotRequired, Self, TypedDict, override from typing import NotRequired, Self, TypedDict, override
from uuid import UUID from uuid import UUID
import parted import parted
@ -12,9 +12,8 @@ from parted import Disk, Geometry, Partition
from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator
from archinstall.lib.hardware import SysInfo from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.users import Password from archinstall.lib.models.users import Password
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
ENC_IDENTIFIER = 'ainst' ENC_IDENTIFIER = 'ainst'
@ -35,15 +34,6 @@ class DiskLayoutType(Enum):
case DiskLayoutType.Pre_mount: case DiskLayoutType.Pre_mount:
return tr('Pre-mounted configuration') 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): class _DiskLayoutConfigurationSerialization(TypedDict):
config_type: str config_type: str
@ -55,7 +45,7 @@ class _DiskLayoutConfigurationSerialization(TypedDict):
@dataclass @dataclass
class DiskLayoutConfiguration(SubConfig): class DiskLayoutConfiguration:
config_type: DiskLayoutType config_type: DiskLayoutType
device_modifications: list[DeviceModification] = field(default_factory=list) device_modifications: list[DeviceModification] = field(default_factory=list)
lvm_config: LvmConfiguration | None = None lvm_config: LvmConfiguration | None = None
@ -65,7 +55,6 @@ class DiskLayoutConfiguration(SubConfig):
# used for pre-mounted config # used for pre-mounted config
mountpoint: Path | None = None mountpoint: Path | None = None
@override
def json(self) -> _DiskLayoutConfigurationSerialization: def json(self) -> _DiskLayoutConfigurationSerialization:
if self.config_type == DiskLayoutType.Pre_mount: if self.config_type == DiskLayoutType.Pre_mount:
return { return {
@ -89,28 +78,6 @@ class DiskLayoutConfiguration(SubConfig):
return config 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 @classmethod
def parse_arg( def parse_arg(
cls, cls,
@ -753,23 +720,23 @@ class BDevice:
return hash(self.disk.device.path) return hash(self.disk.device.path)
class PartitionType(StrEnum): class PartitionType(Enum):
BOOT = auto() Boot = 'boot'
PRIMARY = auto() Primary = 'primary'
_UNKNOWN = 'unknown' _Unknown = 'unknown'
@classmethod @classmethod
def get_type_from_code(cls, code: int) -> Self: def get_type_from_code(cls, code: int) -> Self:
if code == parted.PARTITION_NORMAL: if code == parted.PARTITION_NORMAL:
return cls.PRIMARY return cls.Primary
else: else:
debug(f'Partition code not supported: {code}') debug(f'Partition code not supported: {code}')
return cls._UNKNOWN return cls._Unknown
def get_partition_code(self) -> int | None: def get_partition_code(self) -> int | None:
if self == PartitionType.PRIMARY: if self == PartitionType.Primary:
return parted.PARTITION_NORMAL return parted.PARTITION_NORMAL
elif self == PartitionType.BOOT: elif self == PartitionType.Boot:
return parted.PARTITION_BOOT return parted.PARTITION_BOOT
return None return None
@ -835,9 +802,6 @@ class FilesystemType(StrEnum):
def is_crypto(self) -> bool: def is_crypto(self) -> bool:
return self == FilesystemType.CRYPTO_LUKS return self == FilesystemType.CRYPTO_LUKS
def is_fat(self) -> bool:
return self in (FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32)
@property @property
def parted_value(self) -> str: def parted_value(self) -> str:
return self.value + '(v1)' if self == FilesystemType.LINUX_SWAP else self.value return self.value + '(v1)' if self == FilesystemType.LINUX_SWAP else self.value
@ -1354,7 +1318,7 @@ class _SnapshotConfigSerialization(TypedDict):
type: str type: str
class SnapshotType(StrEnum): class SnapshotType(Enum):
Snapper = 'Snapper' Snapper = 'Snapper'
Timeshift = 'Timeshift' Timeshift = 'Timeshift'
@ -1436,19 +1400,19 @@ class DeviceModification:
} }
class EncryptionType(StrEnum): class EncryptionType(Enum):
NO_ENCRYPTION = auto() NoEncryption = 'no_encryption'
LUKS = auto() Luks = 'luks'
LVM_ON_LUKS = auto() LvmOnLuks = 'lvm_on_luks'
LUKS_ON_LVM = auto() LuksOnLvm = 'luks_on_lvm'
@classmethod @classmethod
def _encryption_type_mapper(cls) -> dict[str, Self]: def _encryption_type_mapper(cls) -> dict[str, Self]:
return { return {
tr('No Encryption'): cls.NO_ENCRYPTION, tr('No Encryption'): cls.NoEncryption,
tr('LUKS'): cls.LUKS, tr('LUKS'): cls.Luks,
tr('LVM on LUKS'): cls.LVM_ON_LUKS, tr('LVM on LUKS'): cls.LvmOnLuks,
tr('LUKS on LVM'): cls.LUKS_ON_LVM, tr('LUKS on LVM'): cls.LuksOnLvm,
} }
@classmethod @classmethod
@ -1472,7 +1436,7 @@ class _DiskEncryptionSerialization(TypedDict):
@dataclass @dataclass
class DiskEncryption: class DiskEncryption:
encryption_type: EncryptionType = EncryptionType.NO_ENCRYPTION encryption_type: EncryptionType = EncryptionType.NoEncryption
encryption_password: Password | None = None encryption_password: Password | None = None
partitions: list[PartitionModification] = field(default_factory=list) partitions: list[PartitionModification] = field(default_factory=list)
lvm_volumes: list[LvmVolume] = field(default_factory=list) lvm_volumes: list[LvmVolume] = field(default_factory=list)
@ -1480,10 +1444,10 @@ class DiskEncryption:
iter_time: int = DEFAULT_ITER_TIME iter_time: int = DEFAULT_ITER_TIME
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.encryption_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and not self.partitions: if self.encryption_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and not self.partitions:
raise ValueError('Luks or LvmOnLuks encryption require partitions to be defined') raise ValueError('Luks or LvmOnLuks encryption require partitions to be defined')
if self.encryption_type == EncryptionType.LUKS_ON_LVM and not self.lvm_volumes: if self.encryption_type == EncryptionType.LuksOnLvm and not self.lvm_volumes:
raise ValueError('LuksOnLvm encryption require LMV volumes to be defined') raise ValueError('LuksOnLvm encryption require LMV volumes to be defined')
def should_generate_encryption_file(self, dev: PartitionModification | LvmVolume) -> bool: def should_generate_encryption_file(self, dev: PartitionModification | LvmVolume) -> bool:
@ -1626,18 +1590,14 @@ class LsblkInfo(BaseModel):
@field_validator('size', mode='before') @field_validator('size', mode='before')
@classmethod @classmethod
def convert_size(cls, value: Any, info: ValidationInfo) -> Any: def convert_size(cls, v: int, info: ValidationInfo) -> Size:
if isinstance(value, int): sector_size = SectorSize(info.data['log_sec'], Unit.B)
sector_size = SectorSize(info.data['log_sec'], Unit.B) return Size(v, Unit.B, sector_size)
return Size(value, Unit.B, sector_size)
return value
@field_validator('mountpoints', 'fsroots', mode='before') @field_validator('mountpoints', 'fsroots', mode='before')
@classmethod @classmethod
def remove_none(cls, value: Any) -> Any: def remove_none(cls, v: list[Path | None]) -> list[Path]:
if isinstance(value, list): return [item for item in v if item is not None]
return [item for item in value if item is not None]
return value
@field_serializer('size', when_used='json') @field_serializer('size', when_used='json')
def serialize_size(self, size: Size) -> str: def serialize_size(self, size: Size) -> str:

View File

@ -1,21 +1,15 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Self, override from typing import Any, Self
from archinstall.lib.locale.utils import get_kb_layout from archinstall.lib.locale.utils import get_kb_layout
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@dataclass @dataclass
class LocaleConfiguration(SubConfig): class LocaleConfiguration:
kb_layout: str kb_layout: str
sys_lang: str sys_lang: str
sys_enc: str sys_enc: str
# this is the default used in ISO other option for hdpi screens TER16x32
# can be checked using
# zgrep "CONFIG_FONT" /proc/config.gz
# https://wiki.archlinux.org/title/Linux_console#Font
console_font: str = 'default8x16'
@classmethod @classmethod
def default(cls) -> Self: def default(cls) -> Self:
@ -24,29 +18,17 @@ class LocaleConfiguration(SubConfig):
layout = 'us' layout = 'us'
return cls(layout, 'en_US.UTF-8', 'UTF-8') return cls(layout, 'en_US.UTF-8', 'UTF-8')
@override
def json(self) -> dict[str, str]: def json(self) -> dict[str, str]:
return { return {
'kb_layout': self.kb_layout, 'kb_layout': self.kb_layout,
'sys_lang': self.sys_lang, 'sys_lang': self.sys_lang,
'sys_enc': self.sys_enc, 'sys_enc': self.sys_enc,
'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: def preview(self) -> str:
output = '{}: {}\n'.format(tr('Keyboard layout'), self.kb_layout) output = '{}: {}\n'.format(tr('Keyboard layout'), self.kb_layout)
output += '{}: {}\n'.format(tr('Locale language'), self.sys_lang) output += '{}: {}\n'.format(tr('Locale language'), self.sys_lang)
output += '{}: {}\n'.format(tr('Locale encoding'), self.sys_enc) output += '{}: {}'.format(tr('Locale encoding'), self.sys_enc)
output += '{}: {}'.format(tr('Console font'), self.console_font)
return output return output
def _load_config(self, args: dict[str, str]) -> None: def _load_config(self, args: dict[str, str]) -> None:
@ -56,8 +38,6 @@ class LocaleConfiguration(SubConfig):
self.sys_enc = args['sys_enc'] self.sys_enc = args['sys_enc']
if 'kb_layout' in args: if 'kb_layout' in args:
self.kb_layout = args['kb_layout'] self.kb_layout = args['kb_layout']
if 'console_font' in args:
self.console_font = args['console_font']
@classmethod @classmethod
def parse_arg(cls, args: dict[str, Any]) -> Self: def parse_arg(cls, args: dict[str, Any]) -> Self:

View File

@ -9,11 +9,9 @@ from typing import TYPE_CHECKING, Any, Self, TypedDict, override
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator from pydantic import BaseModel, ValidationInfo, field_validator, model_validator
from archinstall.lib.log import debug
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.packages import Repository from archinstall.lib.models.packages import Repository
from archinstall.lib.networking import DownloadTimer, ping from archinstall.lib.networking import DownloadTimer, ping
from archinstall.lib.translationhandler import tr from archinstall.lib.output import debug
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.mirror.mirror_handler import MirrorListHandler from archinstall.lib.mirror.mirror_handler import MirrorListHandler
@ -238,7 +236,7 @@ class _MirrorConfigurationSerialization(TypedDict):
@dataclass @dataclass
class MirrorConfiguration(SubConfig): class MirrorConfiguration:
mirror_regions: list[MirrorRegion] = field(default_factory=list) mirror_regions: list[MirrorRegion] = field(default_factory=list)
custom_servers: list[CustomServer] = field(default_factory=list) custom_servers: list[CustomServer] = field(default_factory=list)
optional_repositories: list[Repository] = field(default_factory=list) optional_repositories: list[Repository] = field(default_factory=list)
@ -252,7 +250,6 @@ class MirrorConfiguration(SubConfig):
def custom_server_urls(self) -> str: def custom_server_urls(self) -> str:
return '\n'.join(s.url for s in self.custom_servers) return '\n'.join(s.url for s in self.custom_servers)
@override
def json(self) -> _MirrorConfigurationSerialization: def json(self) -> _MirrorConfigurationSerialization:
regions = {} regions = {}
for m in self.mirror_regions: for m in self.mirror_regions:
@ -265,24 +262,6 @@ class MirrorConfiguration(SubConfig):
'custom_repositories': [c.json() for c in self.custom_repositories], '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: def custom_servers_config(self) -> str:
config = '' config = ''

View File

@ -3,8 +3,7 @@ from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import NotRequired, Self, TypedDict, override from typing import NotRequired, Self, TypedDict, override
from archinstall.lib.log import debug from archinstall.lib.output import debug
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -12,7 +11,6 @@ class NicType(Enum):
ISO = 'iso' ISO = 'iso'
NM = 'nm' NM = 'nm'
NM_IWD = 'nm_iwd' NM_IWD = 'nm_iwd'
IWD = 'iwd'
MANUAL = 'manual' MANUAL = 'manual'
def display_msg(self) -> str: def display_msg(self) -> str:
@ -23,8 +21,6 @@ class NicType(Enum):
return tr('Use Network Manager (default backend)') return tr('Use Network Manager (default backend)')
case NicType.NM_IWD: case NicType.NM_IWD:
return tr('Use Network Manager (iwd backend)') return tr('Use Network Manager (iwd backend)')
case NicType.IWD:
return tr('Use standalone iwd')
case NicType.MANUAL: case NicType.MANUAL:
return tr('Manual configuration') return tr('Manual configuration')
@ -107,11 +103,10 @@ class _NetworkConfigurationSerialization(TypedDict):
@dataclass @dataclass
class NetworkConfiguration(SubConfig): class NetworkConfiguration:
type: NicType type: NicType
nics: list[Nic] = field(default_factory=list) nics: list[Nic] = field(default_factory=list)
@override
def json(self) -> _NetworkConfigurationSerialization: def json(self) -> _NetworkConfigurationSerialization:
config: _NetworkConfigurationSerialization = {'type': self.type.value} config: _NetworkConfigurationSerialization = {'type': self.type.value}
if self.nics: if self.nics:
@ -119,10 +114,6 @@ class NetworkConfiguration(SubConfig):
return config return config
@override
def summary(self) -> str:
return self.type.display_msg()
@classmethod @classmethod
def parse_arg(cls, config: _NetworkConfigurationSerialization) -> Self | None: def parse_arg(cls, config: _NetworkConfigurationSerialization) -> Self | None:
nic_type = config.get('type', None) nic_type = config.get('type', None)
@ -134,10 +125,6 @@ class NetworkConfiguration(SubConfig):
return cls(NicType.ISO) return cls(NicType.ISO)
case NicType.NM: case NicType.NM:
return cls(NicType.NM) return cls(NicType.NM)
case NicType.NM_IWD:
return cls(NicType.NM_IWD)
case NicType.IWD:
return cls(NicType.IWD)
case NicType.MANUAL: case NicType.MANUAL:
nics_arg = config.get('nics', []) nics_arg = config.get('nics', [])
if nics_arg: if nics_arg:

View File

@ -1,12 +0,0 @@
from enum import StrEnum, auto
from typing import Final
class Kernel(StrEnum):
LINUX = auto()
LINUX_LTS = 'linux-lts'
LINUX_ZEN = 'linux-zen'
LINUX_HARDENED = 'linux-hardened'
DEFAULT_KERNEL: Final = Kernel.LINUX

View File

@ -132,6 +132,7 @@ class AvailablePackage(BaseModel):
def longest_key(self) -> int: def longest_key(self) -> int:
return max(len(key) for key in self.model_dump().keys()) return max(len(key) for key in self.model_dump().keys())
# return all package info line by line
def info(self) -> str: def info(self) -> str:
output = '' output = ''
for key, value in self.model_dump().items(): for key, value in self.model_dump().items():

View File

@ -1,7 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Self, TypedDict, override from typing import Self, TypedDict
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -11,7 +10,7 @@ class PacmanConfigSerialization(TypedDict):
@dataclass @dataclass
class PacmanConfiguration(SubConfig): class PacmanConfiguration:
parallel_downloads: int = 5 parallel_downloads: int = 5
color: bool = True color: bool = True
@ -19,19 +18,12 @@ class PacmanConfiguration(SubConfig):
def default(cls) -> Self: def default(cls) -> Self:
return cls() return cls()
@override
def json(self) -> PacmanConfigSerialization: def json(self) -> PacmanConfigSerialization:
return { return {
'parallel_downloads': self.parallel_downloads, 'parallel_downloads': self.parallel_downloads,
'color': self.color, 'color': self.color,
} }
@override
def summary(self) -> str | None:
if self.color:
return tr('Color enabled')
return None
def preview(self) -> str: def preview(self) -> str:
color_str = str(self.color) color_str = str(self.color)
output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads) output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads)

View File

@ -1,10 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Self, TypedDict, override from typing import TYPE_CHECKING, Self, TypedDict
from archinstall.default_profiles.profile import GreeterType, Profile from archinstall.default_profiles.profile import GreeterType, Profile
from archinstall.lib.hardware import GfxDriver from archinstall.lib.hardware import GfxDriver
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.profile.profiles_handler import ProfileSerialization from archinstall.lib.profile.profiles_handler import ProfileSerialization
@ -17,12 +15,11 @@ class _ProfileConfigurationSerialization(TypedDict):
@dataclass @dataclass
class ProfileConfiguration(SubConfig): class ProfileConfiguration:
profile: Profile | None = None profile: Profile | None = None
gfx_driver: GfxDriver | None = None gfx_driver: GfxDriver | None = None
greeter: GreeterType | None = None greeter: GreeterType | None = None
@override
def json(self) -> _ProfileConfigurationSerialization: def json(self) -> _ProfileConfigurationSerialization:
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler
@ -32,23 +29,6 @@ class ProfileConfiguration(SubConfig):
'greeter': self.greeter.value if self.greeter else None, '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 @classmethod
def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self: def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self:
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler

View File

@ -36,66 +36,67 @@ class PasswordStrength(Enum):
case PasswordStrength.STRONG: case PasswordStrength.STRONG:
return 'green' return 'green'
@staticmethod @classmethod
def strength(password: str) -> PasswordStrength: def strength(cls, password: str) -> Self:
digit = any(character.isdigit() for character in password) digit = any(character.isdigit() for character in password)
upper = any(character.isupper() for character in password) upper = any(character.isupper() for character in password)
lower = any(character.islower() for character in password) lower = any(character.islower() for character in password)
symbol = any(not character.isalnum() for character in password) symbol = any(not character.isalnum() for character in password)
return PasswordStrength._check_password_strength(digit, upper, lower, symbol, len(password)) return cls._check_password_strength(digit, upper, lower, symbol, len(password))
@staticmethod @classmethod
def _check_password_strength( def _check_password_strength(
cls,
digit: bool, digit: bool,
upper: bool, upper: bool,
lower: bool, lower: bool,
symbol: bool, symbol: bool,
length: int, length: int,
) -> PasswordStrength: ) -> Self:
# suggested evaluation # suggested evaluation
# https://github.com/archlinux/archinstall/issues/1304#issuecomment-1146768163 # https://github.com/archlinux/archinstall/issues/1304#issuecomment-1146768163
if digit and upper and lower and symbol: if digit and upper and lower and symbol:
match length: match length:
case num if 13 <= num: case num if 13 <= num:
return PasswordStrength.STRONG return cls.STRONG
case num if 11 <= num <= 12: case num if 11 <= num <= 12:
return PasswordStrength.MODERATE return cls.MODERATE
case num if 7 <= num <= 10: case num if 7 <= num <= 10:
return PasswordStrength.WEAK return cls.WEAK
case num if num <= 6: case num if num <= 6:
return PasswordStrength.VERY_WEAK return cls.VERY_WEAK
elif digit and upper and lower: elif digit and upper and lower:
match length: match length:
case num if 14 <= num: case num if 14 <= num:
return PasswordStrength.STRONG return cls.STRONG
case num if 11 <= num <= 13: case num if 11 <= num <= 13:
return PasswordStrength.MODERATE return cls.MODERATE
case num if 7 <= num <= 10: case num if 7 <= num <= 10:
return PasswordStrength.WEAK return cls.WEAK
case num if num <= 6: case num if num <= 6:
return PasswordStrength.VERY_WEAK return cls.VERY_WEAK
elif upper and lower: elif upper and lower:
match length: match length:
case num if 15 <= num: case num if 15 <= num:
return PasswordStrength.STRONG return cls.STRONG
case num if 12 <= num <= 14: case num if 12 <= num <= 14:
return PasswordStrength.MODERATE return cls.MODERATE
case num if 7 <= num <= 11: case num if 7 <= num <= 11:
return PasswordStrength.WEAK return cls.WEAK
case num if num <= 6: case num if num <= 6:
return PasswordStrength.VERY_WEAK return cls.VERY_WEAK
elif lower or upper: elif lower or upper:
match length: match length:
case num if 18 <= num: case num if 18 <= num:
return PasswordStrength.STRONG return cls.STRONG
case num if 14 <= num <= 17: case num if 14 <= num <= 17:
return PasswordStrength.MODERATE return cls.MODERATE
case num if 9 <= num <= 13: case num if 9 <= num <= 13:
return PasswordStrength.WEAK return cls.WEAK
case num if num <= 8: case num if num <= 8:
return PasswordStrength.VERY_WEAK return cls.VERY_WEAK
return PasswordStrength.VERY_WEAK return cls.VERY_WEAK
UserSerialization = TypedDict( UserSerialization = TypedDict(

View File

@ -1,5 +1,3 @@
import textwrap
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
from archinstall.lib.models.network import NetworkConfiguration, NicType from archinstall.lib.models.network import NetworkConfiguration, NicType
from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.profile import ProfileConfiguration
@ -34,13 +32,6 @@ def install_network_config(
_configure_nm_iwd(installation) _configure_nm_iwd(installation)
installation.disable_service('iwd.service') installation.disable_service('iwd.service')
case NicType.IWD:
installation.add_additional_packages(['iwd'])
_configure_iwd_standalone(installation)
installation.enable_service('iwd.service')
installation.enable_service('systemd-networkd.service')
installation.enable_service('systemd-resolved.service')
case NicType.MANUAL: case NicType.MANUAL:
for nic in network_config.nics: for nic in network_config.nics:
installation.configure_nic(nic) installation.configure_nic(nic)
@ -54,36 +45,3 @@ def _configure_nm_iwd(installation: Installer) -> None:
iwd_backend_conf = nm_conf_dir / 'wifi_backend.conf' iwd_backend_conf = nm_conf_dir / 'wifi_backend.conf'
_ = iwd_backend_conf.write_text('[device]\nwifi.backend=iwd\n') _ = iwd_backend_conf.write_text('[device]\nwifi.backend=iwd\n')
def _configure_iwd_standalone(installation: Installer) -> None:
# iwd manages wireless only; systemd-networkd handles wired DHCP.
iwd_conf_dir = installation.target / 'etc/iwd'
iwd_conf_dir.mkdir(parents=True, exist_ok=True)
main_conf = iwd_conf_dir / 'main.conf'
main_conf_content = textwrap.dedent("""\
[General]
EnableNetworkConfiguration=true
[Network]
NameResolvingService=systemd
""")
_ = main_conf.write_text(main_conf_content)
networkd_dir = installation.target / 'etc/systemd/network'
networkd_dir.mkdir(parents=True, exist_ok=True)
wired_conf = networkd_dir / '20-wired.network'
wired_conf_content = textwrap.dedent("""\
[Match]
Type=ether
Kind=!*
[Network]
DHCP=yes
""")
_ = wired_conf.write_text(wired_conf_content)
resolv = installation.target / 'etc/resolv.conf'
resolv.unlink(missing_ok=True)
resolv.symlink_to('/run/systemd/resolve/stub-resolv.conf')

View File

@ -6,8 +6,8 @@ from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType
from archinstall.lib.networking import list_interfaces from archinstall.lib.networking import list_interfaces
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class ManualNetworkConfig(ListManager[Nic]): class ManualNetworkConfig(ListManager[Nic]):
@ -172,17 +172,14 @@ async def select_network(preset: NetworkConfiguration | None) -> NetworkConfigur
""" """
items = [MenuItem(n.display_msg(), value=n) for n in NicType] items = [MenuItem(n.display_msg(), value=n) for n in NicType]
group = MenuItemGroup(items, sort_items=False) group = MenuItemGroup(items, sort_items=True)
if preset: if preset:
group.set_selected_by_value(preset.type) group.set_selected_by_value(preset.type)
header = tr('Choose network configuration') + '\n'
header += tr('Recommended: Network Manager for desktop, Manual for server') + '\n'
result = await Selection[NicType]( result = await Selection[NicType](
group, group,
header=header, header=tr('Choose network configuration'),
allow_reset=True, allow_reset=True,
allow_skip=True, allow_skip=True,
).show() ).show()
@ -202,8 +199,6 @@ async def select_network(preset: NetworkConfiguration | None) -> NetworkConfigur
return NetworkConfiguration(NicType.NM) return NetworkConfiguration(NicType.NM)
case NicType.NM_IWD: case NicType.NM_IWD:
return NetworkConfiguration(NicType.NM_IWD) return NetworkConfiguration(NicType.NM_IWD)
case NicType.IWD:
return NetworkConfiguration(NicType.IWD)
case NicType.MANUAL: case NicType.MANUAL:
preset_nics = preset.nics if preset else [] preset_nics = preset.nics if preset else []
nics = await ManualNetworkConfig(tr('Configure interfaces'), preset_nics).show() nics = await ManualNetworkConfig(tr('Configure interfaces'), preset_nics).show()

View File

@ -5,13 +5,13 @@ from typing import assert_never, override
from archinstall.lib.command import SysCommand from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.network import WifiConfiguredNetwork, WifiNetwork from archinstall.lib.models.network import WifiConfiguredNetwork, WifiNetwork
from archinstall.lib.network.wpa_supplicant import WpaSupplicantConfig from archinstall.lib.network.wpa_supplicant import WpaSupplicantConfig
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui from archinstall.tui.ui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import Result, ResultType from archinstall.tui.ui.result import Result, ResultType
@dataclass @dataclass

View File

@ -1,8 +1,8 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from archinstall.lib.log import debug
from archinstall.lib.models.network import WifiNetwork from archinstall.lib.models.network import WifiNetwork
from archinstall.lib.output import debug
@dataclass @dataclass

View File

@ -13,7 +13,7 @@ from urllib.parse import urlencode
from urllib.request import urlopen from urllib.request import urlopen
from archinstall.lib.exceptions import DownloadTimeout, SysCallError from archinstall.lib.exceptions import DownloadTimeout, SysCallError
from archinstall.lib.log import debug, error, info from archinstall.lib.output import debug, error, info
from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.pacman.pacman import Pacman
@ -46,12 +46,12 @@ class DownloadTimer:
self.previous_handler = signal.signal(signal.SIGALRM, self.raise_timeout) # type: ignore[assignment] self.previous_handler = signal.signal(signal.SIGALRM, self.raise_timeout) # type: ignore[assignment]
self.previous_timer = signal.alarm(self.timeout) self.previous_timer = signal.alarm(self.timeout)
self.start_time = time.monotonic() self.start_time = time.time()
return self return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None: def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
if self.start_time: if self.start_time:
time_delta = time.monotonic() - self.start_time time_delta = time.time() - self.start_time
signal.alarm(0) signal.alarm(0)
self.time = time_delta self.time = time_delta
if self.timeout > 0: if self.timeout > 0:
@ -165,7 +165,7 @@ def build_icmp(payload: bytes) -> bytes:
def ping(hostname: str, timeout: int = 5) -> int: def ping(hostname: str, timeout: int = 5) -> int:
watchdog = select.epoll() watchdog = select.epoll()
started = time.monotonic() started = time.time()
random_identifier = f'archinstall-{random.randint(1000, 9999)}'.encode() random_identifier = f'archinstall-{random.randint(1000, 9999)}'.encode()
# Create a raw socket (requires root, which should be fine on archiso) # Create a raw socket (requires root, which should be fine on archiso)
@ -180,7 +180,7 @@ def ping(hostname: str, timeout: int = 5) -> int:
# Gracefully wait for X amount of time # Gracefully wait for X amount of time
# for a ICMP response or exit with no latency # for a ICMP response or exit with no latency
while latency == -1 and time.monotonic() - started < timeout: while latency == -1 and time.time() - started < timeout:
try: try:
for _fileno, _event in watchdog.poll(0.1): for _fileno, _event in watchdog.poll(0.1):
response, _ = icmp_socket.recvfrom(1024) response, _ = icmp_socket.recvfrom(1024)
@ -188,7 +188,7 @@ def ping(hostname: str, timeout: int = 5) -> int:
# Check if it's an Echo Reply (ICMP type 0) # Check if it's an Echo Reply (ICMP type 0)
if icmp_type == 0 and response[-len(random_identifier) :] == random_identifier: if icmp_type == 0 and response[-len(random_identifier) :] == random_identifier:
latency = round((time.monotonic() - started) * 1000) latency = round((time.time() - started) * 1000)
break break
except OSError as e: except OSError as e:
debug(f'Error: {e}') debug(f'Error: {e}')

335
archinstall/lib/output.py Normal file
View File

@ -0,0 +1,335 @@
import logging
import os
import sys
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
from datetime import UTC, datetime
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any
from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust
if TYPE_CHECKING:
from _typeshed import DataclassInstance
class FormattedOutput:
@staticmethod
def _get_values(
o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg]
filter_list: list[str] = [],
) -> dict[str, Any]:
"""
the original values returned a dataclass as dict thru the call to some specific methods
this version allows thru the parameter class_formatter to call a dynamically selected formatting method.
Can transmit a filter list to the class_formatter,
"""
if class_formatter:
# if invoked per reference it has to be a standard function or a classmethod.
# A method of an instance does not make sense
if callable(class_formatter):
return class_formatter(o, filter_list)
# if is invoked by name we restrict it to a method of the class. No need to mess more
elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)):
func = getattr(o, class_formatter)
return func(filter_list)
raise ValueError('Unsupported formatting call')
elif hasattr(o, 'table_data'):
return o.table_data()
elif hasattr(o, 'json'):
return o.json()
elif is_dataclass(o):
return asdict(o)
else:
return o.__dict__ # type: ignore[unreachable]
@classmethod
def as_table(
cls,
obj: list[Any],
class_formatter: str | Callable | None = None, # type: ignore[type-arg]
filter_list: list[str] = [],
capitalize: bool = False,
) -> str:
"""variant of as_table (subtly different code) which has two additional parameters
filter which is a list of fields which will be shown
class_formatter a special method to format the outgoing data
A general comment, the format selected for the output (a string where every data record is separated by newline)
is for compatibility with a print statement
As_table_filter can be a drop in replacement for as_table
"""
raw_data = [cls._get_values(o, class_formatter, filter_list) for o in obj]
# determine the maximum column size
column_width: dict[str, int] = {}
for o in raw_data:
for k, v in o.items():
if not filter_list or k in filter_list:
column_width.setdefault(k, 0)
column_width[k] = max([column_width[k], len(str(v)), len(k)])
if not filter_list:
filter_list = list(column_width.keys())
# create the header lines
output = ''
key_list = []
for key in filter_list:
width = column_width[key]
key = key.replace('!', '').replace('_', ' ')
if capitalize:
key = key.capitalize()
key_list.append(unicode_ljust(key, width))
output += ' | '.join(key_list) + '\n'
output += '-' * len(output) + '\n'
# create the data lines
for record in raw_data:
obj_data = []
for key in filter_list:
width = column_width.get(key, len(key))
value = record.get(key, '')
if '!' in key:
value = '*' * len(value)
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
obj_data.append(unicode_rjust(str(value), width))
else:
obj_data.append(unicode_ljust(str(value), width))
output += ' | '.join(obj_data) + '\n'
return output
@staticmethod
def as_columns(entries: list[str], cols: int) -> str:
"""
Will format a list into a given number of columns
"""
chunks = []
output = ''
for i in range(0, len(entries), cols):
chunks.append(entries[i : i + cols])
for row in chunks:
out_fmt = '{: <30} ' * len(row)
output += out_fmt.format(*row) + '\n'
return output
class Journald:
@staticmethod
def log(message: str, level: int = logging.DEBUG) -> None:
try:
import systemd.journal # type: ignore[import-not-found]
except ModuleNotFoundError:
return None
log_adapter = logging.getLogger('archinstall')
log_fmt = logging.Formatter('[%(levelname)s]: %(message)s')
log_ch = systemd.journal.JournalHandler()
log_ch.setFormatter(log_fmt)
log_adapter.addHandler(log_ch)
log_adapter.setLevel(logging.DEBUG)
log_adapter.log(level, message)
class Logger:
def __init__(self, path: Path = Path('/var/log/archinstall')) -> None:
self._path = path
@property
def path(self) -> Path:
return self._path / 'install.log'
@property
def directory(self) -> Path:
return self._path
def _check_permissions(self) -> None:
log_file = self.path
try:
self._path.mkdir(exist_ok=True, parents=True)
log_file.touch(exist_ok=True)
with log_file.open('a') as f:
f.write('')
except PermissionError:
# Fallback to creating the log file in the current folder
logger._path = Path('./').absolute()
warn(f'Not enough permission to place log file at {log_file}, creating it in {logger.path} instead')
def log(self, level: int, content: str) -> None:
self._check_permissions()
with self.path.open('a') as f:
ts = _timestamp()
level_name = logging.getLevelName(level)
f.write(f'[{ts}] - {level_name} - {content}\n')
logger = Logger()
def _supports_color() -> bool:
"""
Found first reference here:
https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python
And re-used this:
https://github.com/django/django/blob/master/django/core/management/color.py#L12
Return True if the running system's terminal supports color,
and False otherwise.
"""
supported_platform = sys.platform != 'win32' or 'ANSICON' in os.environ
# isatty is not always implemented, #6223.
is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
return supported_platform and is_a_tty
class Font(Enum):
bold = '1'
italic = '3'
underscore = '4'
blink = '5'
reverse = '7'
conceal = '8'
def _stylize_output(
text: str,
fg: str,
bg: str | None,
reset: bool,
font: list[Font] = [],
) -> str:
"""
Heavily influenced by:
https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13
Color options here:
https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i
Adds styling to a text given a set of color arguments.
"""
colors = {
'black': '0',
'red': '1',
'green': '2',
'yellow': '3',
'blue': '4',
'magenta': '5',
'cyan': '6',
'white': '7',
'teal': '8;5;109', # Extended 256-bit colors (not always supported)
'orange': '8;5;208', # https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html#256-colors
'darkorange': '8;5;202',
'gray': '8;5;246',
'grey': '8;5;246',
'darkgray': '8;5;240',
'lightgray': '8;5;256',
}
foreground = {key: f'3{colors[key]}' for key in colors}
background = {key: f'4{colors[key]}' for key in colors}
code_list = []
if text == '' and reset:
return '\x1b[0m'
code_list.append(foreground[str(fg)])
if bg:
code_list.append(background[str(bg)])
for o in font:
code_list.append(o.value)
ansi = ';'.join(code_list)
return f'\033[{ansi}m{text}\033[0m'
def info(
*msgs: str,
level: int = logging.INFO,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def _timestamp() -> str:
now = datetime.now(tz=UTC)
return now.strftime('%Y-%m-%d %H:%M:%S')
def debug(
*msgs: str,
level: int = logging.DEBUG,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def error(
*msgs: str,
level: int = logging.ERROR,
fg: str = 'red',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def warn(
*msgs: str,
level: int = logging.WARNING,
fg: str = 'yellow',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def log(
*msgs: str,
level: int = logging.INFO,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
text = ' '.join(str(x) for x in msgs)
logger.log(level, text)
# Attempt to colorize the output if supported
# Insert default colors and override with **kwargs
if _supports_color():
text = _stylize_output(text, fg, bg, reset, font)
Journald.log(text, level=level)
if level != logging.DEBUG:
print(text)

View File

@ -1,20 +1,20 @@
from functools import lru_cache from functools import lru_cache
from archinstall.lib.exceptions import SysCallError from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.menu.helpers import Loading, Notify, Selection from archinstall.lib.menu.helpers import Loading, Notify, Selection
from archinstall.lib.models.packages import AvailablePackage, LocalPackage, PackageGroup, Repository from archinstall.lib.models.packages import AvailablePackage, LocalPackage, PackageGroup, Repository
from archinstall.lib.output import debug
from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
def installed_package(package: str) -> LocalPackage | None: def installed_package(package: str) -> LocalPackage | None:
try: try:
package_info = [] package_info = []
for line in Pacman.run(f'-Q --info {package}'): for line in Pacman.run(f'-Q --info {package}'):
package_info.append(line.decode().rstrip()) package_info.append(line.decode().strip())
return _parse_package_output(package_info, LocalPackage) return _parse_package_output(package_info, LocalPackage)
except SysCallError: except SysCallError:
@ -53,7 +53,7 @@ def available_package(package: str) -> AvailablePackage | None:
try: try:
package_info: list[str] = [] package_info: list[str] = []
for line in Pacman.run(f'-S --info {package}'): for line in Pacman.run(f'-S --info {package}'):
package_info.append(line.decode().rstrip()) package_info.append(line.decode().strip())
return _parse_package_output(package_info, AvailablePackage) return _parse_package_output(package_info, AvailablePackage)
except SysCallError: except SysCallError:
@ -79,7 +79,7 @@ def list_available_packages(
debug(f'Failed to sync Arch Linux package database: {e}') debug(f'Failed to sync Arch Linux package database: {e}')
for line in Pacman.run('-S --info'): for line in Pacman.run('-S --info'):
dec_line = line.decode().rstrip() dec_line = line.decode().strip()
current_package.append(dec_line) current_package.append(dec_line)
if dec_line.startswith('Validated'): if dec_line.startswith('Validated'):
@ -187,7 +187,6 @@ async def select_additional_packages(
multi=True, multi=True,
preview_location='right', preview_location='right',
enable_filter=True, enable_filter=True,
wrap_preview=True,
).show() ).show()
match pck_result.type_: match pck_result.type_:

View File

@ -1,6 +1,6 @@
from functools import lru_cache from functools import lru_cache
from archinstall.lib.log import debug from archinstall.lib.output import debug
from archinstall.lib.packages.packages import check_package_upgrade from archinstall.lib.packages.packages import check_package_upgrade

View File

@ -5,7 +5,7 @@ from pathlib import Path
from archinstall.lib.command import SysCommand from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import RequirementError from archinstall.lib.exceptions import RequirementError
from archinstall.lib.log import error, info, warn from archinstall.lib.output import error, info, warn
from archinstall.lib.pathnames import PACMAN_CONF from archinstall.lib.pathnames import PACMAN_CONF
from archinstall.lib.plugins import plugins from archinstall.lib.plugins import plugins
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -29,11 +29,11 @@ class Pacman:
if pacman_db_lock.exists(): if pacman_db_lock.exists():
warn(tr('Pacman is already running, waiting maximum 10 minutes for it to terminate.')) warn(tr('Pacman is already running, waiting maximum 10 minutes for it to terminate.'))
started = time.monotonic() started = time.time()
while pacman_db_lock.exists(): while pacman_db_lock.exists():
time.sleep(0.25) time.sleep(0.25)
if time.monotonic() - started > (60 * 10): if time.time() - started > (60 * 10):
error(tr('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.')) error(tr('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.'))
sys.exit(1) sys.exit(1)

View File

@ -5,8 +5,8 @@ from archinstall.lib.menu.helpers import Confirmation, Input
from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.pathnames import PACMAN_CONF from archinstall.lib.pathnames import PACMAN_CONF
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class PacmanMenu(AbstractSubMenu[PacmanConfiguration]): class PacmanMenu(AbstractSubMenu[PacmanConfiguration]):

View File

@ -7,7 +7,7 @@ import urllib.request
from importlib import metadata from importlib import metadata
from pathlib import Path from pathlib import Path
from archinstall.lib.log import error, info, warn from archinstall.lib.output import error, info, warn
from archinstall.lib.version import get_version from archinstall.lib.version import get_version
plugins = {} plugins = {}

View File

@ -7,8 +7,8 @@ from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class ProfileMenu(AbstractSubMenu[ProfileConfiguration]): class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
@ -95,7 +95,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
driver = await select_driver(preset=preset) driver = await select_driver(preset=preset)
if driver and 'Sway' in profile.current_selection_names(): if driver and 'Sway' in profile.current_selection_names():
if driver.is_nvidia_proprietary(): if driver.is_nvidia():
header = tr('The proprietary Nvidia driver is not supported by Sway.') + '\n' header = tr('The proprietary Nvidia driver is not supported by Sway.') + '\n'
header += tr('It is likely that you will run into issues, are you okay with that?') + '\n' header += tr('It is likely that you will run into issues, are you okay with that?') + '\n'
@ -105,7 +105,8 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
preset=False, preset=False,
).show() ).show()
return driver if result.get_value() else preset if result.get_value():
return preset
return driver return driver
@ -113,7 +114,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
if item.value: if item.value:
driver = item.get_value().value driver = item.get_value().value
packages = item.get_value().packages_text() packages = item.get_value().packages_text()
return f'{tr("Graphics driver")}: {driver}\n{packages}' return f'Driver: {driver}\n{packages}'
return None return None
def _prev_greeter(self, item: MenuItem) -> str | None: def _prev_greeter(self, item: MenuItem) -> str | None:

View File

@ -9,9 +9,9 @@ from typing import TYPE_CHECKING, NotRequired, TypedDict
from archinstall.default_profiles.profile import CustomSetting, GreeterType, Profile from archinstall.default_profiles.profile import CustomSetting, GreeterType, Profile
from archinstall.lib.hardware import GfxDriver, GfxPackage from archinstall.lib.hardware import GfxDriver, GfxPackage
from archinstall.lib.log import debug, error, info
from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.networking import fetch_data_from_url from archinstall.lib.networking import fetch_data_from_url
from archinstall.lib.output import debug, error, info
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
if TYPE_CHECKING: if TYPE_CHECKING:
@ -175,9 +175,6 @@ class ProfileHandler:
case GreeterType.PlasmaLoginManager: case GreeterType.PlasmaLoginManager:
packages = ['plasma-login-manager'] packages = ['plasma-login-manager']
service = ['plasmalogin'] service = ['plasmalogin']
case GreeterType.GreetdDms:
packages = ['greetd']
service = ['greetd']
if packages: if packages:
install_session.add_additional_packages(packages) install_session.add_additional_packages(packages)
@ -197,26 +194,6 @@ class ProfileHandler:
with open(path, 'w') as file: with open(path, 'w') as file:
file.write(filedata) file.write(filedata)
if greeter == GreeterType.GreetdDms:
greetd_config = install_session.target / 'etc/greetd/config.toml'
greetd_config.parent.mkdir(parents=True, exist_ok=True)
greetd_config.write_text(
'[terminal]\n'
'vt = 1\n'
'\n'
'[default_session]\n'
'user = "greeter"\n'
'command = "/usr/share/quickshell/dms/Modules/Greetd/assets/dms-greeter --command niri -p /usr/share/quickshell/dms"\n',
)
tmpfiles = install_session.target / 'etc/tmpfiles.d/dms-greeter.conf'
tmpfiles.parent.mkdir(parents=True, exist_ok=True)
tmpfiles.write_text(
'# Path Mode User Group Age Argument\n'
'd /var/cache/dms-greeter 0750 greeter greeter -\n'
'd /var/lib/greeter 0755 greeter greeter -\n',
)
def install_gfx_driver(self, install_session: Installer, driver: GfxDriver) -> None: def install_gfx_driver(self, install_session: Installer, driver: GfxDriver) -> None:
debug(f'Installing GFX driver: {driver.value}') debug(f'Installing GFX driver: {driver.value}')

View File

@ -2,16 +2,10 @@ import builtins
import gettext import gettext
import json import json
import os import os
import tempfile
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import override from typing import override
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.utils.util import running_from_iso
@dataclass @dataclass
class Language: class Language:
@ -20,7 +14,6 @@ class Language:
translation: gettext.NullTranslations translation: gettext.NullTranslations
translation_percent: int translation_percent: int
translated_lang: str | None translated_lang: str | None
console_font: str | None = None
@property @property
def display_name(self) -> str: def display_name(self) -> str:
@ -38,18 +31,10 @@ class Language:
return self.name_en return self.name_en
_DEFAULT_FONT = 'default8x16'
_ENV_FONT = os.environ.get('FONT')
class TranslationHandler: class TranslationHandler:
def __init__(self) -> None: def __init__(self) -> None:
self._base_pot = 'base.pot' self._base_pot = 'base.pot'
self._languages = 'languages.json' self._languages = 'languages.json'
self._active_language: Language | None = None
self._font_backup: Path | None = None
self._cmap_backup: Path | None = None
self._using_env_font: bool = False
self._total_messages = self._get_total_active_messages() self._total_messages = self._get_total_active_messages()
self._translated_languages = self._get_translations() self._translated_languages = self._get_translations()
@ -58,65 +43,6 @@ class TranslationHandler:
def translated_languages(self) -> list[Language]: def translated_languages(self) -> list[Language]:
return self._translated_languages return self._translated_languages
@property
def active_font(self) -> str | None:
if self._active_language is not None:
return self._active_language.console_font
return None
def _set_font(self, font_name: str | None) -> bool:
"""Set the console font via setfont. Only runs on ISO. Returns True on success."""
if not running_from_iso():
return False
target = font_name or _DEFAULT_FONT
try:
SysCommand(['setfont', target])
return True
except SysCallError as err:
debug(f'Failed to set console font {target}: {err}')
return False
def save_console_font(self) -> None:
"""Save the current console font (with unicode map) and console map to temp files."""
if not running_from_iso():
return
try:
font_fd, font_path = tempfile.mkstemp(prefix='archinstall_font_')
cmap_fd, cmap_path = tempfile.mkstemp(prefix='archinstall_cmap_')
os.close(font_fd)
os.close(cmap_fd)
self._font_backup = Path(font_path)
self._cmap_backup = Path(cmap_path)
SysCommand(['setfont', '-O', str(self._font_backup), '-om', str(self._cmap_backup)])
except SysCallError as err:
debug(f'Failed to save console font: {err}')
self._font_backup = None
self._cmap_backup = None
def restore_console_font(self) -> None:
"""Restore console font (with unicode map) and console map from backup."""
if not running_from_iso():
return
if self._font_backup is None or not self._font_backup.exists():
return
cmd = ['setfont', str(self._font_backup)]
if self._cmap_backup is not None and self._cmap_backup.exists():
cmd += ['-m', str(self._cmap_backup)]
try:
SysCommand(cmd)
except SysCallError as err:
debug(f'Failed to restore console font: {err}')
self._font_backup.unlink(missing_ok=True)
self._font_backup = None
if self._cmap_backup is not None:
self._cmap_backup.unlink(missing_ok=True)
self._cmap_backup = None
def _get_translations(self) -> list[Language]: def _get_translations(self) -> list[Language]:
""" """
Load all translated languages and return a list of such Load all translated languages and return a list of such
@ -131,7 +57,6 @@ class TranslationHandler:
abbr = mapping_entry['abbr'] abbr = mapping_entry['abbr']
lang = mapping_entry['lang'] lang = mapping_entry['lang']
translated_lang = mapping_entry.get('translated_lang', None) translated_lang = mapping_entry.get('translated_lang', None)
console_font = mapping_entry.get('console_font', None)
try: try:
# get a translation for a specific language # get a translation for a specific language
@ -146,7 +71,7 @@ class TranslationHandler:
# prevent cases where the .pot file is out of date and the percentage is above 100 # prevent cases where the .pot file is out of date and the percentage is above 100
percent = min(100, percent) percent = min(100, percent)
language = Language(abbr, lang, translation, percent, translated_lang, console_font) language = Language(abbr, lang, translation, percent, translated_lang)
languages.append(language) languages.append(language)
except FileNotFoundError as err: except FileNotFoundError as err:
raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}") raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}")
@ -202,39 +127,12 @@ class TranslationHandler:
except Exception: except Exception:
raise ValueError(f'No language with abbreviation "{abbr}" found') raise ValueError(f'No language with abbreviation "{abbr}" found')
def activate(self, language: Language, set_font: bool = True) -> None: def activate(self, language: Language) -> None:
""" """
Set the provided language as the current translation Set the provided language as the current translation
""" """
# The install() call has the side effect of assigning GNUTranslations.gettext to builtins._ # The install() call has the side effect of assigning GNUTranslations.gettext to builtins._
language.translation.install() language.translation.install()
self._active_language = language
if set_font and not self._using_env_font:
self._set_font(language.console_font)
def apply_console_font(self) -> None:
"""Apply console font from FONT env var or active language mapping.
If FONT env var is set and valid, use it and skip language mapping.
If FONT is set but invalid, fall back to language font.
If FONT is not set, use active language font.
"""
if not running_from_iso():
return
if _ENV_FONT:
if self._set_font(_ENV_FONT):
self._using_env_font = True
debug(f'Console font set from FONT env var: {_ENV_FONT}')
else:
debug(f'FONT={_ENV_FONT} could not be set, falling back to language font mapping')
if self.active_font:
self._set_font(self.active_font)
debug(f'Console font set from language mapping: {self.active_font}')
elif self.active_font:
self._set_font(self.active_font)
debug(f'Console font set from language mapping: {self.active_font}')
def _get_locales_dir(self) -> Path: def _get_locales_dir(self) -> Path:
""" """

View File

@ -6,8 +6,8 @@ from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import get_password from archinstall.lib.menu.util import get_password
from archinstall.lib.models.users import User from archinstall.lib.models.users import User
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem from archinstall.tui.ui.menu_item import MenuItem
from archinstall.tui.result import ResultType from archinstall.tui.ui.result import ResultType
class UserList(ListManager[User]): class UserList(ListManager[User]):

View File

@ -1,148 +0,0 @@
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
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
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()
def as_columns(entries: list[str], cols: int) -> str:
"""
Will format a list into a given number of columns
"""
chunks: list[list[str]] = []
output = ''
for i in range(0, len(entries), cols):
chunks.append(entries[i : i + cols])
for row in chunks:
out_fmt = '{: <30} ' * len(row)
output += out_fmt.format(*row) + '\n'
return output
def _get_values(
o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]
filter_list: list[str] = [],
) -> dict[str, Any]:
"""
the original values returned a dataclass as dict thru the call to some specific methods
this version allows thru the parameter class_formatter to call a dynamically selected formatting method.
Can transmit a filter list to the class_formatter,
"""
if class_formatter:
# if invoked per reference it has to be a standard function or a classmethod.
# A method of an instance does not make sense
if callable(class_formatter):
return class_formatter(o, filter_list)
# if is invoked by name we restrict it to a method of the class. No need to mess more
elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)):
func = getattr(o, class_formatter)
return func(filter_list)
raise ValueError('Unsupported formatting call')
elif hasattr(o, 'table_data'):
return o.table_data()
elif hasattr(o, 'json'):
return o.json()
elif is_dataclass(o):
return asdict(o)
else:
return o.__dict__ # type: ignore[unreachable]
def as_table(
obj: list[Any],
class_formatter: str | Callable | None = None, # type: ignore[type-arg]
filter_list: list[str] = [],
capitalize: bool = False,
) -> str:
"""variant of as_table (subtly different code) which has two additional parameters
filter which is a list of fields which will be shown
class_formatter a special method to format the outgoing data
A general comment, the format selected for the output (a string where every data record is separated by newline)
is for compatibility with a print statement
As_table_filter can be a drop in replacement for as_table
"""
raw_data = [_get_values(o, class_formatter, filter_list) for o in obj]
# determine the maximum column size
column_width: dict[str, int] = {}
for o in raw_data:
for k, v in o.items():
if not filter_list or k in filter_list:
column_width.setdefault(k, 0)
column_width[k] = max([column_width[k], len(str(v)), len(k)])
if not filter_list:
filter_list = list(column_width.keys())
# create the header lines
output = ''
key_list = []
for key in filter_list:
width = column_width[key]
key = key.replace('!', '').replace('_', ' ')
if capitalize:
key = key.capitalize()
key_list.append(unicode_ljust(key, width))
output += ' | '.join(key_list) + '\n'
output += '-' * len(output) + '\n'
# create the data lines
for record in raw_data:
obj_data = []
for key in filter_list:
width = column_width.get(key, len(key))
value = record.get(key, '')
if '!' in key:
value = '*' * len(value)
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
obj_data.append(unicode_rjust(str(value), width))
else:
obj_data.append(unicode_ljust(str(value), width))
output += ' | '.join(obj_data) + '\n'
return output

Some files were not shown because too many files have changed in this diff Show More