#!/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 -- " >&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" 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" WHITE="${CSI}37m" RED="${CSI}31m" DARK="${CSI}90m" # 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="" 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#* }"; } repeat() { local char="$1" count="$2" out="" i for ((i=0; i 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" DYNAMIC_ROW=$((LOGO_HEIGHT + 3)) LAST_PM=10 # ── Install progress ───────────────────────────────────────────────────────── # # Position = max(floor, work), both non-decreasing, in per-mille of the bar. # # 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 # /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 the bar may run ahead of the count PKG_TAIL_PM=40 # band held back so a stalled bar 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 /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 } progress_bar() { local pm="$1" width="${2:-34}" filled empty (( pm < 0 )) && pm=0 (( pm > 1000 )) && pm=1000 filled=$(( pm * width / 1000 )) empty=$(( width - filled )) printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET" } # 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 bar. 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 ;; "Finalizing Limine boot") lo=850 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=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 the bar 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 } # One tip per 8 seconds, in order. On elapsed time, not a frame counter: a frame # is 0.5s of sleep plus a render, which stretches under pacstrap I/O — on the # machine where the tips, not the bar, are carrying liveness. current_tip() { printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}" } render_static() { printf '%s' "$CLEAR" blank_line render_logo blank_line } render_dynamic() { local pm set_now install_progress pm=$PROGRESS_PM [[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM (( pm > 1000 )) && pm=1000 # POS is monotone by construction; this is a guard, not the mechanism. (( pm < LAST_PM )) && pm=$LAST_PM LAST_PM=$pm printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW" center "Installing Omarchy" "$CONTENT_WIDTH" blank_line line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) "" progress_bar "$pm" 34 printf '\n' blank_line center "${DIM}Tip:${RESET} ${GREEN}$(current_tip)${RESET}" "$CONTENT_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 duration="$(install_duration || true)" duration="${duration:-Complete}" effect_canvas_width="$(term_cols)" (( effect_canvas_width > 1 )) && effect_canvas_width=$((effect_canvas_width - 1)) { printf '%s%s' "$SHOW_CURSOR" "$CLEAR" blank_line render_logo blank_line center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH" blank_line } >"$TTY_PATH" if command -v tte >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 && [[ -f $LOGO_PATH ]]; then # TTE reuse mode restores a saved cursor before moving up over its input. # Save this anchor so the effect redraws the same logo rows shown above. printf '%s%d;1H\0337' "$CSI" "$((LOGO_HEIGHT + 2))" >"$TTY_PATH" timeout 8s tte -i "$LOGO_PATH" \ --canvas-width "$effect_canvas_width" \ --anchor-text c \ --frame-rate 260 \ --reuse-canvas \ laseretch \ >"$TTY_PATH" 2>/dev/null || true printf '%s%d;1H' "$CSI" "$((LOGO_HEIGHT + 8))" >"$TTY_PATH" fi } reboot_prompt() { local prompt_pad # Position the cursor one blank line below the title so the button sits right there. printf '%s%d;1H%s' "$CSI" "$((LOGO_HEIGHT + 5))" "$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() { local rows lines width line tail_log label rows=$(term_size) rows=${rows%% *} lines=$((rows - LOGO_HEIGHT - 18)) (( 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 } render_failure() { local status="$1" state_summary="${2:-}" 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" 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 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 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:-}" >"$TTY_PATH" 2>/dev/null || true ;; "View full log") view_failure_log render_failure "${failure_status:-1}" "${failure_summary:-}" >"$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[@]}" >(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 while jobs -pr | grep -qx "$child_pid"; do render_dynamic >"$TTY_PATH" 2>/dev/null || true sleep 0.5 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 # finished_at lands microseconds before the child exits, so the 0.5s poll # rarely renders a full bar before the finish screen replaces it. 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 render_failure "$failure_status" "$failure_summary" >"$TTY_PATH" 2>/dev/null || true failure_menu exit "$failure_status" 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