Drive installs through the common ISO pipeline, carry install intent in configurator JSON, and finalize Limine boot setup from the ISO.
394 lines
10 KiB
Bash
Executable File
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
|