From 1d656af51d25431c5af08a1c3dd49439319e7054 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sat, 23 May 2026 04:50:32 -0400 Subject: [PATCH] installer: make dashboard own install flow --- bin/test-installer-ui-flow | 137 +++++++++++ configs/airootfs/root/.automated_script.sh | 97 ++------ .../usr/local/bin/omarchy-install-dashboard | 179 +++++++++++---- .../share/omarchy-iso/orchestrator/main.py | 3 - .../omarchy-iso/orchestrator/phases_impl.py | 217 +----------------- .../usr/share/omarchy-iso/orchestrator/ui.py | 6 - 6 files changed, 287 insertions(+), 352 deletions(-) create mode 100755 bin/test-installer-ui-flow diff --git a/bin/test-installer-ui-flow b/bin/test-installer-ui-flow new file mode 100755 index 0000000..f35d07f --- /dev/null +++ b/bin/test-installer-ui-flow @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# Local smoke test for the foreground installer UI supervisor. +# Runs without rebuilding the ISO. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DASHBOARD="$ROOT/configs/airootfs/usr/local/bin/omarchy-install-dashboard" +AUTOMATED="$ROOT/configs/airootfs/root/.automated_script.sh" +ORCH="$ROOT/configs/airootfs/usr/share/omarchy-iso/orchestrator" +OMARCHY_PATH_FOR_TEST="${OMARCHY_PATH_FOR_TEST:-$ROOT/../omarchy-installer}" + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +strip_ansi() { + python - "$1" <<'PY' +from pathlib import Path +import re, sys +s = Path(sys.argv[1]).read_text(errors="ignore") +s = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", s) +sys.stdout.write(s) +PY +} + +assert_contains() { + local file="$1" needle="$2" stripped + stripped="$tmp/stripped.$(basename "$file")" + strip_ansi "$file" >"$stripped" + if ! grep -Fq "$needle" "$stripped"; then + echo "Expected $file to contain: $needle" >&2 + exit 1 + fi +} + +assert_not_contains() { + local file="$1" needle="$2" stripped + stripped="$tmp/stripped.$(basename "$file")" + strip_ansi "$file" >"$stripped" + if grep -Fq "$needle" "$stripped"; then + echo "Expected $file not to contain: $needle" >&2 + exit 1 + fi +} + +write_state_py='import json, sys, time, pathlib +path=pathlib.Path(sys.argv[1]); phase=sys.argv[2] +state={"started_at":time.time(),"current_phase":phase,"phase_started_at":time.time(),"phases":[]} +tmp=path.with_name(".state.json.tmp") +tmp.write_text(json.dumps(state)) +tmp.replace(path)' + +success_worker="$tmp/success-worker" +cat >"$success_worker" <>"\$log" <<'LOG' +=== Installation Time Summary === +Omarchy: 1m 40s +================================= +LOG +EOF +chmod +x "$success_worker" + +fail_worker="$tmp/fail-worker" +cat >"$fail_worker" </dev/null +assert_contains "$success_capture" "Installing Omarchy" +assert_contains "$success_capture" "Installed in 1m 40s" + +fail_state="$tmp/fail-state.json" +fail_log="$tmp/fail.log" +fail_capture="$tmp/fail.capture" +set +e +OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \ + "$DASHBOARD '$fail_log' '$fail_state' -- '$fail_worker' '$fail_state'" \ + "$fail_capture" >/dev/null +fail_status=$? +set -e +if [[ $fail_status -ne 7 ]]; then + echo "Expected failure worker status 7, got $fail_status" >&2 + exit 1 +fi +assert_not_contains "$fail_capture" "Installed in" +assert_not_contains "$fail_capture" "Finished installing" + +bash -n "$DASHBOARD" "$AUTOMATED" +python -m py_compile "$ORCH"/*.py +git -C "$ROOT" diff --check + +if rg -n 'dashboard\.pid|dashboard\.stop|OMARCHY_INSTALL_DASHBOARD|_stop_install_dashboard|_dashboard_|killpg|current_phase.*Finishing|confirm\(|Reboot now' \ + "$AUTOMATED" "$DASHBOARD" "$ORCH"; then + echo "Forbidden legacy dashboard lifecycle pattern found" >&2 + exit 1 +fi + +if ! grep -Fq '/usr/local/bin/omarchy-install-dashboard \' "$AUTOMATED"; then + echo "Expected .automated_script.sh to call dashboard in foreground" >&2 + exit 1 +fi + +if grep -Eq 'omarchy-install-dashboard .*&' "$AUTOMATED"; then + echo "Dashboard must not be launched in background" >&2 + exit 1 +fi + +echo "installer UI flow smoke test passed" diff --git a/configs/airootfs/root/.automated_script.sh b/configs/airootfs/root/.automated_script.sh index 5a678cc..d669161 100644 --- a/configs/airootfs/root/.automated_script.sh +++ b/configs/airootfs/root/.automated_script.sh @@ -43,85 +43,18 @@ 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. -dashboard_tty=$(tty) -export OMARCHY_DASHBOARD_TTY="$dashboard_tty" -rm -f /run/omarchy-install/dashboard.stop /run/omarchy-install/dashboard.pid /run/omarchy-install/state.json -setsid /usr/local/bin/omarchy-install-dashboard "$OMARCHY_INSTALL_LOG_FILE" /run/omarchy-install/state.json <"$dashboard_tty" >"$dashboard_tty" 2>&1 & -dashboard_pid=$! -# setsid may fork/exec differently across environments. The dashboard writes -# its own real PID as soon as it starts; prefer that for shutdown. -for _ in {1..20}; do - [[ -s /run/omarchy-install/dashboard.pid ]] && break - sleep 0.05 -done -if [[ -s /run/omarchy-install/dashboard.pid ]]; then - dashboard_pid=$(cat /run/omarchy-install/dashboard.pid) -fi -export OMARCHY_INSTALL_DASHBOARD_PID="$dashboard_pid" - -stop_install_dashboard() { - touch /run/omarchy-install/dashboard.stop 2>/dev/null || true - if [[ -s /run/omarchy-install/dashboard.pid ]]; then - dashboard_pid=$(cat /run/omarchy-install/dashboard.pid) - fi - if [[ -n ${dashboard_pid:-} ]]; then - if kill -0 "$dashboard_pid" 2>/dev/null; then - # The dashboard runs under setsid, so $dashboard_pid is also its process - # group. Kill the group first so child sleep/tte processes cannot keep - # the dashboard alive over the final/error UI. - kill -- -"$dashboard_pid" 2>/dev/null || true - kill "$dashboard_pid" 2>/dev/null || true - for _ in {1..20}; do - kill -0 "$dashboard_pid" 2>/dev/null || break - sleep 0.05 - done - if kill -0 "$dashboard_pid" 2>/dev/null; then - kill -9 -- -"$dashboard_pid" 2>/dev/null || true - kill -9 "$dashboard_pid" 2>/dev/null || true - fi - fi - wait "$dashboard_pid" 2>/dev/null || true - unset dashboard_pid - fi - printf "\033[?25h" >/dev/tty -} -dashboard_error_handler() { - [[ $ERROR_HANDLING == "true" ]] && return - local exit_code=$? - stop_install_dashboard - catch_errors "$exit_code" -} - -dashboard_exit_handler() { - local exit_code=$? - stop_install_dashboard - if (( exit_code != 0 )) && [[ $ERROR_HANDLING != "true" ]]; then - catch_errors "$exit_code" - else - show_cursor - fi - exit "$exit_code" -} - -trap dashboard_error_handler ERR -trap dashboard_exit_handler EXIT -trap 'stop_install_dashboard; catch_errors 130' INT -trap 'stop_install_dashboard; catch_errors 143' TERM - -# 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. -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 \ - > >(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" +# The foreground dashboard is now the sole visible install UI owner. It starts +# the actual installer as a non-interactive child, logs child output, waits for +# completion, then renders the final installed-time/reboot prompt itself. +export OMARCHY_DASHBOARD_TTY="$(tty)" +rm -f /run/omarchy-install/state.json +/usr/local/bin/omarchy-install-dashboard \ + "$OMARCHY_INSTALL_LOG_FILE" \ + /run/omarchy-install/state.json \ + -- \ + /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 diff --git a/configs/airootfs/usr/local/bin/omarchy-install-dashboard b/configs/airootfs/usr/local/bin/omarchy-install-dashboard index 4013ea8..48e6bd8 100755 --- a/configs/airootfs/usr/local/bin/omarchy-install-dashboard +++ b/configs/airootfs/usr/local/bin/omarchy-install-dashboard @@ -1,17 +1,28 @@ #!/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. +# 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 -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}" +usage() { + echo "Usage: $0 -- " >&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" @@ -20,11 +31,12 @@ 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" +child_pid="" +child_pgid="" + whimsy=( "making Linux feel like home" "installing the good defaults" @@ -46,10 +58,37 @@ whimsy=( cleanup() { printf '%s%s' "$RESET" "$SHOW_CURSOR" >"$TTY_PATH" 2>/dev/null || true - rm -f "$PID_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 'cleanup; exit 0' INT TERM +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#* }"; } @@ -150,8 +189,6 @@ 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), @@ -163,7 +200,6 @@ weights = [ ('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: @@ -178,22 +214,6 @@ print(min(100, start + active), end='') PY } -dashboard_should_stop() { - [[ -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) -phase = state.get('current_phase') -# The final installed-time/reboot prompt is rendered inside the Finishing -# phase. Stop before drawing another dashboard frame so the prompt cannot be -# covered while it waits for input. -raise SystemExit(0 if state.get('finished_at') or phase in ('Finishing', 'Installation complete') else 1) -PY -} - current_whimsy() { local frame="$1" pct="$2" printf '%s' "${whimsy[$(( (pct / 8 + frame / 8) % ${#whimsy[@]} ))]}" @@ -204,11 +224,10 @@ render_static() { 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 + 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 - # If animation fails or times out, clear any partial frame before fallback. printf '%s' "$CLEAR" blank_line fi @@ -239,22 +258,92 @@ render_dynamic() { printf '%s' "$CLEAR_TO_END" } -if [[ ! -e $TTY_PATH ]]; then - exit 0 -fi +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; } + done + done +} -mkdir -p "$STATE_DIR" -echo "$$" >"$PID_FILE" +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)" + 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[@]}" >(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 true; do - [[ -e $STOP_FILE ]] && break - dashboard_should_stop && break +while jobs -pr | grep -qx "$child_pid"; do render_dynamic "$frame" >"$TTY_PATH" - [[ -e $STOP_FILE ]] && break - dashboard_should_stop && break frame=$((frame + 1)) sleep 0.5 done + +set +e +wait "$child_pid" +status=$? +set -e +child_pid="" +child_pgid="" + +if (( status != 0 )); then + printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" + exit "$status" +fi + +render_finish +if [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]]; then + exit 0 +fi + +if reboot_prompt; then + printf '%s' "$CLEAR" >"$TTY_PATH" + reboot 2>/dev/null || systemctl reboot 2>/dev/null || true +fi + +exit 0 diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py index da605a8..93cbd52 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py @@ -38,7 +38,6 @@ def build_phases(ctx: InstallContext): configure_login, validate_boot_full, validate_boot_protected, - finish, ) if ctx.is_protected: @@ -50,7 +49,6 @@ def build_phases(ctx: InstallContext): ("Finalizing in chroot", run_chroot_finalizer), ("Configuring login", configure_login), ("Validating protected boot setup", validate_boot_protected), - ("Finishing", finish), ] return [ @@ -59,7 +57,6 @@ def build_phases(ctx: InstallContext): ("Finalizing in chroot", run_chroot_finalizer), ("Configuring login", configure_login), ("Validating boot setup", validate_boot_full), - ("Finishing", finish), ] 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 b34eec1..b1c20d7 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -11,7 +11,6 @@ Phase ordering (full-disk): run_chroot_finalizer → bind mounts + sudoers shim + arch-chroot finalize.sh configure_login → sddm autologin for unencrypted installs validate_boot_full → assert UKI / limine.conf / kernel cmdline are sane - finish → reboot prompt Phase ordering (protected / pre-mounted): @@ -26,13 +25,11 @@ Phase ordering (protected / pre-mounted): run_chroot_finalizer → same configure_login → same validate_boot_protected → implemented in Step 8 - finish → same """ from __future__ import annotations import json -import os import re import shutil import subprocess @@ -42,7 +39,7 @@ from pathlib import Path from . import archinstall_adapter as arch from .context import InstallContext -from .ui import confirm, info +from .ui import info # Packages installed BEFORE useradd. omarchy-settings and omarchy-nvim @@ -1045,215 +1042,3 @@ def cleanup_protected_state(ctx: InstallContext) -> None: check=False, capture_output=True, ) - - -# ───────────────────────────────────────────────────────────────────────────── -# finish: prompt for reboot. Bind mounts are unwound in main()'s finally. -# ───────────────────────────────────────────────────────────────────────────── - -def _dashboard_stopped(pid: int) -> bool: - stat = Path(f"/proc/{pid}/stat") - if not stat.exists(): - return True - try: - # /proc//stat field 3 is process state. A zombie cannot write to - # the tty anymore; the shell parent will reap it when control returns. - return stat.read_text().split()[2] == "Z" - except OSError: - return True - - -def _pid_is_dashboard(pid: int) -> bool: - try: - cmdline = Path(f"/proc/{pid}/cmdline").read_bytes().replace(b"\0", b" ") - except OSError: - return False - return b"omarchy-install-dashboard" in cmdline - - -def _add_dashboard_pid(pids: list[int], pid_text: str | None) -> None: - if not pid_text: - return - try: - pid = int(pid_text.strip()) - except ValueError: - return - if pid not in pids and _pid_is_dashboard(pid): - pids.append(pid) - - -def _dashboard_pids() -> list[int]: - pids: list[int] = [] - - _add_dashboard_pid(pids, os.environ.get("OMARCHY_INSTALL_DASHBOARD_PID")) - - pid_file = Path("/run/omarchy-install/dashboard.pid") - try: - _add_dashboard_pid(pids, pid_file.read_text()) - except OSError: - pass - - # Last-resort /proc scan. Do not rely on pgrep being present in the ISO. - for proc_dir in Path("/proc").iterdir(): - if not proc_dir.name.isdigit(): - continue - _add_dashboard_pid(pids, proc_dir.name) - - return pids - - -def _signal_dashboard(pid: int, signal_number: int) -> None: - # Dashboard is launched with setsid, making its PID the process group ID. - # Signal the group first so child sleep/tte processes cannot keep drawing. - try: - os.killpg(pid, signal_number) - except OSError: - pass - try: - os.kill(pid, signal_number) - except OSError: - pass - - -def _stop_install_dashboard() -> None: - stop_file = Path("/run/omarchy-install/dashboard.stop") - try: - stop_file.parent.mkdir(parents=True, exist_ok=True) - stop_file.touch() - except OSError: - pass - - pids = _dashboard_pids() - for signal_number in (15, 9): - for pid in pids: - if _dashboard_stopped(pid): - continue - if not _pid_is_dashboard(pid): - continue - _signal_dashboard(pid, signal_number) - deadline = time.time() + (1.0 if signal_number == 15 else 0.5) - while time.time() < deadline: - if all(_dashboard_stopped(pid) for pid in pids): - break - time.sleep(0.05) - if all(_dashboard_stopped(pid) for pid in pids): - break - - 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 _tty_size() -> tuple[int, int]: - try: - with open("/dev/tty", "rb") as tty: - res = subprocess.run( - ["stty", "size"], - stdin=tty, - capture_output=True, - text=True, - check=True, - ) - rows, cols = res.stdout.strip().split() - return int(rows), int(cols) - except Exception: # noqa: BLE001 - return 24, 80 - - -def _center_text(text: str, width: int) -> str: - pad = max((width - len(text)) // 2, 0) - return " " * pad + text - - -def _install_duration(ctx: InstallContext) -> str | None: - candidates = [ - ctx.target / "var" / "log" / "omarchy-install.log", - Path("/var/log/omarchy-install.log"), - ] - patterns = ("Total:", "Omarchy:") - for path in candidates: - if not path.exists(): - continue - try: - lines = path.read_text(errors="ignore").splitlines() - except OSError: - continue - for prefix in patterns: - for line in reversed(lines[-80:]): - if line.startswith(prefix): - duration = line.split(":", 1)[1].strip() - if duration: - return duration - return None - - -def _render_tte_logo(logo_path: Path) -> bool: - if not logo_path.exists() or not shutil.which("tte"): - return False - try: - with open("/dev/tty", "rb", buffering=0) as stdin, open("/dev/tty", "wb", buffering=0) as stdout: - subprocess.run( - [ - "tte", - "-i", str(logo_path), - "--canvas-width", "0", - "--anchor-text", "c", - "--frame-rate", "920", - "laseretch", - ], - stdin=stdin, - stdout=stdout, - stderr=subprocess.DEVNULL, - timeout=8, - check=True, - ) - return True - except (OSError, subprocess.SubprocessError): - return False - - -def _render_static_finish_logo(tty, logo_path: Path, cols: int) -> None: - green = "\033[32m" - reset = "\033[0m" - if logo_path.exists(): - logo_lines = logo_path.read_text(errors="ignore").splitlines() - logo_width = max((len(line) for line in logo_lines), default=0) - left = max((cols - logo_width) // 2, 0) - for line in logo_lines: - tty.write(" " * left + green + line + reset + "\n") - else: - tty.write(_center_text("Omarchy", cols) + "\n") - - -def _render_finish_screen(ctx: InstallContext) -> None: - _, cols = _tty_size() - logo_path = Path("/usr/share/omarchy/logo.txt") - try: - with open("/dev/tty", "w", encoding="utf-8") as tty: - tty.write("\033[?25h\033[H\033[2J\n") - tty.flush() - - if not _render_tte_logo(logo_path): - with open("/dev/tty", "w", encoding="utf-8") as tty: - tty.write("\033[?25h\033[H\033[2J\n") - _render_static_finish_logo(tty, logo_path, cols) - tty.flush() - - with open("/dev/tty", "w", encoding="utf-8") as tty: - tty.write("\n") - duration = _install_duration(ctx) - message = f"Installed in {duration}" if duration else "Finished installing" - tty.write(_center_text(message, cols) + "\n\n") - tty.flush() - except OSError: - pass - - -def finish(ctx: InstallContext) -> None: - _stop_install_dashboard() - _render_finish_screen(ctx) - if confirm("Reboot now?", default=True): - os.system("reboot") diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/ui.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/ui.py index 8db3351..786c141 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/ui.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/ui.py @@ -16,12 +16,6 @@ def style(text: str, *, foreground: str | None = None, padding: str | None = Non subprocess.run(cmd, check=False) -def confirm(prompt: str, *, default: bool = True) -> bool: - cmd = ["gum", "confirm", "--default" if default else "--no-default", prompt] - with open("/dev/tty", "rb") as stdin, open("/dev/tty", "wb") as stdout: - return subprocess.run(cmd, stdin=stdin, stdout=stdout, stderr=stdout).returncode == 0 - - def info(text: str) -> None: style(text, foreground="3", padding="1 0 0 4")