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

View File

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

View File

@ -1,22 +1,28 @@
name: Translation validation
on:
push:
paths:
- 'archinstall/**/*.py'
- 'archinstall/locales/**'
- '.github/workflows/translation-check.yaml'
pull_request:
paths:
- 'archinstall/**/*.py'
- 'archinstall/locales/**'
- '.github/workflows/translation-check.yaml'
jobs:
translations:
name: Validate translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Run translation checks
run: bash archinstall/locales/locales_generator.sh check
#on:
# push:
# paths:
# - 'archinstall/locales/**'
# pull_request:
# paths:
# - 'archinstall/locales/**'
#name: Verify local_generate script was run on translation changes
#jobs:
# translation-check:
# runs-on: ubuntu-latest
# container:
# image: archlinux/archlinux:latest
# steps:
# - uses: actions/checkout@v4
# - run: pacman --noconfirm -Syu python git diffutils
# - name: Verify all translation scripts are up to date
# run: |
# cd ..
# cp -r archinstall archinstall_orig
# cd archinstall/archinstall/locales
# 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
node_modules/
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']
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.14
rev: v0.15.10
hooks:
# fix unused imports and sort them
- id: ruff
@ -31,7 +31,7 @@ repos:
args: [--config=.flake8]
fail_fast: true
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v1.20.1
hooks:
- id: mypy
args: [
@ -41,7 +41,6 @@ repos:
additional_dependencies:
- pydantic
- pytest
- hypothesis
- cryptography
- textual
- repo: local

View File

@ -5,7 +5,7 @@
# Contributor: demostanis worlds <demostanis@protonmail.com>
pkgname=archinstall
pkgver=4.3
pkgver=4.2
pkgrel=1
pkgdesc="Just another guided/automated Arch Linux installer with a twist"
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:
* Provide the stacktrace of the output if applicable
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
* To 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
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
# pip install --upgrade archinstall
# 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>
`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 \
-machine q35,accel=kvm -device intel-iommu \
-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/edk2/x64/OVMF_VARS.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/ovmf/x64/OVMF.4m.fd \
-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 \
-machine q35,accel=kvm -device intel-iommu \
-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/edk2/x64/OVMF_VARS.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/ovmf/x64/OVMF.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw \
-device intel-hda -device hda-duplex,audiodev=snd0 \
-audiodev pa,id=snd0,server=/run/user/1000/pulse/native
@ -219,10 +219,6 @@ qemu-system-x86_64 -enable-kvm \
# 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
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 archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug
from archinstall.lib.models.application import Audio, AudioConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

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

View File

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import Firewall, FirewallConfiguration
from archinstall.lib.output import debug
if TYPE_CHECKING:
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 archinstall.lib.log import debug
from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
@ -21,18 +21,6 @@ class PowerManagementApp:
'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(
self,
install_session: Installer,
@ -43,7 +31,5 @@ class PowerManagementApp:
match power_management_config.power_management:
case PowerManagement.POWER_PROFILES_DAEMON:
install_session.add_additional_packages(self.ppd_packages)
install_session.enable_service(self.ppd_services)
case PowerManagement.TUNED:
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 archinstall.lib.log import debug
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

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

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 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):
@ -29,11 +27,3 @@ class BspwmProfile(Profile):
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
for user in users:
install_session.arch_chroot('mkdir -p ~/.config/bspwm ~/.config/sxhkd', run_as=user.username)
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/bspwmrc ~/.config/bspwm/', run_as=user.username)
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/sxhkdrc ~/.config/sxhkd/', run_as=user.username)
install_session.arch_chroot('chmod +x ~/.config/bspwm/bspwmrc', run_as=user.username)

View File

@ -9,7 +9,7 @@ class BudgieProfile(Profile):
'Budgie',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
display_server=DisplayServerType.Xorg,
)
@property
@ -20,7 +20,6 @@ class BudgieProfile(Profile):
'budgie',
'mate-terminal',
'nemo',
'nemo-fileroller',
'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 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.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):
@ -45,8 +49,26 @@ class HyprlandProfile(Profile):
return [pref]
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
async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value
await self._select_seat_access()

View File

@ -1,7 +1,11 @@
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.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):
@ -39,8 +43,26 @@ class LabwcProfile(Profile):
return [pref]
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
async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value
await self._select_seat_access()

View File

@ -1,13 +1,17 @@
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.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):
def __init__(self) -> None:
super().__init__(
'niri',
'Niri',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
@ -47,8 +51,26 @@ class NiriProfile(Profile):
return [pref]
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
async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value
await self._select_seat_access()

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.packages.packages import available_package, package_group_info
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class PlasmaFlavor(StrEnum):

View File

@ -1,7 +1,11 @@
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.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):
@ -49,8 +53,26 @@ class SwayProfile(Profile):
return [pref]
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
async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value
await self._select_seat_access()

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'
CosmicSession = 'cosmic-greeter'
PlasmaLoginManager = 'plasma-login-manager'
GreetdDms = 'dms-greeter'
class SelectResult(Enum):

View File

@ -1,11 +1,11 @@
from typing import TYPE_CHECKING, Self, override
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.output import info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
if TYPE_CHECKING:
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.bluetooth import BluetoothApp
from archinstall.applications.firewall import FirewallApp
from archinstall.applications.fonts import FontsApp
from archinstall.applications.power_management import PowerManagementApp
from archinstall.applications.print_service import PrintServiceApp
from archinstall.lib.models import Audio
@ -43,9 +42,3 @@ class ApplicationHandler:
install_session,
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,
Firewall,
FirewallConfiguration,
FontPackage,
FontsConfiguration,
PowerManagement,
PowerManagementConfiguration,
PrintServiceConfiguration,
)
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
@ -79,13 +77,6 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
preview_action=self._prev_firewall,
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:
@ -124,13 +115,6 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
return f'{tr("Firewall")}: {config.firewall.value}'
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:
group = MenuItemGroup.from_enum(PowerManagement)
@ -233,31 +217,3 @@ async def select_firewall(preset: FirewallConfiguration | None = None) -> Firewa
return FirewallConfiguration(firewall=result.get_value())
case ResultType.Reset:
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
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass, field
from enum import Enum, StrEnum
from pathlib import Path
from typing import Any, Self
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 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.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration
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.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
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.translationhandler import Language, tr, translation_handler
from archinstall.lib.version import get_version
from archinstall.tui.components import tui
class SubCommand(Enum):
SHARE_LOG = 'share-log'
from archinstall.tui.ui.components import tui
@p_dataclass
@ -62,83 +55,6 @@ class Arguments:
advanced: 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
class ArchConfig:
@ -155,7 +71,7 @@ class ArchConfig:
auth_config: AuthenticationConfiguration | None = None
swap: ZramConfiguration | None = None
hostname: str = 'archlinux'
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
kernels: list[str] = field(default_factory=lambda: ['linux'])
ntp: bool = True
packages: list[str] = field(default_factory=list)
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
@ -163,84 +79,58 @@ class ArchConfig:
services: list[str] = field(default_factory=list)
custom_commands: list[str] = field(default_factory=list)
def unsafe_config(self) -> dict[ArchConfigType, Any]:
config: dict[ArchConfigType, list[UserSerialization] | str | None] = {}
def unsafe_config(self) -> dict[str, Any]:
config: dict[str, list[UserSerialization] | str | None] = {}
if self.auth_config:
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:
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:
disk_encryption = self.disk_config.disk_encryption
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
def safe_config(self) -> dict[ArchConfigType, Any]:
base_config: dict[ArchConfigType, Any] = {
ArchConfigType.VERSION: self.version,
ArchConfigType.SCRIPT: self.script,
ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(),
def safe_config(self) -> dict[str, Any]:
config: Any = {
'version': self.version,
'script': self.script,
'archinstall-language': self.archinstall_language.json(),
'hostname': self.hostname,
'kernels': self.kernels,
'ntp': self.ntp,
'packages': self.packages,
'pacman_config': self.pacman_config.json(),
'swap': self.swap,
'timezone': self.timezone,
'services': self.services,
'custom_commands': self.custom_commands,
'bootloader_config': self.bootloader_config.json() if self.bootloader_config else None,
'app_config': self.app_config.json() if self.app_config else None,
'auth_config': self.auth_config.json() if self.auth_config else None,
}
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:
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:
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:
cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config
config['network_config'] = self.network_config.json()
if self.app_config:
cfg[ArchConfigType.APP_CONFIG] = self.app_config
return cfg
return config
@classmethod
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):
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
translation_handler.activate(arch_config.archinstall_language, set_font=False)
if disk_config := args_config.get('disk_config', {}):
enc_password = args_config.get('encryption_password', '')
@ -371,13 +260,13 @@ class ArchConfig:
class ArchConfigHandler:
def __init__(self) -> None:
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()
try:
self._config = ArchConfig.from_config(config, self._args)
self._config = ArchConfig.from_config(config, args)
self._config.version = get_version()
except ValueError as err:
warn(str(err))
@ -403,13 +292,8 @@ class ArchConfigHandler:
def print_help(self) -> None:
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:
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'-v',
'--version',
@ -545,6 +429,7 @@ class ArchConfigHandler:
default=False,
help='Enabled verbose options',
)
return parser
def _parse_args(self) -> Arguments:

View File

@ -3,9 +3,9 @@ from pathlib import Path
from typing import TYPE_CHECKING
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.users import User
from archinstall.lib.output import debug, info
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
@ -53,7 +53,7 @@ class AuthenticationHandler:
def _add_u2f_entry(self, file: Path, entry: str) -> None:
if not file.exists():
debug(f'File does not exist: {file}')
return
return None
content = file.read_text().splitlines()
@ -81,7 +81,7 @@ class AuthenticationHandler:
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/
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.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import Password, User
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.user.user_menu import select_users
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
@ -65,7 +65,7 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
users: list[User] | None = item.value
if users:
return as_table(users)
return FormattedOutput.as_table(users)
return 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.exceptions import SysCallError
from archinstall.lib.log import error
from archinstall.lib.output import error
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.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
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 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
@ -44,8 +44,8 @@ class SysCommandWorker:
self._trace_log_pos = 0
self.poll_object = epoll()
self.child_fd: int | None = None
self.started = False
self.ended = False
self.started: float | None = None
self.ended: float | None = None
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
def __contains__(self, key: bytes) -> bool:
@ -117,7 +117,7 @@ class SysCommandWorker:
def is_alive(self) -> bool:
self.poll()
if self.started and not self.ended:
if self.started and self.ended is None:
return True
return False
@ -173,11 +173,11 @@ class SysCommandWorker:
self.peak(output)
self._trace_log += output
except OSError:
self.ended = True
self.ended = time.time()
break
if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = True
self.ended = time.time()
try:
wait_status = os.waitpid(self.pid, 0)[1]
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
os.chdir(old_dir)
self.started = True
self.started = time.time()
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True

View File

@ -6,16 +6,14 @@ from typing import Any
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.log import debug, logger, warn
from archinstall.lib.menu.helpers import Confirmation, Selection
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.utils.format import as_key_value_pair
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class ConfigurationOutput:
@ -45,51 +43,25 @@ class ConfigurationOutput:
def user_config_to_json(self) -> str:
config = self._config.safe_config()
adapter = TypeAdapter(dict[ArchConfigType, Any])
adapter = TypeAdapter(dict[str, Any])
python_dict = adapter.dump_python(config)
return json.dumps(python_dict, indent=4, sort_keys=True)
def user_credentials_to_json(self) -> str:
cfg = self._config.unsafe_config()
config = self._config.unsafe_config()
adapter = TypeAdapter(dict[ArchConfigType, Any])
python_dict = adapter.dump_python(cfg)
adapter = TypeAdapter(dict[str, Any])
python_dict = adapter.dump_python(config)
return json.dumps(python_dict, indent=4, sort_keys=True)
def write_debug(self) -> None:
debug(' -- Chosen configuration --')
debug(self.user_config_to_json())
def as_summary(self) -> str:
"""
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:
async def confirm_config(self) -> bool:
header = f'{tr("The specified configuration will be applied")}. '
header += tr('Would you like to continue?') + '\n'
if show_install_warnings:
header += self._render_install_warnings()
group = MenuItemGroup.yes_no()
group.set_preview_for_all(lambda x: self.user_config_to_json())
@ -107,22 +79,6 @@ class ConfigurationOutput:
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:
dest_path_ok = dest_path.exists() and dest_path.is_dir()
if not dest_path_ok:

View File

@ -6,7 +6,7 @@ from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken
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')

View File

@ -15,7 +15,6 @@ from archinstall.lib.disk.utils import (
umount,
)
from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from archinstall.lib.log import debug, error, info, log
from archinstall.lib.models.device import (
DEFAULT_ITER_TIME,
BDevice,
@ -36,6 +35,7 @@ from archinstall.lib.models.device import (
_PartitionInfo,
)
from archinstall.lib.models.users import Password
from archinstall.lib.output import debug, error, info, log
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
@ -250,7 +250,7 @@ class DeviceHandler:
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
# Force create
options.append('-F')
case _ if fs_type.is_fat():
case FilesystemType.FAT12 | FilesystemType.FAT16 | FilesystemType.FAT32:
mkfs_type = 'fat'
# Set FAT size
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.encryption_menu import DiskEncryptionMenu
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.helpers import Confirmation, Notify, Selection, Table
from archinstall.lib.menu.util import prompt_dir
@ -36,10 +35,10 @@ from archinstall.lib.models.device import (
Unit,
_DeviceInfo,
)
from archinstall.lib.output import FormattedOutput, debug
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
@dataclass
@ -222,7 +221,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
for mod in device_mods:
# 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 += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
@ -231,7 +230,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
# create btrfs table
btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols]
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
return output.rstrip()
@ -247,12 +246,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg())
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 += 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)
return output
@ -281,7 +280,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
if enc_config.encryption_password:
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'
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)
if dev and dev.partition_infos:
return as_table(dev.partition_infos)
return FormattedOutput.as_table(dev.partition_infos)
return None
if preset is None:
@ -504,7 +503,7 @@ def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModifi
# boot partition
return PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
type=PartitionType.Primary,
start=start,
length=size,
mountpoint=Path('/boot'),
@ -656,7 +655,7 @@ async def suggest_single_disk_layout(
root_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
type=PartitionType.Primary,
start=root_start,
length=root_length,
mountpoint=Path('/') if not using_subvolumes else None,
@ -681,7 +680,7 @@ async def suggest_single_disk_layout(
home_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
type=PartitionType.Primary,
start=home_start,
length=home_length,
mountpoint=Path('/home'),
@ -766,7 +765,7 @@ async def suggest_multi_disk_layout(
# add root partition to the root device
root_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
type=PartitionType.Primary,
start=root_start,
length=root_length,
mountpoint=Path('/'),
@ -788,7 +787,7 @@ async def suggest_multi_disk_layout(
# add home partition to home device
home_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
type=PartitionType.Primary,
start=home_start,
length=home_length,
mountpoint=Path('/home'),

View File

@ -17,10 +17,10 @@ from archinstall.lib.models.device import (
PartitionModification,
)
from archinstall.lib.models.users import Password
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
@ -105,19 +105,19 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _check_dep_enc_type(self) -> bool:
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 False
def _check_dep_partitions(self) -> bool:
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 False
def _check_dep_lvm_vols(self) -> bool:
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 False
@ -137,13 +137,13 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
assert enc_partitions 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 = []
if enc_type == EncryptionType.LUKS_ON_LVM:
if enc_type == EncryptionType.LuksOnLvm:
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(
encryption_password=enc_password,
encryption_type=enc_type,
@ -199,7 +199,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _prev_partitions(self, item: MenuItem) -> str | None:
if item.value:
output = tr('Partitions to be encrypted') + '\n'
output += as_table(item.value)
output += FormattedOutput.as_table(item.value)
return output.rstrip()
return None
@ -207,7 +207,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _prev_lvm_vols(self, item: MenuItem) -> str | None:
if item.value:
output = tr('LVM volumes to be encrypted') + '\n'
output += as_table(item.value)
output += FormattedOutput.as_table(item.value)
return output.rstrip()
return None
@ -227,7 +227,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
iter_time = item.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 None
@ -240,9 +240,9 @@ async def select_encryption_type(
options: list[EncryptionType] = []
if lvm_config:
options = [EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM]
options = [EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm]
else:
options = [EncryptionType.LUKS]
options = [EncryptionType.Luks]
if not preset:
preset = options[0]
@ -321,7 +321,7 @@ async def select_partitions_to_encrypt(
avail_partitions = [p for p in partitions if not p.exists()]
if avail_partitions:
group = MenuItemGroup.from_objects(avail_partitions)
group = MenuItemGroup.from_objects(partitions)
group.set_selected_by_value(preset)
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:
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('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:
try:

View File

@ -4,9 +4,9 @@ from typing import ClassVar
from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import error, info
from archinstall.lib.models.device import Fido2Device
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

View File

@ -13,7 +13,6 @@ from archinstall.lib.disk.lvm import (
lvm_vol_reduce,
)
from archinstall.lib.disk.utils import udev_sync
from archinstall.lib.log import debug, info
from archinstall.lib.models.device import (
DiskEncryption,
DiskLayoutConfiguration,
@ -28,6 +27,7 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from archinstall.lib.output import debug, info
class FilesystemHandler:
@ -139,7 +139,7 @@ class FilesystemHandler:
self._format_lvm_vols(self._disk_config.lvm_config)
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)
self._setup_lvm(lvm_config, enc_mods)
@ -148,7 +148,7 @@ class FilesystemHandler:
# Don't close LVM or LUKS during setup - keep everything active
# The installation phase will handle unlocking and mounting
# 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)
enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False)
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.disk.utils import get_lsblk_info, umount
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.users import Password
from archinstall.lib.output import debug, info
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.disk.utils import udev_sync
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.device import (
LvmGroupInfo,
LvmPVInfo,
@ -18,6 +17,7 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from archinstall.lib.output import debug
def _lvm_info(

View File

@ -19,10 +19,10 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class FreeSpace:
@ -58,7 +58,7 @@ class DiskSegment:
part_mod = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType._UNKNOWN,
type=PartitionType._Unknown,
start=self.segment.start,
length=self.segment.length,
)
@ -479,7 +479,7 @@ class PartitioningList(ListManager[DiskSegment]):
sector_size = device_info.sector_size
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'
max_sectors = free_space.length.format_size(Unit.sectors, sector_size)
@ -527,7 +527,7 @@ class PartitioningList(ListManager[DiskSegment]):
partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
type=PartitionType.Primary,
start=free_space.start,
length=length,
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.models.device import SubvolumeModification
from archinstall.lib.translationhandler import tr
from archinstall.tui.result import ResultType
from archinstall.tui.ui.result import ResultType
class SubvolumeMenu(ListManager[SubvolumeModification]):

View File

@ -4,8 +4,8 @@ from pydantic import BaseModel
from archinstall.lib.command import SysCommand
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.output import debug, info, warn
class LsblkOutput(BaseModel):

View File

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

View File

@ -3,24 +3,29 @@ from typing import assert_never
from archinstall.lib.hardware import GfxDriver, SysInfo
from archinstall.lib.menu.helpers import Confirmation, Selection
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.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
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.
:return: The string as a selected kernel
:rtype: string
"""
group = MenuItemGroup.from_enum(Kernel, sort_items=True, preset=preset)
group.set_default_by_value(DEFAULT_KERNEL)
group.set_focus_by_value(DEFAULT_KERNEL)
kernels = ['linux', 'linux-lts', 'linux-zen', 'linux-hardened']
default_kernel = 'linux'
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,
header=tr('Select which kernel(s) to install'),
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.authentication.authentication_menu import AuthenticationMenu
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.configuration import ConfigurationOutput, save_config
from archinstall.lib.configuration import save_config
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.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.authentication import AuthenticationConfiguration
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.mirrors import MirrorConfiguration
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.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
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.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman_menu import PacmanMenu
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_table
from archinstall.tui.components import tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.components import tui
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
class GlobalMenu(AbstractMenu[None]):
@ -104,7 +102,7 @@ class GlobalMenu(AbstractMenu[None]):
),
MenuItem(
text=tr('Kernels'),
value=[DEFAULT_KERNEL],
value=['linux'],
action=select_kernel,
preview_action=self._prev_kernel,
mandatory=True,
@ -299,7 +297,7 @@ class GlobalMenu(AbstractMenu[None]):
if item.value:
network_config: NetworkConfiguration = item.value
if network_config.type == NicType.MANUAL:
output = as_table(network_config.nics)
output = FormattedOutput.as_table(network_config.nics)
else:
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'
if auth_config.users:
output += as_table(auth_config.users) + '\n'
output += FormattedOutput.as_table(auth_config.users) + '\n'
if 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:
return None
bootloader = bootloader_config.bootloader
if disk_config := self._item_group.find_by_key('disk_config').value:
for layout in disk_config.device_modifications:
if root_partition := layout.get_root_partition():
@ -486,11 +486,16 @@ class GlobalMenu(AbstractMenu[None]):
if efi_partition is None:
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'
if failure := validate_bootloader_layout(bootloader_config, disk_config):
return failure.description
if bootloader == Bootloader.Limine:
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
@ -502,13 +507,9 @@ class GlobalMenu(AbstractMenu[None]):
return text[:-1] # remove last new line
if error := self._validate_bootloader():
return tr('Invalid configuration: {}').format(error)
return tr(f'Invalid configuration: {error}')
self.sync_all_to_config()
summary = ConfigurationOutput(self._arch_config).as_summary()
if summary:
return f'{tr("Ready to install")}\n\n{summary}'
return tr('Ready to install')
return None
def _prev_profile(self, item: MenuItem) -> str | None:
profile_config: ProfileConfiguration | None = item.value
@ -612,7 +613,7 @@ class GlobalMenu(AbstractMenu[None]):
if mirror_config.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}'
return output.strip()

View File

@ -6,8 +6,8 @@ from typing import Self
from archinstall.lib.command import SysCommand
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.output import debug
from archinstall.lib.translationhandler import tr
@ -68,19 +68,6 @@ class GfxDriver(Enum):
case _:
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:
pkg_names = [p.value for p in self.gfx_packages()]
text = tr('Installed packages') + ':\n'

View File

@ -12,7 +12,6 @@ from types import TracebackType
from typing import Any, Self
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.disk.fido import Fido2
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.linux_path import LPath
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.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 (
DiskEncryption,
DiskLayoutConfiguration,
@ -49,10 +47,10 @@ from archinstall.lib.models.device import (
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration
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.pacman import PacmanConfiguration
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.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman import Pacman
@ -61,12 +59,7 @@ from archinstall.lib.plugins import plugins
from archinstall.lib.translationhandler import tr
# Any package that the Installer() is responsible for (optional and the default ones)
# https://github.com/archlinux/archinstall/issues/4368
# 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]
__packages__ = ['base', 'sudo', 'linux-firmware', 'linux', 'linux-lts', 'linux-zen', 'linux-hardened']
# Additional packages that are installed if the user is running the Live ISO with accessibility tools enabled
__accessibility_packages__ = ['brltty', 'espeakup', 'alsa-utils']
@ -85,11 +78,11 @@ class Installer:
`Installer()` is the wrapper for most basic installation steps.
It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things.
"""
self._base_packages = base_packages or __packages__[:4]
self.kernels = kernels or [DEFAULT_KERNEL.value]
self._base_packages = base_packages or __packages__[:3]
self.kernels = kernels or ['linux']
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.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
@ -189,10 +182,10 @@ class Installer:
if not skip_ntp:
info(tr('Waiting for time sync (timedatectl show) to complete.'))
started_wait = time.monotonic()
started_wait = time.time()
notified = False
while True:
if not notified and time.monotonic() - started_wait > 5:
if not notified and time.time() - started_wait > 5:
notified = True
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] = {}
match self._disk_encryption.encryption_type:
case EncryptionType.NO_ENCRYPTION:
case EncryptionType.NoEncryption:
self._import_lvm()
self._mount_lvm_layout()
case EncryptionType.LUKS:
case EncryptionType.Luks:
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)
self._import_lvm()
self._mount_lvm_layout(luks_handlers)
case EncryptionType.LUKS_ON_LVM:
case EncryptionType.LuksOnLvm:
self._import_lvm()
luks_handlers = self._prepare_luks_lvm(self._disk_encryption.lvm_volumes)
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
if part_mod.mountpoint:
target = self.target / part_mod.relative_mountpoint
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)
mount(part_mod.dev_path, target, options=part_mod.mount_options)
elif part_mod.fs_type == FilesystemType.BTRFS:
# 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]
@ -407,7 +395,7 @@ class Installer:
def _mount_luks_partition(self, part_mod: PartitionModification, luks_handler: Luks2) -> None:
if not luks_handler.mapper_dev:
return
return None
if part_mod.fs_type == FilesystemType.BTRFS and part_mod.btrfs_subvols:
# Only mount BTRFS subvolumes that have mountpoints specified
@ -445,11 +433,11 @@ class Installer:
def generate_key_files(self) -> None:
match self._disk_encryption.encryption_type:
case EncryptionType.LUKS:
case EncryptionType.Luks:
self._generate_key_files_partitions()
case EncryptionType.LUKS_ON_LVM:
case EncryptionType.LuksOnLvm:
self._generate_key_file_lvm_volumes()
case EncryptionType.LVM_ON_LUKS:
case EncryptionType.LvmOnLuks:
# currently LvmOnLuks only supports a single
# partitioning layout (boot + partition)
# 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
# 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:
logfile_target = self.target / LPath(logger.directory).relative_to_root()
logfile_target.mkdir(parents=True, exist_ok=True)
logger.path.copy_into(logfile_target, preserve_metadata=True)
absolute_logfile = logger.path
logfile_target = self.target / absolute_logfile
logfile_target.parent.mkdir(parents=True, exist_ok=True)
absolute_logfile.copy(logfile_target, preserve_metadata=True)
return True
@ -750,9 +739,6 @@ class Installer:
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:
subprocess.check_call(f'arch-chroot {self.target}', shell=True)
@ -913,7 +899,7 @@ class Installer:
if vol.fs_type is not None:
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:
self._prepare_encrypt(lvm)
else:
@ -1001,7 +987,17 @@ class Installer:
}
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:
SysCommand(command, peek_output=True)
@ -1141,7 +1137,7 @@ class Installer:
kernel_parameters = []
match self._disk_encryption.encryption_type:
case EncryptionType.LVM_ON_LUKS:
case EncryptionType.LvmOnLuks:
if not lvm.vg_name:
raise ValueError(f'Unable to determine VG name for {lvm.name}')
@ -1158,7 +1154,7 @@ class Installer:
else:
debug(f'LvmOnLuks, encrypted root partition, identifying by UUID: {uuid}')
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)
if self._disk_encryption.hsm_device:
@ -1167,7 +1163,7 @@ class Installer:
else:
debug(f'LuksOnLvm, encrypted root partition, identifying by UUID: {uuid}')
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}')
kernel_parameters.append(f'root={lvm.safe_dev_path}')
@ -1340,7 +1336,13 @@ class Installer:
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 not efi_partition:
@ -1465,14 +1467,6 @@ class Installer:
elif not efi_partition.mountpoint:
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}')
parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path)
@ -1483,16 +1477,20 @@ class Installer:
if bootloader_removable:
efi_dir_path = efi_dir_path / '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:
efi_dir_path = efi_dir_path / '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)
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:
raise DiskError(f'Failed to install Limine in {self.target}{efi_partition.mountpoint}: {err}')
@ -1541,7 +1539,7 @@ class Installer:
try:
# 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
self.arch_chroot(f'limine bios-install {parent_dev_path}', peek_output=True)
@ -1759,7 +1757,6 @@ class Installer:
self,
root: PartitionModification | LvmVolume,
efi_partition: PartitionModification | None,
keep_initramfs: bool = False,
) -> None:
if not efi_partition or not efi_partition.mountpoint:
raise ValueError(f'Could not detect ESP at mountpoint {self.target}')
@ -1783,11 +1780,11 @@ class Installer:
config = preset.read_text().splitlines(True)
for index, line in enumerate(config):
# Avoid storing redundant image file
if m := image_re.match(line):
if not keep_initramfs:
image = self.target / m.group(2)
image.unlink(missing_ok=True)
config[index] = '#' + m.group(1)
image = self.target / m.group(2)
image.unlink(missing_ok=True)
config[index] = '#' + m.group(1)
elif m := uki_re.match(line):
if diff_mountpoint:
config[index] = m.group(2) + diff_mountpoint + m.group(3)
@ -1806,12 +1803,7 @@ class Installer:
if not self.mkinitcpio(['-P']):
error('Error generating initramfs (continuing anyway)')
def add_bootloader(
self,
bootloader: Bootloader,
uki_enabled: bool = False,
bootloader_removable: bool = False,
) -> None:
def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False, bootloader_removable: bool = False) -> None:
"""
Adds a bootloader to the installation instance.
Archinstall supports one of five types:
@ -1860,13 +1852,7 @@ class Installer:
bootloader_removable = False
if uki_enabled:
keep_initramfs = (
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)
self._config_uki(root, efi_partition)
match bootloader:
case Bootloader.Systemd:
@ -1932,17 +1918,16 @@ class Installer:
if not handled_by_plugin:
info(f'Creating user {user.username}')
cmd = self._chroot_argv('useradd', '-m')
cmd = 'useradd -m'
if user.sudo:
cmd += ['-G', 'wheel']
cmd += ' -G wheel'
cmd += ['--', user.username]
cmd += f' {user.username}'
try:
run(cmd)
except CalledProcessError as err:
debug(f'Error creating user {user.username}: {err}')
self.arch_chroot(cmd)
except SysCallError as err:
raise SystemError(f'Could not create user inside installation: {err}')
for plugin in plugins.values():
@ -1953,11 +1938,7 @@ class Installer:
self.set_user_password(user)
for group in user.groups:
cmd = self._chroot_argv('gpasswd', '-a', user.username, group)
try:
run(cmd)
except CalledProcessError as err:
warn(f'Failed to add {user.username} to group {group}: {err}')
self.arch_chroot(f'gpasswd -a {user.username} {group}')
if user.sudo:
self.enable_sudo(user)
@ -1972,7 +1953,7 @@ class Installer:
return False
input_data = f'{user.username}:{enc_password}'.encode()
cmd = self._chroot_argv('chpasswd', '--encrypted')
cmd = ['arch-chroot', '-S', str(self.target), 'chpasswd', '--encrypted']
try:
run(cmd, input_data=input_data)
@ -1984,31 +1965,29 @@ class Installer:
def user_set_shell(self, user: str, shell: str) -> bool:
info(f'Setting shell for {user} to {shell}')
cmd = self._chroot_argv('chsh', '-s', shell, user)
try:
run(cmd)
self.arch_chroot(f'sh -c "chsh -s {shell} {user}"')
return True
except CalledProcessError as err:
debug(f'Error setting user shell: {err}')
except SysCallError:
return False
def chown(self, owner: str, path: str, options: list[str] | None = None) -> bool:
options = options or []
cmd = self._chroot_argv('chown', *options, '--', owner, path)
def chown(self, owner: str, path: str, options: list[str] = []) -> bool:
cleaned_path = path.replace("'", "\\'")
try:
run(cmd)
self.arch_chroot(f"sh -c 'chown {' '.join(options)} {owner} {cleaned_path}'")
return True
except CalledProcessError as err:
debug(f'Error changing ownership of {path}: {err}')
except SysCallError:
return False
def set_vconsole(self, locale_config: LocaleConfiguration) -> None:
# use the already set 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-'):
self.pacman.strap(['terminus-font'])
font_vconsole = 'default8x16'
# Ensure /etc exists
vconsole_dir: Path = self.target / 'etc'

View File

@ -1,12 +1,12 @@
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.helpers import Selection
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
@ -47,13 +47,6 @@ class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
preview_action=lambda item: item.get_value(),
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
@ -147,25 +140,3 @@ async def select_kb_layout(preset: str | None = None) -> str | None:
return preset
case _:
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.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
@ -29,13 +26,6 @@ def list_locales() -> list[str]:
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]:
return (
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 typing import Any, Self, override
from archinstall.lib.log import error
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import error
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import InstanceRunnable
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Chars
from archinstall.tui.ui.components import InstanceRunnable
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
CONFIG_KEY = '__config__'
@ -153,7 +154,7 @@ class AbstractSubMenu[ValueT](AbstractMenu[ValueT]):
auto_cursor: bool = True,
allow_reset: bool = False,
):
back_text = ' ' + tr('Back')
back_text = f'{Chars.Right_arrow} ' + tr('Back')
item_group.add_item(MenuItem(text=back_text))
super().__init__(

View File

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

View File

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

View File

@ -1,5 +1,5 @@
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.lib.output import FormattedOutput
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
class MenuHelper[ValueT]:
@ -32,7 +32,7 @@ class MenuHelper[ValueT]:
display_data: dict[str, ValueT | str | None] = {}
if data:
table = as_table(data)
table = FormattedOutput.as_table(data)
rows = table.split('\n')
# 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.models.users import Password, PasswordStrength
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import InputInfo, InputInfoType, tui
from archinstall.tui.result import ResultType
from archinstall.tui.ui.components import InputInfo, InputInfoType, tui
from archinstall.tui.ui.result import ResultType
async def get_password(

View File

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

View File

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

View File

@ -1,9 +1,6 @@
from dataclasses import dataclass
from enum import StrEnum, auto
from typing import Any, NotRequired, Self, TypedDict, override
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
from typing import Any, NotRequired, Self, TypedDict
class PowerManagement(StrEnum):
@ -42,31 +39,6 @@ class FirewallConfigSerialization(TypedDict):
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):
ZSTD = auto()
LZO_RLE = 'lzo-rle'
@ -81,7 +53,6 @@ class ApplicationSerialization(TypedDict):
power_management_config: NotRequired[PowerManagementConfigSerialization]
print_service_config: NotRequired[PrintServiceConfigSerialization]
firewall_config: NotRequired[FirewallConfigSerialization]
fonts_config: NotRequired[FontsConfigSerialization]
@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)
class ZramConfiguration(SubConfig):
class ZramConfiguration:
enabled: bool
algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD
@ -182,34 +141,14 @@ class ZramConfiguration(SubConfig):
algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value))
return cls(enabled=enabled, algorithm=ZramAlgorithm(algo))
@override
def json(self) -> dict[str, bool | str]:
return {
'enabled': self.enabled,
'algorithm': self.algorithm.value,
}
@override
def summary(self) -> list[str] | None:
out: list[str] = []
if self.enabled:
out.append(tr('Zram enabled'))
out.append(tr('Zram algorithm {}').format(self.algorithm))
return out
return None
@dataclass
class ApplicationConfiguration(SubConfig):
class ApplicationConfiguration:
bluetooth_config: BluetoothConfiguration | None = None
audio_config: AudioConfiguration | None = None
power_management_config: PowerManagementConfiguration | None = None
print_service_config: PrintServiceConfiguration | None = None
firewall_config: FirewallConfiguration | None = None
fonts_config: FontsConfiguration | None = None
@classmethod
def parse_arg(
@ -238,12 +177,8 @@ class ApplicationConfiguration(SubConfig):
if args and (firewall_config := args.get('firewall_config')) is not None:
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
@override
def json(self) -> ApplicationSerialization:
config: ApplicationSerialization = {}
@ -262,32 +197,4 @@ class ApplicationConfiguration(SubConfig):
if self.firewall_config:
config['firewall_config'] = self.firewall_config.json()
if self.fonts_config:
config['fonts_config'] = self.fonts_config.json()
return config
@override
def summary(self) -> list[str]:
out: list[str] = []
if self.bluetooth_config and self.bluetooth_config.enabled:
out.append(tr('Bluetooth enabled'))
if self.audio_config:
out.append(tr('Audio server "{}"').format(self.audio_config.audio))
if self.power_management_config:
out.append(tr('Power management "{}"').format(self.power_management_config.power_management))
if self.print_service_config and self.print_service_config.enabled:
out.append(tr('Print service enabled'))
if self.firewall_config:
out.append(tr('Firewall "{}"').format(self.firewall_config.firewall))
if self.fonts_config and self.fonts_config.fonts:
fonts = ', '.join(f.value for f in self.fonts_config.fonts)
out.append(tr('Extra fonts "{}"').format(fonts))
return out

View File

@ -1,8 +1,7 @@
from dataclasses import dataclass, field
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.translationhandler import tr
@ -59,7 +58,7 @@ class U2FLoginConfiguration:
@dataclass
class AuthenticationConfiguration(SubConfig):
class AuthenticationConfiguration:
root_enc_password: Password | None = None
users: list[User] = field(default_factory=list)
u2f_config: U2FLoginConfiguration | None = None
@ -76,7 +75,6 @@ class AuthenticationConfiguration(SubConfig):
return auth_config
@override
def json(self) -> AuthenticationSerialization:
config: AuthenticationSerialization = {}
@ -85,21 +83,6 @@ class AuthenticationConfiguration(SubConfig):
return config
@override
def summary(self) -> list[str]:
out: list[str] = []
if self.root_enc_password:
out.append(tr('Root password set'))
if self.users:
out.append(tr('Configured {} user(s)').format(len(self.users)))
if self.u2f_config:
out.append(tr('U2F set up'))
return out
def has_superuser(self) -> bool:
return any(u.sudo for u in self.users)

View File

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

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

View File

@ -1,21 +1,15 @@
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.models.config import SubConfig
from archinstall.lib.translationhandler import tr
@dataclass
class LocaleConfiguration(SubConfig):
class LocaleConfiguration:
kb_layout: str
sys_lang: 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
def default(cls) -> Self:
@ -24,29 +18,17 @@ class LocaleConfiguration(SubConfig):
layout = 'us'
return cls(layout, 'en_US.UTF-8', 'UTF-8')
@override
def json(self) -> dict[str, str]:
return {
'kb_layout': self.kb_layout,
'sys_lang': self.sys_lang,
'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:
output = '{}: {}\n'.format(tr('Keyboard layout'), self.kb_layout)
output += '{}: {}\n'.format(tr('Locale language'), self.sys_lang)
output += '{}: {}\n'.format(tr('Locale encoding'), self.sys_enc)
output += '{}: {}'.format(tr('Console font'), self.console_font)
output += '{}: {}'.format(tr('Locale encoding'), self.sys_enc)
return output
def _load_config(self, args: dict[str, str]) -> None:
@ -56,8 +38,6 @@ class LocaleConfiguration(SubConfig):
self.sys_enc = args['sys_enc']
if 'kb_layout' in args:
self.kb_layout = args['kb_layout']
if 'console_font' in args:
self.console_font = args['console_font']
@classmethod
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 archinstall.lib.log import debug
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.packages import Repository
from archinstall.lib.networking import DownloadTimer, ping
from archinstall.lib.translationhandler import tr
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
@ -238,7 +236,7 @@ class _MirrorConfigurationSerialization(TypedDict):
@dataclass
class MirrorConfiguration(SubConfig):
class MirrorConfiguration:
mirror_regions: list[MirrorRegion] = field(default_factory=list)
custom_servers: list[CustomServer] = 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:
return '\n'.join(s.url for s in self.custom_servers)
@override
def json(self) -> _MirrorConfigurationSerialization:
regions = {}
for m in self.mirror_regions:
@ -265,24 +262,6 @@ class MirrorConfiguration(SubConfig):
'custom_repositories': [c.json() for c in self.custom_repositories],
}
@override
def summary(self) -> list[str]:
out: list[str] = []
if self.mirror_regions:
out.append(tr('Mirror regions "{}"').format(', '.join(m.name for m in self.mirror_regions)))
if self.optional_repositories:
out.append(tr('Optional repositories "{}"').format(', '.join(r.value for r in self.optional_repositories)))
if self.custom_servers:
out.append(tr('Custom servers set up'))
if self.custom_repositories:
out.append(tr('Custom repositories set up'))
return out
def custom_servers_config(self) -> str:
config = ''

View File

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

View File

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

View File

@ -1,10 +1,8 @@
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.lib.hardware import GfxDriver
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
from archinstall.lib.profile.profiles_handler import ProfileSerialization
@ -17,12 +15,11 @@ class _ProfileConfigurationSerialization(TypedDict):
@dataclass
class ProfileConfiguration(SubConfig):
class ProfileConfiguration:
profile: Profile | None = None
gfx_driver: GfxDriver | None = None
greeter: GreeterType | None = None
@override
def json(self) -> _ProfileConfigurationSerialization:
from archinstall.lib.profile.profiles_handler import profile_handler
@ -32,23 +29,6 @@ class ProfileConfiguration(SubConfig):
'greeter': self.greeter.value if self.greeter else None,
}
@override
def summary(self) -> list[str] | None:
out: list[str] = []
if self.profile:
out.append(self.profile.name)
if self.gfx_driver:
out.append(tr('{} grphics driver').format(self.gfx_driver.value))
if self.greeter:
out.append(tr('{} greeter').format(self.greeter.value))
return out
return None
@classmethod
def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self:
from archinstall.lib.profile.profiles_handler import profile_handler

View File

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

View File

@ -1,5 +1,3 @@
import textwrap
from archinstall.lib.installer import Installer
from archinstall.lib.models.network import NetworkConfiguration, NicType
from archinstall.lib.models.profile import ProfileConfiguration
@ -34,13 +32,6 @@ def install_network_config(
_configure_nm_iwd(installation)
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:
for nic in network_config.nics:
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.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.networking import list_interfaces
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
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]
group = MenuItemGroup(items, sort_items=False)
group = MenuItemGroup(items, sort_items=True)
if preset:
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](
group,
header=header,
header=tr('Choose network configuration'),
allow_reset=True,
allow_skip=True,
).show()
@ -202,8 +199,6 @@ async def select_network(preset: NetworkConfiguration | None) -> NetworkConfigur
return NetworkConfiguration(NicType.NM)
case NicType.NM_IWD:
return NetworkConfiguration(NicType.NM_IWD)
case NicType.IWD:
return NetworkConfiguration(NicType.IWD)
case NicType.MANUAL:
preset_nics = preset.nics if preset else []
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.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.network import WifiConfiguredNetwork, WifiNetwork
from archinstall.lib.network.wpa_supplicant import WpaSupplicantConfig
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import Result, ResultType
from archinstall.tui.ui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import Result, ResultType
@dataclass

View File

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

View File

@ -13,7 +13,7 @@ from urllib.parse import urlencode
from urllib.request import urlopen
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
@ -46,12 +46,12 @@ class DownloadTimer:
self.previous_handler = signal.signal(signal.SIGALRM, self.raise_timeout) # type: ignore[assignment]
self.previous_timer = signal.alarm(self.timeout)
self.start_time = time.monotonic()
self.start_time = time.time()
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
if self.start_time:
time_delta = time.monotonic() - self.start_time
time_delta = time.time() - self.start_time
signal.alarm(0)
self.time = time_delta
if self.timeout > 0:
@ -165,7 +165,7 @@ def build_icmp(payload: bytes) -> bytes:
def ping(hostname: str, timeout: int = 5) -> int:
watchdog = select.epoll()
started = time.monotonic()
started = time.time()
random_identifier = f'archinstall-{random.randint(1000, 9999)}'.encode()
# 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
# 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:
for _fileno, _event in watchdog.poll(0.1):
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)
if icmp_type == 0 and response[-len(random_identifier) :] == random_identifier:
latency = round((time.monotonic() - started) * 1000)
latency = round((time.time() - started) * 1000)
break
except OSError as 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 archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.menu.helpers import Loading, Notify, Selection
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.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
def installed_package(package: str) -> LocalPackage | None:
try:
package_info = []
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)
except SysCallError:
@ -53,7 +53,7 @@ def available_package(package: str) -> AvailablePackage | None:
try:
package_info: list[str] = []
for line in Pacman.run(f'-S --info {package}'):
package_info.append(line.decode().rstrip())
package_info.append(line.decode().strip())
return _parse_package_output(package_info, AvailablePackage)
except SysCallError:
@ -79,7 +79,7 @@ def list_available_packages(
debug(f'Failed to sync Arch Linux package database: {e}')
for line in Pacman.run('-S --info'):
dec_line = line.decode().rstrip()
dec_line = line.decode().strip()
current_package.append(dec_line)
if dec_line.startswith('Validated'):
@ -187,7 +187,6 @@ async def select_additional_packages(
multi=True,
preview_location='right',
enable_filter=True,
wrap_preview=True,
).show()
match pck_result.type_:

View File

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

View File

@ -5,7 +5,7 @@ from pathlib import Path
from archinstall.lib.command import SysCommand
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.plugins import plugins
from archinstall.lib.translationhandler import tr
@ -29,11 +29,11 @@ class Pacman:
if pacman_db_lock.exists():
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():
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.'))
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.pathnames import PACMAN_CONF
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class PacmanMenu(AbstractSubMenu[PacmanConfiguration]):

View File

@ -7,7 +7,7 @@ import urllib.request
from importlib import metadata
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
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.models.profile import ProfileConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
@ -95,7 +95,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
driver = await select_driver(preset=preset)
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('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,
).show()
return driver if result.get_value() else preset
if result.get_value():
return preset
return driver
@ -113,7 +114,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
if item.value:
driver = item.get_value().value
packages = item.get_value().packages_text()
return f'{tr("Graphics driver")}: {driver}\n{packages}'
return f'Driver: {driver}\n{packages}'
return 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.lib.hardware import GfxDriver, GfxPackage
from archinstall.lib.log import debug, error, info
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.networking import fetch_data_from_url
from archinstall.lib.output import debug, error, info
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
@ -175,9 +175,6 @@ class ProfileHandler:
case GreeterType.PlasmaLoginManager:
packages = ['plasma-login-manager']
service = ['plasmalogin']
case GreeterType.GreetdDms:
packages = ['greetd']
service = ['greetd']
if packages:
install_session.add_additional_packages(packages)
@ -197,26 +194,6 @@ class ProfileHandler:
with open(path, 'w') as file:
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:
debug(f'Installing GFX driver: {driver.value}')

View File

@ -2,16 +2,10 @@ import builtins
import gettext
import json
import os
import tempfile
from dataclasses import dataclass
from pathlib import Path
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
class Language:
@ -20,7 +14,6 @@ class Language:
translation: gettext.NullTranslations
translation_percent: int
translated_lang: str | None
console_font: str | None = None
@property
def display_name(self) -> str:
@ -38,18 +31,10 @@ class Language:
return self.name_en
_DEFAULT_FONT = 'default8x16'
_ENV_FONT = os.environ.get('FONT')
class TranslationHandler:
def __init__(self) -> None:
self._base_pot = 'base.pot'
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._translated_languages = self._get_translations()
@ -58,65 +43,6 @@ class TranslationHandler:
def translated_languages(self) -> list[Language]:
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]:
"""
Load all translated languages and return a list of such
@ -131,7 +57,6 @@ class TranslationHandler:
abbr = mapping_entry['abbr']
lang = mapping_entry['lang']
translated_lang = mapping_entry.get('translated_lang', None)
console_font = mapping_entry.get('console_font', None)
try:
# 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
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)
except FileNotFoundError as err:
raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}")
@ -202,39 +127,12 @@ class TranslationHandler:
except Exception:
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
"""
# The install() call has the side effect of assigning GNUTranslations.gettext to builtins._
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:
"""

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.models.users import User
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem
from archinstall.tui.result import ResultType
from archinstall.tui.ui.menu_item import MenuItem
from archinstall.tui.ui.result import ResultType
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