systemctl --user enable only writes the .wants symlinks; the services
don't actually run until next login. Switching to --now enables AND
starts them, so the first session has bt-agent (bluetooth pairing) and
omarchy-sleep-lock active immediately rather than waiting a reboot.
omarchy-recover-internal-monitor is Type=oneshot with a
ConditionPathExists guard, so --now is harmless for it (it'll skip if
the toggle isn't set).
Reorganizes Omarchy 4 around three layers for populating $HOME:
Seed: omarchy-settings ships defaults to /etc/skel; useradd -m
copies them on user creation
Finalize: omarchy-finalize-user (renamed from omarchy-setup-user)
handles only the runtime tweaks /etc/skel can't do — skill
symlinks, xdg-user-dirs, default browser/mailto, vconsole→hypr
keyboard sync, and install/user/all.sh
Resync: omarchy-reinstall-configs is the explicit, destructive
resync of /etc/skel into an existing user's $HOME
Package-owned files move out of config/ into default/, where the
omarchy-settings PKGBUILD installs them to real system paths:
config/environment.d/fcitx.conf -> /usr/lib/environment.d/
config/fontconfig/fonts.conf -> /usr/share/fontconfig/conf.avail/
config/mimeapps.list -> /usr/share/applications/
config/omarchy.ttf -> /usr/share/fonts/omarchy/
config/systemd/user/*.service -> /usr/lib/systemd/user/
config/uwsm/default -> /usr/share/omarchy/default/uwsm/
config/uwsm/env -> /usr/share/uwsm/env.d/10-omarchy
config/xdg-terminals.list -> /usr/share/xdg-terminal-exec/
omarchy-upgrade-to-4 grows a 'retire' action (renamed from 'move' to
clarify nothing is copied — the system path is owned by the new package
once the user's hash-matched ~/.config copy is removed). Mismatched
copies are kept as backups so user overrides survive the upgrade.
Other simplifications:
- Single env bootstrap at default/bash/env-bootstrap sourced by
/etc/profile.d/omarchy.sh, /etc/skel/.bashrc,
/usr/share/uwsm/env.d/10-omarchy, and default/bash/envs. PATH
prepend only in dev-link mode (production uses /usr/bin/omarchy-*).
- omarchy-refresh-config reads from /etc/skel/.config so refresh
means 'snap to skel'.
- omarchy-reinstall-configs collapses to 'cp -af /etc/skel/. ~/'
plus limine/plymouth/nvim refresh.
- omarchy-font-set uses awk against our own 30-omarchy.conf instead
of xmlstarlet; xmlstarlet dropped from omarchy-base.packages.
- Defer user systemd enables (bt-agent, sleep-lock,
recover-internal-monitor) to first-run via
install/user/first-run/enable-user-units.sh; delete
omarchy-user-systemctl-enable and the per-hardware install
scripts that called it.
- Wireplumber bluetooth-a2dp-autoconnect.conf moves to config/ so
/etc/skel ships it; install/user/hardware/bluetooth.sh deleted.
- Default terminal switched to foot.desktop.
- docs/file-layout.md documents the three-layer model and the
build-time repo→path map.
Remove legacy online installer entrypoints, collapse migrations for 4.0, and move setup responsibilities into target-side system, hardware, and user commands.
Evaluate NVIDIA session env vars from default/hypr/nvidia.lua during Hyprland startup instead of appending them to each user's envs.lua during setup. This removes the per-user NVIDIA setup script execution while preserving the GSP/non-GSP detection.
The harvest branch already enforces non-empty cmdline + presence of
root=. The pre-populated branch (added in 0d2688a5) was missing the
same validation, so a malformed /etc/default/limine could slip
through and the later cryptdevice= check would silently no-op
because $CMDLINE was empty.
The ISO orchestrator now writes /etc/default/limine directly from the
install config (with the real cmdline already substituted) before the
chroot phase runs. The previous unconditional find+harvest+copy+sed
sequence would (a) find our orchestrator-staged /boot/limine.conf which
has no `cmdline:` line and (b) exit 1 on "failed to extract kernel
cmdline".
Skip the harvest entirely when /etc/default/limine already exists
without the @@CMDLINE@@ placeholder. Parse CMDLINE from the existing
file so the downstream cryptdevice= validation still has it to compare.
The harvest path remains as a fallback for online reinstalls and any
flow where /etc/default/limine wasn't pre-populated by the orchestrator.
The orchestrator only runs on the live ISO, so it lives in omarchy-iso
where the rest of the ISO-specific code (configurator, .automated_script
.sh, archinstall pacman config) lives. omarchy-installer stays the
monorepo of Omarchy source content (configs, themes, default files,
install scripts) without a Python install driver mixed in.
bin/omarchy-install simplifies to a one-liner: it's the online entry
point now, no --config dispatcher. The orchestrator never invokes it;
it runs finalize.sh directly via arch-chroot.
Chunks 2-6 of the C3 refactor. With the adapter in place, the phase
list collapses from 12 stubs to 5 real phases:
prepare_live parse user_*.json into archinstall handlers
arch_install partition + base + bootloader + limine config +
early omarchy + useradd + runtime omarchy (all
inside a single Installer context manager)
run_chroot_finalizer arch-chroot -u $user → finalize.sh
validate_boot halt if /boot/limine.conf, /etc/kernel/cmdline,
or UKI are missing/malformed
finish reboot prompt
Key reordering vs upstream guided.py: write_limine_config runs BETWEEN
add_bootloader and the first add_additional_packages call. By the time
omarchy-limine pulls limine-mkinitcpio-hook (in the runtime package
install), /etc/default/limine and /etc/kernel/cmdline are in place, so
the UKI is built correctly on the first try — no stale-UKI purge,
no follow-up limine-update.
add_additional_packages is split into two calls so /etc/skel is
populated (via omarchy-settings in EARLY_PACKAGES) BEFORE useradd:
EARLY_PACKAGES = base-devel git omarchy-keyring omarchy-settings omarchy-installer
→ installer.create_users(users)
→ runtime: omarchy + omarchy-base.packages
archinstall_adapter.py is the ONLY module that imports from archinstall.
If archinstall's API churns, the blast radius is contained here. Tested
against archinstall 4.3 / Python 3.14.
Limine template lookup tries the new install/assets/limine/ path first
and falls back to the legacy default/limine/ location during the
template-ownership migration (lands in a follow-up chunk).
Chunk 1 of the C3 refactor: replace the two-pass install model (archinstall
CLI + custom shell dance + chroot bash installer) with a single Python
orchestrator that owns phase ordering, using archinstall as a library
subsystem.
Splits the bash entry point into two:
- install.sh: online entry point. Ensures omarchy runtime + base.packages
are installed/up to date, then exec's finalize.sh.
- finalize.sh: the in-target portion (preflight + packaging + config +
login + post-install). Called by install.sh (online) AND by the
orchestrator after arch-chroot -u $USER (offline).
Adds the orchestrator skeleton under install/orchestrator/:
- main.py: entry point, builds + runs the phase list
- context.py: InstallContext (parsed configurator JSON + invocation paths)
- phases.py: phase state machine (logging + state.json + error wrapping)
- phases_impl.py: stubbed phase implementations (filled in by chunks 2-6)
- archinstall_adapter.py: thin compat wall around archinstall lib imports
(only this module imports from archinstall.*)
- ui.py: gum subprocess wrappers so the orchestrator keeps the same
styled-terminal UX as the bash installer
Updates bin/omarchy-install to dispatch:
- --config <json> in args → python -m orchestrator.main (ISO install)
- anything else → bash install.sh (online rerun on installed system)
Concrete phase logic lands in subsequent chunks. All phases currently
raise NotImplementedError; the orchestrator imports cleanly and --help
works as a smoke check.
install/config/hardware/framework/qmk-hid.sh installs the qmk-hid
package on Framework 16 hardware, but it wasn't listed in any of the
ISO builder's package sources, so it wasn't being pulled into the
offline mirror. Adding it under the Framework 16 section in
omarchy-other.packages so the ISO build picks it up.
The lua-based monitors config (~/.config/hypr/monitors.lua) ships from
skel with sensible defaults (omarchy_gdk_scale = 2, omarchy_monitor_scale
= "auto"). Hyprland handles the dynamic monitor scale at runtime via
"auto"; 'omarchy hyprland monitor scaling <N>' already updates both
omarchy_monitor_scale and omarchy_gdk_scale in monitors.lua when the
user wants to adjust.
The first-run 'detect what hyprland picked and write it back' dance
was leftover from the .conf-based config; it's redundant on the lua
flow and was the script that aborted omarchy-first-run under set -e
when it sed'd a missing monitors.conf.
Two bugs caused fresh users to land without branding files / AI skill
symlinks / nautilus extensions / xdg dirs / etc.:
1. install/first-run/gdk-scale.sh sed-edited ~/.config/hypr/monitors.conf,
but the lua-based config flow uses ~/.config/hypr/monitors.lua. sed -i
on the missing .conf returned non-zero, set -e fired, omarchy-first-run
aborted mid-block AFTER deleting ~/.local/state/omarchy/first-run.mode.
omarchy-setup-user (called at the bottom of the script, outside the
if-block) never ran, so setup-user.done never got written and the
per-user defaults (~/.config/omarchy/branding/*, ~/.agents/skills/omarchy,
xdg dirs, etc.) never got created.
Replace the .conf sed with a call to omarchy-hyprland-monitor-scaling
which already knows how to persist into monitors.lua.
2. Move 'omarchy-setup-user || true' to the TOP of omarchy-first-run, before
the privileged first-run block. setup-user is idempotent and gated on
its own marker, so running it first is safe and means a failure in any
of the privileged steps can't prevent user setup from completing.
The git-era flow installed Omarchy to ~/.local/share/omarchy. In the
package-based flow it lives at /usr/share/omarchy (or, in dev mode, at
the path /etc/omarchy.conf's OMARCHY_PATH points at). Files that hard-
coded the old path broke on package installs:
- default/bash/rc: sourced ~/.local/share/omarchy/default/bash/*, so a
fresh user's terminal logged ENOENT for every line. Now sources from
$OMARCHY_PATH with /usr/share/omarchy as the safe default if the var
is unset.
- config/systemd/user/omarchy-recover-internal-monitor.service:
ExecStart pointed at %h/.local/share/omarchy/bin/. The script ships
to /usr/bin via the omarchy-installer package; point straight there.
- install/config/xcompose.sh: wrote an XCompose include line pointing
at the user-home path; use /usr/share/omarchy/ directly.
- default/hypr/paths.lua and config/hypr/hyprland.lua: fallback when
OMARCHY_PATH is unset is /usr/share/omarchy, not the home path.
Dev-only scripts (omarchy-dev-add-migration, omarchy-reinstall-git) and
the SKILL.md docs still reference the old path; those are separate
cleanups since they don't affect a fresh install's runtime.
Per apollo audit: the /boot/limine.conf assertion grep was firing
BEFORE snapper config setup, btrfs quota disable, service enable, and
the mkinitcpio pacman hook re-enable. A validation failure (the failure
path the validation exists for) would leave the system half-configured.
Move the validation block to after 'mkinitcpio hooks re-enabled' so all
the post-config steps run first. Guard it with LIMINE_CONFIGURED=true so
non-limine setups still pass straight through to efibootmgr cleanup.
mkinitcpio -P alone doesn't trigger limine's UKI pipeline. The
limine-mkinitcpio-hook package installs /etc/pacman.d/hooks/90-mkinitcpio
-install.hook (overriding mkinitcpio's normal hook) which executes
/usr/share/libalpm/scripts/limine-mkinitcpio-install only on pacman
transactions, OR via the limine-mkinitcpio / limine-update CLI wrappers.
The previous fix (mkinitcpio -P) rebuilt initramfs-linux.img but left
the stale empty-cmdline UKI at /boot/EFI/Linux/<machine-id>_linux.efi
untouched. limine-update then booted that file.
Changes:
- Validate the cmdline extracted from /boot/limine.conf: fail loudly if
empty or missing root=
- Delete every alternate /boot/limine.conf so limine-update can't pick
the wrong one
- Delete stale UKIs for every installed kernel pkgbase before rebuilding
- Replace 'mkinitcpio -P' with 'limine-update' (which does
limine-install + limine-mkinitcpio, reading KERNEL_CMDLINE from
/etc/default/limine via limine-entry-tool and embedding it into a
fresh UKI)
- Assert /boot/limine.conf has the Omarchy entry and (for encrypted
installs) the cryptdevice= cmdline before continuing
In the package-based install, omarchy-limine is a hard dep of omarchy,
so archinstall pacstraps limine-mkinitcpio-hook (via omarchy-limine's
depends) early. That triggers a UKI build BEFORE limine-snapper.sh
runs, which means the UKI is built with no kernel cmdline (the
'Kernel command line is not available' warning in the archinstall log).
The system then can't unlock the LUKS root on boot because the encrypt
hook has no cryptdevice= to act on, and drops to an emergency shell.
The previous flow worked because 'pacman -S limine-mkinitcpio-hook'
inside limine-snapper.sh installed the hook for the first time, firing
its post-transaction UKI rebuild AFTER /etc/default/limine had been
written. With it pre-installed, '--needed' makes that line a no-op.
Fix: after writing /etc/default/limine, write the same cmdline to
/etc/kernel/cmdline (the source mkinitcpio --uki reads) and explicitly
run mkinitcpio -P to rebuild the UKIs with the correct cmdline.