diff --git a/bin/omarchy-provision-first-run b/bin/omarchy-provision-first-run index 6070edce..66e735d8 100755 --- a/bin/omarchy-provision-first-run +++ b/bin/omarchy-provision-first-run @@ -2,6 +2,7 @@ # omarchy:summary=Finish first-login setup for Omarchy. # omarchy:args=[--force] +# omarchy:hidden=true set -e diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index c1c6498b..2d81a5d9 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -1,9 +1,8 @@ #!/bin/bash # omarchy:summary=First-boot provisioning: create the user on a machine installed in deferred provisioning -# omarchy:group=setup # omarchy:requires-sudo=true -# omarchy:examples=omarchy-provision-owner +# omarchy:hidden=true # Runs on tty1 via omarchy-provision-owner.service when /var/lib/omarchy/provisioning/pending # exists — the state a deferred-provisioning ISO install or omarchy-system-factory-reset leaves @@ -36,6 +35,9 @@ export PATH="$OMARCHY_PATH/bin:$PATH" LOGO_PATH="$OMARCHY_PATH/logo.txt" LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH" 2>/dev/null || echo 0) +LOGO_HEIGHT=$(wc -l <"$LOGO_PATH" 2>/dev/null || echo 0) +(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81 +(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1 export GUM_CONFIRM_PROMPT_FOREGROUND="6" export GUM_CONFIRM_SELECTED_FOREGROUND="0" @@ -43,6 +45,42 @@ export GUM_CONFIRM_SELECTED_BACKGROUND="2" export GUM_CONFIRM_UNSELECTED_FOREGROUND="7" export GUM_CONFIRM_UNSELECTED_BACKGROUND="0" +# Match the first-boot console font to the ISO installer's apparent text size. +# The installer renders on GRUB's low-resolution console (gfxpayload=keep, on +# the ~768-row EFI GOP mode) where the default 8x16 font fills the screen. The +# installed system reaches native KMS resolution, where 8x16 is tiny — so +# first-boot provisioning looked far smaller than the installer it continues. +# +# Pick the kbd console font whose row count lands closest to that installer feel +# (~48 rows), but never one wide enough to wrap the 81-column logo. On a console +# that is already installer-sized (low resolution) this lands on the default +# font, i.e. a no-op. All three fonts ship with kbd, so there's no dependency. +scale_console_font() { + [[ $(tty 2>/dev/null) == /dev/tty* ]] || return 0 + command -v setfont >/dev/null 2>&1 || return 0 + + # Pick the console font empirically: apply each candidate, read the columns + # fbcon actually hands back, and keep the one whose row count lands nearest a + # ~48-row target while still leaving room for the 81-column logo. Measuring + # the real result beats computing from /sys/class/graphics/fb0/virtual_size, + # which under virgl can report a size the text console never actually reaches — + # that mismatch is what wrapped the logo into the top-left corner. + local target_rows=48 + local fonts=("default8x16" "sun12x22" "latarcyrheb-sun32") + local best="" best_diff=999999 name rows cols diff + for name in "${fonts[@]}"; do + setfont "$name" 2>/dev/null || continue + read -r rows cols < <(stty size /dev/null || printf '0 0\n') + [[ $rows =~ ^[0-9]+$ && $cols =~ ^[0-9]+$ ]] || continue + (( cols > LOGO_WIDTH )) || continue # must clear the 81-col logo + diff=$(( rows > target_rows ? rows - target_rows : target_rows - rows )) + (( diff < best_diff )) && { best_diff=$diff; best="$name"; } + done + # Fall back to the narrowest font if nothing cleared the logo width, so the + # form stays usable even on an unexpectedly tiny console. + setfont "${best:-default8x16}" 2>/dev/null || true +} + # Same Tokyo Night VT palette the ISO configurator sets, so the first-boot # form looks like a continuation of the install. set_tokyo_night_colors() { @@ -101,6 +139,401 @@ log_step() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] oem-setup: $1" >>"$LOG_FILE" } +# ── Setup progress screen ──────────────────────────────────────────────────── +# +# A port of the ISO install dashboard's renderer, so first-boot account setup +# looks like a continuation of the install: same logo header, same 34-cell +# bar, same rotating tips. Position = max(floor, work) in +# per-mille, monotonic; the floor is asymptotic per phase band and the work +# signal counts finalize-user's run_logged scripts in the log. + +CSI=$'\033[' +RESET="${CSI}0m" +DIM="${CSI}2m" +HIDE_CURSOR="${CSI}?25l" +SHOW_CURSOR="${CSI}?25h" +CLEAR="${CSI}2J${CSI}H" +CLEAR_LINE="${CSI}2K" +CLEAR_TO_END="${CSI}J" +GREEN="${CSI}32m" +WHITE="${CSI}37m" +DARK="${CSI}90m" + +STATE_FILE=/run/omarchy-provision-owner.state +FINALIZE_WARNING_FLAG=/run/omarchy-provision-owner.finalize-warning +DYNAMIC_ROW=$((LOGO_HEIGHT + 3)) + +tips=( + "Super + Space opens the Omarchy menu for apps, settings, and more" + "Super + K shows all the key bindings" + "Super is the Windows or command key on your keyboard" + "Use Xournal++ to sign PDFs" + "Share files with phones and laptops using LocalSend" + "Turn any website into an app with Install > Web App in the menu" + "Super + Return opens a terminal, Super + Shift + Return the browser" + "Edit images with Pinta, videos with Kdenlive, docs with LibreOffice" + "Super + Ctrl + Print grabs text off the screen with OCR" + "Print takes a screenshot, Alt + Print records the screen" + "Set a reminder with Super + Ctrl + R" + "Switch themes from Style > Theme in the Omarchy menu" + "Double-click the menu bar to make it transparent" + "Run a full Windows VM via Install > Windows in the menu" + "Super + Ctrl + V opens the clipboard manager" + "Super + 1 through 0 switches workspaces, add Shift to bring the window" + "Super + Print picks a color from anywhere on screen" + "Keep the system fresh with Update in the Omarchy menu" +) + +term_cols() { + local cols + cols=$(stty size 2>/dev/null 0 )) || cols=${COLUMNS:-80} + printf '%s' "$cols" +} + +# A fingerprint of the console geometry: the VT size plus the framebuffer's +# identity and pixel size. On a fresh first boot the VT can come up in a +# transitional ~80x25 mode and only widen to the real resolution a second or +# more later, when virtio-gpu's KMS takes over (or the SDL window's size lands). +# Watching this signature settle — rather than trusting one early reading — is +# what keeps the greeter from measuring that transient and rendering into it. +console_signature() { + local size fb_size="" fb_name="" + size=$(stty size 2>/dev/null /dev/null || true) + if [[ -n $current && $current == "$previous" ]]; then + quiet=$((quiet + 1)) + (( quiet >= quiet_samples )) && return 0 + else + previous=$current + quiet=0 + fi + sleep 0.1 + done + return 1 +} + +repeat() { + local char="$1" count="$2" out="" i + for ((i = 0; i < count; i++)); do out+="$char"; done + printf '%s' "$out" +} + +left_padding() { + local width="${1:-$LOGO_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:-$LOGO_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:-$LOGO_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 line + pad="$(left_padding "$LOGO_WIDTH")" + while IFS= read -r line; do + printf '\r%s%s%b%s%b\n' "$CLEAR_LINE" "$pad" "$GREEN" "$line" "$RESET" + done <"$LOGO_PATH" +} + +progress_bar() { + local pm="$1" width="${2:-34}" filled empty + (( pm < 0 )) && pm=0 + (( pm > 1000 )) && pm=1000 + filled=$((pm * width / 1000)) + empty=$((width - filled)) + printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET" +} + +# Monotonic clock so an NTP step during setup cannot jump the bar. +set_now() { + local up rest + if read -r up rest /dev/null && [[ $up == [0-9]* ]]; then + NOW=${up%%.*} + else + NOW=$EPOCHSECONDS + fi + return 0 +} + +# One tip per 8 seconds, elapsed-time driven like the install dashboard. +current_tip() { + printf '%s' "${tips[$(((NOW - SETUP_T0) / 8 % ${#tips[@]}))]}" +} + +SETUP_POS=10 +SETUP_PHASE="" +SETUP_PHASE_T0=0 +SETUP_T0=0 +NOW=0 +FINALIZE_BASE=-1 +FINALIZE_TOTAL=$(grep -c '^run_logged' "$OMARCHY_PATH/install/user/all.sh" 2>/dev/null || echo 0) + +# Whether this boot will re-key LUKS (encrypted installs stage a throwaway key). +# The re-key rebuilds the UKI and is the slowest single step, so it needs a wide +# band of its own; unencrypted installs skip it and let finalize take the room. +REKEY_PENDING=false +[[ -f $PROVISIONING_DIR/luks-key ]] && REKEY_PENDING=true + +# Per-mille bands per phase: "lo hi tau". tau shapes the asymptotic time floor; +# it is not a duration prediction. A wide band moves visibly; a narrow one looks +# stuck. Skipped phases leave a gap the next band's floor crosses on its own. +phase_band() { + if $REKEY_PENDING; then + # Encrypted: finalize is quick, the LUKS re-key (UKI rebuild) is the long + # pole — give it nearly half the bar so it keeps moving while it works. + case $1 in + finalize) echo "80 480 40" ;; + rekey) echo "480 950 30" ;; + boot) echo "950 990 20" ;; + done) echo "1000 1000 1" ;; + *) echo "10 80 3" ;; + esac + else + # Unencrypted: finalize is the dominant step. + case $1 in + finalize) echo "80 900 45" ;; + boot) echo "900 990 20" ;; + done) echo "1000 1000 1" ;; + *) echo "10 80 3" ;; + esac + fi +} + +setup_progress() { + local phase lo hi tau t span floor work completed + phase=$(cat "$STATE_FILE" 2>/dev/null || true) + [[ -n $phase ]] || phase=account + + if [[ $phase != "$SETUP_PHASE" ]]; then + SETUP_PHASE=$phase + SETUP_PHASE_T0=$NOW + FINALIZE_BASE=-1 + fi + + read -r lo hi tau <<<"$(phase_band "$phase")" + (( lo < SETUP_POS )) && lo=$SETUP_POS + + t=$((NOW - SETUP_PHASE_T0)) + span=$((hi - lo)) + floor=$lo + (( span > 1 )) && floor=$((lo + (span - 1) * t / (t + tau))) + + work=0 + if [[ $phase == "finalize" ]] && (( FINALIZE_TOTAL > 0 )); then + completed=$(grep -cE '] (Completed|Failed): ' "$LOG_FILE" 2>/dev/null || true) + [[ $completed =~ ^[0-9]+$ ]] || completed=0 + (( FINALIZE_BASE < 0 )) && FINALIZE_BASE=$completed + completed=$((completed - FINALIZE_BASE)) + (( completed < 0 )) && completed=0 + (( completed > FINALIZE_TOTAL )) && completed=$FINALIZE_TOTAL + work=$((lo + (hi - lo) * completed / FINALIZE_TOTAL)) + fi + + (( floor > SETUP_POS )) && SETUP_POS=$floor + (( work > SETUP_POS )) && SETUP_POS=$work + (( SETUP_POS > 1000 )) && SETUP_POS=1000 + return 0 +} + +render_setup_static() { + printf '%s%s' "$HIDE_CURSOR" "$CLEAR" + blank_line + render_logo + blank_line +} + +render_setup_dynamic() { + set_now + setup_progress + printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW" + center "Setting up your machine" "$LOGO_WIDTH" + blank_line + line_at "$LOGO_WIDTH" $(((LOGO_WIDTH - 34) / 2)) "" + progress_bar "$SETUP_POS" 34 + printf '\n' + blank_line + center "${DIM}Tip:${RESET} ${GREEN}$(current_tip)${RESET}" "$LOGO_WIDTH" + printf '%s' "$CLEAR_TO_END" +} + +# The first thing a new owner sees, shown once before the keyboard step: +# the logo (vertically centered like the boot logo) running a looping ColorShift, +# the Omarchy tagline, and a hint. Return skips ahead into setup at any time. +greeter_screen() { + local anim="" drawn_sig cur_sig resized=0 + + # Paint the whole splash for the console's *current* geometry. Kept in a + # nested function so the wait loop below can repaint it verbatim if the VT + # resizes out from under us: a late resize (virtio-gpu KMS handoff, or the SDL + # window's size arriving) scrolls the old frame into the top-left and resets + # the DEC saved cursor tte paints from — exactly the tiled/garbled logo we saw + # on a fresh first boot. Redrawing on every resize beats any fixed-length + # startup wait, which can only ever guess when the console has stopped moving. + _greeter_draw() { + local rows cols top content_h logo_row tagline_row hint_row + local tagline hint tpad hpad pad line i + + cols=$(term_cols) + rows=$(stty size 2>/dev/null /dev/null 2>&1; then + printf '%s%d;1H\0337' "$CSI" "$((logo_row + LOGO_HEIGHT))" + # 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 - 1))" \ + --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 + } + + _greeter_kill_anim() { + [[ -n ${anim:-} ]] || return 0 + # Guard both: `kill` returns non-zero if tte already exited (crash, or a + # resize race), and `wait` reports tte's kill signal (143) — either would + # abort provisioning under `set -e` and drop straight to the login screen. + kill "$anim" 2>/dev/null || true + wait "$anim" 2>/dev/null || true + anim="" + } + + # Let the console settle before the first paint, then size the font to it. + wait_console_stable 100 15 || true + scale_console_font + wait_console_stable 30 5 || true + + trap 'resized=1' WINCH + + _greeter_draw + drawn_sig=$(console_signature 2>/dev/null || true) + + # Wait for Return, but keep watching the geometry. On any resize (SIGWINCH or + # a changed signature) tear the animation down, settle, re-fit the font, and + # repaint — so a resize arriving five seconds in looks the same as one that + # never happened. + while true; do + if IFS= read -r -t 0.2 _ /dev/null || true) + if (( resized )) || [[ -n $cur_sig && $cur_sig != "$drawn_sig" ]]; then + _greeter_kill_anim + stty sane /dev/null || true + wait_console_stable 50 5 || true + scale_console_font + wait_console_stable 30 5 || true + _greeter_draw + drawn_sig=$(console_signature 2>/dev/null || true) + # Clear last so the font-fitting's own SIGWINCHes don't re-trigger a redraw. + resized=0 + fi + done + + trap - WINCH + _greeter_kill_anim + # 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. Then clear the + # leftover animation frame. + stty sane /dev/null || true + printf '%s%s%s' "$RESET" "$CLEAR" "$SHOW_CURSOR" +} + # The keyboard step the ISO configurator runs — deferred to first boot for OEM # installs, so the machine's owner picks their own layout. Applied immediately # (live VT + persisted) so the password typed next, and the LUKS re-key below, @@ -111,7 +544,6 @@ keyboard_form() { keyboards=$'Azerbaijani|azerty Belarusian|by Belgian|be-latin1 -Bosnian|ba Bulgarian|bg-cp1251 Croatian|croat Czech|cz @@ -137,7 +569,6 @@ Irish|ie Italian|it Japanese|jp106 Kazakh|kazakh -Khmer (Cambodia)|khmer Kyrgyz|kyrgyz Lao|la-latin1 Latvian|lv @@ -166,9 +597,9 @@ Ukrainian|ua' # Load the layout on the live VT and persist it for the installed system. # systemd-firstboot writes both the console KEYMAP and the XKB layout Hyprland -# reads, matching what the ISO's configure_keyboard does at install time. The -# two layouts localectl doesn't know (ba, khmer) keep the default, same as a -# normal install. +# reads, matching what the ISO's configure_keyboard does at install time. A +# keymap localectl doesn't know keeps the default rather than failing (defensive; +# the picker no longer offers any such layout). apply_keyboard() { local keymap="$1" [[ $(tty 2>/dev/null) == /dev/tty* ]] && loadkeys "$keymap" 2>/dev/null || true @@ -227,6 +658,33 @@ user_form() { full_name=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Full name> ") || full_name="" email_address=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Email address> ") || email_address="" + + # Hostname and timezone are deferred to first boot with the rest of the user + # step: the deferred install seeds neutral placeholders (omarchy/UTC) and the + # owner overwrites them here. Same validation and geo-guess the ISO installer's + # user step uses. + while true; do + hostname=$(gum input --placeholder "Letters, digits, and dashes (or return for 'omarchy')" --prompt.foreground="#845DF9" --prompt "Hostname> ") || hostname="" + if [[ -z $hostname ]]; then + hostname="omarchy"; break + elif [[ $hostname =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$ ]]; then + break + else + notice "Hostname must be 1-63 letters, digits, or dashes, and cannot start or end with a dash" 1 + fi + done + + local geo_guessed_timezone + step "Let's set your timezone..." + # A fresh machine usually hasn't joined a network yet, so tzupdate fails often; + # guard it or `set -e` would abort setup before the gum-filter fallback below. + geo_guessed_timezone=$(tzupdate -p 2>/dev/null) || geo_guessed_timezone="" + if [[ -n $geo_guessed_timezone ]]; then + timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$geo_guessed_timezone" --header "Timezone") || timezone="$geo_guessed_timezone" + else + timezone=$(timedatectl list-timezones | gum filter --height 10 --header "Timezone") || timezone="UTC" + fi + [[ -n $timezone ]] || timezone="UTC" } confirm_form() { @@ -237,7 +695,9 @@ Keyboard,${keyboard_label:-English (US)} Username,$username Password,$(printf "%${#password}s" | tr ' ' '*') Full name,${full_name:-[Skipped]} -Email address,${email_address:-[Skipped]}" | +Email address,${email_address:-[Skipped]} +Hostname,${hostname:-omarchy} +Timezone,${timezone:-UTC}" | gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/" echo @@ -329,11 +789,65 @@ configure_login() { printf '[Last]\nSession=omarchy.desktop\nUser=%s\n' "$username" >/var/lib/sddm/state.conf chown -R sddm:sddm /var/lib/sddm 2>/dev/null || true - # Encrypted installs autologin because the LUKS prompt is the auth boundary. - if encrypted_install; then - mkdir -p /etc/sddm.conf.d - printf '[Autologin]\nUser=%s\nSession=omarchy.desktop\n' "$username" >/etc/sddm.conf.d/autologin.conf + # Autologin straight into the desktop after first-boot setup — the owner just + # authenticated in the form, so we don't make them retype at SDDM. + mkdir -p /etc/sddm.conf.d + printf '[Autologin]\nUser=%s\nSession=omarchy.desktop\n' "$username" >/etc/sddm.conf.d/autologin.conf + + # Encrypted installs keep autologin permanently (the LUKS prompt is the auth + # boundary). Unencrypted installs autologin only this first boot, then a + # one-shot service removes the drop-in so later boots use the normal SDDM + # login and the disk isn't left permanently open. + encrypted_install || install_autologin_once_cleanup +} + +# Install a self-removing service that makes the autologin last exactly one +# boot — this one, where the owner just authenticated in the wizard. The unit is +# created now, but graphical.target's job for THIS boot is already computed, so +# it won't run this boot (this boot autologins). On the NEXT boot it's in the +# fresh transaction, ordered Before the display manager, so it runs before SDDM +# reads its config: it removes the autologin drop-in (that boot shows the normal +# login) and deletes its own unit and enablement symlink. Ordering Before= (not +# After=) is what makes it deterministic — no sleep/race against SDDM's startup. +install_autologin_once_cleanup() { + local unit=omarchy-provision-autologin-once.service + cat >"/etc/systemd/system/$unit" <&1 || + printf '%s\n' "$hostname" >/etc/hostname +} + +# Apply the owner's timezone, deferred to first boot with the rest of the user +# step, overwriting the placeholder (UTC) the deferred install seeded. +configure_timezone() { + [[ -n ${timezone:-} ]] || return 0 + if timedatectl set-timezone "$timezone" 2>&1; then + return 0 fi + # Fallback only if the zone file exists, so a bad zone can't point + # /etc/localtime at nothing and break every localtime read afterward. + [[ -e /usr/share/zoneinfo/$timezone ]] && + ln -sf "/usr/share/zoneinfo/$timezone" /etc/localtime 2>&1 || true } finalize_user() { @@ -351,6 +865,7 @@ finalize_user() { OMARCHY_SETUP_CONTEXT=provision-owner \ OMARCHY_USER_NAME="$full_name" \ OMARCHY_USER_EMAIL="$email_address" \ + OMARCHY_INSTALL_LOG_FILE="$LOG_FILE" \ OMARCHY_LOG_TO_STDOUT=1 \ "$OMARCHY_PATH/bin/omarchy-provision-user" --force --first-install } @@ -402,15 +917,30 @@ rekey_luks() { return 1 fi - local new_slot slot + local new_slot slot other_slots new_slot=$(cryptsetup open --test-passphrase --verbose --key-file <(printf '%s' "$password") "$device" 2>&1 | grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' | head -1) - if [[ -n $new_slot ]]; then - for slot in $(cryptsetup luksDump "$device" | awk '/^ +[0-9]+: luks2/ { sub(":", "", $1); print $1 }'); do - [[ $slot == "$new_slot" ]] && continue - cryptsetup luksKillSlot -q --key-file <(printf '%s' "$password") "$device" "$slot" - done + # Retiring the throwaway/seller slots must be all-or-nothing: if we can't + # identify the user's slot or a kill fails, keep the staged key and retry — + # never shred it while a slot the seller knows still unlocks the disk. + if [[ -z $new_slot ]]; then + log_step "could not identify the user's LUKS slot after re-key; keeping the staged key for retry" + say --foreground 1 "Could not confirm the LUKS re-key; will retry." + return 1 fi + if ! other_slots=$(cryptsetup luksDump "$device" | awk '/^ +[0-9]+: luks2/ { sub(":", "", $1); print $1 }'); then + log_step "luksDump failed while retiring slots; keeping the staged key for retry" + say --foreground 1 "Could not enumerate LUKS slots; will retry." + return 1 + fi + for slot in $other_slots; do + [[ $slot == "$new_slot" ]] && continue + if ! cryptsetup luksKillSlot -q --key-file <(printf '%s' "$password") "$device" "$slot"; then + log_step "failed to kill LUKS slot $slot; keeping the staged key for retry" + say --foreground 1 "Could not remove the throwaway LUKS key; will retry." + return 1 + fi + done shred -u "$PROVISIONING_DIR/luks-key" 2>/dev/null || rm -f "$PROVISIONING_DIR/luks-key" } @@ -450,7 +980,10 @@ reset_limine_config() { # Only remove machine-ids the old (Omarchy-managed) limine.conf referenced; # a shared ESP may hold other installations' machine-id directories. if [[ -f $esp/limine.conf ]]; then - old_ids=$(grep -o 'machine-id=[0-9a-f]\{32\}' "$esp/limine.conf" | cut -d= -f2 | sort -u) + # `|| true`: an entry-less limine.conf (left by a failed earlier rebuild) + # has no machine-id lines, so grep exits 1 and pipefail would abort the + # assignment under `set -e` — turning a recoverable retry into a dead loop. + old_ids=$(grep -o 'machine-id=[0-9a-f]\{32\}' "$esp/limine.conf" | cut -d= -f2 | sort -u || true) fi for template in "$OMARCHY_PATH/install/assets/limine/limine.conf" \ @@ -482,38 +1015,30 @@ cleanup_oem_state() { systemctl daemon-reload 2>/dev/null || true } -run_setup() { - while true; do - keyboard_form - user_form - confirm_form && break - done - - clear_logo - echo - say "Setting up your account..." - echo - - touch "$LOG_FILE" - chmod 600 "$LOG_FILE" - +# The provisioning work, backgrounded under the progress screen. Phase writes +# to STATE_FILE drive the bar; all output lands in the log. +run_provisioning() { log_step "creating user $username" create_user install_authorized_keys configure_login + log_step "setting hostname to ${hostname:-omarchy}" + configure_hostname + + log_step "setting timezone to ${timezone:-UTC}" + configure_timezone + log_step "finalizing user" - say "Finalizing your user (this can take a minute)..." - if ! finalize_user >>"$LOG_FILE" 2>&1; then + echo finalize >"$STATE_FILE" + if ! finalize_user; then log_step "finalize-user failed (continuing; user can retry after login)" - say --foreground 1 "User finalization reported errors (see $LOG_FILE)." - say --foreground 1 "Run 'omarchy-provision-user --force' after login to retry." - sleep 3 + touch "$FINALIZE_WARNING_FLAG" fi if [[ -f $PROVISIONING_DIR/luks-key ]]; then log_step "re-keying LUKS to the user's password" - say "Securing disk encryption with your password..." + echo rekey >"$STATE_FILE" rekey_luks log_step "LUKS re-key complete" fi @@ -523,18 +1048,69 @@ run_setup() { # linger and go hash-stale on the first UKI rebuild. Refresh them now. if limine_entries_stale; then log_step "refreshing boot entries for the new machine identity" - say "Refreshing boot entries (this can take a minute)..." + echo boot >"$STATE_FILE" reset_limine_config - limine-update >>"$LOG_FILE" 2>&1 + limine-update fi log_step "cleaning up provisioning state" cleanup_oem_state log_step "first-boot setup complete" +} +run_setup() { + while true; do + keyboard_form + user_form + confirm_form && break + done + + touch "$LOG_FILE" + chmod 600 "$LOG_FILE" + + rm -f "$FINALIZE_WARNING_FLAG" + echo account >"$STATE_FILE" + + run_provisioning >>"$LOG_FILE" 2>&1 & + local worker=$! + + set_now + SETUP_T0=$NOW + SETUP_PHASE_T0=$NOW + + render_setup_static + while kill -0 "$worker" 2>/dev/null; do + render_setup_dynamic + sleep 0.5 + done + + local status=0 + wait "$worker" || status=$? + printf '%s' "$SHOW_CURSOR" + (( status == 0 )) || return "$status" + + # One full-bar frame so the finish doesn't cut in mid-progress. + echo done >"$STATE_FILE" + render_setup_dynamic + sleep 1 + + if [[ -f $FINALIZE_WARNING_FLAG ]]; then + rm -f "$FINALIZE_WARNING_FLAG" + clear_logo + echo + say --foreground 1 "User finalization reported errors (see $LOG_FILE)." + say --foreground 1 "Run 'omarchy-provision-user --force' after login to retry." + sleep 3 + fi + + # Deferred first boot hands straight off to the display manager: SDDM starts + # the moment this oneshot exits (Before=display-manager.service) and autologs + # in on encrypted installs. No timed celebration screen and no "start" button + # here — that send-off belongs to a direct install, not first-boot setup. clear_logo echo - gum spin --spinner "pulse" --title "All set. Starting Omarchy..." -- sleep 2 + say "Starting Omarchy..." + sleep 1 } # A failed first-boot setup must not strand the machine at a user-less login @@ -547,7 +1123,11 @@ if [[ ${1:-} == "--attempt" ]]; then fi main() { + # Font sizing waits for the console to settle, so it lives inside + # greeter_screen (which owns that settle) rather than running here on a VT + # that may still be in a transitional mode this early in first boot. set_tokyo_night_colors + greeter_screen while ! "$0" --attempt; do clear_logo @@ -555,8 +1135,12 @@ main() { say --foreground 1 "Setup hit an error (details in $LOG_FILE)." echo if ! gum confirm --affirmative "Try again" --negative "Drop to console" "Retry first-boot setup?"; then - say "Run 'omarchy-provision-owner' as root to retry later." - exit 1 + # Give an actual usable console: this service owns tty1 (and Conflicts the + # getty), and on a failed deferred setup there's no user account and root + # is locked — so exiting would strand the machine with no way in. Exec a + # root shell on tty1; SDDM starts once it exits. + say "Dropping to a root shell. Run 'omarchy-provision-owner' to retry setup." + exec /bin/bash fi done } diff --git a/bin/omarchy-provision-user b/bin/omarchy-provision-user index 1e67c8df..b9094f72 100755 --- a/bin/omarchy-provision-user +++ b/bin/omarchy-provision-user @@ -1,8 +1,7 @@ #!/bin/bash # omarchy:summary=Finalize Omarchy user setup (runtime tweaks /etc/skel can't do) -# omarchy:group=finalize -# omarchy:examples=omarchy finalize user | omarchy finalize user --force +# omarchy:hidden=true set -euo pipefail diff --git a/bin/omarchy-system-factory-reset-finish b/bin/omarchy-system-factory-reset-finish index cc46c319..7860d237 100755 --- a/bin/omarchy-system-factory-reset-finish +++ b/bin/omarchy-system-factory-reset-finish @@ -1,9 +1,8 @@ #!/bin/bash # omarchy:summary=First-boot worker that finishes an omarchy-system-factory-reset reset -# omarchy:group=setup # omarchy:requires-sudo=true -# omarchy:examples=omarchy-system-factory-reset-finish +# omarchy:hidden=true # Runs once, early on the first boot after omarchy-system-factory-reset, via # omarchy-system-factory-reset-finish.service (armed by /var/lib/omarchy/provisioning/wipe-pending).