Merge master into dev-vm-script

This commit is contained in:
Softer 2026-06-03 16:50:40 +03:00
commit 9a785e5ea4
147 changed files with 8798 additions and 5565 deletions

View File

@ -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

View File

@ -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

View File

@ -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

40
.github/workflows/uki-build.yaml vendored Normal file
View File

@ -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

4
.gitignore vendored
View File

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

View File

@ -1,7 +1,7 @@
default_stages: ['pre-commit']
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.11
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.2
rev: v2.1.0
hooks:
- id: mypy
args: [
@ -41,6 +41,7 @@ repos:
additional_dependencies:
- pydantic
- pytest
- hypothesis
- cryptography
- textual
- repo: local

View File

@ -101,9 +101,9 @@ If you come across any issues, kindly submit your issue here on GitHub or post y
When submitting an issue, please:
* Provide the stacktrace of the output if applicable
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
* To 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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)

View File

@ -18,12 +18,13 @@ class BudgieProfile(Profile):
return [
'materia-gtk-theme',
'budgie',
'konsole',
'dolphin',
'mate-terminal',
'nemo',
'nemo-fileroller',
'papirus-icon-theme',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Sddm
return GreeterType.LightdmSlick

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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
}
}

View File

@ -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; }
}

View File

@ -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"
}
}

View File

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

View File

@ -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
}

View File

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

View File

@ -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"

View File

@ -5,8 +5,8 @@ from archinstall.default_profiles.profile import CustomSetting, DisplayServerTyp
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.packages.packages import available_package, package_group_info
from archinstall.lib.translationhandler import tr
from archinstall.tui.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):

View File

@ -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

View File

@ -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')

View File

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

View File

@ -1,11 +1,11 @@
from typing import TYPE_CHECKING, Self, override
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult
from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.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

View File

@ -17,8 +17,8 @@ from archinstall.lib.models.application import (
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]):

View File

@ -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:
@ -261,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))
@ -293,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',
@ -430,7 +545,6 @@ class ArchConfigHandler:
default=False,
help='Enabled verbose options',
)
return parser
def _parse_args(self) -> Arguments:

View File

@ -3,9 +3,9 @@ from pathlib import Path
from typing import TYPE_CHECKING
from archinstall.lib.command import SysCommandWorker
from archinstall.lib.log import debug, info
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import User
from archinstall.lib.output import debug, info
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
@ -53,7 +53,7 @@ class AuthenticationHandler:
def _add_u2f_entry(self, file: Path, entry: str) -> None:
if not file.exists():
debug(f'File does not exist: {file}')
return 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'

View File

@ -6,11 +6,11 @@ from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import Password, User
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.user.user_menu import select_users
from archinstall.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:

View File

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

View File

@ -5,8 +5,8 @@ from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.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]):

View File

@ -2,6 +2,7 @@ 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
@ -9,6 +10,8 @@ from archinstall.lib.models.device import DiskLayoutConfiguration
class BootloaderValidationFailureKind(Enum):
LimineNonFatBoot = auto()
LimineLayout = auto()
BootloaderRequiresUefi = auto()
EfistubNonFatBoot = auto()
@dataclass(frozen=True)
@ -29,12 +32,32 @@ def validate_bootloader_layout(
if not (bootloader_config and disk_config):
return None
if bootloader_config.bootloader == Bootloader.Limine:
boot_part = next(
(p for m in disk_config.device_modifications if (p := m.get_boot_partition())),
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()):

View File

@ -11,7 +11,7 @@ from types import TracebackType
from typing import Any, Self, override
from archinstall.lib.exceptions import RequirementError, SysCallError
from archinstall.lib.output import debug, error, logger
from archinstall.lib.log import debug, error, logger
from archinstall.lib.utils.encoding import clear_vt100_escape_codes

View File

@ -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:

View File

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

View File

@ -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

View File

@ -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
@ -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:

View File

@ -17,10 +17,10 @@ from archinstall.lib.models.device import (
PartitionModification,
)
from archinstall.lib.models.users import Password
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.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]):
@ -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
@ -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:

View File

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

View File

@ -13,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:

View File

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

View File

@ -7,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(

View File

@ -19,10 +19,10 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.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:
@ -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)

View File

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

View File

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

View File

@ -1,11 +1,11 @@
from enum import Enum
from archinstall.lib.locale.utils import list_timezones
from archinstall.lib.log import warn
from archinstall.lib.menu.helpers import Confirmation, Input, Selection
from archinstall.lib.output import warn
from archinstall.lib.translationhandler import Language, tr
from archinstall.tui.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):

View File

@ -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,

View File

@ -6,7 +6,7 @@ from archinstall.lib.args import ArchConfig
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.configuration import save_config
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
@ -22,17 +22,18 @@ from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutTyp
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]):
@ -103,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,
@ -298,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()}'
@ -320,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
@ -461,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():
@ -490,9 +489,6 @@ class GlobalMenu(AbstractMenu[None]):
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.Refind and not self._uefi:
return 'rEFInd can only be used on UEFI systems'
if failure := validate_bootloader_layout(bootloader_config, disk_config):
return failure.description
@ -506,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
@ -612,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()

View File

@ -6,8 +6,8 @@ from typing import Self
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.networking import enrich_iface_types, list_interfaces
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
@ -68,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'

View File

@ -29,6 +29,7 @@ from archinstall.lib.exceptions import DiskError, HardwareIncompatibilityError,
from archinstall.lib.hardware import SysInfo
from archinstall.lib.linux_path import LPath
from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout
from archinstall.lib.log import debug, error, info, log, logger, warn
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.models.application import ZramAlgorithm
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
@ -48,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
@ -60,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']
@ -79,8 +85,8 @@ 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.NO_ENCRYPTION)
@ -183,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/'))
@ -369,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]
@ -396,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
@ -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)
@ -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)
@ -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:
@ -1761,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}')
@ -1784,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)
@ -1807,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:
@ -1856,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:
@ -1922,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():
@ -1942,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)
@ -1957,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)
@ -1969,7 +1984,7 @@ class Installer:
def user_set_shell(self, user: str, shell: str) -> bool:
info(f'Setting shell for {user} to {shell}')
cmd = ['arch-chroot', '-S', str(self.target), 'chsh', '-s', shell, user]
cmd = self._chroot_argv('chsh', '-s', shell, user)
try:
run(cmd)
return True
@ -1979,7 +1994,7 @@ class Installer:
def chown(self, owner: str, path: str, options: list[str] | None = None) -> bool:
options = options or []
cmd = ['arch-chroot', '-S', str(self.target), 'chown', *options, owner, path]
cmd = self._chroot_argv('chown', *options, '--', owner, path)
try:
run(cmd)
return True
@ -1990,12 +2005,10 @@ class Installer:
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'

View File

@ -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')

View File

@ -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(

259
archinstall/lib/log.py Normal file
View File

@ -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

View File

@ -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__(

View File

@ -4,9 +4,9 @@ from typing import Any, Literal, override
from textual.validation import ValidationResult, Validator
from archinstall.lib.translationhandler import tr
from archinstall.tui.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:

View File

@ -4,8 +4,8 @@ from typing import cast
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.lib.translationhandler import tr
from archinstall.tui.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]:

View File

@ -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

View File

@ -5,8 +5,8 @@ from pathlib import Path
from archinstall.lib.menu.helpers import Confirmation, Input
from archinstall.lib.models.users import Password, PasswordStrength
from archinstall.lib.translationhandler import tr
from archinstall.tui.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(

View File

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

View File

@ -13,10 +13,10 @@ from archinstall.lib.models.mirrors import (
SignOption,
)
from archinstall.lib.models.packages import Repository
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.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:

View File

@ -1,7 +1,8 @@
from dataclasses import dataclass
from enum import StrEnum, auto
from typing import Any, NotRequired, Self, TypedDict
from typing import Any, NotRequired, Self, TypedDict, override
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
@ -168,7 +169,7 @@ class FontsConfiguration:
@dataclass(frozen=True)
class ZramConfiguration:
class ZramConfiguration(SubConfig):
enabled: bool
algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD
@ -181,9 +182,28 @@ class ZramConfiguration:
algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value))
return cls(enabled=enabled, algorithm=ZramAlgorithm(algo))
@override
def json(self) -> dict[str, bool | str]:
return {
'enabled': self.enabled,
'algorithm': self.algorithm.value,
}
@override
def summary(self) -> list[str] | None:
out: list[str] = []
if self.enabled:
out.append(tr('Zram enabled'))
out.append(tr('Zram algorithm {}').format(self.algorithm))
return out
return None
@dataclass
class ApplicationConfiguration:
class ApplicationConfiguration(SubConfig):
bluetooth_config: BluetoothConfiguration | None = None
audio_config: AudioConfiguration | None = None
power_management_config: PowerManagementConfiguration | None = None
@ -223,6 +243,7 @@ class ApplicationConfiguration:
return app_config
@override
def json(self) -> ApplicationSerialization:
config: ApplicationSerialization = {}
@ -245,3 +266,28 @@ class ApplicationConfiguration:
config['fonts_config'] = self.fonts_config.json()
return config
@override
def summary(self) -> list[str]:
out: list[str] = []
if self.bluetooth_config and self.bluetooth_config.enabled:
out.append(tr('Bluetooth enabled'))
if self.audio_config:
out.append(tr('Audio server "{}"').format(self.audio_config.audio))
if self.power_management_config:
out.append(tr('Power management "{}"').format(self.power_management_config.power_management))
if self.print_service_config and self.print_service_config.enabled:
out.append(tr('Print service enabled'))
if self.firewall_config:
out.append(tr('Firewall "{}"').format(self.firewall_config.firewall))
if self.fonts_config and self.fonts_config.fonts:
fonts = ', '.join(f.value for f in self.fonts_config.fonts)
out.append(tr('Extra fonts "{}"').format(fonts))
return out

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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,
@ -1321,7 +1354,7 @@ class _SnapshotConfigSerialization(TypedDict):
type: str
class SnapshotType(Enum):
class SnapshotType(StrEnum):
Snapper = 'Snapper'
Timeshift = 'Timeshift'

View File

@ -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:

View File

@ -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 = ''

View File

@ -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:

View File

@ -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

View File

@ -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():

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -6,8 +6,8 @@ from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType
from archinstall.lib.networking import list_interfaces
from archinstall.lib.translationhandler import tr
from archinstall.tui.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()

View File

@ -5,13 +5,13 @@ from typing import assert_never, override
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.network import WifiConfiguredNetwork, WifiNetwork
from archinstall.lib.network.wpa_supplicant import WpaSupplicantConfig
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.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

View File

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

View File

@ -13,7 +13,7 @@ from urllib.parse import urlencode
from urllib.request import urlopen
from archinstall.lib.exceptions import DownloadTimeout, SysCallError
from archinstall.lib.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}')

View File

@ -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)

View File

@ -1,20 +1,20 @@
from functools import lru_cache
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.menu.helpers import Loading, Notify, Selection
from archinstall.lib.models.packages import AvailablePackage, LocalPackage, PackageGroup, Repository
from archinstall.lib.output import debug
from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr
from archinstall.tui.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_:

View File

@ -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

View File

@ -5,7 +5,7 @@ from pathlib import Path
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import RequirementError
from archinstall.lib.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)

View File

@ -5,8 +5,8 @@ from archinstall.lib.menu.helpers import Confirmation, Input
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.pathnames import PACMAN_CONF
from archinstall.lib.translationhandler import tr
from archinstall.tui.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]):

View File

@ -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 = {}

View File

@ -7,8 +7,8 @@ from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.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:

View File

@ -9,9 +9,9 @@ from typing import TYPE_CHECKING, NotRequired, TypedDict
from archinstall.default_profiles.profile import CustomSetting, GreeterType, Profile
from archinstall.lib.hardware import GfxDriver, GfxPackage
from archinstall.lib.log import debug, error, info
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.networking import fetch_data_from_url
from archinstall.lib.output import debug, error, info
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
@ -175,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}')

View File

@ -9,7 +9,7 @@ from typing import override
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.output import debug
from archinstall.lib.log import debug
from archinstall.lib.utils.util import running_from_iso

View File

@ -6,8 +6,8 @@ from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.users import User
from archinstall.lib.translationhandler import tr
from archinstall.tui.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]):

View File

@ -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

View File

@ -1,8 +1,14 @@
import secrets
import string
from datetime import UTC, datetime
from archinstall.lib.output import FormattedOutput
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
from archinstall.lib.utils.format import as_columns
def timestamp() -> str:
now = datetime.now(tz=UTC)
return now.strftime('%Y-%m-%d %H:%M:%S')
def running_from_iso() -> bool:
@ -36,7 +42,7 @@ def format_cols(items: list[str], header: str | None = None) -> str:
else:
col = 4
text += FormattedOutput.as_columns(items, col)
text += as_columns(items, col)
# remove whitespaces on each row
text = '\n'.join(t.strip() for t in text.split('\n'))
return text

View File

@ -252,6 +252,9 @@ msgstr ""
msgid "Locale encoding"
msgstr ""
msgid "Console font"
msgstr ""
msgid "Drive(s)"
msgstr ""
@ -641,8 +644,8 @@ msgid ""
msgstr ""
msgid ""
"Provides a selection of desktop environments and tiling window managers, e."
"g. gnome, kde, sway"
"Provides a selection of desktop environments and tiling window managers, "
"e.g. gnome, kde, sway"
msgstr ""
msgid "Select your desired desktop environment"
@ -1242,7 +1245,22 @@ msgid "Product"
msgstr ""
#, python-brace-format
msgid "Invalid configuration: {error}"
msgid "Invalid configuration: {}"
msgstr ""
msgid "Ready to install"
msgstr ""
msgid "Disks"
msgstr ""
msgid "Packages"
msgstr ""
msgid "Network"
msgstr ""
msgid "Locale"
msgstr ""
msgid "Type"
@ -1331,8 +1349,8 @@ msgid "Disable Copy-on-Write"
msgstr ""
msgid ""
"Provides a selection of desktop environments and tiling window managers, e."
"g. GNOME, KDE Plasma, Sway"
"Provides a selection of desktop environments and tiling window managers, "
"e.g. GNOME, KDE Plasma, Sway"
msgstr ""
#, python-brace-format
@ -2174,6 +2192,14 @@ msgstr ""
msgid "Choose network configuration"
msgstr ""
msgid "Recommended: Network Manager for desktop, Manual for server"
msgstr ""
msgid ""
"Warning: no network configuration selected. Network will need to be set up "
"manually on the installed system."
msgstr ""
msgid "No packages found"
msgstr ""
@ -2259,3 +2285,181 @@ msgstr ""
#, python-brace-format
msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000"
msgstr ""
#, python-brace-format
msgid "Setting up U2F login: {}"
msgstr ""
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr ""
#, python-brace-format
msgid "{} needs access to your seat"
msgstr ""
msgid "collection of hardware devices i.e. keyboard, mouse"
msgstr ""
#, python-brace-format
msgid "Choose an option how to give {} access to your hardware"
msgstr ""
#, python-brace-format
msgid "About to upload \"{}\" to the publicly accessible {}"
msgstr ""
msgid "Do you want to continue?"
msgstr ""
#, python-brace-format
msgid "Log uploaded successfully. URL: {}"
msgstr ""
msgid "Failed to upload log."
msgstr ""
msgid "ArchInstall Language"
msgstr ""
msgid "Version"
msgstr ""
msgid "Installation Script"
msgstr ""
msgid "Application"
msgstr ""
msgid "Services"
msgstr ""
msgid "Custom commands"
msgstr ""
msgid "Users"
msgstr ""
msgid "Root encrypted password"
msgstr ""
msgid "Zram enabled"
msgstr ""
#, python-brace-format
msgid "Zram algorithm {}"
msgstr ""
msgid "Bluetooth enabled"
msgstr ""
#, python-brace-format
msgid "Audio server \"{}\""
msgstr ""
#, python-brace-format
msgid "Power management \"{}\""
msgstr ""
msgid "Print service enabled"
msgstr ""
#, python-brace-format
msgid "Firewall \"{}\""
msgstr ""
#, python-brace-format
msgid "Extra fonts \"{}\""
msgstr ""
msgid "Root password set"
msgstr ""
#, python-brace-format
msgid "Configured {} user(s)"
msgstr ""
msgid "U2F set up"
msgstr ""
#, python-brace-format
msgid "Bootloader \"{}\""
msgstr ""
msgid "UKI enabled"
msgstr ""
msgid "Default"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Pre-mount"
msgstr ""
#, python-brace-format
msgid "{} layout"
msgstr ""
#, python-brace-format
msgid "Devices {}"
msgstr ""
msgid "LVM set up"
msgstr ""
#, python-brace-format
msgid "{} encryption"
msgstr ""
#, python-brace-format
msgid "Btrfs snapshot \"{}\""
msgstr ""
#, python-brace-format
msgid "Keyboard layout \"{}\""
msgstr ""
#, python-brace-format
msgid "Locale language \"{}\""
msgstr ""
#, python-brace-format
msgid "Locale encoding \"{}\""
msgstr ""
#, python-brace-format
msgid "Console font \"{}\""
msgstr ""
#, python-brace-format
msgid "Mirror regions \"{}\""
msgstr ""
#, python-brace-format
msgid "Optional repositories \"{}\""
msgstr ""
msgid "Custom servers set up"
msgstr ""
msgid "Custom repositories set up"
msgstr ""
msgid "Use standalone iwd"
msgstr ""
msgid "Color enabled"
msgstr ""
#, python-brace-format
msgid "{} grphics driver"
msgstr ""
#, python-brace-format
msgid "{} greeter"
msgstr ""
msgid "Enter a repository name"
msgstr ""

Binary file not shown.

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