#!/usr/bin/env bash
# Foreground Omarchy ISO install UI supervisor.
#
# This process is the only owner of the visible install UI. It launches the
# actual installer as a non-interactive child, sends child output to the support
# log, renders progress while the child runs, then renders the final installed
# time + reboot prompt itself. There is deliberately no background dashboard.
set -euo pipefail

usage() {
  echo "Usage: $0 <log-file> <state-file> -- <installer command...>" >&2
  exit 2
}

(( $# >= 4 )) || usage
LOG_FILE="$1"
STATE_FILE="$2"
shift 2
[[ ${1:-} == "--" ]] || usage
shift
(( $# > 0 )) || usage
INSTALL_CMD=("$@")

TTY_PATH="${OMARCHY_DASHBOARD_TTY:-/dev/tty}"
LOGO_PATH="${OMARCHY_PATH:-/usr/share/omarchy}/logo.txt"

# Autoinstall has no one at the keyboard, and every prompt this dashboard owns
# reads from the TTY, so each would hang forever. Suppress them here rather than
# in the scripts they live in: this process is the only owner of the visible UI.
interactive() { [[ ${OMARCHY_UI_INTERACTIVE:-} != "no" ]]; }

CSI=$'\033['
RESET="${CSI}0m"
DIM="${CSI}2m"
HIDE_CURSOR="${CSI}?25l"
SHOW_CURSOR="${CSI}?25h"
CLEAR="${CSI}2J${CSI}H"
CLEAR_LINE="${CSI}2K"
CLEAR_TO_END="${CSI}J"
GREEN="${CSI}32m"
RED="${CSI}31m"

# The dashboard owns the final reboot prompt now, so keep the gum confirm
# styling that used to come from install/helpers/presentation.sh.
export GUM_CONFIRM_PROMPT_FOREGROUND="${GUM_CONFIRM_PROMPT_FOREGROUND:-6}"
export GUM_CONFIRM_SELECTED_FOREGROUND="${GUM_CONFIRM_SELECTED_FOREGROUND:-0}"
export GUM_CONFIRM_SELECTED_BACKGROUND="${GUM_CONFIRM_SELECTED_BACKGROUND:-2}"
export GUM_CONFIRM_UNSELECTED_FOREGROUND="${GUM_CONFIRM_UNSELECTED_FOREGROUND:-7}"
export GUM_CONFIRM_UNSELECTED_BACKGROUND="${GUM_CONFIRM_UNSELECTED_BACKGROUND:-0}"

child_pid=""
child_pgid=""

whimsy=(
  "delivering delightful defaults"
  "saving setup struggles"
  "picking polished packages"
  "curating cohesive colors"
  "preparing pleasant pixels"
  "hyping happy hyprland"
  "integrating intuitive interfaces"
  "bootstrapping beautiful boxes"
  "crafting cozy commands"
  "making machines marvelous"
  "polishing pretty pixels"
  "tuning tasteful terminals"
  "weaving wonderful workflows"
)

tips=(
  "Super + Space opens the Omarchy menu for apps, settings, and more"
  "Super + K shows all the key bindings"
  "Super is the Windows or command key on your keyboard"
  "Use Xournal++ to sign PDFs"
  "Share files with phones and laptops using LocalSend"
  "Turn any website into an app with Install > Web App in the menu"
  "Super + Return opens a terminal, Super + Shift + Return the browser"
  "Edit images with Pinta, videos with Kdenlive, docs with LibreOffice"
  "Super + Ctrl + Print grabs text off the screen with OCR"
  "Print takes a screenshot, Alt + Print records the screen"
  "Set a reminder with Super + Ctrl + R"
  "Switch themes from Style > Theme in the Omarchy menu"
  "Double-click the menu bar to make it transparent"
  "Run a full Windows VM via Install > Windows in the menu"
  "Super + Ctrl + V opens the clipboard manager"
  "Super + 1 through 0 switches workspaces, add Shift to bring the window"
  "Super + Print picks a color from anywhere on screen"
  "Keep the system fresh with Update in the Omarchy menu"
)

cleanup() {
  printf '%s%s' "$RESET" "$SHOW_CURSOR" >"$TTY_PATH" 2>/dev/null || true
}

dashboard_log() {
  printf '[dashboard] %s\n' "$*" >>"$LOG_FILE" 2>/dev/null || true
}

stop_child() {
  [[ -n ${child_pid:-} ]] || return 0
  if kill -0 "$child_pid" 2>/dev/null; then
    if [[ -n ${child_pgid:-} ]]; then
      kill -- -"$child_pgid" 2>/dev/null || true
    fi
    kill "$child_pid" 2>/dev/null || true
    for _ in {1..20}; do
      kill -0 "$child_pid" 2>/dev/null || break
      sleep 0.05
    done
    if kill -0 "$child_pid" 2>/dev/null; then
      [[ -n ${child_pgid:-} ]] && kill -9 -- -"$child_pgid" 2>/dev/null || true
      kill -9 "$child_pid" 2>/dev/null || true
    fi
  fi
  wait "$child_pid" 2>/dev/null || true
}

interrupt() {
  local code="$1"
  stop_child
  cleanup
  exit "$code"
}

trap cleanup EXIT
trap 'interrupt 130' INT
trap 'interrupt 143' TERM

term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; }
term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; }

# Read the actual character dimensions instead of assuming an 8 x 16 font.
# Linux VTs expose the font through KDFONTOP; desktop terminals may expose
# pixels in their window size or answer the standard cell-size query.
term_cell_size() {
  python3 - "$TTY_PATH" <<'PY'
import fcntl
import os
import re
import select
import struct
import sys
import termios
import time
import tty

with open(sys.argv[1], "r+b", buffering=0) as terminal:
    fd = terminal.fileno()
    rows, cols, width, height = struct.unpack(
        "HHHH", fcntl.ioctl(fd, termios.TIOCGWINSZ, bytes(8)))
    if rows and cols and width and height:
        print(round(width / cols), round(height / rows))
        sys.exit()
    try:
        font = bytearray(struct.pack("@5IP", 1, 0, 0, 0, 0, 0))
        fcntl.ioctl(fd, 0x4B72, font)  # KDFONTOP / KD_FONT_OP_GET, no glyph data
        _, _, width, height, _ = struct.unpack_from("@5I", font)
        if width and height:
            print(width, height)
            sys.exit()
    except OSError:
        pass

    settings = termios.tcgetattr(fd)
    try:
        tty.setcbreak(fd, termios.TCSANOW)
        os.write(fd, b"\x1b[16t")
        response = b""
        deadline = time.monotonic() + 0.15
        while select.select([fd], [], [], max(0, deadline - time.monotonic()))[0]:
            response += os.read(fd, 64)
            match = re.search(rb"\x1b\[6;(\d+);(\d+)t", response)
            if match:
                height, width = map(int, match.groups())
                if width and height:
                    print(width, height)
                    sys.exit()
            if time.monotonic() >= deadline:
                break
    finally:
        termios.tcsetattr(fd, termios.TCSANOW, settings)
    print(8, 16)  # Conventional console font when dimensions aren't available.
PY
}

logo_width() {
  if [[ -f $LOGO_PATH ]]; then
    # The logo is drawn in box-drawing glyphs, three bytes each in UTF-8, so
    # awk's `length` returns 197 rather than 81 unless the locale happens to be
    # UTF-8 — and a width wider than the console makes left_padding clamp to
    # zero, which puts the logo flush left. Count bytes deliberately (LC_ALL=C)
    # and drop the continuation bytes instead, so the answer is columns in every
    # locale rather than only in the ones that have a charset.
    LC_ALL=C awk '
      { gsub(/[\200-\277]/, ""); if (length > max) max = length }
      END { print max+0 }
    ' "$LOGO_PATH"
  else
    printf '81\n'
  fi
}

logo_height() {
  if [[ -f $LOGO_PATH ]]; then
    wc -l <"$LOGO_PATH"
  else
    printf '1\n'
  fi
}

LOGO_WIDTH="$(logo_width)"
LOGO_HEIGHT="$(logo_height)"
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
CONTENT_WIDTH="$LOGO_WIDTH"
TOP_ROW=1
LAST_SIZE=""
LAST_PM=10
# Wordmark, blank, title, and the three rows gum confirm uses.
FINISH_HEIGHT=$((LOGO_HEIGHT + 5))
FINISH_TOP_ROW=2

# ── Install progress ─────────────────────────────────────────────────────────
#
# Position = max(floor, work), both non-decreasing, in per-mille of the drawing.
#
#   floor  lo + (span-1) * t / (t+tau), on time since this process saw the
#          phase. Asymptotic, so it approaches the band end without reaching it.
#          tau is a shape constant, not a duration prediction.
#   work   packages installed / PKG_TOTAL, for the package phase only (~85% of
#          the install). libalpm makes one directory per package under
#          <target>/var/lib/pacman/local; the denominator comes from
#          build-iso.sh, so there is no second package list to drift.
#
# Per-mille because a whole-percent asymptote over a 2-point band has two
# reachable values and stops moving. No give-up predicate: two discontinuous
# curves behind a monotonic clamp freeze. With no signal the floor alone still
# finishes the band.
EXPECTED_PACKAGES_FILE="${OMARCHY_EXPECTED_PACKAGES_FILE:-/usr/share/omarchy-iso/expected-packages}"
PKG_TOTAL=0
if [[ -r $EXPECTED_PACKAGES_FILE ]]; then
  read -r PKG_TOTAL _ <"$EXPECTED_PACKAGES_FILE" 2>/dev/null || PKG_TOTAL=0
fi
[[ $PKG_TOTAL =~ ^[0-9]+$ ]] || PKG_TOTAL=0
# A wrong denominator is worse than none: it never triggers the fallback.
(( PKG_TOTAL < 300 || PKG_TOTAL > 5000 )) && PKG_TOTAL=0

PKG_BONUS_MAX_PM=100   # cap on how far progress may run ahead of the count
PKG_TAIL_PM=40         # band held back so stalled progress has somewhere to go
PKG_FLOOR_TAU=900      # with a work signal, time is only a backstop until the
                       # first package, so slow it until it cannot outrun data

POS=10                 # position, per-mille, monotonic
PROGRESS_PM=10
NOW=0                  # monotonic seconds, refreshed once per frame
DASH_T0=0
PHASE_NAME=""          # last phase this process observed
PHASE_T0=0             # NOW when it was first observed
PHASE_LO=10            # POS on entry, for an unknown phase with no index
PKG_DB_DIR=""
PKG_COUNT=0
PKG_BASE=-1            # package count when the phase started
PKG_SEEN=0             # running max of packages since PKG_BASE
PKG_LAST_CHANGE=0
PKG_BONUS=0

# Monotonic clock: tzupdate and systemd-timesyncd can step the wall clock
# mid-install, which would jump every time-driven band to its ceiling.
set_now() {
  local up rest
  if read -r up rest </proc/uptime 2>/dev/null && [[ $up == [0-9]* ]]; then
    NOW=${up%%.*}
  else
    NOW=$EPOCHSECONDS
  fi
  return 0
}

left_padding() {
  local width="${1:-$CONTENT_WIDTH}" cols pad
  cols="$(term_cols)"
  pad=$(( (cols - width) / 2 ))
  (( pad < 0 )) && pad=0
  printf '%*s' "$pad" ''
}

visible_len() {
  local text="$1"
  text="$(printf '%b' "$text" | sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g')"
  printf '%s' "${#text}"
}

blank_line() { printf '\r%s\n' "$CLEAR_LINE"; }

line_at() {
  local width="${1:-$CONTENT_WIDTH}" indent="${2:-0}"
  shift 2
  printf '\r%s%s%*s' "$CLEAR_LINE" "$(left_padding "$width")" "$indent" ''
  printf '%b' "$*"
}

center() {
  local text="$1" width="${2:-$CONTENT_WIDTH}" len inner_pad
  len="$(visible_len "$text")"
  inner_pad=$(( (width - len) / 2 ))
  (( inner_pad < 0 )) && inner_pad=0
  printf '\r%s%s%*s%b%s\n' "$CLEAR_LINE" "$(left_padding "$width")" "$inner_pad" '' "$text" "$RESET"
}

render_logo() {
  local pad
  pad="$(left_padding "$LOGO_WIDTH")"
  if [[ -f $LOGO_PATH ]]; then
    while IFS= read -r line; do
      printf '\r%s%s%b%s%b\n' "$CLEAR_LINE" "$pad" "$GREEN" "$line" "$RESET"
    done <"$LOGO_PATH"
  else
    center "${GREEN}Omarchy${RESET}" "$CONTENT_WIDTH"
  fi
}

# The icon uses half-block glyphs, scaled to the terminal's actual cell aspect
# ratio so its square silhouette stays square. No graphics protocol is needed.
# Coordinates follow the 15 x 15 grid of Omarchy's icon.svg. Each segment
# paints the full stroke width, starting at the bottom middle of the outer ring.
ICON_WIDTH=30
ICON_HEIGHT=15
CELL_WIDTH=8
CELL_HEIGHT=16
ICON_LEFT=1
ICON_STEPS=0
ICON_DRAWN=0
DISPLAY_PM=0
ICON_FRAME=""
declare -a ICON_ORDER=() ICON_TRACE=() ICON_FILL=()

icon_segment() {
  local scale="$1" x1="$2" y1="$3" x2="$4" y2="$5"
  local x y from to
  if (( y1 == y2 )); then
    from=$(( (x1 * ICON_WIDTH + 14) / 15 ))
    to=$(( ((x2 + 1) * ICON_WIDTH + 14) / 15 ))
    if (( x2 < x1 )); then
      from=$(( ((x1 + 1) * ICON_WIDTH + 14) / 15 - 1 ))
      to=$(( (x2 * ICON_WIDTH + 14) / 15 - 1 ))
    fi
    for ((x=from; x!=to; x+=(x2 >= x1 ? 1 : -1))); do
      for ((y=y1 * scale; y<(y1 + 1) * scale; y++)); do
        icon_pixel "$((y * ICON_WIDTH + x))"
      done
    done
  else
    from=$(( y1 * scale ))
    to=$(( (y2 + 1) * scale ))
    if (( y2 < y1 )); then
      from=$(( (y1 + 1) * scale - 1 ))
      to=$(( y2 * scale - 1 ))
    fi
    for ((y=from; y!=to; y+=(y2 >= y1 ? 1 : -1))); do
      for ((x=(x1 * ICON_WIDTH + 14) / 15; x<((x1 + 1) * ICON_WIDTH + 14) / 15; x++)); do
        icon_pixel "$((y * ICON_WIDTH + x))"
      done
    done
  fi
}

icon_pixel() {
  local pixel="$1"
  if [[ ! -v ICON_TRACE[pixel] ]]; then
    ICON_ORDER+=("$pixel")
    ICON_TRACE[pixel]=${#ICON_ORDER[@]}
  fi
}

build_icon() {
  local scale="$1" x y row
  local -a mask=(
    111111111111111
    100000010000001
    101111110001101
    101000000000101
    101000000000101
    101000000000101
    101000000000101
    111000000000101
    101000000000101
    101000000000101
    101000000000101
    101000000000101
    101111111111101
    100000010000001
    111111110111111
  )
  ICON_WIDTH=$(( (15 * scale * CELL_HEIGHT + CELL_WIDTH) / (2 * CELL_WIDTH) ))
  ICON_HEIGHT=$(((15 * scale + 1) / 2))
  ICON_ORDER=() ICON_TRACE=() ICON_FILL=()
  ICON_DRAWN=0
  # Start at the bottom middle, follow the outer left edge to the top cross,
  # then descend into the inner shape. Fill its two bridges as we reach them.
  icon_segment "$scale" 7 14 0 14
  icon_segment "$scale" 0 14 0 0
  icon_segment "$scale" 0 0 7 0
  icon_segment "$scale" 7 0 7 2
  icon_segment "$scale" 7 2 2 2
  icon_segment "$scale" 2 2 2 7
  icon_segment "$scale" 2 7 0 7
  icon_segment "$scale" 2 7 2 12
  icon_segment "$scale" 2 12 7 12
  icon_segment "$scale" 7 12 7 14
  icon_segment "$scale" 7 12 12 12
  icon_segment "$scale" 12 12 12 2
  icon_segment "$scale" 12 2 11 2
  # Finish from the top cross: right along the outside, down, then left.
  icon_segment "$scale" 7 0 14 0
  icon_segment "$scale" 14 0 14 14
  icon_segment "$scale" 14 14 9 14
  ICON_STEPS=${#ICON_ORDER[@]}
  for ((y=0; y<15 * scale; y++)); do
    row=${mask[y / scale]}
    for ((x=0; x<ICON_WIDTH; x++)); do
      ICON_FILL[y * ICON_WIDTH + x]=${row:x * 15 / ICON_WIDTH:1}
    done
  done
}

# Append one cell with independently colored upper/lower pixels. This also
# handles a green tracing tip sharing a cell with the still-dark outline.
icon_cell() {
  local pixel="$1" drawn="$2" solid="$3" upper=0 lower=0 step
  if (( solid )); then
    (( ICON_FILL[pixel] )) && upper=32
    (( ${ICON_FILL[pixel + ICON_WIDTH]:-0} )) && lower=32
  else
    step=${ICON_TRACE[pixel]:-0}
    (( step > 0 )) && upper=90
    (( step > 0 && step <= drawn )) && upper=32
    step=${ICON_TRACE[pixel + ICON_WIDTH]:-0}
    (( step > 0 )) && lower=90
    (( step > 0 && step <= drawn )) && lower=32
  fi
  if (( upper == 0 && lower == 0 )); then
    ICON_FRAME+="${RESET} "
  elif (( upper == lower )); then
    ICON_FRAME+="${CSI}${upper};49m█"
  elif (( lower == 0 )); then
    ICON_FRAME+="${CSI}${upper};49m▀"
  elif (( upper == 0 )); then
    ICON_FRAME+="${CSI}${lower};49m▄"
  else
    ICON_FRAME+="${CSI}${upper};$((lower + 10))m▀"
  fi
}

render_icon() {
  local pm="$1" full="${2:-0}" drawn i pixel row col
  drawn=$((pm * ICON_STEPS / 1000))
  ICON_FRAME=""
  if (( full || pm == 1000 )); then
    for ((row=0; row<ICON_HEIGHT; row++)); do
      ICON_FRAME+="${CSI}$((TOP_ROW + row));${ICON_LEFT}H"
      for ((col=0; col<ICON_WIDTH; col++)); do
        icon_cell "$((row * 2 * ICON_WIDTH + col))" "$drawn" "$((pm == 1000))"
      done
    done
  else
    # Between layout changes, repaint only newly traced pixels. This keeps the
    # animation cheap even while pacstrap saturates the disk.
    for ((i=ICON_DRAWN; i<drawn; i++)); do
      pixel=${ICON_ORDER[i]}
      row=$((pixel / ICON_WIDTH / 2)) col=$((pixel % ICON_WIDTH))
      ICON_FRAME+="${CSI}$((TOP_ROW + row));$((ICON_LEFT + col))H"
      icon_cell "$((row * 2 * ICON_WIDTH + col))" "$drawn" 0
    done
  fi
  printf '%s%s' "$ICON_FRAME" "$RESET"
  ICON_DRAWN=$drawn
}

# Packages libalpm has finished installing. local/ holds one directory per
# package and nothing else in it is a directory, so */ is exact. One glob, no
# forks: this runs twice a second while the disk is saturated.
count_packages() {
  local -a entries
  PKG_COUNT=0
  [[ -n $PKG_DB_DIR ]] || return 0
  entries=("$PKG_DB_DIR"/*/)
  # Unmatched globs expand to the pattern; nullglob stays off script-wide.
  if (( ${#entries[@]} == 1 )) && [[ ! -d ${entries[0]} ]]; then
    return 0
  fi
  PKG_COUNT=${#entries[@]}
  return 0
}

# Sets PROGRESS_PM. Never call in a command substitution: POS, the phase timer
# and the package counters must survive between frames. Print nothing — output
# here would land on the TTY between the cursor escape and the drawing.
install_progress() {
  local phase finished index total target
  local lo hi tau span t pos work gap n pkg_signal=0
  local -a state

  PROGRESS_PM=$POS
  [[ -f $STATE_FILE ]] || return 0

  mapfile -t state < <(jq -r '
    (.current_phase // "Starting installation"),
    ((.finished_at // 0) | tonumber? // 0 | floor),
    ((.current_index // -1) | tonumber? // -1 | floor),
    ((.total_phases // 0) | tonumber? // 0 | floor),
    (.target // "/mnt")
  ' "$STATE_FILE" 2>/dev/null)
  (( ${#state[@]} == 5 )) || return 0
  phase="${state[0]}" finished="${state[1]}"
  index="${state[2]}" total="${state[3]}" target="${state[4]}"
  [[ -n $target ]] || target="/mnt"

  if (( finished > 0 )) || [[ $phase == "Installation complete" ]]; then
    POS=1000
    PROGRESS_PM=1000
    return 0
  fi

  # Transitions are observed here rather than read from the state file, which
  # keeps wall clock out of the dashboard entirely.
  if [[ $phase != "$PHASE_NAME" ]]; then
    PHASE_NAME="$phase"
    PHASE_T0=$NOW
    PHASE_LO=$POS
    PKG_BASE=-1
    PKG_SEEN=0
    PKG_BONUS=0
    PKG_LAST_CHANGE=$NOW
  fi

  # Bands in per-mille, plus tau. The tail bands are wide relative to what they
  # cost on NVMe: run_system_finalizer can DKMS-build a driver and
  # finalize_limine_boot rebuilds every UKI, so they vary most on real hardware.
  # Over-allotting costs a step at a boundary; under-allotting costs a stall.
  case "$phase" in
    "Starting installation")      lo=10  hi=25   tau=5   ;;
    "Preparing live environment") lo=25  hi=50   tau=25  ;;
    "Preparing install target")   lo=50  hi=65   tau=20  ;;
    "Installing Arch + Omarchy")  lo=65  hi=700  tau=150 pkg_signal=1 ;;
    "Configuring hibernation")    lo=700 hi=720  tau=10  ;;
    "Configuring system")         lo=720 hi=850  tau=120 ;;
    "Staging provisioning")          lo=850 hi=855  tau=5   ;;
    "Finalizing Limine boot")     lo=855 hi=950  tau=90  ;;
    "Finalizing user")            lo=950 hi=978  tau=30  ;;
    "Configuring login")          lo=978 hi=988  tau=5   ;;
    "Configuring DNS resolver")   lo=988 hi=994  tau=5   ;;
    "Validating boot setup")      lo=994 hi=998  tau=5   ;;
    "Creating factory snapshot")  lo=998 hi=1000 tau=5   ;;
    *)
      # Unknown names used to inherit a 1->99 sweep. Derive an even band from
      # the published index instead; with no index, a bounded slice, so a
      # renamed phase degrades to one cell rather than a runaway.
      if (( total >= 2 && index >= 0 && index < total )); then
        lo=$(( 10 + 990 * index / total ))
        hi=$(( 10 + 990 * (index + 1) / total ))
      else
        lo=$PHASE_LO
        hi=$(( PHASE_LO + 30 ))
      fi
      tau=60
      ;;
  esac

  (( pkg_signal && PKG_TOTAL > 0 )) || pkg_signal=0
  (( pkg_signal )) && tau=$PKG_FLOOR_TAU

  t=$(( NOW - PHASE_T0 ))
  (( t < 0 )) && t=0
  span=$(( hi - lo ))
  (( span < 2 )) && span=2
  (( tau < 1 )) && tau=1

  pos=$(( lo + (span - 1) * t / (t + tau) ))

  if (( pkg_signal )); then
    PKG_DB_DIR="$target/var/lib/pacman/local"
    count_packages
    # Baseline on first sight: a target that already holds packages would
    # otherwise pin progress at the band ceiling for the whole phase.
    (( PKG_BASE < 0 )) && PKG_BASE=$PKG_COUNT
    n=$(( PKG_COUNT - PKG_BASE ))
    (( n < 0 )) && n=0
    # Running max: a reinstalled package briefly drops its version-stamped dir.
    if (( n > PKG_SEEN )); then
      PKG_SEEN=$n
      PKG_LAST_CHANGE=$NOW
    fi
    gap=$(( NOW - PKG_LAST_CHANGE ))
    (( gap < 0 )) && gap=0
    (( gap > PKG_BONUS_MAX_PM )) && gap=$PKG_BONUS_MAX_PM
    (( gap > PKG_BONUS )) && PKG_BONUS=$gap
    work=$(( lo + (span - 1 - PKG_TAIL_PM) * PKG_SEEN / PKG_TOTAL + PKG_BONUS ))
    (( work > pos )) && pos=$work
  fi

  (( pos > hi - 1 )) && pos=$(( hi - 1 ))
  (( pos > POS )) && POS=$pos
  (( POS > 1000 )) && POS=1000
  PROGRESS_PM=$POS
  return 0
}

# Keep the original loading messages on their own clock, so progress jumps
# cannot skip phrases and animation frame rate does not change their cadence.
current_whimsy() {
  printf '%s' "${whimsy[$(( (NOW - DASH_T0) / 4 % ${#whimsy[@]} ))]}"
}

# One tip per 8 seconds, in order. Use elapsed time, since rendering can stretch
# under pacstrap I/O, when the tips may be the most visible sign of activity.
current_tip() {
  printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}"
}

# Vertically center a block of the given height, echoing the row it starts on.
# Center both the progress icon and the final wordmark on the console.
centered_top_row() {
  local height="$1" rows top
  rows="$(term_size)"
  rows="${rows%% *}"
  [[ $rows =~ ^[0-9]+$ ]] || rows=24
  top=$(( (rows - height) / 2 ))
  (( top < 0 )) && top=0
  printf '%d' "$(( top + 1 ))"
}

measure_progress_layout() {
  local rows cols scale=1
  LAST_SIZE="$(term_size)"
  read -r rows cols <<<"$LAST_SIZE"
  read -r CELL_WIDTH CELL_HEIGHT < <(term_cell_size 2>/dev/null || printf '8 16\n')
  (( rows >= 34 && cols >= 49 )) && scale=2
  (( rows >= 50 && cols >= 64 )) && scale=3
  while (( scale > 1 && (15 * scale * CELL_HEIGHT + CELL_WIDTH) / (2 * CELL_WIDTH) > cols - 4 )); do
    scale=$((scale - 1))
  done
  build_icon "$scale"
  TOP_ROW="$(centered_top_row "$((ICON_HEIGHT + 6))")"
  ICON_LEFT=$(( (cols - ICON_WIDTH) / 2 + 1 ))
  (( ICON_LEFT < 1 )) && ICON_LEFT=1
  PROGRESS_TEXT_WIDTH=$((cols - 2))
  (( PROGRESS_TEXT_WIDTH > CONTENT_WIDTH )) && PROGRESS_TEXT_WIDTH=$CONTENT_WIDTH
  return 0
}

render_static() {
  measure_progress_layout
  printf '%s' "$CLEAR"
  render_icon "$DISPLAY_PM" 1
}

update_progress() {
  set_now
  install_progress
  local pm=$PROGRESS_PM
  [[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM
  # Only the child's successful exit can complete the drawing.
  (( pm > 999 )) && pm=999
  # POS is monotone by construction; this is a guard, not the mechanism.
  (( pm < LAST_PM )) && pm=$LAST_PM
  LAST_PM=$pm
}

render_dynamic() {
  local step message tip
  # The framebuffer can settle late in VMs. Rebuild at the new size, preserving
  # progress, instead of leaving the drawing at the old screen coordinates.
  [[ "$(term_size)" == "$LAST_SIZE" ]] || render_static
  # Ease phase jumps over several frames, with the percentage following the
  # same position as the trace. The measured progress still never goes back.
  step=$(( (LAST_PM - DISPLAY_PM + 3) / 4 ))
  DISPLAY_PM=$((DISPLAY_PM + step))
  render_icon "$DISPLAY_PM"

  message="$(truncate_for_width "$(current_whimsy)" "$PROGRESS_TEXT_WIDTH")"
  tip="$(truncate_for_width "Tip: $(current_tip)" "$PROGRESS_TEXT_WIDTH")"
  printf '%s%d;1H' "$CSI" "$((TOP_ROW + ICON_HEIGHT + 1))"
  center "${GREEN}$((DISPLAY_PM / 10))%${RESET}" "$PROGRESS_TEXT_WIDTH"
  center "${GREEN}${message}${RESET}" "$PROGRESS_TEXT_WIDTH"
  blank_line
  center "${DIM}${tip}${RESET}" "$PROGRESS_TEXT_WIDTH"
  printf '%s' "$CLEAR_TO_END"
}

install_duration() {
  local path line duration

  if [[ -f $STATE_FILE ]]; then
    duration="$(jq -r '
      ((.started_at // 0) | tonumber? // 0) as $started
      | ((.finished_at // 0) | tonumber? // 0) as $finished
      | if $started <= 0 or $finished <= 0 or $finished < $started then empty
        else (($finished - $started) | round) as $secs
          | if $secs >= 3600 then "\($secs / 3600 | floor)h \($secs % 3600 / 60 | floor)m \($secs % 60)s"
            else "\($secs / 60 | floor)m \($secs % 60)s"
            end
        end
    ' "$STATE_FILE" 2>/dev/null || true)"
    [[ -n $duration ]] && { printf '%s' "$duration"; return 0; }
  fi

  for path in /mnt/var/log/omarchy-install.log "$LOG_FILE"; do
    [[ -f $path ]] || continue
    for prefix in Total Omarchy; do
      line="$(grep "^$prefix:" "$path" 2>/dev/null | tail -n 1 || true)"
      [[ -n $line ]] || continue
      duration="${line#*:}"
      duration="${duration#${duration%%[![:space:]]*}}"
      [[ -n $duration ]] && { printf '%s' "$duration"; return 0; }
    done
  done
  return 0
}

render_finish() {
  local duration effect_canvas_width title="Installed Omarchy"
  duration="$(install_duration || true)"
  [[ -n $duration ]] && title+=" in ${duration}"
  # ttfx centers its text two columns right of plain centering. A canvas two
  # columns narrower aligns the animation with the title and fallback logo.
  effect_canvas_width="$(term_cols)"
  (( effect_canvas_width > 2 )) && effect_canvas_width=$((effect_canvas_width - 2))

  # Set before anything that can fail, so reboot_prompt still lands on the
  # button row even if the logo or the effect below never renders.
  FINISH_TOP_ROW="$(centered_top_row "$FINISH_HEIGHT")"

  {
    # Leave the wordmark area blank: laseretch reveals it for the first time.
    printf '%s%s%s%d;1H' "$HIDE_CURSOR" "$CLEAR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 1))"
    center "$title" "$CONTENT_WIDTH"
    blank_line
  } >"$TTY_PATH"

  if [[ -f $LOGO_PATH ]]; then
    # Reuse mode restores a saved cursor before moving up over its input. Save
    # this anchor so the effect occupies the reserved wordmark rows.
    printf '%s%d;1H\0337' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT))" >"$TTY_PATH"
    # Laseretch runs ~4s at 260fps, so the 8s cap is a stuck-process guard rather
    # than a trim. --xterm-colors keeps the whole effect in the 256-index palette
    # the way the greeter does, since the console reduces indexed codes well and
    # truecolor ones badly. It settles on indexed 2, the Tokyo Night green used
    # throughout the installer. The laser and sparks keep their own hot palette.
    timeout 8s ttfx -i "$LOGO_PATH" \
      --canvas-width "$effect_canvas_width" \
      --anchor-text c \
      --frame-rate 260 \
      --reuse-canvas \
      --xterm-colors \
      laseretch \
      --final-gradient-stops 2 \
      >"$TTY_PATH" 2>/dev/null || {
        dashboard_log "logo effect skipped (ttfx exited $?)"
        { printf '%s%d;1H' "$CSI" "$FINISH_TOP_ROW"; render_logo; } >"$TTY_PATH"
      }
  else
    { printf '%s%d;1H' "$CSI" "$FINISH_TOP_ROW"; render_logo; } >"$TTY_PATH"
  fi
  printf '%s%s%d;1H' "$SHOW_CURSOR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH"
}

reboot_prompt() {
  local prompt_pad

  # Nobody to confirm; the OMARCHY_UI_AUTO_REBOOT check at the call site still
  # applies, so setting both stops on the finish screen without a keyboard.
  interactive || return 0

  # gum paints its prompt row at the cursor and the button two rows under it, so
  # anchor on the row right below the title to put the button as close to it as
  # gum allows. FINISH_HEIGHT budgets for the same three rows.
  printf '%s%d;1H%s' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 2))" "$CLEAR_LINE" >"$TTY_PATH"

  prompt_pad=$(( ( $(term_cols) - 18 ) / 2 ))
  (( prompt_pad < 0 )) && prompt_pad=0
  gum confirm \
    --padding "0 0 0 $prompt_pad" \
    --show-help=false \
    --default \
    --affirmative "Reboot Now" \
    --negative "" \
    "" <"$TTY_PATH" >"$TTY_PATH" 2>&1
}

truncate_for_width() {
  local text="$1" width="$2"
  if (( ${#text} > width )); then
    printf '%s…' "${text:0:$((width - 1))}"
  else
    printf '%s' "$text"
  fi
}

render_failure_log_tail() {
  # The caller passes the rows it spent above this point that the constant below
  # does not know about, so the tail gives way rather than pushing the top of
  # the screen off a short console.
  local reserved="${1:-0}" rows lines width line tail_log label
  rows=$(term_size)
  rows=${rows%% *}
  lines=$((rows - LOGO_HEIGHT - 18 - reserved))
  (( lines < 6 )) && lines=6
  (( lines > 14 )) && lines=14
  width=$((CONTENT_WIDTH - 4))
  (( width < 40 )) && width=40

  tail_log="${OMARCHY_FAILURE_TAIL_LOG:-}"
  if [[ -z $tail_log ]]; then
    if [[ -f /mnt/var/log/omarchy-install.log ]]; then
      tail_log=/mnt/var/log/omarchy-install.log
    else
      tail_log="$LOG_FILE"
    fi
  fi
  [[ -f $tail_log ]] || return 0

  label="Last log lines:"
  [[ $tail_log == /mnt/* ]] && label="Last target log lines:"
  line_at "$CONTENT_WIDTH" 2 "${DIM}${label}${RESET}"
  printf '\n'
  tail -n "$lines" "$tail_log" 2>/dev/null | while IFS= read -r line; do
    line_at "$CONTENT_WIDTH" 2 "$(truncate_for_width "$line" "$width")"
    printf '\n'
  done
}

find_log_uploader() {
  local candidate
  for candidate in omarchy-upload-log /usr/local/bin/omarchy-upload-log /usr/bin/omarchy-upload-log /mnt/usr/bin/omarchy-upload-log; do
    if [[ $candidate == */* ]]; then
      [[ -x $candidate ]] && { printf '%s' "$candidate"; return 0; }
    elif command -v "$candidate" >/dev/null 2>&1; then
      command -v "$candidate"
      return 0
    fi
  done
  return 1
}

prompt_enter() {
  printf '\nPress Enter to continue…' >"$TTY_PATH"
  IFS= read -r _ <"$TTY_PATH" || true
}

upload_failure_log() {
  local uploader
  printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
  if uploader=$(find_log_uploader); then
    "$uploader" install >"$TTY_PATH" 2>&1 || true
  else
    {
      echo "No log uploader is available in this environment."
      echo "Install log: $LOG_FILE"
      [[ -f /mnt/var/log/omarchy-install.log ]] && echo "Target log: /mnt/var/log/omarchy-install.log"
    } >"$TTY_PATH"
  fi
  prompt_enter
}

view_failure_log() {
  printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
  if command -v less >/dev/null 2>&1 && [[ -f $LOG_FILE ]]; then
    less "$LOG_FILE" <"$TTY_PATH" >"$TTY_PATH" 2>&1 || true
  else
    tail -n 200 "$LOG_FILE" >"$TTY_PATH" 2>&1 || true
    prompt_enter
  fi
}

# A failure the medium explains is worth saying before the log tail, because
# the tail is where "invalid or corrupted package" already scrolled past
# without ever naming the USB stick it came off.
diagnose_media() {
  local logs=("$LOG_FILE")

  [[ -f /mnt/var/log/omarchy-install.log ]] && logs+=(/mnt/var/log/omarchy-install.log)
  [[ -n ${OMARCHY_FAILURE_TAIL_LOG:-} ]] && logs+=("$OMARCHY_FAILURE_TAIL_LOG")

  omarchy-install-diagnose-media "${logs[@]}" 2>/dev/null
}

# Rows the diagnosis occupies, its leading blank included.
media_diagnosis_rows() {
  local diagnosis="$1"

  if [[ -z $diagnosis ]]; then
    printf '0\n'
  else
    printf '%s\n' "$(( $(grep -c '' <<<"$diagnosis") + 1 ))"
  fi
}

render_media_diagnosis() {
  local diagnosis="$1" line first=1

  [[ -n $diagnosis ]] || return 0
  blank_line
  while IFS= read -r line; do
    # A blank line inside the diagnosis is deliberate: it separates what
    # happened from what to do about it, so it is drawn rather than skipped.
    if [[ -z $line ]]; then
      blank_line
      continue
    fi
    if (( first )); then
      center "${RED}$(truncate_for_width "$line" "$CONTENT_WIDTH")${RESET}" "$CONTENT_WIDTH"
      first=0
    else
      center "$(truncate_for_width "$line" "$CONTENT_WIDTH")" "$CONTENT_WIDTH"
    fi
  done <<<"$diagnosis"
}

render_failure() {
  local status="$1" state_summary="${2:-}" media_diagnosis="${3:-}" line
  printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
  blank_line
  render_logo
  blank_line
  center "${RED}Omarchy installation stopped${RESET}" "$CONTENT_WIDTH"
  center "Installer exited with status $status" "$CONTENT_WIDTH"

  render_media_diagnosis "$media_diagnosis"

  if [[ -n $state_summary ]]; then
    blank_line
    while IFS= read -r line; do
      [[ -n $line ]] || continue
      center "${DIM}$(truncate_for_width "$line" "$CONTENT_WIDTH")${RESET}" "$CONTENT_WIDTH"
    done <<<"$state_summary"
  fi

  blank_line
  render_failure_log_tail "$(media_diagnosis_rows "$media_diagnosis")"
  blank_line
  center "${DIM}Get help at https://omarchy.org/discord${RESET}" "$CONTENT_WIDTH"
  blank_line
}

failure_menu() {
  local choice uploader

  [[ ${OMARCHY_UI_FAILURE_ACTION:-} == "exit" ]] && return 0
  # The failure screen and log tail have already rendered; exit with the
  # installer's status instead of blocking on a menu nobody can answer.
  interactive || return 0

  while true; do
    uploader=""
    uploader=$(find_log_uploader || true)
    if [[ -n $uploader ]]; then
      choice=$(gum choose \
        --height 6 \
        --header "What would you like to do?" \
        "Upload log for support" \
        "View full log" \
        "Drop to shell" \
        "Reboot" \
        "Power off" \
        <"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell"
    else
      choice=$(gum choose \
        --height 5 \
        --header "What would you like to do?" \
        "View full log" \
        "Drop to shell" \
        "Reboot" \
        "Power off" \
        <"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell"
    fi

    case "$choice" in
      "Upload log for support")
        upload_failure_log
        render_failure "${failure_status:-1}" "${failure_summary:-}" "${failure_media_diagnosis:-}" >"$TTY_PATH" 2>/dev/null || true
        ;;
      "View full log")
        view_failure_log
        render_failure "${failure_status:-1}" "${failure_summary:-}" "${failure_media_diagnosis:-}" >"$TTY_PATH" 2>/dev/null || true
        ;;
      "Reboot")
        reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
        ;;
      "Power off")
        poweroff 2>/dev/null || systemctl poweroff 2>/dev/null || true
        ;;
      "Drop to shell"|"")
        return 0
        ;;
    esac
  done
}

sanitize_child_output() {
  # Archinstall is only a library subsystem in Omarchy's installer. Its own
  # post-install success/reboot banner can be buffered until process exit and
  # appear after an Omarchy failure, which makes the unified log contradictory.
  # Keep that third-party banner out of Omarchy's support log; the raw
  # archinstall log is still uploaded separately by omarchy-upload-log.
  sed -u \
    -e $'s/\x1b\\[[0-9;?]*[A-Za-z]//g' \
    -e '/^Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers$/d' \
    -e '/^Enabling service systemd-timesyncd$/d' \
    -e '/^Setting password for root$/d' \
    -e '/^Updating \/mnt\/etc\/fstab$/d' \
    -e '/^Syncing the system\.\.\.$/d' \
    -e '/^Installation completed without any errors\.$/d' \
    -e '/^Log files temporarily available at \/var\/log\/archinstall\.$/d' \
    -e '/^You may reboot when ready\.$/d'
}

launch_child() {
  mkdir -p "$(dirname "$LOG_FILE")"
  # Child is non-interactive. All visible UI belongs to this supervisor.
  PYTHONUNBUFFERED=1 setsid "${INSTALL_CMD[@]}" </dev/null > >(sanitize_child_output >>"$LOG_FILE") 2>&1 &
  child_pid=$!
  child_pgid=$child_pid
}

[[ -e $TTY_PATH ]] || exit 2
printf '%s' "$HIDE_CURSOR" >"$TTY_PATH"
render_static >"$TTY_PATH"
set_now
DASH_T0=$NOW
PHASE_T0=$NOW
PKG_LAST_CHANGE=$NOW
launch_child

frame=0
while jobs -pr | grep -qx "$child_pid"; do
  # Keep package counting/state reads at 2 Hz; animate the trace at 10 Hz.
  (( frame % 5 == 0 )) && update_progress
  render_dynamic >"$TTY_PATH" 2>/dev/null || true
  frame=$((frame + 1))
  sleep 0.1
done

set +e
wait "$child_pid"
status=$?
set -e
child_pid=""
child_pgid=""

dashboard_log "installer child exited with status $status"

if (( status == 0 )); then
  # Finish the trace even when a short-lived child exits between state polls.
  LAST_PM=1000
  PHASE_NAME="Installation complete"
  while (( DISPLAY_PM < 999 )); do
    render_dynamic >"$TTY_PATH" 2>/dev/null || true
    sleep 0.05
  done
  DISPLAY_PM=1000
  render_dynamic >"$TTY_PATH" 2>/dev/null || true
  sleep 0.4
fi

if (( status != 0 )); then
  failure_status="$status"
  failure_summary=""
  if [[ -f $STATE_FILE ]]; then
    failure_summary="$(jq -r '
      (.current_phase // empty | "last installer phase: \(.)"),
      ((.phases // []) | map(select(.status == "failed")) | last // empty
        | "failed phase: \(.name): \(.error)")
    ' "$STATE_FILE" 2>/dev/null || true)"
    if [[ -n $failure_summary ]]; then
      while IFS= read -r line; do
        [[ -n $line ]] && dashboard_log "$line"
      done <<<"$failure_summary"
    fi
  fi
  failure_media_diagnosis="$(diagnose_media || true)"
  render_failure "$failure_status" "$failure_summary" "$failure_media_diagnosis" >"$TTY_PATH" 2>/dev/null || true
  # Recorded after the screen is drawn, not before: the log tail on that screen
  # reads this same file, and the diagnosis printed twice reads as two findings.
  if [[ -n $failure_media_diagnosis ]]; then
    while IFS= read -r line; do
      [[ -n $line ]] && dashboard_log "$line"
    done <<<"$failure_media_diagnosis"
  fi
  failure_menu
  exit "$failure_status"
fi

# Deferred-provisioning installs finish silently and reboot on their own — the
# owner completes setup (and sees the send-off) at first boot, so the "Installed
# Omarchy in ..." celebration and the Reboot Now prompt belong to a direct
# install only.
if [[ ${OMARCHY_UI_DEFER_PROVISIONING:-} == "yes" ]]; then
  printf '%s' "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
  [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]] && exit 0
  reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
  exit 0
fi

set +e
render_finish
finish_render_status=$?
set -e
if (( finish_render_status != 0 )); then
  dashboard_log "finish screen render failed with status $finish_render_status; using fallback"
  {
    printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
    printf '\nFinished installing\n\n'
  } >"$TTY_PATH" 2>/dev/null || true
fi

if reboot_prompt; then
  if [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]]; then
    exit 0
  fi
  printf '%s' "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
  reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
fi

exit 0
