Improve ISO installer flow and VM tooling

This commit is contained in:
Ryan Hughes committed 2026-05-25 19:54:12 -04:00
1 parent c50d0bc299
commit afa8293b2b
8 files changed
+807 -246

No files matched your search

+279 -31
View File
@@ -1,20 +1,276 @@
#!/bin/bash
# The configurator normally runs under this small PTY proxy so Ctrl-T can work
# as a global tools shortcut even while gum owns the terminal in raw mode. The
# child configurator keeps stdout on the existing stream (so generated config and
# logs behave normally), while stderr and /dev/tty traffic are relayed through the
# PTY. Pressing Ctrl-T kills the current configurator screen, opens Tools, then
# starts the configurator over at keyboard selection.
if [[ -z "${OMARCHY_CONFIGURATOR_INNER:-}" && -z "${OMARCHY_CONFIGURATOR_NO_KEY_PROXY:-}" ]] && { [[ -t 0 ]] || [[ -t 2 ]]; }; then
OMARCHY_CONFIGURATOR_PYTHON=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true)
if [[ -n "$OMARCHY_CONFIGURATOR_PYTHON" ]]; then
export OMARCHY_CONFIGURATOR_SCRIPT
OMARCHY_CONFIGURATOR_SCRIPT=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || printf '%s' "${BASH_SOURCE[0]}")
exec "$OMARCHY_CONFIGURATOR_PYTHON" - "$@" <<'PY'
import errno
import fcntl
import os
import pty
import select
import signal
import sys
import termios
import time
CTRL_T = b"\x14"
SCRIPT = os.environ["OMARCHY_CONFIGURATOR_SCRIPT"]
ARGS = sys.argv[1:]
try:
tty_fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY)
except OSError:
env = os.environ.copy()
env["OMARCHY_CONFIGURATOR_INNER"] = "1"
os.execvpe("bash", ["bash", SCRIPT] + ARGS, env)
stdout_fd = os.dup(1)
original_tty = termios.tcgetattr(tty_fd)
current_master = None
current_pgid = None
def restore_tty():
try:
termios.tcsetattr(tty_fd, termios.TCSADRAIN, original_tty)
except termios.error:
pass
def write_tty(data):
if data:
os.write(tty_fd, data)
def copy_window_size(master):
try:
size = fcntl.ioctl(tty_fd, termios.TIOCGWINSZ, b"\0" * 8)
fcntl.ioctl(master, termios.TIOCSWINSZ, size)
except OSError:
pass
def on_winch(signum, frame):
if current_master is not None:
copy_window_size(current_master)
if current_pgid is not None:
try:
os.killpg(current_pgid, signal.SIGWINCH)
except OSError:
pass
def on_termination(signum, frame):
if current_pgid is not None:
try:
os.killpg(current_pgid, signum)
except OSError:
pass
restore_tty()
raise SystemExit(128 + signum)
def exit_code(status):
if os.WIFEXITED(status):
return os.WEXITSTATUS(status)
if os.WIFSIGNALED(status):
return 128 + os.WTERMSIG(status)
return 1
def spawn_child(tools_only=False):
global current_master, current_pgid
pid, master = pty.fork()
if pid == 0:
os.dup2(stdout_fd, 1)
try:
os.close(stdout_fd)
except OSError:
pass
env = os.environ.copy()
env["OMARCHY_CONFIGURATOR_INNER"] = "1"
env["OMARCHY_CONFIGURATOR_CTRL_T"] = "1"
if tools_only:
env["OMARCHY_CONFIGURATOR_TOOLS_ONLY"] = "1"
else:
env.pop("OMARCHY_CONFIGURATOR_TOOLS_ONLY", None)
os.execvpe("bash", ["bash", SCRIPT] + ARGS, env)
os.set_blocking(master, False)
current_master = master
current_pgid = pid
copy_window_size(master)
return pid, master
def wait_for_child(pid, timeout):
deadline = time.time() + timeout
while time.time() < deadline:
try:
done, status = os.waitpid(pid, os.WNOHANG)
except ChildProcessError:
return 0
if done == pid:
return exit_code(status)
time.sleep(0.05)
return None
def terminate_child(pid):
for sig, timeout in ((signal.SIGTERM, 0.5), (signal.SIGKILL, 0.2)):
try:
os.killpg(pid, sig)
except ProcessLookupError:
return 0
except OSError:
try:
os.kill(pid, sig)
except OSError:
return 0
code = wait_for_child(pid, timeout)
if code is not None:
return code
return 130
def drain_master(master):
while True:
try:
data = os.read(master, 4096)
except BlockingIOError:
return
except OSError:
return
if not data:
return
write_tty(data)
def relay_child(tools_only=False):
global current_master, current_pgid
pid, master = spawn_child(tools_only)
try:
while True:
try:
done, status = os.waitpid(pid, os.WNOHANG)
except ChildProcessError:
drain_master(master)
return 0
if done == pid:
drain_master(master)
return exit_code(status)
try:
readable, _, _ = select.select([tty_fd, master], [], [], 0.1)
except InterruptedError:
continue
if tty_fd in readable:
try:
data = os.read(tty_fd, 1024)
except OSError:
data = b""
if not data:
continue
if not tools_only and CTRL_T in data:
before = data.split(CTRL_T, 1)[0]
if before:
try:
os.write(master, before)
except OSError:
pass
terminate_child(pid)
drain_master(master)
return "tools"
try:
os.write(master, data)
except OSError:
pass
if master in readable:
try:
data = os.read(master, 4096)
except BlockingIOError:
continue
except OSError as exc:
if exc.errno != errno.EIO:
raise
data = b""
if data:
write_tty(data)
finally:
current_master = None
current_pgid = None
try:
os.close(master)
except OSError:
pass
signal.signal(signal.SIGWINCH, on_winch)
signal.signal(signal.SIGHUP, on_termination)
signal.signal(signal.SIGTERM, on_termination)
# Use raw-ish input so Ctrl-T/Ctrl-C can be intercepted and relayed, but keep
# the real terminal's output processing intact. Full raw mode disables ONLCR,
# causing gum/logo newlines to stair-step across the screen.
tty_attrs = termios.tcgetattr(tty_fd)
tty_attrs[0] &= ~(termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON)
tty_attrs[2] |= termios.CS8
tty_attrs[3] &= ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
tty_attrs[6][termios.VMIN] = 1
tty_attrs[6][termios.VTIME] = 0
termios.tcsetattr(tty_fd, termios.TCSADRAIN, tty_attrs)
try:
while True:
result = relay_child(tools_only=False)
if result == "tools":
relay_child(tools_only=True)
continue
raise SystemExit(result)
finally:
restore_tty()
try:
os.close(stdout_fd)
os.close(tty_fd)
except OSError:
pass
PY
fi
fi
unset OMARCHY_CONFIGURATOR_PYTHON
# Minimal live-ISO presentation helpers. Keep these local to the ISO: the
# installed Omarchy package owns target setup helpers, while the ISO owns the
# installer UI that runs before any target packages exist.
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
LOGO_PATH="${LOGO_PATH:-$OMARCHY_PATH/logo.txt}"
TERM_HEIGHT=${LINES:-24}
TERM_WIDTH=${COLUMNS:-80}
if [[ -e /dev/tty ]]; then
TERM_SIZE=$(stty size 2>/dev/null </dev/tty || true)
fi
if [[ -n ${TERM_SIZE:-} ]]; then
TERM_HEIGHT=${TERM_SIZE%% *}
TERM_WIDTH=${TERM_SIZE##* }
else
TERM_HEIGHT=${LINES:-24}
TERM_WIDTH=${COLUMNS:-80}
if [[ $TERM_SIZE =~ ^([0-9]+)[[:space:]]+([0-9]+)$ ]] && (( BASH_REMATCH[1] > 0 && BASH_REMATCH[2] > 0 )); then
TERM_HEIGHT=${BASH_REMATCH[1]}
TERM_WIDTH=${BASH_REMATCH[2]}
fi
fi
if [[ -f $LOGO_PATH ]]; then
@@ -60,10 +316,20 @@ abort() {
exit 1
}
tools_hint() {
[[ -n "${OMARCHY_CONFIGURATOR_CTRL_T:-}" && -z "${OMARCHY_CONFIGURATOR_TOOLS_ONLY:-}" ]] || return 0
# Draw the shortcut as a fixed footer instead of in the form body. Keep the
# cursor where the active gum widget expects it; do not print a newline from
# the bottom row or the VT will scroll.
printf "\0337\033[%s;1H\033[2K%s\033[38;5;8mCtrl-T Tools\033[0m\0338" "$TERM_HEIGHT" "$PADDING_LEFT_SPACES"
}
step() {
clear_logo
echo
gum style --padding "0 0 0 $PADDING_LEFT" "$1"
tools_hint
echo
}
@@ -80,8 +346,8 @@ notice() {
}
# Tools menu — recovery-style actions kept off the main install path.
# Inlines its own disk picker so the launcher can run before disk_form is
# defined further down in the script.
# Inlines its own disk picker so Ctrl-T can open it before disk_form is defined
# further down in the script.
tools_menu() {
while true; do
step "Tools"
@@ -126,28 +392,10 @@ tools_menu() {
done
}
# Landing screen: Enter installs, Ctrl-T opens the tools menu.
launcher_form() {
while true; do
step "Install Omarchy"
say "Press Enter to install Omarchy on this machine."
echo
say --foreground 8 "Ctrl-T Tools"
local key
IFS= read -rsn1 key || true
case "$key" in
""|$'\n'|$'\r')
return 0
;;
$'\x14')
tools_menu
;;
esac
done
}
launcher_form
if [[ -n "${OMARCHY_CONFIGURATOR_TOOLS_ONLY:-}" ]]; then
tools_menu
exit 0
fi
# STEP 1: KEYBOARD LAYOUT