Compare commits
89 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
b96afcc579 | |
|
|
3bc248b47f | |
|
|
af2120c0e9 | |
|
|
d92a98d3bf | |
|
|
af58671e88 | |
|
|
e17e036f54 | |
|
|
ac4bc442de | |
|
|
5fd2f9b627 | |
|
|
dccd0c8ccf | |
|
|
22bf6e3c35 | |
|
|
b5d323762b | |
|
|
42d9113611 | |
|
|
516a61d8af | |
|
|
e48ca45b0b | |
|
|
74a1066661 | |
|
|
b81fe955f0 | |
|
|
13944f3cca | |
|
|
b95321d38c | |
|
|
3dec5025c3 | |
|
|
f7a6f70fc8 | |
|
|
7fc33c2507 | |
|
|
2de7254b21 | |
|
|
af106eb238 | |
|
|
4265be6e43 | |
|
|
b0b7983af2 | |
|
|
ef1cfe7b56 | |
|
|
7b5dddf34b | |
|
|
dcc38fe9aa | |
|
|
11607851c4 | |
|
|
ba7dbeadfc | |
|
|
b5501d9507 | |
|
|
528c27fbe1 | |
|
|
b18cc57216 | |
|
|
dd34954011 | |
|
|
76629ecc15 | |
|
|
dc7d9cf0a2 | |
|
|
b936fa11e3 | |
|
|
6fefa3b6f5 | |
|
|
3b026cb603 | |
|
|
05560d29af | |
|
|
3223ae4212 | |
|
|
468e33d54f | |
|
|
ccbc53d576 | |
|
|
c4d68855cb | |
|
|
8a929f603f | |
|
|
e1efa34d8a | |
|
|
cb0f3a6eba | |
|
|
1b7a32a3b3 | |
|
|
08cba236f6 | |
|
|
8cc35f41d8 | |
|
|
9010ccf9eb | |
|
|
074dfbb178 | |
|
|
008d303aaf | |
|
|
3795bfc21a | |
|
|
210329ed80 | |
|
|
9e05260df5 | |
|
|
d836ab0a66 | |
|
|
3c4c87bdd6 | |
|
|
efda78cad5 | |
|
|
c8e5c85e20 | |
|
|
bb46e295d1 | |
|
|
de43019094 | |
|
|
6e222bcdd8 | |
|
|
6652197e35 | |
|
|
80ae85cc8b | |
|
|
00834f9c6e | |
|
|
e7d38d0e82 | |
|
|
d1765323d8 | |
|
|
094798b496 | |
|
|
fa54c9d6c9 | |
|
|
b842e54091 | |
|
|
03b245a77a | |
|
|
7d10f9e08b | |
|
|
d80bdb4fad | |
|
|
68d137cdce | |
|
|
82a46c66ca | |
|
|
86dc1bbc12 | |
|
|
cd62eff4a7 | |
|
|
e8ea33c41c | |
|
|
4fcef35af0 | |
|
|
9fdd7eb12e | |
|
|
8fe8d4e35f | |
|
|
02faf9fbb9 | |
|
|
9b9f9d6ac0 | |
|
|
98d72a6b57 | |
|
|
06488dfb9a | |
|
|
29588fadee | |
|
|
3cabe860ae | |
|
|
7ea171369a |
|
|
@ -41,8 +41,8 @@ body:
|
|||
attributes:
|
||||
value: >
|
||||
**Note**: Assuming you have network connectivity,
|
||||
you can easily post the installation log using the following command:
|
||||
`curl -F'file=@/var/log/archinstall/install.log' https://0x0.st`
|
||||
you can easily upload the installation log and get a shareable URL by running:
|
||||
`archinstall share-log`
|
||||
|
||||
- type: textarea
|
||||
id: freeform
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
- run: cat /etc/os-release
|
||||
- run: pacman-key --init
|
||||
- run: pacman --noconfirm -Sy archlinux-keyring
|
||||
- run: ./build_iso.sh
|
||||
- run: ./test_tooling/mkarchiso/build_iso.sh
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: Arch Live ISO
|
||||
|
|
|
|||
|
|
@ -1,28 +1,22 @@
|
|||
#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)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
# 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
|
||||
|
|
@ -41,3 +41,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
default_stages: ['pre-commit']
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.10
|
||||
rev: v0.15.14
|
||||
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: v1.20.1
|
||||
rev: v2.1.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [
|
||||
|
|
@ -41,6 +41,7 @@ repos:
|
|||
additional_dependencies:
|
||||
- pydantic
|
||||
- pytest
|
||||
- hypothesis
|
||||
- cryptography
|
||||
- textual
|
||||
- repo: local
|
||||
|
|
|
|||
2
PKGBUILD
2
PKGBUILD
|
|
@ -5,7 +5,7 @@
|
|||
# Contributor: demostanis worlds <demostanis@protonmail.com>
|
||||
|
||||
pkgname=archinstall
|
||||
pkgver=4.2
|
||||
pkgver=4.3
|
||||
pkgrel=1
|
||||
pkgdesc="Just another guided/automated Arch Linux installer with a twist"
|
||||
arch=(any)
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -101,9 +101,9 @@ If you come across any issues, kindly submit your issue here on GitHub or post y
|
|||
When submitting an issue, please:
|
||||
* Provide the stacktrace of the output if applicable
|
||||
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
|
||||
* To extract the log from the ISO image, one way is to use<br>
|
||||
* To upload the log from the ISO image and get a shareable URL, run<br>
|
||||
```shell
|
||||
curl -F'file=@/var/log/archinstall/install.log' https://0x0.st
|
||||
archinstall share-log
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -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/ovmf/x64/OVMF.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.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/edk2/x64/OVMF_CODE.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.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/ovmf/x64/OVMF.4m.fd \
|
||||
-drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
|
||||
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
|
||||
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.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/ovmf/x64/OVMF.4m.fd \
|
||||
-drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
|
||||
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
|
||||
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.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,6 +219,10 @@ 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.log import debug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
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)
|
||||
|
|
@ -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,6 +21,18 @@ 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,
|
||||
|
|
@ -31,5 +43,7 @@ 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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.log import debug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.models.users import User
|
||||
|
||||
|
||||
class DesktopProfile(Profile):
|
||||
|
|
@ -88,6 +89,11 @@ class DesktopProfile(Profile):
|
|||
for profile in self.current_selection:
|
||||
profile.post_install(install_session)
|
||||
|
||||
@override
|
||||
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||
for profile in self.current_selection:
|
||||
profile.provision(install_session, users)
|
||||
|
||||
@override
|
||||
def install(self, install_session: Installer) -> None:
|
||||
# Install common packages for all desktop environments
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class SeatAccess(Enum):
|
||||
seatd = 'seatd'
|
||||
polkit = 'polkit'
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.models.users import User
|
||||
|
||||
|
||||
class BspwmProfile(Profile):
|
||||
|
|
@ -27,3 +29,11 @@ class BspwmProfile(Profile):
|
|||
@override
|
||||
def default_greeter_type(self) -> GreeterType:
|
||||
return GreeterType.Lightdm
|
||||
|
||||
@override
|
||||
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||
for user in users:
|
||||
install_session.arch_chroot('mkdir -p ~/.config/bspwm ~/.config/sxhkd', run_as=user.username)
|
||||
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/bspwmrc ~/.config/bspwm/', run_as=user.username)
|
||||
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/sxhkdrc ~/.config/sxhkd/', run_as=user.username)
|
||||
install_session.arch_chroot('chmod +x ~/.config/bspwm/bspwmrc', run_as=user.username)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class BudgieProfile(Profile):
|
|||
'Budgie',
|
||||
ProfileType.DesktopEnv,
|
||||
support_gfx_driver=True,
|
||||
display_server=DisplayServerType.Xorg,
|
||||
display_server=DisplayServerType.Wayland,
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -20,6 +20,7 @@ class BudgieProfile(Profile):
|
|||
'budgie',
|
||||
'mate-terminal',
|
||||
'nemo',
|
||||
'nemo-fileroller',
|
||||
'papirus-icon-theme',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.default_profiles.desktops import SeatAccess
|
||||
from archinstall.default_profiles.desktops.utils import select_seat_access
|
||||
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):
|
||||
|
|
@ -49,26 +45,8 @@ 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:
|
||||
await self._select_seat_access()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.default_profiles.desktops import SeatAccess
|
||||
from archinstall.default_profiles.desktops.utils import select_seat_access
|
||||
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):
|
||||
|
|
@ -43,26 +39,8 @@ 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:
|
||||
await self._select_seat_access()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.default_profiles.desktops import SeatAccess
|
||||
from archinstall.default_profiles.desktops.utils import select_seat_access
|
||||
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,
|
||||
|
|
@ -51,26 +47,8 @@ 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:
|
||||
await self._select_seat_access()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
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')
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
recent-windows {
|
||||
highlight {
|
||||
corner-radius 12
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
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; }
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// ! 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Place cursor configuration here.
|
||||
// Example:
|
||||
// cursor {
|
||||
// xcursor-theme "Adwaita"
|
||||
// xcursor-size 24
|
||||
// }
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// ! 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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// Place per-output configuration here.
|
||||
// Example:
|
||||
// output "DP-1" {
|
||||
// mode "2560x1440@165"
|
||||
// position x=0 y=0
|
||||
// scale 1
|
||||
// }
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
// 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"
|
||||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class PlasmaFlavor(StrEnum):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.default_profiles.desktops import SeatAccess
|
||||
from archinstall.default_profiles.desktops.utils import select_seat_access
|
||||
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):
|
||||
|
|
@ -53,26 +49,8 @@ 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:
|
||||
await self._select_seat_access()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
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')
|
||||
|
|
@ -37,6 +37,7 @@ class GreeterType(Enum):
|
|||
Ly = 'ly'
|
||||
CosmicSession = 'cosmic-greeter'
|
||||
PlasmaLoginManager = 'plasma-login-manager'
|
||||
GreetdDms = 'dms-greeter'
|
||||
|
||||
|
||||
class SelectResult(Enum):
|
||||
|
|
|
|||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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
|
||||
|
|
@ -42,3 +43,9 @@ class ApplicationHandler:
|
|||
install_session,
|
||||
app_config.firewall_config,
|
||||
)
|
||||
|
||||
if app_config.fonts_config:
|
||||
FontsApp().install(
|
||||
install_session,
|
||||
app_config.fonts_config,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,13 +10,15 @@ from archinstall.lib.models.application import (
|
|||
BluetoothConfiguration,
|
||||
Firewall,
|
||||
FirewallConfiguration,
|
||||
FontPackage,
|
||||
FontsConfiguration,
|
||||
PowerManagement,
|
||||
PowerManagementConfiguration,
|
||||
PrintServiceConfiguration,
|
||||
)
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
||||
|
|
@ -77,6 +79,13 @@ 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:
|
||||
|
|
@ -115,6 +124,13 @@ 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)
|
||||
|
|
@ -217,3 +233,31 @@ 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
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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
|
||||
|
|
@ -13,23 +14,29 @@ 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.ui.components import tui
|
||||
from archinstall.tui.components import tui
|
||||
|
||||
|
||||
class SubCommand(Enum):
|
||||
SHARE_LOG = 'share-log'
|
||||
|
||||
|
||||
@p_dataclass
|
||||
|
|
@ -55,6 +62,83 @@ 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:
|
||||
|
|
@ -71,7 +155,7 @@ class ArchConfig:
|
|||
auth_config: AuthenticationConfiguration | None = None
|
||||
swap: ZramConfiguration | None = None
|
||||
hostname: str = 'archlinux'
|
||||
kernels: list[str] = field(default_factory=lambda: ['linux'])
|
||||
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
|
||||
ntp: bool = True
|
||||
packages: list[str] = field(default_factory=list)
|
||||
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
|
||||
|
|
@ -79,58 +163,84 @@ class ArchConfig:
|
|||
services: list[str] = field(default_factory=list)
|
||||
custom_commands: list[str] = field(default_factory=list)
|
||||
|
||||
def unsafe_config(self) -> dict[str, Any]:
|
||||
config: dict[str, list[UserSerialization] | str | None] = {}
|
||||
def unsafe_config(self) -> dict[ArchConfigType, Any]:
|
||||
config: dict[ArchConfigType, list[UserSerialization] | str | None] = {}
|
||||
|
||||
if self.auth_config:
|
||||
if self.auth_config.users:
|
||||
config['users'] = [user.json() for user in self.auth_config.users]
|
||||
config[ArchConfigType.USERS] = [user.json() for user in self.auth_config.users]
|
||||
|
||||
if self.auth_config.root_enc_password:
|
||||
config['root_enc_password'] = self.auth_config.root_enc_password.enc_password
|
||||
config[ArchConfigType.ROOT_ENC_PASSWORD] = self.auth_config.root_enc_password.enc_password
|
||||
|
||||
if self.disk_config:
|
||||
disk_encryption = self.disk_config.disk_encryption
|
||||
if disk_encryption and disk_encryption.encryption_password:
|
||||
config['encryption_password'] = disk_encryption.encryption_password.plaintext
|
||||
config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext
|
||||
|
||||
return config
|
||||
|
||||
def safe_config(self) -> dict[str, Any]:
|
||||
config: Any = {
|
||||
'version': self.version,
|
||||
'script': self.script,
|
||||
'archinstall-language': self.archinstall_language.json(),
|
||||
'hostname': self.hostname,
|
||||
'kernels': self.kernels,
|
||||
'ntp': self.ntp,
|
||||
'packages': self.packages,
|
||||
'pacman_config': self.pacman_config.json(),
|
||||
'swap': self.swap,
|
||||
'timezone': self.timezone,
|
||||
'services': self.services,
|
||||
'custom_commands': self.custom_commands,
|
||||
'bootloader_config': self.bootloader_config.json() if self.bootloader_config else None,
|
||||
'app_config': self.app_config.json() if self.app_config else None,
|
||||
'auth_config': self.auth_config.json() if self.auth_config else None,
|
||||
def safe_config(self) -> dict[ArchConfigType, Any]:
|
||||
base_config: dict[ArchConfigType, Any] = {
|
||||
ArchConfigType.VERSION: self.version,
|
||||
ArchConfigType.SCRIPT: self.script,
|
||||
ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(),
|
||||
}
|
||||
|
||||
if self.locale_config:
|
||||
config['locale_config'] = self.locale_config.json()
|
||||
base_config.update(self.plain_cfg())
|
||||
sub_config = self.sub_cfg()
|
||||
|
||||
if self.disk_config:
|
||||
config['disk_config'] = self.disk_config.json()
|
||||
for config_type, value in sub_config.items():
|
||||
if not hasattr(value, 'json'):
|
||||
raise ValueError(f'Config value for {config_type} must implement json() method')
|
||||
base_config[config_type] = value.json()
|
||||
|
||||
if self.profile_config:
|
||||
config['profile_config'] = self.profile_config.json()
|
||||
return base_config
|
||||
|
||||
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:
|
||||
config['mirror_config'] = self.mirror_config.json()
|
||||
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
|
||||
|
||||
if self.profile_config:
|
||||
cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config
|
||||
|
||||
if self.network_config:
|
||||
config['network_config'] = self.network_config.json()
|
||||
cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config
|
||||
|
||||
return config
|
||||
if self.app_config:
|
||||
cfg[ArchConfigType.APP_CONFIG] = self.app_config
|
||||
|
||||
return cfg
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
|
||||
|
|
@ -143,6 +253,7 @@ 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', '')
|
||||
|
|
@ -260,13 +371,13 @@ class ArchConfig:
|
|||
class ArchConfigHandler:
|
||||
def __init__(self) -> None:
|
||||
self._parser: ArgumentParser = self._define_arguments()
|
||||
args: Arguments = self._parse_args()
|
||||
self._args = args
|
||||
self._add_sub_parsers()
|
||||
|
||||
self._args: Arguments = self._parse_args()
|
||||
config = self._parse_config()
|
||||
|
||||
try:
|
||||
self._config = ArchConfig.from_config(config, args)
|
||||
self._config = ArchConfig.from_config(config, self._args)
|
||||
self._config.version = get_version()
|
||||
except ValueError as err:
|
||||
warn(str(err))
|
||||
|
|
@ -292,8 +403,13 @@ 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',
|
||||
|
|
@ -429,7 +545,6 @@ class ArchConfigHandler:
|
|||
default=False,
|
||||
help='Enabled verbose options',
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def _parse_args(self) -> Arguments:
|
||||
|
|
|
|||
|
|
@ -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 None
|
||||
return
|
||||
|
||||
content = file.read_text().splitlines()
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ class AuthenticationHandler:
|
|||
|
||||
install_session.pacman.strap('pam-u2f')
|
||||
|
||||
print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}'))
|
||||
print(tr('Setting up U2F login: {}').format(u2f_config.u2f_login_method.value))
|
||||
|
||||
# https://developers.yubico.com/pam-u2f/
|
||||
u2f_auth_file = install_session.target / 'etc/u2f_mappings'
|
||||
|
|
|
|||
|
|
@ -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.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.lib.utils.format import as_table
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
||||
|
|
@ -65,7 +65,7 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
|||
users: list[User] | None = item.value
|
||||
|
||||
if users:
|
||||
return FormattedOutput.as_table(users)
|
||||
return as_table(users)
|
||||
return None
|
||||
|
||||
def _prev_root_pwd(self, item: MenuItem) -> str | None:
|
||||
|
|
|
|||
|
|
@ -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.output import error
|
||||
from archinstall.lib.log import error
|
||||
|
||||
|
||||
class Boot:
|
||||
|
|
|
|||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
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
|
||||
|
|
@ -11,7 +11,7 @@ from types import TracebackType
|
|||
from typing import Any, Self, override
|
||||
|
||||
from archinstall.lib.exceptions import RequirementError, SysCallError
|
||||
from archinstall.lib.output import debug, error, logger
|
||||
from archinstall.lib.log 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: float | None = None
|
||||
self.ended: float | None = None
|
||||
self.started = False
|
||||
self.ended = False
|
||||
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 self.ended is None:
|
||||
if self.started and not self.ended:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
@ -173,11 +173,11 @@ class SysCommandWorker:
|
|||
self.peak(output)
|
||||
self._trace_log += output
|
||||
except OSError:
|
||||
self.ended = time.time()
|
||||
self.ended = True
|
||||
break
|
||||
|
||||
if self.ended or (not got_output and not _pid_exists(self.pid)):
|
||||
self.ended = time.time()
|
||||
self.ended = True
|
||||
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 = time.time()
|
||||
self.started = True
|
||||
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ from typing import Any
|
|||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from archinstall.lib.args import ArchConfig
|
||||
from archinstall.lib.args import ArchConfig, ArchConfigType
|
||||
from archinstall.lib.crypt import encrypt
|
||||
from archinstall.lib.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.output import debug, logger, warn
|
||||
from archinstall.lib.models.network import NetworkConfiguration
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.lib.utils.format import as_key_value_pair
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class ConfigurationOutput:
|
||||
|
|
@ -43,25 +45,51 @@ class ConfigurationOutput:
|
|||
def user_config_to_json(self) -> str:
|
||||
config = self._config.safe_config()
|
||||
|
||||
adapter = TypeAdapter(dict[str, Any])
|
||||
adapter = TypeAdapter(dict[ArchConfigType, Any])
|
||||
python_dict = adapter.dump_python(config)
|
||||
return json.dumps(python_dict, indent=4, sort_keys=True)
|
||||
|
||||
def user_credentials_to_json(self) -> str:
|
||||
config = self._config.unsafe_config()
|
||||
cfg = self._config.unsafe_config()
|
||||
|
||||
adapter = TypeAdapter(dict[str, Any])
|
||||
python_dict = adapter.dump_python(config)
|
||||
adapter = TypeAdapter(dict[ArchConfigType, Any])
|
||||
python_dict = adapter.dump_python(cfg)
|
||||
return json.dumps(python_dict, indent=4, sort_keys=True)
|
||||
|
||||
def write_debug(self) -> None:
|
||||
debug(' -- Chosen configuration --')
|
||||
debug(self.user_config_to_json())
|
||||
|
||||
async def confirm_config(self) -> bool:
|
||||
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:
|
||||
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())
|
||||
|
||||
|
|
@ -79,6 +107,22 @@ 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:
|
||||
|
|
|
|||
|
|
@ -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.output import debug
|
||||
from archinstall.lib.log import debug
|
||||
|
||||
libcrypt = ctypes.CDLL('libcrypt.so')
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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,
|
||||
|
|
@ -35,7 +36,6 @@ 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 FilesystemType.FAT12 | FilesystemType.FAT16 | FilesystemType.FAT32:
|
||||
case _ if fs_type.is_fat():
|
||||
mkfs_type = 'fat'
|
||||
# Set FAT size
|
||||
options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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
|
||||
|
|
@ -35,10 +36,10 @@ from archinstall.lib.models.device import (
|
|||
Unit,
|
||||
_DeviceInfo,
|
||||
)
|
||||
from archinstall.lib.output import FormattedOutput, debug
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.lib.utils.format import as_table
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -221,7 +222,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
|
|||
|
||||
for mod in device_mods:
|
||||
# create partition table
|
||||
partition_table = FormattedOutput.as_table(mod.partitions)
|
||||
partition_table = as_table(mod.partitions)
|
||||
|
||||
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
|
||||
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
|
||||
|
|
@ -230,7 +231,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 += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
|
||||
output_btrfs += as_table(partition.btrfs_subvols) + '\n'
|
||||
|
||||
output = output_partition + output_btrfs
|
||||
return output.rstrip()
|
||||
|
|
@ -246,12 +247,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 = FormattedOutput.as_table(vol_gp.pvs)
|
||||
pv_table = as_table(vol_gp.pvs)
|
||||
output += '{}:\n{}'.format(tr('Physical volumes'), pv_table)
|
||||
|
||||
output += f'\nVolume Group: {vol_gp.name}'
|
||||
|
||||
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
|
||||
lvm_volumes = as_table(vol_gp.volumes)
|
||||
output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes)
|
||||
|
||||
return output
|
||||
|
|
@ -280,7 +281,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
|
|||
if enc_config.encryption_password:
|
||||
output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n'
|
||||
|
||||
if enc_type != EncryptionType.NoEncryption:
|
||||
if enc_type != EncryptionType.NO_ENCRYPTION:
|
||||
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
|
||||
|
||||
if enc_config.partitions:
|
||||
|
|
@ -302,7 +303,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 FormattedOutput.as_table(dev.partition_infos)
|
||||
return as_table(dev.partition_infos)
|
||||
return None
|
||||
|
||||
if preset is None:
|
||||
|
|
@ -503,7 +504,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'),
|
||||
|
|
@ -655,7 +656,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,
|
||||
|
|
@ -680,7 +681,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'),
|
||||
|
|
@ -765,7 +766,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('/'),
|
||||
|
|
@ -787,7 +788,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'),
|
||||
|
|
|
|||
|
|
@ -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.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.lib.utils.format import as_table
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.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.NoEncryption:
|
||||
if enc_type and enc_type != EncryptionType.NO_ENCRYPTION:
|
||||
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.LvmOnLuks]:
|
||||
if enc_type and enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS]:
|
||||
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.LuksOnLvm:
|
||||
if enc_type and enc_type == EncryptionType.LUKS_ON_LVM:
|
||||
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.LvmOnLuks] and enc_partitions:
|
||||
if enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and enc_partitions:
|
||||
enc_lvm_vols = []
|
||||
|
||||
if enc_type == EncryptionType.LuksOnLvm:
|
||||
if enc_type == EncryptionType.LUKS_ON_LVM:
|
||||
enc_partitions = []
|
||||
|
||||
if enc_type != EncryptionType.NoEncryption and enc_password and (enc_partitions or enc_lvm_vols):
|
||||
if enc_type != EncryptionType.NO_ENCRYPTION 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 += FormattedOutput.as_table(item.value)
|
||||
output += 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 += FormattedOutput.as_table(item.value)
|
||||
output += 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.NoEncryption:
|
||||
if iter_time and enc_type != EncryptionType.NO_ENCRYPTION:
|
||||
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.LvmOnLuks, EncryptionType.LuksOnLvm]
|
||||
options = [EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM]
|
||||
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(partitions)
|
||||
group = MenuItemGroup.from_objects(avail_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(f'Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000') + '\n'
|
||||
header += tr('Default: {}ms, Recommended range: 1000-60000').format(DEFAULT_ITER_TIME) + '\n'
|
||||
|
||||
def validate_iter_time(value: str) -> str | None:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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,
|
||||
|
|
@ -27,7 +28,6 @@ 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.LvmOnLuks:
|
||||
if enc_config.encryption_type == EncryptionType.LVM_ON_LUKS:
|
||||
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.LuksOnLvm:
|
||||
elif enc_config.encryption_type == EncryptionType.LUKS_ON_LVM:
|
||||
self._setup_lvm(lvm_config)
|
||||
enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False)
|
||||
self._format_lvm_vols(lvm_config, enc_vols)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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,
|
||||
|
|
@ -17,7 +18,6 @@ from archinstall.lib.models.device import (
|
|||
Size,
|
||||
Unit,
|
||||
)
|
||||
from archinstall.lib.output import debug
|
||||
|
||||
|
||||
def _lvm_info(
|
||||
|
|
|
|||
|
|
@ -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.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.lib.utils.format import as_table
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.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 = FormattedOutput.as_table([free_space])
|
||||
free_space_table = 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,
|
||||
|
|
|
|||
|
|
@ -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.ui.result import ResultType
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class SubvolumeMenu(ListManager[SubvolumeModification]):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.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: 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'
|
||||
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'
|
||||
|
||||
result = await Selection[Language](
|
||||
header=title,
|
||||
|
|
|
|||
|
|
@ -3,29 +3,24 @@ 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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
async def select_kernel(preset: list[str] = []) -> list[str]:
|
||||
async def select_kernel(preset: list[Kernel] = []) -> list[Kernel]:
|
||||
"""
|
||||
Asks the user to select a kernel for system.
|
||||
|
||||
:return: The string as a selected kernel
|
||||
:rtype: string
|
||||
"""
|
||||
kernels = ['linux', 'linux-lts', 'linux-zen', 'linux-hardened']
|
||||
default_kernel = 'linux'
|
||||
group = MenuItemGroup.from_enum(Kernel, sort_items=True, preset=preset)
|
||||
group.set_default_by_value(DEFAULT_KERNEL)
|
||||
group.set_focus_by_value(DEFAULT_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](
|
||||
result = await Selection[Kernel](
|
||||
group,
|
||||
header=tr('Select which kernel(s) to install'),
|
||||
allow_skip=True,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ 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.configuration import save_config
|
||||
from archinstall.lib.bootloader.utils import validate_bootloader_layout
|
||||
from archinstall.lib.configuration import ConfigurationOutput, 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
|
||||
|
|
@ -17,21 +18,22 @@ 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, FilesystemType, PartitionModification
|
||||
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, 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.tui.ui.components import tui
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.lib.utils.format import as_table
|
||||
from archinstall.tui.components import tui
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
|
||||
|
||||
class GlobalMenu(AbstractMenu[None]):
|
||||
|
|
@ -102,7 +104,7 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
),
|
||||
MenuItem(
|
||||
text=tr('Kernels'),
|
||||
value=['linux'],
|
||||
value=[DEFAULT_KERNEL],
|
||||
action=select_kernel,
|
||||
preview_action=self._prev_kernel,
|
||||
mandatory=True,
|
||||
|
|
@ -297,7 +299,7 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
if item.value:
|
||||
network_config: NetworkConfiguration = item.value
|
||||
if network_config.type == NicType.MANUAL:
|
||||
output = FormattedOutput.as_table(network_config.nics)
|
||||
output = as_table(network_config.nics)
|
||||
else:
|
||||
output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'
|
||||
|
||||
|
|
@ -319,7 +321,7 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
|
||||
|
||||
if auth_config.users:
|
||||
output += FormattedOutput.as_table(auth_config.users) + '\n'
|
||||
output += as_table(auth_config.users) + '\n'
|
||||
|
||||
if auth_config.u2f_config:
|
||||
u2f_config = auth_config.u2f_config
|
||||
|
|
@ -460,8 +462,6 @@ 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,16 +486,11 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
if efi_partition is None:
|
||||
return 'EFI system partition (ESP) not found'
|
||||
|
||||
if efi_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
|
||||
if efi_partition.fs_type is None or not efi_partition.fs_type.is_fat():
|
||||
return 'ESP must be formatted as a FAT filesystem'
|
||||
|
||||
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'
|
||||
if failure := validate_bootloader_layout(bootloader_config, disk_config):
|
||||
return failure.description
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -507,9 +502,13 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
return text[:-1] # remove last new line
|
||||
|
||||
if error := self._validate_bootloader():
|
||||
return tr(f'Invalid configuration: {error}')
|
||||
return tr('Invalid configuration: {}').format(error)
|
||||
|
||||
return None
|
||||
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')
|
||||
|
||||
def _prev_profile(self, item: MenuItem) -> str | None:
|
||||
profile_config: ProfileConfiguration | None = item.value
|
||||
|
|
@ -613,7 +612,7 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
|
||||
if mirror_config.custom_repositories:
|
||||
title = tr('Custom repositories')
|
||||
table = FormattedOutput.as_table(mirror_config.custom_repositories)
|
||||
table = as_table(mirror_config.custom_repositories)
|
||||
output += f'{title}:\n\n{table}'
|
||||
|
||||
return output.strip()
|
||||
|
|
|
|||
|
|
@ -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,6 +68,19 @@ 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'
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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
|
||||
|
|
@ -28,9 +29,10 @@ 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
|
||||
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
|
||||
from archinstall.lib.models.device import (
|
||||
DiskEncryption,
|
||||
DiskLayoutConfiguration,
|
||||
|
|
@ -47,10 +49,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
|
||||
|
|
@ -59,7 +61,12 @@ 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)
|
||||
__packages__ = ['base', 'sudo', 'linux-firmware', 'linux', 'linux-lts', 'linux-zen', 'linux-hardened']
|
||||
# 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]
|
||||
|
||||
# Additional packages that are installed if the user is running the Live ISO with accessibility tools enabled
|
||||
__accessibility_packages__ = ['brltty', 'espeakup', 'alsa-utils']
|
||||
|
|
@ -78,11 +85,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__[:3]
|
||||
self.kernels = kernels or ['linux']
|
||||
self._base_packages = base_packages or __packages__[:4]
|
||||
self.kernels = kernels or [DEFAULT_KERNEL.value]
|
||||
self._disk_config = disk_config
|
||||
|
||||
self._disk_encryption = disk_config.disk_encryption or DiskEncryption(EncryptionType.NoEncryption)
|
||||
self._disk_encryption = disk_config.disk_encryption or DiskEncryption(EncryptionType.NO_ENCRYPTION)
|
||||
self.target: Path = target
|
||||
|
||||
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
|
||||
|
|
@ -182,10 +189,10 @@ class Installer:
|
|||
if not skip_ntp:
|
||||
info(tr('Waiting for time sync (timedatectl show) to complete.'))
|
||||
|
||||
started_wait = time.time()
|
||||
started_wait = time.monotonic()
|
||||
notified = False
|
||||
while True:
|
||||
if not notified and time.time() - started_wait > 5:
|
||||
if not notified and time.monotonic() - started_wait > 5:
|
||||
notified = True
|
||||
warn(tr('Time synchronization not completing, while you wait - check the docs for workarounds: https://archinstall.readthedocs.io/'))
|
||||
|
||||
|
|
@ -254,16 +261,16 @@ class Installer:
|
|||
luks_handlers: dict[Any, Luks2] = {}
|
||||
|
||||
match self._disk_encryption.encryption_type:
|
||||
case EncryptionType.NoEncryption:
|
||||
case EncryptionType.NO_ENCRYPTION:
|
||||
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.LvmOnLuks:
|
||||
case EncryptionType.LVM_ON_LUKS:
|
||||
luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions)
|
||||
self._import_lvm()
|
||||
self._mount_lvm_layout(luks_handlers)
|
||||
case EncryptionType.LuksOnLvm:
|
||||
case EncryptionType.LUKS_ON_LVM:
|
||||
self._import_lvm()
|
||||
luks_handlers = self._prepare_luks_lvm(self._disk_encryption.lvm_volumes)
|
||||
self._mount_lvm_layout(luks_handlers)
|
||||
|
|
@ -368,7 +375,12 @@ 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
|
||||
mount(part_mod.dev_path, target, options=part_mod.mount_options)
|
||||
options = part_mod.mount_options
|
||||
|
||||
if part_mod.is_efi():
|
||||
options = list(dict.fromkeys(options + ['fmask=0077', 'dmask=0077']))
|
||||
|
||||
mount(part_mod.dev_path, target, options=options)
|
||||
elif part_mod.fs_type == FilesystemType.BTRFS:
|
||||
# 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]
|
||||
|
|
@ -395,7 +407,7 @@ class Installer:
|
|||
|
||||
def _mount_luks_partition(self, part_mod: PartitionModification, luks_handler: Luks2) -> None:
|
||||
if not luks_handler.mapper_dev:
|
||||
return None
|
||||
return
|
||||
|
||||
if part_mod.fs_type == FilesystemType.BTRFS and part_mod.btrfs_subvols:
|
||||
# Only mount BTRFS subvolumes that have mountpoints specified
|
||||
|
|
@ -433,11 +445,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.LuksOnLvm:
|
||||
case EncryptionType.LUKS_ON_LVM:
|
||||
self._generate_key_file_lvm_volumes()
|
||||
case EncryptionType.LvmOnLuks:
|
||||
case EncryptionType.LVM_ON_LUKS:
|
||||
# currently LvmOnLuks only supports a single
|
||||
# partitioning layout (boot + partition)
|
||||
# so we won't need any keyfile generation atm
|
||||
|
|
@ -505,10 +517,9 @@ 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:
|
||||
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)
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -739,6 +750,9 @@ 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)
|
||||
|
||||
|
|
@ -899,7 +913,7 @@ class Installer:
|
|||
if vol.fs_type is not None:
|
||||
self._prepare_fs_type(vol.fs_type, vol.mountpoint)
|
||||
|
||||
types = (EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm)
|
||||
types = (EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM)
|
||||
if self._disk_encryption.encryption_type in types:
|
||||
self._prepare_encrypt(lvm)
|
||||
else:
|
||||
|
|
@ -987,17 +1001,7 @@ class Installer:
|
|||
}
|
||||
|
||||
for config_name, mountpoint in snapper.items():
|
||||
command = [
|
||||
'arch-chroot',
|
||||
'-S',
|
||||
str(self.target),
|
||||
'snapper',
|
||||
'--no-dbus',
|
||||
'-c',
|
||||
config_name,
|
||||
'create-config',
|
||||
mountpoint,
|
||||
]
|
||||
command = self._chroot_argv('snapper', '--no-dbus', '-c', config_name, 'create-config', mountpoint)
|
||||
|
||||
try:
|
||||
SysCommand(command, peek_output=True)
|
||||
|
|
@ -1137,7 +1141,7 @@ class Installer:
|
|||
kernel_parameters = []
|
||||
|
||||
match self._disk_encryption.encryption_type:
|
||||
case EncryptionType.LvmOnLuks:
|
||||
case EncryptionType.LVM_ON_LUKS:
|
||||
if not lvm.vg_name:
|
||||
raise ValueError(f'Unable to determine VG name for {lvm.name}')
|
||||
|
||||
|
|
@ -1154,7 +1158,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.LuksOnLvm:
|
||||
case EncryptionType.LUKS_ON_LVM:
|
||||
uuid = self._get_luks_uuid_from_mapper_dev(lvm.mapper_path)
|
||||
|
||||
if self._disk_encryption.hsm_device:
|
||||
|
|
@ -1163,7 +1167,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.NoEncryption:
|
||||
case EncryptionType.NO_ENCRYPTION:
|
||||
debug(f'Identifying root lvm by mapper device: {lvm.dev_path}')
|
||||
kernel_parameters.append(f'root={lvm.safe_dev_path}')
|
||||
|
||||
|
|
@ -1336,13 +1340,7 @@ class Installer:
|
|||
|
||||
boot_dir = Path('/boot')
|
||||
|
||||
command = [
|
||||
'arch-chroot',
|
||||
'-S',
|
||||
str(self.target),
|
||||
'grub-install',
|
||||
'--debug',
|
||||
]
|
||||
command = self._chroot_argv('grub-install', '--debug')
|
||||
|
||||
if SysInfo.has_uefi():
|
||||
if not efi_partition:
|
||||
|
|
@ -1467,6 +1465,14 @@ 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)
|
||||
|
|
@ -1477,20 +1483,16 @@ 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(efi_dir_path)
|
||||
(limine_path / file).copy_into(efi_dir_path)
|
||||
except Exception as err:
|
||||
raise DiskError(f'Failed to install Limine in {self.target}{efi_partition.mountpoint}: {err}')
|
||||
|
||||
|
|
@ -1539,7 +1541,7 @@ class Installer:
|
|||
|
||||
try:
|
||||
# The `limine-bios.sys` file contains stage 3 code.
|
||||
(limine_path / 'limine-bios.sys').copy(boot_limine_path)
|
||||
(limine_path / 'limine-bios.sys').copy_into(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)
|
||||
|
|
@ -1757,6 +1759,7 @@ 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}')
|
||||
|
|
@ -1780,11 +1783,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):
|
||||
image = self.target / m.group(2)
|
||||
image.unlink(missing_ok=True)
|
||||
config[index] = '#' + m.group(1)
|
||||
if not keep_initramfs:
|
||||
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)
|
||||
|
|
@ -1803,7 +1806,12 @@ 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:
|
||||
|
|
@ -1852,7 +1860,13 @@ class Installer:
|
|||
bootloader_removable = False
|
||||
|
||||
if uki_enabled:
|
||||
self._config_uki(root, efi_partition)
|
||||
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)
|
||||
|
||||
match bootloader:
|
||||
case Bootloader.Systemd:
|
||||
|
|
@ -1918,16 +1932,17 @@ class Installer:
|
|||
if not handled_by_plugin:
|
||||
info(f'Creating user {user.username}')
|
||||
|
||||
cmd = 'useradd -m'
|
||||
cmd = self._chroot_argv('useradd', '-m')
|
||||
|
||||
if user.sudo:
|
||||
cmd += ' -G wheel'
|
||||
cmd += ['-G', 'wheel']
|
||||
|
||||
cmd += f' {user.username}'
|
||||
cmd += ['--', user.username]
|
||||
|
||||
try:
|
||||
self.arch_chroot(cmd)
|
||||
except SysCallError as err:
|
||||
run(cmd)
|
||||
except CalledProcessError as err:
|
||||
debug(f'Error creating user {user.username}: {err}')
|
||||
raise SystemError(f'Could not create user inside installation: {err}')
|
||||
|
||||
for plugin in plugins.values():
|
||||
|
|
@ -1938,7 +1953,11 @@ class Installer:
|
|||
self.set_user_password(user)
|
||||
|
||||
for group in user.groups:
|
||||
self.arch_chroot(f'gpasswd -a {user.username} {group}')
|
||||
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}')
|
||||
|
||||
if user.sudo:
|
||||
self.enable_sudo(user)
|
||||
|
|
@ -1953,7 +1972,7 @@ class Installer:
|
|||
return False
|
||||
|
||||
input_data = f'{user.username}:{enc_password}'.encode()
|
||||
cmd = ['arch-chroot', '-S', str(self.target), 'chpasswd', '--encrypted']
|
||||
cmd = self._chroot_argv('chpasswd', '--encrypted')
|
||||
|
||||
try:
|
||||
run(cmd, input_data=input_data)
|
||||
|
|
@ -1965,29 +1984,31 @@ 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:
|
||||
self.arch_chroot(f'sh -c "chsh -s {shell} {user}"')
|
||||
run(cmd)
|
||||
return True
|
||||
except SysCallError:
|
||||
except CalledProcessError as err:
|
||||
debug(f'Error setting user shell: {err}')
|
||||
return False
|
||||
|
||||
def chown(self, owner: str, path: str, options: list[str] = []) -> bool:
|
||||
cleaned_path = path.replace("'", "\\'")
|
||||
def chown(self, owner: str, path: str, options: list[str] | None = None) -> bool:
|
||||
options = options or []
|
||||
cmd = self._chroot_argv('chown', *options, '--', owner, path)
|
||||
try:
|
||||
self.arch_chroot(f"sh -c 'chown {' '.join(options)} {owner} {cleaned_path}'")
|
||||
run(cmd)
|
||||
return True
|
||||
except SysCallError:
|
||||
except CalledProcessError as err:
|
||||
debug(f'Error changing ownership of {path}: {err}')
|
||||
return False
|
||||
|
||||
def set_vconsole(self, locale_config: LocaleConfiguration) -> None:
|
||||
# use the already set kb layout
|
||||
kb_vconsole: str = locale_config.kb_layout
|
||||
# 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
|
||||
font_vconsole = locale_config.console_font
|
||||
|
||||
font_vconsole = 'default8x16'
|
||||
if font_vconsole.startswith('ter-'):
|
||||
self.pacman.strap(['terminus-font'])
|
||||
|
||||
# Ensure /etc exists
|
||||
vconsole_dir: Path = self.target / 'etc'
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.lib.locale.utils import list_keyboard_languages, list_locales, set_kb_layout
|
||||
from archinstall.lib.locale.utils import list_console_fonts, 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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
|
||||
|
|
@ -47,6 +47,13 @@ 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
|
||||
|
|
@ -140,3 +147,25 @@ 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')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
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.output import error
|
||||
from archinstall.lib.log import error
|
||||
from archinstall.lib.utils.util import running_from_iso
|
||||
|
||||
|
||||
|
|
@ -26,6 +29,13 @@ 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(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
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
|
||||
|
|
@ -2,13 +2,12 @@ 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.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
|
||||
from archinstall.tui.components import InstanceRunnable
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
CONFIG_KEY = '__config__'
|
||||
|
||||
|
|
@ -154,7 +153,7 @@ class AbstractSubMenu[ValueT](AbstractMenu[ValueT]):
|
|||
auto_cursor: bool = True,
|
||||
allow_reset: bool = False,
|
||||
):
|
||||
back_text = f'{Chars.Right_arrow} ' + tr('Back')
|
||||
back_text = '← ' + tr('Back')
|
||||
item_group.add_item(MenuItem(text=back_text))
|
||||
|
||||
super().__init__(
|
||||
|
|
|
|||
|
|
@ -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.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
|
||||
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
|
||||
|
||||
|
||||
class Selection[ValueT]:
|
||||
|
|
@ -20,6 +20,7 @@ class Selection[ValueT]:
|
|||
preview_location: Literal['right', 'bottom'] | None = None,
|
||||
multi: bool = False,
|
||||
enable_filter: bool = False,
|
||||
wrap_preview: bool = False,
|
||||
):
|
||||
self._header = header
|
||||
self._title = title
|
||||
|
|
@ -29,6 +30,7 @@ class Selection[ValueT]:
|
|||
self._preview_location = preview_location
|
||||
self._multi = multi
|
||||
self._enable_filter = enable_filter
|
||||
self._wrap_preview = wrap_preview
|
||||
|
||||
async def show(self) -> Result[ValueT]:
|
||||
if self._multi:
|
||||
|
|
@ -39,6 +41,7 @@ class Selection[ValueT]:
|
|||
allow_reset=self._allow_reset,
|
||||
preview_location=self._preview_location,
|
||||
enable_filter=self._enable_filter,
|
||||
wrap_preview=self._wrap_preview,
|
||||
).run()
|
||||
else:
|
||||
result = await OptionListScreen[ValueT](
|
||||
|
|
@ -49,6 +52,7 @@ class Selection[ValueT]:
|
|||
allow_reset=self._allow_reset,
|
||||
preview_location=self._preview_location,
|
||||
enable_filter=self._enable_filter,
|
||||
wrap_preview=self._wrap_preview,
|
||||
).run()
|
||||
|
||||
if result.type_ == ResultType.Reset:
|
||||
|
|
|
|||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class ListManager[ValueT]:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from archinstall.lib.output import FormattedOutput
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.lib.utils.format import as_table
|
||||
from archinstall.tui.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 = FormattedOutput.as_table(data)
|
||||
table = as_table(data)
|
||||
rows = table.split('\n')
|
||||
|
||||
# these are the header rows of the table
|
||||
|
|
|
|||
|
|
@ -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.ui.components import InputInfo, InputInfoType, tui
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.components import InputInfo, InputInfoType, tui
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
async def get_password(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.lib.utils.format import as_table
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.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 respository name'),
|
||||
header=tr('Enter a repository 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 = FormattedOutput.as_table(custom_mirrors)
|
||||
output = as_table(custom_mirrors)
|
||||
return output.strip()
|
||||
|
||||
def _prev_custom_servers(self, item: MenuItem) -> str | None:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import StrEnum, auto
|
||||
from typing import Any, NotRequired, Self, TypedDict
|
||||
from typing import Any, NotRequired, Self, TypedDict, override
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
class PowerManagement(StrEnum):
|
||||
|
|
@ -39,6 +42,31 @@ 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'
|
||||
|
|
@ -53,6 +81,7 @@ class ApplicationSerialization(TypedDict):
|
|||
power_management_config: NotRequired[PowerManagementConfigSerialization]
|
||||
print_service_config: NotRequired[PrintServiceConfigSerialization]
|
||||
firewall_config: NotRequired[FirewallConfigSerialization]
|
||||
fonts_config: NotRequired[FontsConfigSerialization]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -127,8 +156,20 @@ 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:
|
||||
class ZramConfiguration(SubConfig):
|
||||
enabled: bool
|
||||
algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD
|
||||
|
||||
|
|
@ -141,14 +182,34 @@ class ZramConfiguration:
|
|||
algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value))
|
||||
return cls(enabled=enabled, algorithm=ZramAlgorithm(algo))
|
||||
|
||||
@override
|
||||
def json(self) -> dict[str, bool | str]:
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'algorithm': self.algorithm.value,
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str] | None:
|
||||
out: list[str] = []
|
||||
|
||||
if self.enabled:
|
||||
out.append(tr('Zram enabled'))
|
||||
|
||||
out.append(tr('Zram algorithm {}').format(self.algorithm))
|
||||
return out
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationConfiguration:
|
||||
class ApplicationConfiguration(SubConfig):
|
||||
bluetooth_config: BluetoothConfiguration | None = None
|
||||
audio_config: AudioConfiguration | None = None
|
||||
power_management_config: PowerManagementConfiguration | None = None
|
||||
print_service_config: PrintServiceConfiguration | None = None
|
||||
firewall_config: FirewallConfiguration | None = None
|
||||
fonts_config: FontsConfiguration | None = None
|
||||
|
||||
@classmethod
|
||||
def parse_arg(
|
||||
|
|
@ -177,8 +238,12 @@ class ApplicationConfiguration:
|
|||
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 = {}
|
||||
|
||||
|
|
@ -197,4 +262,32 @@ class ApplicationConfiguration:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, NotRequired, Self, TypedDict
|
||||
from typing import Any, NotRequired, Self, TypedDict, override
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.models.users import Password, User
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ class U2FLoginConfiguration:
|
|||
|
||||
|
||||
@dataclass
|
||||
class AuthenticationConfiguration:
|
||||
class AuthenticationConfiguration(SubConfig):
|
||||
root_enc_password: Password | None = None
|
||||
users: list[User] = field(default_factory=list)
|
||||
u2f_config: U2FLoginConfiguration | None = None
|
||||
|
|
@ -75,6 +76,7 @@ class AuthenticationConfiguration:
|
|||
|
||||
return auth_config
|
||||
|
||||
@override
|
||||
def json(self) -> AuthenticationSerialization:
|
||||
config: AuthenticationSerialization = {}
|
||||
|
||||
|
|
@ -83,6 +85,21 @@ class AuthenticationConfiguration:
|
|||
|
||||
return config
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out: list[str] = []
|
||||
|
||||
if self.root_enc_password:
|
||||
out.append(tr('Root password set'))
|
||||
|
||||
if self.users:
|
||||
out.append(tr('Configured {} user(s)').format(len(self.users)))
|
||||
|
||||
if self.u2f_config:
|
||||
out.append(tr('U2F set up'))
|
||||
|
||||
return out
|
||||
|
||||
def has_superuser(self) -> bool:
|
||||
return any(u.sudo for u in self.users)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Self
|
||||
from typing import Any, Self, override
|
||||
|
||||
from archinstall.lib.output import warn
|
||||
from archinstall.lib.log import warn
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
|
|
@ -25,6 +26,13 @@ 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
|
||||
|
||||
|
|
@ -53,14 +61,26 @@ class Bootloader(Enum):
|
|||
|
||||
|
||||
@dataclass
|
||||
class BootloaderConfiguration:
|
||||
class BootloaderConfiguration(SubConfig):
|
||||
bootloader: Bootloader
|
||||
uki: bool = False
|
||||
removable: bool = True
|
||||
|
||||
@override
|
||||
def json(self) -> dict[str, Any]:
|
||||
return {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out = [tr('Bootloader "{}"').format(self.bootloader.value)]
|
||||
|
||||
if self.uki:
|
||||
out.append(tr('UKI enabled'))
|
||||
if self.removable:
|
||||
out.append(tr('Removable'))
|
||||
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self:
|
||||
bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SubConfig(ABC):
|
||||
@abstractmethod
|
||||
def json(self) -> Any:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def summary(self) -> str | list[str] | None:
|
||||
pass
|
||||
|
|
@ -4,7 +4,7 @@ import uuid
|
|||
from dataclasses import dataclass, field
|
||||
from enum import Enum, StrEnum, auto
|
||||
from pathlib import Path
|
||||
from typing import NotRequired, Self, TypedDict, override
|
||||
from typing import Any, NotRequired, Self, TypedDict, override
|
||||
from uuid import UUID
|
||||
|
||||
import parted
|
||||
|
|
@ -12,8 +12,9 @@ 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'
|
||||
|
|
@ -34,6 +35,15 @@ class DiskLayoutType(Enum):
|
|||
case DiskLayoutType.Pre_mount:
|
||||
return tr('Pre-mounted configuration')
|
||||
|
||||
def short_msg(self) -> str:
|
||||
match self:
|
||||
case DiskLayoutType.Default:
|
||||
return tr('Default')
|
||||
case DiskLayoutType.Manual:
|
||||
return tr('Manual')
|
||||
case DiskLayoutType.Pre_mount:
|
||||
return tr('Pre-mount')
|
||||
|
||||
|
||||
class _DiskLayoutConfigurationSerialization(TypedDict):
|
||||
config_type: str
|
||||
|
|
@ -45,7 +55,7 @@ class _DiskLayoutConfigurationSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class DiskLayoutConfiguration:
|
||||
class DiskLayoutConfiguration(SubConfig):
|
||||
config_type: DiskLayoutType
|
||||
device_modifications: list[DeviceModification] = field(default_factory=list)
|
||||
lvm_config: LvmConfiguration | None = None
|
||||
|
|
@ -55,6 +65,7 @@ class DiskLayoutConfiguration:
|
|||
# used for pre-mounted config
|
||||
mountpoint: Path | None = None
|
||||
|
||||
@override
|
||||
def json(self) -> _DiskLayoutConfigurationSerialization:
|
||||
if self.config_type == DiskLayoutType.Pre_mount:
|
||||
return {
|
||||
|
|
@ -78,6 +89,28 @@ class DiskLayoutConfiguration:
|
|||
|
||||
return config
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out = [tr('{} layout').format(self.config_type.short_msg())]
|
||||
|
||||
devices = set(mod.device_path for mod in self.device_modifications)
|
||||
|
||||
if devices:
|
||||
dev_str = ', '.join(str(d) for d in devices)
|
||||
out.append(tr('Devices {}').format(dev_str))
|
||||
|
||||
if self.lvm_config is not None:
|
||||
out.append(tr('LVM set up'))
|
||||
|
||||
if self.disk_encryption is not None:
|
||||
out.append(tr('{} encryption').format(self.disk_encryption.encryption_type.type_to_text()))
|
||||
|
||||
if self.btrfs_options is not None:
|
||||
if self.btrfs_options.snapshot_config:
|
||||
out.append(tr('Btrfs snapshot "{}"').format(self.btrfs_options.snapshot_config.snapshot_type))
|
||||
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def parse_arg(
|
||||
cls,
|
||||
|
|
@ -720,23 +753,23 @@ class BDevice:
|
|||
return hash(self.disk.device.path)
|
||||
|
||||
|
||||
class PartitionType(Enum):
|
||||
Boot = 'boot'
|
||||
Primary = 'primary'
|
||||
_Unknown = 'unknown'
|
||||
class PartitionType(StrEnum):
|
||||
BOOT = auto()
|
||||
PRIMARY = auto()
|
||||
_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
|
||||
|
||||
|
|
@ -802,6 +835,9 @@ 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
|
||||
|
|
@ -1318,7 +1354,7 @@ class _SnapshotConfigSerialization(TypedDict):
|
|||
type: str
|
||||
|
||||
|
||||
class SnapshotType(Enum):
|
||||
class SnapshotType(StrEnum):
|
||||
Snapper = 'Snapper'
|
||||
Timeshift = 'Timeshift'
|
||||
|
||||
|
|
@ -1400,19 +1436,19 @@ class DeviceModification:
|
|||
}
|
||||
|
||||
|
||||
class EncryptionType(Enum):
|
||||
NoEncryption = 'no_encryption'
|
||||
Luks = 'luks'
|
||||
LvmOnLuks = 'lvm_on_luks'
|
||||
LuksOnLvm = 'luks_on_lvm'
|
||||
class EncryptionType(StrEnum):
|
||||
NO_ENCRYPTION = auto()
|
||||
LUKS = auto()
|
||||
LVM_ON_LUKS = auto()
|
||||
LUKS_ON_LVM = auto()
|
||||
|
||||
@classmethod
|
||||
def _encryption_type_mapper(cls) -> dict[str, Self]:
|
||||
return {
|
||||
tr('No Encryption'): cls.NoEncryption,
|
||||
tr('LUKS'): cls.Luks,
|
||||
tr('LVM on LUKS'): cls.LvmOnLuks,
|
||||
tr('LUKS on LVM'): cls.LuksOnLvm,
|
||||
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,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1436,7 +1472,7 @@ class _DiskEncryptionSerialization(TypedDict):
|
|||
|
||||
@dataclass
|
||||
class DiskEncryption:
|
||||
encryption_type: EncryptionType = EncryptionType.NoEncryption
|
||||
encryption_type: EncryptionType = EncryptionType.NO_ENCRYPTION
|
||||
encryption_password: Password | None = None
|
||||
partitions: list[PartitionModification] = field(default_factory=list)
|
||||
lvm_volumes: list[LvmVolume] = field(default_factory=list)
|
||||
|
|
@ -1444,10 +1480,10 @@ class DiskEncryption:
|
|||
iter_time: int = DEFAULT_ITER_TIME
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.encryption_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and not self.partitions:
|
||||
if self.encryption_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and not self.partitions:
|
||||
raise ValueError('Luks or LvmOnLuks encryption require partitions to be defined')
|
||||
|
||||
if self.encryption_type == EncryptionType.LuksOnLvm and not self.lvm_volumes:
|
||||
if self.encryption_type == EncryptionType.LUKS_ON_LVM 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:
|
||||
|
|
@ -1590,14 +1626,18 @@ class LsblkInfo(BaseModel):
|
|||
|
||||
@field_validator('size', mode='before')
|
||||
@classmethod
|
||||
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)
|
||||
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
|
||||
|
||||
@field_validator('mountpoints', 'fsroots', mode='before')
|
||||
@classmethod
|
||||
def remove_none(cls, v: list[Path | None]) -> list[Path]:
|
||||
return [item for item in v if item is not None]
|
||||
def remove_none(cls, value: Any) -> Any:
|
||||
if isinstance(value, list):
|
||||
return [item for item in value if item is not None]
|
||||
return value
|
||||
|
||||
@field_serializer('size', when_used='json')
|
||||
def serialize_size(self, size: Size) -> str:
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Any, Self
|
||||
from typing import Any, Self, override
|
||||
|
||||
from archinstall.lib.locale.utils import get_kb_layout
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocaleConfiguration:
|
||||
class LocaleConfiguration(SubConfig):
|
||||
kb_layout: str
|
||||
sys_lang: str
|
||||
sys_enc: str
|
||||
# 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:
|
||||
|
|
@ -18,17 +24,29 @@ class LocaleConfiguration:
|
|||
layout = 'us'
|
||||
return cls(layout, 'en_US.UTF-8', 'UTF-8')
|
||||
|
||||
@override
|
||||
def json(self) -> dict[str, str]:
|
||||
return {
|
||||
'kb_layout': self.kb_layout,
|
||||
'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 += '{}: {}'.format(tr('Locale encoding'), self.sys_enc)
|
||||
output += '{}: {}\n'.format(tr('Locale encoding'), self.sys_enc)
|
||||
output += '{}: {}'.format(tr('Console font'), self.console_font)
|
||||
return output
|
||||
|
||||
def _load_config(self, args: dict[str, str]) -> None:
|
||||
|
|
@ -38,6 +56,8 @@ class LocaleConfiguration:
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ from typing import TYPE_CHECKING, Any, Self, TypedDict, override
|
|||
|
||||
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator
|
||||
|
||||
from archinstall.lib.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.output import debug
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
|
||||
|
|
@ -236,7 +238,7 @@ class _MirrorConfigurationSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class MirrorConfiguration:
|
||||
class MirrorConfiguration(SubConfig):
|
||||
mirror_regions: list[MirrorRegion] = field(default_factory=list)
|
||||
custom_servers: list[CustomServer] = field(default_factory=list)
|
||||
optional_repositories: list[Repository] = field(default_factory=list)
|
||||
|
|
@ -250,6 +252,7 @@ class MirrorConfiguration:
|
|||
def custom_server_urls(self) -> str:
|
||||
return '\n'.join(s.url for s in self.custom_servers)
|
||||
|
||||
@override
|
||||
def json(self) -> _MirrorConfigurationSerialization:
|
||||
regions = {}
|
||||
for m in self.mirror_regions:
|
||||
|
|
@ -262,6 +265,24 @@ class MirrorConfiguration:
|
|||
'custom_repositories': [c.json() for c in self.custom_repositories],
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out: list[str] = []
|
||||
|
||||
if self.mirror_regions:
|
||||
out.append(tr('Mirror regions "{}"').format(', '.join(m.name for m in self.mirror_regions)))
|
||||
|
||||
if self.optional_repositories:
|
||||
out.append(tr('Optional repositories "{}"').format(', '.join(r.value for r in self.optional_repositories)))
|
||||
|
||||
if self.custom_servers:
|
||||
out.append(tr('Custom servers set up'))
|
||||
|
||||
if self.custom_repositories:
|
||||
out.append(tr('Custom repositories set up'))
|
||||
|
||||
return out
|
||||
|
||||
def custom_servers_config(self) -> str:
|
||||
config = ''
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ from dataclasses import dataclass, field
|
|||
from enum import Enum
|
||||
from typing import NotRequired, Self, TypedDict, override
|
||||
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.log import debug
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
|
|
@ -11,6 +12,7 @@ class NicType(Enum):
|
|||
ISO = 'iso'
|
||||
NM = 'nm'
|
||||
NM_IWD = 'nm_iwd'
|
||||
IWD = 'iwd'
|
||||
MANUAL = 'manual'
|
||||
|
||||
def display_msg(self) -> str:
|
||||
|
|
@ -21,6 +23,8 @@ 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')
|
||||
|
||||
|
|
@ -103,10 +107,11 @@ class _NetworkConfigurationSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class NetworkConfiguration:
|
||||
class NetworkConfiguration(SubConfig):
|
||||
type: NicType
|
||||
nics: list[Nic] = field(default_factory=list)
|
||||
|
||||
@override
|
||||
def json(self) -> _NetworkConfigurationSerialization:
|
||||
config: _NetworkConfigurationSerialization = {'type': self.type.value}
|
||||
if self.nics:
|
||||
|
|
@ -114,6 +119,10 @@ class NetworkConfiguration:
|
|||
|
||||
return config
|
||||
|
||||
@override
|
||||
def summary(self) -> str:
|
||||
return self.type.display_msg()
|
||||
|
||||
@classmethod
|
||||
def parse_arg(cls, config: _NetworkConfigurationSerialization) -> Self | None:
|
||||
nic_type = config.get('type', None)
|
||||
|
|
@ -125,6 +134,10 @@ class NetworkConfiguration:
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
|
|
@ -132,7 +132,6 @@ class AvailablePackage(BaseModel):
|
|||
def longest_key(self) -> int:
|
||||
return max(len(key) for key in self.model_dump().keys())
|
||||
|
||||
# return all package info line by line
|
||||
def info(self) -> str:
|
||||
output = ''
|
||||
for key, value in self.model_dump().items():
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Self, TypedDict
|
||||
from typing import Self, TypedDict, override
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ class PacmanConfigSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class PacmanConfiguration:
|
||||
class PacmanConfiguration(SubConfig):
|
||||
parallel_downloads: int = 5
|
||||
color: bool = True
|
||||
|
||||
|
|
@ -18,12 +19,19 @@ class PacmanConfiguration:
|
|||
def default(cls) -> Self:
|
||||
return cls()
|
||||
|
||||
@override
|
||||
def json(self) -> PacmanConfigSerialization:
|
||||
return {
|
||||
'parallel_downloads': self.parallel_downloads,
|
||||
'color': self.color,
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> str | None:
|
||||
if self.color:
|
||||
return tr('Color enabled')
|
||||
return None
|
||||
|
||||
def preview(self) -> str:
|
||||
color_str = str(self.color)
|
||||
output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Self, TypedDict
|
||||
from typing import TYPE_CHECKING, Self, TypedDict, override
|
||||
|
||||
from archinstall.default_profiles.profile import GreeterType, Profile
|
||||
from archinstall.lib.hardware import GfxDriver
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.profile.profiles_handler import ProfileSerialization
|
||||
|
|
@ -15,11 +17,12 @@ class _ProfileConfigurationSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class ProfileConfiguration:
|
||||
class ProfileConfiguration(SubConfig):
|
||||
profile: Profile | None = None
|
||||
gfx_driver: GfxDriver | None = None
|
||||
greeter: GreeterType | None = None
|
||||
|
||||
@override
|
||||
def json(self) -> _ProfileConfigurationSerialization:
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
|
||||
|
|
@ -29,6 +32,23 @@ class ProfileConfiguration:
|
|||
'greeter': self.greeter.value if self.greeter else None,
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str] | None:
|
||||
out: list[str] = []
|
||||
|
||||
if self.profile:
|
||||
out.append(self.profile.name)
|
||||
|
||||
if self.gfx_driver:
|
||||
out.append(tr('{} grphics driver').format(self.gfx_driver.value))
|
||||
|
||||
if self.greeter:
|
||||
out.append(tr('{} greeter').format(self.greeter.value))
|
||||
|
||||
return out
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self:
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
|
|
|
|||
|
|
@ -36,67 +36,66 @@ class PasswordStrength(Enum):
|
|||
case PasswordStrength.STRONG:
|
||||
return 'green'
|
||||
|
||||
@classmethod
|
||||
def strength(cls, password: str) -> Self:
|
||||
@staticmethod
|
||||
def strength(password: str) -> PasswordStrength:
|
||||
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 cls._check_password_strength(digit, upper, lower, symbol, len(password))
|
||||
return PasswordStrength._check_password_strength(digit, upper, lower, symbol, len(password))
|
||||
|
||||
@classmethod
|
||||
@staticmethod
|
||||
def _check_password_strength(
|
||||
cls,
|
||||
digit: bool,
|
||||
upper: bool,
|
||||
lower: bool,
|
||||
symbol: bool,
|
||||
length: int,
|
||||
) -> Self:
|
||||
) -> PasswordStrength:
|
||||
# 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 cls.STRONG
|
||||
return PasswordStrength.STRONG
|
||||
case num if 11 <= num <= 12:
|
||||
return cls.MODERATE
|
||||
return PasswordStrength.MODERATE
|
||||
case num if 7 <= num <= 10:
|
||||
return cls.WEAK
|
||||
return PasswordStrength.WEAK
|
||||
case num if num <= 6:
|
||||
return cls.VERY_WEAK
|
||||
return PasswordStrength.VERY_WEAK
|
||||
elif digit and upper and lower:
|
||||
match length:
|
||||
case num if 14 <= num:
|
||||
return cls.STRONG
|
||||
return PasswordStrength.STRONG
|
||||
case num if 11 <= num <= 13:
|
||||
return cls.MODERATE
|
||||
return PasswordStrength.MODERATE
|
||||
case num if 7 <= num <= 10:
|
||||
return cls.WEAK
|
||||
return PasswordStrength.WEAK
|
||||
case num if num <= 6:
|
||||
return cls.VERY_WEAK
|
||||
return PasswordStrength.VERY_WEAK
|
||||
elif upper and lower:
|
||||
match length:
|
||||
case num if 15 <= num:
|
||||
return cls.STRONG
|
||||
return PasswordStrength.STRONG
|
||||
case num if 12 <= num <= 14:
|
||||
return cls.MODERATE
|
||||
return PasswordStrength.MODERATE
|
||||
case num if 7 <= num <= 11:
|
||||
return cls.WEAK
|
||||
return PasswordStrength.WEAK
|
||||
case num if num <= 6:
|
||||
return cls.VERY_WEAK
|
||||
return PasswordStrength.VERY_WEAK
|
||||
elif lower or upper:
|
||||
match length:
|
||||
case num if 18 <= num:
|
||||
return cls.STRONG
|
||||
return PasswordStrength.STRONG
|
||||
case num if 14 <= num <= 17:
|
||||
return cls.MODERATE
|
||||
return PasswordStrength.MODERATE
|
||||
case num if 9 <= num <= 13:
|
||||
return cls.WEAK
|
||||
return PasswordStrength.WEAK
|
||||
case num if num <= 8:
|
||||
return cls.VERY_WEAK
|
||||
return PasswordStrength.VERY_WEAK
|
||||
|
||||
return cls.VERY_WEAK
|
||||
return PasswordStrength.VERY_WEAK
|
||||
|
||||
|
||||
UserSerialization = TypedDict(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import textwrap
|
||||
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.models.network import NetworkConfiguration, NicType
|
||||
from archinstall.lib.models.profile import ProfileConfiguration
|
||||
|
|
@ -32,6 +34,13 @@ 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)
|
||||
|
|
@ -45,3 +54,36 @@ 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')
|
||||
|
|
|
|||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class ManualNetworkConfig(ListManager[Nic]):
|
||||
|
|
@ -172,14 +172,17 @@ async def select_network(preset: NetworkConfiguration | None) -> NetworkConfigur
|
|||
"""
|
||||
|
||||
items = [MenuItem(n.display_msg(), value=n) for n in NicType]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
|
||||
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=tr('Choose network configuration'),
|
||||
header=header,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
).show()
|
||||
|
|
@ -199,6 +202,8 @@ 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()
|
||||
|
|
|
|||
|
|
@ -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.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
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from urllib.parse import urlencode
|
|||
from urllib.request import urlopen
|
||||
|
||||
from archinstall.lib.exceptions import DownloadTimeout, SysCallError
|
||||
from archinstall.lib.output import debug, error, info
|
||||
from archinstall.lib.log 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.time()
|
||||
self.start_time = time.monotonic()
|
||||
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.time() - self.start_time
|
||||
time_delta = time.monotonic() - 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.time()
|
||||
started = time.monotonic()
|
||||
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.time() - started < timeout:
|
||||
while latency == -1 and time.monotonic() - 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.time() - started) * 1000)
|
||||
latency = round((time.monotonic() - started) * 1000)
|
||||
break
|
||||
except OSError as e:
|
||||
debug(f'Error: {e}')
|
||||
|
|
|
|||
|
|
@ -1,335 +0,0 @@
|
|||
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)
|
||||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.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().strip())
|
||||
package_info.append(line.decode().rstrip())
|
||||
|
||||
return _parse_package_output(package_info, LocalPackage)
|
||||
except SysCallError:
|
||||
|
|
@ -53,7 +53,7 @@ def available_package(package: str) -> AvailablePackage | None:
|
|||
try:
|
||||
package_info: list[str] = []
|
||||
for line in Pacman.run(f'-S --info {package}'):
|
||||
package_info.append(line.decode().strip())
|
||||
package_info.append(line.decode().rstrip())
|
||||
|
||||
return _parse_package_output(package_info, AvailablePackage)
|
||||
except SysCallError:
|
||||
|
|
@ -79,7 +79,7 @@ def list_available_packages(
|
|||
debug(f'Failed to sync Arch Linux package database: {e}')
|
||||
|
||||
for line in Pacman.run('-S --info'):
|
||||
dec_line = line.decode().strip()
|
||||
dec_line = line.decode().rstrip()
|
||||
current_package.append(dec_line)
|
||||
|
||||
if dec_line.startswith('Validated'):
|
||||
|
|
@ -187,6 +187,7 @@ async def select_additional_packages(
|
|||
multi=True,
|
||||
preview_location='right',
|
||||
enable_filter=True,
|
||||
wrap_preview=True,
|
||||
).show()
|
||||
|
||||
match pck_result.type_:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from functools import lru_cache
|
||||
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.log import debug
|
||||
from archinstall.lib.packages.packages import check_package_upgrade
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
|
||||
from archinstall.lib.command import SysCommand
|
||||
from archinstall.lib.exceptions import RequirementError
|
||||
from archinstall.lib.output import error, info, warn
|
||||
from archinstall.lib.log 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.time()
|
||||
started = time.monotonic()
|
||||
while pacman_db_lock.exists():
|
||||
time.sleep(0.25)
|
||||
|
||||
if time.time() - started > (60 * 10):
|
||||
if time.monotonic() - started > (60 * 10):
|
||||
error(tr('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.'))
|
||||
sys.exit(1)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class PacmanMenu(AbstractSubMenu[PacmanConfiguration]):
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import urllib.request
|
|||
from importlib import metadata
|
||||
from pathlib import Path
|
||||
|
||||
from archinstall.lib.output import error, info, warn
|
||||
from archinstall.lib.log import error, info, warn
|
||||
from archinstall.lib.version import get_version
|
||||
|
||||
plugins = {}
|
||||
|
|
|
|||
|
|
@ -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.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.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():
|
||||
if driver.is_nvidia_proprietary():
|
||||
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,8 +105,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
|
|||
preset=False,
|
||||
).show()
|
||||
|
||||
if result.get_value():
|
||||
return preset
|
||||
return driver if result.get_value() else preset
|
||||
|
||||
return driver
|
||||
|
||||
|
|
@ -114,7 +113,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
|
|||
if item.value:
|
||||
driver = item.get_value().value
|
||||
packages = item.get_value().packages_text()
|
||||
return f'Driver: {driver}\n{packages}'
|
||||
return f'{tr("Graphics driver")}: {driver}\n{packages}'
|
||||
return None
|
||||
|
||||
def _prev_greeter(self, item: MenuItem) -> str | None:
|
||||
|
|
|
|||
|
|
@ -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,6 +175,9 @@ 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)
|
||||
|
|
@ -194,6 +197,26 @@ 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}')
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,16 @@ 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:
|
||||
|
|
@ -14,6 +20,7 @@ class Language:
|
|||
translation: gettext.NullTranslations
|
||||
translation_percent: int
|
||||
translated_lang: str | None
|
||||
console_font: str | None = None
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
|
|
@ -31,10 +38,18 @@ 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()
|
||||
|
|
@ -43,6 +58,65 @@ 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
|
||||
|
|
@ -57,6 +131,7 @@ 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
|
||||
|
|
@ -71,7 +146,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)
|
||||
language = Language(abbr, lang, translation, percent, translated_lang, console_font)
|
||||
languages.append(language)
|
||||
except FileNotFoundError as err:
|
||||
raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}")
|
||||
|
|
@ -127,12 +202,39 @@ class TranslationHandler:
|
|||
except Exception:
|
||||
raise ValueError(f'No language with abbreviation "{abbr}" found')
|
||||
|
||||
def activate(self, language: Language) -> None:
|
||||
def activate(self, language: Language, set_font: bool = True) -> 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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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.ui.menu_item import MenuItem
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.menu_item import MenuItem
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class UserList(ListManager[User]):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
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
Loading…
Reference in New Issue