Files
omarchy-iso/configs/airootfs/usr/local/bin/omarchy-install-dashboard
T
2026-07-23 16:15:29 -07:00

596 lines
17 KiB
Bash
Executable File

#!/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"
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=""
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"
)
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<count; i++)); do out+="$char"; done
printf '%s' "$out"
}
logo_width() {
if [[ -f $LOGO_PATH ]]; then
awk '{ 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"
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' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET"
}
install_progress() {
local phase finished phase_started elapsed span active pct
local start=1 end=99 expected=90
local -a state
[[ -f $STATE_FILE ]] || { printf '1'; return; }
mapfile -t state < <(jq -r '
(.current_phase // "Starting installation"),
((.finished_at // 0) | tonumber? // 0 | floor),
((.phase_started_at // .started_at // 0) | tonumber? // 0 | floor)
' "$STATE_FILE" 2>/dev/null)
(( ${#state[@]} == 3 )) || { printf '1'; return; }
phase="${state[0]}" finished="${state[1]}" phase_started="${state[2]}"
if (( finished > 0 )) || [[ $phase == "Installation complete" ]]; then
printf '100'
return
fi
# Phase weights: start%, end%, expected seconds in phase.
case "$phase" in
"Starting installation") start=1 end=2 expected=2 ;;
"Preparing live environment") start=2 end=6 expected=5 ;;
"Verifying protected mounts") start=6 end=8 expected=3 ;;
"Installing Arch + Omarchy") start=8 end=86 expected=95 ;;
"Installing base system") start=8 end=84 expected=85 ;;
"Configuring protected boot") start=84 end=86 expected=8 ;;
"Configuring hibernation") start=86 end=88 expected=5 ;;
"Configuring system") start=88 end=93 expected=10 ;;
"Finalizing Limine boot") start=93 end=96 expected=8 ;;
"Finalizing user") start=96 end=98 expected=10 ;;
"Finalizing in chroot") start=88 end=98 expected=10 ;;
"Configuring login") start=98 end=99 expected=3 ;;
"Configuring DNS resolver") start=99 end=100 expected=2 ;;
"Validating protected boot setup") start=99 end=100 expected=5 ;;
"Validating boot setup") start=99 end=100 expected=5 ;;
esac
(( phase_started > 0 )) || phase_started=$EPOCHSECONDS
elapsed=$(( EPOCHSECONDS - phase_started ))
(( elapsed < 0 )) && elapsed=0
span=$(( end - start ))
(( span < 0 )) && span=0
(( expected < 1 )) && expected=1
active=$(( elapsed * span / expected ))
(( active > span - 1 )) && active=$(( span - 1 ))
(( active < 0 )) && active=0
pct=$(( start + active ))
(( pct > 100 )) && pct=100
printf '%s' "$pct"
}
current_whimsy() {
local frame="$1" pct="$2"
printf '%s' "${whimsy[$(( (pct / 8 + frame / 8) % ${#whimsy[@]} ))]}"
}
render_static() {
printf '%s' "$CLEAR"
blank_line
render_logo
blank_line
}
render_dynamic() {
local frame="$1" pct
pct="$(install_progress)"
[[ $pct =~ ^[0-9]+$ ]] || pct=$LAST_PCT
(( pct > 100 )) && pct=100
(( pct < LAST_PCT )) && pct=$LAST_PCT
LAST_PCT=$pct
printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW"
center "Installing Omarchy" "$CONTENT_WIDTH"
blank_line
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) ""
progress_bar "$pct" 34
printf '\n'
blank_line
center "${GREEN}$(current_whimsy "$frame" "$pct")${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[@]}" </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"
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="$(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