diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index 2d81a5d9..53f6234d 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -33,6 +33,16 @@ fi export PATH="$OMARCHY_PATH/bin:$PATH" +# The shared setup form — the same questions, prompts, and validation the ISO +# configurator's user step asks. The ISO vendors this very file out of the +# runtime package it bundles, so an install and the first boot that finishes it +# can never offer different layouts or accept different usernames. +source "$OMARCHY_PATH/install/provisioning/setup-form.sh" + +# Unlike a fresh install target, this machine already has accounts; a pinned +# resume (below) bypasses the prompt entirely. +omarchy_username_taken() { getent passwd "$1" >/dev/null; } + 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) @@ -539,62 +549,34 @@ greeter_screen() { # (live VT + persisted) so the password typed next, and the LUKS re-key below, # use the chosen layout. keyboard_form() { - step "Let's setup your keyboard..." - local keyboards choice - keyboards=$'Azerbaijani|azerty -Belarusian|by -Belgian|be-latin1 -Bulgarian|bg-cp1251 -Croatian|croat -Czech|cz -Danish|dk-latin1 -Dutch|nl -English (UK)|uk -English (US)|us -English (US, Dvorak)|dvorak -English (US, Colemak)|colemak -Estonian|et -Finnish|fi -French|fr -French (Canada)|cf -French (Switzerland)|fr_CH -Georgian|ge -German|de -German (Switzerland)|de_CH-latin1 -Greek|gr -Hebrew|il -Hungarian|hu -Icelandic|is-latin1 -Irish|ie -Italian|it -Japanese|jp106 -Kazakh|kazakh -Kyrgyz|kyrgyz -Lao|la-latin1 -Latvian|lv -Lithuanian|lt -Macedonian|mk-utf -Norwegian|no-latin1 -Polish|pl -Portuguese|pt-latin1 -Portuguese (Brazil)|br-abnt2 -Romanian|ro -Russian|ru -Serbian|sr-latin -Slovak|sk-qwertz -Slovenian|slovene -Spanish|es -Spanish (Latin American)|la-latin1 -Swedish|sv-latin1 -Tajik|tj_alt-UTF8 -Turkish|trq -Ukrainian|ua' - choice=$(printf '%s\n' "$keyboards" | cut -d'|' -f1 | gum choose --height 10 --selected "English (US)" --header "Select keyboard layout") || choice="English (US)" - keyboard=$(printf '%s\n' "$keyboards" | awk -F'|' -v c="$choice" '$1==c{print $2; exit}') - keyboard_label="$choice" + local status + + while true; do + step "Let's setup your keyboard..." + omarchy_prompt_keyboard && status=0 || status=$? + ((status == 0)) && break + + # Esc means "back", and nothing precedes the first screen, so re-ask. + ((status == OMARCHY_FORM_BACK)) && continue + + # Ctrl+C is the way out of a screen the owner cannot get past. Rebooting is + # safe but not an escape from setup: this service runs again at next boot, + # and encrypted machines still auto-unlock from the staged keyfile until the + # re-key below. Confirm it so a stray Ctrl+C doesn't bounce the machine. + confirm_reboot && exec systemctl reboot + done + apply_keyboard "$keyboard" } +confirm_reboot() { + clear_logo + echo + say "Setup starts again after the reboot." + echo + gum confirm --affirmative "Yes, reboot" --negative "No, keep setting up" "Reboot this machine?" +} + # 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. A @@ -624,67 +606,19 @@ user_form() { say "Continuing setup for user: $username" echo else - while true; do - username=$(gum input --placeholder "Alphanumeric without spaces (like dhh)" --prompt.foreground="#845DF9" --prompt "Username> ") || continue - - if [[ "$username" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]]; then - if [[ "$username" =~ ^(root|bin|daemon|mail|ftp|http|nobody|dbus|systemd-coredump|systemd-network|systemd-oom|systemd-journal-remote|systemd-resolve|systemd-timesync|tss|uuidd|alpm|git|avahi|cups|lp|_talkd|polkitd|rtkit|qemu|brltty|gluster|rpc|libvirt-qemu|pcscd|nvidia-persistenced|sddm)$ ]]; then - notice "Username is reserved for system" 1 - elif getent passwd "$username" >/dev/null; then - # provisioning state has no users, so any existing account is off limits; - # a pinned resume (above) bypasses this prompt entirely. - notice "That username already exists on this machine" 1 - else - break - fi - else - notice "Username must be alphanumeric with no spaces" 1 - fi - done + omarchy_prompt_username || return $? fi - while true; do - password=$(gum input --placeholder "Used for user + root, and disk encryption when enabled" --prompt.foreground="#845DF9" --password --prompt "Password> ") || continue - password_confirmation=$(gum input --placeholder "Must match the password you just typed" --prompt.foreground="#845DF9" --password --prompt "Confirm> ") || continue - - if [[ -n "$password" && "$password" == "$password_confirmation" ]]; then - break - elif [[ -z "$password" ]]; then - notice "Your password can't be blank!" 1 - else - notice "Passwords didn't match!" 1 - fi - done - - 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="" + omarchy_prompt_password || return $? + omarchy_prompt_identity || return $? # 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 + # owner overwrites them here. + omarchy_prompt_hostname || return $? - 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" + omarchy_prompt_timezone || return $? } confirm_form() { @@ -1059,9 +993,17 @@ run_provisioning() { } run_setup() { + local status + while true; do keyboard_form - user_form + user_form && status=0 || status=$? + if ((status != 0)); then + # Esc unwinds to the keyboard step. Ctrl+C is the only other way out of a + # prompt, and it offers the reboot instead of returning a value. + ((status == OMARCHY_FORM_SIGNAL)) && confirm_reboot && exec systemctl reboot + continue + fi confirm_form && break done diff --git a/install/provisioning/setup-form.sh b/install/provisioning/setup-form.sh new file mode 100644 index 00000000..909668eb --- /dev/null +++ b/install/provisioning/setup-form.sh @@ -0,0 +1,183 @@ +# The setup form: every question Omarchy asks a human to describe their machine +# — keyboard, account, hostname, timezone — plus the rules those answers are +# checked against. Shared by the two places that ask them: the ISO +# configurator's user step and this package's first-boot owner setup +# (omarchy-provision-owner). Sourced by both, so the copies cannot drift the way +# the keyboard list already did. +# +# Every prompt reports one of three statuses, and both callers read them the +# same way: +# +# 0 field is set, move on +# 1 Esc — unwind to the start of the form +# 130 Ctrl+C — a per-caller side channel (the installer arms deferred +# provisioning or toggles encryption; first-boot setup offers a reboot) +# +# gum is the reason those two are the whole vocabulary: Esc and Ctrl+C are the +# only keys any gum widget exits on, and Ctrl+C arrives as a byte in raw mode, +# so it never reaches the shell as SIGINT. Act on the status, never on a trap. +# +# Callers supply `notice ` for validation feedback, and set +# the variables these prompts write: keyboard, keyboard_label, username, +# password, password_confirmation, full_name, email_address, hostname, timezone. + +OMARCHY_FORM_BACK=1 +OMARCHY_FORM_SIGNAL=130 + +# The English layouts lead, then everything else alphabetically. gum choose +# paginates in --height-sized pages and jumps to the page holding --selected, +# so an alphabetical English (US) landed deep enough to sit alone at the edge +# of a page of layouts nobody scanning for it reads. Up here the default and +# its variants are the first thing on screen no matter how the list grows. +OMARCHY_KEYBOARD_LAYOUTS=$'English (US)|us +English (UK)|uk +English (US, Dvorak)|dvorak +English (US, Colemak)|colemak +Azerbaijani|azerty +Belarusian|by +Belgian|be-latin1 +Bulgarian|bg-cp1251 +Croatian|croat +Czech|cz +Danish|dk-latin1 +Dutch|nl +Estonian|et +Finnish|fi +French|fr +French (Canada)|cf +French (Switzerland)|fr_CH +Georgian|ge +German|de +German (Switzerland)|de_CH-latin1 +Greek|gr +Hebrew|il +Hungarian|hu +Icelandic|is-latin1 +Irish|ie +Italian|it +Japanese|jp106 +Kazakh|kazakh +Kyrgyz|kyrgyz +Lao|la-latin1 +Latvian|lv +Lithuanian|lt +Macedonian|mk-utf +Norwegian|no-latin1 +Polish|pl +Portuguese|pt-latin1 +Portuguese (Brazil)|br-abnt2 +Romanian|ro +Russian|ru +Serbian|sr-latin +Slovak|sk-qwertz +Slovenian|slovene +Spanish|es +Spanish (Latin American)|la-latin1 +Swedish|sv-latin1 +Tajik|tj_alt-UTF8 +Turkish|trq +Ukrainian|ua' + +OMARCHY_USERNAME_PATTERN='^[a-z_][a-z0-9_-]*[$]?$' +OMARCHY_RESERVED_USERNAMES='^(root|bin|daemon|mail|ftp|http|nobody|dbus|systemd-coredump|systemd-network|systemd-oom|systemd-journal-remote|systemd-resolve|systemd-timesync|tss|uuidd|alpm|git|avahi|cups|lp|_talkd|polkitd|rtkit|qemu|brltty|gluster|rpc|libvirt-qemu|pcscd|nvidia-persistenced|sddm)$' +OMARCHY_HOSTNAME_PATTERN='^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$' +OMARCHY_HOSTNAME_DEFAULT='omarchy' + +# Installer targets are empty, so any account is fair game; first-boot setup +# overrides this because its machine already has users. +omarchy_username_taken() { return 1; } + +# `x=$(gum ...) && status=0 || status=$?` rather than a bare assignment followed +# by `status=$?`: one caller runs under `set -e`, where a cancelled prompt is a +# failing assignment that would kill the script before the status is read. + +omarchy_prompt_keyboard() { + local choice status + choice=$(printf '%s\n' "$OMARCHY_KEYBOARD_LAYOUTS" | cut -d'|' -f1 | + gum choose --height 10 --selected "English (US)" --header "Select keyboard layout") && status=0 || status=$? + ((status == 0)) || return $status + + keyboard_label="$choice" + keyboard=$(printf '%s\n' "$OMARCHY_KEYBOARD_LAYOUTS" | awk -F'|' -v c="$choice" '$1==c{print $2; exit}') +} + +omarchy_prompt_username() { + local status + while true; do + username=$(gum input --placeholder "Alphanumeric without spaces (like dhh)" --prompt.foreground="#845DF9" --prompt "Username> ") && status=0 || status=$? + ((status == 0)) || return $status + + if [[ "$username" =~ $OMARCHY_USERNAME_PATTERN ]]; then + if [[ "$username" =~ $OMARCHY_RESERVED_USERNAMES ]]; then + notice "Username is reserved for system" 1 + elif omarchy_username_taken "$username"; then + notice "That username already exists on this machine" 1 + else + return 0 + fi + else + notice "Username must be alphanumeric with no spaces" 1 + fi + done +} + +omarchy_prompt_password() { + local status + while true; do + password=$(gum input --placeholder "Used for user + root, and disk encryption when enabled" --prompt.foreground="#845DF9" --password --prompt "Password> ") && status=0 || status=$? + ((status == 0)) || return $status + password_confirmation=$(gum input --placeholder "Must match the password you just typed" --prompt.foreground="#845DF9" --password --prompt "Confirm> ") && status=0 || status=$? + ((status == 0)) || return $status + + if [[ -n "$password" && "$password" == "$password_confirmation" ]]; then + return 0 + elif [[ -z "$password" ]]; then + notice "Your password can't be blank!" 1 + else + notice "Passwords didn't match!" 1 + fi + done +} + +# Both fields are skippable with Return, so an empty value is a real answer and +# only Esc/Ctrl+C end the prompt early. +omarchy_prompt_identity() { + local status + full_name=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Full name> ") && status=0 || status=$? + ((status == 0)) || return $status + email_address=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Email address> ") && status=0 || status=$? + return $status +} + +omarchy_prompt_hostname() { + local status + while true; do + hostname=$(gum input --placeholder "Letters, digits, and dashes (or return for 'omarchy')" --prompt.foreground="#845DF9" --prompt "Hostname> ") && status=0 || status=$? + ((status == 0)) || return $status + + if [[ -z $hostname ]]; then + hostname="$OMARCHY_HOSTNAME_DEFAULT" + return 0 + elif [[ "$hostname" =~ $OMARCHY_HOSTNAME_PATTERN ]]; then + return 0 + else + notice "Hostname must be 1-63 letters, digits, or dashes, and cannot start or end with a dash" 1 + fi + done +} + +# A fresh machine often hasn't joined a network yet, so the geo guess fails +# often; guard it or a `set -e` caller dies before the filter fallback. +omarchy_prompt_timezone() { + local guess status + guess=$(tzupdate -p 2>/dev/null) || guess="" + + if [[ -n $guess ]]; then + timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$guess" --header "Timezone") && status=0 || status=$? + else + timezone=$(timedatectl list-timezones | gum filter --height 10 --header "Timezone") && status=0 || status=$? + fi + ((status == 0)) || return $status + + [[ -n $timezone ]] || timezone="UTC" +}