installer: show dashboard during ISO install

This commit is contained in:
Ryan Hughes committed 2026-05-22 17:34:01 -04:00
1 parent de0f013b5e
commit 337ba2e590
5 files changed
+628 -6

No files matched your search

+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env bash
# Preview Omarchy installer UI concepts without building or booting the ISO.
# Usage: bin/omarchy-install-ui-preview [dashboard|matrix|forge|logs]
set -euo pipefail
concept="${1:-dashboard}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
LOGO_PATH="${OMARCHY_PREVIEW_LOGO:-}"
if [[ -z $LOGO_PATH ]]; then
for candidate in \
"$SCRIPT_DIR/../../omarchy-installer/logo.txt" \
"$SCRIPT_DIR/../configs/airootfs/usr/share/omarchy/logo.txt" \
"/usr/share/omarchy/logo.txt"; do
if [[ -f $candidate ]]; then
LOGO_PATH="$candidate"
break
fi
done
fi
CSI=$'\033['
RESET="${CSI}0m"
BOLD="${CSI}1m"
DIM="${CSI}2m"
HIDE_CURSOR="${CSI}?25l"
SHOW_CURSOR="${CSI}?25h"
CLEAR="${CSI}2J${CSI}H"
HOME="${CSI}H"
CLEAR_LINE="${CSI}2K"
CLEAR_TO_END="${CSI}J"
GREEN="${CSI}38;5;150m"
BLUE="${CSI}38;5;111m"
PURPLE="${CSI}38;5;141m"
YELLOW="${CSI}38;5;221m"
RED="${CSI}38;5;203m"
GRAY="${CSI}38;5;245m"
DARK="${CSI}38;5;238m"
logo_width() {
if [[ -n ${LOGO_PATH:-} && -f $LOGO_PATH ]]; then
awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH"
else
printf '78\n'
fi
}
LOGO_WIDTH="$(logo_width)"
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=78
CONTENT_WIDTH="${OMARCHY_PREVIEW_WIDTH:-$LOGO_WIDTH}"
(( CONTENT_WIDTH > 0 )) || CONTENT_WIDTH="$LOGO_WIDTH"
phases=(
"Preparing live environment"
"Partitioning target disk"
"Installing base system"
"Installing Omarchy packages"
"Building Limine UKI"
"Configuring Snapper"
"Validating boot setup"
"Finishing"
)
logs=(
"pacman-key: populating archlinux keyring"
"mount: /mnt ready"
"pacstrap: installing linux btrfs-progs limine"
"pacman: installing omarchy-installer"
"useradd: creating ryan with /etc/skel defaults"
"limine-update: generating omarchy_linux.efi"
"snapper: creating root config"
"efibootmgr: registered Limine"
"validation: UKI present, fstab present, boot entry present"
)
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"
}
trap cleanup EXIT INT TERM
term_cols() {
# Match install/helpers/presentation.sh: read the real tty dimensions even
# if stdout is redirected through a pipe/process substitution.
if [[ -e /dev/tty ]]; then
local size
size=$(stty size 2>/dev/null </dev/tty || true)
if [[ -n $size ]]; then
printf '%s\n' "${size#* }"
return
fi
fi
tput cols 2>/dev/null || echo 100
}
term_rows() {
if [[ -e /dev/tty ]]; then
local size
size=$(stty size 2>/dev/null </dev/tty || true)
if [[ -n $size ]]; then
printf '%s\n' "${size%% *}"
return
fi
fi
tput lines 2>/dev/null || echo 32
}
repeat() {
local char="$1" count="$2"
local out=""
for ((i=0; i<count; i++)); do out+="$char"; done
printf '%s' "$out"
}
left_padding() {
local width="${1:-$CONTENT_WIDTH}"
local cols
cols=$(term_cols)
local pad=$(( (cols - width) / 2 ))
(( pad < 0 )) && pad=0
printf '%*s' "$pad" ''
}
logo_padding() {
# Exact equivalent of the installer's:
# PADDING_LEFT=$(((TERM_WIDTH - LOGO_WIDTH) / 2))
left_padding "$LOGO_WIDTH"
}
visible_len() {
# ANSI color/control codes should not affect centering.
local text="$1"
text="$(printf '%b' "$text" | sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g')"
printf '%s' "${#text}"
}
center() {
# Match the real installer's gum-style layout: fixed content width shifted by
# a computed left gutter, rather than trying to center against the full tty.
local text="$1" width="${2:-$CONTENT_WIDTH}"
local len
len="$(visible_len "$text")"
local 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"
}
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' "$*"
}
blank_line() {
printf '\r%s\n' "$CLEAR_LINE"
}
progress_bar() {
local pct="$1" width="${2:-36}"
local filled=$(( pct * width / 100 ))
local empty=$(( width - filled ))
printf '%s%s%s%s%s %3d%%' "$GREEN" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET" "$pct"
}
logo() {
if [[ -n ${LOGO_PATH:-} && -f $LOGO_PATH ]]; then
cat "$LOGO_PATH"
else
printf 'Omarchy\n'
fi
}
render_logo() {
# Draw the logo like clear_logo does: one left gutter derived from the max
# logo width, then the logo text exactly as stored in logo.txt.
local pad
pad="$(logo_padding)"
while IFS= read -r line; do
printf '\r%s%s%b%s%b\n' "$CLEAR_LINE" "$pad" "$GREEN" "$line" "$RESET"
done < <(logo)
}
current_log() {
local i="$1"
printf '%s' "${logs[$(( i % ${#logs[@]} ))]}"
}
current_whimsy() {
local frame="$1" phase_idx="${2:-0}"
# Change slower than the spinner so it feels intentional, not jittery.
printf '%s' "${whimsy[$(( (phase_idx * 3 + frame / 16) % ${#whimsy[@]} ))]}"
}
render_dashboard() {
local frame="$1" phase_idx=$(( frame / 12 ))
(( phase_idx >= ${#phases[@]} )) && phase_idx=$((${#phases[@]} - 1))
local pct=$(( frame * 100 / 95 ))
(( pct > 100 )) && pct=100
printf '%s' "$HOME"
blank_line
render_logo
blank_line
center "Phase $((phase_idx + 1))/${#phases[@]} ${phases[$phase_idx]}" "$CONTENT_WIDTH"
blank_line
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 40) / 2 )) ""
progress_bar "$pct" 34
printf '\n'
blank_line
center "Now: $(current_log "$frame")" "$CONTENT_WIDTH"
blank_line
local spin=("◐" "◓" "◑" "◒")
center "${GREEN}${spin[$((frame % 4))]} $(current_whimsy "$frame" "$phase_idx") ${RESET}" "$CONTENT_WIDTH"
blank_line
center "${DIM}Press Ctrl-C to quit${RESET}" "$CONTENT_WIDTH"
}
render_matrix() {
local frame="$1" rows cols
rows=$(term_rows); cols=$(term_cols)
(( rows > 34 )) && rows=34
(( cols > 110 )) && cols=110
local words=(pacstrap limine btrfs snapper uki kernel hyprland omarchy archlinux)
printf '%s' "$HOME"
printf '%s%s' "$DIM" "$PURPLE"
center "TOKYO NIGHT PACKAGE RAIN" "$cols"
printf '%s' "$RESET"
echo
for ((r=2; r<rows-7; r++)); do
line=""
for ((c=0; c<cols; c+=10)); do
if (( (r + c + frame) % 7 == 0 )); then
word="${words[$(((r + c + frame) % ${#words[@]}))]}"
line+="${GREEN}${word:0:9}${RESET}"
elif (( (r * c + frame) % 19 == 0 )); then
line+="${BLUE}│${RESET} "
else
line+=" "
fi
done
printf '%b\n' "${line:0:cols}"
done
local phase_idx=$(( frame / 10 ))
(( phase_idx >= ${#phases[@]} )) && phase_idx=$((${#phases[@]} - 1))
echo
center "${BOLD}${GREEN}${phases[$phase_idx]}${RESET}" "$cols"
center "${GRAY}$(current_log "$frame")${RESET}" "$cols"
}
render_forge() {
local frame="$1" phase_idx=$(( frame / 12 ))
(( phase_idx >= ${#phases[@]} )) && phase_idx=$((${#phases[@]} - 1))
local sparks=("·" "∙" "*" "✦" "✧" "⋆")
local hammer=("⚒" " " "⚒" " ")
local pct=$(( frame * 100 / 95 ))
(( pct > 100 )) && pct=100
printf '%s' "$HOME"
printf '%s' "$YELLOW"
center "THE OMARCHY FORGE" 80
printf '%s\n\n' "$RESET"
center " ${sparks[$((frame % 6))]} ${sparks[$(((frame+2) % 6))]} ${sparks[$(((frame+4) % 6))]}" 80
center " ${YELLOW}${hammer[$((frame % 4))]}${RESET}" 80
center " _____________" 80
center " / \\" 80
center " ____/ ARCH ROOTFS \____" 80
center " / \_______________/ \\" 80
center " / BTRFS │ LIMINE │ UKI \\" 80
center " \_________│__________│______/" 80
echo
center "${GREEN}${phases[$phase_idx]}${RESET}" 80
echo
line_at 80 22 ""
progress_bar "$pct" 36
echo -e "\n"
center "${GRAY}$(current_log "$frame")${RESET}" 80
}
render_logs() {
local frame="$1" phase_idx=$(( frame / 12 ))
(( phase_idx >= ${#phases[@]} )) && phase_idx=$((${#phases[@]} - 1))
local pct=$(( frame * 100 / 95 ))
(( pct > 100 )) && pct=100
printf '%s' "$HOME"
render_logo
blank_line
line_at "$CONTENT_WIDTH" 0 "${BOLD}Omarchy Install${RESET}\n"
line_at "$CONTENT_WIDTH" 0 ""
progress_bar "$pct" 50
printf '\n'
blank_line
line_at "$CONTENT_WIDTH" 0 "${GREEN}${phases[$phase_idx]}${RESET}\n"
blank_line
local start=$(( frame - 12 ))
(( start < 0 )) && start=0
for ((i=start; i<=frame; i++)); do
line_at "$CONTENT_WIDTH" 2 "${GRAY}→${RESET} [$(printf '%02d:%02d' "$((i / 3))" "$(((i * 7) % 60))")] $(current_log "$i")\n"
done
blank_line
line_at "$CONTENT_WIDTH" 0 "${DIM}This is the boring/debuggable mode: curated progress plus log tail.${RESET}\n"
}
case "$concept" in
dashboard|matrix|forge|logs) ;;
-h|--help|help)
echo "Usage: $0 [dashboard|matrix|forge|logs]"
exit 0
;;
*)
echo "Unknown concept: $concept" >&2
echo "Usage: $0 [dashboard|matrix|forge|logs]" >&2
exit 2
;;
esac
printf '%s%s' "$HIDE_CURSOR" "$CLEAR"
for ((frame=0; frame<=96; frame++)); do
"render_$concept" "$frame"
printf '%s' "$CLEAR_TO_END"
sleep 0.08
done
+24 -2
View File
@@ -43,11 +43,33 @@ export FORCE_COLOR=1
cd /root
./configurator
# Keep the actual install screen calm: the dashboard owns /dev/tty while the
# noisy installer stream is captured to the support log.
/usr/local/bin/omarchy-install-dashboard "$OMARCHY_INSTALL_LOG_FILE" /run/omarchy-install/state.json /mnt/var/log/omarchy-install.log >/dev/tty 2>&1 &
dashboard_pid=$!
export OMARCHY_INSTALL_DASHBOARD_PID="$dashboard_pid"
stop_install_dashboard() {
if [[ -n ${dashboard_pid:-} ]]; then
kill "$dashboard_pid" 2>/dev/null || true
wait "$dashboard_pid" 2>/dev/null || true
unset dashboard_pid
fi
printf "\033[?25h" >/dev/tty
}
# Absolute paths because omarchy-iso-install cd's into /usr/share/omarchy-iso
# before exec'ing python; relative paths would resolve against the wrong dir.
exec /usr/local/bin/omarchy-iso-install \
set +e
/usr/local/bin/omarchy-iso-install \
--config /root/user_configuration.json \
--creds /root/user_credentials.json \
--full-name-file /root/user_full_name.txt \
--email-file /root/user_email_address.txt \
--encrypt-file /root/user_encrypt_installation.txt
--encrypt-file /root/user_encrypt_installation.txt \
> >(sed -u 's/\x1b\[[0-9;]*[a-zA-Z]//g' >>"$OMARCHY_INSTALL_LOG_FILE") 2>&1
install_status=$?
set -e
stop_install_dashboard
exit "$install_status"
+222
View File
@@ -0,0 +1,222 @@
#!/usr/bin/env bash
# Render the live Omarchy install dashboard on tty1 while installer output is
# captured to logs. Intentionally has no external UI dependencies.
set -euo pipefail
LOG_FILE="${1:-/var/log/omarchy-install.log}"
STATE_FILE="${2:-/run/omarchy-install/state.json}"
TARGET_LOG_FILE="${3:-/mnt/var/log/omarchy-install.log}"
LOGO_PATH="${OMARCHY_PATH:-/usr/share/omarchy}/logo.txt"
CSI=$'\033['
RESET="${CSI}0m"
BOLD="${CSI}1m"
DIM="${CSI}2m"
HIDE_CURSOR="${CSI}?25l"
SHOW_CURSOR="${CSI}?25h"
CLEAR="${CSI}2J${CSI}H"
HOME="${CSI}H"
CLEAR_LINE="${CSI}2K"
CLEAR_TO_END="${CSI}J"
GREEN="${CSI}38;5;150m"
GRAY="${CSI}38;5;245m"
DARK="${CSI}38;5;238m"
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" >/dev/tty 2>/dev/null || true
}
trap cleanup EXIT INT TERM
term_size() {
stty size 2>/dev/null </dev/tty || printf '24 80\n'
}
term_cols() {
local size
size="$(term_size)"
printf '%s\n' "${size#* }"
}
repeat() {
local char="$1" count="$2" out=""
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_WIDTH="$(logo_width)"
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
CONTENT_WIDTH="$LOGO_WIDTH"
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"
}
state_value() {
local key="$1" fallback="$2"
[[ -f $STATE_FILE ]] || { printf '%s' "$fallback"; return; }
python - "$STATE_FILE" "$key" "$fallback" <<'PY' 2>/dev/null || true
import json, sys
path, key, fallback = sys.argv[1:4]
try:
with open(path) as f:
state = json.load(f)
except Exception:
print(fallback, end='')
raise SystemExit
if key == 'phase':
print(state.get('current_phase') or 'Starting installation', end='')
elif key == 'index':
print(state.get('current_index') or 0, end='')
elif key == 'total':
print(state.get('total_phases') or 1, end='')
elif key == 'done':
print(len([p for p in state.get('phases', []) if p.get('status') == 'ok']), end='')
else:
print(fallback, end='')
PY
}
last_log_line() {
local file line files
if [[ -s $TARGET_LOG_FILE && $TARGET_LOG_FILE -nt $LOG_FILE ]]; then
files=("$TARGET_LOG_FILE" "$LOG_FILE")
else
files=("$LOG_FILE" "$TARGET_LOG_FILE")
fi
for file in "${files[@]}"; do
[[ -s $file ]] || continue
line="$(grep -avE '^(\s*|===|\[.*\] (Starting|Completed):)' "$file" 2>/dev/null | tail -n 1 || true)"
if [[ -n $line ]]; then
line="${line//$'\r'/}"
line="$(printf '%s' "$line" | sed -E 's/\x1b\[[0-9;?]*[A-Za-z]//g')"
printf '%s' "${line:0:$((CONTENT_WIDTH - 8))}"
return
fi
done
printf 'starting installer'
}
current_whimsy() {
local frame="$1" index="$2"
printf '%s' "${whimsy[$(( (index * 3 + frame / 8) % ${#whimsy[@]} ))]}"
}
render() {
local frame="$1" phase index total done pct spin log_line
phase="$(state_value phase 'Starting installation')"
index="$(state_value index 0)"
total="$(state_value total 1)"
done="$(state_value done 0)"
(( total < 1 )) && total=1
pct=$(( (done * 100) / total ))
# Give the active phase a little movement without pretending exact progress.
if (( done < total )); then
pct=$(( pct + (frame % 8) * (100 / total) / 8 ))
fi
(( pct > 100 )) && pct=100
spin=("◐" "◓" "◑" "◒")
log_line="$(last_log_line)"
printf '%s' "$HOME"
blank_line
render_logo
blank_line
center "Phase $((index + 1))/$total $phase" "$CONTENT_WIDTH"
blank_line
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 40) / 2 )) ""
progress_bar "$pct" 34
printf '\n'
blank_line
center "Now: $log_line" "$CONTENT_WIDTH"
blank_line
center "${GREEN}${spin[$((frame % 4))]} $(current_whimsy "$frame" "$index") ${RESET}" "$CONTENT_WIDTH"
blank_line
center "${DIM}Logs are being saved for support${RESET}" "$CONTENT_WIDTH"
printf '%s' "$CLEAR_TO_END"
}
printf '%s%s' "$HIDE_CURSOR" "$CLEAR" >/dev/tty
frame=0
while true; do
render "$frame" >/dev/tty
frame=$((frame + 1))
sleep 0.5
done
@@ -23,10 +23,21 @@ class PhaseError(Exception):
def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None:
ctx.state_dir.mkdir(parents=True, exist_ok=True)
state_path = ctx.state_dir / "state.json"
state = {"started_at": time.time(), "phases": []}
state = {
"started_at": time.time(),
"total_phases": len(phases),
"current_index": 0,
"current_phase": "Starting installation",
"phases": [],
}
_write_state(state_path, state)
for name, fn in phases:
for index, (name, fn) in enumerate(phases):
state["current_index"] = index
state["current_phase"] = name
state["phase_started_at"] = time.time()
_write_state(state_path, state)
info(f"› {name}")
started = time.time()
try:
@@ -49,6 +60,8 @@ def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None:
state["phases"].append({"name": name, "status": "ok", "elapsed": elapsed})
_write_state(state_path, state)
state["current_index"] = max(len(phases) - 1, 0)
state["current_phase"] = "Installation complete"
state["finished_at"] = time.time()
_write_state(state_path, state)
@@ -21,8 +21,8 @@ Phase ordering (protected / pre-mounted):
arch_install_base → archinstall used as pacstrap + users +
packages driver only; no bootloader,
no fstab, no mkinitcpio
configure_protected_boot → Omarchy-owned fstab/crypttab/mkinitcpio/
bootloader — implemented in Step 8
configure_protected_boot → protected fstab/crypttab + Limine EFI handoff;
final UKI build still happens in finalizer
run_chroot_finalizer → same
configure_login → same
validate_boot_protected → implemented in Step 8
@@ -1047,7 +1047,26 @@ def cleanup_protected_state(ctx: InstallContext) -> None:
# finish: prompt for reboot. Bind mounts are unwound in main()'s finally.
# ─────────────────────────────────────────────────────────────────────────────
def _stop_install_dashboard() -> None:
pid = os.environ.get("OMARCHY_INSTALL_DASHBOARD_PID")
if not pid:
return
try:
os.kill(int(pid), 15)
except (OSError, ValueError):
pass
# Give the shell dashboard a moment to restore the cursor before gum draws.
time.sleep(0.1)
try:
with open("/dev/tty", "w", encoding="utf-8") as tty:
tty.write("\033[?25h\033[H\033[2J")
tty.flush()
except OSError:
pass
def finish(ctx: InstallContext) -> None:
_stop_install_dashboard()
info("Installation finished. Reboot when ready.")
if confirm("Reboot now?", default=True):
os.system("reboot")