Merge branch 'master' into feat/add_plymouth

This commit is contained in:
Mário Victor Ribeiro Silva 2026-06-09 08:15:45 -03:00
commit e61b81d5cc
60 changed files with 6161 additions and 6779 deletions

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- run: pacman --noconfirm -Syu bandit
- name: Security checkup with Bandit
run: bandit -r archinstall || exit 0

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Prepare arch
run: |
pacman-key --init

View File

@ -21,7 +21,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- name: Install pre-dependencies
run: |
@ -30,7 +30,7 @@ jobs:
run: |
sphinx-build docs _build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4
if: ${{ github.event_name != 'pull_request' }}
with:
publish_branch: gh-pages

View File

@ -26,13 +26,13 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- run: pwd
- run: find .
- 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

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Prepare arch
run: |
pacman-key --init

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Prepare arch
run: |
pacman-key --init

View File

@ -7,7 +7,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Prepare arch
run: |
pacman-key --init

View File

@ -11,7 +11,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Prepare arch
run: |
pacman-key --init

View File

@ -18,7 +18,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Prepare arch
run: |
pacman-key --init

View File

@ -4,6 +4,6 @@ jobs:
ruff_format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
- run: ruff format --diff

View File

@ -4,5 +4,5 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0

View File

@ -15,7 +15,7 @@ jobs:
name: Validate translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Run translation checks

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@df4cb1c069e1874edd31b4311f1884172cec0e10 # 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.14
rev: v0.15.16
hooks:
# fix unused imports and sort them
- id: ruff

View File

@ -7,7 +7,7 @@ from archinstall.default_profiles.profile import CustomSetting, DisplayServerTyp
class NiriProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Niri',
'niri',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,

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

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

View File

@ -11,7 +11,6 @@ from archinstall.lib.crypt import encrypt
from archinstall.lib.log import debug, logger, warn
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password, prompt_dir
from archinstall.lib.models.network import NetworkConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_key_value_pair
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
@ -83,13 +82,10 @@ class ConfigurationOutput:
return simple_summary
async def confirm_config(self, show_install_warnings: bool = False) -> bool:
async def confirm_config(self) -> bool:
header = f'{tr("The specified configuration will be applied")}. '
header += tr('Would you like to continue?') + '\n'
if show_install_warnings:
header += self._render_install_warnings()
group = MenuItemGroup.yes_no()
group.set_preview_for_all(lambda x: self.user_config_to_json())
@ -107,22 +103,6 @@ class ConfigurationOutput:
return True
def get_install_warnings(self) -> list[str]:
warnings: list[str] = []
if not isinstance(self._config.network_config, NetworkConfiguration):
warnings.append(tr('Warning: no network configuration selected. Network will need to be set up manually on the installed system.'))
return warnings
def _render_install_warnings(self) -> str:
warnings = self.get_install_warnings()
if not warnings:
return ''
return '\n' + '\n'.join(f'[yellow]{w}[/]' for w in warnings) + '\n'
def _is_valid_path(self, dest_path: Path) -> bool:
dest_path_ok = dest_path.exists() and dest_path.is_dir()
if not dest_path_ok:

View File

@ -149,7 +149,11 @@ class Luks2:
'luks2',
]
result = run(cmd, input_data=passphrase)
try:
result = run(cmd, input_data=passphrase)
except CalledProcessError as err:
output = err.stdout.decode().rstrip()
raise DiskError(f'Could not unlock luks2 device "{self.luks_dev_path}": {output}')
debug(f'cryptsetup open output: {result.stdout.decode().rstrip()}')

View File

@ -33,7 +33,7 @@ from archinstall.lib.pacman.pacman_menu import PacmanMenu
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_table
from archinstall.tui.components import tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult
class GlobalMenu(AbstractMenu[None]):
@ -494,21 +494,48 @@ class GlobalMenu(AbstractMenu[None]):
return None
def _prev_install_invalid_config(self, item: MenuItem) -> str | None:
if missing := self._missing_configs():
text = tr('Missing configurations:\n')
for m in missing:
text += f'- {m}\n'
return text[:-1] # remove last new line
def _get_install_warnings(self) -> list[str]:
warnings: list[str] = []
if error := self._validate_bootloader():
return tr('Invalid configuration: {}').format(error)
if not isinstance(self._arch_config.network_config, NetworkConfiguration):
warnings.append(tr('No network configuration selected. Network will need to be set up manually on the installed system.'))
return warnings
def _prev_install_invalid_config(self, item: MenuItem) -> PreviewResult | 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')
config_output = ConfigurationOutput(self._arch_config)
warnings = self._get_install_warnings()
messages: list[tuple[str, MsgLevelType]] = []
errors = ''
if missing := self._missing_configs():
errors += f'{tr("Missing configurations:")}\n'
errors += '\n'.join(f'- {m}' for m in missing)
disk_item = self._item_group.find_by_key('disk_config')
if disk_item.has_value():
if error := self._validate_bootloader():
if errors:
errors += '\n\n'
errors += f'{tr("Invalid configuration:")}\n- {error}'
if errors:
messages.append((errors, MsgLevelType.MsgError))
else:
messages.append((tr('Ready to install'), MsgLevelType.MsgInfo))
if warnings:
text = f'{tr("Warnings:")}\n' + '\n'.join(f'- {w}' for w in warnings)
messages.append((text, MsgLevelType.MsgWarning))
if not errors:
summary = config_output.as_summary()
if summary:
messages.append((summary, MsgLevelType.MsgNone))
return PreviewResult(messages)
def _prev_profile(self, item: MenuItem) -> str | None:
profile_config: ProfileConfiguration | None = item.value

View File

@ -5,7 +5,8 @@ from pathlib import Path
from archinstall.lib.menu.helpers import Confirmation, Input
from archinstall.lib.models.users import Password, PasswordStrength
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import InputInfo, InputInfoType, tui
from archinstall.tui.components import InputInfo, tui
from archinstall.tui.menu_item import MsgLevelType
from archinstall.tui.result import ResultType
@ -20,11 +21,11 @@ async def get_password(
return None
strength = PasswordStrength.strength(value)
if strength in (PasswordStrength.VERY_WEAK, PasswordStrength.WEAK):
return InputInfo(message=tr('Password strength: Weak'), info_type=InputInfoType.MsgError)
return InputInfo(message=tr('Password strength: Weak'), msg_level=MsgLevelType.MsgError)
elif strength == PasswordStrength.MODERATE:
return InputInfo(message=tr('Password strength: Moderate'), info_type=InputInfoType.MsgWarning)
return InputInfo(message=tr('Password strength: Moderate'), msg_level=MsgLevelType.MsgWarning)
elif strength == PasswordStrength.STRONG:
return InputInfo(message=tr('Password strength: Strong'), info_type=InputInfoType.MsgInfo)
return InputInfo(message=tr('Password strength: Strong'), msg_level=MsgLevelType.MsgInfo)
return None
while True:

View File

@ -40,7 +40,7 @@ class ProfileConfiguration(SubConfig):
out.append(self.profile.name)
if self.gfx_driver:
out.append(tr('{} grphics driver').format(self.gfx_driver.value))
out.append(tr('{} graphics driver').format(self.gfx_driver.value))
if self.greeter:
out.append(tr('{} greeter').format(self.greeter.value))

View File

@ -1,4 +1,3 @@
from asyncio import sleep
from dataclasses import dataclass
from pathlib import Path
from typing import assert_never, override
@ -109,36 +108,25 @@ class WifiHandler(InstanceRunnable[bool]):
debug(f'Found wifi interface: {wifi_iface}')
async def get_wifi_networks() -> MenuItemGroup:
debug('Scanning Wifi networks')
result = self._wpa_cli('scan', wifi_iface)
wifi_networks = await self._scan_wifi(wifi_iface)
if not result.success:
debug(f'Failed to scan wifi networks: {result.error}')
return MenuItemGroup([])
if not wifi_networks:
debug('No networks found')
await NotifyScreen(header=tr('No wifi networks found')).run()
tui.exit(Result.false())
return False
await sleep(5)
wifi_networks = self._get_scan_results(wifi_iface)
items = [MenuItem(network.ssid, value=network) for network in wifi_networks]
return MenuItemGroup(items)
items = [MenuItem(network.ssid, value=network) for network in wifi_networks]
result = await TableSelectionScreen[WifiNetwork](
header=tr('Select wifi network to connect to'),
loading_header=tr('Scanning wifi networks...'),
group_callback=get_wifi_networks,
group=MenuItemGroup(items),
allow_skip=True,
allow_reset=True,
).run()
match result.type_:
case ResultType.Selection:
if not result.has_data():
debug('No networks found')
await NotifyScreen(header=tr('No wifi networks found')).run()
tui.exit(Result.false())
return False
network = result.get_value()
case ResultType.Skip | ResultType.Reset:
tui.exit(Result.false())
@ -184,6 +172,18 @@ class WifiHandler(InstanceRunnable[bool]):
return True
async def _scan_wifi(self, wifi_iface: str) -> list[WifiNetwork]:
debug('Scanning Wifi networks')
scan_result = self._wpa_cli('scan', wifi_iface)
if not scan_result.success:
debug(f'Failed to scan wifi networks: {scan_result.error}')
return []
await LoadingScreen(timer=5, header=tr('Scanning wifi networks...')).run()
return self._get_scan_results(wifi_iface)
async def _notify_failure(self) -> None:
await NotifyScreen(header=tr('Failed setting up wifi')).run()

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -70,25 +70,10 @@ cmd_check_no_tr_fstring() {
echo "No tr(f-string) anti-pattern found."
}
cmd_check_pot_freshness() {
# msgcmp (not diff) because base.pot carries legacy stale entries from
# --join-existing; diff would always fail until a full cleanup is done.
echo "Checking base.pot for missing strings..."
find . -type f -iname '*.py' | sort \
| xargs xgettext --no-location --omit-header --keyword='tr' \
-d base -o /tmp/generated.pot
if ! msgcmp --use-untranslated locales/base.pot /tmp/generated.pot; then
echo "ERROR: base.pot is missing strings - run: locales_generator.sh all" >&2
return 1
fi
echo "base.pot contains all translatable strings."
}
cmd_check() {
local failed=0
cmd_check_po_syntax || failed=1
cmd_check_no_tr_fstring || failed=1
cmd_check_pot_freshness || failed=1
if [ "$failed" -eq 1 ]; then
echo "Some translation checks failed." >&2
exit 1

File diff suppressed because it is too large Load Diff

View File

@ -2230,3 +2230,162 @@ msgstr "Налаштування входу з пристроєм двофакт
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr "Стандартно: {}мс, Рекомендований діапазон - 1000-60000"
msgid "ArchInstall Language"
msgstr "Мова ArchInstall"
msgid "Version"
msgstr "Версія"
msgid "Installation Script"
msgstr "Скрипт встановлення"
msgid "Application"
msgstr "Додаток"
msgid "Services"
msgstr "Служби"
msgid "Custom commands"
msgstr "Користувацькі команди"
msgid "Users"
msgstr "Користувачі"
msgid "Root encrypted password"
msgstr "Зашифрований пароль root"
msgid "Audio server \"{}\""
msgstr "Аудіосервер \"{}\""
msgid "Firewall \"{}\""
msgstr "Брандмауер \"{}\""
msgid "Bootloader \"{}\""
msgstr "Завантажувач \"{}\""
msgid "Power management \"{}\""
msgstr "Керування живленням \"{}\""
msgid "Btrfs snapshot \"{}\""
msgstr "Знімок Btrfs \"{}\""
msgid "Keyboard layout \"{}\""
msgstr "Розкладка клавіатури \"{}\""
msgid "Locale language \"{}\""
msgstr "Мова локалізації \"{}\""
msgid "Locale encoding \"{}\""
msgstr "Кодування локалізації \"{}\""
msgid "Console font \"{}\""
msgstr "Шрифт консолі \"{}\""
msgid "Mirror regions \"{}\""
msgstr "Регіони дзеркал \"{}\""
msgid "Optional repositories \"{}\""
msgstr "Необов'язкові репозиторії \"{}\""
msgid "Extra fonts \"{}\""
msgstr "Додаткові шрифти \"{}\""
msgid "Bluetooth enabled"
msgstr "Bluetooth увімкнено"
msgid "Color enabled"
msgstr "Колір увімкнено"
msgid "Print service enabled"
msgstr "Службу друку увімкнено"
msgid "Zram enabled"
msgstr "Zram увімкнено"
msgid "UKI enabled"
msgstr "UKI увімкнено"
msgid "Root password set"
msgstr "Пароль root встановлено"
msgid "U2F set up"
msgstr "U2F налаштовано"
msgid "LVM set up"
msgstr "LVM налаштовано"
msgid "Custom servers set up"
msgstr "Користувацькі сервери налаштовано"
msgid "Custom repositories set up"
msgstr "Користувацькі репозиторії налаштовано"
msgid "Configured {} user(s)"
msgstr "Налаштовано користувачів: {}"
msgid "Zram algorithm {}"
msgstr "Алгоритм Zram {}"
msgid "Devices {}"
msgstr "Пристрої {}"
msgid "{} grphics driver"
msgstr "Графічний драйвер {}"
msgid "{} encryption"
msgstr "Шифрування {}"
msgid "{} layout"
msgstr "Розмітка {}"
msgid "{} greeter"
msgstr "Менеджер входу {}"
msgid "About to upload \"{}\" to the publicly accessible {}"
msgstr "Зараз \"{}\" буде вивантажено на загальнодоступний {}"
msgid "Log uploaded successfully. URL: {}"
msgstr "Журнал успішно вивантажено. URL: {}"
msgid "Failed to upload log."
msgstr "Не вдалося вивантажити журнал."
msgid "Manual"
msgstr "Вручну"
msgid "Pre-mount"
msgstr "Передмонтування"
msgid "Default"
msgstr "Типово"
msgid "Warnings:"
msgstr "Попередження:"
msgid "Missing configurations:"
msgstr "Відсутні конфігурації:"
msgid "Invalid configuration:"
msgstr "Неправильна конфігурація:"
msgid "Do you want to continue?"
msgstr "Бажаєте продовжити?"
msgid "Enter a repository name"
msgstr "Введіть назву репозиторію"
msgid "Use standalone iwd"
msgstr "Використовувати автономний iwd"
msgid "{} needs access to your seat"
msgstr "{} потребує доступу до вашого seat"
msgid "Choose an option how to give {} access to your hardware"
msgstr "Виберіть, як надати {} доступ до вашого обладнання"
msgid "collection of hardware devices i.e. keyboard, mouse"
msgstr "набір апаратних пристроїв, напр. клавіатура, миша"
msgid "No network configuration selected. Network will need to be set up manually on the installed system."
msgstr "Конфігурацію мережі не обрано. Мережу доведеться налаштувати вручну на встановленій системі."

View File

@ -231,7 +231,7 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None:
if not arch_config_handler.args.silent:
aborted = False
res: bool = tui.run(lambda: config.confirm_config(show_install_warnings=True))
res: bool = tui.run(config.confirm_config)
if not res:
debug('Installation aborted')

View File

@ -77,7 +77,7 @@ async def main(arch_config_handler: ArchConfigHandler | None = None) -> None:
if not arch_config_handler.args.silent:
aborted = False
res: bool = tui.run(lambda: config.confirm_config(show_install_warnings=True))
res: bool = tui.run(config.confirm_config)
if not res:
debug('Installation aborted')

View File

@ -2,9 +2,9 @@ import sys
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, replace
from enum import Enum, auto
from typing import Any, ClassVar, Literal, TypeVar, cast, override
from rich.text import Text
from textual import work
from textual.app import App, ComposeResult
from textual.binding import Binding, BindingsMap
@ -21,12 +21,28 @@ from textual.worker import WorkerCancelled
from archinstall.lib.log import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult
from archinstall.tui.result import Result, ResultType
ValueT = TypeVar('ValueT')
def _update_preview(widget: Label, result: str | PreviewResult | None) -> None:
if result is None:
widget.update('')
return
if isinstance(result, str):
widget.update(result)
else:
text = Text()
for i, (message, level) in enumerate(result.messages):
if i > 0:
text.append('\n\n')
text.append(message, style=level.style())
widget.update(text)
def _translate_bindings(source: BindingsMap | None, target: BindingsMap) -> None:
"""Translate binding descriptions from source to target.
@ -361,13 +377,9 @@ class OptionListScreen(BaseScreen[ValueT]):
item = self._group.find_by_id(item_id)
if item.preview_action is not None:
maybe_preview = item.preview_action(item)
if maybe_preview is not None:
preview_widget.update(maybe_preview)
return
preview_widget.update('')
_update_preview(preview_widget, item.preview_action(item))
else:
_update_preview(preview_widget, None)
class _SelectionList(SelectionList[ValueT]):
@ -614,12 +626,9 @@ class SelectListScreen(BaseScreen[ValueT]):
preview_widget = self.query_one('#preview_content', Label)
if item.preview_action is not None:
maybe_preview = item.preview_action(item)
if maybe_preview is not None:
preview_widget.update(maybe_preview)
return
preview_widget.update('')
_update_preview(preview_widget, item.preview_action(item))
else:
_update_preview(preview_widget, None)
# DEPRECATED: Removed when switching to async
@ -735,13 +744,8 @@ class ConfirmationScreen(BaseScreen[ValueT]):
if self._preview_header is not None:
preview = self.query_one('#preview_content', Label)
if focused.preview_action is None:
preview.update('')
else:
text = focused.preview_action(focused)
if text is not None:
preview.update(text)
result = focused.preview_action(focused) if focused.preview_action else None
_update_preview(preview, result)
else:
button.remove_class('-active')
@ -768,16 +772,10 @@ class NotifyScreen(ConfirmationScreen[ValueT]):
super().__init__(group, header)
class InputInfoType(Enum):
MsgInfo = auto()
MsgWarning = auto()
MsgError = auto()
@dataclass
class InputInfo:
message: str
info_type: InputInfoType
msg_level: MsgLevelType
class InputScreen(BaseScreen[str]):
@ -889,11 +887,11 @@ class InputScreen(BaseScreen[str]):
result = self._info_callback(event.value)
if result:
css_class = ''
if result.info_type == InputInfoType.MsgError:
if result.msg_level == MsgLevelType.MsgError:
css_class = 'input-hint-msg-error'
elif result.info_type == InputInfoType.MsgWarning:
elif result.msg_level == MsgLevelType.MsgWarning:
css_class = 'input-hint-msg-warning'
elif result.info_type == InputInfoType.MsgInfo:
elif result.msg_level == MsgLevelType.MsgInfo:
css_class = 'input-hint-msg-info'
info_label.update(result.message)
info_label.set_classes(css_class)
@ -1138,13 +1136,7 @@ class TableSelectionScreen(BaseScreen[ValueT]):
return
preview_widget = self.query_one('#preview_content', Label)
maybe_preview = item.preview_action(item)
if maybe_preview is not None:
preview_widget.update(maybe_preview)
return
preview_widget.update('')
_update_preview(preview_widget, item.preview_action(item))
def _set_cursor(self, row_index: int) -> None:
data_table = self.query_one(DataTable)
@ -1279,6 +1271,7 @@ class _AppInstance(App[ValueT]):
background: black;
border-left: vkey white 20%;
}
"""
def __init__(self, main: InstanceRunnable[ValueT] | Callable[[], Awaitable[ValueT]]) -> None:

View File

@ -1,12 +1,44 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import dataclass, field
from enum import Enum
from enum import Enum, StrEnum, auto
from functools import cached_property
from typing import Any, ClassVar, Self, override
from archinstall.lib.translationhandler import tr
class MsgLevelStyle(StrEnum):
White = 'white'
Green = 'green'
Yellow = 'bright_yellow'
Red = 'red'
class MsgLevelType(Enum):
MsgNone = auto()
MsgInfo = auto()
MsgWarning = auto()
MsgError = auto()
def style(self) -> MsgLevelStyle:
match self:
case MsgLevelType.MsgNone:
return MsgLevelStyle.White
case MsgLevelType.MsgInfo:
return MsgLevelStyle.Green
case MsgLevelType.MsgWarning:
return MsgLevelStyle.Yellow
case MsgLevelType.MsgError:
return MsgLevelStyle.Red
@dataclass
class PreviewResult:
messages: list[tuple[str, MsgLevelType]]
@dataclass
class MenuItem:
text: str
@ -18,7 +50,7 @@ class MenuItem:
dependencies: list[str | Callable[[], bool]] = field(default_factory=list)
dependencies_not: list[str] = field(default_factory=list)
display_action: Callable[[Any], str] | None = None
preview_action: Callable[[Self], str | None] | None = None
preview_action: Callable[[Self], str | PreviewResult | None] | None = None
key: str | None = None
_id: str = ''

View File

@ -133,7 +133,10 @@
"ntp": true,
"offline": false,
"packages": [],
"parallel downloads": 0,
"pacman_config": {
"color": false,
"parallel_downloads": 5
},
"profile_config": {
"gfx_driver": "All open-source (default)",
"greeter": "sddm",

View File

@ -21,7 +21,7 @@ dependencies = [
"pyparted==3.13.0",
"pydantic==2.13.4",
"cryptography==48.0.0",
"textual==8.2.6",
"textual==8.2.7",
"markdown-it-py==4.0.0",
"linkify-it-py==2.1.0",
]
@ -37,7 +37,7 @@ dev = [
"mypy==2.1.0",
"flake8==7.3.0",
"pre-commit==4.6.0",
"ruff==0.15.14",
"ruff==0.15.16",
"pylint==4.0.5",
"pytest==9.0.3",
"hypothesis>=6.152.4",
@ -61,6 +61,7 @@ include-package-data = true
"**/*.po",
"**/*.pot",
"**/*.json",
"**/*.kdl",
]
[tool.setuptools.package-dir]
@ -69,7 +70,10 @@ archinstall = "archinstall"
[tool.mypy]
python_version = "3.14"
files = "."
exclude = "^build/"
exclude = [
"^build/",
"^test_tooling/",
]
disallow_any_explicit = false
disallow_any_expr = false
disallow_any_unimported = true

View File

@ -0,0 +1,12 @@
# To build
mkosi build -B
# To run
mkosi qemu \
--drive=archinstall_small:25G \
-- \
-device nvme,serial=archinstall_small,drive=archinstall_small
*note: in order to boot the installation, we need to disable UKI being added to -kernel. I don't know of a way to do this yet, unless we tinker with mkosi/qemu.py - https://github.com/Torxed/mkosi/commit/6f3c20802bd73f88b672cc96ef0db1e542084316 - in which case we could do: `mkosi qemu --drive=archinstall_small:25G -- -device nvme,serial=archinstall_small,drive=archinstall_small,bootindex=0 -kernel none` but it still won't boot properly.*

View File

View File

@ -0,0 +1,50 @@
[Distribution]
Distribution=arch
# LocalMirror=file:///var/lib/localmirror
[Output]
Format=uki
# Format=disk
[Include]
Include=mkosi-vm
[Validation]
SecureBoot=false
SecureBootAutoEnroll=false
Sign=false
# Signing artifacts (hashsums etc) using a GPG key:
# Key=D4B58E897A929F2E
# Signing secure boot using PIV on yubikey:
# SecureBoot=true
# SecureBootKey=pkcs11:
# SecureBootCertificate=secureboot.crt
[Content]
Packages=
pacman
archlinux-keyring
amd-ucode
intel-ucode
# tpm2-tss
# libfido2
# libp11-kit
WithDocs=false
RootPassword=toor
Timezone=Europe/Stockholm
Keymap=sv-latin1
InitrdProfiles=network
[Config]
Profiles=archinstall
[Build]
Incremental=true
ToolsTree=default
ToolsTreeProfiles=devel,misc,package-manager,runtime,gui
WithNetwork=yes
[Runtime]
Console=gui
CPUs=4
RAM=8G

View File

@ -0,0 +1,24 @@
[Match]
# Matching with "Type=ether" causes issues with containers because it also matches virtual Ethernet interfaces (veth*).
# See https://bugs.archlinux.org/task/70892
# Instead match by globbing the network interface name.
Name=en*
Name=eth*
[Link]
RequiredForOnline=routable
[Network]
DHCP=yes
MulticastDNS=yes
# systemd-networkd does not set per-interface-type default route metrics
# https://github.com/systemd/systemd/issues/17698
# Explicitly set route metric, so that Ethernet is preferred over Wi-Fi and Wi-Fi is preferred over mobile broadband.
# Use values from NetworkManager. From nm_device_get_route_metric_default in
# https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/src/core/devices/nm-device.c
[DHCPv4]
RouteMetric=100
[IPv6AcceptRA]
RouteMetric=100

View File

@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=-/usr/bin/agetty --noreset --noclear --autologin root - ${TERM}

View File

@ -0,0 +1,5 @@
cd archinstall-git
rm -rf dist
uv build --no-build-isolation --wheel
uv pip install dist/*.whl --break-system-packages --system --no-build --no-deps

View File

@ -0,0 +1 @@
enable systemd-networkd.service

View File

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
git clone https://github.com/archlinux/archinstall.git /root/archinstall-git
(cd /root/archinstall-git && git checkout master)
# TODO: Set geo-mirrors statically instead
curl -s "https://archlinux.org/mirrorlist/?country=SE&protocol=https&use_mirror_status=on" | sed -e 's/^#Server/Server/' -e '/^#/d' | rankmirrors -n 5 - > /etc/pacman.d/mirrorlist
pacman-key --init
pacman-key --populate archlinux

View File

@ -0,0 +1,56 @@
[Content]
Packages=
acpid
intel-media-driver
linux-firmware
linux-firmware-intel
ca-certificates-mozilla
ca-certificates-utils
nano
gvfs
noto-fonts
cantarell-fonts
ttf-dejavu
polkit
bash
bzip2
coreutils
file
filesystem
findutils
gawk
gcc-libs
gettext
glibc
grep
gzip
iproute2
iputils
licenses
pciutils
procps-ng
psmisc
sed
tar
linux
diffutils
less
strace
util-linux
xz
pacman-contrib
gcc
git
pkgconfig
python
python-pip
python-uv
python-setuptools
python-pyparted
python-pydantic
python-textual
dosfstools
btrfs-progs
arch-install-scripts
WithDocs=false
KernelCommandLine=quiet splash

View File

@ -0,0 +1 @@
13

View File

@ -0,0 +1,18 @@
# Qemu helper
Can be used with the `mkosi` test tooling
After `mkosi -B build` has been executed, run the following:
python test_tooling/qemu/qemu.py \
--uki ./test_tooling/mkosi/mkosi.output/image_13.efi \
--harddrive ~/test.qcow2:15G \
--harddrive ~/test_large.qcow2:25G
And install using `archinstall`, after the machine has been shutdown, run:
python test_tooling/qemu/qemu.py \
--harddrive ~/test.qcow2:15G \
--harddrive ~/test_large.qcow2:25G
As this will boot EFI mode with just the harddrives to verify the installation.

483
test_tooling/qemu/qemu.py Normal file
View File

@ -0,0 +1,483 @@
import getpass
import grp
import hashlib
import os
import pathlib
import re
import shlex
import shutil
import subprocess
import sys
import time
from argparse import ArgumentParser
from collections.abc import Iterator
from select import EPOLLHUP, EPOLLIN, epoll
from shutil import which
from types import TracebackType
from typing import Any, Self, override
class RequirementError(Exception):
pass
class ArgumentError(Exception):
pass
def get_master(interface):
master_path = pathlib.Path(f'/sys/class/net/{interface}/master')
return master_path.readlink().name if master_path.exists() else None
def gray(text):
return f'\033[38;5;246m{text}\033[0m'
def orange(text):
return f'\033[38;5;208m{text}\033[0m'
def red(text):
return f'\033[31m{text}\033[0m'
sudo_password = None # Gets populated later
harddrives = {}
username = getpass.getuser()
groupname = grp.getgrgid(os.getgid()).gr_name
# https://stackoverflow.com/a/43627833/929999
_VT100_ESCAPE_REGEX = r'\x1B\[[?0-9;]*[a-zA-Z]'
_VT100_ESCAPE_REGEX_BYTES = _VT100_ESCAPE_REGEX.encode()
parser = ArgumentParser(description='A set of common parameters for the tooling', add_help=True)
# Defaults to the order of which the harddrives are defined.
boot_option = parser.add_mutually_exclusive_group()
boot_option.add_argument('--uki', help='Boot a UKI (EFI) image')
boot_option.add_argument('--kernel', help='Boot a Linux kernel')
boot_option.add_argument('--iso', help='Boot a ISO 9660')
networking = parser.add_argument_group('Networking', "Disables the default '-net nic -net user' network behavior of Qemu.")
networking.add_argument('--tap', nargs='?', help='Configures a TAP interface and passes it in as a virtio-net-pci.', default=None, type=str)
networking.add_argument('--tap-mac', nargs='?', help='MAC for the --tap interface', default='52:54:00:00:00:02')
networking.add_argument('--bridge', nargs='?', help='Configures a bridge, to which the --tap is added.', default=None, type=str)
networking.add_argument('--bridge-mac', nargs='?', help='MAC for the interface', default=None)
networking.add_argument('--bridge-master', nargs='?', help="Which interface to set as 'master' on the bridge.", default=None, type=str)
hardware = parser.add_argument_group('Hardware', 'General hardware specs for the virtual machine')
# To override the use of EFI boot (will not work with --uki for obvious reasons)
hardware.add_argument('--bios', action='store_true', help='Disables EFI (edk2/ovmf) and uses BIOS support instead', default=False)
hardware.add_argument('--memory', nargs='?', help='Ammount of memory to supply the machine', default=8192)
hardware.add_argument('--harddrive', action='append', help='Sets up one or more virtio-scsi-pci, size is defined by --harddrive test.qcow2:15G', type=str)
hardware.add_argument('--cpu', help='Sets the number of cores to allocate (default nproc -1)', type=str, default=os.cpu_count() - 1 if os.cpu_count() else 1)
hardware.add_argument('--resolution', help="Sets Qemu's VGA resolution", type=str, default='1920x1107')
kernel = parser.add_argument_group('Kernel', '--kernel specific arguments')
kernel.add_argument('--initrd', nargs='?', help='Defines which ISO to run (skips build all together)', default=None, type=pathlib.Path)
args, unknowns = parser.parse_known_args() # pylint: disable=redefined-outer-name
if args.bios and args.uki:
raise ArgumentError('Cannot boot a --uki image with --bios mode (at least not that I know of).')
if args.uki is None and args.kernel is None and args.iso is None and args.harddrive is None:
raise ArgumentError('Cannot boot this machine, define at least one of: --uki, --kernel, --iso, --harddrive')
if args.bridge is None and args.bridge_master:
raise ArgumentError('Cannot use --bridge-master without defining --bridge')
if args.bridge is None and args.bridge_mac:
raise ArgumentError('Cannot use --bridge-mac without defining --bridge')
elif args.bridge and args.bridge_mac is None:
args.bridge_mac = '52:54:00:00:00:1'
if args.tap and not args.bridge and get_master(args.tap) is None:
# We'll allow it, because maybe we're tesing what happens without networking, but the NIC exists.
# Or the user has some creative iptables/nftables forwarding.
print(orange('--tap does not have a master, consider adding --bridge or manual set a master using ip-link(8).'))
if args.tap is None and args.bridge:
print(orange("--bridge* arguments will be ignored since there's no --tap defined"))
elif args.tap and args.tap_mac is None:
args.tap_mac = '52:54:00:00:00:2'
class SysCallError(Exception):
def __init__(self, message: str, exit_code: int | None = None, worker_log: bytes = b'') -> None:
super().__init__(message)
self.message = message
self.exit_code = exit_code
self.worker_log = worker_log
def clear_vt100_escape_codes(data: bytes) -> bytes:
return re.sub(_VT100_ESCAPE_REGEX_BYTES, b'', data)
def locate_binary(name: str) -> str:
if path := which(name):
return path
raise RequirementError(f'Binary {name} does not exist.')
def _pid_exists(pid: int) -> bool:
try:
return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
except subprocess.CalledProcessError:
return False
class SysCommandWorker:
def __init__(
self,
cmd: str | list[str],
peek_output: bool | None = False,
environment_vars: dict[str, str] | None = None,
working_directory: str = './',
remove_vt100_escape_codes_from_lines: bool = True,
):
if isinstance(cmd, str):
cmd = shlex.split(cmd)
if cmd and not cmd[0].startswith(('/', './')): # Path() does not work well
cmd[0] = locate_binary(cmd[0])
self.cmd = cmd
self.peek_output = peek_output
# define the standard locale for command outputs. For now the C ascii one. Can be overridden
self.environment_vars = {'LC_ALL': 'C'}
if environment_vars:
self.environment_vars.update(environment_vars)
self.working_directory = working_directory
self.exit_code: int | None = None
self._trace_log = b''
self._trace_log_pos = 0
self.poll_object = epoll()
self.child_fd: int | 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:
"""
Contains will also move the current buffert position forward.
This is to avoid re-checking the same data when looking for output.
"""
assert isinstance(key, bytes)
index = self._trace_log.find(key, self._trace_log_pos)
if index >= 0:
self._trace_log_pos += index + len(key)
return True
return False
def __iter__(self, *args: str, **kwargs: dict[str, Any]) -> Iterator[bytes]: # pylint: disable=redefined-outer-name
last_line = self._trace_log.rfind(b'\n')
lines = filter(None, self._trace_log[self._trace_log_pos : last_line].splitlines())
for line in lines:
if self.remove_vt100_escape_codes_from_lines:
line = clear_vt100_escape_codes(line)
yield line + b'\n'
self._trace_log_pos = last_line
@override
def __repr__(self) -> str:
self.make_sure_we_are_executing()
return str(self._trace_log)
@override
def __str__(self) -> str:
try:
return self._trace_log.decode('utf-8')
except UnicodeDecodeError:
return str(self._trace_log)
def __enter__(self) -> Self:
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
# b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync.
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if self.child_fd:
try:
os.close(self.child_fd)
except Exception:
pass
if self.peek_output:
# To make sure any peaked output didn't leave us hanging
# on the same line we were on.
sys.stdout.write('\n')
sys.stdout.flush()
if exc_type is not None:
print(gray(str(exc_value)))
if self.exit_code != 0:
raise SysCallError(
f'{self.cmd} exited with abnormal exit code [{self.exit_code}]: {str(self)[-500:]}',
self.exit_code,
worker_log=self._trace_log,
)
def is_alive(self) -> bool:
self.poll()
if self.started and not self.ended:
return True
return False
def write(self, data: bytes, line_ending: bool = True) -> int:
assert isinstance(data, bytes) # TODO: Maybe we can support str as well and encode it
self.make_sure_we_are_executing()
if self.child_fd:
return os.write(self.child_fd, data + (b'\n' if line_ending else b''))
return 0
def make_sure_we_are_executing(self) -> bool:
if not self.started:
return self.execute()
return True
def tell(self) -> int:
self.make_sure_we_are_executing()
return self._trace_log_pos
def seek(self, pos: int) -> None:
self.make_sure_we_are_executing()
# Safety check to ensure 0 < pos < len(tracelog)
self._trace_log_pos = min(max(0, pos), len(self._trace_log))
def peak(self, output: str | bytes) -> bool:
if self.peek_output:
if isinstance(output, bytes):
try:
output = output.decode('UTF-8')
except UnicodeDecodeError:
return False
sys.stdout.write(output)
sys.stdout.flush()
return True
def poll(self) -> None:
self.make_sure_we_are_executing()
if self.child_fd:
got_output = False
for _fileno, _event in self.poll_object.poll(0.1):
try:
output = os.read(self.child_fd, 8192)
got_output = True
self.peak(output)
self._trace_log += output
except OSError:
self.ended = True
break
if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = True
try:
wait_status = os.waitpid(self.pid, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
except ChildProcessError:
try:
wait_status = os.waitpid(self.child_fd, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
except ChildProcessError:
self.exit_code = 1
def execute(self) -> bool:
import pty
if (old_dir := os.getcwd()) != self.working_directory:
os.chdir(str(self.working_directory))
# Note: If for any reason, we get a Python exception between here
# and until os.close(), the traceback will get locked inside
# stdout of the child_fd object. `os.read(self.child_fd, 8192)` is the
# only way to get the traceback without losing it.
self.pid, self.child_fd = pty.fork()
# https://stackoverflow.com/questions/4022600/python-pty-fork-how-does-it-work
if not self.pid:
try:
os.execve(self.cmd[0], list(self.cmd), {**os.environ, **self.environment_vars})
except FileNotFoundError:
print(red(f'{self.cmd[0]} does not exist.'))
self.exit_code = 1
return False
else:
# Only parent process moves back to the original working directory
os.chdir(old_dir)
self.started = True
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True
def decode(self, encoding: str = 'UTF-8') -> str:
return self._trace_log.decode(encoding)
def ensure_sudo():
global sudo_password # pylint: disable=global-statement
if sudo_password is None:
if (sudo_password := getpass.getpass(f'[sudo] password for {username}: ')) == '':
raise ValueError('Certain commands need sudo to work and no sudo password was given.')
def setup_networking():
if args.tap:
if pathlib.Path(f'/sys/class/net/{args.tap}').exists() is False:
print(gray(f'Creating {args.tap} for user {username} and group {groupname}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip tuntap add dev {args.tap} mode tap user {username} group {groupname}'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
if args.bridge:
if pathlib.Path(f'/sys/class/net/{args.bridge}').exists() is False:
print(gray(f'Creating {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link add name {args.bridge} type bridge'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
if args.bridge_mac:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge} address {args.bridge_mac}'), False
print(gray(f'Setting bridge {args.bridge} MAC address to {args.bridge_mac}'))
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
if args.bridge_master and get_master(args.bridge) != args.bridge_master:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge_master} master {args.bridge}'), False
print(gray(f'Setting interface {args.bridge_master} master to {args.bridge}'))
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
print(gray(f'Setting interface {args.tap} master to {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.tap} master {args.bridge}'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
print(gray(f'Bringing up bridge {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge} up'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
print(gray(f'Bringing interface {args.tap} up'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.tap} up'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
def setup_disks():
if args.harddrive:
for harddrive_arg in args.harddrive:
path, size = harddrive_arg.split(':')
path = pathlib.Path(path.strip()).expanduser().resolve().absolute()
harddrives[path] = size.strip()
if path.exists() is False:
handle = SysCommandWorker(f'qemu-img create -f qcow2 {hdd} {size}')
while handle.is_alive():
time.sleep(0.01)
if handle.exit_code != 0:
raise ValueError(f'Could not create harddrive {hdd}: {handle}')
setup_networking()
setup_disks()
if args.uki or args.bios is False:
disk_paths_hash = hashlib.sha1((''.join(sorted([str(x) for x in harddrives.keys()]))).encode()).hexdigest()
shutil.copy2('/usr/share/ovmf/x64/OVMF_CODE.secboot.4m.fd', f'./OVMF_CODE.secboot.4m.fd.{disk_paths_hash}')
shutil.copy2('/usr/share/ovmf/x64/OVMF_VARS.4m.fd', f'./OVMF_VARS.4m.fd.{disk_paths_hash}')
boot_index = 0
qemu = 'qemu-system-x86_64'
qemu += ' -cpu host'
qemu += ' -enable-kvm'
qemu += ' -machine q35,accel=kvm'
qemu += ' -object rng-random,filename=/dev/urandom,id=rng0'
qemu += ' -device virtio-rng-pci,rng=rng0'
qemu += ' -global driver=cfi.pflash01,property=secure,value=on'
qemu += f' -smp {args.cpu},sockets=1,dies=1,cores={args.cpu},threads=1'
# qemu += f' -vga vga'
qemu += f' -device VGA,edid=on,xres={args.resolution.split("x")[0]},yres={args.resolution.split("x")[1]}'
qemu += ' -device intel-iommu,device-iotlb=on,caching-mode=on'
qemu += f' -m {args.memory}'
if args.bios is False:
qemu += f' -drive if=pflash,format=raw,readonly=on,file=./OVMF_CODE.secboot.4m.fd.{disk_paths_hash}'
qemu += f' -drive if=pflash,format=raw,file=./OVMF_VARS.4m.fd.{disk_paths_hash}'
if args.uki:
qemu += f' -kernel {args.uki}'
boot_index += 1
scsi_index = 0
for scsi_index, hdd in enumerate(harddrives.keys()):
# qemu += f' -device virtio-scsi-pci,bus=pcie.0,id=scsi{index}'
# qemu += f' -device scsi-hd,drive=hdd{index},bus=scsi{index}.0,id=scsi{index}.0,bootindex={hdd_boot_priority+index}'
# qemu += f' -drive file={hdd},if=none,format=qcow2,discard=unmap,aio=native,cache=none,id=hdd{index}'
qemu += f' -device virtio-scsi-pci,bus=pcie.0,id=scsi{scsi_index},addr=0x{scsi_index + 8}'
qemu += f' -device scsi-hd,drive=libvirt-{scsi_index}-format,bus=scsi{scsi_index}.0,id=scsi{scsi_index}-0-0-0,channel=0,scsi-id=0,lun=0,device_id=drive-scsi0-0-0-0,bootindex={boot_index},write-cache=on' # noqa: E501
qemu += f' -blockdev \'{{"driver":"file","filename":"{hdd}","aio":"threads","node-name":"libvirt-{scsi_index}-storage","cache":{{"direct":false,"no-flush":false}},"auto-read-only":true,"discard":"unmap"}}\'' # noqa: E501
qemu += f' -blockdev \'{{"node-name":"libvirt-{scsi_index}-format","read-only":false,"discard":"unmap","cache":{{"direct":true,"no-flush":false}},"driver":"qcow2","file":"libvirt-{scsi_index}-storage","backing":null}}\'' # noqa: E501
boot_index += 1
if args.iso:
qemu += f' -device virtio-scsi-pci,bus=pcie.0,id=scsi{scsi_index + 1}'
qemu += f' -device scsi-cd,drive=cdrom0,bus=scsi{scsi_index + 1}.0,bootindex={boot_index}'
qemu += f' -drive file={args.iso},media=cdrom,if=none,format=raw,cache=none,id=cdrom0'
boot_index += 1
# if args.vfio:
# qemu += f' -drive file={args.vfio},index=2,media=cdrom'
if args.tap:
qemu += f' -device virtio-net-pci,mac={args.tap_mac},id=network0,netdev=network0.0,status=on,bus=pcie.0'
qemu += f' -netdev tap,ifname={args.tap},id=network0.0,script=no,downscript=no'
print(gray(qemu))
qemu_session = subprocess.run(shlex.split(qemu), check=True, capture_output=True)
if qemu_session.stdout:
print(qemu_session.stdout.decode())
if qemu_session.returncode != 0:
print(red(qemu_session.stderr.decode()))