390 lines
10 KiB
Bash
Executable File
390 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, 82, 60),
|
|
('Installing base system', 8, 82, 60),
|
|
('Configuring protected boot', 82, 86, 8),
|
|
('Finalizing in chroot', 86, 98, 90),
|
|
('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
|