diff --git a/bin/omarchy-iso-test b/bin/omarchy-iso-test index 7292632..3e670a8 100755 --- a/bin/omarchy-iso-test +++ b/bin/omarchy-iso-test @@ -606,6 +606,11 @@ audio-panel|meta_l-ctrl-a|omarchy-keyboard-panel|omarchy.audio' drive_configurator() { log "Driving the installer wizard" + # A greeter (animated logo + tagline) opens the installer; Return begins. + wait_for_screen "Opinionated" 300 + capture_console "success-installer-00-greeter" + press ret + # Keyboard is the first screen. Ctrl+C there is the hidden entry into deferred provisioning # mode; a normal install just selects the layout. wait_for_screen "keyboard layout" 300 @@ -710,7 +715,12 @@ drive_provision_owner() { # verifier can prove the deferred step applied a non-default layout — UK # leaves every letter identical to US, so the typed password (sent as raw # keycodes) still matches the literal one the harness pipes to sudo. - wait_for_screen "keyboard layout" 600 + # A greeter (animated logo + tagline) opens first boot; Return begins. + wait_for_screen "Opinionated" 600 + capture_console "success-provision-00-greeter" + press ret + + wait_for_screen "keyboard layout" 120 press up capture_console "success-provision-01-keyboard-layout-uk" press ret @@ -743,13 +753,25 @@ drive_provision_owner() { capture_console "success-provision-04-email" press ret + # Hostname is deferred to first boot too. + wait_for_screen "Hostname" 60 + type_text "$GUEST_HOSTNAME" + capture_console "success-provision-05-hostname" + press ret + + # Timezone is deferred to first boot too; accept the geo-guessed default. + # Generous timeout: the tzupdate geo-guess over first-boot networking can lag. + wait_for_screen "Timezone" 180 + capture_console "success-provision-06-timezone" + press ret + wait_for_screen "look right" 60 - capture_console "success-provision-05-review" + capture_console "success-provision-07-review" press ret # Finalization (offline Node unpack, LUKS re-key on encrypted installs) # runs before SDDM appears. - capture_console "success-provision-06-finalizing" + capture_console "success-provision-08-finalizing" } wait_for_install() { @@ -766,6 +788,15 @@ wait_for_install() { return 0 fi + # Deferred-provisioning installs skip the celebration and reboot on their own + # (no Reboot Now prompt); the first-boot greeter tagline appearing means the + # install finished and rebooted into setup. + if $PROVISION && grep -qi "Opinionated" <<<"$text"; then + log "Install finished and auto-rebooted into first-boot setup." + capture_console "success-installer-15-autoreboot" + return 0 + fi + if grep -qi "installation stopped" <<<"$text"; then capture_console "failure-installer-installation-stopped" echo "Install failed — screenshot saved to $RUN_DIR" >&2 @@ -965,8 +996,17 @@ install_phase() { # flag to clear before powering off. if $PROVISION; then log "Waiting for provisioning first-boot setup to finish" - local waited=0 - while ssh_guest "test -f /var/lib/omarchy/provisioning/pending" 2>/dev/null; do + local waited=0 rc + while true; do + # Capture rc via `|| rc=$?` so a non-zero result doesn't trip `set -e` + # (this command is intentionally allowed to "fail" — that's the signal). + rc=0 + ssh_guest "test -f /var/lib/omarchy/provisioning/pending" 2>/dev/null || rc=$? + # rc 0 = flag still present; rc 255 = SSH not up yet (finalization may + # still be running) — keep waiting. Only a clean "file absent" (rc 1) + # means provisioning finished, so we don't snapshot mid-finalization on a + # transient SSH blip. + (( rc == 0 || rc == 255 )) || break if ((waited >= 900)); then capture_console "failure-provision-finalization-timeout" echo "Timed out waiting for provisioning first-boot setup to complete" >&2 @@ -975,7 +1015,29 @@ install_phase() { sleep 10 ((waited += 10)) done - capture_console "success-provision-06-setup-complete" + + # The owner's hostname and timezone are set at first boot now, not baked + # into the install — confirm they actually landed on the running system. + # Hostname is deterministic (we typed it), so a mismatch is a hard failure; + # timezone is geo-guessed, so just report what landed. + local got_host="" got_tz="" + got_host=$(ssh_guest "cat /etc/hostname" 2>/dev/null | tr -d '\r\n') || got_host="" + got_tz=$(ssh_guest "timedatectl show -p Timezone --value" 2>/dev/null | tr -d '\r\n') || got_tz="" + if [[ $got_host != "$GUEST_HOSTNAME" ]]; then + capture_console "failure-provision-hostname-not-applied" + echo "First-boot hostname is '${got_host:-}', expected '$GUEST_HOSTNAME'" >&2 + return 1 + fi + log "hostname applied at first boot: $got_host" + if [[ -n $got_tz && $got_tz != "UTC" ]]; then + log "timezone applied at first boot: $got_tz" + else + echo "WARN: timezone is '${got_tz:-}' (expected the owner's choice, not UTC)" >&2 + fi + + # Deferred first-boot setup hands straight off to SDDM once the oneshot + # service exits — no celebration screen, no Start Omarchy button to confirm. + capture_console "success-provision-09-handoff" fi ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install-timing.json 2>/dev/null" \ diff --git a/configs/airootfs/root/.automated_script.sh b/configs/airootfs/root/.automated_script.sh index 6057034..16904f8 100644 --- a/configs/airootfs/root/.automated_script.sh +++ b/configs/airootfs/root/.automated_script.sh @@ -101,6 +101,16 @@ else ./configurator fi +# Deferred-provisioning installs skip the celebration/reboot prompt and reboot on +# their own — the owner completes setup at first boot. Signalled by the config's +# defer_provisioning flag (interactive) or the defer-provisioning marker (cidata). +# Parse the flag with jq (the same JSON semantics the orchestrator uses) rather +# than a line regex, so a reformatted config can't read as a direct install. +if [[ -f /root/defer-provisioning ]] || + [[ "$(jq -r '.omarchy_install.defer_provisioning // false' /root/user_configuration.json 2>/dev/null)" == "true" ]]; then + export OMARCHY_UI_DEFER_PROVISIONING=yes +fi + # 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. diff --git a/configs/airootfs/root/configurator b/configs/airootfs/root/configurator index 1ab29f0..3a1ca9e 100644 --- a/configs/airootfs/root/configurator +++ b/configs/airootfs/root/configurator @@ -42,14 +42,12 @@ measure_terminal # console and only grows once the kernel framebuffer/KMS takes over (noticeably # late in VMs). The logo is wider than 80 columns, so a first draw against that # transient narrow console wraps the logo ("smushed") and can't center it -# (padding clamps to 0, so it hugs the left). Every later screen re-measures and -# looks right, so this only bites the very first screen. Hold that first draw -# until the width settles at (or above) the logo width. Terminal emulators (dry -# runs) are already at final size, so skip them; a real VT that never widens -# (e.g. nomodeset) still proceeds after the cap rather than hanging. +# (padding clamps to 0, so it hugs the left). A terminal emulator (or sudo's +# pty) has the same problem the other way: it reports a stale 24x80 for a few +# hundred ms after start before its real winsize is set. Either way, hold the +# first draw until the width settles at (or above) the logo width. A real VT +# that never widens (e.g. nomodeset) still proceeds after the cap. wait_for_stable_terminal() { - [[ $(tty 2>/dev/null) == /dev/tty* ]] || return 0 - local width last="" stable=0 waited=0 while (( waited < 5000 )); do width=$(stty size 2>/dev/null /dev/null /dev/null 2>&1; then + printf '\033[%d;1H\0337' "$((logo_row + logo_h))" + # Run tte directly (not inside a `while` subshell) so $anim is tte's own + # PID: killing a wrapping subshell would orphan tte, which then keeps + # painting the logo over the keyboard step. --cycles is high enough that it + # never ends on its own before Return. + tte -i "$LOGO_PATH" \ + --canvas-width "$cols" \ + --anchor-text c \ + --frame-rate 60 \ + --reuse-canvas \ + colorshift \ + --gradient-stops 2 10 6 10 \ + --gradient-frames 3 \ + --cycles 1000 \ + --final-gradient-stops 2 \ + /dev/tty 2>/dev/null & + anim=$! + fi + + IFS= read -r _ /dev/null; wait "$anim" 2>/dev/null || true; } + # tte leaves the tty in raw/no-echo mode when killed; restore it or the gum + # prompts in the keyboard step that follows silently die. + stty sane /dev/null || true + # Reset the screen so the leftover animation frame doesn't linger under the + # keyboard step that follows. + printf '\033[0m\033[H\033[2J\033[?25h' +} + abort() { gum style "${1:-Aborted installation}" echo @@ -116,7 +188,6 @@ keyboard_form() { keyboards=$'Azerbaijani|azerty Belarusian|by Belgian|be-latin1 -Bosnian|ba Bulgarian|bg-cp1251 Croatian|croat Czech|cz @@ -142,7 +213,6 @@ Irish|ie Italian|it Japanese|jp106 Kazakh|kazakh -Khmer (Cambodia)|khmer Kyrgyz|kyrgyz Lao|la-latin1 Latvian|lv @@ -975,6 +1045,8 @@ install_target="full_disk" # isn't measured against the transient 80-column boot console. wait_for_stable_terminal +greeter + keyboard_form true if $defer_provisioning; then @@ -988,6 +1060,8 @@ if $defer_provisioning; then password_hash="" full_name="" email_address="" + # Neutral defaults for the install; the owner overwrites both in the + # first-boot OOBE (omarchy-provision-owner). hostname="omarchy" timezone="UTC" diff --git a/configs/airootfs/usr/local/bin/omarchy-install-dashboard b/configs/airootfs/usr/local/bin/omarchy-install-dashboard index 541a230..bf6da9d 100755 --- a/configs/airootfs/usr/local/bin/omarchy-install-dashboard +++ b/configs/airootfs/usr/local/bin/omarchy-install-dashboard @@ -736,6 +736,17 @@ if (( status != 0 )); then exit "$failure_status" fi +# Deferred-provisioning installs finish silently and reboot on their own — the +# owner completes setup (and sees the send-off) at first boot, so the "Installed +# Omarchy in ..." celebration and the Reboot Now prompt belong to a direct +# install only. +if [[ ${OMARCHY_UI_DEFER_PROVISIONING:-} == "yes" ]]; then + printf '%s' "$CLEAR" >"$TTY_PATH" 2>/dev/null || true + [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]] && exit 0 + reboot 2>/dev/null || systemctl reboot 2>/dev/null || true + exit 0 +fi + set +e render_finish finish_render_status=$?