Compare commits

..
Author SHA1 Message Date
David Heinemeier HanssonandClaude f32247afc3 Confirm the Start Omarchy Now prompt after provisioning setup (#100)
* Confirm the Start Omarchy Now prompt after provisioning setup

First-boot account setup now ends on a celebration screen whose Start
Omarchy Now button gates the SDDM handoff, so the provisioning test
must press it — same as the installer's Reboot Now prompt — once the
pending flag clears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add a centered greeter before the installer keyboard step

Open the ISO configurator with the same greeter the deferred-provisioning
first boot uses — vertically centered logo, the Omarchy tagline, and a
single button — so install and first boot bookend setup identically. The
installer's button reads "Start install"; first boot's reads "Start setup".

Co-Authored-By: Claude <noreply@anthropic.com>

* Animate the installer greeter logo with a skippable ColorShift

Match the first-boot greeter: a looping tte ColorShift over the centered
logo with a "Press Return to Start Install" hint instead of a button.
Return begins the install at any time.

Co-Authored-By: Claude <noreply@anthropic.com>

* Theme the installer greeter ColorShift to the Omarchy palette

Match the first-boot greeter: sweep the logo ColorShift through the
Tokyo Night accent colors instead of tte's default rainbow.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix installer greeter ColorShift: green base, no flash, clean handoff

Mirror the first-boot greeter fixes: indexed ANSI colors (green base with
a cyan accent) because the framebuffer console can't render tte truecolor;
one long tte invocation so the loop never flashes; stty sane + clear after
Return so the keyboard step starts clean.

Co-Authored-By: Claude <noreply@anthropic.com>

* Dismiss the greeter in the QEMU test drivers

The installer and first-boot flows now open with a greeter (animated logo
+ tagline). Wait for the tagline and press Return before the keyboard
step, in both drive paths, so the harness gets past the greeter.

Co-Authored-By: Claude <noreply@anthropic.com>

* Kill the installer greeter animation cleanly

Run tte directly instead of inside a backgrounded `while` subshell, so
killing the tracked PID actually stops the effect. Killing the subshell
orphaned tte, which kept painting the logo over the keyboard step.

Co-Authored-By: Claude <noreply@anthropic.com>

* Guard the greeter wait against set -e in the configurator too

Harmless today (no set -e), but keeps the greeter's animation teardown
robust if that changes.

Co-Authored-By: Claude <noreply@anthropic.com>

* Also settle the terminal in emulators, not just real VTs

wait_for_stable_terminal skipped terminal emulators on the assumption
they're already at final size, but a fresh terminal (or sudo's pty)
reports a stale 24x80 for a moment at startup too. Drop the VT-only gate
so the greeter centers there as well.

Co-Authored-By: Claude <noreply@anthropic.com>

* Test harness: drive the first-boot timezone step and direct SDDM handoff

Match the deferred-provisioning first-boot flow changes: the owner now picks
a timezone after the email step, and setup hands straight off to SDDM instead
of pausing on a celebration screen with a Start Omarchy Now button. Drive the
new Timezone screen (accept the geo-guessed default) and drop the button
confirmation.

Co-Authored-By: Claude <noreply@anthropic.com>

* Don't bake hostname/timezone into deferred installs; set them at first boot

Deferred provisioning defers the user step to first boot, so the machine's
owner should name the machine and pick the timezone there — not inherit values
chosen at install time. Stop setting hostname/timezone in the configurator's
deferred branch: the orchestrator already skips an empty timezone (the system
stays UTC until first boot), and it now falls back to a neutral "archlinux"
hostname for the install so archinstall still succeeds. The first-boot
omarchy-provision-owner form collects and applies both.

Harness: drive the new Hostname screen and assert both hostname and timezone
actually landed on the running system after first boot.

Co-Authored-By: Claude <noreply@anthropic.com>

* Match the installer greeter: solid green logo, no color animation

Same fix as the first-boot greeter — tte's ColorShift judders on the 16-color
framebuffer console (truecolor crush). Show the logo in solid green and drop
the animation so both greeters look identical and flash-free.

Co-Authored-By: Claude <noreply@anthropic.com>

* Harden the provision harness: SSH-blip wait, timezone timeout, hostname assert

- The pending-flag wait treated an SSH connection failure (rc 255) the same as
  "flag absent" (rc 1), so a transient blip during finalization would exit early
  and snapshot the base mid-finalization. Only break on a clean rc 1; keep
  waiting on 0 or 255.
- Bump the first-boot Timezone screen wait 60s -> 180s (tzupdate geo-guess over
  first-boot networking can lag; matches the installer's headroom).
- Make the applied-hostname check a hard failure (it's deterministic — we typed
  it), so a first-boot apply regression can't pass green; timezone stays a
  report since it's geo-guessed.

Co-Authored-By: Claude <noreply@anthropic.com>

* Seed hostname=omarchy/timezone=UTC in deferred install instead of empties

Write neutral valid defaults in the configurator's deferred branch rather than
empty strings, and drop the orchestrator's `config.hostname or "archlinux"`
fallback (hostname is always non-empty now). The first-boot wizard overwrites
both. This keeps the "owner sets them at first boot" behavior without depending
on archinstall tolerating empty hostname/timezone at config-load time.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix set -e regression in the provision pending-wait loop

The previous hardening moved `ssh_guest "test -f pending"` out of the `while`
condition into a standalone command, but the harness runs `set -euo pipefail` —
so the moment the flag cleared (ssh returns 1, the intended "done" signal) it
tripped errexit and aborted the whole run right after provisioning, before the
session/acceptance phase. Capture the result with `|| rc=$?` so the non-zero is
the signal, not a fatal error. Also guard the follow-on hostname/timezone
substitutions with `|| got=""` so a transient SSH failure there can't abort
either (an empty value then correctly fails the hostname assertion).

Co-Authored-By: Claude <noreply@anthropic.com>

* Bring back the installer greeter ColorShift animation

Match the first-boot greeter: restore the static green logo + looping tte
ColorShift overlay. The framebuffer truecolor crush gives it a quick flash,
which is acceptable — both greeters keep the animation.

Co-Authored-By: Claude <noreply@anthropic.com>

* Skip the install celebration on deferred installs; reboot directly

A deferred-provisioning install has nothing to celebrate yet — the owner
finishes setup (and gets the send-off) at first boot. So the installer's
"Installed Omarchy in ..." screen and the Reboot Now prompt now belong to a
direct install only: for a deferred install the dashboard clears and reboots on
its own. .automated_script.sh detects deferred mode (the config's
defer_provisioning flag, or the cidata defer-provisioning marker) and passes
OMARCHY_UI_DEFER_PROVISIONING to the dashboard.

Harness: a deferred install no longer shows Reboot Now, so wait_for_install
recognizes the auto-reboot by the first-boot greeter tagline appearing.

Co-Authored-By: Claude <noreply@anthropic.com>

* Detect deferred mode with jq, not a line regex

The dashboard's deferred detection grepped user_configuration.json for the
defer_provisioning boolean on one line; a reformatted config would read as a
direct install and wrongly show the celebration. Parse it with jq using the
same path the orchestrator reads (.omarchy_install.defer_provisioning).

Co-Authored-By: Claude <noreply@anthropic.com>

* Drop Bosnian and Khmer from the installer keyboard picker

Match the first-boot picker: neither has a console keymap, so both silently
fell back to US. Remove them rather than offer a layout that can't apply.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 21:56:01 +02:00
David Heinemeier HanssonandClaude 81de5087ea Wait for the live VT to settle before drawing the first screen
tty1 boots as an 80-column console and only widens once the kernel
framebuffer/KMS takes over, which lands late — especially in VMs. The
configurator's logo is wider than 80 columns, so the very first draw
measured against that transient narrow console: the logo wrapped
("smushed") and, with left padding clamped to zero, hugged the left
edge. Every later screen re-measures once the console has grown, which
is why only the opening keyboard screen looked wrong.

Hold the first draw until the width settles at or above the logo width,
capped so a console that never widens still proceeds. Terminal emulators
(dry runs) are already at final size and are skipped.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 03:13:00 -07:00
David Heinemeier HanssonandClaude Fable 5 d98b383e20 Fix orchestrator caller of the renamed user-provisioning command
run_chroot_finalizer still invoked /usr/bin/omarchy-finalize-user, which the
rename moved to omarchy-provision-user — so a normal install died with 'No
such file'. (Deferred-provisioning installs skip this phase, so only normal
installs were affected.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 02:45:57 -07:00
David Heinemeier HanssonandClaude Fable 5 5f2f336aba Rename OEM vocabulary to provisioning (ISO)
Matches the runtime rename. The deferred-provisioning install mode replaces
'OEM' everywhere:
  omarchy_install.oem            → omarchy_install.defer_provisioning
  --oem / --oem-file             → --defer-provisioning / --defer-provisioning-file
  OMARCHY_INSTALL_OEM_FILE       → OMARCHY_INSTALL_DEFER_PROVISIONING_FILE
  cidata 'oem' marker            → 'defer-provisioning'
  /var/lib/omarchy/oem/          → /var/lib/omarchy/provisioning/
  /etc/omarchy/oem.key           → /etc/omarchy/provisioning.key
  install/oem/omarchy-oem-setup.service → install/provisioning/omarchy-provision-owner.service
  stage_oem_state phase          → stage_provisioning_state ('Staging provisioning')

Orchestrator internals (OEM_STATE_DIR, _validate_oem_state, ctx.oem, …),
the configurator, cidata-load, the dashboard phase band, the harness
(--oem → --provision), and the tests all move together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 02:37:50 -07:00
David Heinemeier HanssonandClaude Fable 5 7c196f8d12 Keyboard screen: Ctrl+C hint directly under the header, no blank line
Match the disk-overwrite screen, where the unencrypted-install hint sits on
the line right below its message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:35:35 -07:00
David Heinemeier HanssonandClaude Fable 5 c11276d49a Tighten the prepare-for-another-owner confirm wording
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:33:58 -07:00
David Heinemeier HanssonandClaude Fable 5 944877fdab Only show the mode picker when free space is also an option
Restore the pre-OEM behavior: a disk with no unallocated free space has just
one viable mode, so the wizard skips the mode picker and goes straight to the
overwrite confirmation. The picker appears only when a free-space install is
also on offer.

Also give the harness's first-boot SSH bootstrap more headroom (360s): an
encrypted OEM first boot re-keys LUKS and rebuilds the UKI before sshd
settles, which can exceed 180s under parallel VM load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:30:12 -07:00
David Heinemeier HanssonandClaude Fable 5 57beb18b72 Restore keyboard→user→disk order; drop OEM wording from the configurator UI
The normal install flow returns to its original order: keyboard, then the
user step, then disk selection and mode. The Ctrl+C shortcut on the first
screen and its confirmation now speak only of preparing the machine for
another owner — no 'OEM' in any user-facing string (the omarchy_install.oem
data contract is unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:14:56 -07:00
David Heinemeier HanssonandClaude Fable 5 2f54e5579b OEM mode via Ctrl+C on the first screen, not a mode-picker entry
Restore the original screen order (keyboard first, then disk) and make Ctrl+C
on the keyboard screen the entry into OEM mode — the same hidden-Ctrl+C idiom
the disk-overwrite screen already uses for unencrypted installs. A confirm
guards against an accidental Ctrl+C; once confirmed, OEM jumps straight to
disk selection and the overwrite confirmation (no keyboard, no user), since
OEM is always a full-disk install. The 'OEM install' entry leaves the mode
picker. The harness drives the new path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:08:58 -07:00
David Heinemeier HanssonandClaude Fable 5 30759f2082 Drop the configurator's runtime-support gate for OEM mode
Remove oem_supported() and its branches: the configurator always offers OEM
install now, rather than scanning the offline mirror's runtime package to
decide. Building an ISO against a runtime that ships OEM support is the
build's responsibility; the orchestrator still fails an OEM install with a
clear error if the staged target somehow lacks omarchy-oem-setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 00:58:27 -07:00
David Heinemeier HanssonandClaude Fable 5 98a345c017 Update OEM comments to the renamed factory-reset command
Reference omarchy-system-factory-reset (was omarchy-reset-computer) in the
orchestrator comments and the plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 00:52:14 -07:00
David Heinemeier HanssonandClaude Fable 5 a478349ca9 Harness: prove the OEM keyboard step applies a non-default layout
Drive the OEM first-boot keyboard picker to English (UK) and assert the
installed system ends up with KEYMAP=uk and XKBLAYOUT=gb — proving the
deferred keyboard step actually applied a layout, not just rendered. UK keeps
every letter identical to US so the raw-keycode password still matches, and
the email's '@' (Shift+' under UK, not Shift+2) is typed with the UK keycode.
The console bootstrap now fetches-then-runs instead of piping, so it survives
the non-US layout the keyboard step installs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:13:53 -07:00
David Heinemeier HanssonandClaude Fable 5 7f1fdf2427 Reorder configurator: disk before keyboard, skip keyboard for OEM
The wizard now opens on disk selection; keyboard moves to after the install
mode is chosen and stays before the user step (so the LUKS passphrase is
still typed under the chosen layout). OEM installs skip keyboard and user
entirely — the machine's owner picks both at first boot — so the emitted
config just carries a default 'us' keymap as a placeholder.

The QEMU harness drives the new order and the OEM first-boot keyboard step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 22:36:56 -07:00
David Heinemeier HanssonandClaude Fable 5 8aee09cb29 Second review pass: strip account fields, clear stale cidata inputs, await OEM finalization
Codex xhigh round 2:

- Strip account authentication fields (!users, !root-password,
  root_enc_password, auth_config account entries) from the archinstall config
  in OEM mode, not just user_credentials — a combined/legacy config could
  otherwise have archinstall create a provisioning account before OOBE.
- Clear stale OEM inputs (oem marker, credentials, optional files) from /root
  before copying a cidata drive, so a retried install in the same live session
  can't force a fresh drive back into OEM mode or reuse an old LUKS passphrase.
- The test harness waits for the OEM pending flag to clear before saving the
  base image, so it never snapshots a half-finalized first boot; it also
  handles the deliberately skipped mode picker on blank disks built against a
  pre-OEM runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 19:03:49 -07:00
David Heinemeier HanssonandClaude Fable 5 e49ea2bc96 Keep provisioning credentials out of OEM installs and factory snapshots
Review findings from codex and Copilot:

- A rig-supplied credentials file in OEM mode now contributes only its LUKS
  passphrase: preserved users/root_enc_password fields would have created a
  hidden provisioning account despite OEM promising no user until first boot.
- The factory snapshot is taken writable, scrubbed of deployment credentials
  (staged authorized_keys, tailscale auth key and join unit, the dead OEM
  LUKS key and its initramfs drop-ins), then set read-only — a reset years
  later must not authorize the original deployment key or rejoin its tailnet.
- The missing-Node-tarball case is a hard install error on every install,
  not just OEM: the stash is what lets a factory reset finalize offline.
- The configurator's OEM gate checks the runtime package for the service
  unit as well as the setup binary.
- The test harness handles the deliberately skipped mode picker on blank
  disks when the bundled runtime lacks OEM support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 18:09:10 -07:00
David Heinemeier HanssonandClaude Fable 5 445db32792 Make OEM state world-readable for user-context finalization
The Node tarball in /var/lib/omarchy/oem/packages is read by
omarchy-finalize-user running as the freshly created user; a 0700 directory
forced the offline install onto its network fallback. The only secret inside
(luks-key) is itself 0600 root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:51:01 -07:00
David Heinemeier HanssonandClaude Fable 5 fbee4ba437 Isolate the test harness SSH identity from the local ssh-agent
A loaded agent offers its keys before the harness identity and can exhaust
the server's MaxAuthTries before the right key is ever tried; IdentitiesOnly
pins authentication to the per-run key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:03:15 -07:00
David Heinemeier HanssonandClaude Fable 5 b5f7e96c70 Offer OEM mode only when the bundled runtime supports it
The configurator now peeks into the offline mirror's runtime package for
omarchy-oem-setup before offering the OEM install mode, so an ISO built
against a pre-OEM runtime can't start an install doomed to fail at the
staging phase (which still hard-checks as the backstop). Blank disks with no
OEM choice to offer skip the mode picker entirely, like the pre-OEM flow.

Also keep the test harness QMP socket in /tmp — unix socket paths cap at
~108 bytes, which nested test-run directories can exceed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:52:55 -07:00
David Heinemeier HanssonandClaude Fable 5 679edce2e3 Add OEM install mode and factory snapshots
OEM mode installs the entire system but defers user creation to first boot,
where the runtime's omarchy-oem-setup asks for the owner and lands them in the
already-installed system — and the same machinery arms omarchy-reset-computer.

The configurator's install-mode step gains an OEM choice, which required
moving the user step after mode selection (OEM skips it entirely). Autoinstall
rigs select OEM with an `oem` marker file on the cidata drive, which also
replaces the user_credentials.json requirement.

In the orchestrator, OEM swaps the user phases for a stage_oem_state phase:
it stashes the bundled Node tarball (every install, for offline factory
resets), arms omarchy-oem-setup.service in the target, and on encrypted
installs stages the throwaway LUKS passphrase — generated by InstallContext
when the input carries none — with a keyfile embedded in the UKI so boot
auto-unlocks until first-boot setup re-keys to the owner's password. The
install fails with a clear error if the bundled runtime lacks OEM support.

Every install now also ends with a read-only @factory snapshot of @ at the
btrfs top level — outside snapper's reach — which is what makes
omarchy-reset-computer a true factory reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:41:04 -07:00
8 changed files with 205 additions and 260 deletions

No files matched your search

+5 -5
View File
@@ -711,17 +711,17 @@ drive_provision_owner() {
log "Driving the deferred-provisioning first-boot setup" log "Driving the deferred-provisioning first-boot setup"
# Deferred provisioning defers the keyboard step to first boot; it comes before the user form. # Deferred provisioning defers the keyboard step to first boot; it comes before the user form.
# Pick English (UK) (one row below the preselected English (US), which leads # Pick English (UK) (one row above the preselected English (US)) so the
# the list) so the verifier can prove the deferred step applied a non-default # verifier can prove the deferred step applied a non-default layout — UK
# layout — UK leaves every letter identical to US, so the typed password (sent # leaves every letter identical to US, so the typed password (sent as raw
# as raw keycodes) still matches the literal one the harness pipes to sudo. # keycodes) still matches the literal one the harness pipes to sudo.
# A greeter (animated logo + tagline) opens first boot; Return begins. # A greeter (animated logo + tagline) opens first boot; Return begins.
wait_for_screen "Opinionated" 600 wait_for_screen "Opinionated" 600
capture_console "success-provision-00-greeter" capture_console "success-provision-00-greeter"
press ret press ret
wait_for_screen "keyboard layout" 120 wait_for_screen "keyboard layout" 120
press down press up
capture_console "success-provision-01-keyboard-layout-uk" capture_console "success-provision-01-keyboard-layout-uk"
press ret press ret
+1 -27
View File
@@ -117,7 +117,7 @@ cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/"
# The selected omarchy-settings package is needed here so its post_install hook # The selected omarchy-settings package is needed here so its post_install hook
# drops Omarchy's plymouthd.conf into /etc/plymouth before mkarchiso builds the # drops Omarchy's plymouthd.conf into /etc/plymouth before mkarchiso builds the
# live initramfs. # live initramfs.
arch_packages=(linux-t2 git gum jq openssl plymouth ttfx tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted) arch_packages=(linux-t2 git gum jq openssl plymouth python-terminaltexteffects tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted)
printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.x86_64" printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.x86_64"
# The live ISO boots linux-t2 (see airootfs/etc/mkinitcpio.d/linux-t2.preset), so # The live ISO boots linux-t2 (see airootfs/etc/mkinitcpio.d/linux-t2.preset), so
@@ -139,7 +139,6 @@ sed -i -E '/^(linux|broadcom-wl)$/d' "$build_cache_dir/packages.x86_64"
# pulls the published omarchy* from the network mirror like any other package. # pulls the published omarchy* from the network mirror like any other package.
if [[ -d /omarchy-source ]]; then if [[ -d /omarchy-source ]]; then
base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages) base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages)
setup_form=/omarchy-source/install/provisioning/setup-form.sh
else else
# Pull the same package lists out of the freshly-downloaded Omarchy runtime # Pull the same package lists out of the freshly-downloaded Omarchy runtime
# package so we don't need a local checkout in the non-local-source path. # package so we don't need a local checkout in the non-local-source path.
@@ -155,37 +154,12 @@ else
mkdir -p /tmp/omarchy-pkglists mkdir -p /tmp/omarchy-pkglists
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/omarchy-base.packages usr/share/omarchy/install/omarchy-other.packages bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/omarchy-base.packages usr/share/omarchy/install/omarchy-other.packages
base_pkg_lists=(/tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-base.packages /tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-other.packages) base_pkg_lists=(/tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-base.packages /tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-other.packages)
# Extracted on its own, tolerating a miss: bsdtar exits non-zero for a member
# it can't find, so asking for this alongside the package lists would abort the
# build here (set -e) with a bare "Not found in archive" instead of the
# actionable error below.
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/provisioning/setup-form.sh 2>/dev/null || true
setup_form=/tmp/omarchy-pkglists/usr/share/omarchy/install/provisioning/setup-form.sh
fi fi
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso" mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
cp "${base_pkg_lists[0]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages" cp "${base_pkg_lists[0]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
cp "${base_pkg_lists[1]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages" cp "${base_pkg_lists[1]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages"
# The configurator's setup form comes from the runtime this ISO bundles, so the
# installer and the first-boot setup that finishes a deferred install can never
# disagree. A runtime predating the split ships no such file, which would leave
# the configurator with no prompts at all.
if [[ ! -f $setup_form ]]; then
if [[ -d /omarchy-source ]]; then
echo "ERROR: the --local-source checkout ships no install/provisioning/setup-form.sh" >&2
remedy="Update the checkout to a revision carrying the shared setup form."
else
echo "ERROR: $OMARCHY_RUNTIME_PACKAGE does not ship install/provisioning/setup-form.sh" >&2
remedy="Publish a runtime carrying the shared setup form, or build with --local-source against a checkout that has it."
fi
echo " The configurator sources its prompts from that file, so this ISO" >&2
echo " would boot into an installer with no questions to ask." >&2
echo " $remedy" >&2
exit 1
fi
cp "$setup_form" "$build_cache_dir/airootfs/usr/share/omarchy-iso/setup-form.sh"
# Collect every package we want available in the offline mirror. # Collect every package we want available in the offline mirror.
declare -a all_packages declare -a all_packages
mapfile -t all_packages < <( mapfile -t all_packages < <(
@@ -1,3 +0,0 @@
# linux-t2 ships for every machine, and on a pre-T2 Mac its applesmc oopses
# registering the keyboard-backlight LED, wedging udev. Nothing here needs the SMC.
blacklist applesmc
+163 -111
View File
@@ -9,21 +9,6 @@ OMARCHY_SETTINGS_PACKAGE="${OMARCHY_SETTINGS_PACKAGE:-omarchy-settings}"
LOGO_PATH="$OMARCHY_PATH/logo.txt" LOGO_PATH="$OMARCHY_PATH/logo.txt"
# The setup form — keyboard, account, hostname, and timezone questions, plus the
# rules their answers are checked against — shared verbatim with the first-boot
# owner setup that finishes a deferred install. build-iso.sh vendors it out of
# the runtime package this ISO bundles, so the two can never offer different
# layouts or accept different usernames. On the ISO that vendored copy is what
# loads; a dry run outside the ISO falls back to the omarchy checkout
# bin/omarchy-iso-configurator already points OMARCHY_PATH at.
SETUP_FORM="${SETUP_FORM:-/usr/share/omarchy-iso/setup-form.sh}"
[[ -r $SETUP_FORM ]] || SETUP_FORM="$OMARCHY_PATH/install/provisioning/setup-form.sh"
if [[ ! -r $SETUP_FORM ]]; then
echo "ERROR: shared setup form not found at $SETUP_FORM" >&2
exit 1
fi
source "$SETUP_FORM"
LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH") LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH")
export GUM_CONFIRM_PROMPT_FOREGROUND="6" export GUM_CONFIRM_PROMPT_FOREGROUND="6"
@@ -121,42 +106,36 @@ greeter() {
# ColorShift the logo: a green base (indexed color 2) with a cyan accent (6) # ColorShift the logo: a green base (indexed color 2) with a cyan accent (6)
# drifting through, settling on green. Indexed ANSI colors, not hex — the # drifting through, settling on green. Indexed ANSI colors, not hex — the
# framebuffer console can't render truecolor faithfully (it crushes the # framebuffer console can't render tte's truecolor faithfully (it crushes the
# palette to a muddy lavender), but the 16 indexed colors map to the Tokyo # palette to a muddy lavender), but the 16 indexed colors map to the Tokyo
# Night palette and render true. --xterm-colors is what keeps them indexed: # Night palette and render true. One long-running invocation (many cycles) so
# ttfx resolves even indexed stops to truecolor otherwise, which would put the # it never restarts — a restart is what flashed. The effect reads from
# settle color out of step with the `gum style --foreground 2` logo underneath. # /dev/null so it never swallows the Return; --reuse-canvas paints upward from
# One long-running invocation (many cycles) so it never restarts — a restart is # the saved cursor, anchored one row below the logo.
# what flashed. The effect reads from /dev/null so it never swallows the if command -v tte >/dev/null 2>&1; then
# Return; --reuse-canvas paints upward from the saved cursor, anchored one row printf '\033[%d;1H\0337' "$((logo_row + logo_h))"
# below the logo. # Run tte directly (not inside a `while` subshell) so $anim is tte's own
printf '\033[%d;1H\0337' "$((logo_row + logo_h))" # PID: killing a wrapping subshell would orphan tte, which then keeps
# Run ttfx directly (not inside a `while` subshell) so $anim is ttfx's own # painting the logo over the keyboard step. --cycles is high enough that it
# PID: killing a wrapping subshell would orphan ttfx, which then keeps # never ends on its own before Return.
# painting the logo over the keyboard step. --cycles is high enough that it tte -i "$LOGO_PATH" \
# never ends on its own before Return. --canvas-width "$cols" \
# Two columns narrower than the console: ttfx centers its text two columns --anchor-text c \
# right of plain centering, so a full-width canvas would sit the animated --frame-rate 60 \
# logo a column off the gum-drawn one underneath it. --reuse-canvas \
ttfx -i "$LOGO_PATH" \ colorshift \
--canvas-width "$((cols > 2 ? cols - 2 : cols))" \ --gradient-stops 2 10 6 10 \
--anchor-text c \ --gradient-frames 3 \
--frame-rate 60 \ --cycles 1000 \
--reuse-canvas \ --final-gradient-stops 2 \
--xterm-colors \ </dev/null >/dev/tty 2>/dev/null &
colorshift \ anim=$!
--gradient-stops 2 10 6 10 \ fi
--gradient-frames 3 \
--cycles 1000 \
--final-gradient-stops 2 \
</dev/null >/dev/tty 2>/dev/null &
anim=$!
IFS= read -r _ </dev/tty || true IFS= read -r _ </dev/tty || true
# Tolerate ttfx's kill signal in `wait` so a future `set -e` here can't abort. # Tolerate tte's kill signal in `wait` so a future `set -e` here can't abort.
kill "$anim" 2>/dev/null [[ -n ${anim:-} ]] && { kill "$anim" 2>/dev/null; wait "$anim" 2>/dev/null || true; }
wait "$anim" 2>/dev/null || true # tte leaves the tty in raw/no-echo mode when killed; restore it or the gum
# ttfx leaves the tty in raw/no-echo mode when killed; restore it or the gum
# prompts in the keyboard step that follows silently die. # prompts in the keyboard step that follows silently die.
stty sane </dev/tty 2>/dev/null || true stty sane </dev/tty 2>/dev/null || true
# Reset the screen so the leftover animation frame doesn't linger under the # Reset the screen so the leftover animation frame doesn't linger under the
@@ -193,33 +172,81 @@ notice() {
# STEP 1: KEYBOARD LAYOUT # STEP 1: KEYBOARD LAYOUT
keyboard_form() { keyboard_form() {
# Ctrl+C is the hidden entry into deferred provisioning, offered on every # The Ctrl+C hint sits directly under the header (no blank line between),
# visit: the user step unwinds back here and nothing is on disk yet. Callers # matching the disk-overwrite screen's unencrypted offer. Ctrl+C on the first
# re-check defer_provisioning after this returns. # screen is the hidden entry into deferred provisioning; $1 is "true" only for that first
local status # invocation, so the review-loop re-edit can't flip a user install into it.
local allow_defer_provisioning="${1:-false}"
while true; do clear_logo
clear_logo echo
echo say "Let's setup your machine..."
say "Let's setup your machine..." if $allow_defer_provisioning; then
say --foreground 8 "Press Ctrl+C to prepare this machine for another owner." say --foreground 8 "Press Ctrl+C to prepare this machine for another owner."
echo fi
echo
omarchy_prompt_keyboard && status=0 || status=$? keyboards=$'Azerbaijani|azerty
((status == 0)) && break Belarusian|by
Belgian|be-latin1
Bulgarian|bg-cp1251
Croatian|croat
Czech|cz
Danish|dk-latin1
Dutch|nl
English (UK)|uk
English (US)|us
English (US, Dvorak)|dvorak
English (US, Colemak)|colemak
Estonian|et
Finnish|fi
French|fr
French (Canada)|cf
French (Switzerland)|fr_CH
Georgian|ge
German|de
German (Switzerland)|de_CH-latin1
Greek|gr
Hebrew|il
Hungarian|hu
Icelandic|is-latin1
Irish|ie
Italian|it
Japanese|jp106
Kazakh|kazakh
Kyrgyz|kyrgyz
Lao|la-latin1
Latvian|lv
Lithuanian|lt
Macedonian|mk-utf
Norwegian|no-latin1
Polish|pl
Portuguese|pt-latin1
Portuguese (Brazil)|br-abnt2
Romanian|ro
Russian|ru
Serbian|sr-latin
Slovak|sk-qwertz
Slovenian|slovene
Spanish|es
Spanish (Latin American)|la-latin1
Swedish|sv-latin1
Tajik|tj_alt-UTF8
Turkish|trq
Ukrainian|ua'
choice=$(printf '%s\n' "$keyboards" | cut -d'|' -f1 | gum choose --height 10 --selected "English (US)" --header "Select keyboard layout")
local status=$?
# Esc means "back", and nothing precedes the first screen, so re-ask. if $allow_defer_provisioning && (( status == 130 )); then
((status == OMARCHY_FORM_BACK)) && continue if confirm_prepare_for_another_owner; then
defer_provisioning=true
if ((status == OMARCHY_FORM_SIGNAL)); then return 0
if confirm_prepare_for_another_owner; then
defer_provisioning=true
return 0
fi
continue # declined: back to the keyboard picker
fi fi
abort keyboard_form true # declined: back to the keyboard picker
done return
fi
(( status == 0 )) || abort
keyboard=$(printf '%s\n' "$keyboards" | awk -F'|' -v c="$choice" '$1==c{print $2; exit}')
# Only attempt to load keyboard layout if we're on a real console # Only attempt to load keyboard layout if we're on a real console
# loadkeys only works on Linux virtual consoles (tty), not in terminal emulators # loadkeys only works on Linux virtual consoles (tty), not in terminal emulators
@@ -248,37 +275,66 @@ confirm_prepare_for_another_owner() {
user_form() { user_form() {
step "Let's setup your user account..." step "Let's setup your user account..."
# Each prompt reports 0 (set), OMARCHY_FORM_BACK (Esc), or OMARCHY_FORM_SIGNAL while true; do
# (Ctrl+C). Both non-zero cases unwind to user_step, which decides what they username=$(gum input --placeholder "Alphanumeric without spaces (like dhh)" --prompt.foreground="#845DF9" --prompt "Username> ") || abort
# mean here; the form itself never aborts.
omarchy_prompt_username || return $?
omarchy_prompt_password || return $?
# Hash the password with SHA-512 crypt ($6$), which is what openssl passwd offers if [[ "$username" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]]; then
password_hash=$(printf '%s' "$password" | openssl passwd -6 -stdin) if [[ "$username" =~ ^(root|bin|daemon|mail|ftp|http|nobody|dbus|systemd-coredump|systemd-network|systemd-oom|systemd-journal-remote|systemd-resolve|systemd-timesync|tss|uuidd|alpm|git|avahi|cups|lp|_talkd|polkitd|rtkit|qemu|brltty|gluster|rpc|libvirt-qemu|pcscd|nvidia-persistenced|sddm)$ ]] ; then
notice "Username is reserved for system" 1
omarchy_prompt_identity || return $? else
omarchy_prompt_hostname || return $? break
omarchy_prompt_timezone || return $? fi
} else
notice "Username must be alphanumeric with no spaces" 1
# Returns 0 on confirmed account details, or as soon as the keyboard screen fi
# arms deferred provisioning. done
user_step() {
local status
while true; do while true; do
user_form && status=0 || status=$? password=$(gum input --placeholder "Used for user + root, and disk encryption when enabled" --prompt.foreground="#845DF9" --password --prompt "Password> ") || abort
if ((status != 0)); then password_confirmation=$(gum input --placeholder "Must match the password you just typed" --prompt.foreground="#845DF9" --password --prompt "Confirm> ") || abort
# Esc unwinds to the start of the form. Ctrl+C has no side channel in the
# user step (only the keyboard and disk screens give it a meaning), so it
# keeps its long-standing behaviour of ending the install.
((status == OMARCHY_FORM_BACK)) || abort
keyboard_form
$defer_provisioning && return 0
continue
fi
if [[ -n "$password" && "$password" == "$password_confirmation" ]]; then
break
elif [[ -z "$password" ]]; then
notice "Your password can't be blank!" 1
else
notice "Passwords didn't match!" 1
fi
done
# Hash the password using yescrypt
password_hash=$(printf '%s' "$password" | openssl passwd -6 -stdin)
full_name=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Full name> ")
email_address=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Email address> ")
while true; do
hostname=$(gum input --placeholder "Letters, digits, and dashes (or return for 'omarchy')" --prompt.foreground="#845DF9" --prompt "Hostname> ")
if [[ "$hostname" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$ ]]; then
break
elif [[ -z $hostname ]]; then
hostname="omarchy"
break
else
notice "Hostname must be 1-63 letters, digits, or dashes, and cannot start or end with a dash" 1
fi
done
# Pick timezone
geo_guessed_timezone=$(tzupdate -p 2>/dev/null)
if [[ -n $geo_guessed_timezone ]]; then
timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$geo_guessed_timezone" --header "Timezone") || abort
else
timezone=$(timedatectl list-timezones | gum filter --height 10 --header "Timezone") || abort
fi
}
user_step() {
user_form
while true; do
# Add manual padding since gum table -p doesn't respect padding # Add manual padding since gum table -p doesn't respect padding
echo -e "Field,Value echo -e "Field,Value
Username,$username Username,$username
@@ -295,7 +351,7 @@ Keyboard,$keyboard" |
break break
else else
keyboard_form keyboard_form
$defer_provisioning && return 0 user_form
fi fi
done done
} }
@@ -513,9 +569,8 @@ run_partition_decide() {
bl_part=$(detect_bitlocker || true) bl_part=$(detect_bitlocker || true)
if [[ -n "$bl_part" ]]; then if [[ -n "$bl_part" ]]; then
say --foreground 1 "BitLocker signature detected on $bl_part." say --foreground 1 "BitLocker signature detected on $bl_part."
say "Turn BitLocker off in Windows and wait for the drive to finish decrypting, then try again." say "Suspend or disable BitLocker in Windows before installing alongside it."
say "Suspending BitLocker is not enough — the drive stays encrypted." abort "Aborted: BitLocker is active on this disk."
abort "Aborted: BitLocker is enabled on this disk."
fi fi
# Omarchy always creates its own dedicated ESP in free space and never # Omarchy always creates its own dedicated ESP in free space and never
@@ -938,7 +993,7 @@ confirm_disk_overwrite() {
# Decide what and how to install: sets install_target (full_disk|free_space), # Decide what and how to install: sets install_target (full_disk|free_space),
# install_target and encrypt_installation. Nothing is written to disk yet — # install_target and encrypt_installation. Nothing is written to disk yet —
# the user step runs between this and the execution below. (deferred provisioning never # the user step runs between this and the execution below. (deferred provisioning never
# reaches here; it's armed by Ctrl+C on the keyboard screen and goes straight to # reaches here; it's armed by Ctrl+C on the first screen and goes straight to
# disk selection.) # disk selection.)
select_installation() { select_installation() {
local full_disk_only local full_disk_only
@@ -992,14 +1047,7 @@ wait_for_stable_terminal
greeter greeter
keyboard_form keyboard_form true
# Normal install: keyboard (loaded above, so the LUKS passphrase is typed under
# the right layout) → user → disk → install mode. The user step can unwind to
# the keyboard screen and arm deferral there, hence the second check.
if ! $defer_provisioning; then
user_step
fi
if $defer_provisioning; then if $defer_provisioning; then
# Deferred provisioning: no user setup, and the keyboard is deferred to the machine's first # Deferred provisioning: no user setup, and the keyboard is deferred to the machine's first
@@ -1025,6 +1073,10 @@ if $defer_provisioning; then
done done
install_target="full_disk" install_target="full_disk"
else else
# Normal install: keyboard (done above) → user → disk → install mode. The
# keyboard chosen above is already loaded, so the LUKS passphrase entered in
# the user step is typed under the right layout.
user_step
disk_form disk_form
select_installation select_installation
fi fi
@@ -123,16 +123,7 @@ repeat() {
logo_width() { logo_width() {
if [[ -f $LOGO_PATH ]]; then if [[ -f $LOGO_PATH ]]; then
# The logo is drawn in box-drawing glyphs, three bytes each in UTF-8, so awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH"
# 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 else
printf '81\n' printf '81\n'
fi fi
@@ -151,18 +142,8 @@ LOGO_HEIGHT="$(logo_height)"
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81 (( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1 (( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
CONTENT_WIDTH="$LOGO_WIDTH" CONTENT_WIDTH="$LOGO_WIDTH"
# Rows the progress screen occupies: logo, blank, title, blank, bar, blank, tip.
PROGRESS_HEIGHT=$((LOGO_HEIGHT + 6))
TOP_ROW=1
DYNAMIC_ROW=$((LOGO_HEIGHT + 3)) DYNAMIC_ROW=$((LOGO_HEIGHT + 3))
LAST_SIZE=""
LAST_PM=10 LAST_PM=10
# The finish screen mirrors the greeter's frame — logo, blank, one line, gap,
# one button — so the last thing the installer shows matches the first. It runs
# one row taller: `gum confirm` always paints prompt/blank/button and clears all
# three, so the button cannot sit closer than two rows under a title we drew.
FINISH_HEIGHT=$((LOGO_HEIGHT + 5))
FINISH_TOP_ROW=2
# ── Install progress ───────────────────────────────────────────────────────── # ── Install progress ─────────────────────────────────────────────────────────
# #
@@ -408,40 +389,15 @@ current_tip() {
printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}" printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}"
} }
# Vertically center a block of the given height, echoing the row it starts on.
# Both screens here center the way the configurator's greeter centers its
# welcome, so the install opens, runs, and closes on the same composition
# rather than stacking each frame from row one.
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() {
LAST_SIZE="$(term_size)"
TOP_ROW="$(centered_top_row "$PROGRESS_HEIGHT")"
DYNAMIC_ROW=$(( TOP_ROW + LOGO_HEIGHT + 1 ))
}
render_static() { render_static() {
measure_progress_layout printf '%s' "$CLEAR"
printf '%s%s%d;1H' "$CLEAR" "$CSI" "$TOP_ROW" blank_line
render_logo render_logo
blank_line blank_line
} }
render_dynamic() { render_dynamic() {
local pm local pm
# The console can still resize under us (the framebuffer settles late in VMs).
# Horizontal padding is recomputed every frame anyway, but the logo is drawn
# once, so redraw the static half at the new center rather than leave the
# centered layout stranded against a stale geometry.
[[ "$(term_size)" == "$LAST_SIZE" ]] || render_static
set_now set_now
install_progress install_progress
pm=$PROGRESS_PM pm=$PROGRESS_PM
@@ -496,45 +452,30 @@ render_finish() {
local duration effect_canvas_width local duration effect_canvas_width
duration="$(install_duration || true)" duration="$(install_duration || true)"
duration="${duration:-Complete}" duration="${duration:-Complete}"
# ttfx centers its text two columns right of plain centering, so hand it a
# canvas two columns narrower and the animated logo lands exactly on the
# static one left_padding already drew. Otherwise the logo jumps a column
# sideways when the effect starts and again when it ends.
effect_canvas_width="$(term_cols)" effect_canvas_width="$(term_cols)"
(( effect_canvas_width > 2 )) && effect_canvas_width=$((effect_canvas_width - 2)) (( effect_canvas_width > 1 )) && effect_canvas_width=$((effect_canvas_width - 1))
# 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")"
{ {
printf '%s%s%s%d;1H' "$SHOW_CURSOR" "$CLEAR" "$CSI" "$FINISH_TOP_ROW" printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
blank_line
render_logo render_logo
blank_line blank_line
center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH" center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH"
blank_line blank_line
} >"$TTY_PATH" } >"$TTY_PATH"
if [[ -f $LOGO_PATH ]]; then if command -v tte >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 && [[ -f $LOGO_PATH ]]; then
# Reuse mode restores a saved cursor before moving up over its input. Save # TTE reuse mode restores a saved cursor before moving up over its input.
# this anchor so the effect redraws the same logo rows shown above. # Save this anchor so the effect redraws the same logo rows shown above.
printf '%s%d;1H\0337' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT))" >"$TTY_PATH" printf '%s%d;1H\0337' "$CSI" "$((LOGO_HEIGHT + 2))" >"$TTY_PATH"
# Laseretch runs ~4s at 260fps, so the 8s cap is a stuck-process guard rather timeout 8s tte -i "$LOGO_PATH" \
# 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, and it settles on indexed 2 — the green gum already
# drew the logo in above, so the send-off ends on the frame it started from
# instead of recoloring it. The laser and sparks keep their own hot palette.
timeout 8s ttfx -i "$LOGO_PATH" \
--canvas-width "$effect_canvas_width" \ --canvas-width "$effect_canvas_width" \
--anchor-text c \ --anchor-text c \
--frame-rate 260 \ --frame-rate 260 \
--reuse-canvas \ --reuse-canvas \
--xterm-colors \
laseretch \ laseretch \
--final-gradient-stops 2 \ >"$TTY_PATH" 2>/dev/null || true
>"$TTY_PATH" 2>/dev/null || dashboard_log "logo effect skipped (ttfx exited $?)" printf '%s%d;1H' "$CSI" "$((LOGO_HEIGHT + 8))" >"$TTY_PATH"
printf '%s%d;1H' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH"
fi fi
} }
@@ -545,10 +486,8 @@ reboot_prompt() {
# applies, so setting both stops on the finish screen without a keyboard. # applies, so setting both stops on the finish screen without a keyboard.
interactive || return 0 interactive || return 0
# gum paints its prompt row at the cursor and the button two rows under it, so # Position the cursor one blank line below the title so the button sits right there.
# anchor on the row right below the title to put the button as close to it as printf '%s%d;1H%s' "$CSI" "$((LOGO_HEIGHT + 5))" "$CLEAR_LINE" >"$TTY_PATH"
# 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"
prompt_pad=$(( ( $(term_cols) - 18 ) / 2 )) prompt_pad=$(( ( $(term_cols) - 18 ) / 2 ))
(( prompt_pad < 0 )) && prompt_pad=0 (( prompt_pad < 0 )) && prompt_pad=0
@@ -18,9 +18,8 @@ from pathlib import Path
def configure_keyboard(target: Path, language: str) -> bool: def configure_keyboard(target: Path, language: str) -> bool:
"""Write the console keymap into a mounted target without booting it. """Write the console keymap into a mounted target without booting it.
Returns False for layouts localectl doesn't know, matching archinstall: Returns False for layouts localectl doesn't know (the configurator offers
warn and keep the default. Every layout the configurator offers is known; two, ba and khmer), matching archinstall: warn and keep the default.
the guard is for the kb_layout an autoinstall drive can name freely.
""" """
if not language.strip(): if not language.strip():
return True return True
@@ -11,7 +11,7 @@ Phase ordering (full-disk and protected/pre-mounted):
base install, early Omarchy packages, Limine setup, base install, early Omarchy packages, Limine setup,
useradd, runtime Omarchy packages, fstab useradd, runtime Omarchy packages, fstab
configure_hibernation → root-owned swap/resume drop-ins configure_hibernation → root-owned swap/resume drop-ins
run_system_finalizer → arch-chroot root omarchy-apply-system, including Snapper run_system_finalizer → arch-chroot root omarchy-setup-system, including Snapper
finalize_limine_boot → final Limine config/UKI build after hardware drop-ins finalize_limine_boot → final Limine config/UKI build after hardware drop-ins
run_chroot_finalizer → arch-chroot -u user omarchy-provision-user run_chroot_finalizer → arch-chroot -u user omarchy-provision-user
configure_login → sddm state + encrypted-install autologin configure_login → sddm state + encrypted-install autologin
@@ -598,7 +598,7 @@ DEFERRED_BOOT_HOOKS = (
# limine-entry-tool's 90-mkinitcpio-install.hook triggers on usr/lib/firmware/*, # limine-entry-tool's 90-mkinitcpio-install.hook triggers on usr/lib/firmware/*,
# usr/src/*/dkms.conf and usr/lib/modules/*/pkgbase, and anything but a # usr/src/*/dkms.conf and usr/lib/modules/*/pkgbase, and anything but a
# usr/lib/modules path makes it rebuild the initramfs and UKI for EVERY # usr/lib/modules path makes it rebuild the initramfs and UKI for EVERY
# installed kernel. omarchy-apply-system's hardware scripts routinely install # installed kernel. omarchy-setup-system's hardware scripts routinely install
# such packages (sof-firmware on Intel audio, nvidia-open-dkms, linux-ptl on # such packages (sof-firmware on Intel audio, nvidia-open-dkms, linux-ptl on
# Panther Lake, linux-t2 on Macs), so the phase can pay for several full UKI # Panther Lake, linux-t2 on Macs), so the phase can pay for several full UKI
# builds. finalize_limine_boot runs limine-update right after, which pipes # builds. finalize_limine_boot runs limine-update right after, which pipes
@@ -1005,7 +1005,7 @@ def _debug_run(ctx: InstallContext, cmd: list[str]) -> None:
# 1. point the target at the offline pacman.conf # 1. point the target at the offline pacman.conf
# 2. bind-mount the offline mirror + /opt/packages into /mnt for target pacman # 2. bind-mount the offline mirror + /opt/packages into /mnt for target pacman
# and bundled language runtimes # and bundled language runtimes
# 3. arch-chroot as root → omarchy-apply-system --first-install # 3. arch-chroot as root → omarchy-setup-system --first-install
# 4. arch-chroot as user → omarchy-provision-user --first-install # 4. arch-chroot as user → omarchy-provision-user --first-install
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@@ -1137,9 +1137,9 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
def run_system_finalizer(ctx: InstallContext) -> None: def run_system_finalizer(ctx: InstallContext) -> None:
if ctx.defer_provisioning: if ctx.defer_provisioning:
cmd = ["/usr/bin/omarchy-apply-system", "--defer-provisioning", "--first-install"] cmd = ["/usr/bin/omarchy-setup-system", "--defer-provisioning", "--first-install"]
else: else:
cmd = ["/usr/bin/omarchy-apply-system", "--install-user", ctx.username, "--first-install"] cmd = ["/usr/bin/omarchy-setup-system", "--install-user", ctx.username, "--first-install"]
_mask_mkinitcpio_pacman_hooks(ctx, ctx.target, TARGET_DEFERRED_BOOT_HOOKS) _mask_mkinitcpio_pacman_hooks(ctx, ctx.target, TARGET_DEFERRED_BOOT_HOOKS)
try: try:
+14 -30
View File
@@ -14,30 +14,14 @@ KEYBOARD = importlib.util.module_from_spec(SPEC)
assert SPEC.loader assert SPEC.loader
SPEC.loader.exec_module(KEYBOARD) SPEC.loader.exec_module(KEYBOARD)
# The layout list lives in the Omarchy runtime now, shared verbatim with the CONFIGURATOR = (ROOT / "configs/airootfs/root/configurator").read_text()
# first-boot owner setup; build-iso.sh vendors it onto the ISO. Read it from KEYBOARD_BLOCK = re.search(r"keyboards=\$'(.*?)'\n choice=", CONFIGURATOR, re.DOTALL)
# wherever this checkout can see a runtime, and skip the coverage test rather assert KEYBOARD_BLOCK
# than fail when none is around (a bare CI checkout of just this repo). SUPPORTED_KEYMAPS = [
SETUP_FORM_CANDIDATES = ( line.split("|", 1)[1]
Path("/omarchy-source/install/provisioning/setup-form.sh"), for line in KEYBOARD_BLOCK.group(1).splitlines()
ROOT.parent / "omarchy/install/provisioning/setup-form.sh", ]
Path("/usr/share/omarchy/install/provisioning/setup-form.sh"), UNSUPPORTED_KEYMAPS = {"ba", "khmer"}
)
def supported_keymaps():
for candidate in SETUP_FORM_CANDIDATES:
if not candidate.is_file():
continue
block = re.search(
r"OMARCHY_KEYBOARD_LAYOUTS=\$'(.*?)'\n", candidate.read_text(), re.DOTALL
)
assert block, f"no layout list in {candidate}"
return [line.split("|", 1)[1] for line in block.group(1).splitlines()]
return None
SUPPORTED_KEYMAPS = supported_keymaps()
class KeyboardConfigurationTest(unittest.TestCase): class KeyboardConfigurationTest(unittest.TestCase):
@@ -61,15 +45,15 @@ class KeyboardConfigurationTest(unittest.TestCase):
self.assertEqual(lines.count("FONT=default8x16"), 1) self.assertEqual(lines.count("FONT=default8x16"), 1)
def test_all_configurator_keymaps_are_known_to_localectl(self): def test_all_configurator_keymaps_are_known_to_localectl(self):
if SUPPORTED_KEYMAPS is None:
self.skipTest("no Omarchy runtime checkout to read the layout list from")
for keymap in SUPPORTED_KEYMAPS: for keymap in SUPPORTED_KEYMAPS:
with self.subTest(keymap=keymap), tempfile.TemporaryDirectory() as directory: with self.subTest(keymap=keymap), tempfile.TemporaryDirectory() as directory:
target = self.target(directory) target = self.target(directory)
self.assertTrue(KEYBOARD.configure_keyboard(target, keymap)) configured = KEYBOARD.configure_keyboard(target, keymap)
lines = (target / "etc/vconsole.conf").read_text().splitlines() self.assertEqual(configured, keymap not in UNSUPPORTED_KEYMAPS)
self.assertIn(f"KEYMAP={keymap}", lines) if configured:
self.assertEqual(lines.count("FONT=default8x16"), 1) lines = (target / "etc/vconsole.conf").read_text().splitlines()
self.assertIn(f"KEYMAP={keymap}", lines)
self.assertEqual(lines.count("FONT=default8x16"), 1)
def test_unknown_and_empty_keymaps_match_archinstall_behavior(self): def test_unknown_and_empty_keymaps_match_archinstall_behavior(self):
for keymap, expected in (("definitely-not-a-keymap", False), ("", True)): for keymap, expected in (("definitely-not-a-keymap", False), ("", True)):