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.
1207 lines
42 KiB
Bash
Executable File
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
|