#!/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" 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="" whimsy=( "making Linux feel like home" "installing the good defaults" "putting the system in order" "setting up a civilized desktop" "clearing the path to Hyprland" "preparing the command line sanctuary" "teaching pacman the house rules" "laying down the Tokyo Night carpet" "arranging the sharp tools safely" "keeping the boot path clean" "giving Limine the keys" "making snapshots worth having" "saving future you a few hours" "choosing boring where boring wins" "leaving room for your dotfiles" "tightening the last few screws" ) 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_PCT=1 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 pct="$1" width="${2:-34}" filled empty (( pct < 0 )) && pct=0 (( pct > 100 )) && pct=100 filled=$(( pct * width / 100 )) empty=$(( width - filled )) printf '%s%s%s%s%s %3d%%' "$GREEN" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET" "$pct" } install_progress() { [[ -f $STATE_FILE ]] || { printf '1'; return; } python - "$STATE_FILE" <<'PY' 2>/dev/null || printf '1' import json, sys, time path = sys.argv[1] try: state = json.load(open(path)) except Exception: print(1, end='') raise SystemExit phase = state.get('current_phase') or 'Starting installation' if state.get('finished_at') or phase == 'Installation complete': print(100, end='') raise SystemExit weights = [ ('Starting installation', 1, 2, 2), ('Preparing live environment', 2, 6, 5), ('Verifying protected mounts', 6, 8, 3), ('Installing Arch + Omarchy', 8, 86, 95), ('Installing base system', 8, 84, 85), ('Configuring protected boot', 84, 86, 8), ('Configuring hibernation', 86, 88, 5), ('Configuring system', 88, 93, 10), ('Finalizing Limine boot', 93, 96, 8), ('Finalizing user', 96, 98, 10), ('Finalizing in chroot', 88, 98, 10), ('Configuring login', 98, 99, 3), ('Configuring DNS resolver', 99, 100, 2), ('Validating protected boot setup', 99, 100, 5), ('Validating boot setup', 99, 100, 5), ] start, end, expected = 1, 99, 90 for name, s, e, seconds in weights: if phase == name: start, end, expected = s, e, seconds break phase_started = state.get('phase_started_at') or state.get('started_at') or time.time() elapsed = max(0, time.time() - float(phase_started)) span = max(0, end - start) active = min(max(span - 1, 0), int((elapsed / max(expected, 1)) * span)) print(min(100, start + active), end='') PY } current_whimsy() { local frame="$1" pct="$2" printf '%s' "${whimsy[$(( (pct / 8 + frame / 8) % ${#whimsy[@]} ))]}" } render_static() { printf '%s' "$CLEAR" blank_line if command -v tte >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 && [[ -f $LOGO_PATH ]]; then if timeout 8s tte -i "$LOGO_PATH" --canvas-width 0 --anchor-text c --frame-rate 920 laseretch >"$TTY_PATH" 2>/dev/null; then blank_line return fi printf '%s' "$CLEAR" blank_line fi render_logo blank_line } render_dynamic() { local frame="$1" pct spin pct="$(install_progress)" [[ $pct =~ ^[0-9]+$ ]] || pct=$LAST_PCT (( pct > 100 )) && pct=100 (( pct < LAST_PCT )) && pct=$LAST_PCT LAST_PCT=$pct spin=("◐" "◓" "◑" "◒") printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW" center "Installing Omarchy" "$CONTENT_WIDTH" blank_line line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 40) / 2 )) "" progress_bar "$pct" 34 printf '\n' blank_line center "${GREEN}${spin[$((frame % 4))]} $(current_whimsy "$frame" "$pct") ${RESET}" "$CONTENT_WIDTH" blank_line center "${DIM}Logs are being saved for support${RESET}" "$CONTENT_WIDTH" printf '%s' "$CLEAR_TO_END" } install_duration() { local path line duration if [[ -f $STATE_FILE ]]; then duration="$(python - "$STATE_FILE" <<'PY' 2>/dev/null || true import json, sys try: state = json.load(open(sys.argv[1])) started = float(state.get("started_at") or 0) finished = float(state.get("finished_at") or 0) except Exception: raise SystemExit(0) if started <= 0 or finished <= 0 or finished < started: raise SystemExit(0) seconds = int(round(finished - started)) hours, remainder = divmod(seconds, 3600) minutes, seconds = divmod(remainder, 60) if hours: print(f"{hours}h {minutes}m {seconds}s", end="") else: print(f"{minutes}m {seconds}s", end="") PY )" [[ -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 message cols cols="$(term_cols)" printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" if command -v tte >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 && [[ -f $LOGO_PATH ]]; then timeout 8s tte -i "$LOGO_PATH" --canvas-width 0 --anchor-text c --frame-rate 920 laseretch >"$TTY_PATH" 2>/dev/null || true else render_logo >"$TTY_PATH" fi duration="$(install_duration || true)" message="Finished installing" [[ -n $duration ]] && message="Installed in $duration" { blank_line center "$message" "$cols" blank_line } >"$TTY_PATH" } reboot_prompt() { local prompt_pad 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" launch_child frame=0 while jobs -pr | grep -qx "$child_pid"; do render_dynamic "$frame" >"$TTY_PATH" 2>/dev/null || true frame=$((frame + 1)) 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 failure_status="$status" failure_summary="" if [[ -f $STATE_FILE ]]; then failure_summary="$(python - "$STATE_FILE" <<'PY' 2>/dev/null || true import json, sys try: state = json.load(open(sys.argv[1])) except Exception: raise SystemExit(0) phase = state.get("current_phase") if phase: print(f"last installer phase: {phase}") for item in reversed(state.get("phases", [])): if item.get("status") == "failed": print(f"failed phase: {item.get('name')}: {item.get('error')}") break PY )" 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 [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]]; then exit 0 fi if reboot_prompt; then printf '%s' "$CLEAR" >"$TTY_PATH" 2>/dev/null || true reboot 2>/dev/null || systemctl reboot 2>/dev/null || true fi exit 0