From 4c8002cda710e2ecfda8a9fb7e50db94fdc689da Mon Sep 17 00:00:00 2001 From: s8n Date: Sat, 2 May 2026 04:39:27 +0100 Subject: [PATCH] v0.5.1: gum installer + full veilor-os kickstart generation (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * v0.5.1: gum installer + full veilor-os ks generation Two changes, one commit (matches v0.5.1 milestone): 1. Swap whiptail → gum (charm.sh) - Source /usr/share/veilor-os/assets/installer/colors.gum at top so all prompts pick up branded GUM_* env vars. - Render banner.txt via `gum style --border rounded`. - Wrap every prompt behind prompt_choose / prompt_input / prompt_password / prompt_confirm / prompt_message / prompt_error helpers that dispatch gum→whiptail based on `command -v gum`. Defensive: minimal images without /usr/local/bin/gum still get a working TUI. - Main menu items now use literal labels (case-matched), not 1..5 tags. 2. Generated kickstart now installs full veilor-os Previously emitted a vanilla F43 KDE + ~12 hardening packages with no overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks 63-141) for %packages, plus: - %post --nochroot copies overlay/, scripts/, assets/ from /run/install/repo/veilor (single source — boot ISO mount path). - %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh, selinux/build-policy.sh, kde-theme-apply.sh. - `chage -d 0 admin` so first login forces password change. (Account itself is created by anaconda from the `user` directive — admin pw collected via gum is passed through --plaintext.) - `systemctl set-default graphical.target` (real install boots SDDM, not the TTY1 installer like live). - Drops live-only entries (livesys-scripts, anaconda-live, dracut-live, isomd5sum, xorriso, livesys.service enables). Tested: bash -n clean; ksvalidator on a substituted-placeholder copy exits 0. gum binary itself (/usr/local/bin/gum) is vendored by a separate build-side change — not in this PR. * fix: escape sed special chars + reject & | / in passwords Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched pattern. Two layers of defense: 1. validate_pw rejects & | / newline at input 2. sed_escape() helper escapes any remaining special chars before substitution --------- Co-authored-by: veilor-org Co-authored-by: Claude Opus 4.7 --- overlay/usr/local/sbin/veilor-installer | 450 +++++++++++++++++++----- 1 file changed, 367 insertions(+), 83 deletions(-) diff --git a/overlay/usr/local/sbin/veilor-installer b/overlay/usr/local/sbin/veilor-installer index eab8337..f1c7047 100644 --- a/overlay/usr/local/sbin/veilor-installer +++ b/overlay/usr/local/sbin/veilor-installer @@ -3,34 +3,65 @@ # Runs on tty1 in place of getty (live ISO boot path). # # Flow: -# 1. ASCII banner -# 2. Menu: Install / Live shell / Reboot / Power off -# 3. If Install: collect answers via whiptail (disk, hostname, LUKS pw, +# 1. ASCII banner (assets/installer/banner.txt) +# 2. Menu: Install / Live desktop / Live shell / Reboot / Power off +# 3. If Install: collect answers via gum (disk, hostname, LUKS pw, # admin pw, locale) # 4. Generate /run/install/veilor-generated.ks from template + answers +# (full veilor-os install: package list + overlay + scripts + harden) # 5. Exec anaconda --kickstart=/run/install/veilor-generated.ks # 6. On finish: reboot into installed system # -# v0.5.0 — first cut. v0.5.1 swaps whiptail for gum (Go TUI, prettier). +# v0.5.1 — gum (charm.sh) replaces whiptail; whiptail kept as fallback. +# Generated kickstart now installs full veilor-os (matches live ks). set -uo pipefail export TERM="${TERM:-linux}" LOG=/var/log/veilor-installer.log exec > >(tee -a "$LOG") 2>&1 +# ── Branded styling for gum ───────────────────────────────────────────── +# colors.gum sets GUM_* env vars — pure-black palette from veilor-black KDE. +# Sourced at top so every prompt below picks up branded colors. +COLORS=/usr/share/veilor-os/assets/installer/colors.gum +[[ -r $COLORS ]] && source "$COLORS" + +BANNER_FILE=/usr/share/veilor-os/assets/installer/banner.txt + +# Detect TUI backend once. gum is preferred; whiptail is the fallback so the +# installer keeps working on minimal images or if /usr/local/bin/gum is +# missing (e.g. broken vendored binary, /usr remount issues). +if command -v gum >/dev/null 2>&1; then + TUI=gum +elif command -v whiptail >/dev/null 2>&1; then + TUI=whiptail +else + echo "[ERR] neither gum nor whiptail available — cannot run TUI" >&2 + exit 1 +fi + banner() { clear - cat << 'EOF' + if [[ -r $BANNER_FILE ]]; then + if [[ $TUI == gum ]]; then + # gum style draws a rounded border + padding around the banner. + gum style --border rounded --margin "1" --padding "1 2" \ + --border-foreground "${VEILOR_DIM:-240}" \ + --foreground "${VEILOR_FG:-15}" \ + "$(cat "$BANNER_FILE")" + else + cat "$BANNER_FILE" + echo + fi + else + # Fallback ASCII if banner.txt missing (older overlay). + cat << 'EOF' - ▌ ▌▙▀▖▌ ▐▌▛▀▖▌ ▖▙▀▖▖▖ - ▙▖▌█ ▐▖▟▘▙▄▘▌ ▌▌ ▙▟ - ▘ veilor-os installer hardened. branded. yours. EOF - echo "──────────────────────────────────────────" - echo + fi } require_tty() { @@ -40,82 +71,176 @@ require_tty() { fi } +# ── TUI wrapper functions ─────────────────────────────────────────────── +# Each prompt_* call abstracts gum / whiptail. Always emit the chosen value +# on stdout; non-zero exit on cancel/ESC. Callers use `||` to propagate. + +# prompt_choose
[opt2 ...] +# Single-select menu. Returns the selected option literal on stdout. +prompt_choose() { + local header=$1; shift + if [[ $TUI == gum ]]; then + gum choose --header "$header" "$@" + else + # whiptail menu needs tag/desc pairs. Use the option as both. + local args=() + local opt + for opt in "$@"; do args+=("$opt" "$opt"); done + whiptail --title "veilor-os" --menu "$header" 18 70 8 \ + "${args[@]}" 3>&1 1>&2 2>&3 + fi +} + +# prompt_choose_pairs
[tag2 desc2 ...] +# Tag/description menu. Returns the chosen tag. +# Used when display label differs from machine-readable value (e.g. disk +# path /dev/nvme0n1 vs description "476G WDC PC SN740"). +prompt_choose_pairs() { + local header=$1; shift + if [[ $TUI == gum ]]; then + # gum has no tag/desc concept — render "tag — desc" lines, parse. + local lines=() i=1 + while (( i <= $# )); do + local tag=${!i}; ((i++)) + local desc=${!i}; ((i++)) + lines+=("$tag — $desc") + done + local picked + picked=$(printf '%s\n' "${lines[@]}" | gum choose --header "$header") || return 1 + # Strip " — desc" suffix to recover the tag. + echo "${picked%% — *}" + else + whiptail --title "veilor-os" --menu "$header" 18 70 8 \ + "$@" 3>&1 1>&2 2>&3 + fi +} + +# prompt_input
[default] +prompt_input() { + local header=$1 default=${2:-} + if [[ $TUI == gum ]]; then + gum input --header "$header" --value "$default" + else + whiptail --title "veilor-os" --inputbox "$header" 10 60 "$default" \ + 3>&1 1>&2 2>&3 + fi +} + +# prompt_password
+prompt_password() { + local header=$1 + if [[ $TUI == gum ]]; then + gum input --password --header "$header" + else + whiptail --title "veilor-os" --passwordbox "$header" 10 60 \ + 3>&1 1>&2 2>&3 + fi +} + +# prompt_confirm +# Exits 0 on yes, 1 on no — matches whiptail --yesno semantics. +prompt_confirm() { + local msg=$1 + if [[ $TUI == gum ]]; then + gum confirm "$msg" + else + whiptail --title "Confirm" --yesno "$msg" 16 60 + fi +} + +# prompt_message +# Non-blocking notice. gum has no msgbox; print styled + sleep. +prompt_message() { + local msg=$1 + if [[ $TUI == gum ]]; then + gum style --foreground "${VEILOR_FG:-15}" --border rounded \ + --border-foreground "${VEILOR_DIM:-240}" --padding "1 2" -- "$msg" + sleep 2 + else + whiptail --title "veilor-os" --msgbox "$msg" 10 60 + fi +} + +# prompt_error +# Same as message but with a red foreground for visibility. +prompt_error() { + local msg=$1 + if [[ $TUI == gum ]]; then + gum style --foreground 1 --border rounded --border-foreground 1 \ + --padding "1 2" -- "$msg" + sleep 2 + else + whiptail --title "Error" --msgbox "$msg" 10 60 + fi +} + main_menu() { - local choice - choice=$(whiptail --title "veilor-os" \ - --menu "Welcome. What would you like to do?" 16 60 5 \ - "1" "Install veilor-os to disk" \ - "2" "Try live — desktop (KDE Plasma)" \ - "3" "Try live — shell" \ - "4" "Reboot" \ - "5" "Power off" \ - 3>&1 1>&2 2>&3) - echo "$choice" + prompt_choose "Welcome. What would you like to do?" \ + "Install veilor-os to disk" \ + "Try live — desktop (KDE Plasma)" \ + "Try live — shell" \ + "Reboot" \ + "Power off" } collect_answers() { local disk hostname luks_pw admin_pw locale - local disks_list + local disks_pairs # ── Disk ── - # Build "tag description" pairs for whiptail. Model strings have spaces - # (e.g. "WD PC SN740"), so collapse model to underscores for menu. - disks_list=$(lsblk -dpno NAME,SIZE,MODEL | grep -E '^/dev/(sd|nvme|vd|mmcblk)' | \ - awk '{name=$1; size=$2; $1=""; $2=""; sub(/^ +/,""); gsub(/ /,"_"); model=$0; if(model=="")model="unknown"; print name, size"_"model}') - if [[ -z $disks_list ]]; then - whiptail --title "veilor-os" --msgbox "No installable disks found." 8 50 + # Build whiptail-style "tag desc" pairs. prompt_choose_pairs reshapes + # for gum if needed. + # shellcheck disable=SC2207 + disks_pairs=($(lsblk -dpno NAME,SIZE,MODEL | grep -E '^/dev/(sd|nvme|vd|mmcblk)' | \ + awk '{name=$1; size=$2; $1=""; $2=""; sub(/^ +/,""); gsub(/ /,"_"); model=$0; if(model=="")model="unknown"; print name, size"_"model}')) + if [[ ${#disks_pairs[@]} -eq 0 ]]; then + prompt_error "No installable disks found." return 1 fi - disk=$(whiptail --title "Select install disk" \ - --menu "WARNING: selected disk will be ERASED." 18 70 8 \ - $disks_list 3>&1 1>&2 2>&3) || return 1 + disk=$(prompt_choose_pairs "Select install disk (will be ERASED)" "${disks_pairs[@]}") || return 1 # ── Hostname ── - hostname=$(whiptail --title "Hostname" \ - --inputbox "Set hostname:" 10 60 "veilor" \ - 3>&1 1>&2 2>&3) || return 1 + hostname=$(prompt_input "Set hostname" "veilor") || return 1 - # Reject shell-special chars in passwords. Generated kickstart writes - # them via heredoc + sed substitution; bare $, ", \, ` would corrupt - # the ks line or partially expand. 8-char min for entropy. + # Reject shell-special and sed-special chars in passwords. Generated + # kickstart writes them via heredoc + sed substitution; bare $, ", \, ` + # would corrupt the ks line or partially expand at heredoc time. + # &, |, /, newline are sed-special: & expands to the matched pattern + # (so `aA1!@#%^&*()` becomes `aA1!@#%^__ADMIN_PW__*()`), | is our + # delimiter, / would match if delimiter changes, newline breaks the + # sed expression. sed_escape() below adds defense-in-depth, but we + # also reject these at input so the user sees an immediate error + # rather than a corrupted ks file. 8-char min for entropy. validate_pw() { local pw=$1 label=$2 if [[ ${#pw} -lt 8 ]]; then - whiptail --title "Weak $label" --msgbox "Min 8 chars." 8 40 + prompt_error "Weak $label — minimum 8 characters." return 1 fi - if [[ $pw =~ [\"\$\\\`] ]]; then - whiptail --title "Invalid $label" --msgbox \ - "Cannot contain: \" \$ \\ \`" 8 50 + if [[ $pw =~ [\"\$\\\`\&\|/$'\n'] ]]; then + prompt_error "Invalid $label — cannot contain: \" \$ \\ \` & | / newline" return 1 fi return 0 } # ── LUKS passphrase ── - luks_pw=$(whiptail --title "Disk encryption" \ - --passwordbox "LUKS passphrase (full-disk encryption):" 10 60 \ - 3>&1 1>&2 2>&3) || return 1 + luks_pw=$(prompt_password "LUKS passphrase (full-disk encryption)") || return 1 validate_pw "$luks_pw" "passphrase" || return 1 # ── Admin password ── - admin_pw=$(whiptail --title "Admin password" \ - --passwordbox "Admin user password (login after install):" 10 60 \ - 3>&1 1>&2 2>&3) || return 1 + admin_pw=$(prompt_password "Admin user password (login after install)") || return 1 validate_pw "$admin_pw" "password" || return 1 # ── Locale ── - locale=$(whiptail --title "Locale" \ - --menu "Choose locale:" 14 50 4 \ - "en_GB.UTF-8" "English (UK)" \ - "en_US.UTF-8" "English (US)" \ - "de_DE.UTF-8" "Deutsch" \ - "fr_FR.UTF-8" "Francais" \ - 3>&1 1>&2 2>&3) || return 1 + locale=$(prompt_choose "Choose locale" \ + "en_GB.UTF-8" \ + "en_US.UTF-8" \ + "de_DE.UTF-8" \ + "fr_FR.UTF-8") || return 1 # ── Confirmation ── - whiptail --title "Confirm install" --yesno \ -"About to install veilor-os: + prompt_confirm "About to install veilor-os: Disk: $disk (will be ERASED) Hostname: $hostname @@ -123,7 +248,7 @@ collect_answers() { LUKS: set Admin pw: set -Proceed?" 16 60 || return 1 +Proceed?" || return 1 # Export to caller via globals SEL_DISK=$disk @@ -134,10 +259,26 @@ Proceed?" 16 60 || return 1 return 0 } +# sed_escape — escape sed special chars in a replacement string. +# Replacement-side metacharacters: & (matched pattern), \ (escape), +# | (our chosen delimiter), / (alternate delimiter — escape too in case +# delimiter ever changes). Newline is rejected in validate_pw because +# escaping it portably across BSD/GNU sed is fiddly. +# Order matters: \ must be escaped FIRST so we don't double-escape the +# backslashes we're about to emit for &, |, /. +sed_escape() { + printf '%s' "$1" | sed -e 's/[\\&|/]/\\&/g' +} + generate_ks() { # Build kickstart for actual disk install. # NOTE: passwords go in via --plaintext to avoid storing crypted hash # collisions; anaconda hashes per /etc/login.defs at install time. + # + # %packages mirrors live ks lines 63-141 (full hardening pkg list, + # minus livesys/anaconda-live which are live-only). + # %post --nochroot copies overlay + scripts + assets from boot ISO. + # %post (chroot) runs the same hardening scripts the live build runs. local out=/run/install/veilor-generated.ks local disk_basename disk_basename=$(basename "$SEL_DISK") @@ -179,12 +320,40 @@ part pv.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passph volgroup veilor pv.veilor logvol / --vgname=veilor --name=root --fstype=btrfs --size=8192 --grow +# ── Packages — mirrors live ks (kickstart/veilor-os.ks 63-141), minus +# live-only entries (livesys-scripts, anaconda-live, @anaconda-tools, +# dracut-live, isomd5sum, xorriso) which are pointless on a real disk. %packages --excludedocs @^kde-desktop-environment @kde-apps @core @hardware-support @standard + +kernel-modules +kernel-modules-extra +glibc-all-langpacks +dracut-config-generic +kernel +grub2-efi-x64 +grub2-efi-x64-modules +grub2-pc +grub2-pc-modules +grub2-tools +grub2-tools-extra +shim-x64 +efibootmgr +syslinux + +# veilor-installer dependencies (kept on installed system so +# `sudo veilor-installer` from a recovery shell still works). +newt +parted +cryptsetup +lvm2 +btrfs-progs + +# core hardening tools fail2ban fail2ban-firewalld usbguard @@ -195,57 +364,172 @@ tuned chrony firewalld plymouth + +# admin essentials git vim-enhanced tmux htop podman +skopeo NetworkManager NetworkManager-wifi + +# fonts fontconfig +freetype fira-code-fonts + +# zram (no swap-on-disk) zram-generator + +# remove fluff +# Note: KDE Plasma 6 hard-deps on cups/geoclue2/ModemManager/PackageKit +# transitively; daemons disabled at runtime via 20-harden-kernel.sh. -abrt* -snapd -kde-connect +-open-vm-tools-desktop -mlocate + +%end + +# ── Post-install (nochroot): copy overlay + scripts + assets from boot ISO. +# Mirror of live ks 144-196 trimmed to single-source: ISO mounted at +# /run/install/repo, veilor/ subtree contains overlay/, scripts/, assets/. +# (See build/Containerfile — ISO build copies the repo into veilor/.) +%post --nochroot --erroronfail +set -uo pipefail +DEST="${INSTALL_ROOT:-/mnt/sysimage}" +[[ -d $DEST ]] || { echo "[ERR] DEST=$DEST does not exist" >&2; exit 1; } + +SRC=/run/install/repo/veilor +if [[ ! -d $SRC/overlay ]]; then + echo "[ERR] $SRC/overlay missing — boot ISO did not include veilor/ tree" >&2 + exit 1 +fi + +echo "[INFO] using SRC=$SRC DEST=$DEST" +set -x +cp -a "$SRC/overlay/." "$DEST/" || echo "[ERR] overlay cp failed: $?" +mkdir -p "$DEST/usr/share/veilor-os" || echo "[ERR] mkdir failed: $?" +cp -a "$SRC/assets" "$DEST/usr/share/veilor-os/" || echo "[ERR] assets cp failed: $?" +cp -a "$SRC/scripts" "$DEST/usr/share/veilor-os/" || echo "[ERR] scripts cp failed: $?" +ls -la "$DEST/usr/share/veilor-os/" 2>&1 || echo "[ERR] dest dir missing post-cp" +# Force root ownership on everything we copied — `cp -a` preserves +# CI runner uid (1001), which makes sudo refuse to read /etc/sudoers.d. +chown -R 0:0 "$DEST/etc" "$DEST/usr/share/veilor-os" "$DEST/usr/local/bin" "$DEST/usr/local/sbin" 2>&1 || echo "[WARN] chown failed" +set +x + +{ + echo "=== %post --nochroot trace ===" + date + echo "SRC=$SRC DEST=$DEST" + ls -la "$DEST/usr/share/veilor-os/" 2>&1 + ls -la "$DEST/usr/local/sbin/" 2>&1 +} > "$DEST/var/log/veilor-nochroot.log" 2>&1 || true +%end + +# ── Post-install (chroot): apply hardening, theme, branding ──────────── +%post +set -uo pipefail +exec > >(tee -a /var/log/veilor-install.log) 2>&1 + +echo "════════════════════════════════════════════════════════" +echo " veilor-os install — %post (real install)" +echo "════════════════════════════════════════════════════════" + +REPO=/usr/share/veilor-os +chmod +x $REPO/scripts/*.sh $REPO/scripts/selinux/*.sh \ + /usr/local/bin/veilor-power /usr/local/sbin/veilor-firstboot \ + /usr/local/sbin/veilor-installer 2>/dev/null || true + +# /etc/machine-id reset on first boot +> /etc/machine-id + +# Apply hardening +bash $REPO/scripts/10-harden-base.sh +bash $REPO/scripts/20-harden-kernel.sh + +# Build SELinux module +bash $REPO/scripts/selinux/build-policy.sh || echo "[WARN] SELinux build failed; load on first boot" + +# Apply KDE theme + DuckSans + os-release branding +bash $REPO/scripts/kde-theme-apply.sh + +# Symlink display-manager.service → sddm.service. (Anaconda usually +# handles this when sddm is the only DM, but be explicit.) +ln -sf /usr/lib/systemd/system/sddm.service /etc/systemd/system/display-manager.service + +# Real install boots straight to SDDM (NOT to TTY1 installer like live). +systemctl set-default graphical.target + +# zram swap (no disk swap; keys never leak to platter) +cat > /etc/systemd/zram-generator.conf << 'EOF' +[zram0] +zram-size = min(ram, 8192) +compression-algorithm = zstd +EOF + +# Enable services +systemctl enable veilor-firstboot.service 2>/dev/null || true +systemctl enable veilor-modules-lock.service 2>/dev/null || true +systemctl enable sshd fail2ban usbguard tuned auditd firewalld chronyd sddm + +# Default tuned profile = balanced (AC/battery udev rule will override) +tuned-adm profile veilor-balanced 2>/dev/null || true + +# Force admin to set a fresh password on first login. The `user` directive +# above already created the account with the user's chosen password — +# `chage -d 0` expires it so SDDM/passwd prompts for change immediately. +chage -d 0 admin 2>/dev/null || true + +# Lock root explicitly (kickstart --lock should already do this) +passwd -l root + +# Sanity: zero references to onyx / personal IPs in installed system +if grep -rqi 'onyx\|192\.168\.0\.\|fedora\.local' /etc/veilor* /etc/tuned/profiles/veilor-* 2>/dev/null; then + echo "[ERR] brand leak detected in /etc — investigate" +fi + +echo "════════════════════════════════════════════════════════" +echo " veilor-os install complete" +echo "════════════════════════════════════════════════════════" %end # Reboot when done reboot KSEOF - # Substitute placeholders. Use | as sed delimiter (passwords might - # contain /). Forbidden chars in passwords (validated upstream): "$\` - # — sed safe. + # Substitute placeholders. Use | as sed delimiter. validate_pw() + # already rejects "$\`&|/\n at input — sed_escape() is defence in + # depth in case future code paths feed unsanitised values (e.g. + # locale/hostname from a file, or a relaxed validator). sed -i \ - -e "s|__LOCALE__|$SEL_LOCALE|" \ - -e "s|__HOSTNAME__|$SEL_HOSTNAME|" \ - -e "s|__DISK_BASENAME__|$disk_basename|" \ - -e "s|__LUKS_PW__|$SEL_LUKS_PW|" \ - -e "s|__ADMIN_PW__|$SEL_ADMIN_PW|" \ + -e "s|__LOCALE__|$(sed_escape "$SEL_LOCALE")|" \ + -e "s|__HOSTNAME__|$(sed_escape "$SEL_HOSTNAME")|" \ + -e "s|__DISK_BASENAME__|$(sed_escape "$disk_basename")|" \ + -e "s|__LUKS_PW__|$(sed_escape "$SEL_LUKS_PW")|" \ + -e "s|__ADMIN_PW__|$(sed_escape "$SEL_ADMIN_PW")|" \ "$out" echo "[INFO] generated kickstart at $out" return 0 } run_install() { - whiptail --title "Installing" --infobox \ -"Installing veilor-os to $SEL_DISK ... + prompt_message "Installing veilor-os to $SEL_DISK ... This will take 10-30 minutes. -Logs: /var/log/veilor-installer.log + /tmp/anaconda.log" 10 60 - sleep 2 +Logs: /var/log/veilor-installer.log + /tmp/anaconda.log" + sleep 1 # Hand off to anaconda. --kickstart runs unattended. if anaconda --kickstart=/run/install/veilor-generated.ks; then - whiptail --title "Done" --msgbox \ -"Install complete. System will reboot. -Remove the install media after shutdown." 10 50 + prompt_message "Install complete. System will reboot. +Remove the install media after shutdown." sleep 3 systemctl reboot else - whiptail --title "Install failed" --msgbox \ -"Anaconda exited non-zero. + prompt_error "Anaconda exited non-zero. Logs at /tmp/anaconda.log + /var/log/veilor-installer.log. -Press OK to drop to shell." 12 60 +Press OK to drop to shell." return 1 fi } @@ -268,10 +552,6 @@ EOF exec /bin/bash --login } -# ── Entry ── -require_tty -banner - launch_desktop() { clear echo "Launching KDE Plasma..." @@ -281,17 +561,21 @@ launch_desktop() { # If user logs out, they come back here. Loop continues. } +# ── Entry ── +require_tty +banner + while true; do case "$(main_menu)" in - 1) + "Install veilor-os to disk") if collect_answers && generate_ks; then run_install || continue fi ;; - 2) launch_desktop ;; - 3) drop_to_shell ;; - 4) systemctl reboot ;; - 5) systemctl poweroff ;; + "Try live — desktop (KDE Plasma)") launch_desktop ;; + "Try live — shell") drop_to_shell ;; + "Reboot") systemctl reboot ;; + "Power off") systemctl poweroff ;; *) drop_to_shell ;; esac done