setup is where a user goes to configure something: direct boot, security
keys, hibernation. These three are not that. omarchy-apply-system is the
ISO's entry point in the target chroot, omarchy-apply-hardware is what it
calls for device quirks, and omarchy-apply-lock is called by
install/config/lockscreen-pam.sh.
apply is the verb they already used to describe themselves, and it carries
the contract: declared state under install/ converged onto the machine,
idempotent, safe to repeat.
The group gets no GROUP_DESCRIPTIONS entry on purpose. That table drives the
top-level group list on its own, so an entry would put apply back in front of
users even with every command in it hidden, the way provision already stays
out. A test covers it.
The ISO installs the runtime from the mirror it ships with, so it moves to
the new names in lockstep and no compatibility route is needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add OEM first-boot setup and factory reset
An OEM-mode ISO install (or omarchy-reset-computer) leaves the machine in OEM
state: fully installed, no user, /var/lib/omarchy/oem/pending armed. On the
next boot omarchy-oem-setup.service runs the configurator's user form on tty1,
creates the user with the groups system setup recorded, finalizes it offline
from the stashed Node tarball, re-keys LUKS from the throwaway install
passphrase to the user's password, and hands off to SDDM.
omarchy-reset-computer returns a machine to that state: it swaps the running
root for a fresh clone of the @factory snapshot the ISO takes at install time,
scrubs machine identity and prior users, and stages omarchy-factory-wipe to
drop the old root and recreate @home/@log on the next boot. Machines installed
before @factory existed get a degraded reset (current system kept, users and
state wiped) with that caveat surfaced in the confirmation.
omarchy-setup-system/-hardware gain --oem to run without an install user; the
group-granting install scripts now record their groups in
/var/lib/omarchy/oem/groups and only call usermod when the user exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Harden OEM setup: correct cryptsetup key-file usage, retry on failure
cryptsetup reads --test-passphrase/--key-file inputs byte-for-byte, so feed
passphrases through process substitution consistently instead of positional
args or stdin (which has different newline semantics). Run each first-boot
setup attempt as its own process so a failure offers a retry instead of
stranding the machine at a user-less login screen — bash ignores errexit
inside `while !` conditions, a child process does not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Always grant wheel sudo in OEM first-boot setup
Detecting an existing %wheel grant by grepping sudoers is error-prone:
omarchy ships narrow '%wheel ALL=(ALL) NOPASSWD: <command>' rules (e.g.
asdcontrol) that match the naive pattern, which left the OEM-created user
matching sudoers entries but unable to run anything. Write the drop-in
unconditionally — a duplicate of an existing full grant is harmless.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix LUKS re-key device resolution and OEM state readability
archinstall's encrypted installs put cryptdevice=PARTUUID=... on the kernel
cmdline, not UUID=, so the first-boot re-key never found its device and
silently skipped — leaving the throwaway auto-unlock keyfile in place, i.e.
the disk effectively unencrypted. Parse every cryptdevice= source spec form
and make any re-key failure abort the attempt loudly: a retry prompt beats a
machine that quietly boots without a passphrase forever.
The OEM state directory also has to be world-readable (its one secret,
luks-key, stays 0600): user finalization reads the stashed Node tarball as
the new user, and the 0700 directory forced it onto the network fallback.
Step markers now land in /var/log/omarchy-oem-setup.log for debuggability.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Purge stale machine-id boot entries when resetting or re-keying
limine-entry-tool keys its limine.conf OS entries by machine-id. A factory
reset gives the machine a fresh identity, so the previous system's entry
survived every rebuild, sorted first, and made Limine stop at a Blake2b
hash-mismatch warning once the UKI was rebuilt. Start limine.conf over from
the shipped template (and drop foreign machine-id history directories on the
ESP) before any post-reset rebuild: in the staged chroot rebuild, in the
first-boot LUKS re-key, and — for unencrypted resets, where nothing else
rebuilds — in a dedicated first-boot refresh when foreign entries are found.
The staged rebuild also verifies every UKI hash referenced by limine.conf
against the file on the ESP before the subvolume swap, and the running
system's limine-snapper-sync is runtime-masked during staging so it cannot
rewrite the config behind the rebuild.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Harden reset and first-boot setup failure paths
Review findings from codex and Copilot:
- Generate throwaway passphrases without a trailing head stage: under
pipefail, SIGPIPE from the infinite tr failed the substitution and errexit
aborted every encrypted reset before it could stage anything.
- Stage the fallible parts of a degraded reset (LUKS re-key, boot rebuild)
before arming the wipe, so a staging failure leaves the machine untouched
instead of scheduling a wipe for a reset that never finished.
- Gate first-boot setup on the factory wipe having succeeded
(ConditionPathExists=!wipe-pending plus an in-script guard): creating the
new user on a half-wiped system would hand their data to the wipe retry.
- Abort the wipe (keeping its retry marker) when deleting the old root or
recreating @home/@log fails, and abort resets that cannot remove a prior
account — a surviving account keeps its password and wheel membership.
- Resume a partially-created account on setup retry instead of rejecting the
username the failed attempt just created.
- Only purge machine-id directories the old limine.conf actually referenced;
a shared ESP may hold other installations' boot artifacts.
- Recreate the hibernation swapfile (nested subvolume, so never captured by
the factory snapshot) inside the factory root before its UKI rebuild, so a
reset machine keeps disk-backed swap and a valid resume offset.
- Source base-test.sh in the OEM groups test per test conventions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Recreate the hibernation swapfile even when resume drop-ins survive
omarchy-hibernation-setup short-circuits as 'already set up' when the resume
mkinitcpio drop-in exists — which it always does in a factory root, while the
swapfile itself never survives the snapshot (nested subvolume). Drop the
marker when the swapfile is gone so setup reconfigures from scratch, and
verify the swapfile actually exists before proceeding with the reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Second review pass: encrypted-config coverage, factory-baseline sanitization, recoverable rekey
Codex xhigh round 2:
- Detect the LUKS backing device by walking the root's device tree, not only
the cmdline cryptdevice=; reset/first-boot now re-key roots reached via
rd.luks/crypttab too, instead of silently leaving the seller's slots valid.
- Sanitize the retained @factory baseline (accounts, /etc/shadow, machine
identity) during a full reset: the new wheel user could otherwise mount it
to recover the seller's data, and a second reset would restore the account.
- Re-key the disk recoverably: rebuild the no-auto-unlock UKI before killing
the throwaway slot or destroying the staged key, and restore the keyfile if
that rebuild fails, so a retry with a different password can never leave the
disk locked to the first attempt's password.
- Roll back a degraded reset's live-root auto-unlock material if its boot
rebuild fails, instead of leaving it for a later rebuild to embed.
- Treat a missing current-machine limine entry as stale so a retry after a
failed rebuild repairs the config instead of clearing OEM state over it.
- Erase fingerprint enrollments (/var/lib/fprint) in degraded wipes.
- Remove the resume-offset drop-in too when recreating the factory swapfile,
so the rebuilt UKI gets a correct offset.
- Pin first-boot retries to the account the first attempt created.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Expose factory reset in the Setup menu
Add a 'Reset Computer' entry under Setup (Omarchy's Settings menu, where OS
factory resets conventionally live), guarded to btrfs roots and launched in a
floating terminal. omarchy-reset-computer now self-elevates via sudo so the
menu entry needs no sudo prefix, forwarding the caller's gum theme env as
env arguments so styling survives an env_reset sudoers. The typed 'reset'
confirmation and the sudo password prompt remain as the guards against
accidental triggering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Defer keyboard selection to first boot for OEM installs
The OEM first-boot setup now runs a keyboard step before the user form,
mirroring the ISO configurator: it loads the chosen layout on the live VT so
the password (and the LUKS re-key that follows) are typed under it, and
persists it with systemd-firstboot so the installed system gets both the
console KEYMAP and the XKB layout Hyprland reads — exactly what a normal
install writes. Layouts localectl doesn't know keep the default, same as the
installer.
This lets the OEM operator set nothing user-specific: the machine's owner
picks their keyboard alongside their account at first boot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rename factory-reset commands to omarchy-system-factory-reset[-finish]
omarchy-reset-computer -> omarchy-system-factory-reset
omarchy-factory-wipe -> omarchy-system-factory-reset-finish
(and its systemd unit, log path, and temp mount to match)
Pure rename: every reference — the Setup menu action, the first-boot finish
service the reset stages and enables, the oem-setup ordering/gating, comments,
and the menu test — moves together, with no behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rename OEM vocabulary to provisioning (runtime)
Commands unify under the provisioning family:
omarchy-oem-setup → omarchy-provision-owner
omarchy-finalize-user → omarchy-provision-user
omarchy-first-run → omarchy-provision-first-run
And the deferred-provisioning state/vocabulary replaces 'OEM':
/var/lib/omarchy/oem/ → /var/lib/omarchy/provisioning/
/etc/omarchy/oem.key → /etc/omarchy/provisioning.key
install/oem/ → install/provisioning/
OMARCHY_SETUP_CONTEXT=oem-firstboot → provision-owner
omarchy-setup-system/-hardware --oem → --defer-provisioning
All callers (provision-first-run→provision-user, autostart, factory-reset
staging the provisioning units, the group-recording scripts) and comments
move together.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop remaining OEM mentions from the provisioning groups test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Finish the omarchy-first-run rename in the docs
Two doc references to omarchy-first-run were missed when the script was renamed
to omarchy-provision-first-run; update them to match.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Clean up the terminal and reconnect when SSH connections drop
A remote tmux, herdr, or editor arms terminal modes over the SSH pipe
(mouse tracking, focus reporting, the alternate screen) that only it can
disarm. When the connection dies instead of exiting cleanly, those modes
stay armed on the local terminal, and every mouse move floods the prompt
with escape-sequence junk.
Wrap ssh in a shell function that disarms those modes after every exit,
and automatically reconnects when an established interactive session
drops. Remote commands, configured RemoteCommands, and redirected stdin
never reconnect, so their side effects cannot replay, and the retry loop
runs in a subshell so Ctrl-C cancels both the in-flight attempt and the
loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Detect dead SSH connections within a minute
Without keepalives, ssh does not notice a dead peer until TCP gives up,
which can take hours of sitting on a hung terminal with remote-armed
terminal modes stuck on. Ship a client keepalive default so drops are
detected in about 45 seconds, letting the shell's ssh wrapper clean up
and reconnect. ~/.ssh/config is read first and wins, so per-host
overrides still apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fail closed when ssh -G cannot resolve the effective config
An unresolvable configuration could hide a RemoteCommand, so treat it
as non-interactive rather than reconnectable. Also strengthen the
tests from Copilot review: assert the complete disarm sequence, and
verify on a real interactive pty that Ctrl-C during a retry attempt
kills the reconnect loop itself, not just the in-flight attempt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Tolerate the explicit RemoteCommand none when probing ssh -G
The literal "none" is how ssh_config cancels a configured
RemoteCommand, and some OpenSSH versions emit it even when unset, which
would have silently disabled reconnecting entirely. Treat it as no
remote command while still failing closed on real ones and unresolvable
configs, and make the fake ssh -G emit the "none" form so the behavior
tests cover it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Give SSH commands the user-level tool paths
ssh host cmd runs neither a login nor an interactive shell, so on Arch it
gets the bare sshd PATH and can't find mise-managed tools like the agent
CLIs herdr scans for. Set PATH in the PAM environment (per-user via
@{HOME}), append the user-level dirs in env-bootstrap so login shells and
the uwsm session get them too, and source env-bootstrap before bashrc's
interactive guard for bash variants that read it non-interactively.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Don't let an empty PATH turn into a cwd entry
Appending with a bare "$PATH:" prefix leaves a leading colon when PATH
is unset, which shells treat as the current directory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Nothing stood between "memory is tight" and "processes die at random":
the kernel OOM killer only fires after an allocation has already failed,
so a machine under pressure thrashes in reclaim until something breaks
somewhere fatal — seen in practice as Hyprland taking SIGBUS mid-memcpy
from a client shm pool that memory pressure was tearing down.
Enable systemd-oomd, which keys on PSI stall time rather than free
pages, and mark app.slice — and only app.slice — as its kill candidate.
Hyprland runs in session.slice, so the compositor is structurally
ineligible as a victim: oomd takes the app that caused the pressure and
the session survives. Thresholds (50% stall over 20s, Fedora's desktop
defaults) live in an oomd.conf.d drop-in; candidacy ships as a vendor
drop-in under /usr/lib/systemd/user so existing users get it on package
upgrade with no per-user seeding.
New installs enable the daemon from enable-services.sh; a migration
covers existing ones, restarting an already-running oomd so it doesn't
keep stale thresholds until reboot.
Deliberately not earlyoom: it triggers only when free RAM and free swap
are both under threshold, so Omarchy's large, mostly-idle swapfiles
keep it asleep through exactly this failure.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
updatedb refuses to run at all on a config that defines a variable twice
("variable `PRUNEPATHS' was already defined"), so any rewrite that misses
an existing line and appends a second one takes the locate index down
rather than fixing it. Two shapes updatedb accepts got missed: a trailing
comment after the value, and a setting indented by whitespace.
Read the existing paths out of the quoted value and write the whole
setting back canonically instead of splicing into a line of unknown
shape. Quotes are not optional to updatedb ("value in quotes expected
after `='"), so a bare value is already a broken config: rewriting it
quoted repairs the file as a side effect.
The tests now hand every rewritten file to the real parser through
`updatedb --config-file`, which is what caught this.
Read the Snapper config as root when the running user cannot read it.
snapper create-config leaves the config root-only, and a config the user
cannot read was passing for one that wants its timeline snapshots kept.
Report the snapshots the drain could not delete. omarchy-migrate writes
the completion marker whether or not the batches succeeded, so there is
no later run to pick up the remainder, whatever the comment claimed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The machines this targets are the ones with an updatedb already grinding
through every snapshot, and `systemctl start` on an active unit is a
no-op. updatedb reads /etc/updatedb.conf once at startup, so a run that
began before the rewrite keeps burning CPU on the old config until it
finishes. Restart the service instead: it's Type=oneshot and plocate
builds into a temp db, so nothing is lost by replacing the run.
Quotes are optional in updatedb.conf, so parse the existing paths out of
whatever quoting the file uses and write the setting back in one
canonical form. `PRUNEPATHS=/tmp` previously fell through to the append
branch and got a second PRUNEPATHS line, which drops /tmp from the
pruned set. Comparing whole paths rather than substrings also keeps a
config that already prunes something like /var/lib/machines/.snapshots
from being mistaken for one that prunes /.snapshots.
Prefer $OMARCHY_PATH over the packaged copy when locating the config
script, per docs/migrations.md, so the migration test exercises the
checked-out script rather than whatever release is installed. Skip when
neither exists: omarchy-migrate runs under set -e, so a missing script
would take down every migration queued behind it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stock Arch updatedb.conf interacts badly with Omarchy's Btrfs
layout in both directions:
- Snapper snapshots under /.snapshots are nested subvolumes reached by
plain directory traversal, so updatedb indexes the entire system once
per snapshot. On machines that accumulated snapshots this means
multi-hour updatedb runs at full CPU, gigabytes of RAM, and a
multi-gigabyte plocate.db (observed: 18 GB db, 7.5 h runs at 96% CPU
with 592 snapshots; 43 MB and ~1 min after the fix).
- PRUNE_BIND_MOUNTS="yes" treats Btrfs subvolume mounts like /home as
bind mounts, so locate finds nothing in home directories at all.
Configure updatedb.conf at install time and migrate existing installs,
then rebuild the index in the background. Both settings are matched
tolerantly and appended when absent, so a hand-edited updatedb.conf is
fixed rather than silently skipped.
graphical.target was gated on network-online.target: cups-browsed orders
itself After=network-online.target and sits in multi-user.target, so the
session waited for NetworkManager-wait-online (DHCP/Wi-Fi association)
before coming up. On the bench VM that was ~140ms, but it's the classic
multi-second boot stall on real Wi-Fi.
Mask NetworkManager-wait-online.service so network-online.target is
reached immediately and nothing in the boot waits for connectivity;
NetworkManager still comes up and cups-browsed browses once the link is
live. Mirrors the existing systemd-networkd-wait-online mask. Enabled at
install (enable-services.sh) and masked on existing installs via
migration.
Verified on a fresh boot: the graphical.target critical chain no longer
passes through network-online.target / NetworkManager-wait-online.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 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.
User-level files (~/.config, ~/.local/share, ~/.agents, etc.) shouldn't
be written by install.sh — it runs as root or with system-level sudo,
and in offline (ISO chroot) mode the user doesn't exist yet. Moved into
a per-user idempotent command, called from omarchy-first-run.
Moved into bin/omarchy-setup-user (deleted from install/config/):
- omarchy-ai-skill.sh AI skill symlinks
- omarchy-toggles.sh hypr toggle flags
- nautilus-python.sh nautilus extensions
- branding.sh ~/.config/omarchy/branding/{about,screensaver}.txt
- user-dirs.sh xdg-user-dirs-update + gtk bookmarks
- detect-keyboard-layout.sh layout/variant sed into ~/.config/hypr/input.lua
- toggles.sh ~/.local/state/omarchy/toggles mkdir
- mimetypes.sh omarchy-refresh-applications + xdg-mime/xdg-settings
Kept in install/config/ (rely on OMARCHY_USER_NAME / OMARCHY_USER_EMAIL
that only exist during install):
- git.sh git config --global user.{name,email}
- xcompose.sh ~/.XCompose with name/email substituted
omarchy-setup-user:
- Refuses to run as root
- Idempotent via ~/.local/state/omarchy/setup-user.done marker
- --force escape hatch for re-running after dev-link or upstream changes
- Defaults OMARCHY_PATH to /usr/share/omarchy so it works outside install
bin/omarchy-first-run now invokes omarchy-setup-user unconditionally
(it self-gates), so first user login picks up the user setup.
install/config/all.sh: dropped the eight deleted entries.
All three packages (omarchy, omarchy-settings, omarchy-installer) still
build clean.
Drop online-git: the new world has a single package-backed install path
that's either online (pacman pulls from omacom-pkgs.org) or offline (ISO
chroot pulls from the bundled mirror). The git-clone-and-run-installer
path is gone — boot.sh becomes a thin bootstrap that installs the
omarchy-installer package and invokes its installer.
- install/helpers/mode.sh: two modes, validation message updated. Default
fallback is 'online' (the boot.sh path); OMARCHY_CHROOT_INSTALL=1 still
shims to offline, OMARCHY_ONLINE_INSTALL=true still shims to online.
- install/preflight/all.sh: first-run-mode + disable-mkinitcpio gated on
offline (offline is the from-scratch chroot path; online runs on an
existing system).
- install/preflight/pacman.sh: DELETED. boot.sh now handles repo config
before install.sh runs.
- install/packaging/all.sh: drop base.sh entirely. Both modes have
base packages installed via omarchy-installer's depends before
install.sh runs. base.sh remains on disk in case the ISO mirror build
flow wants to invoke it directly.
- install/helpers/chroot.sh, errors.sh, post-install/finished.sh,
config/mise-work.sh: rename mode checks to use online/offline.
Three modes the installer now branches on:
iso-chroot Running inside the new system's chroot from the ISO build.
pacman is configured by archinstall; systemd isn't running,
so service enables skip --now.
online-package Running on an existing Arch where the omarchy-* packages
are already installed. Skip base.sh and the bootstrap
preflight steps; only the per-host runtime ops in
config/login/post-install need to run.
online-git boot.sh path: clone into $HOME/.local/share/omarchy and
bootstrap everything from scratch.
install.sh derives OMARCHY_PATH from its own location, so the same script
works whether it's at /usr/share/omarchy/install.sh (package mode) or
$HOME/.local/share/omarchy/install.sh (git mode). install_mode_is helper
replaces direct ${OMARCHY_CHROOT_INSTALL:-} / ${OMARCHY_ONLINE_INSTALL:-}
checks in chroot.sh, finished.sh, errors.sh, mise-work.sh, preflight/pacman.sh.
Legacy vars still work through export_legacy_mode_flags, which sets
OMARCHY_CHROOT_INSTALL=1 / OMARCHY_ONLINE_INSTALL=true based on the
canonical mode for any caller that hasn't been updated.
bin/omarchy-install-mode prints the current mode for scripts/users that
need to introspect.
Smoke-tested all four entry paths (explicit mode, both legacy shims,
auto-detection from OMARCHY_PATH); each resolves correctly and the
legacy flags are exported in matching modes.
A pass over the install scripts, dev-tools commands, and Hyprland Lua
files that I had stuffed with explain-everything preambles. Most of
those rationales (which files ship where, why hyprctl setenv doesn't
suffice, etc.) belong in commit messages or PR descriptions, not in
code people have to read forever. Kept the few comments that document
genuinely non-obvious behaviour: the keybind-env reason for hl.env in
envs.lua, why the runtime PAM seds stay scripted in
increase-lockout-limit, the chroot/--now distinction in chroot.sh, and
the dev-pkg-test split-install reason.
The previous centralization into enable-services.sh accidentally changed
semantics for three services: docker.socket, iwd.service, and
power-profiles-daemon.service used to be enabled with bare
'sudo systemctl enable' (no --now); chrootable_systemctl_enable promoted
them to 'enable --now' outside chroot.
For iwd this is the riskiest: it's typically what's keeping the installer
online, so starting it mid-install is at best a no-op and at worst can
disrupt the active network connection. For docker.socket and
power-profiles-daemon the change is cosmetic (the socket is already
inactive, the daemon does no harm to start) but the original intent was
deferral to first boot.
New helper in install/helpers/chroot.sh:
chrootable_systemctl_enable_only <unit> # plain enable, no --now
enable-services.sh uses it for the three services that were previously
bare-enabled. The other five (bluetooth, cups, cups-browsed,
avahi-daemon, linux-modules-cleanup) keep the --now behavior they had
under chrootable_systemctl_enable.
The package-installed paths live at /usr/share/omarchy; the script-install
paths live at ~/.local/share/omarchy. $OMARCHY_PATH is set by install.sh
during the install flow and by /etc/profile.d/omarchy.sh in package mode.
Scripts that hard-code the script-install path don't honour that and will
misbehave once shipped to /usr/bin.
16 files migrated via sed s|~/\.local/share/omarchy|$OMARCHY_PATH|g
and s|$HOME/\.local/share/omarchy|$OMARCHY_PATH|g:
bin/:
- omarchy-plymouth-{preview,reset,set}
- omarchy-refresh-{limine,plymouth}
- omarchy-reinstall-configs
- omarchy-show-logo
- omarchy-games-retro-install
- omarchy-install-gaming-battlenet
install/:
- config/branding.sh
- packaging/{fonts,icons}.sh
- post-install/{finished,pacman}.sh
- preflight/{migrations,pacman}.sh
Deliberately NOT migrated:
- bin/omarchy-dev-add-migration: uses 'cd ~/.local/share/omarchy' to
resolve a git repo for 'git log'. The packaged binary doesn't have a
git repo to read; this command only makes sense in dev mode where
the home path is the right one.
- bin/omarchy-install-browser:69: writes a string into a chromium
config file that chromium itself interprets; not a bash-resolved path.
- install/helpers/errors.sh:125: the boot.sh-style relaunch behaviour
needs a separate redesign.
The previous setup scattered systemctl enables across docker.sh,
printer.sh, bluetooth.sh, network.sh, powerprofilesctl-rules.sh, and
kernel-modules-hook.sh. Some used chrootable_systemctl_enable; others
used bare 'sudo systemctl enable'. Centralizing makes the install-time
service surface auditable in one place and consistent in chroot/non-chroot
behavior.
install/config/enable-services.sh enables:
- bluetooth.service
- cups.service, cups-browsed.service, avahi-daemon.service
- docker.socket
- iwd.service
- linux-modules-cleanup.service
- power-profiles-daemon.service
Each via chrootable_systemctl_enable, which uses 'enable --now' on a live
system and bare 'enable' in chroot (set OMARCHY_CHROOT_INSTALL=1).
Stays in original script (deliberately):
- sddm.sh: bare 'systemctl enable sddm.service' (no --now during install
would log the user out; lives in install/login/)
- limine-snapper.sh: limine-snapper-sync (bootloader-specific path)
- first-run/firewall.sh: ufw (deferred to first-run for UX)
- hardware/{t2,intel/lpmd,intel/thermald,apple/fix-suspend-nvme}: gated
on hardware detection
Side cleanups:
- install/config/kernel-modules-hook.sh: DELETED (sole line was the enable
for linux-modules-cleanup, now in enable-services.sh).
- install/config/hardware/printer.sh: DELETED (sole purpose was the
three cups/avahi enables; nothing else to do).
- install/config/hardware/network.sh: keep the systemd-networkd-wait-online
disable+mask; drop the iwd enable.
- install/config/hardware/bluetooth.sh: keep the AutoEnable=false sed and
the wireplumber/bt-agent user-session setup; drop the system enable.
- install/config/docker.sh: drop the docker.socket enable.
- install/config/powerprofilesctl-rules.sh: drop the
power-profiles-daemon enable.
- install/config/all.sh: register enable-services.sh; remove the two
deleted entries.
- bin/omarchy-wifi-powersave: shopt -s nullglob. Previously, machines
with no wireless interfaces ran the loop once with the literal glob
('iface=*'), failed iw, and exited 237. With the new always-installed
udev rule that invokes this on every AC transition, the failure
would surface as failed transient omarchy-wifi-powersave-* units on
desktops. Now it cleanly no-ops.
- migrations/1779307845.sh: add 'systemctl daemon-reload' so the
plocate-updatedb.service.d/ac-only.conf drop-in is picked up on
upgrade. Arch's systemd pacman hook only triggers on
/usr/lib/systemd/system/*, not /etc/systemd/system/*.
- install/config/powerprofilesctl-rules.sh and wifi-powersave-rules.sh:
rm -f the pre-rename legacy paths before udevadm reload, so the
cleanup is idempotent outside the one-shot migration (matters for
re-install/downgrade testing).
Four scripts that wrote static /etc or /usr files become package-shipped
files. The other config-script audit candidates (omarchy-ai-skill,
nautilus-python, omarchy-toggles, input-group) correctly stay scripts —
they're user-level operations, not /etc writes.
New package-owned files (omarchy-installer/etc/, shipped by
omarchy-settings):
- etc/systemd/system/plocate-updatedb.service.d/ac-only.conf
- etc/udev/rules.d/99-omarchy-power-profile.rules
- etc/udev/rules.d/99-omarchy-wifi-powersave.rules
The two udev rule files are renamed for namespacing (99-power-profile
-> 99-omarchy-power-profile; 99-wifi-powersave -> 99-omarchy-wifi-powersave)
and rewritten to invoke /usr/bin/omarchy-powerprofiles-set and
/usr/bin/omarchy-wifi-powersave instead of $HOME/.local/share/omarchy/bin/...
(both binaries ship in the omarchy package at /usr/bin/).
The battery-present gate from the original scripts is dropped — the rules
trigger on power_supply udev events, which are benign on desktops without
batteries. (The runtime commands no-op on AC-only systems.)
Script changes:
- install/config/plocate-ac-only.sh: DELETED (file ships, daemon-reload
happens via pacman hook).
- install/config/unmount-fuse.sh: DELETED (file ships at
/usr/lib/systemd/system-sleep/unmount-fuse via omarchy-settings).
- install/config/powerprofilesctl-rules.sh: SHRINK to runtime ops
(enable power-profiles-daemon, udevadm reload+trigger).
- install/config/wifi-powersave-rules.sh: SHRINK to udevadm reload+trigger.
- install/config/all.sh: drop the two deleted entries.
bin/omarchy-update-system-pkgs: add --overwrite for plocate-updatedb
drop-in and unmount-fuse paths (the udev rule renames need no overwrite
since the new paths are virgin).
migrations/1779307845.sh: remove the legacy 99-power-profile.rules and
99-wifi-powersave.rules paths on existing installs, then reload udev.
Four corrections to the omarchy-settings transition:
- bin/omarchy-update-system-pkgs: pass --overwrite for the 13 paths
omarchy-settings now owns. Existing Omarchy installs would otherwise
fail pacman conflict checks on the upgrade that ships the package
(the files exist as unowned filesystem entries from the previous
install scripts). The flags are no-ops once the transition release
is everyone's baseline; remove then.
- config/mimeapps.list: drop the HEY.desktop mailto mapping. HEY.desktop
is generated by install/packaging/webapps.sh at install time, not
shipped under applications/, so it isn't valid for a fresh /etc/skel
user before the installer runs.
- install/config/mimetypes.sh: add 'xdg-mime default HEY.desktop
x-scheme-handler/mailto' as a runtime op since the mapping no longer
lives in mimeapps.list.
- install/config/increase-lockout-limit.sh: delete existing
pam_faillock.so authsucc lines before re-adding, so re-running the
installer doesn't duplicate the authsucc entry in
/etc/pam.d/sddm-autologin.
config/mimeapps.list ships at /etc/skel/.config/mimeapps.list (via
omarchy-settings's existing config/** -> /etc/skel/.config copy).
New users get the full Omarchy MIME defaults on first login without
running install scripts.
install/config/mimetypes.sh shrinks to two runtime ops that a static
file can't replace:
- omarchy-refresh-applications: copies .desktop entries and icons into
the user home and refreshes the desktop database
- xdg-settings set default-web-browser chromium.desktop: sets the
system-default browser; omarchy-install-browser overrides this if
the user later picks a different browser.
The three /etc writes (resolved.conf.d drop-in, nsswitch.conf override,
cups-browsed.conf override) now ship via omarchy-settings. The service
enables stay scripted because they need chrootable_systemctl_enable to
work both inside the ISO chroot and on a live system.
Each script's static-file write moves to omarchy-settings. The scripts
keep only the runtime side: reload systemd, restart dirmngr, sysctl --system,
or (for lockout-limit) the PAM seds that have to stay scripted because
/etc/pam.d/system-auth and /etc/pam.d/sddm-autologin are upstream-owned and
need targeted line edits rather than a full-file override.
faillock.conf's deny=10 (formerly inside increase-sudo-tries.sh sed) now
rides through the etc-overrides dance in omarchy-settings.
The three Docker config files (etc/docker/daemon.json,
etc/systemd/system/docker.service.d/no-block-boot.conf,
etc/systemd/resolved.conf.d/20-docker-dns.conf) now ship via the
omarchy-settings package. The script keeps only the actions a static
file can't do:
- restart systemd-resolved so the new drop-in takes effect
- usermod -aG docker $USER (user-specific, can't be packaged)
- systemctl enable docker.socket and daemon-reload
Each of these scripts only wrote a single /etc file (or copied
config/bashrc into the user home). The corresponding files now ship
from the omarchy-settings package — either directly into /etc (drop-ins
the package fully owns) or via the /usr/share/omarchy/etc-overrides
post_install dance (for upstream-owned paths).
Deleted:
- install/config/timezones.sh -> etc/sudoers.d/omarchy-tzupdate
- install/config/sudoless-asdcontrol.sh -> etc/sudoers.d/omarchy-asdcontrol
($USER rewritten to %wheel)
- install/config/increase-sudo-tries.sh -> etc/sudoers.d/omarchy-passwd-tries
(faillock.conf side now in etc-overrides)
- install/config/hardware/ignore-power-button.sh
-> etc/systemd/logind.conf.d/10-ignore-power-button.conf
(replaces sed-edit of logind.conf)
- install/config/hardware/usb-autosuspend.sh
-> etc/modprobe.d/omarchy-usb-autosuspend.conf
- install/config/ssh-flakiness.sh -> etc/sysctl.d/99-omarchy-sysctl.conf
- install/config/config.sh -> /etc/skel seeds ~/.config from config/**;
~/.bashrc seeded from etc-overrides
- install/login/plymouth.sh -> theme files already shipped by
omarchy-settings; etc/plymouth/plymouthd.conf
handled via etc-overrides
A separate migration tidies up the old paths on existing installs.
all.sh entries updated to drop these scripts.
Based entirely on the work of @robzolkos in #5711. That attempt was
adapted to better fit the standard templating system while also adding
color mixing for use in other themes.
Adds first-party omarchy.notifications service plugin that hosts a
freedesktop notification server and renders popups + a history popup
inside the shell. Uninstalls mako and retargets every helper, keybind,
indicator, and migration entry to the new daemon.
Plugin (default/quickshell/omarchy-shell/plugins/notifications/):
- Service.qml: NotificationServer, popupModel + pendingModel + pastModel
(two-tier history, see below), DND via PersistentProperties +
cache-file backstop, image cache for /tmp screenshots, IpcHandler with
toggleDnd/setDnd/isDnd/showHistory/clear/clearPending/markAllSeen/
dismissAll/dismissOne/invokeLast/dismiss, per-theme override file
~/.config/omarchy/current/theme/notifications.json honoring
borderColor/backgroundColor/textColor/countdownColor.
- components/NotificationCard.qml: theme-driven card (Color.foreground/
background/border tokens from Commons/Color.qml), 32x32 icon slot,
Nerd Font glyph fallback via omarchy-glyph hint, hero image strip
for screenshot/image-path notifications, hover-pause progress bar,
uses bar.fontFamily so all surfaces share one font.
Filtering and DND:
- transient hint and CLI-style senders (app_name in notify-send /
omarchy-action) bypass history but still pop.
- DND only allows omarchy-action toasts and notify-send -u critical
through; real-app urgency=critical (Discord, Slack, Vesktop) is
silenced and lands in pending instead.
- Pending vs past split surfaced via tabs in the bar widget popup;
past tab is auto-pruned at the 15-minute mark.
- Click-to-jump: notifications without a libnotify default action focus
the matching Hyprland window via class lookup.
Shell host:
- shell.qml: generic first-party service loader (mirrors the existing
noctalia-compat path) and an alias for the bar so plugins can read
barSize / barHidden / position for anchoring.
- Commons/Color.qml: parses the theme's hyprland.conf for
$activeBorderColor so notifications match Hyprland window borders;
picks the explicit accent= key over the color4= alias.
Bar widget rebase (plugins/bar/widgets/notificationCenter.qml):
- Drops the chunk-1 stub server, binds count/dnd state to the service,
hosts the history popup via PopupCard so it drops down from the
notification glyph the same way Quick Settings does.
- Pending/Past tabs, dismiss-individual close X, mark-all-as-seen and
clear-recent action buttons, theme-driven palette.
Quick Settings rework (plugins/bar/widgets/controlCenter.qml):
- DND tile binds directly to service.doNotDisturb for instant feedback.
- Drops the volume slider (already in audioPanel) and the no-op Theme
tile; adds a Bluetooth toggle bound to Quickshell.Bluetooth.
- Bigger 44x44 wallet was scaled back to 32x32 for tighter rows.
Notification scripts (bin/omarchy-*):
- omarchy-notification-send: passes glyph as a custom hint instead of
prepending to the summary; adds -a omarchy-action and -u urgency
automatically; supports -e/--transient passthrough.
- User-action toasts in the capture / toggle / hyprland / default-*
scripts and bindings/utilities.lua now tag themselves
-a omarchy-action so DND treats them as intent-based bypass.
- omarchy-toggle-notification-silencing, omarchy-notification-dismiss,
default/waybar/indicators/notification-silencing.sh, and the Hyprland
comma-keybinds all route through omarchy-shell-ipc notifications.
- omarchy-capture-screenshot / -screenrecording set the image-path
hint properly so the hero-image rendering kicks in.
Mako removal (migrations/1778743515.sh):
- pkill -x mako, systemctl --user stop mako.service, pacman -Rns mako
(uninstalling deletes /usr/lib/systemd/user/mako.service so D-Bus
activation can't respawn it). Removes ~/.config/mako/ and the legacy
toggle file. Restarts quickshell so it claims the bus name.
- Drops mako from install/omarchy-base.packages, autostart.lua,
install/config/theme.sh + toggles.sh, default/themed/mako.ini.tpl,
default/mako/, the omarchy-menu Mako restart row, bin/omarchy
GROUP_DESCRIPTIONS, the settings panel catalogue, and the
default/omarchy-skill paths table.
- Removed scripts: bin/omarchy-restart-mako,
bin/omarchy-style-corners-mako.
- bin/omarchy-style-corners summary updated; corner radius for the
notification card reads ~/.local/state/omarchy/toggles/quickshell-menu.json
alongside the rest of the shell.