installer: make dashboard own install flow

This commit is contained in:
Ryan Hughes committed 2026-05-23 04:50:32 -04:00
1 parent af4679b153
commit 1d656af51d
6 files changed
+287 -352

No files matched your search

+137
View File
@@ -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" <<EOF
#!/usr/bin/env bash
set -euo pipefail
state="\$1"; log="\$2"
mkdir -p "\$(dirname "\$state")" "\$(dirname "\$log")"
python - "\$state" "Preparing live environment" <<'PY'
$write_state_py
PY
sleep 0.2
python - "\$state" "Installing Arch + Omarchy" <<'PY'
$write_state_py
PY
sleep 0.4
python - "\$state" "Validating boot setup" <<'PY'
$write_state_py
PY
sleep 0.2
cat >>"\$log" <<'LOG'
=== Installation Time Summary ===
Omarchy: 1m 40s
=================================
LOG
EOF
chmod +x "$success_worker"
fail_worker="$tmp/fail-worker"
cat >"$fail_worker" <<EOF
#!/usr/bin/env bash
set -euo pipefail
state="\$1"
mkdir -p "\$(dirname "\$state")"
python - "\$state" "Installing Arch + Omarchy" <<'PY'
$write_state_py
PY
sleep 0.2
exit 7
EOF
chmod +x "$fail_worker"
success_state="$tmp/success-state.json"
success_log="$tmp/success.log"
success_capture="$tmp/success.capture"
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
"$DASHBOARD '$success_log' '$success_state' -- '$success_worker' '$success_state' '$success_log'" \
"$success_capture" >/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"
+15 -82
View File
@@ -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
@@ -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 <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"
@@ -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[@]}" </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 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
@@ -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),
]
@@ -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/<pid>/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")
@@ -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")