diff --git a/bin/omarchy-install-ui-preview b/bin/omarchy-install-ui-preview new file mode 100755 index 0000000..cf2c70e --- /dev/null +++ b/bin/omarchy-install-ui-preview @@ -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/null || echo 100 +} + +term_rows() { + if [[ -e /dev/tty ]]; then + local size + size=$(stty size 2>/dev/null /dev/null || echo 32 +} + +repeat() { + local char="$1" count="$2" + local out="" + for ((i=0; i= ${#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= ${#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 diff --git a/configs/airootfs/root/.automated_script.sh b/configs/airootfs/root/.automated_script.sh index 95e8bfc..e8b071b 100644 --- a/configs/airootfs/root/.automated_script.sh +++ b/configs/airootfs/root/.automated_script.sh @@ -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" diff --git a/configs/airootfs/usr/local/bin/omarchy-install-dashboard b/configs/airootfs/usr/local/bin/omarchy-install-dashboard new file mode 100755 index 0000000..f4618e3 --- /dev/null +++ b/configs/airootfs/usr/local/bin/omarchy-install-dashboard @@ -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 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 diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases.py index d84bd66..ffbd8fd 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases.py @@ -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) diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py index 9dad240..deb003b 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -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")