Files
omarchy-iso/configs/airootfs/usr/local/bin/omarchy-install-dashboard
T

258 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Render the live Omarchy install dashboard on tty1 while installer output is
# captured to logs. This is presentation only: it must never read input or gate
# install completion.
set -euo pipefail
LOG_FILE="${1:-/var/log/omarchy-install.log}"
STATE_FILE="${2:-/run/omarchy-install/state.json}"
STATE_DIR="$(dirname "$STATE_FILE")"
STOP_FILE="$STATE_DIR/dashboard.stop"
PID_FILE="$STATE_DIR/dashboard.pid"
LOGO_PATH="${OMARCHY_PATH:-/usr/share/omarchy}/logo.txt"
TTY_PATH="${OMARCHY_DASHBOARD_TTY:-/dev/tty}"
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"
# Use the 16-color ANSI palette configured by .automated_script.sh. The Linux
# VT does not reliably map 256-color indexes the same way terminal emulators do.
GREEN="${CSI}32m"
DARK="${CSI}90m"
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
rm -f "$PID_FILE" 2>/dev/null || true
}
trap cleanup EXIT
trap 'cleanup; exit 0' INT 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
# Internal weighting only. Keep estimates conservative but close to observed
# ISO installs so the bar does not sit at 12% then jump to 90%.
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),
('Finishing', 99, 100, 3),
]
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
}
install_finished() {
[[ -f $STATE_FILE ]] || return 1
python - "$STATE_FILE" <<'PY' 2>/dev/null
import json, sys
try:
state = json.load(open(sys.argv[1]))
except Exception:
raise SystemExit(1)
raise SystemExit(0 if state.get('finished_at') else 1)
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 2>/dev/null; then
blank_line
return
fi
# If animation fails or times out, clear any partial frame before fallback.
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"
}
if [[ ! -e $TTY_PATH ]]; then
exit 0
fi
mkdir -p "$STATE_DIR"
echo "$$" >"$PID_FILE"
printf '%s' "$HIDE_CURSOR" >"$TTY_PATH"
render_static >"$TTY_PATH"
frame=0
while true; do
[[ -e $STOP_FILE ]] && break
render_dynamic "$frame" >"$TTY_PATH"
if install_finished || [[ -e $STOP_FILE ]]; then
sleep 1
break
fi
frame=$((frame + 1))
sleep 0.5
done