Files
arthur-os/bin/omarchy-upgrade-to-4
T

1255 lines
41 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Upgrade a legacy Omarchy install to the package-backed Omarchy 4 layout.
# omarchy:args=[--yes] [--reboot] [--channel stable|rc|edge] [--user USER]
# omarchy:examples=omarchy upgrade to 4
# omarchy:requires-sudo=true
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: omarchy-upgrade-to-4 [--yes] [--reboot] [--channel stable|rc|edge] [--user USER]
Upgrade any pre-4 Omarchy install to the package-backed Omarchy 4 layout.
This script is intentionally self-contained so it can be shipped as a command
in Omarchy 3.8.x or run directly with curl | bash on older systems.
Options:
-y, --yes Do not prompt before upgrading
--reboot Reboot automatically after a successful upgrade
--channel stable|rc|edge Override detected Omarchy package channel
--user USER User account to refresh after package install
-h, --help Show this help
USAGE
}
log() {
printf '\033[32m==>\033[0m %s\n' "$*"
}
warn() {
printf '\033[33mWarning:\033[0m %s\n' "$*" >&2
}
fail() {
printf '\033[31mError:\033[0m %s\n' "$*" >&2
exit 1
}
normalize_channel() {
case "${1:-}" in
stable|master|main) echo stable ;;
rc) echo rc ;;
edge|dev) echo edge ;;
*) return 1 ;;
esac
}
valid_channel() {
normalize_channel "$1" >/dev/null 2>&1
}
channel_override="${OMARCHY_UPGRADE_CHANNEL:-${OMARCHY_CHANNEL:-${OMARCHY_MIRROR:-}}}"
target_user="${OMARCHY_INSTALL_USER:-}"
yes=0
auto_reboot=0
while (($#)); do
case "$1" in
-y|--yes)
yes=1
shift
;;
--reboot)
auto_reboot=1
shift
;;
--channel)
channel_override="${2:-}"
shift 2
;;
--user)
target_user="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
fail "Unknown option: $1"
;;
esac
done
if ! command -v pacman >/dev/null 2>&1; then
fail "This upgrade is only supported on Arch/Omarchy systems with pacman."
fi
if ! command -v sudo >/dev/null 2>&1 && (( EUID != 0 )); then
fail "sudo is required when not running as root."
fi
if [[ -z $target_user ]]; then
if (( EUID == 0 )); then
target_user="${SUDO_USER:-}"
else
target_user="${USER:-$(id -un)}"
fi
fi
if [[ -z $target_user || $target_user == root ]]; then
fail "Could not determine the non-root Omarchy user. Re-run with --user USER."
fi
if ! getent passwd "$target_user" >/dev/null; then
fail "User '$target_user' does not exist."
fi
target_home=$(getent passwd "$target_user" | cut -d: -f6)
[[ -n $target_home && -d $target_home ]] || fail "Home directory for '$target_user' was not found."
target_uid=$(id -u "$target_user")
target_runtime_dir="/run/user/$target_uid"
package_path="/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin:$target_home/.local/bin"
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
run_as_user() {
if (( EUID == 0 )); then
if command -v sudo >/dev/null 2>&1; then
sudo -u "$target_user" -H "$@"
else
runuser -u "$target_user" -- "$@"
fi
else
"$@"
fi
}
run_as_user_session() {
run_as_user env \
HOME="$target_home" \
USER="$target_user" \
LOGNAME="$target_user" \
XDG_RUNTIME_DIR="$target_runtime_dir" \
DBUS_SESSION_BUS_ADDRESS="unix:path=$target_runtime_dir/bus" \
XDG_CURRENT_DESKTOP=Hyprland \
XDG_SESSION_DESKTOP=Hyprland \
DESKTOP_SESSION=hyprland \
"$@"
}
target_hyprland_pid() {
pgrep -u "$target_uid" -x Hyprland 2>/dev/null | head -n1 || true
}
target_process_env() {
local name="$1" pid value
pid=$(target_hyprland_pid)
[[ -n $pid && -r /proc/$pid/environ ]] || return 1
value=$(tr '\0' '\n' <"/proc/$pid/environ" 2>/dev/null | awk -v name="$name" '
BEGIN { prefix = name "=" }
index($0, prefix) == 1 { print substr($0, length(prefix) + 1); exit }
' || true)
[[ -n $value ]] || return 1
printf '%s\n' "$value"
}
valid_wayland_display() {
[[ -n ${1:-} && -S $target_runtime_dir/$1 ]]
}
discover_wayland_display() {
local candidate
candidate="${WAYLAND_DISPLAY:-}"
if valid_wayland_display "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
candidate=$(target_process_env WAYLAND_DISPLAY || true)
if valid_wayland_display "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
local candidates=()
mapfile -t candidates < <(find "$target_runtime_dir" -maxdepth 1 -type s -name 'wayland-*' -printf '%f\n' 2>/dev/null || true)
(( ${#candidates[@]} == 1 )) || return 1
printf '%s\n' "${candidates[0]}"
}
valid_hyprland_signature() {
[[ -n ${1:-} ]] || return 1
[[ -S $target_runtime_dir/hypr/$1/.socket.sock || -S $target_runtime_dir/hypr/$1/.socket2.sock ]]
}
discover_hyprland_signature() {
local candidate
candidate="${HYPRLAND_INSTANCE_SIGNATURE:-}"
if valid_hyprland_signature "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
candidate=$(target_process_env HYPRLAND_INSTANCE_SIGNATURE || true)
if valid_hyprland_signature "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
local candidates=()
mapfile -t candidates < <(find "$target_runtime_dir/hypr" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null || true)
(( ${#candidates[@]} == 1 )) || return 1
printf '%s\n' "${candidates[0]}"
}
detect_channel() {
local candidate="${channel_override:-}"
if [[ -n $candidate ]] && valid_channel "$candidate"; then
normalize_channel "$candidate"
return 0
fi
if command -v omarchy-version-channel >/dev/null 2>&1; then
candidate=$(omarchy-version-channel 2>/dev/null | awk '{ print $1 }' || true)
if [[ -n $candidate ]] && valid_channel "$candidate"; then
normalize_channel "$candidate"
return 0
fi
fi
if [[ -f /etc/pacman.conf ]]; then
if grep -q 'https://pkgs\.omarchy\.org/rc/' /etc/pacman.conf; then
echo rc
return 0
elif grep -q 'https://pkgs\.omarchy\.org/edge/' /etc/pacman.conf; then
echo edge
return 0
elif grep -q 'https://pkgs\.omarchy\.org/stable/' /etc/pacman.conf; then
echo stable
return 0
fi
fi
if [[ -f /etc/pacman.d/mirrorlist ]]; then
if grep -q 'https://rc-mirror\.omarchy\.org/' /etc/pacman.d/mirrorlist; then
echo rc
return 0
elif grep -q 'https://mirror\.omarchy\.org/' /etc/pacman.d/mirrorlist; then
echo edge
return 0
elif grep -q 'https://stable-mirror\.omarchy\.org/' /etc/pacman.d/mirrorlist; then
echo stable
return 0
fi
fi
if [[ -d $target_home/.local/share/omarchy/.git ]]; then
if (( EUID == 0 )); then
candidate=$(run_as_user git -C "$target_home/.local/share/omarchy" branch --show-current 2>/dev/null || true)
else
candidate=$(git -C "$target_home/.local/share/omarchy" branch --show-current 2>/dev/null || true)
fi
if [[ -n $candidate ]] && valid_channel "$candidate"; then
normalize_channel "$candidate"
return 0
fi
fi
echo stable
}
channel=$(detect_channel)
if ! valid_channel "$channel"; then
fail "Invalid channel '$channel'. Use stable, rc, or edge."
fi
case "$channel" in
stable)
mirror_server='https://stable-mirror.omarchy.org/$repo/os/$arch'
package_server='https://pkgs.omarchy.org/stable/$arch'
;;
rc)
mirror_server='https://rc-mirror.omarchy.org/$repo/os/$arch'
package_server='https://pkgs.omarchy.org/rc/$arch'
;;
edge)
mirror_server='https://mirror.omarchy.org/$repo/os/$arch'
package_server='https://pkgs.omarchy.org/edge/$arch'
;;
esac
if (( ! yes )); then
cat <<EOF
This will upgrade Omarchy for user '$target_user' to Omarchy 4 using the '$channel' channel.
It will install the package-backed Omarchy 4 stack, overwrite files previously
written by the legacy installer, remove retired default packages, and apply the
Omarchy 4 system/user transition inline. A reboot is strongly recommended afterward.
EOF
if [[ -r /dev/tty ]]; then
printf '\nContinue? [y/N] ' >/dev/tty
read -r answer </dev/tty
[[ $answer =~ ^[Yy]$ ]] || exit 0
else
fail "No TTY available for confirmation. Re-run with --yes to proceed non-interactively."
fi
fi
sudo_keepalive_pid=""
hyprland_config_reload_suppressed=0
cleanup_on_exit() {
local exit_status=$?
if (( hyprland_config_reload_suppressed )) && declare -F restore_hyprland_config_reload >/dev/null; then
restore_hyprland_config_reload >/dev/null 2>&1 || true
fi
if [[ -n ${sudo_keepalive_pid:-} ]]; then
kill "$sudo_keepalive_pid" 2>/dev/null || true
fi
exit "$exit_status"
}
trap cleanup_on_exit EXIT
if (( EUID != 0 )); then
sudo -v
while true; do sudo -n true; sleep 60; done 2>/dev/null &
sudo_keepalive_pid=$!
fi
backup_suffix=$(date +%Y%m%d%H%M%S)
configure_pacman_channel() {
log "Configuring Omarchy pacman repositories ($channel)"
as_root install -d -m 0755 /etc/pacman.d
if [[ -f /etc/pacman.d/mirrorlist ]]; then
as_root cp -f /etc/pacman.d/mirrorlist "/etc/pacman.d/mirrorlist.omarchy-upgrade-to-4.$backup_suffix.bak"
fi
printf 'Server = %s\n' "$mirror_server" | as_root tee /etc/pacman.d/mirrorlist >/dev/null
if [[ ! -f /etc/pacman.conf ]]; then
fail "/etc/pacman.conf does not exist."
fi
as_root cp -f /etc/pacman.conf "/etc/pacman.conf.omarchy-upgrade-to-4.$backup_suffix.bak"
tmp=$(mktemp)
awk -v server="$package_server" '
BEGIN { in_omarchy = 0; wrote = 0 }
/^\[omarchy\]$/ {
if (!wrote) {
print "[omarchy]"
print "SigLevel = Optional TrustAll"
print "Server = " server
wrote = 1
}
in_omarchy = 1
next
}
/^\[/ && in_omarchy { in_omarchy = 0 }
!in_omarchy { print }
END {
if (!wrote) {
print ""
print "[omarchy]"
print "SigLevel = Optional TrustAll"
print "Server = " server
}
}
' /etc/pacman.conf >"$tmp"
as_root install -m 0644 "$tmp" /etc/pacman.conf
rm -f "$tmp"
}
install_keyrings() {
log "Refreshing package databases and keyrings"
as_root pacman -Sy --noconfirm archlinux-keyring omarchy-keyring || {
warn "Keyring package install failed; attempting to trust the Omarchy packaging key directly."
as_root pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org || true
as_root pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 || true
as_root pacman -Sy --noconfirm archlinux-keyring omarchy-keyring
}
}
remove_legacy_installer_package() {
if pacman -Q omarchy-installer >/dev/null 2>&1; then
log "Removing legacy omarchy-installer package"
# The pre-4 omarchy meta package may depend on omarchy-installer. Ignore
# that edge temporarily; the next transaction installs the Omarchy 4 meta.
as_root pacman -Rdd --noconfirm omarchy-installer
fi
}
remove_conflicting_legacy_packages() {
# Remove legacy packages that are known to conflict with Omarchy 4 defaults
# before the main package transaction. Broader retired-package cleanup runs
# after NetworkManager and the rest of the new stack are installed.
local conflicting_packages=(
asdcontrol-git
lazydocker-bin
localsend-bin
omarchy-chromium
omarchy-lazyvim
openai-codex-bin
ttf-jetbrains-mono
ttf-jetbrains-mono-nerd
wayfreeze
wayfreeze-git
)
local pkg
for pkg in "${conflicting_packages[@]}"; do
if pacman -Q "$pkg" >/dev/null 2>&1; then
# Some pre-4 meta packages depend on these retired/default packages. We
# intentionally break those old dependency edges before installing the
# Omarchy 4 meta, otherwise pacman prompts to remove conflicts (for
# example ttf-jetbrains-mono-nerd -> ttf-jetbrains-mono-nerd-basic) and
# --noconfirm answers "no".
as_root pacman -Rdd --noconfirm "$pkg" || warn "Could not remove conflicting legacy package '$pkg'; continuing."
fi
done
}
install_omarchy_4_packages() {
local core_packages=(
omarchy-keyring
omarchy-settings
omarchy-limine
omarchy-nvim
omarchy
)
log "Installing Omarchy 4 packages"
# --noconfirm answers "no" to conflict-removal prompts. --ask 4 preselects
# removing the conflicting package, which is required for legacy packages like
# ttf-jetbrains-mono-nerd -> ttf-jetbrains-mono-nerd-basic.
as_root pacman -Syu --needed --noconfirm --ask 4 --overwrite='*' "${core_packages[@]}"
if [[ -f /usr/share/omarchy/install/omarchy-base.packages ]]; then
log "Installing Omarchy 4 default package set"
mapfile -t base_packages < <(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' /usr/share/omarchy/install/omarchy-base.packages)
if (( ${#base_packages[@]} )); then
as_root pacman -S --needed --noconfirm --ask 4 --overwrite='*' "${base_packages[@]}"
fi
else
warn "/usr/share/omarchy/install/omarchy-base.packages was not found; skipping default package set install."
fi
# Archinstall installs the audio application during fresh ISO installs, so
# legacy upgrades need to ensure the same PipeWire stack is present without
# depending on Archinstall or Omarchy setup helpers.
local audio_packages=(
pipewire
pipewire-alsa
pipewire-jack
pipewire-pulse
gst-plugin-pipewire
libpulse
wireplumber
)
as_root pacman -S --needed --noconfirm --ask 4 --overwrite='*' "${audio_packages[@]}"
}
remove_retired_default_packages() {
local retired_packages=(
1password-beta
1password-cli
alacritty
asdcontrol-git
blueberry
bluetui
chaotic-keyring
chaotic-mirrorlist
claude-code
elephant
elephant-all
elephant-bluetooth
elephant-calc
elephant-clipboard
elephant-desktopapplications
elephant-files
elephant-menus
elephant-providerlist
elephant-runner
elephant-symbols
elephant-todo
elephant-unicode
elephant-websearch
fcitx5-configtool
gcc14
github-cli
hypridle
hyprlock
impala
iwd
lazydocker-bin
lmstudio
lmstudio-bin
localsend-bin
makima-bin
mako
omarchy-chromium
omarchy-lazyvim
omarchy-walker
openai-codex
openai-codex-bin
opencode
pavucontrol
playerctl
polkit-gnome
qt5-remoteobjects
signal-desktop
spotify
swaybg
swayosd
ttf-jetbrains-mono
ttf-jetbrains-mono-nerd
vlc
waybar
wayfreeze
wayfreeze-git
wf-recorder
wiremix
wl-screenrec
yay-bin-debug
zoom
)
log "Removing packages retired from the Omarchy 4 default install"
local pkg
local installed_retired=()
for pkg in "${retired_packages[@]}"; do
if pacman -Q "$pkg" >/dev/null 2>&1; then
installed_retired+=("$pkg")
fi
done
((${#installed_retired[@]})) || return 0
# Remove as one transaction so dependency anchors like omarchy-walker and all
# of its elephant-* modules disappear together instead of failing one-by-one.
if as_root pacman -Rns --noconfirm --ask 4 "${installed_retired[@]}"; then
return 0
fi
warn "Batch retired-package removal failed; retrying packages individually."
for pkg in "${installed_retired[@]}"; do
if pacman -Q "$pkg" >/dev/null 2>&1; then
as_root pacman -Rns --noconfirm --ask 4 "$pkg" || warn "Could not remove retired package '$pkg'; leaving it installed."
fi
done
}
cleanup_legacy_user_paths() {
log "Cleaning legacy user command shims"
run_as_user env TARGET_HOME="$target_home" BACKUP_SUFFIX="$backup_suffix" bash <<'USER_CLEANUP'
set -euo pipefail
local_bin="$TARGET_HOME/.local/bin"
legacy_root="$TARGET_HOME/.local/share/omarchy"
legacy_bin="$legacy_root/bin/"
if [[ -d $local_bin ]]; then
shopt -s nullglob
for link in "$local_bin"/omarchy*; do
[[ -L $link ]] || continue
target=$(readlink "$link" || true)
case "$target" in
"$legacy_bin"*|../share/omarchy/bin/*|*.local/share/omarchy/bin/*)
rm -f "$link"
;;
esac
done
fi
# Existing terminals still have OMARCHY_PATH=$HOME/.local/share/omarchy and
# often have $OMARCHY_PATH/bin early in PATH. Make that stale path resolve to
# the package-backed tree for the rest of the live session, while backing up
# the old git checkout for inspection/rollback.
if [[ -e /usr/share/omarchy ]]; then
mkdir -p "$TARGET_HOME/.local/share"
if [[ -L $legacy_root ]]; then
ln -sfn /usr/share/omarchy "$legacy_root"
elif [[ -e $legacy_root ]]; then
backup="$legacy_root.omarchy-upgrade-to-4.$BACKUP_SUFFIX.bak"
mv "$legacy_root" "$backup"
ln -sfn /usr/share/omarchy "$legacy_root"
else
ln -sfn /usr/share/omarchy "$legacy_root"
fi
fi
USER_CLEANUP
}
cleanup_retired_user_unit_files() {
run_as_user bash <<'USER_UNITS'
set -euo pipefail
units=(
hyprpolkitagent.service
mako.service
omarchy-battery-monitor.service
omarchy-battery-monitor.timer
omarchy-shell.service
swayosd-server.service
elephant.service
app-walker@autostart.service
)
shopt -s nullglob
for base in "$HOME/.config/systemd/user" "$HOME/.local/share/systemd/user"; do
[[ -d $base ]] || continue
for unit in "${units[@]}"; do
rm -f "$base/$unit"
for link in "$base"/*.wants/"$unit" "$base"/*.requires/"$unit"; do
rm -f "$link"
done
done
done
USER_UNITS
}
cleanup_retired_services() {
log "Disabling services retired by Omarchy 4"
# Do not stop iwd mid-upgrade; disabling is enough and avoids dropping a
# running Wi-Fi connection before the user can reboot into NetworkManager.
as_root systemctl disable iwd.service >/dev/null 2>&1 || true
cleanup_retired_user_unit_files
if [[ -S $target_runtime_dir/bus ]]; then
run_as_user_session systemctl --user disable \
hyprpolkitagent.service \
mako.service \
omarchy-battery-monitor.service \
omarchy-battery-monitor.timer \
omarchy-shell.service \
swayosd-server.service \
elephant.service \
app-walker@autostart.service \
>/dev/null 2>&1 || true
run_as_user_session systemctl --user daemon-reload >/dev/null 2>&1 || true
fi
}
stop_retired_session_processes() {
log "Stopping retired Omarchy 3 session processes"
if [[ -S $target_runtime_dir/bus ]]; then
run_as_user_session systemctl --user stop \
hyprpolkitagent.service \
mako.service \
omarchy-battery-monitor.service \
omarchy-battery-monitor.timer \
omarchy-shell.service \
swayosd-server.service \
elephant.service \
app-walker@autostart.service \
>/dev/null 2>&1 || true
fi
# Removed packages do not stop already-running processes. Stop the old UI
# only after the new Quickshell has been verified, so users are not left
# without a bar/launcher if current-session shell startup fails.
run_as_user pkill -x waybar >/dev/null 2>&1 || true
run_as_user pkill -x walker >/dev/null 2>&1 || true
run_as_user pkill -x elephant >/dev/null 2>&1 || true
run_as_user pkill -x mako >/dev/null 2>&1 || true
run_as_user pkill -x swayosd-server >/dev/null 2>&1 || true
run_as_user_session systemctl --user daemon-reload >/dev/null 2>&1 || true
}
start_omarchy_shell_session() {
# Omarchy 4 starts Quickshell from Hyprland autostart, not a user service.
# Reboot is still the real cutover, but start the new shell in the current
# Wayland session when possible so users do not sit without a bar after the
# retired waybar/walker/elephant processes are stopped.
local quickshell_bin omarchy_shell_bin shell_log
quickshell_bin=$(command -v quickshell || true)
[[ -n $quickshell_bin ]] || { warn "quickshell is not available; Omarchy shell will start after reboot."; return 1; }
omarchy_shell_bin=/usr/bin/omarchy-shell
[[ -x $omarchy_shell_bin ]] || omarchy_shell_bin=$(command -v omarchy-shell || true)
[[ -n $omarchy_shell_bin ]] || { warn "omarchy-shell is not available; Omarchy shell will start after reboot."; return 1; }
[[ -f /usr/share/omarchy/shell/shell.qml ]] || { warn "/usr/share/omarchy/shell is missing; Omarchy shell will start after reboot."; return 1; }
[[ -d $target_runtime_dir ]] || { warn "No running user session found; Omarchy shell will start after reboot."; return 1; }
shell_log="$target_home/.local/state/omarchy/omarchy-shell-upgrade.log"
run_as_user mkdir -p "$target_home/.local/state/omarchy" >/dev/null 2>&1 || true
local wayland_display hyprland_signature
wayland_display=$(discover_wayland_display || true)
[[ -n $wayland_display ]] || { warn "No Wayland display found; Omarchy shell will start after reboot."; return 1; }
hyprland_signature=$(discover_hyprland_signature || true)
[[ -n $hyprland_signature ]] || { warn "No Hyprland session found; Omarchy shell will start after reboot."; return 1; }
log "Starting Omarchy shell in the current session"
run_as_user env \
HOME="$target_home" \
USER="$target_user" \
LOGNAME="$target_user" \
XDG_RUNTIME_DIR="$target_runtime_dir" \
WAYLAND_DISPLAY="$wayland_display" \
HYPRLAND_INSTANCE_SIGNATURE="$hyprland_signature" \
DBUS_SESSION_BUS_ADDRESS="unix:path=$target_runtime_dir/bus" \
XDG_CURRENT_DESKTOP=Hyprland \
XDG_SESSION_DESKTOP=Hyprland \
XDG_SESSION_TYPE=wayland \
QT_QPA_PLATFORM=wayland \
GDK_BACKEND=wayland,x11,* \
DESKTOP_SESSION=hyprland \
OMARCHY_PATH=/usr/share/omarchy \
PATH="$package_path" \
bash -c '
omarchy_shell_bin=$1
quickshell_bin=$2
shell_log=$3
if [[ $("$omarchy_shell_bin" lock isLocked 2>/dev/null || true) == "true" ]]; then
exit 2
fi
pkill -x quickshell 2>/dev/null || true
sleep 0.2
: >"$shell_log"
setsid "$quickshell_bin" -n -p /usr/share/omarchy/shell >>"$shell_log" 2>&1 &
' bash "$omarchy_shell_bin" "$quickshell_bin" "$shell_log" \
|| { warn "Could not start Omarchy shell in the current session; it will start after reboot. See $shell_log"; return 1; }
local attempt
for attempt in {1..60}; do
if run_as_user env \
HOME="$target_home" \
USER="$target_user" \
LOGNAME="$target_user" \
XDG_RUNTIME_DIR="$target_runtime_dir" \
DBUS_SESSION_BUS_ADDRESS="unix:path=$target_runtime_dir/bus" \
OMARCHY_PATH=/usr/share/omarchy \
PATH="$package_path" \
"$omarchy_shell_bin" shell ping >/dev/null 2>&1; then
return 0
fi
sleep 0.25
done
warn "Omarchy shell did not respond in the current session; it will start after reboot. See $shell_log"
return 1
}
run_hyprctl_session() {
local wayland_display hyprland_signature hyprctl_bin
[[ -d $target_runtime_dir ]] || return 1
hyprctl_bin=/usr/bin/hyprctl
[[ -x $hyprctl_bin ]] || hyprctl_bin=$(command -v hyprctl || true)
[[ -n $hyprctl_bin ]] || return 1
wayland_display=$(discover_wayland_display || true)
[[ -n $wayland_display ]] || return 1
hyprland_signature=$(discover_hyprland_signature || true)
[[ -n $hyprland_signature ]] || return 1
run_as_user env \
HOME="$target_home" \
USER="$target_user" \
LOGNAME="$target_user" \
XDG_RUNTIME_DIR="$target_runtime_dir" \
WAYLAND_DISPLAY="$wayland_display" \
HYPRLAND_INSTANCE_SIGNATURE="$hyprland_signature" \
DBUS_SESSION_BUS_ADDRESS="unix:path=$target_runtime_dir/bus" \
XDG_CURRENT_DESKTOP=Hyprland \
XDG_SESSION_DESKTOP=Hyprland \
XDG_SESSION_TYPE=wayland \
DESKTOP_SESSION=hyprland \
OMARCHY_PATH=/usr/share/omarchy \
PATH="$package_path" \
"$hyprctl_bin" "$@"
}
suppress_hyprland_config_reload() {
log "Suppressing live Hyprland reloads during the config swap"
local changed=0
if run_hyprctl_session keyword misc:disable_autoreload true >/dev/null 2>&1; then
changed=1
fi
if run_hyprctl_session eval 'hl.config({ misc = { disable_autoreload = true }, debug = { suppress_errors = true } })' >/dev/null 2>&1; then
changed=1
fi
run_hyprctl_session keyword debug:suppress_errors true >/dev/null 2>&1 || true
if (( changed )); then
hyprland_config_reload_suppressed=1
else
warn "Could not disable Hyprland auto-reload; the live session may briefly report config errors."
fi
}
restore_hyprland_config_reload() {
(( hyprland_config_reload_suppressed )) || return 0
run_hyprctl_session keyword misc:disable_autoreload false >/dev/null 2>&1 || true
run_hyprctl_session eval 'hl.config({ misc = { disable_autoreload = false } })' >/dev/null 2>&1 || true
run_hyprctl_session keyword debug:suppress_errors false >/dev/null 2>&1 || true
run_hyprctl_session eval 'hl.config({ debug = { suppress_errors = false } })' >/dev/null 2>&1 || true
hyprland_config_reload_suppressed=0
}
reload_hyprland_config_after_upgrade() {
log "Reloading Hyprland config with a full reset"
if run_hyprctl_session reload full-reset >/dev/null 2>&1; then
restore_hyprland_config_reload
return 0
fi
warn "hyprctl reload full-reset failed; trying a regular Hyprland reload. If this session is still running an older Hyprland, reboot will complete the cutover."
if ! run_hyprctl_session reload >/dev/null 2>&1; then
warn "Could not reload the live Hyprland session; the new config will load after reboot."
fi
restore_hyprland_config_reload
}
enable_system_service() {
local unit="$1"
as_root systemctl enable "$unit" >/dev/null 2>&1 || warn "Could not enable $unit; it may not be installed on this system."
}
apply_firewall_defaults() {
if ! command -v ufw >/dev/null 2>&1; then
warn "ufw is not installed; skipping firewall defaults."
return 0
fi
log "Applying Omarchy firewall defaults"
as_root ufw default deny incoming >/dev/null || true
as_root ufw default allow outgoing >/dev/null || true
as_root ufw allow 53317/udp >/dev/null || true
as_root ufw allow 53317/tcp >/dev/null || true
as_root ufw allow in proto udp from 172.16.0.0/12 to 172.17.0.1 port 53 comment 'allow-docker-dns' >/dev/null || true
as_root ufw allow in proto udp from 192.168.0.0/16 to 172.17.0.1 port 53 comment 'allow-docker-dns' >/dev/null || true
as_root sed -i 's/^ENABLED=.*/ENABLED=yes/' /etc/ufw/ufw.conf 2>/dev/null || true
enable_system_service ufw.service
}
apply_system_transition() {
log "Applying Omarchy 4 system transition"
[[ -d /usr/share/omarchy ]] || fail "/usr/share/omarchy was not installed."
as_root install -d -m 0755 /usr/share/icons/Yaru/scalable/actions
as_root ln -snf /usr/share/icons/Adwaita/symbolic/actions/go-previous-symbolic.svg \
/usr/share/icons/Yaru/scalable/actions/go-previous-symbolic.svg
as_root ln -snf /usr/share/icons/Adwaita/symbolic/actions/go-next-symbolic.svg \
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true
as_root install -d -m 0777 /etc/chromium/policies/managed
as_root install -d -m 0755 /usr/lib/chromium
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
if getent group docker >/dev/null; then
as_root usermod -aG docker "$target_user"
fi
if [[ -f /usr/bin/powerprofilesctl ]]; then
as_root sed -i '/env python3/ c\#!/bin/python3' /usr/bin/powerprofilesctl || true
fi
as_root install -d -m 0755 /etc/systemd/resolved.conf.d /etc/docker
cat <<'EOF' | as_root tee /etc/systemd/resolved.conf.d/10-disable-multicast.conf >/dev/null
[Resolve]
LLMNR=no
MulticastDNS=no
EOF
cat <<'EOF' | as_root tee /etc/systemd/resolved.conf.d/20-docker-dns.conf >/dev/null
[Resolve]
DNSStubListenerExtra=172.17.0.1
EOF
cat <<'EOF' | as_root tee /etc/docker/daemon.json >/dev/null
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "5" },
"dns": ["172.17.0.1"],
"bip": "172.17.0.1/16"
}
EOF
enable_system_service cups.service
enable_system_service cups-browsed.service
enable_system_service avahi-daemon.service
enable_system_service linux-modules-cleanup.service
enable_system_service docker.socket
enable_system_service systemd-resolved.service
enable_system_service NetworkManager.service
enable_system_service power-profiles-daemon.service
enable_system_service sddm.service
apply_firewall_defaults
log "Normalizing SDDM login configuration"
as_root install -d -m 0755 /etc/sddm.conf.d
printf '[Theme]\nCurrent=omarchy\n' | as_root tee /etc/sddm.conf.d/10-theme.conf >/dev/null
cat <<'EOF' | as_root tee /etc/sddm.conf.d/10-wayland.conf >/dev/null
[General]
DisplayServer=wayland
[Wayland]
CompositorCommand=start-hyprland -- --config /usr/share/sddm/hyprland.lua
EOF
cat <<'EOF' | as_root tee /etc/sddm.conf.d/99-omarchy-login.conf >/dev/null
[Theme]
Current=omarchy
[Users]
RememberLastUser=true
RememberLastSession=true
EOF
if as_root test -f /etc/sddm.conf.d/autologin.conf; then
local autologin_user
autologin_user=$(as_root awk -F= '/^User=/ { print $2; exit }' /etc/sddm.conf.d/autologin.conf 2>/dev/null || true)
[[ -n $autologin_user ]] || autologin_user="$target_user"
cat <<EOF | as_root tee /etc/sddm.conf.d/autologin.conf >/dev/null
[Autologin]
User=$autologin_user
Session=omarchy.desktop
EOF
fi
as_root install -d -m 0755 -o sddm -g sddm /var/lib/sddm 2>/dev/null || as_root install -d -m 0755 /var/lib/sddm
cat <<EOF | as_root tee /var/lib/sddm/state.conf >/dev/null
[Last]
Session=omarchy.desktop
User=$target_user
EOF
as_root chown sddm:sddm /var/lib/sddm /var/lib/sddm/state.conf 2>/dev/null || true
as_root rm -f /etc/systemd/system/getty@tty1.service.d/autologin.conf
as_root rm -f /etc/systemd/system/plymouth-quit.service.d/wait-for-graphical.conf
as_root rm -f /etc/systemd/system/omarchy-seamless-login.service
as_root rm -f /usr/local/bin/seamless-login
as_root systemctl disable omarchy-seamless-login.service >/dev/null 2>&1 || true
as_root systemctl disable makima.service >/dev/null 2>&1 || true
as_root rm -rf /etc/systemd/system/makima.service.d
as_root rm -f /etc/udev/rules.d/99-uinput.rules
as_root systemctl disable dell-xps-haptic-touchpad.service >/dev/null 2>&1 || true
as_root rm -f /etc/systemd/system/dell-xps-haptic-touchpad.service
as_root rm -f /etc/systemd/system/multi-user.target.wants/dell-xps-haptic-touchpad.service
as_root rm -rf /etc/systemd/system/dell-xps-haptic-touchpad.service.d
as_root rm -f /etc/udev/rules.d/99-dell-xps-haptic-touchpad.rules
as_root rm -f /etc/omarchy-dell-haptic-touchpad.env
as_root rm -f /etc/pacman.d/hooks/walker-restart.hook
as_root systemctl daemon-reload >/dev/null 2>&1 || true
if [[ -f /etc/pam.d/sddm ]]; then
as_root sed -i '/-auth.*pam_gnome_keyring\.so/d' /etc/pam.d/sddm
as_root sed -i '/-password.*pam_gnome_keyring\.so/d' /etc/pam.d/sddm
fi
if [[ -f /etc/pam.d/system-auth ]]; then
as_root sed -i 's|^\(auth\s\+required\s\+pam_faillock.so\)\s\+preauth.*$|\1 preauth silent deny=10 unlock_time=120|' /etc/pam.d/system-auth
as_root sed -i 's|^\(auth\s\+\[default=die\]\s\+pam_faillock.so\)\s\+authfail.*$|\1 authfail deny=10 unlock_time=120|' /etc/pam.d/system-auth
fi
if [[ -f /etc/pam.d/sddm-autologin ]]; then
as_root sed -i '/pam_faillock\.so preauth/d' /etc/pam.d/sddm-autologin
as_root sed -i '/pam_faillock\.so authsucc/d' /etc/pam.d/sddm-autologin
as_root sed -i '/auth.*pam_permit\.so/a auth required pam_faillock.so authsucc' /etc/pam.d/sddm-autologin
fi
}
apply_user_transition() {
log "Applying Omarchy 4 user transition"
[[ -d /usr/share/omarchy ]] || fail "/usr/share/omarchy was not installed."
run_as_user env \
TARGET_HOME="$target_home" \
BACKUP_SUFFIX="$backup_suffix" \
OMARCHY_USER_NAME="${OMARCHY_USER_NAME:-}" \
OMARCHY_USER_EMAIL="${OMARCHY_USER_EMAIL:-}" \
bash <<'USER_SETUP'
set -euo pipefail
root=/usr/share/omarchy
state_dir="$HOME/.local/state/omarchy"
mkdir -p "$state_dir" "$HOME/.config"
if [[ -d $root/config ]]; then
cp -R "$root/config/." "$HOME/.config/"
fi
if [[ -f $root/default/bashrc ]]; then
cp "$root/default/bashrc" "$HOME/.bashrc"
echo '[[ -f ~/.bashrc ]] && . ~/.bashrc' >"$HOME/.bash_profile"
fi
mkdir -p "$HOME/.agents/skills" "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.pi/agent/skills"
if [[ -d $root/default/omarchy-skill ]]; then
ln -sfn "$root/default/omarchy-skill" "$HOME/.agents/skills/omarchy"
ln -sfn "$root/default/omarchy-skill" "$HOME/.claude/skills/omarchy"
ln -sfn "$root/default/omarchy-skill" "$HOME/.codex/skills/omarchy"
ln -sfn "$root/default/omarchy-skill" "$HOME/.pi/agent/skills/omarchy"
fi
mkdir -p "$HOME/.local/state/omarchy/toggles/hypr"
if [[ -f $root/default/hypr/toggles/flags.lua ]]; then
cp "$root/default/hypr/toggles/flags.lua" "$HOME/.local/state/omarchy/toggles/hypr/"
fi
mkdir -p "$HOME/.local/share/nautilus-python/extensions"
for extension in localsend.py transcode.py; do
if [[ -f $root/default/nautilus-python/extensions/$extension ]]; then
cp "$root/default/nautilus-python/extensions/$extension" "$HOME/.local/share/nautilus-python/extensions/"
fi
done
mkdir -p "$HOME/.config/omarchy/branding"
[[ -f $root/icon.txt ]] && cp "$root/icon.txt" "$HOME/.config/omarchy/branding/about.txt"
[[ -f $root/logo.txt ]] && cp "$root/logo.txt" "$HOME/.config/omarchy/branding/screensaver.txt"
mkdir -p "$HOME/Downloads" "$HOME/Pictures" "$HOME/Videos" "$HOME/Projects" "$HOME/.config/gtk-3.0"
if command -v xdg-user-dirs-update >/dev/null 2>&1; then
xdg-user-dirs-update --set TEMPLATES "$HOME" || true
xdg-user-dirs-update --set PUBLICSHARE "$HOME" || true
xdg-user-dirs-update --set DESKTOP "$HOME" || true
fi
rmdir "$HOME/Templates" "$HOME/Public" "$HOME/Desktop" 2>/dev/null || true
touch "$HOME/.config/gtk-3.0/bookmarks"
for dir in Downloads Projects Pictures Videos; do
bookmark="file://$HOME/$dir $dir"
grep -qxF "$bookmark" "$HOME/.config/gtk-3.0/bookmarks" || echo "$bookmark" >>"$HOME/.config/gtk-3.0/bookmarks"
done
conf=/etc/vconsole.conf
hyprlua="$HOME/.config/hypr/input.lua"
if [[ -f $conf && -f $hyprlua ]]; then
sed -i '/^[[:space:]]*kb_layout[[:space:]]*=/d' "$hyprlua"
sed -i '/^[[:space:]]*kb_variant[[:space:]]*=/d' "$hyprlua"
if grep -q '^XKBLAYOUT=' "$conf"; then
layout=$(grep '^XKBLAYOUT=' "$conf" | cut -d= -f2 | tr -d '"')
sed -i "/^[[:space:]]*kb_options *=/i\ kb_layout = \"$layout\"," "$hyprlua"
fi
if grep -q '^XKBVARIANT=' "$conf"; then
variant=$(grep '^XKBVARIANT=' "$conf" | cut -d= -f2 | tr -d '"')
sed -i "/^[[:space:]]*kb_options *=/i\ kb_variant = \"$variant\"," "$hyprlua"
fi
fi
if [[ -n ${OMARCHY_USER_NAME//[[:space:]]/} ]]; then
git config --global user.name "$OMARCHY_USER_NAME" || true
fi
if [[ -n ${OMARCHY_USER_EMAIL//[[:space:]]/} ]]; then
git config --global user.email "$OMARCHY_USER_EMAIL" || true
fi
if [[ ! -f $HOME/.XCompose ]]; then
cat >"$HOME/.XCompose" <<EOF
# Run omarchy-restart-xcompose to apply changes
# Include fast emoji access
include "/usr/share/omarchy/default/xcompose"
# Identification
<Multi_key> <space> <n> : "$OMARCHY_USER_NAME"
<Multi_key> <space> <e> : "$OMARCHY_USER_EMAIL"
EOF
fi
mkdir -p "$HOME/.config/omarchy/themes" "$HOME/.config/btop/themes"
ln -snf "$HOME/.config/omarchy/current/theme/btop.theme" "$HOME/.config/btop/themes/current.theme"
if [[ ! -e $HOME/.config/omarchy/current/background && -d $HOME/.config/omarchy/current/theme/backgrounds ]]; then
background=$(find "$HOME/.config/omarchy/current/theme/backgrounds" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) | sort | head -n1)
[[ -n ${background:-} ]] && ln -snf "$background" "$HOME/.config/omarchy/current/background"
fi
mkdir -p "$HOME/.config/wireplumber/wireplumber.conf.d"
if [[ -f $root/default/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf ]]; then
cp "$root/default/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf" "$HOME/.config/wireplumber/wireplumber.conf.d/"
fi
enable_user_unit() {
local unit="$1" unit_path="$HOME/.config/systemd/user/$1" target
local -a targets=()
[[ -f $unit_path ]] || return 0
mapfile -t targets < <(awk -F= '
/^\[Install\]/ { in_install=1; next }
/^\[/ { in_install=0 }
in_install && $1 == "WantedBy" {
n=split($2, values, /[[:space:]]+/)
for (i = 1; i <= n; i++) if (values[i] != "") print values[i]
}
' "$unit_path")
((${#targets[@]})) || targets=(default.target)
for target in "${targets[@]}"; do
mkdir -p "$HOME/.config/systemd/user/$target.wants"
ln -sfn "../$unit" "$HOME/.config/systemd/user/$target.wants/$unit"
done
}
enable_user_unit bt-agent.service
enable_user_unit omarchy-sleep-lock.service
mkdir -p "$HOME/.config/systemd/user/default.target.wants"
for unit in pipewire-pulse.service pipewire-pulse.socket; do
if [[ -e /usr/lib/systemd/user/$unit ]]; then
ln -sfn "/usr/lib/systemd/user/$unit" "$HOME/.config/systemd/user/default.target.wants/$unit"
fi
done
keyring_dir="$HOME/.local/share/keyrings"
keyring_file="$keyring_dir/Default_keyring.keyring"
default_file="$keyring_dir/default"
mkdir -p "$keyring_dir"
if [[ ! -f $keyring_file ]]; then
cat >"$keyring_file" <<EOF
[keyring]
display-name=Default keyring
ctime=$(date +%s)
mtime=0
lock-on-idle=false
lock-after=false
EOF
fi
if [[ ! -f $default_file ]]; then
printf 'Default_keyring\n' >"$default_file"
fi
chmod 700 "$keyring_dir"
chmod 600 "$keyring_file"
chmod 644 "$default_file"
mkdir -p "$HOME/.local/share/applications"
rm -f "$HOME/.config/autostart/walker.desktop"
rm -rf "$HOME/.config/elephant" "$HOME/.config/makima" "$HOME/.config/wiremix"
rm -rf "$HOME/.config/systemd/user/app-walker@autostart.service.d"
rm -f \
"$HOME/.config/systemd/user/omarchy-battery-monitor.service" \
"$HOME/.config/systemd/user/omarchy-battery-monitor.timer" \
"$HOME/.config/systemd/user/omarchy-shell.service" \
"$HOME/.config/systemd/user/swayosd-server.service" \
"$HOME/.config/systemd/user/elephant.service"
rm -f \
"$HOME/.local/state/omarchy/toggles/mako.ini" \
"$HOME/.local/state/omarchy/toggles/waybar-off" \
"$HOME/.local/state/omarchy/toggles/hyprlock.conf" \
"$HOME/.local/state/omarchy/toggles/lock.conf" \
"$HOME/.local/state/omarchy/toggles/hypr/rounded-corners.lua"
for desktop in \
About.desktop \
Activity.desktop \
Alacritty.desktop \
Cliamp.desktop \
blueberry.desktop \
nvim.desktop \
omarchy.desktop \
org.pulseaudio.pavucontrol.desktop \
vlc.desktop \
wiremix.desktop; do
rm -f "$HOME/.local/share/applications/$desktop"
done
legacy_icons_dir="$HOME/.local/share/applications/icons"
legacy_icons_backup="$HOME/.local/share/applications/icons.omarchy-upgrade-to-4.$BACKUP_SUFFIX.bak"
if [[ -d $legacy_icons_dir ]]; then
mkdir -p "$legacy_icons_backup"
for icon in \
Basecamp.png \
Battle.net.png \
ChatGPT.png \
Cliamp.png \
Discord.png \
"Disk Usage.png" \
Docker.png \
Figma.png \
Fizzy.png \
GitHub.png \
"Google Contacts.png" \
"Google Maps.png" \
"Google Messages.png" \
"Google Photos.png" \
HEY.png \
imv.png \
"Retro Gaming.png" \
WhatsApp.png \
windows.png \
X.png \
YouTube.png \
Zoom.png; do
if [[ -e $legacy_icons_dir/$icon ]]; then
mv -f "$legacy_icons_dir/$icon" "$legacy_icons_backup/"
fi
done
rmdir "$legacy_icons_dir" 2>/dev/null || true
rmdir "$legacy_icons_backup" 2>/dev/null || true
fi
if [[ -d $root/applications ]]; then
find "$root/applications" -maxdepth 1 -type f -name '*.desktop' -exec cp -f {} "$HOME/.local/share/applications/" \;
fi
if [[ -f $root/default/alacritty/Alacritty.desktop && ! -f $HOME/.local/share/applications/Alacritty.desktop ]]; then
cp "$root/default/alacritty/Alacritty.desktop" "$HOME/.local/share/applications/"
fi
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true
fi
if command -v xdg-settings >/dev/null 2>&1; then
xdg-settings set default-web-browser chromium.desktop || true
fi
if command -v xdg-mime >/dev/null 2>&1; then
xdg-mime default HEY.desktop x-scheme-handler/mailto || true
fi
touch "$state_dir/setup-user.done"
USER_SETUP
}
configure_pacman_channel
install_keyrings
remove_legacy_installer_package
remove_conflicting_legacy_packages
install_omarchy_4_packages
cleanup_legacy_user_paths
apply_system_transition
suppress_hyprland_config_reload
apply_user_transition
cleanup_retired_services
remove_retired_default_packages
reload_hyprland_config_after_upgrade
if start_omarchy_shell_session; then
stop_retired_session_processes
else
warn "Leaving retired Omarchy 3 session processes running until reboot."
fi
cat <<EOF
Omarchy 4 upgrade complete.
IMPORTANT: Review the output above before rebooting.
DO NOT REBOOT IF THERE ARE ERRORS above; fix the errors or ask for help first.
A reboot is required to finish switching to the package-backed Omarchy 4 session.
EOF
if (( auto_reboot )); then
log "Rebooting because --reboot was passed"
as_root systemctl reboot
elif { exec {tty_fd}<>/dev/tty; } 2>/dev/null; then
printf '\nReboot now? [y/N] ' >&$tty_fd
read -r reboot_answer <&$tty_fd || reboot_answer=""
exec {tty_fd}>&-
if [[ $reboot_answer =~ ^[Yy]([Ee][Ss])?$ ]]; then
as_root systemctl reboot
else
echo "Not rebooting. Please reboot manually after confirming there were no errors."
fi
else
echo "No TTY available for reboot prompt. Please reboot manually after confirming there were no errors."
fi