#!/bin/bash

# omarchy:summary=Upgrade a legacy Omarchy install to the package-backed Omarchy quattro layout.
# omarchy:args=[--yes] [--reboot] [--dev] [--channel stable|rc|edge] [--user USER]
# omarchy:examples=omarchy upgrade to quattro | omarchy upgrade to quattro --dev
# omarchy:requires-sudo=true

set -euo pipefail

usage() {
  cat <<'USAGE'
Usage: omarchy-upgrade-to-quattro [--yes] [--reboot] [--dev] [--channel stable|rc|edge] [--user USER]

Upgrade any pre-quattro Omarchy install to the package-backed Omarchy quattro 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
  --dev                     Use dev packages [omarchy-dev / omarchy-settings-dev] (uses edge channel)
  --channel stable|rc|edge  Override the default stable 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
}

# The channel the legacy install is already on, read the way
# omarchy-version-channel reads it. Order matters: mirror.omarchy.org is a
# substring of both of the others.
detect_installed_channel() {
  [[ -f /etc/pacman.d/mirrorlist ]] || return 1

  if grep -q 'https://stable-mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
    echo stable
  elif grep -q 'https://rc-mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
    echo rc
  elif grep -q 'https://mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
    echo edge
  else
    return 1
  fi
}

channel_override="${OMARCHY_UPGRADE_CHANNEL:-}"
channel_override_cli=0
target_user="${OMARCHY_INSTALL_USER:-}"
yes=0
auto_reboot=0
boot_cmdline_unsafe=0
use_dev_packages=${OMARCHY_UPGRADE_DEV:-0}

while (($#)); do
  case "$1" in
    -y|--yes)
      yes=1
      shift
      ;;
    --reboot)
      auto_reboot=1
      shift
      ;;
    --dev)
      use_dev_packages=1
      shift
      ;;
    --channel)
      channel_override="${2:-}"
      channel_override_cli=1
      shift 2
      ;;
    --user)
      target_user="${2:-}"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      fail "Unknown option: $1"
      ;;
  esac
done

case "$use_dev_packages" in
  1|true|yes|on) use_dev_packages=1 ;;
  0|false|no|off|"") use_dev_packages=0 ;;
  *) fail "Invalid OMARCHY_UPGRADE_DEV value '$use_dev_packages'. Use 1/0, true/false, or pass --dev." ;;
esac

# Land on the Quattro equivalent of wherever the machine already is, unless the
# caller said otherwise. Stable machines get the production packages off the
# stable channel. rc and edge machines stay on their own mirror, but take the
# dev packages, because that is where Quattro lives until it ships.
if [[ -z $channel_override ]] && (( ! use_dev_packages )); then
  case "$(detect_installed_channel || true)" in
    stable) channel_override=stable ;;
    rc) channel_override=rc use_dev_packages=1 ;;
    edge) channel_override=edge use_dev_packages=1 ;;
  esac
fi

if (( use_dev_packages )); then
  # The dev packages are only published to the edge package repo, which the rc
  # and edge channels both point at; only stable is incompatible. rc keeps its
  # own Arch mirror either way.
  if [[ -n $channel_override ]]; then
    if ! valid_channel "$channel_override"; then
      fail "Invalid channel '$channel_override'. Use stable, rc, or edge."
    fi
    if [[ $(normalize_channel "$channel_override") == stable ]]; then
      fail "--dev needs the edge package repo; use --channel rc or --channel edge."
    fi
  else
    channel_override=edge
  fi
fi

if { [[ -n $channel_override ]] || (( channel_override_cli )); } && ! valid_channel "$channel_override"; then
  fail "Invalid channel '$channel_override'. Use stable, rc, or edge."
fi

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 \
    "$@"
}

# Run an Omarchy command as the user with the package-backed paths, without
# assuming a live session. Extra NAME=VALUE args extend the environment.
run_as_user_omarchy() {
  run_as_user env \
    HOME="$target_home" \
    USER="$target_user" \
    LOGNAME="$target_user" \
    OMARCHY_PATH=/usr/share/omarchy \
    PATH="$package_path" \
    "$@"
}

package_installed_exact() {
  local pkg="$1"
  pacman -Qq "$pkg" 2>/dev/null | grep -Fxq "$pkg"
}

mark_packages_explicit() {
  local pkg installed_pkg
  local installed_packages=()

  for pkg in "$@"; do
    # pacman -Qq resolves installed providers too (for example nvim -> neovim),
    # while package_installed_exact intentionally does not.
    installed_pkg=$(pacman -Qq "$pkg" 2>/dev/null | head -n1 || true)
    [[ -n $installed_pkg ]] && installed_packages+=("$installed_pkg")
  done

  ((${#installed_packages[@]})) || return 0
  mapfile -t installed_packages < <(printf '%s\n' "${installed_packages[@]}" | sort -u)

  if ! as_root pacman -D --asexplicit "${installed_packages[@]}" >/dev/null; then
    warn "Could not mark installed default packages as explicit; retired-package cleanup may leave extra dependencies installed."
  fi
}

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]}"
}

channel=$(normalize_channel "${channel_override:-stable}")

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/edge/$arch'
    ;;
  edge)
    mirror_server='https://mirror.omarchy.org/$repo/os/$arch'
    package_server='https://pkgs.omarchy.org/edge/$arch'
    ;;
esac

if (( ! yes )); then
  printf '\033[2J\033[3J\033[H' >/dev/tty
  cat <<EOF
████████▄   ███    █▄     ▄████████     ███         ███        ▄████████  ▄██████▄
███    ███  ███    ███   ███    ███ ▀█████████▄ ▀█████████▄   ███    ███ ███    ███
███    ███  ███    ███   ███    ███    ▀███▀▀██    ▀███▀▀██   ███    ███ ███    ███
███    ███  ███    ███   ███    ███     ███   ▀     ███   ▀  ▄███▄▄▄▄██▀ ███    ███
███    ███  ███    ███ ▀███████████     ███         ███     ▀▀███▀▀▀▀▀   ███    ███
███    ███  ███    ███   ███    ███     ███         ███     ▀███████████ ███    ███
███  ▀ ███  ███    ███   ███    ███     ███         ███       ███    ███ ███    ███
 ▀██████▀▄█ ████████▀    ███    █▀     ▄████▀      ▄████▀     ███    ███  ▀██████▀
                                                              ███    ███

Upgrading Omarchy to Quattro is a one-way street!
You cannot downgrade from Quattro.
Make sure you have a backup.

EOF

  gum confirm "Continue with upgrade?" </dev/tty || exit 0
fi

sudo_keepalive_pid=""
hyprland_config_reload_suppressed=0
upgrade_started=0
upgrade_completed=0

cleanup_on_exit() {
  local exit_status=$?

  if (( hyprland_config_reload_suppressed )); 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

  # set -e aborts without a word, and the last thing on screen is a green
  # progress line, so a half-finished upgrade otherwise reads as a finished one.
  if (( exit_status != 0 && upgrade_started && ! upgrade_completed )); then
    printf '\n\033[31mUpgrade incomplete - do NOT reboot.\033[0m\n' >&2
    printf 'The system is part Omarchy 3 and part Omarchy quattro; rebooting now can leave it without a working network or desktop.\n' >&2
    printf 'Fix the error reported above and run this script again. Re-running is safe and resumes the remaining steps.\n' >&2
  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-quattro.$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-quattro.$backup_suffix.bak"

  # Replace the normal Omarchy package repository. Dev package mode uses the
  # edge channel and resolves omarchy-dev / omarchy-settings-dev from [omarchy].
  local tmp

  tmp=$(mktemp)
  awk -v server="$package_server" '
    BEGIN { in_omarchy = 0; wrote = 0 }
    /^[[:space:]]*\[omarchy\][[:space:]]*$/ {
      if (!wrote) {
        print "[omarchy]"
        print "SigLevel = Optional TrustAll"
        print "Server = " server
        wrote = 1
      }
      in_omarchy = 1
      next
    }
    /^[[:space:]]*\[/ { 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"

  # Pre-quattro installs cached omarchy packages downloaded from the legacy
  # repo. The quattro package server rebuilds some of them under the same
  # name and version with different bytes, so the cached copies fail the new
  # database's checksum and abort --noconfirm transactions as "corrupted".
  as_root find /var/cache/pacman/pkg -maxdepth 1 -name 'omarchy-*' -delete 2>/dev/null || true

  # -Syy forces the database download. configure_pacman_channel just repointed
  # the mirrors, and a plain -Sy keeps the legacy database whenever the new
  # server's copy isn't newer, leaving stale checksums that no amount of
  # re-downloading can satisfy.

  as_root pacman -Syy --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 -Syy --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-quattro omarchy meta package may depend on omarchy-installer. Ignore
    # that edge temporarily; the next transaction installs the Omarchy quattro meta.
    as_root pacman -Rdd --noconfirm omarchy-installer
  fi
}

remove_legacy_limine_configs() {
  # Mirrors the old 1762446739 migration: old Limine installs sometimes left
  # alternate limine.conf files in paths Limine searches before /boot/limine.conf.
  # If any are still present, Limine may ignore the package-managed default config.
  local canonical_limine_config=/boot/limine.conf
  local legacy_limine_configs=(
    /boot/EFI/arch-limine/limine.conf
    /boot/EFI/limine/limine.conf
    /boot/EFI/BOOT/limine.conf
    /boot/limine/limine.conf
  )
  local legacy_limine_config
  local found_legacy_limine_configs=()

  for legacy_limine_config in "${legacy_limine_configs[@]}"; do
    if as_root test -f "$legacy_limine_config"; then
      found_legacy_limine_configs+=("$legacy_limine_config")
    fi
  done

  ((${#found_legacy_limine_configs[@]})) || return 0

  if ! as_root test -f "$canonical_limine_config"; then
    fail "Legacy Limine configs exist (${found_legacy_limine_configs[*]}) but $canonical_limine_config does not. Do not reboot until the bootloader config is repaired."
  fi

  log "Removing legacy alternate Limine configs"
  as_root rm -f "${found_legacy_limine_configs[@]}"
}

normalize_limine_config() {
  local machine_id legacy_uki

  as_root test -f /boot/limine.conf || return 0

  log "Normalizing Limine config"
  # Mirrors old 1777570652: normalize British/American spelling, the old color
  # index, and help colors that pre-quattro installs may never have migrated.
  as_root sed -i -E 's/^interface_branding_colou?r: 2$/interface_branding_color: 9ece6a/' /boot/limine.conf
  as_root sed -i 's/^interface_branding_colour: /interface_branding_color: /' /boot/limine.conf
  as_root sed -i -E '/^interface_help_colou?r(_bright)?:/d' /boot/limine.conf
  as_root sed -i '/^interface_branding_color:/a interface_help_color_bright: 9ece6a' /boot/limine.conf
  as_root sed -i '/^interface_branding_color:/a interface_help_color: 9ece6a' /boot/limine.conf

  # Pre-quattro custom-UKI installs used <machine-id>_linux.efi. Omarchy quattro uses the
  # stable omarchy_linux.efi path generated by limine-entry-tool.
  machine_id=$(as_root cat /etc/machine-id 2>/dev/null || true)
  legacy_uki="/boot/EFI/Linux/${machine_id}_linux.efi"
  if [[ -n $machine_id ]] && as_root test -f /boot/EFI/Linux/omarchy_linux.efi && as_root test -f "$legacy_uki"; then
    as_root rm -f "$legacy_uki"
  fi
}

preserve_kernel_cmdline_root() {
  local default_conf=/etc/default/limine
  local cmdline param key root_source root_stack root_uuid subvol uki
  local boot_params=()
  local missing=()
  local have_root=0 have_mount_mode=0 have_unlock=0

  command -v limine-mkinitcpio >/dev/null 2>&1 || return 0
  as_root test -f /boot/limine.conf || return 0

  # The omarchy-defaults.conf drop-in appends to KERNEL_CMDLINE[default] with
  # +=, which makes limine-entry-tool ignore /etc/kernel/cmdline and
  # /proc/cmdline. Fresh installs pin root= in /etc/default/limine via the ISO;
  # upgrades never created that file, so root= silently drops out and the next
  # boot lands in an emergency shell. Ask the tool for its effective default
  # cmdline — the key this function appends to — rather than parsing the config
  # layers ourselves.
  if as_root limine-entry-tool --get-cmdline default 2>/dev/null |
    grep -qE '(^|[[:space:]])root='; then
    return 0
  fi

  # Copy the boot-critical parameters of the running kernel verbatim, so a
  # PARTUUID, a LABEL or a bare device node all survive.
  cmdline=$(cat /proc/cmdline 2>/dev/null || true)
  for param in $cmdline; do
    key=${param%%=*}
    case $key in
      root)
        have_root=1
        boot_params+=("$param")
        ;;
      rw | ro)
        have_mount_mode=1
        boot_params+=("$param")
        ;;
      cryptdevice | cryptkey | rd.luks.name | rd.luks.uuid | rd.luks.options | rd.luks.key | rd.luks.crypttab | dm-mod.create)
        have_unlock=1
        boot_params+=("$param")
        ;;
      rootflags | rootfstype | rootwait | rootdelay | resume | resume_offset | rd.lvm.lv | rd.lvm.vg | rd.md.uuid | rd.dm.uuid)
        boot_params+=("$param")
        ;;
    esac
  done

  # Already-broken system (booted through the emergency shell): derive root=
  # from the mounted root instead. Refuse when the root sits on dm-crypt and
  # the cmdline carries no unlock parameters, since root= alone would still not
  # boot. lsblk -s walks the parents, where the crypt layer hides on
  # LVM-on-LUKS; --nofsroot keeps findmnt from appending a btrfs subvolume
  # lsblk cannot resolve; the capture keeps a SIGPIPE under pipefail from
  # reading as "no crypt layer".
  if ((!have_root)); then
    root_source=$(findmnt -no SOURCE --nofsroot / 2>/dev/null || true)
    root_stack=$(lsblk -nso TYPE "$root_source" 2>/dev/null || true)
    if grep -qx crypt <<<"$root_stack" && ((!have_unlock)); then
      warn "The root filesystem is on dm-crypt and the booted kernel cmdline carries no unlock parameters, so they cannot be recovered automatically. Add root= and the matching cryptdevice or rd.luks.* parameters to $default_conf by hand, then run limine-mkinitcpio."
      boot_cmdline_unsafe=1
      return 0
    fi

    root_uuid=$(findmnt -no UUID / 2>/dev/null || true)
    if [[ -z $root_uuid ]]; then
      warn "Could not determine the root filesystem UUID; the kernel cmdline was left untouched."
      boot_cmdline_unsafe=1
      return 0
    fi

    boot_params=("root=UUID=$root_uuid" "${boot_params[@]}")
    ((have_mount_mode)) || boot_params+=(rw)

    # Btrfs needs its subvolume too, or the initramfs mounts the wrong tree.
    subvol=$(findmnt -no FSROOT / 2>/dev/null || true)
    if [[ $(findmnt -no FSTYPE / 2>/dev/null) == "btrfs" && -n $subvol && $subvol != "/" ]]; then
      boot_params+=("rootflags=subvol=${subvol#/}")
    fi
  fi

  log "Preserving the kernel cmdline root parameters in $default_conf"
  as_root tee -a "$default_conf" >/dev/null <<EOF
# Written by omarchy-upgrade-to-quattro. The += drop-ins in
# /etc/limine-entry-tool.d/ stop limine-entry-tool from reading
# /etc/kernel/cmdline and /proc/cmdline, so the parameters identifying the
# root filesystem have to be stated explicitly.
KERNEL_CMDLINE[default]+=" ${boot_params[*]}"
EOF

  # Keep going on failure so the verification below still runs; the unsafe
  # flag blocks the reboot at the end of the upgrade.
  if ! as_root limine-mkinitcpio; then
    warn "limine-mkinitcpio failed; the boot entries were not regenerated."
    boot_cmdline_unsafe=1
  fi

  as_root grep -qE '(^|[[:space:]])root=' /boot/limine.conf || missing+=(/boot/limine.conf)

  # The cmdline that boots is the one embedded in the UKIs, so check those too.
  if command -v objcopy >/dev/null 2>&1; then
    while read -r uki; do
      [[ -n $uki ]] || continue
      as_root objcopy -O binary --only-section=.cmdline "$uki" /dev/stdout 2>/dev/null |
        tr -d '\0' | grep -E '(^|[[:space:]])root=' >/dev/null || missing+=("$uki")
    done < <(as_root find /boot/EFI/Linux -maxdepth 1 -name 'omarchy_linux*.efi' 2>/dev/null)
  fi

  if ((${#missing[@]})); then
    warn "root= is still missing from ${missing[*]}. Do not reboot until the kernel cmdline is repaired, or the system will drop to an emergency shell."
    boot_cmdline_unsafe=1
  fi
}

configure_snapper_policy() {
  local snapper_config_script=/usr/share/omarchy/install/config/snapper.sh

  if ! as_root test -f "$snapper_config_script"; then
    warn "$snapper_config_script is unavailable; Snapper snapshot retention was not normalized."
    return 0
  fi

  log "Configuring Omarchy snapshot retention"
  as_root env OMARCHY_PATH=/usr/share/omarchy bash -euo pipefail "$snapper_config_script"
}

configure_lock_authentication() {
  # The upgrade runs against whatever the channel currently ships, which can
  # still predate the rename of omarchy-setup-lock to omarchy-apply-lock.
  local apply_lock="" candidate
  for candidate in /usr/share/omarchy/bin/omarchy-{apply,setup}-lock; do
    if [[ -x $candidate ]]; then
      apply_lock=$candidate
      break
    fi
  done

  [[ -n $apply_lock ]] || fail "Neither omarchy-apply-lock nor omarchy-setup-lock is available under /usr/share/omarchy/bin; lock screen authentication could not be configured."

  log "Configuring lock screen authentication"
  as_root env \
    OMARCHY_INSTALL_USER="$target_user" \
    OMARCHY_PATH=/usr/share/omarchy \
    PATH="$package_path" \
    "$apply_lock"
}

create_pre_upgrade_snapshot() {
  # 127 means Snapper is deliberately absent. Any other failure already said
  # what went wrong, and a missing snapshot is not worth blocking the upgrade
  # over, but it must not pass for one either.
  omarchy-snapshot create || (($? == 127)) ||
    echo -e "\e[33mContinuing the upgrade without a snapshot.\e[0m" >&2
}

remove_conflicting_legacy_packages() {
  # Remove legacy packages that are known to conflict with Omarchy quattro 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 package_installed_exact "$pkg"; then
      # Some pre-quattro meta packages depend on these retired/default packages. We
      # intentionally break those old dependency edges before installing the
      # Omarchy quattro 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
}

migrate_1password_beta_package() {
  if ! package_installed_exact 1password-beta && ! package_installed_exact 1password-beta-bin; then
    return 0
  fi

  log "Replacing 1Password Beta with stable 1Password"
  if ! as_root pacman -S --noconfirm --ask 4 1password; then
    warn "Could not replace 1Password Beta with stable 1Password; leaving the existing 1Password package installed."
  fi
}

install_omarchy_quattro_packages() {
  local omarchy_package=omarchy
  local settings_package=omarchy-settings
  if (( use_dev_packages )); then
    omarchy_package=omarchy-dev
    settings_package=omarchy-settings-dev
  fi

  local core_packages=(
    omarchy-keyring
    "$settings_package"
    omarchy-nvim
    "$omarchy_package"
    fakeroot
    pacman-contrib
  )
  local base_packages=()

  log "Upgrading system packages and installing Omarchy quattro packages"
  (( use_dev_packages )) && log "Using dev packages [omarchy-dev / omarchy-settings-dev]"
  # --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 env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --needed --noconfirm --ask 4 --overwrite='*' "${core_packages[@]}"
  mark_packages_explicit "${core_packages[@]}"

  if [[ -f /usr/share/omarchy/install/omarchy-base.packages ]]; then
    log "Installing Omarchy quattro 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[@]}"
      mark_packages_explicit "${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[@]}"
  mark_packages_explicit "${audio_packages[@]}"
}

install_hardware_transition_packages() {
  local hardware_packages=()

  if lspci | grep -iE '(Multimedia audio controller|Audio device).*Intel' >/dev/null; then
    hardware_packages+=(sof-firmware)
  fi

  if lspci | grep -iE '(VGA|Display).*Intel' >/dev/null; then
    hardware_packages+=(vulkan-intel)
  fi
  if lspci | grep -iE '(VGA|Display).*AMD' >/dev/null; then
    hardware_packages+=(vulkan-radeon)
  fi
  if lspci | grep -iE '(VGA|Display).*Apple' >/dev/null; then
    hardware_packages+=(vulkan-asahi)
  fi

  if (( ${#hardware_packages[@]} > 0 )); then
    log "Installing hardware support needed by the Omarchy quattro layout"
    as_root pacman -S --needed --noconfirm "${hardware_packages[@]}"
    mark_packages_explicit "${hardware_packages[@]}"
  fi
}

remove_retired_default_packages() {
  local retired_packages=(
    asdcontrol-git
    blueberry
    bluetui
    chaotic-keyring
    chaotic-mirrorlist
    claude-code
    dust
    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
    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
    satty
    swaybg
    swayosd
    walker-bin
    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 quattro default install"
  local pkg
  local installed_retired=()
  for pkg in "${retired_packages[@]}"; do
    if package_installed_exact "$pkg"; then
      installed_retired+=("$pkg")
    fi
  done

  ((${#installed_retired[@]})) || return 0

  # Remove as one transaction so dependency anchors like waybar/playerctl and
  # omarchy-walker/elephant-* disappear together instead of failing one-by-one.
  # Preflight first so dependency failures do not print scary pacman errors in
  # the middle of an otherwise successful upgrade.
  if pacman -Rs --print "${installed_retired[@]}" >/dev/null 2>&1; then
    if as_root pacman -Rns --noconfirm --ask 4 "${installed_retired[@]}"; then
      return 0
    fi
    warn "Batch retired-package removal failed; retrying dependency-aware groups."
  else
    warn "Some retired packages are still required by non-retired packages; retrying dependency-aware groups."
  fi

  local group group_pkg required_by_line
  local group_packages=()
  local installed_group=()
  local fallback_groups=(
    "omarchy-walker walker elephant-all elephant elephant-bluetooth elephant-calc elephant-clipboard elephant-desktopapplications elephant-files elephant-menus elephant-providerlist elephant-runner elephant-symbols elephant-todo elephant-unicode elephant-websearch"
    "waybar playerctl"
    "impala iwd"
    "blueberry bluetui"
    "wiremix pavucontrol"
  )

  for group in "${fallback_groups[@]}"; do
    read -r -a group_packages <<<"$group"
    installed_group=()
    for group_pkg in "${group_packages[@]}"; do
      if package_installed_exact "$group_pkg"; then
        installed_group+=("$group_pkg")
      fi
    done
    ((${#installed_group[@]})) || continue

    if pacman -Rs --print "${installed_group[@]}" >/dev/null 2>&1; then
      as_root pacman -Rns --noconfirm --ask 4 "${installed_group[@]}" || warn "Could not remove retired package group: ${installed_group[*]}"
    else
      warn "Leaving retired package group installed because other packages still depend on it: ${installed_group[*]}"
    fi
  done

  for pkg in "${installed_retired[@]}"; do
    package_installed_exact "$pkg" || continue

    if pacman -Rs --print "$pkg" >/dev/null 2>&1; then
      as_root pacman -Rns --noconfirm --ask 4 "$pkg" || warn "Could not remove retired package '$pkg'; leaving it installed."
    else
      required_by_line=$(LC_ALL=C pacman -Qi "$pkg" 2>/dev/null | awk -F: '/^Required By/ { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit }')
      if [[ -n $required_by_line && $required_by_line != None ]]; then
        warn "Leaving retired package '$pkg' installed because it is still required by: $required_by_line"
      else
        warn "Leaving retired package '$pkg' installed because pacman reports remaining dependency constraints."
      fi
    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

legacy_hypr_config_active() {
  local hyprland_conf="$TARGET_HOME/.config/hypr/hyprland.conf"
  [[ -f $hyprland_conf ]] || return 1
  grep -q '\.local/share/omarchy/default/hypr' "$hyprland_conf"
}

populate_legacy_hypr_defaults() {
  local shim_hypr_dir="$1" backup="$2" tmp_dir archive_dir

  rm -rf "$shim_hypr_dir"
  mkdir -p "$shim_hypr_dir"

  # The backup is the checkout the running session is sourcing right now, so it
  # is both the correct content and already on disk. Only reach for the network
  # when there is no backup, which means the legacy root was already a symlink.
  if [[ -n $backup && -d $backup/default/hypr ]]; then
    cp -a "$backup/default/hypr/." "$shim_hypr_dir/"
  elif command -v curl >/dev/null 2>&1 && command -v tar >/dev/null 2>&1; then
    tmp_dir=$(mktemp -d)
    archive_dir="$tmp_dir/archive"
    mkdir -p "$archive_dir"
    if curl -fsSL https://github.com/basecamp/omarchy/archive/refs/heads/master.tar.gz | tar -xz -C "$archive_dir" &&
      [[ -d $archive_dir/omarchy-master/default/hypr ]]; then
      cp -a "$archive_dir/omarchy-master/default/hypr/." "$shim_hypr_dir/"
    fi
    rm -rf "$tmp_dir"
  fi

  find "$shim_hypr_dir" -type f -name '*.conf' -print -quit | grep -q .
}

create_live_omarchy_overlay() {
  local backup="$1" compat_root shim_hypr_dir entry base cleanup_hook

  compat_root="$TARGET_HOME/.local/state/omarchy/upgrade-to-quattro-live-omarchy"
  shim_hypr_dir="$TARGET_HOME/.local/state/omarchy/upgrade-to-quattro-hypr-defaults"

  populate_legacy_hypr_defaults "$shim_hypr_dir" "$backup" || return 1

  rm -rf "$compat_root"
  mkdir -p "$compat_root/default"

  shopt -s dotglob nullglob
  for entry in /usr/share/omarchy/*; do
    [[ -e $entry ]] || continue
    base=$(basename "$entry")
    [[ $base == default ]] && continue
    ln -s "$entry" "$compat_root/$base"
  done
  for entry in /usr/share/omarchy/default/*; do
    [[ -e $entry ]] || continue
    base=$(basename "$entry")
    [[ $base == hypr ]] && continue
    ln -s "$entry" "$compat_root/default/$base"
  done
  ln -s "$shim_hypr_dir" "$compat_root/default/hypr"

  cleanup_hook="$TARGET_HOME/.config/omarchy/hooks/post-boot.d/cleanup-upgrade-to-quattro-live-shim"
  mkdir -p "$(dirname "$cleanup_hook")"
  cat >"$cleanup_hook" <<CLEANUP_HOOK
#!/bin/bash
set -euo pipefail
legacy_root="\$HOME/.local/share/omarchy"
compat_root="$compat_root"
shim_hypr_dir="$shim_hypr_dir"
if [[ -L \$legacy_root && \$(readlink "\$legacy_root") == "\$compat_root" ]]; then
  ln -sfn /usr/share/omarchy "\$legacy_root"
fi
rm -rf "\$compat_root" "\$shim_hypr_dir"
rm -f "\$0"
CLEANUP_HOOK
  chmod +x "$cleanup_hook"

  printf '%s\n' "$compat_root"
}

# 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 the live Hyprland session is
# still using legacy ~/.config/hypr/*.conf files, keep those user files in place
# and provide a temporary default/hypr/*.conf shim so reloads do not explode.
if [[ -e /usr/share/omarchy ]]; then
  mkdir -p "$TARGET_HOME/.local/share"
  backup=""
  if [[ -L $legacy_root ]]; then
    rm -f "$legacy_root"
  elif [[ -e $legacy_root ]]; then
    backup="$legacy_root.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
    mv "$legacy_root" "$backup"
  fi

  if legacy_hypr_config_active; then
    if compat_root=$(create_live_omarchy_overlay "$backup"); then
      ln -sfn "$compat_root" "$legacy_root"
    else
      echo "Warning: Could not prepare legacy Hyprland conf shim; linking ~/.local/share/omarchy directly to /usr/share/omarchy." >&2
      ln -sfn /usr/share/omarchy "$legacy_root"
    fi
  else
    ln -sfn /usr/share/omarchy "$legacy_root"
  fi
fi
USER_CLEANUP
}

retired_user_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
)

cleanup_retired_user_unit_files() {
  run_as_user env RETIRED_UNITS="${retired_user_units[*]}" bash <<'USER_UNITS'
set -euo pipefail
read -r -a units <<<"$RETIRED_UNITS"
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 quattro"

  # iwd is retired alongside the NetworkManager enable it depends on. Do not
  # stop systemd-networkd mid-upgrade; disabling is enough and avoids dropping a
  # running connection before the user can reboot into NetworkManager.
  local networkd_unit
  for networkd_unit in \
    systemd-networkd.service \
    systemd-networkd.socket \
    systemd-networkd-varlink.socket \
    systemd-networkd-varlink-metrics.socket \
    systemd-networkd-resolve-hook.socket; do
    as_root systemctl disable "$networkd_unit" >/dev/null 2>&1 || true
  done
  as_root systemctl disable systemd-networkd-wait-online.service >/dev/null 2>&1 || true
  as_root systemctl mask systemd-networkd-wait-online.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 "${retired_user_units[@]}" >/dev/null 2>&1 || true
    run_as_user_session systemctl --user daemon-reload >/dev/null 2>&1 || true
  fi
}

# The Omarchy 3 session keeps its bar, launcher, and notifications until the
# reboot, which is the real cutover. Swapping the UI out underneath a running
# session buys nothing and only creates ways for the upgrade to look broken.

# Run a command inside the user's live Wayland session. Fails if no session
# can be discovered. Extra NAME=VALUE args extend the environment.
run_as_user_wayland_session() {
  local wayland_display hyprland_signature
  [[ -d $target_runtime_dir ]] || return 1

  wayland_display=$(discover_wayland_display) || return 1
  hyprland_signature=$(discover_hyprland_signature) || return 1

  run_as_user_session \
    WAYLAND_DISPLAY="$wayland_display" \
    HYPRLAND_INSTANCE_SIGNATURE="$hyprland_signature" \
    XDG_SESSION_TYPE=wayland \
    OMARCHY_PATH=/usr/share/omarchy \
    PATH="$package_path" \
    "$@"
}

run_hyprctl_session() {
  local hyprctl_bin
  hyprctl_bin=/usr/bin/hyprctl
  [[ -x $hyprctl_bin ]] || hyprctl_bin=$(command -v hyprctl || true)
  [[ -n $hyprctl_bin ]] || return 1

  run_as_user_wayland_session "$hyprctl_bin" "$@"
}

suppress_hyprland_config_reload() {
  # Say so up front when the session cannot be reached at all. Otherwise this
  # silently does nothing and the only evidence is the config error bar that
  # shows up minutes later.
  if ! run_as_user_wayland_session true >/dev/null 2>&1; then
    warn "Could not reach the live Hyprland session; it will report config errors during the swap, and the Omarchy shell will start after reboot."
    return 0
  fi

  log "Suppressing live Hyprland reloads during the config swap"
  set_hyprland_reload_state true
  hyprland_config_reload_suppressed=1
}

# Which API answers depends on the parser the running Hyprland was started
# with: the legacy .conf parser only takes keyword, and Quattro's Lua parser
# rejects it outright with "keyword can't work with non-legacy parsers". The
# session being upgraded is the legacy one, but the hyprctl binary is whichever
# the package transaction has installed by then, so ask both ways.
set_hyprland_reload_state() {
  local value="$1"

  run_hyprctl_session keyword misc:disable_autoreload "$value" >/dev/null 2>&1 || true
  run_hyprctl_session keyword debug:suppress_errors "$value" >/dev/null 2>&1 || true
  run_hyprctl_session eval \
    "hl.config({ misc = { disable_autoreload = $value }, debug = { suppress_errors = $value } })" \
    >/dev/null 2>&1 || true
}

restore_hyprland_config_reload() {
  (( hyprland_config_reload_suppressed )) || return 0

  set_hyprland_reload_state false

  # Backstop. An explicit hyprctl reload re-reads the config from disk, which
  # resets both keywords above before it reports what it found, so suppression
  # cannot survive one, and Hyprland keeps the resulting error bar up until a
  # later clean reload that this script deliberately never performs. Callers
  # that reload during the swap are supposed to check
  # OMARCHY_UPGRADE_TO_QUATTRO_LIVE, but one slipping through should not leave
  # an error bar on a finished upgrade. By now the shims have made the legacy
  # config resolve again, so clearing the overlay is safe.
  run_hyprctl_session seterror disable >/dev/null 2>&1 || true

  hyprland_config_reload_suppressed=0
}

ensure_sleep_lock_service() {
  [[ -S $target_runtime_dir/bus ]] || return 0

  log "Ensuring Omarchy sleep lock service is enabled"
  run_as_user_session systemctl --user daemon-reload >/dev/null 2>&1 || true
  run_as_user_session systemctl --user reset-failed omarchy-sleep-lock.service >/dev/null 2>&1 || true
  if ! run_as_user_session systemctl --user enable --now omarchy-sleep-lock.service >/dev/null 2>&1; then
    warn "Could not start omarchy-sleep-lock.service; it should start after reboot."
  fi
}

run_post_upgrade_migrations() {
  PATH="$package_path" command -v omarchy-migrate >/dev/null 2>&1 || return 0

  log "Running Omarchy migrations"
  if ! run_as_user_omarchy OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 omarchy-migrate; then
    warn "Could not run Omarchy migrations; the user may be prompted to run them after login."
  fi
}

run_final_system_package_upgrade() {
  log "Checking for remaining system package updates"
  as_root env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm --ask 4 \
    --overwrite '/etc/docker/daemon.json' \
    --overwrite '/etc/gnupg/dirmngr.conf' \
    --overwrite '/etc/mkinitcpio.conf.d/omarchy_hooks.conf' \
    --overwrite '/etc/mkinitcpio.conf.d/thunderbolt_module.conf' \
    --overwrite '/etc/modprobe.d/omarchy-usb-autosuspend.conf' \
    --overwrite '/etc/sddm.conf.d/10-theme.conf' \
    --overwrite '/etc/sddm.conf.d/10-wayland.conf' \
    --overwrite '/etc/sudoers.d/omarchy-asdcontrol' \
    --overwrite '/etc/sudoers.d/omarchy-passwd-tries' \
    --overwrite '/etc/sudoers.d/omarchy-tzupdate' \
    --overwrite '/etc/sysctl.d/90-omarchy-file-watchers.conf' \
    --overwrite '/etc/sysctl.d/99-omarchy-sysctl.conf' \
    --overwrite '/etc/systemd/logind.conf.d/10-ignore-power-button.conf' \
    --overwrite '/etc/systemd/resolved.conf.d/10-disable-multicast.conf' \
    --overwrite '/etc/systemd/resolved.conf.d/20-docker-dns.conf' \
    --overwrite '/etc/systemd/system.conf.d/10-faster-shutdown.conf' \
    --overwrite '/etc/systemd/system/user@.service.d/10-faster-shutdown.conf' \
    --overwrite '/etc/systemd/system/docker.service.d/no-block-boot.conf' \
    --overwrite '/etc/systemd/system/plocate-updatedb.service.d/ac-only.conf' \
    --overwrite '/etc/systemd/system.conf.d/20-omarchy-nofile.conf' \
    --overwrite '/etc/systemd/user.conf.d/20-omarchy-nofile.conf' \
    --overwrite '/usr/lib/systemd/system-sleep/unmount-fuse' \
    --overwrite '/usr/share/plymouth/themes/omarchy/*' \
    --overwrite '/usr/share/sddm/hyprland.lua' \
    --overwrite '/usr/share/sddm/themes/omarchy/*'
}

run_post_upgrade_update_steps() {
  local update_output update_status

  if PATH="$package_path" command -v omarchy-update-aur-pkgs >/dev/null 2>&1; then
    if ! run_as_user_omarchy omarchy-update-aur-pkgs; then
      warn "Could not update AUR packages; running omarchy-update after reboot may still update AUR packages."
    fi
  fi

  if PATH="$package_path" command -v omarchy-update-mise >/dev/null 2>&1; then
    if ! run_as_user_omarchy omarchy-update-mise; then
      warn "Could not update mise tools; running omarchy-update after reboot may still update mise-managed tools."
    fi
  fi

  if PATH="$package_path" command -v omarchy-update-available >/dev/null 2>&1; then
    log "Refreshing update indicator state"
    update_status=0
    update_output=$(run_as_user_omarchy omarchy-update-available 2>&1) || update_status=$?

    if (( update_status == 0 )); then
      warn "Updates are still available after the upgrade; run omarchy-update after reboot:"
      printf '%s\n' "$update_output" >&2
    fi
  fi
}

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() {
  # Run the packaged config script rather than a copy of it, the same way
  # configure_snapper_policy and configure_lock_authentication already do. The
  # copy this replaces had drifted: it never installed the ufw-docker rules, so
  # upgraded machines came up without the Docker firewall protections a fresh
  # install gets.
  local firewall_script=/usr/share/omarchy/install/config/firewall.sh

  if ! command -v ufw >/dev/null 2>&1; then
    warn "ufw is not installed; skipping firewall defaults."
    return 0
  fi

  if ! as_root test -f "$firewall_script"; then
    warn "$firewall_script is unavailable; firewall defaults were not applied."
    return 0
  fi

  log "Applying Omarchy firewall defaults"
  as_root env OMARCHY_PATH=/usr/share/omarchy PATH="$package_path" \
    bash -euo pipefail "$firewall_script" ||
    warn "Could not apply firewall defaults; run 'sudo bash $firewall_script' after reboot."
}

root_filesystem_encrypted() {
  local root_source root_type

  root_source=$(findmnt -n -o SOURCE / 2>/dev/null || true)
  [[ $root_source == /dev/mapper/* ]] && return 0

  if [[ -n $root_source ]]; then
    root_type=$(lsblk -no TYPE "$root_source" 2>/dev/null | head -n1 || true)
    [[ $root_type == crypt ]] && return 0
  fi

  as_root bash -c '[[ -s /etc/crypttab ]] && grep -qvE "^[[:space:]]*(#|$)" /etc/crypttab' >/dev/null 2>&1
}

should_enable_sddm_autologin() {
  as_root test -f /etc/sddm.conf.d/autologin.conf && return 0
  as_root systemctl is-enabled omarchy-seamless-login.service >/dev/null 2>&1 && return 0
  root_filesystem_encrypted
}

apply_system_transition() {
  log "Applying Omarchy quattro 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

  # Deliberately do NOT add the user to the docker group. That group is
  # root-equivalent, so it is opt-in now (Setup > Security > Sudoless Docker);
  # re-granting it on upgrade would silently undo that default for everyone.

  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
  as_root ln -sfn ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

  # Fresh Omarchy socket-activates Docker. Disable the old always-on service if
  # an upgraded install had enabled it, but do not stop running containers mid-upgrade.
  as_root systemctl disable docker.service >/dev/null 2>&1 || true

  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
  # NetworkManager replaces iwd, and both halves of that swap belong in one
  # step: a machine that boots with both enabled has two daemons fighting over
  # the Wi-Fi adapter. iwd is disabled but never stopped, so the running
  # connection survives until the reboot.
  enable_system_service NetworkManager.service
  as_root systemctl disable iwd.service >/dev/null 2>&1 || true
  enable_system_service bluetooth.service
  enable_system_service power-profiles-daemon.service
  enable_system_service sddm.service

  apply_firewall_defaults

  log "Normalizing SDDM login configuration"
  # The omarchy package already installed 10-theme.conf and 10-wayland.conf.
  as_root install -d -m 0755 /etc/sddm.conf.d
  cat <<'EOF' | as_root tee /etc/sddm.conf.d/99-omarchy-login.conf >/dev/null
[Users]
RememberLastUser=true
RememberLastSession=true
EOF

  local autologin_user
  if should_enable_sddm_autologin; then
    if as_root test -f /etc/sddm.conf.d/autologin.conf; then
      autologin_user=$(as_root awk -F= '/^User=/ { print $2; exit }' /etc/sddm.conf.d/autologin.conf 2>/dev/null || true)
    fi
    [[ -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
  else
    as_root rm -f /etc/sddm.conf.d/autologin.conf
  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 rm -f \
    /etc/systemd/system.conf.d/99-omarchy-nofile.conf \
    /etc/systemd/user.conf.d/99-omarchy-nofile.conf
  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 quattro 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_UPGRADE_YES="$yes" \
    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"

file_sha256() {
  sha256sum "$1" | awk '{print $1}'
}

known_config_default_hashes() {
  cat <<'DEFAULT_HASHES'
# action<TAB>relative ~/.config path<TAB>sha256
# refresh: hash-matched shipped defaults
refresh	alacritty/alacritty.toml	d43ab27e10ba1fd0505d50fe374b9bd02fa2e1d888fe2252c5208542fe5ddc8f
refresh	alacritty/alacritty.toml	4e627909552ceebc80f5839955a09a7f804c501fee9a1fe76e13d95ad6cf5030
refresh	alacritty/alacritty.toml	73dc34be744039613ef522aab84c27468b61ff13d7bc0ed2d57a83904e13f613
refresh	alacritty/alacritty.toml	e25fae44db040d97714cf4ca3d77e48f0ae32fed9e02e6b3db9ad4d705bdaed5
refresh	alacritty/alacritty.toml	999562ef35642910bec05c2444c3acdcd6e29bc60ec541529d9c69a20cad5c88
refresh	alacritty/alacritty.toml	8bf6dee79e634949617cd7acdd2756da52f7c3031b8eac98a7948f2fef0cf07e
refresh	alacritty/alacritty.toml	e4d201b6b9afd2f591d64f98eb50eb19247b8b839c2eaacfe2623628203c486f
refresh	autostart/org.fcitx.Fcitx5.desktop	a0260e7f35579a73f77e257e9e3795abd8b33119a17b931ca6016a07036e1670
refresh	btop/btop.conf	e6b86ddda073e06bf64cfbbde9652f9335171d8ca8c622e617478a3c696a6a3d
refresh	btop/btop.conf	7f4122f2ed5f4c95489950cce2c0b192e19ae6fa74b0666d25e0b10040b2cee8
refresh	btop/btop.conf	4e17dd28c43948af48a408ed9b0901c008e57dfe3b8fcf09649ec10f28746137
refresh	chromium/Default/Preferences	67aef8ae3a6e0ee1180057198f47f2ea412ea1f01051d77edce4c465fb83d887
refresh	chromium-flags.conf	3a8fe5b85bc42b27f5a0f36f3b674dc8e22a7d670a6aabbdc4f2d4057b97a1d4
refresh	chromium-flags.conf	3d52925a48ac3704f9427ecec70310d9c89be5442c41ece183122a9246837be4
refresh	chromium-flags.conf	4bc8710615d2151464e9bd6d1123fbfbb8f56dea4cf0568320bc7b50d24ad205
refresh	chromium-flags.conf	1df1a5282f9525bfbb97a697c8169c08c1adb2b5a4de5ae94d84df7b4a78f2fc
refresh	chromium-flags.conf	0408c524892830f5cf43e94c389b897cbc093ede312f49a4627bfabf692653c2

refresh	fcitx5/conf/clipboard.conf	2d53b40811ee7ceb71c7db0422a3ed733647f389379a57dbf2915d522dff4193
refresh	fcitx5/conf/xcb.conf	f5cf059182c4361fa7b16d5149f20a09eb1e0915f4463ba60dae626b86df7d9c
refresh	foot/foot.ini	52e30647172522459b2179af714c19b62a35fe11bc23c49839fda5dca3253e84
refresh	foot/foot.ini	ac16453a4b4ebc997ff6732e7e4448dc759c25587fdb8aa1d1a7eee92b518987
refresh	ghostty/config	ed589d69296f5aa083e4dba43fc3d40cfbe2f6d10b382f3d0dc2ff76d5ec8fe5
refresh	ghostty/config	2832c9c27cdcd735e913d538c3c61005fd90e909541bdcb746bb454851ae79ca
refresh	ghostty/config	8200bc34b7ced8170d512c619f0408ac7dd7d7b4e64021ac66393d8a8bf1d951
refresh	ghostty/config	006fbe4048de02fc040eeefd0974081c6f7df5dae419f8d542fc1adf08b0d6fb
refresh	ghostty/config	e55a409a490d4f80c19c9242a59db8e00e271e01feddb8fda6a5a7102d39af20
refresh	ghostty/config	b65e4903813d442a25540eef5a24313121c640a57114ee3d76d1ec7408d86b27
refresh	ghostty/config	4e6ccba3d0584832cedf22713842f8ce7eca383c2ea3bfe61286d2b8df73f3fe
refresh	git/config	3e1bdf77aa65b7bb412cc25c02bac4734e98c10e4a63815b98737bf817b6f682
refresh	hypr/hyprsunset.conf	3ac2e9fe716fb8c612f07ca608ba7eee491921c2386118aae54ff9c8121726f2
refresh	hypr/hyprsunset.conf	e6fa7d909a90b64df7f75e8842bc2541e75463a47bbc7213ec79a56364f563f2
refresh	hypr/hyprsunset.conf	afe595d9d31574bde0f3ab8f7d370d0710bc188d6e5643f73255ffa7a202b647
refresh	hypr/hyprsunset.conf	3042e4a923030239f4edf7b1943f41f4ed57107550177130c96f3b215b226e22
refresh	hypr/xdph.conf	637899cd3926c4b8c758d3cb812cc1e7ac7643aca83a8e5030208234d064c839
refresh	hypr/xdph.conf	2fded88a27b89f33cbc2b64a1258117df74b591f39a84c78ec3504a7d3b09136
refresh	hyprland-preview-share-picker/config.yaml	cb7f8768566ce41f8b09249b504ea8f6dbd6ed830637eef24c4e94e94486d113
refresh	hyprland-preview-share-picker/config.yaml	cf88364e497e4af2adaff9a0b7ca12227d29f831bc7e18846dd8f03bf4c618a8
refresh	imv/config	58fd37d4862eb484d50c2c0cc507836e685a776d8da8dd9f20bd894809c31cd6
refresh	imv/config	569b52b9614a78be67ce9f0a828ff1a7a73e4741d4751577ec6b247ab78e29ea
refresh	imv/config	832b19821b4c69f9381aea6902b1b80c5682c30091e6d4a59493c8edf827bf72
refresh	kitty/kitty.conf	69d6cbd2f8adcaec09407b5fb71ec7956cd510f6d64c36600ab7f7e7d3812cc6
refresh	kitty/kitty.conf	6c5fe293bb92b1ea5fd955afcec126597ae0a665395db1d38b9beb6461c587d1
refresh	kitty/kitty.conf	68779240358653a03ede95c4c872d94e3ab3742cf64a982b17e94f42aa2b74d4
refresh	kitty/kitty.conf	9599cfd1f1fbd7e14fe1ba48196137be3cf8bcfa95f6c24d15d8053d88f73f0a
refresh	kitty/kitty.conf	ea3bc5e353f7312d774362310f7c511584efd6076c163b4cad1a9cc05c0f80ab
refresh	kitty/kitty.conf	8984216fe0c0d006b969a7494dd4aad79ac78e9d15bd7a82ccbf5f10eac48fa6
refresh	lazygit/config.yml	e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
refresh	obsidian/user-flags.conf	6c00f4daeff2b018bda247151a8edee44115273f554cfda00909bc5e56a3ae85
refresh	omarchy/hooks/battery-low.d/play-warning-sound.sample	9623f5534cd52f76a51cfbb4e8a0699a550bf7f0f5f2ff54a1c78534d907fdf1
refresh	omarchy/hooks/font-set.d/show-font-notification.sample	d28090fd796b02fa799dcf7160e46ce9fc8d7a17b476ef87639d0b83a347dc37
refresh	omarchy/hooks/post-boot.d/weather.sample	fe37567a09046f1e4fcff9a9c74e4a036270cdf6da73dcc5cd3aa617f9a7c2ba
refresh	omarchy/hooks/post-update.d/show-update-notification.sample	7995adc93a0ac6cd1afcd7ed9ac6d0ff058751ce4df4594aa78855603a613815
refresh	omarchy/hooks/theme-set.d/show-theme-notification.sample	b9d6754d95057bdb2f37595fd2f40055872118cb2f442bdc7b0b900317bac332
refresh	omarchy/themed/alacritty.toml.tpl.sample	ea5889bf79e273aa9cf1e7a1b2916dae7751fc0dced2cd53af36a2ebeecb4f1e
refresh	omarchy/themed/alacritty.toml.tpl.sample	a05d015227a406470b571d9d8f372eb8042df94b32cf746e71fd2f513c9880ac
refresh	opencode/opencode.json	3806450805c8661a729c46aea325826e9ee7d158d650c9d750f5d0a763ec2c03
refresh	opencode/opencode.json	fb067af039252d48ef5b5c573a5e12c17357af4324b218aca5efa36f882846dd
refresh	starship.toml	b17c9b5f096fc125e97050e359171c6011d6b3c7d7e9ac28f630e75b4d4bb9db
refresh	tmux/tmux.conf	345abaa3f128529abbbe4da3a24c227170c9679fb0af1f3d87eb15959e183219
refresh	tmux/tmux.conf	93088311b8b67eb10ede3040b55e81193ff43fe16c5b2a6e3799300e00f39e3d
refresh	tmux/tmux.conf	02d7c3914b73b8fec139316b9e1e88a40764617c6f4f1b6a3c05ad9c4a68b944
refresh	tmux/tmux.conf	6633a3c59441087fdead8c30601e50c39fc7e74a66a32514296657332ff6cde9
refresh	tmux/tmux.conf	473abae53d3aa7da59be65cc6301a15f6cc878045ae3f4c5e180ee013a0a04a7
refresh	tmux/tmux.conf	bf881713284aa2cc6722a199e42d57c005ae5922ef4eb6305593ead09b13ca7c
refresh	xournalpp/settings.xml	47eafeeb3c246af1a5fb2cd1d87a748ce127d13410c351194c5d89bbaefb623d
refresh	xournalpp/settings.xml	1c1a9efbf1b6dc7813bf3440fd5bf8409fc283e8d0192ca297dc36a10e12c846
# retire: nothing is actually copied. The new package owns the file at the
# system path below (shipped by omarchy-settings), so once the user's
# hash-matched copy in ~/.config is removed, the system path takes effect
# via the usual XDG/fontconfig/systemd lookup order. User overrides
# (hash mismatch) are kept active as backups.
# Legacy ~/.config path -> package-owned destination (where the file now lives):
# environment.d/fcitx.conf -> /usr/lib/environment.d/10-omarchy-fcitx.conf
# fastfetch/config.jsonc -> /etc/fastfetch/config.jsonc
# fontconfig/fonts.conf -> /usr/share/fontconfig/conf.avail/50-omarchy.conf
# mimeapps.list -> /usr/share/applications/mimeapps.list
# omarchy.ttf -> /usr/share/fonts/omarchy/omarchy.ttf
# systemd/user/bt-agent.service -> /usr/lib/systemd/user/bt-agent.service
# systemd/user/omarchy-recover-internal-monitor.service -> /usr/lib/systemd/user/omarchy-recover-internal-monitor.service
# systemd/user/omarchy-sleep-lock.service -> /usr/lib/systemd/user/omarchy-sleep-lock.service
# uwsm/default -> /usr/share/omarchy/default/uwsm/default
# uwsm/env -> /usr/share/uwsm/env.d/10-omarchy
# xdg-terminals.list -> /usr/share/xdg-terminal-exec/hyprland-xdg-terminals.list
retire	environment.d/fcitx.conf	faacbd703a8f797013968b235fec6289a9c921d3ec7b7f8b4e1cd24392f68b9c
retire	environment.d/fcitx.conf	8b077b6ea2594dc1d4b57592a3bc62703df4959e86b93ea0fc488b6686309095
retire	fastfetch/config.jsonc	6adebf87d93902fb4bc68824a8e7c71e482846af23fea199e489c5496f791590
retire	fastfetch/config.jsonc	fe61d9f7cfac800adb7bd21f75f1bfcca266c609b6efa02a6e25303ac192e86e
retire	fastfetch/config.jsonc	62fa26fa5fb8e11d31c5ec9f3a53b9b6b1124b29134b19329a092878ff624581
retire	fastfetch/config.jsonc	abb336459df8dea8f84df1dd54a2f223adec558df3de39cce84ca83f88acc02f
retire	fastfetch/config.jsonc	f1c9a999a4f31a215b10474eeac8894ce6da220d680c30d2aab7b3c1b7a539a6
retire	fastfetch/config.jsonc	97aefdea2103d6c73e063bf0d11ad26da8fe60630305ac4eb2970cfc50cb0d91
retire	fastfetch/config.jsonc	22565532d139ab8c2ddb21dcfe56460dc9f2f8a712b8d29f125c1c8b29beca37
retire	fastfetch/config.jsonc	b65cb9ca36eed45929bfc35443762b14957d3847f6bed361a338224a87450e3a
retire	fastfetch/config.jsonc	3d153225131d3031b99b2e1a07e84d22cd0e51fcae1e701b08f6b6a558bc7a9c
retire	fastfetch/config.jsonc	404d0273c380cfdad4a037fa87202fc1d96be178c867e8a9d27b0a772f6e2fa1
retire	fastfetch/config.jsonc	4a98ff77ed46109d08ef7301249ea6823d1abba6c39c0871680feebf96620bf2
retire	fastfetch/config.jsonc	2cc59c562e51ed3a58a784a17414ad40699253d0a9522eb7ce01faf6d7cff7c2
retire	fastfetch/config.jsonc	07027778c12914c7077022cc835de4ac6c38af90aed539a871b13c7c89603e87
retire	fastfetch/config.jsonc	0bc8cb0a24ec07e786cccbad446fd80b72520e7effd9bff8cfa27b7361211f4b
retire	fastfetch/config.jsonc	c827c3a24f64c418d309244a8a967def0c522d824addf65d8ee0741afbc970bd
retire	fastfetch/config.jsonc	c909b43ba000887cd418d9eaf98c1c9c5ba938aa27f9246cf8fa5662946c4e87
retire	fastfetch/config.jsonc	b2e5757851f0703edb8bcfc4b51a00bf5e303c1df26b8c63e6b7df6be4ead4ed
retire	fastfetch/config.jsonc	1a0a085e7a80036f415148da21f772e99898de7e70768ec7b7613f8cc620700c
retire	fontconfig/fonts.conf	7db33b0f8673f71f42604255cad222194294cd401315fecde6b83151eecebe91
retire	fontconfig/fonts.conf	f35b5b4bd0d8d3926f5d9a49ca791fc66b00793786420508d4a4c41ae9ccbefc
retire	fontconfig/fonts.conf	6dec98b539388b95ecfdb3c7ab951001c712820eb0581002832cb7bfdde1a654
retire	fontconfig/fonts.conf	3545f6c5a8c1465df7a4e251b3c2047d4ba03654c86636794f64cb0d8ea8ef63
retire	fontconfig/fonts.conf	0f085b449f1cbe8eda3b59a6235bf2a3ddb6e982ab5d22fa642248916e63bafc
retire	mimeapps.list	3b574cef135b5deb7a8a0c7e17139037cf1fe155300e3809f60bed0e04120975
retire	omarchy.ttf	e55e67119e82f56f92d90cbf54b7ccc1b2946b32c535a29370439d7ef5215966
retire	systemd/user/bt-agent.service	0406b577a1225dc2a9f86638d3c346eb3635168576f04050be50ebcc0be6be12
retire	systemd/user/omarchy-recover-internal-monitor.service	b9b92cedc44cf3cb6216948629be55b53d16746e31896dc6469fd49ba55e82f4
retire	systemd/user/omarchy-recover-internal-monitor.service	e1483079b9f2aefcd43b4722c75a31643e5f3bb5a2eada5f52f1d7d201e8c289
retire	systemd/user/omarchy-sleep-lock.service	6870b232a6c0474b59187882e6d25ae771bba735098bcbedef8a2b73b97e2b6a
retire	systemd/user/omarchy-sleep-lock.service	bcd1a76cb5c63514922bc5e11af22ae480fc6d06a99863364e02bdf3c7bdceaf
retire	uwsm/default	2e89b03a3710b70cf754ea0c8db4388bb35a7519870036abff5ea8a584eafbc0
retire	uwsm/default	3622ba134d29b639a155424a5226215e213c8508144e98001ffe128a8967c9b5
retire	uwsm/default	73712c5a677532bd4afcee5800ae9b76e3d02a2130659a96a8b7fbe2fd2c3d53
retire	uwsm/default	56f968d461da1b8360bc21d5fb7d5375121b772dd06d9397ebcda55a9097c2a0
retire	uwsm/default	f7558795e1f6b89357a780ad4e1c63ffdd56e423f31c9a888738afe894e16623
retire	uwsm/env	60eae2629f2dbc70bb068de7bcb245032a429dd989f98b573eb315c0760b4ea5
retire	uwsm/env	daf70280edd80bae94660f8509d294df2aaa242d3428320bf6542341efff5a6d
retire	uwsm/env	61a4147404a2b6b920f6f414a8c9bcef69b8bcf2a1251e1ddd2fbc37fe45e04d
retire	uwsm/env	32130b0033e1d3229d89e6f73394e272eb3adad07314f740f8df9629c1f89beb
retire	uwsm/env	278df755a1679f2735742c61de9789614cdb2021fbbb4e802f056c0403d1a6b8
retire	uwsm/env	7be477d4e734d30ebc2caf9dbad6133dcb4aef38d64eb0979beab46f1c0e2464
retire	uwsm/env	0c2cd9c32bd8aea26c62bc18c4a287a29999d4b2ec2b3216b666fb204ab3c471
retire	uwsm/env	957dc0a95f8e909a378ce82967d7a55eb474e2fe5b2672212272e88530c4d4aa
retire	uwsm/env	425036ed11f004f59a5fca324305b13189a6aca4b4dd1582532f075d14413d93
retire	uwsm/env	9eb186609afce36a525e58e0b0858eb586869c5d067a7e5545bb5be606ab8a44
retire	xdg-terminals.list	7303fa8d1d74eda73cf5fb4c90183a8608c191bb80d7fb9d96ab60f9df06022e
retire	xdg-terminals.list	d035da571a6bfc7d626d4a88846ea86a8adb6f6662f1f8b4abe2330e53ceb71b
DEFAULT_HASHES
}

declare -A known_config_default_hashes_by_key=()
while IFS=$'	' read -r action rel hash; do
  [[ -n ${action:-} && $action != \#* ]] || continue
  known_config_default_hashes_by_key["$action:$rel:$hash"]=1
done < <(known_config_default_hashes)

default_hash_paths() {
  local action_filter="$1" action rel hash
  known_config_default_hashes | while IFS=$'	' read -r action rel hash; do
    [[ -n ${action:-} && $action != \#* ]] || continue
    [[ $action == "$action_filter" ]] || continue
    printf '%s\n' "$rel"
  done | sort -u
}

is_known_default_hash() {
  local action="$1" rel="$2" file="$3" hash
  [[ -f $file ]] || return 1
  hash=$(file_sha256 "$file")
  [[ -n ${known_config_default_hashes_by_key["$action:$rel:$hash"]+set} ]]
}

backup_config_file() {
  local file="$1"
  [[ -e $file || -L $file ]] || return 0
  cp -af "$file" "$file.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
}

mapfile -t retired_config_files < <(default_hash_paths retire)

# These are new Omarchy quattro config entry points. Older installs cannot have an
# Omarchy-shipped legacy copy to hash-match, so always install the quattro default.
always_copy_config_files=(
  hypr/.luarc.json
  hypr/autostart.lua
  hypr/bindings.lua
  hypr/hyprland.lua
  hypr/input.lua
  hypr/looknfeel.lua
  hypr/monitors.lua
  omarchy/extensions/omarchy-menu.jsonc
  omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
  omarchy/shell.json
)

migrate_uwsm_env_customizations() {
  local source="$1" dest="$HOME/.config/uwsm/env.d/99-omarchy-upgrade-env" tmp
  [[ -f $source ]] || return 0
  is_known_default_hash retire uwsm/env "$source" && return 0

  tmp=$(mktemp)
  awk '
    function omitted(reason) { print ": # omarchy-upgrade omitted legacy Omarchy " reason; next }

    /^[[:space:]]*export[[:space:]]+OMARCHY_PATH=\$HOME\/\.local\/share\/omarchy[[:space:]]*$/ { omitted("OMARCHY_PATH") }
    /^[[:space:]]*export[[:space:]]+OMARCHY_PATH="\$\{OMARCHY_PATH:-\/usr\/share\/omarchy\}"[[:space:]]*$/ { omitted("OMARCHY_PATH") }
    /^[[:space:]]*\[ -f \/etc\/omarchy\.conf \] && \. \/etc\/omarchy\.conf[[:space:]]*$/ { omitted("/etc/omarchy.conf source") }
    /^[[:space:]]*export[[:space:]]+PATH=\$OMARCHY_PATH\/bin:\$PATH:\$HOME\/\.local\/bin[[:space:]]*$/ { omitted("PATH setup") }
    /^[[:space:]]*export[[:space:]]+PATH="\$OMARCHY_PATH\/bin:\$PATH:\$HOME\/\.local\/bin"[[:space:]]*$/ { omitted("PATH setup") }
    /^[[:space:]]*source[[:space:]]+~\/\.config\/uwsm\/default[[:space:]]*$/ { omitted("uwsm/default source") }
    /^[[:space:]]*\[ -f "\$HOME\/\.config\/uwsm\/default" \] && \. "\$HOME\/\.config\/uwsm\/default"[[:space:]]*$/ { omitted("uwsm/default source") }
    /^[[:space:]]*omarchy-cmd-present[[:space:]]+mise && eval "\$\(mise activate bash --shims\)"[[:space:]]*$/ { omitted("mise activation") }

    { print }
  ' "$source" >"$tmp"

  if [[ -s $tmp ]]; then
    if ! sh -n "$tmp" >/dev/null 2>&1; then
      cp -f "$source" "$tmp"
    fi

    mkdir -p "$(dirname "$dest")"
    {
      echo '# Preserved custom ~/.config/uwsm/env content during Omarchy quattro upgrade.'
      echo '# Exact legacy Omarchy-managed setup lines were replaced with no-op markers where safe.'
      cat "$tmp"
    } >"$dest"
  fi

  rm -f "$tmp"
}

is_retired_config_file() {
  local rel="$1" retired
  [[ $rel == systemd/user/* ]] && return 0

  for retired in "${retired_config_files[@]}"; do
    [[ $rel == "$retired" ]] && return 0
  done

  return 1
}

copy_config_default() {
  local rel="$1" source="$root/config/$1" target="$HOME/.config/$1"
  [[ -e $source || -L $source ]] || return 0

  if [[ -e $target || -L $target ]]; then
    if [[ -f $source && -f $target ]] && cmp -s "$source" "$target"; then
      return 0
    fi
    backup_config_file "$target"
    rm -f "$target"
  fi

  mkdir -p "$(dirname "$target")"
  cp -P "$source" "$target"
}

copy_missing_config_defaults() {
  local source_dir="$1" target_dir="$2" source_path rel target_path
  [[ -d $source_dir ]] || return 0

  while IFS= read -r -d '' source_path; do
    rel="${source_path#$source_dir/}"
    is_retired_config_file "$rel" && continue

    target_path="$target_dir/$rel"
    [[ -e $target_path || -L $target_path ]] && continue

    mkdir -p "$(dirname "$target_path")"
    cp -P "$source_path" "$target_path"
  done < <(find "$source_dir" \( -type f -o -type l \) -print0)
}

refresh_known_config_defaults() {
  local rel file default

  while IFS= read -r rel; do
    file="$HOME/.config/$rel"
    default="$root/config/$rel"
    [[ -f $file && -f $default ]] || continue
    is_known_default_hash refresh "$rel" "$file" || continue
    copy_config_default "$rel"
  done < <(default_hash_paths refresh)
}

mark_removed_preinstalls_from_legacy_bindings() {
  local bindings_file="$HOME/.config/hypr/bindings.lua"
  local plain_bindings_sha="5d92d4008b04848256445da3cf5c10422a8d537baad499a4e63f60daecaefc6f"

  [[ -f $bindings_file ]] || return 0
  [[ $(file_sha256 "$bindings_file") == "$plain_bindings_sha" ]] || return 0

  mkdir -p "$state_dir"
  touch "$state_dir/preinstalls-removed"
}

copy_always_config_defaults() {
  local rel
  for rel in "${always_copy_config_files[@]}"; do
    copy_config_default "$rel"
  done
}

repair_chromium_copy_url_extension_flags() {
  local file
  local -a flag_files=(
    "$HOME/.config/chromium-flags.conf"
    "$HOME/.config/chrome-flags.conf"
    "$HOME/.config/google-chrome-flags.conf"
    "$HOME/.config/google-chrome-stable-flags.conf"
    "$HOME/.config/brave-flags.conf"
    "$HOME/.config/brave-browser-flags.conf"
    "$HOME/.config/brave-origin-beta-flags.conf"
    "$HOME/.config/microsoft-edge-flags.conf"
    "$HOME/.config/microsoft-edge-stable-flags.conf"
    "$HOME/.config/opera-flags.conf"
    "$HOME/.config/vivaldi-flags.conf"
    "$HOME/.config/helium-flags.conf"
  )

  for file in "${flag_files[@]}"; do
    [[ -f $file ]] || continue
    grep -qF '.local/share/omarchy/default/chromium/extensions/copy-url' "$file" || continue

    backup_config_file "$file"
    python3 - "$file" <<'CHROMIUM_FLAGS_PATCH_PY'
import sys
from pathlib import Path

path = Path(sys.argv[1])
new_path = "/usr/share/omarchy/default/chromium/extensions/copy-url"
legacy_paths = [
    "~/.local/share/omarchy/default/chromium/extensions/copy-url",
    "$HOME/.local/share/omarchy/default/chromium/extensions/copy-url",
    str(Path.home() / ".local/share/omarchy/default/chromium/extensions/copy-url"),
]
text = path.read_text()
for legacy_path in legacy_paths:
    text = text.replace(legacy_path, new_path)
path.write_text(text)
CHROMIUM_FLAGS_PATCH_PY
  done
}

repair_sleep_lock_unit_override() {
  local unit="$HOME/.config/systemd/user/omarchy-sleep-lock.service"
  local legacy_exec='ExecStart=%h/.local/share/omarchy/bin/omarchy-system-sleep-monitor'
  local current_exec='ExecStart=/usr/bin/omarchy-system-sleep-monitor'
  local tmp
  [[ -f $unit ]] || return 0

  if grep -qxF "$legacy_exec" "$unit"; then
    backup_config_file "$unit"
    tmp=$(mktemp)
    LEGACY_EXEC="$legacy_exec" CURRENT_EXEC="$current_exec" awk '
      $0 == ENVIRON["LEGACY_EXEC"] { print ENVIRON["CURRENT_EXEC"]; next }
      { print }
    ' "$unit" >"$tmp" && mv "$tmp" "$unit"
    rm -f "$tmp"
  fi
}

is_known_bashrc_default() {
  local file="$1" hash
  [[ -f $file ]] || return 1
  hash=$(file_sha256 "$file")

  case "$hash" in
    06ef4dcaa0530d7410513b65b82eea45f147d386a7689dcf1f5636fdad7561ff|\
    907d367c7589361788a2b0456003f6ef0a2e049548b77b9acf5b0f8cecc2ab0f|\
    7f46fdc7e3016ab2309d49607a9dddc907b06066cebcf23a63e0110f5d962d73|\
    322f495563435da1dc70e16968ee33f6a9d81ab361b79ebfd97814b83a89e011|\
    56009cd83e46e0715911877f173bf1dab966f3e22b6995c530937f2feb18b667|\
    1a8c98df85360a85acde31effa33c419ce9d27bc17e3a0d5900d4a6cb1743bcc)
      return 0
      ;;
  esac

  return 1
}

install_bash_startup() {
  local bashrc="$HOME/.bashrc" bash_profile="$HOME/.bash_profile"
  [[ -f $root/default/bashrc ]] || return 0

  backup_config_file "$bashrc"

  if [[ ! -e $bashrc && ! -L $bashrc ]] || is_known_bashrc_default "$bashrc"; then
    cp "$root/default/bashrc" "$bashrc"
  elif grep -qxF 'source ~/.local/share/omarchy/default/bash/rc' "$bashrc" || grep -qxF 'source "$OMARCHY_PATH/default/bash/rc"' "$bashrc"; then
    python3 - "$bashrc" <<'BASHRC_PATCH_PY'
import sys
from pathlib import Path

path = Path(sys.argv[1])
text = path.read_text()
new_block = '''# /etc/omarchy.conf is written by omarchy-dev-link. When absent, force the
# package default instead of preserving a stale inherited dev-link value before
# we decide which rc file to source.
if [[ -f /etc/omarchy.conf ]]; then
  source /etc/omarchy.conf
  export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
else
  export OMARCHY_PATH=/usr/share/omarchy
fi
source "$OMARCHY_PATH/default/bash/rc"
'''
text = text.replace('source ~/.local/share/omarchy/default/bash/rc\n', new_block)
text = text.replace(': "${OMARCHY_PATH:=/usr/share/omarchy}"\nsource "$OMARCHY_PATH/default/bash/rc"\n', new_block)
path.write_text(text)
BASHRC_PATCH_PY
  fi

  if [[ ! -e $bash_profile && ! -L $bash_profile ]]; then
    echo '[[ -f ~/.bashrc ]] && . ~/.bashrc' >"$bash_profile"
  fi
}

# Some former ~/.config defaults are now package-owned elsewhere so future
# updates can change defaults without rewriting user files. Back them up before
# cleanup, then keep only real user customizations active.
uwsm_env="$HOME/.config/uwsm/env"
if [[ -f $uwsm_env ]]; then
  cp -f "$uwsm_env" "$uwsm_env.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
  migrate_uwsm_env_customizations "$uwsm_env"
fi

for rel in "${retired_config_files[@]}"; do
  file="$HOME/.config/$rel"
  backup_config_file "$file"
done

# Existing user config may be customized. Refresh only files whose hash matches
# a default Omarchy has shipped before, fill in missing defaults for old installs,
# and always write the quattro-only entry points that older installs cannot have.
refresh_known_config_defaults
copy_missing_config_defaults "$root/config" "$HOME/.config"
mark_removed_preinstalls_from_legacy_bindings
copy_always_config_defaults
repair_chromium_copy_url_extension_flags

# Carry over user-added backgrounds from the legacy git checkout backup, then
# remove old symlinks that pointed ~/.config/omarchy/themes/* back into the
# pre-quattro checkout. Omarchy quattro reads stock themes from /usr/share/omarchy/themes.
legacy_omarchy_backup="$HOME/.local/share/omarchy.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
if [[ -d $legacy_omarchy_backup/themes && -d $legacy_omarchy_backup/.git ]]; then
  (
    cd "$legacy_omarchy_backup"
    git ls-files --others -- 'themes/*/backgrounds/*' 2>/dev/null | while IFS= read -r bg_file; do
      theme_name=${bg_file#themes/}
      theme_name=${theme_name%%/*}
      mkdir -p "$HOME/.config/omarchy/backgrounds/$theme_name"
      mv "$bg_file" "$HOME/.config/omarchy/backgrounds/$theme_name/"
    done
  )
fi
if [[ -d $HOME/.config/omarchy/themes ]]; then
  find "$HOME/.config/omarchy/themes" -mindepth 1 -maxdepth 1 -type l -delete
fi

# Retire legacy UI configs replaced by omarchy-shell. Hyprland .conf files are
# intentionally left in place for users to reference/port after the upgrade.
for rel in waybar swayosd mako walker; do
  file="$HOME/.config/$rel"
  if [[ -e $file || -L $file ]]; then
    backup="$file.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
    rm -rf "$backup"
    mv "$file" "$backup"
  fi
done

for rel in "${retired_config_files[@]}"; do
  file="$HOME/.config/$rel"
  backup="$file.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"

  # UWSM env was hardcoded to the legacy checkout on older installs. Custom
  # non-Omarchy lines were migrated to env.d above; never keep the active file.
  if [[ $rel == uwsm/env ]]; then
    rm -f "$file"
    continue
  fi

  if [[ -f $backup ]] && ! is_known_default_hash retire "$rel" "$backup"; then
    # Keep intentional user overrides active; package defaults remain lower
    # priority under /usr/lib or /usr/share.
    mkdir -p "$(dirname "$file")"
    cp -af "$backup" "$file"
  else
    rm -f "$file"
  fi
done
repair_sleep_lock_unit_override
install_bash_startup

mkdir -p "$HOME/.agents/skills" "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.pi/agent/skills"
if [[ -d $root/default/agents/skills/omarchy ]]; then
  ln -sfn "$root/default/agents/skills/omarchy" "$HOME/.agents/skills/omarchy"
  ln -sfn "$root/default/agents/skills/omarchy" "$HOME/.claude/skills/omarchy"
  ln -sfn "$root/default/agents/skills/omarchy" "$HOME/.codex/skills/omarchy"
  ln -sfn "$root/default/agents/skills/omarchy" "$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
# Legacy Hyprland .conf entry points source ~/.local/state/omarchy/toggles/hypr/*.conf.
# Keep an empty match around for the live pre-reboot session; quattro Lua ignores it.
if [[ -f $HOME/.config/hypr/hyprland.conf ]]; then
  : >"$HOME/.local/state/omarchy/toggles/hypr/flags.conf"
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

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"
legacy_current_dir="$HOME/.config/omarchy/current"
current_state_dir="$HOME/.local/state/omarchy/current"
if [[ -e $legacy_current_dir || -L $legacy_current_dir ]]; then
  mkdir -p "$HOME/.local/state/omarchy"
  if [[ ! -e $current_state_dir && ! -L $current_state_dir ]]; then
    mv "$legacy_current_dir" "$current_state_dir"
  else
    if [[ -d $legacy_current_dir && -d $current_state_dir ]]; then
      cp -an "$legacy_current_dir/." "$current_state_dir/" 2>/dev/null || true
    fi
    rm -rf "$legacy_current_dir"
  fi
fi
replace_legacy_current_path() {
  local file="$1"

  [[ -f $file ]] || return 0
  python3 - "$file" "$HOME" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
home = sys.argv[2]
text = path.read_text()
replacements = {
  f"{home}/.config/omarchy/current": f"{home}/.local/state/omarchy/current",
  "~/.config/omarchy/current": "~/.local/state/omarchy/current",
  "../omarchy/current": "../../.local/state/omarchy/current",
}
for old, new in replacements.items():
  text = text.replace(old, new)
path.write_text(text)
PY
}
for file in \
  "$HOME/.config/alacritty/alacritty.toml" \
  "$HOME/.config/foot/foot.ini" \
  "$HOME/.config/ghostty/config" \
  "$HOME/.config/hypr/hyprland.conf" \
  "$HOME/.config/hyprland-preview-share-picker/config.yaml" \
  "$HOME/.config/kitty/kitty.conf"; do
  replace_legacy_current_path "$file"
done
ln -snf "$HOME/.local/state/omarchy/current/theme/btop.theme" "$HOME/.config/btop/themes/current.theme"
if [[ ! -e $HOME/.local/state/omarchy/current/background && -d $HOME/.local/state/omarchy/current/theme/backgrounds ]]; then
  background=$(find "$HOME/.local/state/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/.local/state/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 link_target target
  local -a targets=()

  if [[ -f $HOME/.config/systemd/user/$unit ]]; then
    unit_path="$HOME/.config/systemd/user/$unit"
    link_target="../$unit"
  elif [[ -f /usr/lib/systemd/user/$unit ]]; then
    unit_path="/usr/lib/systemd/user/$unit"
    link_target="$unit_path"
  elif [[ -f $root/default/systemd/user/$unit ]]; then
    unit_path="$root/default/systemd/user/$unit"
    link_target="$unit_path"
  else
    return 0
  fi

  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 "$link_target" "$HOME/.config/systemd/user/$target.wants/$unit"
  done
}
enable_user_unit bt-agent.service
enable_user_unit omarchy-sleep-lock.service
enable_user_unit omarchy-recover-internal-monitor.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-quattro.$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 \
    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
  env -u BROWSER 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

# This script is fetched from the branch and runs against whatever packaged
# tree the channel currently serves, so the packaged tree can be older than the
# script. The completion markers are written directly rather than through
# omarchy-done, because a command missing from an older build would abort the
# upgrade two thirds of the way through.
done_dir="$state_dir/done"
mkdir -p "$done_dir"
touch "$done_dir/first-run-user" "$done_dir/finalize-user"
rm -f "$state_dir/first-run-user.done" "$state_dir/finalize-user.done"
USER_SETUP
}

apply_user_hardware_transition() {
  if grep -qi "DX13260" /sys/class/dmi/id/product_name 2>/dev/null ||
    grep -qi "DX13260" /sys/class/dmi/id/product_family 2>/dev/null; then
    log "Adjusting text scaling for Dell XPS 13 DX13260"
    if ! run_as_user_session gsettings set org.gnome.desktop.interface text-scaling-factor 0.95; then
      warn "Could not adjust text scaling for Dell XPS 13 DX13260; continuing."
    fi
  fi
}

write_legacy_theme_hyprland_conf() {
  run_as_user env HOME="$target_home" bash <<'THEME_COMPAT'
set -euo pipefail
hyprland_conf="$HOME/.config/hypr/hyprland.conf"
[[ -f $hyprland_conf ]] || exit 0
# Legacy current-path references were already rewritten during the user
# transition; only the theme hyprland.conf shim is needed here.
grep -q 'current/theme/hyprland.conf' "$hyprland_conf" || exit 0

theme_dir="$HOME/.local/state/omarchy/current/theme"
colors="$theme_dir/colors.toml"
target="$theme_dir/hyprland.conf"
mkdir -p "$theme_dir"
accent="7aa2f7"
if [[ -f $colors ]]; then
  parsed=$(awk -F= '/^[[:space:]]*accent[[:space:]]*=/ { gsub(/[[:space:]"#]/, "", $2); print $2; exit }' "$colors")
  [[ -n ${parsed:-} ]] && accent="$parsed"
fi
{
  printf '$activeBorderColor = rgb(%s)\n\n' "$accent"
  cat <<'EOF'
general {
    col.active_border = $activeBorderColor
}

group {
    col.border_active = $activeBorderColor
}
EOF
} >"$target"
THEME_COMPAT
}

refresh_current_theme_after_upgrade() {
  local theme_name runtime_dir

  theme_name=$(run_as_user env HOME="$target_home" bash -c '
    set -euo pipefail
    theme_name_path="$HOME/.local/state/omarchy/current/theme.name"
    current_theme_path="$HOME/.local/state/omarchy/current/theme"
    legacy_theme_name_path="$HOME/.config/omarchy/current/theme.name"
    legacy_current_theme_path="$HOME/.config/omarchy/current/theme"

    if [[ -f $theme_name_path ]]; then
      read -r theme_name <"$theme_name_path" || true
      printf "%s" "${theme_name:-}"
    elif [[ -f $legacy_theme_name_path ]]; then
      read -r theme_name <"$legacy_theme_name_path" || true
      printf "%s" "${theme_name:-}"
    elif [[ -L $current_theme_path ]]; then
      basename -- "$(readlink "$current_theme_path")"
    elif [[ -L $legacy_current_theme_path ]]; then
      basename -- "$(readlink "$legacy_current_theme_path")"
    else
      printf ""
    fi
  ')
  theme_name=${theme_name//$'\r'/}
  theme_name=${theme_name//$'\n'/}
  [[ -n $theme_name ]] || theme_name="Tokyo Night"

  log "Refreshing current theme with Omarchy quattro templates ($theme_name)"

  runtime_dir="$target_runtime_dir"
  [[ -d $runtime_dir ]] || runtime_dir=/tmp

  if ! run_as_user_omarchy \
    XDG_RUNTIME_DIR="$runtime_dir" \
    DBUS_SESSION_BUS_ADDRESS="unix:path=$runtime_dir/bus" \
    XDG_CURRENT_DESKTOP=Hyprland \
    XDG_SESSION_DESKTOP=Hyprland \
    XDG_SESSION_TYPE=wayland \
    DESKTOP_SESSION=hyprland \
    OMARCHY_THEME_HEADLESS=1 \
    omarchy-theme-set "$theme_name"; then
    warn "Could not refresh current theme '$theme_name'. Run 'omarchy theme set \"$theme_name\"' after reboot if theme-dependent app config looks stale."
  fi

  write_legacy_theme_hyprland_conf

  # Headless theme refresh intentionally skips omarchy-theme-set's live post
  # hooks because one of them runs `hyprctl reload`. Still poke terminal
  # emulators so the active upgrade terminal picks up generated theme files.
  run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
}

# Everything below mutates the system, so a non-zero exit from here on leaves a
# half-upgraded machine that must not be rebooted.
upgrade_started=1

# Suppress live Hyprland reloads before anything touches the legacy config
# trees; the swap windows otherwise surface transient "source= globbing error"
# popups in the running session.
suppress_hyprland_config_reload
create_pre_upgrade_snapshot
configure_pacman_channel
install_keyrings
remove_legacy_installer_package
remove_legacy_limine_configs
remove_conflicting_legacy_packages
install_omarchy_quattro_packages
install_hardware_transition_packages
normalize_limine_config
preserve_kernel_cmdline_root
configure_snapper_policy
configure_lock_authentication
migrate_1password_beta_package
cleanup_legacy_user_paths
apply_system_transition
apply_user_transition
apply_user_hardware_transition
run_as_user_omarchy omarchy-refresh-applications ||
  warn "Could not refresh application launchers; run 'omarchy refresh applications' after reboot."
run_as_user_omarchy omarchy-bar defaults ||
  warn "Could not restore the default bar widgets; run 'omarchy bar defaults' after reboot."
cleanup_retired_services
ensure_sleep_lock_service
remove_retired_default_packages
run_post_upgrade_migrations
run_final_system_package_upgrade
run_post_upgrade_update_steps
refresh_current_theme_after_upgrade
# Do not force-reload Hyprland in the live upgraded session. The legacy
# default/hypr and theme hyprland.conf shims above exist specifically so the
# pre-reboot session can keep running safely; the reboot performs the real
# Omarchy quattro Lua-config cutover.
restore_hyprland_config_reload

upgrade_completed=1

cat <<EOF

WARNING: You must address any errors in the above before rebooting.

EOF

if (( boot_cmdline_unsafe )); then
  warn "Skipping reboot: root= could not be confirmed in the boot entries. Repair the kernel cmdline (see the warnings above) before rebooting, or the system will drop to an emergency shell."
elif (( auto_reboot )); then
  log "Rebooting because --reboot was passed"
  as_root systemctl reboot
elif gum confirm "Reboot to complete Quattro upgrade now?" </dev/tty; then
  as_root systemctl reboot
else
  echo "Not rebooting. Please reboot manually after confirming there were no errors."
fi
