Compare commits

...

89 Commits
4.2 ... master

Author SHA1 Message Date
renovate[bot] b96afcc579
Update dependency arch/python-textual to v8.2.7 (#4556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 21:58:49 +10:00
BB 3bc248b47f
add niri DankMaterialShell profile (#4554) 2026-05-25 21:13:56 +10:00
Anton Hvornum af2120c0e9
mkosi for test image builds (#4539)
Added a `mkosi` profile as well as organized test tooling a bit.

---------

Co-authored-by: 0xdeadd <clintdotphillips@gmail.com>
2026-05-23 13:49:54 +02:00
renovate[bot] d92a98d3bf
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.14 (#4552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 12:39:28 +10:00
renovate[bot] af58671e88
Update dependency ruff to v0.15.14 (#4551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 12:39:11 +10:00
utuhiro78 e17e036f54
Update Japanese translation (#4550) 2026-05-21 07:25:23 +10:00
Daniel Girtler ac4bc442de
Refactor logging and formatting logic (#4542)
* Refactor logging and formatting

* Refactor logging and formatting

* Refactor logging and formatting

* Updaet
2026-05-19 17:48:03 +02:00
Clinton Phillips 5fd2f9b627
fix: restrict EFI partition permissions with fmask/dmask=0077 (#4506)
* fix: restrict EFI partition permissions with fmask/dmask=0077

Mount the ESP with fmask=0077 and dmask=0077 to prevent world-readable
files like /efi/loader/random-seed.

Closes #4241

* fix(efi): collapse fmask/dmask dedup to dict.fromkeys one-liner

Per @Torxed's review feedback. Same semantics as the previous loop
(dedupe by exact-string match) but shorter. dict.fromkeys preserves
insertion order, where set() would not.

* fix(efi): drop defensive list wrap per review

The list() copy on line 378 was load-bearing only if options were
mutated downstream, but the EFI branch reassigns options via
dict.fromkeys() (line 381) and the non-EFI branch passes through
to mount() without mutating. Drop the copy.
2026-05-19 13:36:28 +10:00
Softer dccd0c8ccf
Add translation CI validation (#4519)
* Fix broken localization: tr(f-string) never matches translation catalog

tr(f'Invalid configuration: {error}') evaluates the f-string before
tr() runs, so xgettext extracts the literal placeholder as the msgid
while runtime passes the formatted string - the two never match.
Switch to tr('...{}').format(...) and update msgid in base.pot.

* Add CI validation for translations and pot_tools dev utility

Add translation-check workflow with two jobs:
- validate-po: msgfmt --check on changed .po files, .mo sync warning,
  tr(f-string) anti-pattern grep on changed .py files
- validate-pot: verify all tr() strings exist in base.pot when .py
  files change

Workflow only triggers on .py/.po/.pot file changes.

Add scripts/pot_tools.py developer utility (stats, list, add_missing)
for managing base.pot.

* Fix code style: use tabs and reformat xgettext arguments

Align check_pot_freshness.py and pot_tools.py with project
indentation (tabs) and ruff format requirements.

Sorry :-)

* Replace custom PO parser with msgcmp, drop pot_tools.py

Address review feedback: use standard gettext msgcmp instead of
hand-rolled parser for base.pot freshness check. Remove pot_tools.py
that duplicated locales_generator.sh functionality.

* Move translation checks into locales_generator.sh, simplify CI workflow

Use msgcmp instead of diff for base.pot validation to avoid failing on
legacy stale entries - the same cascading breakage that killed the
original workflow (disabled 2023, removed in #4483).

* Fix broken .po files: duplicate msgid in Hindi, missing format args in Finnish
2026-05-18 20:10:56 +10:00
renovate[bot] 22bf6e3c35
Update dependency arch/python-textual to v8.2.6 (#4546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 15:36:33 +10:00
Matteo b5d323762b
Update Italian translation + fix a string (#4545)
* Update Italian translation

* Added new strings
* Improved existing translations

* Fix wrong string + Italian translation

* Mispelled "respository" -> "repository"
2026-05-17 11:26:30 +10:00
utuhiro78 42d9113611
Update Japanese translation (#4544) 2026-05-16 09:29:27 +10:00
Daniel Girtler 516a61d8af
Enhance log sharing capability (#4526) 2026-05-15 21:38:17 +10:00
renovate[bot] e48ca45b0b
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.13 (#4541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 16:30:33 +10:00
renovate[bot] 74a1066661
Update dependency ruff to v0.15.13 (#4540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 13:58:00 +10:00
HADEON b81fe955f0
Add IWD standalone option to network configuration + fix NM_IWD (#4528)
* Add iwd standalone option to network configuration

Adds NicType.IWD as a third option alongside NM and NM_IWD: install
iwd, write /etc/iwd/main.conf with EnableNetworkConfiguration=true and
NameResolvingService=systemd, enable iwd.service + systemd-resolved.service.
iwd handles DHCP itself and resolved picks up its DNS via the symlink, so
no NetworkManager pulled in.

Also fills in parse_arg cases for NM_IWD and IWD so config files
round-trip both nic types.

Assisted-By: Flint

* wire up resolv stub and networkd service

* exclude virtual devices, dont harcode iface name instead match 'ether'
similar pattern to .network files shipped in /etc/systemd/network

* use dedent and rename menu option
2026-05-15 13:57:31 +10:00
renovate[bot] 13944f3cca
Update pre-commit hook pre-commit/mirrors-mypy to v2.1.0 (#4538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-13 08:54:05 +10:00
renovate[bot] b95321d38c
Update dependency mypy to v2.1.0 (#4535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-13 08:33:42 +10:00
renovate[bot] 3dec5025c3
Update dependency arch/python-pydantic to v2.13.4 (#4534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-13 08:21:44 +10:00
stinga11 f7a6f70fc8
Replace terminal and file manager in Budgie profile (#4533)
I think it was a mistake to have made the previous changes to KDE Apps. In retrospect, it seemed like a good idea since the Budgie developer had done it that way in Fedora, which is the distro the developer uses as a reference for Budgie. When you start using it, you realize there's no bridge between the desktop and the KDE Apps, and things like Dolphin recognizes icons and themes, requiring some rather annoying manual configurations. Then, if you want to change them again, you have to change those configurations again. Files don't open by default with the apps either; you have to configure them for that to work.

At first, I thought the Budgie packager for Arch had forgotten some stray dependency, but with some free time, I tested it with Fedora 44, and to my surprise, it has exactly the same problems, which is completely unacceptable for a final stable release. I suppose he'll make the necessary changes in the near future, but right now, it's a disaster.
2026-05-13 08:21:15 +10:00
Daniel Girtler 7fc33c2507
Enhance config types and summary (#4532)
* Enhance config types and summary

* Update
2026-05-10 22:35:53 +02:00
Lena Pastwa 2de7254b21
Update Polish translation (#4529) 2026-05-08 11:28:36 +10:00
renovate[bot] af106eb238
Update dependency mypy to v2 (#4523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 21:57:10 +10:00
renovate[bot] 4265be6e43
Update pre-commit hook pre-commit/mirrors-mypy to v2 (#4525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 21:56:30 +10:00
Softer b0b7983af2
Fix bspwm black screen: add provision() delegation and default configs (#4518)
* Fix bspwm black screen: add provision() delegation and default configs

* Move TYPE_CHECKING imports to top level in bspwm profile
2026-05-07 10:20:09 +10:00
Softer ef1cfe7b56
Add share-log subcommand to upload install.log to paste.rs (#4511)
* Add --share-log flag to upload install.log to paste.rs

* Apply ruff-format to share_log.py

* Rework share-log per review: subcommand, TUI confirmation, truncate large logs

* Replace curl/SysCommand with urllib.request, remove defensive try/except

Per review: use stdlib urllib instead of shelling out to curl,
drop unnecessary try/except around TUI confirmation,
remove tempfile (content is passed directly as bytes).

* Fix TUI imports after ui module was pulled one level up (#4515)

* Decouple share_install_log from TUI module

* Add unit tests for share_install_log function

* Update docs to use share-log subcommand syntax
2026-05-07 10:18:56 +10:00
Softer 7b5dddf34b
Fix truncated package metadata in additional packages preview (#3580) (#4510)
* Fix truncated package metadata in additional packages preview (#3580)

* Simplify package info fix: use rstrip and CSS wrap instead of env var plumbing

* Apply preview text wrap conditionally via wrap_preview parameter

* Add missing wrap-preview CSS to OptionListScreen and pass parameter through SelectMenu
2026-05-07 10:17:13 +10:00
renovate[bot] dcc38fe9aa
Update dependency arch/python-cryptography to v48 (#4520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 15:25:45 +10:00
Softer 11607851c4
Keep standalone initramfs for grub-btrfs when UKI is enabled (#4513)
* Keep standalone initramfs for grub-btrfs when UKI is enabled

Fixes #4505

* Move keep_initramfs logic into add_bootloader()

The grub-btrfs snapshot detection was in guided.py, forcing custom
script authors to replicate it. Since Installer already holds
disk_config, the check belongs inside add_bootloader().
2026-05-06 14:26:59 +10:00
Daniel Girtler ba7dbeadfc
Refactor profile seat access selection (#4457)
* Refactor profile seat access selection

* Update
2026-05-04 14:45:43 +10:00
aronmr-1 b5501d9507
Improved Finnish translation further (#4516) 2026-05-04 08:16:16 +10:00
renovate[bot] 528c27fbe1
Update dependency arch/python-textual to v8.2.5 (#4517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 08:14:56 +10:00
Daniel Girtler b18cc57216
Pull `ui` module one level up (#4515)
* Pull the UI module one level up

* Update
2026-05-03 23:54:42 +02:00
Softer dd34954011
Remove dead Cutefish desktop profile (#4514)
The cutefish package was never in the official Arch repos (AUR only)
and the upstream project is abandoned. Installing this profile would
always fail with a pacman "not found" error.
2026-05-03 10:33:48 +10:00
utuhiro78 76629ecc15
Update Japanese translation (#4504) 2026-04-30 17:37:37 +10:00
Softer dc7d9cf0a2
Improve Ukrainian translation (#4502) 2026-04-30 09:23:08 +10:00
correctmost b936fa11e3
Fix some inconsistent-return-statements Pylint warnings (#4503) 2026-04-30 09:22:45 +10:00
aronmr-1 6fefa3b6f5
Improved Finnish language (#4501) 2026-04-30 07:43:01 +10:00
Softer 3b026cb603
Fix broken localization in U2F, LUKS, and gfx driver preview (#4500) 2026-04-30 07:33:21 +10:00
correctmost 05560d29af
Add basic Pyrefly configuration to pyproject.toml (#4494)
This makes it easier to use the Pyrefly type checker with various
IDEs.
2026-04-29 21:54:59 +10:00
correctmost 3223ae4212
Remove the old curses-based TUI code (#4497)
The new Textual-based TUI shipped in the April .iso.
2026-04-29 17:08:24 +10:00
Steen Rabol 468e33d54f
Danish translation (#4493)
* Danish translation

* updated base.po and  created base.mo

---------

Co-authored-by: Steen Rabol <steen@rabol.dev>
2026-04-29 16:06:10 +10:00
utuhiro78 ccbc53d576
Update Japanese translation (#4498) 2026-04-29 13:44:11 +10:00
correctmost c4d68855cb
Replace time.time with time.monotonic to avoid clock drift issues (#4492)
Using time.time() can be inaccurate if the system clock gets
updated in between calls.
2026-04-28 13:34:33 +02:00
Softer 8a929f603f
Fix sway+nvidia confirmation dialog (#4481) (#4485)
* Fix sway+nvidia confirmation dialog (#4481)

Two bugs in the Sway+Nvidia driver confirmation:

1. The boolean was inverted - confirming "yes, I'm okay with issues"
   reverted the driver to the previous choice instead of keeping it.

2. The warning triggered for any Nvidia driver, including the
   open-source nouveau driver which is officially supported by Sway.

Add GfxDriver.is_nvidia_proprietary() and is_nvidia_nouveau() methods
so the warning fires only for nvidia-open-dkms (proprietary userspace).

* Address review feedback (#4485)

- Drop is_nvidia_nouveau() helper. It is not called anywhere yet; can
  be re-added when a consumer lands.
- Collapse the Sway+Nvidia confirmation result handling into a single
  expression now that allow_skip=False guarantees a boolean answer.
2026-04-28 21:02:32 +10:00
Softer e1efa34d8a
Install mkinitcpio explicitly in base packages (#4484)
Prevents pacstrap from picking a different initramfs provider
(dracut, booster) when running from a non-Arch host.

Fixes #4368
2026-04-28 21:01:05 +10:00
Softer cb0f3a6eba
Warn when no network configuration is selected (#4408)
* Make network configuration mandatory with explicit "No network" option

* Add network configuration recommendation hint and disable sorting

* Warn when network selection was skipped instead of making it mandatory

Replace the mandatory network_config menu requirement with a yellow
warning on the final confirmation screen, shown only when the user
skipped the network menu entirely. Explicitly picking "No network
configuration" is treated as a conscious choice and does not trigger
the warning.

- Drop mandatory=True from the network_config global menu item
- Rename NicType.NONE menu label from "No network" to
  "No network configuration" (an installed system still has an NIC;
  only the install-time configuration is absent)
- Add ConfigurationOutput.get_install_warnings() and render them in
  confirm_config when show_install_warnings=True
- guided.py and minimal.py enable the warning on the confirm screen
- Reuse the existing "No network configuration" msgid in .pot; add the
  warning string; update Ukrainian translations accordingly

* Drop NicType.NONE from this PR

The NONE option introduced earlier in this branch is being removed per
review feedback (#4408). Whether to add explicit "None" options across
multiple menus (greeter, gfx driver, network, bootloader) is now being
discussed in #4464 and should land as a separate change there.

This PR keeps only the warning-on-confirm behaviour: when the user
skipped the network menu entirely (network_config is None), a yellow
warning is shown on the final confirmation screen.
2026-04-28 20:17:40 +10:00
Hassaan Amin 1b7a32a3b3
Fix typos in known issues documentation (#4488)
First Edit :
- "effort" is singular so use has not have

Second Edit :
- we use "an" before vowel sounds (an old )


Third Edit :  
- use Sometimes because sometimes means occasionally and Some times means  some period or era 

Fourth Edit :
- Plural of ISO is ISOs not ISO's
2026-04-28 20:03:49 +10:00
Softer 08cba236f6
Show install summary when configuration is valid (#4475)
* Show install summary when configuration is valid

Previously the Install menu preview was empty when everything was valid,
leaving the user with no "ready" signal. Now show "Ready to install"
plus a two-column summary of the current configuration.

- New _install_summary() composes an aligned key/value table with rows
  for disks+FS+LUKS, bootloader, kernel, profile, greeter, package
  count, network, locale and timezone. Column width adapts to the
  longest translated label so translations keep the alignment.
- Rows whose underlying config is not set are skipped rather than
  rendered as empty.
- base.pot / uk base.po: add new msgids for summary labels.

* Move install summary into ConfigurationOutput

The summary helper that renders the install preview's two-column
configuration overview lived in GlobalMenu. Move it to
ConfigurationOutput.as_summary() so it sits alongside the JSON
output methods that share the same role. The preview now syncs
menu state to ArchConfig and delegates rendering.
2026-04-28 19:22:01 +10:00
Franco Castillo 8cc35f41d8
Update Spanish translation (#4490)
Signed-off-by: Franco Castillo <castillofrancodamian@gmail.com>
2026-04-28 17:08:39 +10:00
codefiles 9010ccf9eb
Add kernel enum (#4489) 2026-04-28 17:08:19 +10:00
Anton Hvornum 074dfbb178
Adding a reference to why AUR is not bundled as an option in archinstall (#4468) 2026-04-27 08:44:14 +02:00
Softer 008d303aaf
Remove disabled translation-check workflow (#4483)
The workflow has been fully commented out since #2119 (2023-09-28) when
translation handling was reworked. Because the file has no `on:` triggers,
GitHub Actions creates a failed workflow run for every push, polluting
the Actions tab without affecting PR check-runs.
2026-04-27 09:35:34 +10:00
utuhiro78 3795bfc21a
Update Japanese translation (#4482) 2026-04-27 09:32:10 +10:00
Softer 210329ed80
Add console font selection to Locales menu (#4469)
* Add console font selection to Locales menu

Add a 4th menu item "Console font" to the Locales configuration,
allowing users to select a console font for the target system.
The selected font is written to /etc/vconsole.conf.

If a terminus font (ter-*) is selected, the terminus-font package
is automatically installed on the target system.

* Switch list_console_fonts to pathlib and add @lru_cache

Address svartkanin's review on #4469. Replace os.listdir +
chained removesuffix with Path.glob('*.gz') + split('.')[0],
and cache the result via lru_cache - the kbd consolefonts
directory is static at runtime so re-scanning on every menu
reopen was wasted I/O.
2026-04-27 09:30:20 +10:00
Softer 9e05260df5
Fix argv injection in _create_user and gpasswd loop (#4473)
* Fix argv injection in _create_user and gpasswd loop

Use argv list with run() instead of f-string interpolation into
SysCommand, add debug logging on failure.

* Extract _chroot_argv helper and harden user/file ops

Address svartkanin's review on #4473: factor the
['arch-chroot', '-S', str(self.target), ...] boilerplate into a
private Installer._chroot_argv() helper, and migrate the seven
existing argv-form call sites to it (useradd, gpasswd, chpasswd,
chsh, chown, snapper-create-config, grub-install).

Two related hardening tweaks while in the area:

- Raise gpasswd failure log from debug() to warn(). The group-add
  loop has no return-False feedback channel for the caller, so a
  silent debug() means a half-configured user looks like a
  successful install.

- Add `--` end-of-options separator for useradd and chown so a
  username or path starting with `-` cannot smuggle flags. The TUI
  validates usernames, but parse_arguments() in models/users.py
  does not, so config.json is the residual hole; this closes it
  for these two sites at zero cost.
2026-04-27 09:24:51 +10:00
Softer d836ab0a66
Extend validate_bootloader_layout with UEFI-dependent checks (#4474)
* Extend validate_bootloader_layout with UEFI-dependent checks

Add is_uefi parameter and three new validations: Systemd-boot, Efistub
and rEFInd require UEFI; Efistub additionally requires a FAT boot
partition. Move the rEFInd UEFI-only check out of GlobalMenu so
guided.py and Installer silent-install paths get the same coverage.

* Encapsulate UEFI-only flag in Bootloader enum

Replace module-level _UEFI_ONLY_BOOTLOADERS tuple with an
is_uefi_only() method on the Bootloader enum, mirroring the
existing has_uki_support() / has_removable_support() pattern.

* Drop is_uefi parameter from validate_bootloader_layout

The UEFI flag is a constant system fact for the run, so the
validator retrieves it via SysInfo.has_uefi() directly instead
of having every caller pass it in. Updates all three call sites
in global_menu.py, installer.py and guided.py, and removes the
now-unused SysInfo import from guided.py.
2026-04-26 19:14:23 +10:00
renovate[bot] 3c4c87bdd6
Update dependency arch/python-cryptography to v47 (#4478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-26 19:03:16 +10:00
renovate[bot] efda78cad5
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.12 (#4477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-26 18:36:10 +10:00
renovate[bot] c8e5c85e20
Update dependency ruff to v0.15.12 (#4476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-26 18:35:31 +10:00
Softer bb46e295d1
Fix OVMF paths in README QEMU examples (#4472)
All three qemu-system-x86_64 examples in README pointed both -drive
if=pflash entries at the same file (/usr/share/ovmf/x64/OVMF.4m.fd).
OVMF needs CODE for the first pflash and VARS for the second; as
written, EFI NVRAM is not initialized correctly.

The path /usr/share/ovmf/x64/ is also stale - the ovmf package has
been replaced by edk2-ovmf, which installs under /usr/share/edk2/x64/.

Fix both in the three examples (testimage loop section, base Boot ISO
block, espeakup variant).
2026-04-24 12:19:01 +02:00
Thierry M de43019094
Update base.po (#4467) 2026-04-23 20:07:54 +10:00
renovate[bot] 6e222bcdd8
Update dependency pre-commit to v4.6.0 (#4459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 11:08:51 +10:00
renovate[bot] 6652197e35
Update pre-commit hook pre-commit/mirrors-mypy to v1.20.2 (#4463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 11:06:09 +10:00
renovate[bot] 80ae85cc8b
Update dependency mypy to v1.20.2 (#4458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-22 21:41:09 +10:00
correctmost 00834f9c6e
Use bools instead of time values for SysCommandWorker attributes (#4462)
Only the truthiness of the time values was being evaluated. This
also reduces the usage of time.time(), which makes the codebase
more resilient against clock drift issues.
2026-04-22 21:33:35 +10:00
Softer e7d38d0e82
Fix Limine install with ESP mounted outside /boot (#4442)
* Fix Limine install with ESP mounted outside /boot

Place limine.conf next to the EFI binary on the ESP so it is found
regardless of ESP mountpoint, and block unbootable layouts (non-UKI
Limine with ESP not at /boot and no separate /boot partition) in
GlobalMenu validation, guided.main() and _add_limine_bootloader().

Fixes #4333

* Extract bootloader layout validation into lib/bootloader/utils

* Consolidate Limine layout validation in bootloader utils

Move the boot-partition FAT check from GlobalMenu into
validate_bootloader_layout so all three call sites (GlobalMenu,
guided.py, Installer._add_limine_bootloader) share one function.

Return a BootloaderValidationFailure dataclass (kind + description)
instead of str | None, so callers can match on the failure kind and
the description is built where partition context is in scope.

* Encapsulate FAT filesystem detection in FilesystemType.is_fat()
2026-04-22 21:32:41 +10:00
correctmost d1765323d8
Use staticmethod for PasswordStrength methods (#4461)
The methods don't need access to the class, so they don't need to
be classmethods. This change reduces the number of 'bad return'
warnings seen when running Pyright, Pyrefly, and ty.
2026-04-22 21:31:06 +10:00
javier-anton-ordonez 094798b496
ES Language updated (#4456)
* ES langauje updated and added diferent translations

* Update archinstall/locales/es/LC_MESSAGES/base.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Cerrar -> Ocultar

Github copilot suggested

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix Spanish translations and accents in base.po

Github suggestions

* ./locales_generator.sh generated

* Accent updated and ./locales_generator.sh generated

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 10:34:41 +10:00
stinga11 fa54c9d6c9
Change Budgie display server and update packages (#4450)
Some changes consistent with other distros.
2026-04-22 10:23:09 +10:00
renovate[bot] b842e54091
Update dependency arch/python-textual to v8.2.4 (#4448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 19:52:17 +10:00
Softer 03b245a77a
Set console font automatically when selecting language (#4356)
* Set console font automatically when selecting language

Add console_font field to languages.json and Language dataclass.
When a language is activated, setfont is called automatically,
falling back to default8x16 on error or for languages without
a custom font. Also activate translation when loading language
from config file.

* Support FONT environment variable for console font override

When FONT env var is set, use it as the console font instead of the language-specific font mapping from languages.json. The font is applied at startup and preserved across language switches.

On exit (success or failure), restore the console font to default8x16.

* CI checks fix

* Try to restore original console font with setfont -O

* Fix for pylint

* Restore console font before Textual exits application mode

Move font set/restore into Textual lifecycle to prevent color
artifacts from 256/512 glyph transitions. Apply FONT env var
and language font in on_mount, restore in _on_exit_app.

Skip font change when loading language from config (set_font=False)
to defer it until TUI starts.

* Fall back to language font mapping when FONT env var is invalid

Add _using_env_font flag to skip mapping only when FONT was
successfully applied. Show info message after TUI exits if FONT
could not be set.

* Use tempfile for console font backup files

* Fix linter errors: use mkstemp, close fds, add @override

* Fix ruff formatting

* Move font state from module singleton into TranslationHandler

* Refactor font handling per review feedback

* Make font methods members of TranslationHandler, skip on non-ISO

* ci: trigger tests

* Move running_from_iso import to module level

No circular dep, simpler than per-method local imports.

* Add explicit ISO guard to restore_console_font

Matches the existing guards in _set_font and save_console_font.
Behaviour was already safe implicitly via _font_backup=None, but
the explicit check makes intent obvious at the call site.

* Skip apply_console_font off-ISO

* Use list form for setfont SysCommand calls
2026-04-21 18:36:29 +10:00
Dylan M. Taylor 7d10f9e08b
ci: Strip Arch pkgrel from Repology versions in Renovate config (#4453)
* ci: Strip Arch pkgrel from Repology versions in Renovate config

* ci: Use .+ instead of .* in extractVersionTemplate

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>

---------

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>
2026-04-21 15:18:25 +10:00
Anton Hvornum d80bdb4fad
Bumping version to: 4.3 (#4452) 2026-04-20 22:31:22 +02:00
Anton Hvornum 68d137cdce
Bumping version to: 4.3 2026-04-20 22:28:35 +02:00
Softer 82a46c66ca
Fix shell injection in user_set_shell and chown (#4443)
Use argv list with run() instead of sh -c with f-string interpolation,
fix mutable default argument in chown, add debug logging on failure.
2026-04-20 18:36:49 +10:00
Dhruv 86dc1bbc12
locale(hindi): Completed the hindi translation locale (#4446)
* locale: ran generate_locale before making change

* locale: first 1000 confirmed everything is fine

* locale(hi): fixed rest of the fuzzy and empty strings

* locale(hi): ran locale generator
2026-04-20 18:29:26 +10:00
codefiles cd62eff4a7
Fix copying into target directory (#4441) 2026-04-18 12:05:00 +10:00
Softer e8ea33c41c
Enable power management services after package installation (#4440) 2026-04-18 12:04:14 +10:00
Softer 4fcef35af0
Add optional "Additional fonts" selection to Applications menu (#4420)
* Add Fonts application with multi-select for emoji and CJK packages

* Rename to Additional fonts and add package descriptions

* Add noto-fonts to font package selection

* Move font descriptions into FontPackage enum method

* ci: trigger tests

* Add ttf-liberation and ttf-dejavu to font selection
2026-04-18 12:02:47 +10:00
codefiles 9fdd7eb12e
Refactor EncryptionType (#4438)
* Use UPPER_CASE for EncryptionType

* Use StrEnum for EncryptionType
2026-04-17 10:08:23 +10:00
codefiles 8fe8d4e35f
Pin linkify-it-py to Arch Linux package version (#4437) 2026-04-17 10:07:57 +10:00
renovate[bot] 02faf9fbb9
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.11 (#4436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 10:07:09 +10:00
renovate[bot] 9b9f9d6ac0
Update dependency ruff to v0.15.11 (#4435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 10:06:51 +10:00
Dylan M. Taylor 98d72a6b57
ci: Update renovate to track Arch Linux repos using repology (#4434) 2026-04-16 19:17:07 +10:00
Daniel Girtler 06488dfb9a
Fix table column error (#4406) 2026-04-16 07:30:38 +02:00
Daniel Girtler 29588fadee
Fix encrypted partition selection (#4407) 2026-04-16 07:29:53 +02:00
codefiles 3cabe860ae
Refactor LsblkInfo field validators (#4428) 2026-04-16 14:06:23 +10:00
codefiles 7ea171369a
Refactor PartitionType (#4432)
* Use UPPER_CASE for PartitionType

* Use StrEnum for PartitionType
2026-04-16 14:05:08 +10:00
164 changed files with 10654 additions and 6114 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

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

View File

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

View File

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

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

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

View File

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

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

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

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

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

View File

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

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:
@ -143,6 +253,7 @@ class ArchConfig:
if archinstall_lang := args_config.get('archinstall-language', None):
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
translation_handler.activate(arch_config.archinstall_language, set_font=False)
if disk_config := args_config.get('disk_config', {}):
enc_password = args_config.get('encryption_password', '')
@ -260,13 +371,13 @@ class ArchConfig:
class ArchConfigHandler:
def __init__(self) -> None:
self._parser: ArgumentParser = self._define_arguments()
args: Arguments = self._parse_args()
self._args = args
self._add_sub_parsers()
self._args: Arguments = self._parse_args()
config = self._parse_config()
try:
self._config = ArchConfig.from_config(config, args)
self._config = ArchConfig.from_config(config, self._args)
self._config.version = get_version()
except ValueError as err:
warn(str(err))
@ -292,8 +403,13 @@ class ArchConfigHandler:
def print_help(self) -> None:
self._parser.print_help()
def _add_sub_parsers(self) -> None:
subparsers = self._parser.add_subparsers(dest='command', help='Available subcommands')
_ = subparsers.add_parser(SubCommand.SHARE_LOG.value, help='Upload log file to public server')
def _define_arguments(self) -> ArgumentParser:
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'-v',
'--version',
@ -429,7 +545,6 @@ class ArchConfigHandler:
default=False,
help='Enabled verbose options',
)
return parser
def _parse_args(self) -> Arguments:

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

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

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
@ -44,8 +44,8 @@ class SysCommandWorker:
self._trace_log_pos = 0
self.poll_object = epoll()
self.child_fd: int | None = None
self.started: float | None = None
self.ended: float | None = None
self.started = False
self.ended = False
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
def __contains__(self, key: bytes) -> bool:
@ -117,7 +117,7 @@ class SysCommandWorker:
def is_alive(self) -> bool:
self.poll()
if self.started and self.ended is None:
if self.started and not self.ended:
return True
return False
@ -173,11 +173,11 @@ class SysCommandWorker:
self.peak(output)
self._trace_log += output
except OSError:
self.ended = time.time()
self.ended = True
break
if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = time.time()
self.ended = True
try:
wait_status = os.waitpid(self.pid, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
@ -215,7 +215,7 @@ class SysCommandWorker:
# Only parent process moves back to the original working directory
os.chdir(old_dir)
self.started = time.time()
self.started = True
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True

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
@ -250,7 +250,7 @@ class DeviceHandler:
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
# Force create
options.append('-F')
case FilesystemType.FAT12 | FilesystemType.FAT16 | FilesystemType.FAT32:
case _ if fs_type.is_fat():
mkfs_type = 'fat'
# Set FAT size
options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))

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
@ -280,7 +281,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
if enc_config.encryption_password:
output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n'
if enc_type != EncryptionType.NoEncryption:
if enc_type != EncryptionType.NO_ENCRYPTION:
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
if enc_config.partitions:
@ -302,7 +303,7 @@ async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | N
dev = device_handler.get_device(device.path)
if dev and dev.partition_infos:
return FormattedOutput.as_table(dev.partition_infos)
return as_table(dev.partition_infos)
return None
if preset is None:
@ -503,7 +504,7 @@ def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModifi
# boot partition
return PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.Primary,
type=PartitionType.PRIMARY,
start=start,
length=size,
mountpoint=Path('/boot'),
@ -655,7 +656,7 @@ async def suggest_single_disk_layout(
root_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.Primary,
type=PartitionType.PRIMARY,
start=root_start,
length=root_length,
mountpoint=Path('/') if not using_subvolumes else None,
@ -680,7 +681,7 @@ async def suggest_single_disk_layout(
home_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.Primary,
type=PartitionType.PRIMARY,
start=home_start,
length=home_length,
mountpoint=Path('/home'),
@ -765,7 +766,7 @@ async def suggest_multi_disk_layout(
# add root partition to the root device
root_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.Primary,
type=PartitionType.PRIMARY,
start=root_start,
length=root_length,
mountpoint=Path('/'),
@ -787,7 +788,7 @@ async def suggest_multi_disk_layout(
# add home partition to home device
home_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.Primary,
type=PartitionType.PRIMARY,
start=home_start,
length=home_length,
mountpoint=Path('/home'),

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

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:
@ -139,7 +139,7 @@ class FilesystemHandler:
self._format_lvm_vols(self._disk_config.lvm_config)
def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None:
if enc_config.encryption_type == EncryptionType.LvmOnLuks:
if enc_config.encryption_type == EncryptionType.LVM_ON_LUKS:
enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False)
self._setup_lvm(lvm_config, enc_mods)
@ -148,7 +148,7 @@ class FilesystemHandler:
# Don't close LVM or LUKS during setup - keep everything active
# The installation phase will handle unlocking and mounting
# Closing causes "parent leaked" and lvchange errors
elif enc_config.encryption_type == EncryptionType.LuksOnLvm:
elif enc_config.encryption_type == EncryptionType.LUKS_ON_LVM:
self._setup_lvm(lvm_config)
enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False)
self._format_lvm_vols(lvm_config, enc_vols)

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:
@ -58,7 +58,7 @@ class DiskSegment:
part_mod = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType._Unknown,
type=PartitionType._UNKNOWN,
start=self.segment.start,
length=self.segment.length,
)
@ -479,7 +479,7 @@ class PartitioningList(ListManager[DiskSegment]):
sector_size = device_info.sector_size
text = tr('Selected free space segment on device {}:').format(device_info.path) + '\n\n'
free_space_table = FormattedOutput.as_table([free_space])
free_space_table = as_table([free_space])
prompt = text + free_space_table + '\n'
max_sectors = free_space.length.format_size(Unit.sectors, sector_size)
@ -527,7 +527,7 @@ class PartitioningList(ListManager[DiskSegment]):
partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.Primary,
type=PartitionType.PRIMARY,
start=free_space.start,
length=length,
fs_type=fs_type,

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

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

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

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

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

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

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

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

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

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

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

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

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

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