#!/bin/bash # The configurator normally runs under this small PTY proxy so Ctrl-T can work # as a global tools shortcut even while gum owns the terminal in raw mode. The # child configurator keeps stdout on the existing stream (so generated config and # logs behave normally), while stderr and /dev/tty traffic are relayed through the # PTY. Pressing Ctrl-T kills the current configurator screen, opens Tools, then # starts the configurator over at keyboard selection. if [[ -z "${OMARCHY_CONFIGURATOR_INNER:-}" && -z "${OMARCHY_CONFIGURATOR_NO_KEY_PROXY:-}" ]] && { [[ -t 0 ]] || [[ -t 2 ]]; }; then OMARCHY_CONFIGURATOR_PYTHON=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true) if [[ -n "$OMARCHY_CONFIGURATOR_PYTHON" ]]; then export OMARCHY_CONFIGURATOR_SCRIPT OMARCHY_CONFIGURATOR_SCRIPT=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || printf '%s' "${BASH_SOURCE[0]}") exec "$OMARCHY_CONFIGURATOR_PYTHON" - "$@" <<'PY' import errno import fcntl import os import pty import select import signal import sys import termios import time CTRL_T = b"\x14" SCRIPT = os.environ["OMARCHY_CONFIGURATOR_SCRIPT"] ARGS = sys.argv[1:] try: tty_fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY) except OSError: env = os.environ.copy() env["OMARCHY_CONFIGURATOR_INNER"] = "1" os.execvpe("bash", ["bash", SCRIPT] + ARGS, env) stdout_fd = os.dup(1) original_tty = termios.tcgetattr(tty_fd) current_master = None current_pgid = None def restore_tty(): try: termios.tcsetattr(tty_fd, termios.TCSADRAIN, original_tty) except termios.error: pass def write_tty(data): if data: os.write(tty_fd, data) def copy_window_size(master): try: size = fcntl.ioctl(tty_fd, termios.TIOCGWINSZ, b"\0" * 8) fcntl.ioctl(master, termios.TIOCSWINSZ, size) except OSError: pass def on_winch(signum, frame): if current_master is not None: copy_window_size(current_master) if current_pgid is not None: try: os.killpg(current_pgid, signal.SIGWINCH) except OSError: pass def on_termination(signum, frame): if current_pgid is not None: try: os.killpg(current_pgid, signum) except OSError: pass restore_tty() raise SystemExit(128 + signum) def exit_code(status): if os.WIFEXITED(status): return os.WEXITSTATUS(status) if os.WIFSIGNALED(status): return 128 + os.WTERMSIG(status) return 1 def spawn_child(tools_only=False): global current_master, current_pgid pid, master = pty.fork() if pid == 0: os.dup2(stdout_fd, 1) try: os.close(stdout_fd) except OSError: pass env = os.environ.copy() env["OMARCHY_CONFIGURATOR_INNER"] = "1" env["OMARCHY_CONFIGURATOR_CTRL_T"] = "1" if tools_only: env["OMARCHY_CONFIGURATOR_TOOLS_ONLY"] = "1" else: env.pop("OMARCHY_CONFIGURATOR_TOOLS_ONLY", None) os.execvpe("bash", ["bash", SCRIPT] + ARGS, env) os.set_blocking(master, False) current_master = master current_pgid = pid copy_window_size(master) return pid, master def wait_for_child(pid, timeout): deadline = time.time() + timeout while time.time() < deadline: try: done, status = os.waitpid(pid, os.WNOHANG) except ChildProcessError: return 0 if done == pid: return exit_code(status) time.sleep(0.05) return None def terminate_child(pid): for sig, timeout in ((signal.SIGTERM, 0.5), (signal.SIGKILL, 0.2)): try: os.killpg(pid, sig) except ProcessLookupError: return 0 except OSError: try: os.kill(pid, sig) except OSError: return 0 code = wait_for_child(pid, timeout) if code is not None: return code return 130 def drain_master(master): while True: try: data = os.read(master, 4096) except BlockingIOError: return except OSError: return if not data: return write_tty(data) def relay_child(tools_only=False): global current_master, current_pgid pid, master = spawn_child(tools_only) try: while True: try: done, status = os.waitpid(pid, os.WNOHANG) except ChildProcessError: drain_master(master) return 0 if done == pid: drain_master(master) return exit_code(status) try: readable, _, _ = select.select([tty_fd, master], [], [], 0.1) except InterruptedError: continue if tty_fd in readable: try: data = os.read(tty_fd, 1024) except OSError: data = b"" if not data: continue if not tools_only and CTRL_T in data: before = data.split(CTRL_T, 1)[0] if before: try: os.write(master, before) except OSError: pass terminate_child(pid) drain_master(master) return "tools" try: os.write(master, data) except OSError: pass if master in readable: try: data = os.read(master, 4096) except BlockingIOError: continue except OSError as exc: if exc.errno != errno.EIO: raise data = b"" if data: write_tty(data) finally: current_master = None current_pgid = None try: os.close(master) except OSError: pass signal.signal(signal.SIGWINCH, on_winch) signal.signal(signal.SIGHUP, on_termination) signal.signal(signal.SIGTERM, on_termination) # Use raw-ish input so Ctrl-T/Ctrl-C can be intercepted and relayed, but keep # the real terminal's output processing intact. Full raw mode disables ONLCR, # causing gum/logo newlines to stair-step across the screen. tty_attrs = termios.tcgetattr(tty_fd) tty_attrs[0] &= ~(termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON) tty_attrs[2] |= termios.CS8 tty_attrs[3] &= ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) tty_attrs[6][termios.VMIN] = 1 tty_attrs[6][termios.VTIME] = 0 termios.tcsetattr(tty_fd, termios.TCSADRAIN, tty_attrs) try: while True: result = relay_child(tools_only=False) if result == "tools": relay_child(tools_only=True) continue raise SystemExit(result) finally: restore_tty() try: os.close(stdout_fd) os.close(tty_fd) except OSError: pass PY fi fi unset OMARCHY_CONFIGURATOR_PYTHON # Minimal live-ISO presentation helpers. Keep these local to the ISO: the # installed Omarchy package owns target setup helpers, while the ISO owns the # installer UI that runs before any target packages exist. OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}" if [[ -f /usr/share/omarchy-iso/package-targets ]]; then # shellcheck disable=SC1091 source /usr/share/omarchy-iso/package-targets fi OMARCHY_RUNTIME_PACKAGE="${OMARCHY_RUNTIME_PACKAGE:-omarchy}" OMARCHY_SETTINGS_PACKAGE="${OMARCHY_SETTINGS_PACKAGE:-omarchy-settings}" OMARCHY_NVIM_PACKAGE="${OMARCHY_NVIM_PACKAGE:-omarchy-nvim}" LOGO_PATH="${LOGO_PATH:-$OMARCHY_PATH/logo.txt}" TERM_HEIGHT=${LINES:-24} TERM_WIDTH=${COLUMNS:-80} if [[ -e /dev/tty ]]; then TERM_SIZE=$(stty size 2>/dev/null 0 && BASH_REMATCH[2] > 0 )); then TERM_HEIGHT=${BASH_REMATCH[1]} TERM_WIDTH=${BASH_REMATCH[2]} fi fi if [[ -f $LOGO_PATH ]]; then LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH" 2>/dev/null || echo 0) else LOGO_WIDTH=7 fi (( LOGO_WIDTH > 0 )) || LOGO_WIDTH=7 PADDING_LEFT=$(((TERM_WIDTH - LOGO_WIDTH) / 2)) (( PADDING_LEFT < 0 )) && PADDING_LEFT=0 PADDING_LEFT_SPACES=$(printf "%*s" "$PADDING_LEFT" "") PADDING="0 0 0 $PADDING_LEFT" export TERM_HEIGHT TERM_WIDTH LOGO_PATH PADDING_LEFT PADDING_LEFT_SPACES PADDING export GUM_CONFIRM_PROMPT_FOREGROUND="6" export GUM_CONFIRM_SELECTED_FOREGROUND="0" export GUM_CONFIRM_SELECTED_BACKGROUND="2" export GUM_CONFIRM_UNSELECTED_FOREGROUND="7" export GUM_CONFIRM_UNSELECTED_BACKGROUND="0" export GUM_CHOOSE_PADDING="$PADDING" export GUM_FILTER_PADDING="$PADDING" export GUM_INPUT_PADDING="$PADDING" export GUM_SPIN_PADDING="$PADDING" export GUM_TABLE_PADDING="$PADDING" export GUM_CONFIRM_PADDING="$PADDING" clear_logo() { printf "\033[H\033[2J" if [[ -f $LOGO_PATH ]]; then gum style --foreground 2 --padding "1 0 0 $PADDING_LEFT" "$(<"$LOGO_PATH")" else gum style --foreground 2 --padding "1 0 0 $PADDING_LEFT" "Omarchy" fi } # Ensure tzupdate is available for guessing timezone command -v tzupdate >/dev/null || sudo pacman -Sy --noconfirm tzupdate abort() { gum style "${1:-Aborted installation}" echo gum style "You can retry later by running: ./.automated_script.sh" exit 1 } tools_hint() { [[ -n "${OMARCHY_CONFIGURATOR_CTRL_T:-}" && -z "${OMARCHY_CONFIGURATOR_TOOLS_ONLY:-}" ]] || return 0 # Draw the shortcut as a fixed footer instead of in the form body. Keep the # cursor where the active gum widget expects it; do not print a newline from # the bottom row or the VT will scroll. printf "\0337\033[%s;1H\033[2K%s\033[38;5;8mCtrl-T Tools\033[0m\0338" "$TERM_HEIGHT" "$PADDING_LEFT_SPACES" } step() { clear_logo echo gum style --padding "0 0 0 $PADDING_LEFT" "$1" tools_hint echo } # Padded body text to line up with the logo and gum widgets. say() { gum style --padding "0 0 0 $PADDING_LEFT" "$@" } notice() { clear_logo echo gum spin --spinner "pulse" --title "$1" -- sleep "${2:-2}" echo } # Tools menu — recovery-style actions kept off the main install path. # Inlines its own disk picker so Ctrl-T can open it before disk_form is defined # further down in the script. tools_menu() { while true; do step "Tools" local choice choice=$(gum choose --header "Pick a tool" \ "Open partition tool" \ "Drop to shell" \ "Reboot" \ "Power off" \ "Back to installer") || return 0 case "$choice" in "Open partition tool") local d disks disks=$(lsblk -dpno NAME,TYPE 2>/dev/null | awk '$2=="disk"{print $1}' | grep -E '/dev/(sd|hd|vd|nvme|mmcblk|xv)' || true) if [[ -z "$disks" ]]; then notice "No suitable disks found" 2 continue fi d=$(echo "$disks" | gum choose --header "Select disk") || continue clear cfdisk "$d" || true partprobe "$d" 2>/dev/null || true sync sleep 1 ;; "Drop to shell") clear echo "Type 'exit' to return to the tools menu." bash --norc || true ;; "Reboot") reboot ;; "Power off") poweroff ;; "Back to installer") return 0 ;; esac done } if [[ -n "${OMARCHY_CONFIGURATOR_TOOLS_ONLY:-}" ]]; then tools_menu exit 0 fi # STEP 1: KEYBOARD LAYOUT keyboard_form() { step "Let's setup your machine..." keyboards=$'Azerbaijani|azerty Belarusian|by Belgian|be-latin1 Bosnian|ba 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 Khmer (Cambodia)|khmer 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") || abort keyboard=$(printf '%s\n' "$keyboards" | awk -F'|' -v c="$choice" '$1==c{print $2; exit}') # Only attempt to load keyboard layout if we're on a real console # loadkeys only works on Linux virtual consoles (tty), not in terminal emulators if [[ $(tty 2>/dev/null) == "/dev/tty"* ]]; then loadkeys "$keyboard" 2>/dev/null fi } keyboard_form # STEP 2: USER user_form() { step "Let's setup your user account..." while true; do username=$(gum input --placeholder "Alphanumeric without spaces (like dhh)" --prompt.foreground="#845DF9" --prompt "Username> ") || abort if [[ "$username" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]]; then break else notice "Username must be alphanumeric with no spaces" 1 fi done while true; do password=$(gum input --placeholder "Used for user + root, and disk encryption when enabled" --prompt.foreground="#845DF9" --password --prompt "Password> ") || abort password_confirmation=$(gum input --placeholder "Must match the password you just typed" --prompt.foreground="#845DF9" --password --prompt "Confirm> ") || abort 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 # Hash the password using yescrypt password_hash=$(printf '%s' "$password" | openssl passwd -6 -stdin) full_name=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Full name> ") email_address=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Email address> ") while true; do hostname=$(gum input --placeholder "Letters, digits, and dashes (or return for 'omarchy')" --prompt.foreground="#845DF9" --prompt "Hostname> ") if [[ "$hostname" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$ ]]; then break elif [[ -z $hostname ]]; then hostname="omarchy" break else notice "Hostname must be 1-63 letters, digits, or dashes, and cannot start or end with a dash" 1 fi done # Pick timezone geo_guessed_timezone=$(tzupdate -p 2>/dev/null) if [[ -n $geo_guessed_timezone ]]; then timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$geo_guessed_timezone" --header "Timezone") || abort else timezone=$(timedatectl list-timezones | gum filter --height 10 --header "Timezone") || abort fi } user_form while true; do # Add manual padding since gum table -p doesn't respect padding echo -e "Field,Value Username,$username Password,$(printf "%${#password}s" | tr ' ' '*') Full name,${full_name:-[Skipped]} Email address,${email_address:-[Skipped]} Hostname,$hostname Timezone,$timezone Keyboard,$keyboard" | gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/" echo if gum confirm --negative "No, change it" "Does this look right?"; then break else keyboard_form user_form fi done # STEP 3: DISK get_disk_info() { local device="$1" local size vendor model label # Get basic disk information size=$(lsblk -dno SIZE "$device" 2>/dev/null) vendor=$(lsblk -dno VENDOR "$device" 2>/dev/null | sed 's/ *$//') model=$(lsblk -dno MODEL "$device" 2>/dev/null | sed 's/ *$//') # Combine vendor and model, avoiding duplication label="" if [[ -n $vendor && -n $model ]]; then if [[ $model == *$vendor* ]]; then label="$model" else label="$vendor $model" fi elif [[ -n $model ]]; then label="$model" elif [[ -n $vendor ]]; then label="$vendor" fi # Format display string local display="$device" [[ -n $size ]] && display="$display ($size)" [[ -n $label ]] && display="$display - $label" # Append compact partition summary local part_summary part_summary=$(lsblk -nro TYPE,NAME,FSTYPE,MOUNTPOINT "$device" 2>/dev/null | \ awk '$1=="part" { printf "%s%s%s", s, ($3==""?"unknown":$3), ($4==""?"":"("$4")"); s=", " }') [[ -n $part_summary ]] && display+=" [$part_summary]" echo "$display" } get_root_disk() { local device="$1" local parent [[ -n $device ]] || return 1 # /run/archiso/bootmnt usually resolves to the boot partition, but the # installer picker works with whole disks. Walk back to the parent disk so # the install media itself never shows up as a wipe target. device=$(readlink -f "$device" 2>/dev/null || printf "%s\n" "$device") while true; do parent=$(lsblk -dno PKNAME "$device" 2>/dev/null | tail -n1) [[ -n $parent ]] || break device="/dev/$parent" done if [[ $(lsblk -dno TYPE "$device" 2>/dev/null) == "disk" ]]; then printf "%s\n" "$device" fi } # Compute partition device path, handling NVMe/mmcblk's pN naming. partition_path() { local _disk="$1" _num="$2" if [[ "$_disk" == *nvme* || "$_disk" == *mmcblk* ]]; then echo "${_disk}p${_num}" else echo "${_disk}${_num}" fi } # BitLocker writes the "-FVE-FS-" signature at offset 3 of the partition. detect_bitlocker() { local p sig for p in $(lsblk -lnpo NAME,TYPE "$disk" | awk '$2=="part"{print $1}'); do sig=$(dd if="$p" bs=1 skip=3 count=8 status=none 2>/dev/null) if [[ "$sig" == "-FVE-FS-" ]]; then echo "$p" return 0 fi done return 1 } # Look for an existing Windows ESP (FAT partition containing EFI/Microsoft) on $disk. detect_windows_esp() { local p tmp_mp for p in $(blkid -t TYPE=vfat -o device 2>/dev/null); do [[ "$p" == "$disk"* ]] || continue tmp_mp=$(mktemp -d) if mount -o ro "$p" "$tmp_mp" 2>/dev/null; then if [[ -d "$tmp_mp/EFI/Microsoft" ]]; then umount "$tmp_mp"; rmdir "$tmp_mp" echo "$p" return 0 fi umount "$tmp_mp" fi rmdir "$tmp_mp" 2>/dev/null || true done return 1 } to_gb() { awk -v bytes="$1" 'BEGIN {printf "%.1fGB", bytes/1024/1024/1024}' } # Shared error screen for every "can't fit Omarchy here" outcome of the # partition install. Offers the partition tool as the meaningful second # action so users have a way to actually fix the situation. not_enough_space() { local available="${1:-0}" step "Not enough free space on $disk" say --foreground 1 "$disk has $(to_gb $available) of usable free space; Omarchy needs at least 32GB." say "Open the partition tool to free at least 32GB on $disk, then try again." echo if ! gum confirm --affirmative "Back" --negative "Open partition tool" "Return to installation mode?"; then open_partition_tool fi return 1 } run_partition_install() { local dry=false [[ "${1:-}" == "dry" ]] && dry=true step "Checking for BitLocker on $disk" local bl_part bl_part=$(detect_bitlocker || true) if [[ -n "$bl_part" ]]; then say --foreground 1 "BitLocker signature detected on $bl_part." say "Suspend or disable BitLocker in Windows before installing alongside it." abort "Aborted: BitLocker is active on this disk." fi # Omarchy always creates its own dedicated ESP in free space and never # adopts an existing (Windows) ESP. Sharing the Windows ESP is fragile: # Windows Update/repair can reclaim or reformat it, the typical 100-260MiB # Windows ESP is far too small for our Unified Kernel Images, and a shared # ESP forces the install unencrypted. A separate Linux ESP keeps Windows # and Omarchy fully isolated and keeps LUKS on the table. # # shared_esp stays declared but is intentionally always empty so every # downstream `[[ -z "$shared_esp" ]]` branch takes the dedicated-ESP path. step "Checking existing EFI partitions on $disk" local shared_esp="" local detected_win_esp detected_win_esp=$(detect_windows_esp || true) if [[ -n "$detected_win_esp" ]]; then say "Found a Windows ESP at $detected_win_esp — leaving it untouched." say "Omarchy will create its own dedicated EFI partition in free space." else say "A new EFI partition will be created in free space." fi step "Analyzing free space on $disk" partprobe "$disk" 2>/dev/null || true sleep 1 # An unlabeled disk has no partition table for parted to scan, so we # synthesize a whole-disk free region and mklabel gpt before mkpart. local FREE_SPACE_INFO needs_mklabel=false pt_type disk_size_b pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null) if [[ -z "$pt_type" ]]; then disk_size_b=$(lsblk -bdno SIZE "$disk" 2>/dev/null) if [[ -n "$disk_size_b" && "$disk_size_b" -gt 0 ]]; then local _mib=$((1024 * 1024)) local _start=$_mib local _end=$(( (disk_size_b - _mib) / _mib * _mib )) FREE_SPACE_INFO="$_start $_end $((_end - _start))" needs_mklabel=true fi else # -s (script mode) keeps parted from prompting on stderr for warnings # like a stale GPT backup; without it the configurator hangs invisibly. FREE_SPACE_INFO=$(parted -ms "$disk" unit B print free /dev/null | \ awk -F: ' $NF ~ /free/ { start=$2; end=$3; size=$4; gsub(/B/, "", start); gsub(/B/, "", end); gsub(/B/, "", size); if (size+0 > max_size+0) { max_size=size; max_start=start; max_end=end; } } END { if (max_size+0 > 0) printf "%s %s %s\n", max_start, max_end, max_size; }') fi if [[ -z "$FREE_SPACE_INFO" ]]; then not_enough_space 0 return $? fi local FREE_START_B FREE_END_B FREE_SIZE_B FREE_START_B=$(echo "$FREE_SPACE_INFO" | awk '{print $1}') FREE_END_B=$(echo "$FREE_SPACE_INFO" | awk '{print $2}') FREE_SIZE_B=$(echo "$FREE_SPACE_INFO" | awk '{print $3}') local mib=$((1024 * 1024)) gib=$((1024 * 1024 * 1024)) local ALIGN=$mib local EFI_SIZE_B=$((2 * gib)) local MIN_INSTALL_B=$((32 * gib)) # Minimum total Omarchy allocation local EFI_START_B=0 EFI_END_B=0 ROOT_START_B ROOT_END_B if [[ -z "$shared_esp" ]]; then EFI_START_B=$(( (FREE_START_B + ALIGN - 1) / ALIGN * ALIGN )) EFI_END_B=$((EFI_START_B + EFI_SIZE_B)) ROOT_START_B=$(( (EFI_END_B + 1 + ALIGN - 1) / ALIGN * ALIGN )) else ROOT_START_B=$(( (FREE_START_B + ALIGN - 1) / ALIGN * ALIGN )) fi ROOT_END_B=$((FREE_END_B / ALIGN * ALIGN)) if (( ROOT_END_B <= ROOT_START_B )); then not_enough_space "$FREE_SIZE_B" return $? fi local INSTALL_START_B INSTALL_MAX_B INSTALL_MAX_GIB INSTALL_SIZE_GIB if [[ -z "$shared_esp" ]]; then INSTALL_START_B=$EFI_START_B else INSTALL_START_B=$ROOT_START_B fi INSTALL_MAX_B=$((ROOT_END_B - INSTALL_START_B)) if (( INSTALL_MAX_B < MIN_INSTALL_B )); then not_enough_space "$INSTALL_MAX_B" return $? fi INSTALL_MAX_GIB=$((INSTALL_MAX_B / gib)) while true; do step "Choose Omarchy size" say "Free region: $(to_gb $FREE_START_B) — $(to_gb $FREE_END_B) ($(to_gb $FREE_SIZE_B))" if [[ -z "$shared_esp" ]]; then say "The size includes a new 2GiB EFI partition; the rest becomes root." else say "Using existing ESP: $shared_esp" fi say "Omarchy can use 32–${INSTALL_MAX_GIB}GiB total. Leave the default to use all available space." echo INSTALL_SIZE_GIB=$(gum input \ --value "$INSTALL_MAX_GIB" \ --placeholder "32-${INSTALL_MAX_GIB}" \ --prompt.foreground="#845DF9" \ --prompt "Omarchy size in GiB> ") || return 1 if [[ $INSTALL_SIZE_GIB =~ ^[0-9]+$ ]] && (( INSTALL_SIZE_GIB >= 32 && INSTALL_SIZE_GIB <= INSTALL_MAX_GIB )); then break fi notice "Enter a whole number from 32 to ${INSTALL_MAX_GIB}" 1 done if [[ -z "$shared_esp" ]]; then ROOT_END_B=$((EFI_START_B + INSTALL_SIZE_GIB * gib)) else ROOT_END_B=$((ROOT_START_B + INSTALL_SIZE_GIB * gib)) fi # Default to encrypted and hide the unencrypted path behind Ctrl+C, # matching the full-disk flow. The dedicated Omarchy ESP always supports # LUKS, so encryption is always on the table here. local mode="encrypted" affirmative confirm_status while true; do step "Proposed layout on $disk" say "Free region: $(to_gb $FREE_START_B) — $(to_gb $FREE_END_B) ($(to_gb $FREE_SIZE_B))" if [[ -z "$shared_esp" ]]; then say " New EFI partition: $(to_gb $EFI_START_B) — $(to_gb $EFI_END_B) (2GB FAT32)" else say " Shared ESP: $shared_esp (Windows, mounted at /efi)" fi say " New root (Btrfs): $(to_gb $ROOT_START_B) — $(to_gb $ROOT_END_B) ($(to_gb $((ROOT_END_B - ROOT_START_B))))" echo case $mode in encrypted) say --foreground 8 "Press Ctrl+C for unencrypted install." affirmative="Yes, install" ;; unencrypted) affirmative="Yes, install without encryption" ;; esac echo gum confirm --affirmative "$affirmative" --negative "Cancel" \ "Create Omarchy partitions in the free space on $disk?" confirm_status=$? case $confirm_status in 0) [[ $mode == "encrypted" ]] && encrypt_installation=true || encrypt_installation=false break ;; 1) return 1 ;; 130) case $mode in encrypted) mode="unencrypted" ;; unencrypted) mode="encrypted" ;; esac ;; *) abort ;; esac done local esp_mount_in_target if [[ "$encrypt_installation" == "true" ]]; then esp_mount_in_target=/boot else esp_mount_in_target=/efi fi # Detect T2 Mac and set appropriate kernel if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then kernel_choice="linux-t2" else kernel_choice="linux" fi local last_part efi_part_num root_part_num last_part=$(lsblk -n -o PARTN "$disk" 2>/dev/null | grep -E '^[0-9]+$' | sort -n | tail -1) if [[ -z "$shared_esp" ]]; then efi_part_num=$(( ${last_part:-0} + 1 )) root_part_num=$(( efi_part_num + 1 )) else efi_part_num=0 # not created by us root_part_num=$(( ${last_part:-0} + 1 )) fi local efi_dev root_partition_device root_mapper if [[ -n "$shared_esp" ]]; then efi_dev="$shared_esp" else efi_dev=$(partition_path "$disk" "$efi_part_num") fi root_partition_device=$(partition_path "$disk" "$root_part_num") if [[ "$encrypt_installation" == "true" ]]; then root_mapper="/dev/mapper/omarchy_root" else root_mapper="$root_partition_device" fi if $dry; then $needs_mklabel && say "[dry] would initialize GPT on $disk" say "[dry] would create root partition at $root_partition_device" [[ -z "$shared_esp" ]] && say "[dry] would create EFI partition at $efi_dev" say "[dry] encrypted=$encrypt_installation kernel=$kernel_choice" else step "Creating partitions on $disk" if $needs_mklabel; then parted --script "$disk" mklabel gpt partprobe "$disk" 2>/dev/null || true sleep 1 fi if [[ -z "$shared_esp" ]]; then parted --script "$disk" mkpart primary fat32 "${EFI_START_B}B" "${EFI_END_B}B" parted --script "$disk" set "$efi_part_num" esp on parted --script "$disk" name "$efi_part_num" "OMARCHY_EFI" fi parted --script "$disk" mkpart primary btrfs "${ROOT_START_B}B" "${ROOT_END_B}B" parted --script "$disk" name "$root_part_num" "OMARCHY_ROOT" partprobe "$disk" sync sleep 2 local i for i in 1 2 3 4 5; do if [[ -b "$root_partition_device" ]] && { [[ -n "$shared_esp" ]] || [[ -b "$efi_dev" ]]; }; then break fi sleep 1 done if [[ "$encrypt_installation" == "true" ]]; then step "Setting up LUKS2 on $root_partition_device" printf "%s" "$password" | cryptsetup luksFormat --type luks2 --batch-mode "$root_partition_device" - printf "%s" "$password" | cryptsetup open "$root_partition_device" omarchy_root - root_mapper="/dev/mapper/omarchy_root" else wipefs -af "$root_partition_device" root_mapper="$root_partition_device" fi step "Creating Btrfs filesystem and subvolumes" mkfs.btrfs -f -L OMARCHY "$root_mapper" for i in 1 2 3 4 5; do [[ -b "$root_mapper" ]] && break sleep 1 done mkdir -p /mnt/btrfs-root mount "$root_mapper" /mnt/btrfs-root btrfs subvolume create /mnt/btrfs-root/@ btrfs subvolume create /mnt/btrfs-root/@home btrfs subvolume create /mnt/btrfs-root/@log btrfs subvolume create /mnt/btrfs-root/@pkg umount /mnt/btrfs-root rmdir /mnt/btrfs-root mount -o noatime,compress=zstd,subvol=@ "$root_mapper" /mnt mkdir -p /mnt/home /mnt/var/log /mnt/var/cache/pacman/pkg /mnt"$esp_mount_in_target" mount -o noatime,compress=zstd,subvol=@home "$root_mapper" /mnt/home mount -o noatime,compress=zstd,subvol=@log "$root_mapper" /mnt/var/log mount -o noatime,compress=zstd,subvol=@pkg "$root_mapper" /mnt/var/cache/pacman/pkg if [[ -z "$shared_esp" ]]; then mkfs.fat -F32 -n OMARCHY_EFI "$efi_dev" fi mount "$efi_dev" /mnt"$esp_mount_in_target" fi clear echo "$full_name" >user_full_name.txt echo "$email_address" >user_email_address.txt printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt local password_escaped password_hash_escaped username_escaped password_escaped=$(echo -n "$password" | jq -Rsa) password_hash_escaped=$(echo -n "$password_hash" | jq -Rsa) username_escaped=$(echo -n "$username" | jq -Rsa) local credentials_encryption_line="" if [[ "$encrypt_installation" == "true" ]]; then credentials_encryption_line=" \"encryption_password\": $password_escaped," fi cat <<-_EOF_ >user_credentials.json { $credentials_encryption_line "root_enc_password": $password_hash_escaped, "users": [ { "enc_password": $password_hash_escaped, "groups": [], "sudo": true, "username": $username_escaped } ] } _EOF_ local luks_uuid_json="null" luks_uuid if [[ "$encrypt_installation" == "true" ]]; then if $dry; then luks_uuid="00000000-0000-0000-0000-000000000000" else luks_uuid=$(blkid -s UUID -o value "$root_partition_device") fi luks_uuid_json="\"$luks_uuid\"" fi cat <<-_EOF_ >user_configuration.json { "app_config": null, "archinstall-language": "English", "auth_config": {}, "audio_config": { "audio": "pipewire" }, "bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false }, "custom_commands": [], "omarchy_install": { "mode": "protected", "target_mount": "/mnt", "boot": { "esp_mount": "$esp_mount_in_target", "esp_path": "/EFI/limine", "efi_binary": "limine_x64.efi", "enable_fallback": false }, "storage": { "esp_device": "$efi_dev", "root_device": "$root_partition_device", "root_mapper": "$root_mapper", "luks_uuid": $luks_uuid_json, "kernel": "$kernel_choice" } }, "disk_config": { "config_type": "pre_mounted_config", "mountpoint": "/mnt" }, "hostname": "$hostname", "kernels": [ "$kernel_choice" ], "network_config": { "type": "iso" }, "ntp": true, "parallel_downloads": 8, "script": null, "services": [], "swap": true, "timezone": "$timezone", "locale_config": { "kb_layout": "$keyboard", "sys_enc": "UTF-8", "sys_lang": "en_US.UTF-8" }, "mirror_config": { "custom_repositories": [], "custom_servers": [ {"url": "https://mirror.omarchy.org/\$repo/os/\$arch"}, {"url": "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch"}, {"url": "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"} ], "mirror_regions": {}, "optional_repositories": [] }, "packages": [ "base-devel", "git", "omarchy-keyring", "$OMARCHY_SETTINGS_PACKAGE", "$OMARCHY_RUNTIME_PACKAGE" ], "profile_config": { "gfx_driver": null, "greeter": null, "profile": {} }, "version": "3.0.9" } _EOF_ if $dry; then echo -e "User Configuration:" cat user_configuration.json echo -e "\n\nUser Credentials:" cat user_credentials.json echo -e "\n\nUser Full Name:" cat user_full_name.txt echo -e "\nUser Email Address:" cat user_email_address.txt echo -e "\nDisk Encryption:" cat user_encrypt_installation.txt rm -f user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt fi } disk_form() { step "Let's select where to install Omarchy..." # Don't offer the install media as an option (Arch ISO mounts it here) local boot_source local exclude_disk boot_source=$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null || true) exclude_disk=$(get_root_disk "$boot_source") # List all installable disks, excluding the boot device if present local available_disks available_disks=$( lsblk -dpno NAME,TYPE | awk '$2=="disk"{print $1}' | grep -E '/dev/(sd|hd|vd|nvme|mmcblk|xv)' | { if [[ -n "$exclude_disk" ]]; then grep -Fvx "$exclude_disk"; else cat; fi; } ) # Get available disks and format them with info local disk_options="" while IFS= read -r device; do if [[ -n "$device" ]]; then disk_info=$(get_disk_info "$device") disk_options="$disk_options$disk_info"$'\n' fi done <<<"$available_disks" selected_display=$(echo "$disk_options" | gum choose --header "Select install disk") || abort disk=$(echo "$selected_display" | awk '{print $1}') } disk_form # STEP 4: INSTALL MODE open_partition_tool() { step "Partition tool for $disk" gum style "Create unallocated free space for Omarchy, then write changes and quit." gum style --foreground 8 "Do not create an Omarchy partition here — leave the target area as Free space." gum style --foreground 8 "Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists." echo gum confirm --affirmative "Open cfdisk" --negative "Back" "Open partition tool for $disk?" || return 0 clear cfdisk "$disk" || true partprobe "$disk" 2>/dev/null || true sync sleep 1 } install_mode_form() { local choices=("Full disk install") # The free-space/protected path registers an EFI boot entry and is UEFI-only. # Full-disk installs still support BIOS through the orchestrator's BIOS branch. if [[ -d /sys/firmware/efi ]]; then choices+=("Free space install (alongside existing data)") fi choices+=("Choose a different disk") step "Installation mode" install_mode=$(gum choose --header "How should Omarchy be installed on $disk?" "${choices[@]}") || abort } while true; do install_mode_form case "$install_mode" in "Full disk install") break ;; "Free space install (alongside existing data)") if run_partition_install "$@"; then exit 0 fi ;; "Choose a different disk") disk_form ;; esac done confirm_disk_overwrite() { local mode="encrypted" local affirmative confirm_status while true; do clear_logo echo gum style "Everything will be overwritten. There is no recovery possible." if [[ $mode == "encrypted" ]]; then gum style --foreground 8 "Press Ctrl+C for unencrypted install." affirmative="Yes, install" else affirmative="Yes, install without encryption" fi echo gum confirm --affirmative "$affirmative" --negative "No, change it" "Confirm overwriting ${disk}" confirm_status=$? case $confirm_status in 0) [[ $mode == "encrypted" ]] && encrypt_installation=true || encrypt_installation=false return 0 ;; 1) return 1 ;; 130) [[ $mode == "encrypted" ]] && mode="unencrypted" || mode="encrypted" ;; *) abort ;; esac done } while ! confirm_disk_overwrite; do disk_form done clear # Save user full name and email address, so the autostart can # set them as OMARCHY_USER_NAME and OMARCHY_USER_EMAIL echo "$full_name" >user_full_name.txt echo "$email_address" >user_email_address.txt # Escape username and password for safe inclusion password_escaped=$(echo -n "$password" | jq -Rsa) password_hash_escaped=$(echo -n "$password_hash" | jq -Rsa) username_escaped=$(echo -n "$username" | jq -Rsa) if [[ $encrypt_installation == true ]]; then credentials_encryption_line=" \"encryption_password\": $password_escaped," else credentials_encryption_line="" fi cat <<-_EOF_ >user_credentials.json { $credentials_encryption_line "root_enc_password": $password_hash_escaped, "users": [ { "enc_password": $password_hash_escaped, "groups": [], "sudo": true, "username": $username_escaped } ] } _EOF_ printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt # Setup partition layout disk_size=$(lsblk -bdno SIZE "$disk") mib=$((1024 * 1024)) gib=$((mib * 1024)) disk_size_in_mib=$((disk_size / mib * mib)) # Rounds to nearest MiB gpt_backup_reserve=$((mib)) boot_partition_start=$((mib)) boot_partition_size=$((2 * gib)) main_partition_start=$((boot_partition_size + boot_partition_start)) main_partition_size=$((disk_size_in_mib - main_partition_start - gpt_backup_reserve)) # Detect T2 Mac and set appropriate kernel if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then kernel_choice="linux-t2" else kernel_choice="linux" fi if [[ $encrypt_installation == true ]]; then disk_encryption_config=$(cat <<-_EOF_ , "disk_encryption": { "encryption_type": "luks", "lvm_volumes": [], "iter_time": 2000, "partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ], "encryption_password": $password_escaped } _EOF_ ) else disk_encryption_config="" fi cat <<-_EOF_ >user_configuration.json { "app_config": null, "archinstall-language": "English", "auth_config": {}, "audio_config": { "audio": "pipewire" }, "bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false }, "custom_commands": [], "omarchy_install": { "mode": "full_disk", "target_mount": "/mnt", "boot": { "esp_mount": "/boot", "esp_path": "/EFI/limine", "efi_binary": "limine_x64.efi", "enable_fallback": true }, "storage": { "kernel": "$kernel_choice" } }, "disk_config": { "btrfs_options": { "snapshot_config": { "type": "Snapper" } }, "config_type": "default_layout", "device_modifications": [ { "device": "$disk", "partitions": [ { "btrfs": [], "dev_path": null, "flags": [ "boot", "esp" ], "fs_type": "fat32", "mount_options": [], "mountpoint": "/boot", "obj_id": "ea21d3f2-82bb-49cc-ab5d-6f81ae94e18d", "size": { "sector_size": { "unit": "B", "value": 512 }, "unit": "B", "value": $boot_partition_size }, "start": { "sector_size": { "unit": "B", "value": 512 }, "unit": "B", "value": $boot_partition_start }, "status": "create", "type": "primary" }, { "btrfs": [ { "mountpoint": "/", "name": "@" }, { "mountpoint": "/home", "name": "@home" }, { "mountpoint": "/var/log", "name": "@log" }, { "mountpoint": "/var/cache/pacman/pkg", "name": "@pkg" } ], "dev_path": null, "flags": [], "fs_type": "btrfs", "mount_options": [ "compress=zstd" ], "mountpoint": null, "obj_id": "8c2c2b92-1070-455d-b76a-56263bab24aa", "size": { "sector_size": { "unit": "B", "value": 512 }, "unit": "B", "value": $main_partition_size }, "start": { "sector_size": { "unit": "B", "value": 512 }, "unit": "B", "value": $main_partition_start }, "status": "create", "type": "primary" } ], "wipe": true } ]$disk_encryption_config }, "hostname": "$hostname", "kernels": [ "$kernel_choice" ], "network_config": { "type": "iso" }, "ntp": true, "parallel_downloads": 8, "script": null, "services": [], "swap": true, "timezone": "$timezone", "locale_config": { "kb_layout": "$keyboard", "sys_enc": "UTF-8", "sys_lang": "en_US.UTF-8" }, "mirror_config": { "custom_repositories": [], "custom_servers": [ {"url": "https://mirror.omarchy.org/\$repo/os/\$arch"}, {"url": "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch"}, {"url": "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"} ], "mirror_regions": {}, "optional_repositories": [] }, "packages": [ "base-devel", "git", "omarchy-keyring", "$OMARCHY_SETTINGS_PACKAGE", "$OMARCHY_RUNTIME_PACKAGE" ], "profile_config": { "gfx_driver": null, "greeter": null, "profile": {} }, "version": "3.0.9" } _EOF_ if [[ $1 == "dry" ]]; then echo -e "User Configuration:" cat user_configuration.json echo -e "\n\nUser Credentials:" cat user_credentials.json echo -e "\n\nUser Full Name:" cat user_full_name.txt echo -e "\nUser Email Address:" cat user_email_address.txt echo -e "\nDisk Encryption:" cat user_encrypt_installation.txt rm user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt fi