Files
omarchy-iso/configs/airootfs/usr/local/bin/omarchy-install-dashboard
T
Ryan Hughes c50d0bc299 Unify ISO install orchestration
Drive installs through the common ISO pipeline, carry install intent in configurator JSON, and finalize Limine boot setup from the ISO.
2026-05-25 12:50:31 -04:00

394 lines
10 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"
DARK="${CSI}90m"
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<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 %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),
('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
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
}
launch_child() {
mkdir -p "$(dirname "$LOG_FILE")"
# Child is non-interactive. All visible UI belongs to this supervisor.
setsid "${INSTALL_CMD[@]}" </dev/null > >(sed -u 's/\x1b\[[0-9;?]*[A-Za-z]//g' >>"$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
if [[ -f $STATE_FILE ]]; then
state_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 ${state_summary:-} ]]; then
while IFS= read -r line; do
[[ -n $line ]] && dashboard_log "$line"
done <<<"$state_summary"
fi
fi
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
exit "$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