Files
omarchy-iso/configs/airootfs/usr/local/bin/omarchy-install-dashboard
T
ZacharyZhang-NY e22a13dfeb Ask for the language first, and install in Chinese in a CJK console
The first screen offers English or Simplified Chinese. Chinese reruns
the configurator in cage + foot, which draw Chinese, with Noto Sans
Mono CJK unpacked from the offline mirror's noto-fonts-cjk; no font
joins the live image. There the keyboard list keeps US-keymap layouts
and preselects Chinese (Simplified, Pinyin), every later screen and
the install dashboard read in Chinese, and the target gets
LANG=zh_CN.UTF-8 through locale_config. Without a display or the
font, Chinese continues in English on the VT. The summary shows the
keyboard by its label instead of its keymap.

Needs omarchy's omarchy_prompt_language and omarchy_text in
install/provisioning/setup-form.sh.
2026-10-12 01:21:41 -04:00

1207 lines
42 KiB
Bash
Executable File

#!/usr/bin/env bash
# Foreground Omarchy ISO install UI supervisor.
#
# This process is the only owner of the visible install UI. It launches the
# actual installer as a non-interactive child, sends child output to the support
# log, renders progress while the child runs, then renders the final installed
# time + reboot prompt itself. There is deliberately no background dashboard.
set -euo pipefail
usage() {
echo "Usage: $0 <log-file> <state-file> -- <installer command...>" >&2
exit 2
}
(( $# >= 4 )) || usage
LOG_FILE="$1"
STATE_FILE="$2"
shift 2
[[ ${1:-} == "--" ]] || usage
shift
(( $# > 0 )) || usage
INSTALL_CMD=("$@")
TTY_PATH="${OMARCHY_DASHBOARD_TTY:-/dev/tty}"
LOGO_PATH="${OMARCHY_PATH:-/usr/share/omarchy}/logo.txt"
# Autoinstall has no one at the keyboard, and every prompt this dashboard owns
# reads from the TTY, so each would hang forever. Suppress them here rather than
# in the scripts they live in: this process is the only owner of the visible UI.
interactive() { [[ ${OMARCHY_UI_INTERACTIVE:-} != "no" ]]; }
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"
RED="${CSI}31m"
# The dashboard owns the final reboot prompt now, so keep the gum confirm
# styling that used to come from install/helpers/presentation.sh.
export GUM_CONFIRM_PROMPT_FOREGROUND="${GUM_CONFIRM_PROMPT_FOREGROUND:-6}"
export GUM_CONFIRM_SELECTED_FOREGROUND="${GUM_CONFIRM_SELECTED_FOREGROUND:-0}"
export GUM_CONFIRM_SELECTED_BACKGROUND="${GUM_CONFIRM_SELECTED_BACKGROUND:-2}"
export GUM_CONFIRM_UNSELECTED_FOREGROUND="${GUM_CONFIRM_UNSELECTED_FOREGROUND:-7}"
export GUM_CONFIRM_UNSELECTED_BACKGROUND="${GUM_CONFIRM_UNSELECTED_BACKGROUND:-0}"
child_pid=""
child_pgid=""
whimsy=(
"delivering delightful defaults"
"saving setup struggles"
"picking polished packages"
"curating cohesive colors"
"preparing pleasant pixels"
"hyping happy hyprland"
"integrating intuitive interfaces"
"bootstrapping beautiful boxes"
"crafting cozy commands"
"making machines marvelous"
"polishing pretty pixels"
"tuning tasteful terminals"
"weaving wonderful workflows"
)
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"
)
# omarchy_text and its Chinese table come from the setup form the configurator uses.
SETUP_FORM="${SETUP_FORM:-/usr/share/omarchy-iso/setup-form.sh}"
[[ -r $SETUP_FORM ]] || SETUP_FORM="${OMARCHY_PATH:-/usr/share/omarchy}/install/provisioning/setup-form.sh"
source "$SETUP_FORM"
OMARCHY_ZH+=(
["Tip: %s"]="提示:%s"
["Installed Omarchy"]="Omarchy 安装完成"
["Installed Omarchy in %s"]="Omarchy 安装完成,用时 %s"
["Reboot Now"]="立即重启"
["Last log lines:"]="日志末尾:"
["Last target log lines:"]="目标系统日志末尾:"
["Press Enter to continue…"]="按回车继续……"
["No log uploader is available in this environment."]="这个环境没有日志上传工具。"
["Install log: %s"]="安装日志:%s"
["Target log: %s"]="目标系统日志:%s"
["Omarchy installation stopped"]="Omarchy 安装已停止"
["Installer exited with status %s"]="安装程序退出,状态码 %s"
["Get help at https://omarchy.org/discord"]="求助:https://omarchy.org/discord"
["What would you like to do?"]="接下来做什么?"
["Upload log for support"]="上传日志求助"
["View full log"]="查看完整日志"
["Drop to shell"]="进入 shell"
["Reboot"]="重启"
["Power off"]="关机"
["The install medium was misread"]="安装介质读取出错"
["This package is intact on this ISO, so the download and the write were fine:"]="这个软件包在 ISO 中完好,下载和写入都没有问题:"
["Try another USB port or another stick, and test this machine's memory"]="换一个 USB 口或换一个 U 盘,并检测这台机器的内存"
["The install medium is damaged"]="安装介质已损坏"
["This package no longer matches the checksum it was built with:"]="这个软件包和构建时的校验和不再一致:"
["Re-download the ISO, verify it with sha256sum, and write the USB again"]="重新下载 ISO,用 sha256sum 校验,再重新写入 U 盘"
["If a verified ISO fails here too, the USB stick itself is the problem"]="如果校验过的 ISO 在这里也失败,问题出在 U 盘本身"
["The install medium is damaged or misread"]="安装介质已损坏或读取出错"
["pacman rejected this package, read straight off this ISO:"]="pacman 拒绝了这个直接从 ISO 读出的软件包:"
["Verify your download with sha256sum, then write the USB again"]="用 sha256sum 校验下载的文件,再重新写入 U 盘"
["If the download verifies, the stick or this machine's memory is at fault"]="如果下载校验无误,问题出在 U 盘或这台机器的内存"
)
if [[ ${OMARCHY_INSTALL_LANG:-} == "zh" ]]; then
whimsy=(
"备好贴心的默认设置"
"省掉装机的折腾"
"挑选打磨过的软件包"
"调配协调的配色"
"准备悦目的像素"
"唤醒 Hyprland"
"接好顺手的界面"
"搭起漂亮的系统"
"写好趁手的命令"
"让机器焕然一新"
"擦亮每一个像素"
"调好终端的品味"
"织出流畅的工作流"
)
tips=(
"Super + Space 打开 Omarchy 菜单:应用、设置等"
"Super + K 查看全部快捷键"
"Super 是键盘上的 Windows 或 Command 键"
"用 Xournal++ 给 PDF 签名"
"用 LocalSend 和手机、电脑互传文件"
"菜单 安装 > 网页应用 把任意网站变成应用"
"Super + Return 打开终端,Super + Shift + Return 打开浏览器"
"图片用 Pinta,视频用 Kdenlive,文档用 LibreOffice"
"Super + Ctrl + Print 用 OCR 抓取屏幕文字"
"Print 截屏,Alt + Print 录屏"
"Super + Ctrl + R 设置提醒"
"菜单 风格 > 主题 切换主题"
"双击菜单栏让它透明"
"菜单 安装 > Windows 运行完整的 Windows 虚拟机"
"Super + Ctrl + V 打开剪贴板管理器"
"Super + 1 到 0 切换工作区,加 Shift 带上窗口"
"Super + Print 拾取屏幕上任意位置的颜色"
"用菜单里的 更新 让系统保持最新"
)
fi
cleanup() {
printf '%s%s' "$RESET" "$SHOW_CURSOR" >"$TTY_PATH" 2>/dev/null || true
}
dashboard_log() {
printf '[dashboard] %s\n' "$*" >>"$LOG_FILE" 2>/dev/null || true
}
stop_child() {
[[ -n ${child_pid:-} ]] || return 0
if kill -0 "$child_pid" 2>/dev/null; then
if [[ -n ${child_pgid:-} ]]; then
kill -- -"$child_pgid" 2>/dev/null || true
fi
kill "$child_pid" 2>/dev/null || true
for _ in {1..20}; do
kill -0 "$child_pid" 2>/dev/null || break
sleep 0.05
done
if kill -0 "$child_pid" 2>/dev/null; then
[[ -n ${child_pgid:-} ]] && kill -9 -- -"$child_pgid" 2>/dev/null || true
kill -9 "$child_pid" 2>/dev/null || true
fi
fi
wait "$child_pid" 2>/dev/null || true
}
interrupt() {
local code="$1"
stop_child
cleanup
exit "$code"
}
trap cleanup EXIT
trap 'interrupt 130' INT
trap 'interrupt 143' TERM
term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; }
term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; }
# Read the actual character dimensions instead of assuming an 8 x 16 font.
# Linux VTs expose the font through KDFONTOP; desktop terminals may expose
# pixels in their window size or answer the standard cell-size query.
term_cell_size() {
python3 - "$TTY_PATH" <<'PY'
import fcntl
import os
import re
import select
import struct
import sys
import termios
import time
import tty
with open(sys.argv[1], "r+b", buffering=0) as terminal:
fd = terminal.fileno()
rows, cols, width, height = struct.unpack(
"HHHH", fcntl.ioctl(fd, termios.TIOCGWINSZ, bytes(8)))
if rows and cols and width and height:
print(round(width / cols), round(height / rows))
sys.exit()
try:
font = bytearray(struct.pack("@5IP", 1, 0, 0, 0, 0, 0))
fcntl.ioctl(fd, 0x4B72, font) # KDFONTOP / KD_FONT_OP_GET, no glyph data
_, _, width, height, _ = struct.unpack_from("@5I", font)
if width and height:
print(width, height)
sys.exit()
except OSError:
pass
settings = termios.tcgetattr(fd)
try:
tty.setcbreak(fd, termios.TCSANOW)
os.write(fd, b"\x1b[16t")
response = b""
deadline = time.monotonic() + 0.15
while select.select([fd], [], [], max(0, deadline - time.monotonic()))[0]:
response += os.read(fd, 64)
match = re.search(rb"\x1b\[6;(\d+);(\d+)t", response)
if match:
height, width = map(int, match.groups())
if width and height:
print(width, height)
sys.exit()
if time.monotonic() >= deadline:
break
finally:
termios.tcsetattr(fd, termios.TCSANOW, settings)
print(8, 16) # Conventional console font when dimensions aren't available.
PY
}
logo_width() {
if [[ -f $LOGO_PATH ]]; then
# The logo is drawn in box-drawing glyphs, three bytes each in UTF-8, so
# awk's `length` returns 197 rather than 81 unless the locale happens to be
# UTF-8 — and a width wider than the console makes left_padding clamp to
# zero, which puts the logo flush left. Count bytes deliberately (LC_ALL=C)
# and drop the continuation bytes instead, so the answer is columns in every
# locale rather than only in the ones that have a charset.
LC_ALL=C awk '
{ gsub(/[\200-\277]/, ""); if (length > max) max = length }
END { print max+0 }
' "$LOGO_PATH"
else
printf '81\n'
fi
}
logo_height() {
if [[ -f $LOGO_PATH ]]; then
wc -l <"$LOGO_PATH"
else
printf '1\n'
fi
}
LOGO_WIDTH="$(logo_width)"
LOGO_HEIGHT="$(logo_height)"
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
CONTENT_WIDTH="$LOGO_WIDTH"
TOP_ROW=1
LAST_SIZE=""
LAST_PM=10
# Wordmark, blank, title, and the three rows gum confirm uses.
FINISH_HEIGHT=$((LOGO_HEIGHT + 5))
FINISH_TOP_ROW=2
# ── Install progress ─────────────────────────────────────────────────────────
#
# Position = max(floor, work), both non-decreasing, in per-mille of the drawing.
#
# floor lo + (span-1) * t / (t+tau), on time since this process saw the
# phase. Asymptotic, so it approaches the band end without reaching it.
# tau is a shape constant, not a duration prediction.
# work packages installed / PKG_TOTAL, for the package phase only (~85% of
# the install). libalpm makes one directory per package under
# <target>/var/lib/pacman/local; the denominator comes from
# build-iso.sh, so there is no second package list to drift.
#
# Per-mille because a whole-percent asymptote over a 2-point band has two
# reachable values and stops moving. No give-up predicate: two discontinuous
# curves behind a monotonic clamp freeze. With no signal the floor alone still
# finishes the band.
EXPECTED_PACKAGES_FILE="${OMARCHY_EXPECTED_PACKAGES_FILE:-/usr/share/omarchy-iso/expected-packages}"
PKG_TOTAL=0
if [[ -r $EXPECTED_PACKAGES_FILE ]]; then
read -r PKG_TOTAL _ <"$EXPECTED_PACKAGES_FILE" 2>/dev/null || PKG_TOTAL=0
fi
[[ $PKG_TOTAL =~ ^[0-9]+$ ]] || PKG_TOTAL=0
# A wrong denominator is worse than none: it never triggers the fallback.
(( PKG_TOTAL < 300 || PKG_TOTAL > 5000 )) && PKG_TOTAL=0
PKG_BONUS_MAX_PM=100 # cap on how far progress may run ahead of the count
PKG_TAIL_PM=40 # band held back so stalled progress has somewhere to go
PKG_FLOOR_TAU=900 # with a work signal, time is only a backstop until the
# first package, so slow it until it cannot outrun data
POS=10 # position, per-mille, monotonic
PROGRESS_PM=10
NOW=0 # monotonic seconds, refreshed once per frame
DASH_T0=0
PHASE_NAME="" # last phase this process observed
PHASE_T0=0 # NOW when it was first observed
PHASE_LO=10 # POS on entry, for an unknown phase with no index
PKG_DB_DIR=""
PKG_COUNT=0
PKG_BASE=-1 # package count when the phase started
PKG_SEEN=0 # running max of packages since PKG_BASE
PKG_LAST_CHANGE=0
PKG_BONUS=0
# Monotonic clock: tzupdate and systemd-timesyncd can step the wall clock
# mid-install, which would jump every time-driven band to its ceiling.
set_now() {
local up rest
if read -r up rest </proc/uptime 2>/dev/null && [[ $up == [0-9]* ]]; then
NOW=${up%%.*}
else
NOW=$EPOCHSECONDS
fi
return 0
}
left_padding() {
local width="${1:-$CONTENT_WIDTH}" cols pad
cols="$(term_cols)"
pad=$(( (cols - width) / 2 ))
(( pad < 0 )) && pad=0
printf '%*s' "$pad" ''
}
visible_len() {
local text="$1"
# wc -L counts display columns, so double-width CJK centers too.
printf '%b' "$text" | sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g' | wc -L
}
blank_line() { printf '\r%s\n' "$CLEAR_LINE"; }
line_at() {
local width="${1:-$CONTENT_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:-$CONTENT_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
pad="$(left_padding "$LOGO_WIDTH")"
if [[ -f $LOGO_PATH ]]; then
while IFS= read -r line; do
printf '\r%s%s%b%s%b\n' "$CLEAR_LINE" "$pad" "$GREEN" "$line" "$RESET"
done <"$LOGO_PATH"
else
center "${GREEN}Omarchy${RESET}" "$CONTENT_WIDTH"
fi
}
# The icon uses half-block glyphs, scaled to the terminal's actual cell aspect
# ratio so its square silhouette stays square. No graphics protocol is needed.
# Coordinates follow the 15 x 15 grid of Omarchy's icon.svg. Each segment
# paints the full stroke width, starting at the bottom middle of the outer ring.
ICON_WIDTH=30
ICON_HEIGHT=15
CELL_WIDTH=8
CELL_HEIGHT=16
ICON_LEFT=1
ICON_STEPS=0
ICON_DRAWN=0
DISPLAY_PM=0
ICON_FRAME=""
declare -a ICON_ORDER=() ICON_TRACE=() ICON_FILL=()
icon_segment() {
local scale="$1" x1="$2" y1="$3" x2="$4" y2="$5"
local x y from to
if (( y1 == y2 )); then
from=$(( (x1 * ICON_WIDTH + 14) / 15 ))
to=$(( ((x2 + 1) * ICON_WIDTH + 14) / 15 ))
if (( x2 < x1 )); then
from=$(( ((x1 + 1) * ICON_WIDTH + 14) / 15 - 1 ))
to=$(( (x2 * ICON_WIDTH + 14) / 15 - 1 ))
fi
for ((x=from; x!=to; x+=(x2 >= x1 ? 1 : -1))); do
for ((y=y1 * scale; y<(y1 + 1) * scale; y++)); do
icon_pixel "$((y * ICON_WIDTH + x))"
done
done
else
from=$(( y1 * scale ))
to=$(( (y2 + 1) * scale ))
if (( y2 < y1 )); then
from=$(( (y1 + 1) * scale - 1 ))
to=$(( y2 * scale - 1 ))
fi
for ((y=from; y!=to; y+=(y2 >= y1 ? 1 : -1))); do
for ((x=(x1 * ICON_WIDTH + 14) / 15; x<((x1 + 1) * ICON_WIDTH + 14) / 15; x++)); do
icon_pixel "$((y * ICON_WIDTH + x))"
done
done
fi
}
icon_pixel() {
local pixel="$1"
if [[ ! -v ICON_TRACE[pixel] ]]; then
ICON_ORDER+=("$pixel")
ICON_TRACE[pixel]=${#ICON_ORDER[@]}
fi
}
build_icon() {
local scale="$1" x y row
local -a mask=(
111111111111111
100000010000001
101111110001101
101000000000101
101000000000101
101000000000101
101000000000101
111000000000101
101000000000101
101000000000101
101000000000101
101000000000101
101111111111101
100000010000001
111111110111111
)
ICON_WIDTH=$(( (15 * scale * CELL_HEIGHT + CELL_WIDTH) / (2 * CELL_WIDTH) ))
ICON_HEIGHT=$(((15 * scale + 1) / 2))
ICON_ORDER=() ICON_TRACE=() ICON_FILL=()
ICON_DRAWN=0
# Start at the bottom middle, follow the outer left edge to the top cross,
# then descend into the inner shape. Fill its two bridges as we reach them.
icon_segment "$scale" 7 14 0 14
icon_segment "$scale" 0 14 0 0
icon_segment "$scale" 0 0 7 0
icon_segment "$scale" 7 0 7 2
icon_segment "$scale" 7 2 2 2
icon_segment "$scale" 2 2 2 7
icon_segment "$scale" 2 7 0 7
icon_segment "$scale" 2 7 2 12
icon_segment "$scale" 2 12 7 12
icon_segment "$scale" 7 12 7 14
icon_segment "$scale" 7 12 12 12
icon_segment "$scale" 12 12 12 2
icon_segment "$scale" 12 2 11 2
# Finish from the top cross: right along the outside, down, then left.
icon_segment "$scale" 7 0 14 0
icon_segment "$scale" 14 0 14 14
icon_segment "$scale" 14 14 9 14
ICON_STEPS=${#ICON_ORDER[@]}
for ((y=0; y<15 * scale; y++)); do
row=${mask[y / scale]}
for ((x=0; x<ICON_WIDTH; x++)); do
ICON_FILL[y * ICON_WIDTH + x]=${row:x * 15 / ICON_WIDTH:1}
done
done
}
# Append one cell with independently colored upper/lower pixels. This also
# handles a green tracing tip sharing a cell with the still-dark outline.
icon_cell() {
local pixel="$1" drawn="$2" solid="$3" upper=0 lower=0 step
if (( solid )); then
(( ICON_FILL[pixel] )) && upper=32
(( ${ICON_FILL[pixel + ICON_WIDTH]:-0} )) && lower=32
else
step=${ICON_TRACE[pixel]:-0}
(( step > 0 )) && upper=90
(( step > 0 && step <= drawn )) && upper=32
step=${ICON_TRACE[pixel + ICON_WIDTH]:-0}
(( step > 0 )) && lower=90
(( step > 0 && step <= drawn )) && lower=32
fi
if (( upper == 0 && lower == 0 )); then
ICON_FRAME+="${RESET} "
elif (( upper == lower )); then
ICON_FRAME+="${CSI}${upper};49m█"
elif (( lower == 0 )); then
ICON_FRAME+="${CSI}${upper};49m▀"
elif (( upper == 0 )); then
ICON_FRAME+="${CSI}${lower};49m▄"
else
ICON_FRAME+="${CSI}${upper};$((lower + 10))m▀"
fi
}
render_icon() {
local pm="$1" full="${2:-0}" drawn i pixel row col
drawn=$((pm * ICON_STEPS / 1000))
ICON_FRAME=""
if (( full || pm == 1000 )); then
for ((row=0; row<ICON_HEIGHT; row++)); do
ICON_FRAME+="${CSI}$((TOP_ROW + row));${ICON_LEFT}H"
for ((col=0; col<ICON_WIDTH; col++)); do
icon_cell "$((row * 2 * ICON_WIDTH + col))" "$drawn" "$((pm == 1000))"
done
done
else
# Between layout changes, repaint only newly traced pixels. This keeps the
# animation cheap even while pacstrap saturates the disk.
for ((i=ICON_DRAWN; i<drawn; i++)); do
pixel=${ICON_ORDER[i]}
row=$((pixel / ICON_WIDTH / 2)) col=$((pixel % ICON_WIDTH))
ICON_FRAME+="${CSI}$((TOP_ROW + row));$((ICON_LEFT + col))H"
icon_cell "$((row * 2 * ICON_WIDTH + col))" "$drawn" 0
done
fi
printf '%s%s' "$ICON_FRAME" "$RESET"
ICON_DRAWN=$drawn
}
# Packages libalpm has finished installing. local/ holds one directory per
# package and nothing else in it is a directory, so */ is exact. One glob, no
# forks: this runs twice a second while the disk is saturated.
count_packages() {
local -a entries
PKG_COUNT=0
[[ -n $PKG_DB_DIR ]] || return 0
entries=("$PKG_DB_DIR"/*/)
# Unmatched globs expand to the pattern; nullglob stays off script-wide.
if (( ${#entries[@]} == 1 )) && [[ ! -d ${entries[0]} ]]; then
return 0
fi
PKG_COUNT=${#entries[@]}
return 0
}
# Sets PROGRESS_PM. Never call in a command substitution: POS, the phase timer
# and the package counters must survive between frames. Print nothing — output
# here would land on the TTY between the cursor escape and the drawing.
install_progress() {
local phase finished index total target
local lo hi tau span t pos work gap n pkg_signal=0
local -a state
PROGRESS_PM=$POS
[[ -f $STATE_FILE ]] || return 0
mapfile -t state < <(jq -r '
(.current_phase // "Starting installation"),
((.finished_at // 0) | tonumber? // 0 | floor),
((.current_index // -1) | tonumber? // -1 | floor),
((.total_phases // 0) | tonumber? // 0 | floor),
(.target // "/mnt")
' "$STATE_FILE" 2>/dev/null)
(( ${#state[@]} == 5 )) || return 0
phase="${state[0]}" finished="${state[1]}"
index="${state[2]}" total="${state[3]}" target="${state[4]}"
[[ -n $target ]] || target="/mnt"
if (( finished > 0 )) || [[ $phase == "Installation complete" ]]; then
POS=1000
PROGRESS_PM=1000
return 0
fi
# Transitions are observed here rather than read from the state file, which
# keeps wall clock out of the dashboard entirely.
if [[ $phase != "$PHASE_NAME" ]]; then
PHASE_NAME="$phase"
PHASE_T0=$NOW
PHASE_LO=$POS
PKG_BASE=-1
PKG_SEEN=0
PKG_BONUS=0
PKG_LAST_CHANGE=$NOW
fi
# Bands in per-mille, plus tau. The tail bands are wide relative to what they
# cost on NVMe: run_system_finalizer can DKMS-build a driver and
# finalize_limine_boot rebuilds every UKI, so they vary most on real hardware.
# Over-allotting costs a step at a boundary; under-allotting costs a stall.
case "$phase" in
"Starting installation") lo=10 hi=25 tau=5 ;;
"Preparing live environment") lo=25 hi=50 tau=25 ;;
"Preparing install target") lo=50 hi=65 tau=20 ;;
"Installing Arch + Omarchy") lo=65 hi=700 tau=150 pkg_signal=1 ;;
"Configuring hibernation") lo=700 hi=720 tau=10 ;;
"Configuring system") lo=720 hi=850 tau=120 ;;
"Staging provisioning") lo=850 hi=855 tau=5 ;;
"Finalizing Limine boot") lo=855 hi=950 tau=90 ;;
"Finalizing user") lo=950 hi=978 tau=30 ;;
"Configuring login") lo=978 hi=988 tau=5 ;;
"Configuring DNS resolver") lo=988 hi=994 tau=5 ;;
"Validating boot setup") lo=994 hi=998 tau=5 ;;
"Creating factory snapshot") lo=998 hi=1000 tau=5 ;;
*)
# Unknown names used to inherit a 1->99 sweep. Derive an even band from
# the published index instead; with no index, a bounded slice, so a
# renamed phase degrades to one cell rather than a runaway.
if (( total >= 2 && index >= 0 && index < total )); then
lo=$(( 10 + 990 * index / total ))
hi=$(( 10 + 990 * (index + 1) / total ))
else
lo=$PHASE_LO
hi=$(( PHASE_LO + 30 ))
fi
tau=60
;;
esac
(( pkg_signal && PKG_TOTAL > 0 )) || pkg_signal=0
(( pkg_signal )) && tau=$PKG_FLOOR_TAU
t=$(( NOW - PHASE_T0 ))
(( t < 0 )) && t=0
span=$(( hi - lo ))
(( span < 2 )) && span=2
(( tau < 1 )) && tau=1
pos=$(( lo + (span - 1) * t / (t + tau) ))
if (( pkg_signal )); then
PKG_DB_DIR="$target/var/lib/pacman/local"
count_packages
# Baseline on first sight: a target that already holds packages would
# otherwise pin progress at the band ceiling for the whole phase.
(( PKG_BASE < 0 )) && PKG_BASE=$PKG_COUNT
n=$(( PKG_COUNT - PKG_BASE ))
(( n < 0 )) && n=0
# Running max: a reinstalled package briefly drops its version-stamped dir.
if (( n > PKG_SEEN )); then
PKG_SEEN=$n
PKG_LAST_CHANGE=$NOW
fi
gap=$(( NOW - PKG_LAST_CHANGE ))
(( gap < 0 )) && gap=0
(( gap > PKG_BONUS_MAX_PM )) && gap=$PKG_BONUS_MAX_PM
(( gap > PKG_BONUS )) && PKG_BONUS=$gap
work=$(( lo + (span - 1 - PKG_TAIL_PM) * PKG_SEEN / PKG_TOTAL + PKG_BONUS ))
(( work > pos )) && pos=$work
fi
(( pos > hi - 1 )) && pos=$(( hi - 1 ))
(( pos > POS )) && POS=$pos
(( POS > 1000 )) && POS=1000
PROGRESS_PM=$POS
return 0
}
# Keep the original loading messages on their own clock, so progress jumps
# cannot skip phrases and animation frame rate does not change their cadence.
current_whimsy() {
printf '%s' "${whimsy[$(( (NOW - DASH_T0) / 4 % ${#whimsy[@]} ))]}"
}
# One tip per 8 seconds, in order. Use elapsed time, since rendering can stretch
# under pacstrap I/O, when the tips may be the most visible sign of activity.
current_tip() {
printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}"
}
# Vertically center a block of the given height, echoing the row it starts on.
# Center both the progress icon and the final wordmark on the console.
centered_top_row() {
local height="$1" rows top
rows="$(term_size)"
rows="${rows%% *}"
[[ $rows =~ ^[0-9]+$ ]] || rows=24
top=$(( (rows - height) / 2 ))
(( top < 0 )) && top=0
printf '%d' "$(( top + 1 ))"
}
measure_progress_layout() {
local rows cols scale=1
LAST_SIZE="$(term_size)"
read -r rows cols <<<"$LAST_SIZE"
read -r CELL_WIDTH CELL_HEIGHT < <(term_cell_size 2>/dev/null || printf '8 16\n')
(( rows >= 34 && cols >= 49 )) && scale=2
(( rows >= 50 && cols >= 64 )) && scale=3
while (( scale > 1 && (15 * scale * CELL_HEIGHT + CELL_WIDTH) / (2 * CELL_WIDTH) > cols - 4 )); do
scale=$((scale - 1))
done
build_icon "$scale"
TOP_ROW="$(centered_top_row "$((ICON_HEIGHT + 6))")"
ICON_LEFT=$(( (cols - ICON_WIDTH) / 2 + 1 ))
(( ICON_LEFT < 1 )) && ICON_LEFT=1
PROGRESS_TEXT_WIDTH=$((cols - 2))
(( PROGRESS_TEXT_WIDTH > CONTENT_WIDTH )) && PROGRESS_TEXT_WIDTH=$CONTENT_WIDTH
return 0
}
render_static() {
measure_progress_layout
printf '%s' "$CLEAR"
render_icon "$DISPLAY_PM" 1
}
update_progress() {
set_now
install_progress
local pm=$PROGRESS_PM
[[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM
# Only the child's successful exit can complete the drawing.
(( pm > 999 )) && pm=999
# POS is monotone by construction; this is a guard, not the mechanism.
(( pm < LAST_PM )) && pm=$LAST_PM
LAST_PM=$pm
}
render_dynamic() {
local step message tip
# The framebuffer can settle late in VMs. Rebuild at the new size, preserving
# progress, instead of leaving the drawing at the old screen coordinates.
[[ "$(term_size)" == "$LAST_SIZE" ]] || render_static
# Ease phase jumps over several frames, with the percentage following the
# same position as the trace. The measured progress still never goes back.
step=$(( (LAST_PM - DISPLAY_PM + 3) / 4 ))
DISPLAY_PM=$((DISPLAY_PM + step))
render_icon "$DISPLAY_PM"
message="$(truncate_for_width "$(current_whimsy)" "$PROGRESS_TEXT_WIDTH")"
tip="$(truncate_for_width "$(omarchy_text "Tip: %s" "$(current_tip)")" "$PROGRESS_TEXT_WIDTH")"
printf '%s%d;1H' "$CSI" "$((TOP_ROW + ICON_HEIGHT + 1))"
center "${GREEN}$((DISPLAY_PM / 10))%${RESET}" "$PROGRESS_TEXT_WIDTH"
center "${GREEN}${message}${RESET}" "$PROGRESS_TEXT_WIDTH"
blank_line
center "${DIM}${tip}${RESET}" "$PROGRESS_TEXT_WIDTH"
printf '%s' "$CLEAR_TO_END"
}
install_duration() {
local path line duration
if [[ -f $STATE_FILE ]]; then
duration="$(jq -r '
((.started_at // 0) | tonumber? // 0) as $started
| ((.finished_at // 0) | tonumber? // 0) as $finished
| if $started <= 0 or $finished <= 0 or $finished < $started then empty
else (($finished - $started) | round) as $secs
| if $secs >= 3600 then "\($secs / 3600 | floor)h \($secs % 3600 / 60 | floor)m \($secs % 60)s"
else "\($secs / 60 | floor)m \($secs % 60)s"
end
end
' "$STATE_FILE" 2>/dev/null || true)"
[[ -n $duration ]] && { printf '%s' "$duration"; return 0; }
fi
for path in /mnt/var/log/omarchy-install.log "$LOG_FILE"; do
[[ -f $path ]] || continue
for prefix in Total Omarchy; do
line="$(grep "^$prefix:" "$path" 2>/dev/null | tail -n 1 || true)"
[[ -n $line ]] || continue
duration="${line#*:}"
duration="${duration#${duration%%[![:space:]]*}}"
[[ -n $duration ]] && { printf '%s' "$duration"; return 0; }
done
done
return 0
}
render_finish() {
local duration effect_canvas_width title
duration="$(install_duration || true)"
if [[ -n $duration ]]; then
title=$(omarchy_text "Installed Omarchy in %s" "$duration")
else
title=$(omarchy_text "Installed Omarchy")
fi
# ttfx centers its text two columns right of plain centering. A canvas two
# columns narrower aligns the animation with the title and fallback logo.
effect_canvas_width="$(term_cols)"
(( effect_canvas_width > 2 )) && effect_canvas_width=$((effect_canvas_width - 2))
# Set before anything that can fail, so reboot_prompt still lands on the
# button row even if the logo or the effect below never renders.
FINISH_TOP_ROW="$(centered_top_row "$FINISH_HEIGHT")"
{
# Leave the wordmark area blank: laseretch reveals it for the first time.
printf '%s%s%s%d;1H' "$HIDE_CURSOR" "$CLEAR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 1))"
center "$title" "$CONTENT_WIDTH"
blank_line
} >"$TTY_PATH"
if [[ -f $LOGO_PATH ]]; then
# Reuse mode restores a saved cursor before moving up over its input. Save
# this anchor so the effect occupies the reserved wordmark rows.
printf '%s%d;1H\0337' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT))" >"$TTY_PATH"
# Laseretch runs ~4s at 260fps, so the 8s cap is a stuck-process guard rather
# than a trim. --xterm-colors keeps the whole effect in the 256-index palette
# the way the greeter does, since the console reduces indexed codes well and
# truecolor ones badly. It settles on indexed 2, the Tokyo Night green used
# throughout the installer. The laser and sparks keep their own hot palette.
timeout 8s ttfx -i "$LOGO_PATH" \
--canvas-width "$effect_canvas_width" \
--anchor-text c \
--frame-rate 260 \
--reuse-canvas \
--xterm-colors \
laseretch \
--final-gradient-stops 2 \
>"$TTY_PATH" 2>/dev/null || {
dashboard_log "logo effect skipped (ttfx exited $?)"
{ printf '%s%d;1H' "$CSI" "$FINISH_TOP_ROW"; render_logo; } >"$TTY_PATH"
}
else
{ printf '%s%d;1H' "$CSI" "$FINISH_TOP_ROW"; render_logo; } >"$TTY_PATH"
fi
printf '%s%s%d;1H' "$SHOW_CURSOR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH"
}
reboot_prompt() {
local prompt_pad label
# Nobody to confirm; the OMARCHY_UI_AUTO_REBOOT check at the call site still
# applies, so setting both stops on the finish screen without a keyboard.
interactive || return 0
# gum paints its prompt row at the cursor and the button two rows under it, so
# anchor on the row right below the title to put the button as close to it as
# gum allows. FINISH_HEIGHT budgets for the same three rows.
printf '%s%d;1H%s' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 2))" "$CLEAR_LINE" >"$TTY_PATH"
# gum pads the button label by four columns on each side.
label=$(omarchy_text "Reboot Now")
prompt_pad=$(( ( $(term_cols) - $(visible_len "$label") - 8 ) / 2 ))
(( prompt_pad < 0 )) && prompt_pad=0
gum confirm \
--padding "0 0 0 $prompt_pad" \
--show-help=false \
--default \
--affirmative "$label" \
--negative "" \
"" <"$TTY_PATH" >"$TTY_PATH" 2>&1
}
# Columns, not characters: each CJK character takes two.
truncate_for_width() {
local text="$1" width="$2"
if (( $(printf '%s' "$text" | wc -L) <= width )); then
printf '%s' "$text"
return
fi
text=${text:0:$((width - 1))}
while (( $(printf '%s' "$text" | wc -L) > width - 1 )); do
text=${text%?}
done
printf '%s…' "$text"
}
render_failure_log_tail() {
# The caller passes the rows it spent above this point that the constant below
# does not know about, so the tail gives way rather than pushing the top of
# the screen off a short console.
local reserved="${1:-0}" rows lines width line tail_log label
rows=$(term_size)
rows=${rows%% *}
lines=$((rows - LOGO_HEIGHT - 18 - reserved))
(( lines < 6 )) && lines=6
(( lines > 14 )) && lines=14
width=$((CONTENT_WIDTH - 4))
(( width < 40 )) && width=40
tail_log="${OMARCHY_FAILURE_TAIL_LOG:-}"
if [[ -z $tail_log ]]; then
if [[ -f /mnt/var/log/omarchy-install.log ]]; then
tail_log=/mnt/var/log/omarchy-install.log
else
tail_log="$LOG_FILE"
fi
fi
[[ -f $tail_log ]] || return 0
label=$(omarchy_text "Last log lines:")
[[ $tail_log == /mnt/* ]] && label=$(omarchy_text "Last target log lines:")
line_at "$CONTENT_WIDTH" 2 "${DIM}${label}${RESET}"
printf '\n'
tail -n "$lines" "$tail_log" 2>/dev/null | while IFS= read -r line; do
line_at "$CONTENT_WIDTH" 2 "$(truncate_for_width "$line" "$width")"
printf '\n'
done
}
find_log_uploader() {
local candidate
for candidate in omarchy-upload-log /usr/local/bin/omarchy-upload-log /usr/bin/omarchy-upload-log /mnt/usr/bin/omarchy-upload-log; do
if [[ $candidate == */* ]]; then
[[ -x $candidate ]] && { printf '%s' "$candidate"; return 0; }
elif command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
return 0
fi
done
return 1
}
prompt_enter() {
printf '\n%s' "$(omarchy_text "Press Enter to continue…")" >"$TTY_PATH"
IFS= read -r _ <"$TTY_PATH" || true
}
upload_failure_log() {
local uploader
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
if uploader=$(find_log_uploader); then
"$uploader" install >"$TTY_PATH" 2>&1 || true
else
{
printf '%s\n' "$(omarchy_text "No log uploader is available in this environment.")"
printf '%s\n' "$(omarchy_text "Install log: %s" "$LOG_FILE")"
[[ -f /mnt/var/log/omarchy-install.log ]] && printf '%s\n' "$(omarchy_text "Target log: %s" /mnt/var/log/omarchy-install.log)"
} >"$TTY_PATH"
fi
prompt_enter
}
view_failure_log() {
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
if command -v less >/dev/null 2>&1 && [[ -f $LOG_FILE ]]; then
less "$LOG_FILE" <"$TTY_PATH" >"$TTY_PATH" 2>&1 || true
else
tail -n 200 "$LOG_FILE" >"$TTY_PATH" 2>&1 || true
prompt_enter
fi
}
# A failure the medium explains is worth saying before the log tail, because
# the tail is where "invalid or corrupted package" already scrolled past
# without ever naming the USB stick it came off.
diagnose_media() {
local logs=("$LOG_FILE")
[[ -f /mnt/var/log/omarchy-install.log ]] && logs+=(/mnt/var/log/omarchy-install.log)
[[ -n ${OMARCHY_FAILURE_TAIL_LOG:-} ]] && logs+=("$OMARCHY_FAILURE_TAIL_LOG")
omarchy-install-diagnose-media "${logs[@]}" 2>/dev/null
}
# Rows the diagnosis occupies, its leading blank included.
media_diagnosis_rows() {
local diagnosis="$1"
if [[ -z $diagnosis ]]; then
printf '0\n'
else
printf '%s\n' "$(( $(grep -c '' <<<"$diagnosis") + 1 ))"
fi
}
render_media_diagnosis() {
local diagnosis="$1" line first=1
[[ -n $diagnosis ]] || return 0
blank_line
while IFS= read -r line; do
# A blank line inside the diagnosis is deliberate: it separates what
# happened from what to do about it, so it is drawn rather than skipped.
if [[ -z $line ]]; then
blank_line
continue
fi
# The support log keeps English; the screen shows the translation, and package names pass through.
[[ ${OMARCHY_INSTALL_LANG:-} == "zh" ]] && line=${OMARCHY_ZH[$line]:-$line}
if (( first )); then
center "${RED}$(truncate_for_width "$line" "$CONTENT_WIDTH")${RESET}" "$CONTENT_WIDTH"
first=0
else
center "$(truncate_for_width "$line" "$CONTENT_WIDTH")" "$CONTENT_WIDTH"
fi
done <<<"$diagnosis"
}
render_failure() {
local status="$1" state_summary="${2:-}" media_diagnosis="${3:-}" line
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
blank_line
render_logo
blank_line
center "${RED}$(omarchy_text "Omarchy installation stopped")${RESET}" "$CONTENT_WIDTH"
center "$(omarchy_text "Installer exited with status %s" "$status")" "$CONTENT_WIDTH"
render_media_diagnosis "$media_diagnosis"
if [[ -n $state_summary ]]; then
blank_line
while IFS= read -r line; do
[[ -n $line ]] || continue
center "${DIM}$(truncate_for_width "$line" "$CONTENT_WIDTH")${RESET}" "$CONTENT_WIDTH"
done <<<"$state_summary"
fi
blank_line
render_failure_log_tail "$(media_diagnosis_rows "$media_diagnosis")"
blank_line
center "${DIM}$(omarchy_text "Get help at https://omarchy.org/discord")${RESET}" "$CONTENT_WIDTH"
blank_line
}
failure_menu() {
local choice uploader actions shown i
[[ ${OMARCHY_UI_FAILURE_ACTION:-} == "exit" ]] && return 0
# The failure screen and log tail have already rendered; exit with the
# installer's status instead of blocking on a menu nobody can answer.
interactive || return 0
while true; do
uploader=""
uploader=$(find_log_uploader || true)
actions=("View full log" "Drop to shell" "Reboot" "Power off")
[[ -n $uploader ]] && actions=("Upload log for support" "${actions[@]}")
shown=()
for i in "${!actions[@]}"; do
shown+=("$(omarchy_text "${actions[i]}")")
done
choice=$(gum choose \
--height "$((${#actions[@]} + 1))" \
--header "$(omarchy_text "What would you like to do?")" \
"${shown[@]}" \
<"$TTY_PATH" 2>"$TTY_PATH") || choice=""
for i in "${!shown[@]}"; do
[[ ${shown[i]} == "$choice" ]] && choice=${actions[i]}
done
case "$choice" in
"Upload log for support")
upload_failure_log
render_failure "${failure_status:-1}" "${failure_summary:-}" "${failure_media_diagnosis:-}" >"$TTY_PATH" 2>/dev/null || true
;;
"View full log")
view_failure_log
render_failure "${failure_status:-1}" "${failure_summary:-}" "${failure_media_diagnosis:-}" >"$TTY_PATH" 2>/dev/null || true
;;
"Reboot")
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
;;
"Power off")
poweroff 2>/dev/null || systemctl poweroff 2>/dev/null || true
;;
"Drop to shell"|"")
return 0
;;
esac
done
}
sanitize_child_output() {
# Archinstall is only a library subsystem in Omarchy's installer. Its own
# post-install success/reboot banner can be buffered until process exit and
# appear after an Omarchy failure, which makes the unified log contradictory.
# Keep that third-party banner out of Omarchy's support log; the raw
# archinstall log is still uploaded separately by omarchy-upload-log.
sed -u \
-e $'s/\x1b\\[[0-9;?]*[A-Za-z]//g' \
-e '/^Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers$/d' \
-e '/^Enabling service systemd-timesyncd$/d' \
-e '/^Setting password for root$/d' \
-e '/^Updating \/mnt\/etc\/fstab$/d' \
-e '/^Syncing the system\.\.\.$/d' \
-e '/^Installation completed without any errors\.$/d' \
-e '/^Log files temporarily available at \/var\/log\/archinstall\.$/d' \
-e '/^You may reboot when ready\.$/d'
}
launch_child() {
mkdir -p "$(dirname "$LOG_FILE")"
# Child is non-interactive. All visible UI belongs to this supervisor.
PYTHONUNBUFFERED=1 setsid "${INSTALL_CMD[@]}" </dev/null > >(sanitize_child_output >>"$LOG_FILE") 2>&1 &
child_pid=$!
child_pgid=$child_pid
}
[[ -e $TTY_PATH ]] || exit 2
printf '%s' "$HIDE_CURSOR" >"$TTY_PATH"
render_static >"$TTY_PATH"
set_now
DASH_T0=$NOW
PHASE_T0=$NOW
PKG_LAST_CHANGE=$NOW
launch_child
frame=0
while jobs -pr | grep -qx "$child_pid"; do
# Keep package counting/state reads at 2 Hz; animate the trace at 10 Hz.
(( frame % 5 == 0 )) && update_progress
render_dynamic >"$TTY_PATH" 2>/dev/null || true
frame=$((frame + 1))
sleep 0.1
done
set +e
wait "$child_pid"
status=$?
set -e
child_pid=""
child_pgid=""
dashboard_log "installer child exited with status $status"
if (( status == 0 )); then
# Finish the trace even when a short-lived child exits between state polls.
LAST_PM=1000
PHASE_NAME="Installation complete"
while (( DISPLAY_PM < 999 )); do
render_dynamic >"$TTY_PATH" 2>/dev/null || true
sleep 0.05
done
DISPLAY_PM=1000
render_dynamic >"$TTY_PATH" 2>/dev/null || true
sleep 0.4
fi
if (( status != 0 )); then
failure_status="$status"
failure_summary=""
if [[ -f $STATE_FILE ]]; then
failure_summary="$(jq -r '
(.current_phase // empty | "last installer phase: \(.)"),
((.phases // []) | map(select(.status == "failed")) | last // empty
| "failed phase: \(.name): \(.error)")
' "$STATE_FILE" 2>/dev/null || true)"
if [[ -n $failure_summary ]]; then
while IFS= read -r line; do
[[ -n $line ]] && dashboard_log "$line"
done <<<"$failure_summary"
fi
fi
failure_media_diagnosis="$(diagnose_media || true)"
render_failure "$failure_status" "$failure_summary" "$failure_media_diagnosis" >"$TTY_PATH" 2>/dev/null || true
# Recorded after the screen is drawn, not before: the log tail on that screen
# reads this same file, and the diagnosis printed twice reads as two findings.
if [[ -n $failure_media_diagnosis ]]; then
while IFS= read -r line; do
[[ -n $line ]] && dashboard_log "$line"
done <<<"$failure_media_diagnosis"
fi
failure_menu
exit "$failure_status"
fi
# Deferred-provisioning installs finish silently and reboot on their own — the
# owner completes setup (and sees the send-off) at first boot, so the "Installed
# Omarchy in ..." celebration and the Reboot Now prompt belong to a direct
# install only.
if [[ ${OMARCHY_UI_DEFER_PROVISIONING:-} == "yes" ]]; then
printf '%s' "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
[[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]] && exit 0
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
exit 0
fi
set +e
render_finish
finish_render_status=$?
set -e
if (( finish_render_status != 0 )); then
dashboard_log "finish screen render failed with status $finish_render_status; using fallback"
{
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
printf '\nFinished installing\n\n'
} >"$TTY_PATH" 2>/dev/null || true
fi
if reboot_prompt; then
if [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]]; then
exit 0
fi
printf '%s' "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
fi
exit 0