Merge quattro and route refreshes through omarchy-update-pacman
Upstream now runs every Omarchy-owned pacman transaction through the hidden omarchy-update-pacman helper so a mid-transaction systemd reexec cannot kill it. Keep the deferred pre-refresh-pacman hook and the command-scoped sudo wrapper, and call the helper from the refresh and channel commands; the wrapper still applies to the helper's own sudo. The sudo boundary fixture copies the helper into its root and runs a systemd-run stand-in that execs the wrapped pacman step in place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
4bd593f4ed
105 files changed
+2541
-361
No files matched your search
@@ -1,2 +0,0 @@
|
||||
# Merges to protected branches need sign-off from an org owner.
|
||||
* @dhh @ryanrhughes
|
||||
@@ -70,10 +70,6 @@ magick -background white -fill black -font default/fonts/omarchy/omarchy.ttf \
|
||||
-pointsize 110 label:@/tmp/row.txt /tmp/font-row.png
|
||||
```
|
||||
|
||||
Then confirm it in the running menu per
|
||||
[`visual-verification.md`](visual-verification.md). Fontconfig prefers the
|
||||
packaged font over a copy in `~/.local/share/fonts` for the same family, so a
|
||||
preview needs either the real file replaced or a `<rejectfont>` rule in
|
||||
`~/.config/fontconfig/conf.d/` pointing fontconfig away from the packaged one.
|
||||
Restart the shell afterwards — Qt reads the font database at startup, so
|
||||
`omarchy menu refresh` alone will not pick up a changed font.
|
||||
Then confirm it in the running menu per [`visual-verification.md`](visual-verification.md). Avoid leaving two fonts with the `omarchy` family registered: Qt can use an old copy in `~/.local/share/fonts` even when `fc-match omarchy` reports the packaged font. For a preview, either replace the packaged file in the disposable VM or temporarily exclude it with a `<rejectfont>` rule in `~/.config/fontconfig/conf.d/` before loading the candidate.
|
||||
|
||||
Refresh the font cache and restart the shell afterwards — Qt reads the font database at startup, so `omarchy menu refresh` alone will not pick up a changed font. Remove temporary fonts and rules after verification.
|
||||
@@ -13,6 +13,7 @@ commands and reusable setup leaves:
|
||||
- use `$OMARCHY_INSTALL` and `$OMARCHY_PATH` instead of hard-coded Omarchy paths.
|
||||
- keep root-scoped hardware setup under `install/hardware/` and orchestrate it through `install/hardware/all.sh`.
|
||||
- keep every per-user setup leaf under `install/user/` (including `install/user/hardware/` and `install/user/first-run/`) so it is clear what must run for each user.
|
||||
- The base install supplies matching kernel headers before hardware setup. DKMS installers should install their driver packages and assume the headers exist.
|
||||
- prefer helper commands for package and command checks where available.
|
||||
|
||||
Raw `command -v`, `pacman`, and `pacman-key` are acceptable in package-helper
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
# start.
|
||||
#
|
||||
# Env: OMARCHY_SCREENRECORD_DEBUG=true appends gpu-screen-recorder's stderr (and
|
||||
# the picker target it was launched with) to /tmp/omarchy-screenrecord.log so
|
||||
# users can attach a log when reporting capture failures.
|
||||
# the picker target it was launched with) to
|
||||
# $XDG_RUNTIME_DIR/omarchy-screenrecord.log so users can attach a log when
|
||||
# reporting capture failures.
|
||||
|
||||
[[ -f ~/.config/user-dirs.dirs ]] && source ~/.config/user-dirs.dirs
|
||||
OUTPUT_DIR="${OMARCHY_SCREENRECORD_DIR:-${XDG_VIDEOS_DIR:-$HOME/Videos}}"
|
||||
@@ -34,9 +35,18 @@ WEBCAM_SIZE="medium"
|
||||
RESOLUTION=""
|
||||
FULLSCREEN="false"
|
||||
STOP_RECORDING="false"
|
||||
RECORDING_FILE="/tmp/omarchy-screenrecord-filename"
|
||||
REGION_FILE="${XDG_RUNTIME_DIR:-/tmp}/omarchy-screenrecord-region"
|
||||
LOG_FILE=$([[ ${OMARCHY_SCREENRECORD_DEBUG:-false} == "true" ]] && echo "/tmp/omarchy-screenrecord.log" || echo "/dev/null")
|
||||
# Both of these live in the per-user runtime directory, which is 0700. What
|
||||
# RECORDING_FILE holds is read back on stop and used as a path -- ffmpeg writes
|
||||
# beside it, `mv` replaces it, and `rm -f` deletes its preview -- so a name in
|
||||
# world-writable /tmp is a name any other local account can create first and
|
||||
# then point wherever it likes. The debug log carries the recorder's stderr and
|
||||
# the picked geometry, which is nobody else's business either. Fall back to
|
||||
# the state directory when there is no session runtime dir -- :-/tmp would put
|
||||
# both files back at the fixed names this comment describes.
|
||||
RUNTIME_DIR="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}"
|
||||
RECORDING_FILE="$RUNTIME_DIR/omarchy-screenrecord-filename"
|
||||
REGION_FILE="$RUNTIME_DIR/omarchy-screenrecord-region"
|
||||
LOG_FILE=$([[ ${OMARCHY_SCREENRECORD_DEBUG:-false} == "true" ]] && echo "$RUNTIME_DIR/omarchy-screenrecord.log" || echo "/dev/null")
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
@@ -144,6 +154,11 @@ select_capture_target() {
|
||||
}
|
||||
|
||||
start_screenrecording() {
|
||||
mkdir -p "$RUNTIME_DIR" || return 1
|
||||
# XDG_STATE_HOME may be outside a private home; protect existing fallbacks too.
|
||||
if [[ -z ${XDG_RUNTIME_DIR:-} ]]; then
|
||||
chmod 700 "$RUNTIME_DIR" || return 1
|
||||
fi
|
||||
local capture_args=()
|
||||
local target
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
set -euo pipefail
|
||||
|
||||
readonly MARGIN=40
|
||||
readonly REGION_FILE="${XDG_RUNTIME_DIR:-/tmp}/omarchy-screenrecord-region"
|
||||
# Has to resolve to whatever omarchy-capture-screenrecording writes, fallback
|
||||
# included, or a recording without a session runtime dir anchors the camera to
|
||||
# the whole monitor instead of the region it recorded.
|
||||
readonly REGION_FILE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}/omarchy-screenrecord-region"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-capture-webcam-resize <smaller|larger|reset|small|medium|large>" >&2
|
||||
|
||||
@@ -47,7 +47,7 @@ validate_dev_checkout() {
|
||||
|
||||
link_dev_checkout() {
|
||||
local checkout="$1" required
|
||||
[[ -d $checkout/.git ]] || git clone https://github.com/basecamp/omarchy.git "$checkout"
|
||||
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
|
||||
|
||||
# Check the destination before changing /etc/omarchy.conf or sudo's path.
|
||||
# An existing checkout is not pulled automatically and may predate this policy.
|
||||
@@ -94,6 +94,10 @@ case "$channel" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# A failure past this point leaves the channel switch half-applied, so say how
|
||||
# to pick it back up rather than dying silently under set -e.
|
||||
trap 'echo -e "\nThe channel switch did not complete. Review the error above, then rerun: omarchy-channel-set '"$channel"'" >&2' ERR
|
||||
|
||||
if [[ -z $dev_checkout && $OMARCHY_PATH != "/usr/share/omarchy" ]]; then
|
||||
leaving_dev=1
|
||||
fi
|
||||
@@ -107,7 +111,7 @@ fi
|
||||
|
||||
omarchy-refresh-pacman "$pacman_channel" defer-hook
|
||||
# --ask 4 accepts omarchy <-> omarchy-dev replacement prompts without file overwrites.
|
||||
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -S --needed --noconfirm --ask 4 "${packages[@]}"
|
||||
omarchy-update-pacman -S --needed --noconfirm --ask 4 "${packages[@]}"
|
||||
|
||||
if [[ -z $dev_checkout ]]; then
|
||||
omarchy-dev-unlink --no-reboot
|
||||
|
||||
@@ -86,6 +86,10 @@ if ! agent_install; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $agent == "claude" ]]; then
|
||||
omarchy-install-chromium-claude 2>/dev/null || true
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$agent_file")"
|
||||
printf '%s\n' "$agent" >"$agent_file"
|
||||
|
||||
|
||||
@@ -11,6 +11,18 @@ if (( $# < 1 )); then
|
||||
fi
|
||||
|
||||
HOOK=$1
|
||||
|
||||
# Hook names are fixed labels chosen by Omarchy code (post-update, theme-set,
|
||||
# font-set). The name becomes a filename under the hooks directory. A slash
|
||||
# would turn it into directory levels, and a bare `.` or `..` would point bash
|
||||
# at the directory itself or its parent. Refuse those rather than follow them.
|
||||
# Dots inside a name (a..b) are fine; once slashes are out, only the whole
|
||||
# name being `.` or `..` can leave the directory.
|
||||
if [[ -z $HOOK || $HOOK == */* || $HOOK == "." || $HOOK == ".." ]]; then
|
||||
echo "Invalid hook name: $HOOK" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
HOOK_PATH="$HOME/.config/omarchy/hooks/$1"
|
||||
HOOK_DIR="$HOOK_PATH.d"
|
||||
shift
|
||||
|
||||
@@ -15,6 +15,17 @@ fi
|
||||
|
||||
HOOK_TYPE=$1
|
||||
HOOK_FILE=$2
|
||||
|
||||
# Hook types are the same labels omarchy-hook runs (post-update, theme-set).
|
||||
# The type becomes a directory under the hooks directory. A slash would turn
|
||||
# it into directory levels. A bare `.` or `..` is a name the runner already
|
||||
# refuses, so installing under it would write a hook nothing can run. Refuse
|
||||
# those rather than mkdir/cp into them. Dots inside a name (a..b) are fine.
|
||||
if [[ -z $HOOK_TYPE || $HOOK_TYPE == */* || $HOOK_TYPE == "." || $HOOK_TYPE == ".." ]]; then
|
||||
echo "Invalid hook name: $HOOK_TYPE" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
HOOK_DIR="$HOME/.config/omarchy/hooks/$HOOK_TYPE.d"
|
||||
HOOK_NAME=$(basename "$HOOK_FILE")
|
||||
HOOK_PATH="$HOOK_DIR/$HOOK_NAME"
|
||||
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Detect whether an Elgato Cam Link 4K is plugged in.
|
||||
|
||||
grep -qsx "Cam Link 4K" /sys/bus/usb/devices/*/product
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install the Claude desktop app
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "Installing Claude..."
|
||||
omarchy-pkg-add claude-desktop
|
||||
|
||||
echo "Opening Claude..."
|
||||
setsid uwsm-app -- /usr/bin/claude-desktop >/dev/null 2>&1 &
|
||||
|
||||
echo ""
|
||||
echo "Claude has been installed."
|
||||
@@ -140,7 +140,10 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, sys.argv[1])
|
||||
from hermes_cli.main import _write_desktop_build_stamp
|
||||
if Path(sys.argv[1], "hermes_cli/main_desktop.py").is_file():
|
||||
from hermes_cli.main_desktop import _write_desktop_build_stamp
|
||||
else:
|
||||
from hermes_cli.main import _write_desktop_build_stamp
|
||||
|
||||
_write_desktop_build_stamp(Path(sys.argv[1]), source_mode=False)
|
||||
PY
|
||||
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install the Claude extension for Chromium-based browsers
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if (( EUID == 0 )); then
|
||||
export PATH=/usr/bin:/bin
|
||||
fi
|
||||
|
||||
EXTENSION_ID="fcoeoabgfenejglbffodgkkbkcdhcgfn"
|
||||
EXTENSION_JSON='{ "external_update_url": "https://clients2.google.com/service/update2/crx" }'
|
||||
PACKAGED_PATH=/usr/bin/omarchy-install-chromium-claude
|
||||
|
||||
# Brave and Brave Origin share Chromium's external extension directory.
|
||||
# Seed all supported browsers, including those installed after choosing Claude.
|
||||
EXTENSION_DIRS=(
|
||||
/usr/share/chromium/extensions
|
||||
/usr/share/google-chrome/extensions
|
||||
/usr/share/microsoft-edge/extensions
|
||||
)
|
||||
|
||||
installed=true
|
||||
for dir in "${EXTENSION_DIRS[@]}"; do
|
||||
if [[ ! -f $dir/$EXTENSION_ID.json ]] || [[ $(cat "$dir/$EXTENSION_ID.json") != "$EXTENSION_JSON" ]]; then
|
||||
installed=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $installed == "true" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if (( EUID == 0 )); then
|
||||
for dir in "${EXTENSION_DIRS[@]}"; do
|
||||
printf '%s\n' "$EXTENSION_JSON" | install -D -m 0644 /dev/stdin "$dir/$EXTENSION_ID.json"
|
||||
done
|
||||
elif [[ -t 0 ]]; then
|
||||
exec sudo "$PACKAGED_PATH"
|
||||
else
|
||||
exec pkexec "$PACKAGED_PATH"
|
||||
fi
|
||||
@@ -10,7 +10,7 @@ set -e
|
||||
echo "Installing Xbox controller Bluetooth support..."
|
||||
|
||||
# Install xpadneo to ensure controllers work out of the box
|
||||
omarchy-pkg-add linux-headers xpadneo-dkms
|
||||
omarchy-pkg-add xpadneo-dkms
|
||||
|
||||
# Prevent xpad/xpadneo driver conflict
|
||||
echo blacklist xpad | sudo tee /etc/modprobe.d/blacklist-xpad.conf >/dev/null
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
source omarchy-restart-gum
|
||||
|
||||
cmd="$*"
|
||||
presentation_script="omarchy-show-logo; $cmd; if (( \$? != 130 )); then omarchy-show-done; fi"
|
||||
# 130 is the user bailing with Ctrl-C; anything else gets the Done/Failed prompt.
|
||||
presentation_script="omarchy-show-logo; $cmd; code=\$?; if (( code != 130 )); then omarchy-show-done \$code; fi"
|
||||
|
||||
exec setsid uwsm-app -- xdg-terminal-exec --app-id=org.omarchy.terminal --title=Omarchy -e bash -c "$presentation_script"
|
||||
@@ -24,6 +24,7 @@ if [[ -n $pkg_names ]]; then
|
||||
source omarchy-sudo-keepalive
|
||||
|
||||
echo "$pkg_names" | sed 's/^/aur\//' | tr '\n' ' ' | xargs yay -S --noconfirm
|
||||
code=$?
|
||||
sudo updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots
|
||||
omarchy-show-done
|
||||
omarchy-show-done $code
|
||||
fi
|
||||
@@ -22,5 +22,5 @@ if [[ -n $pkg_names ]]; then
|
||||
|
||||
# Convert newline-separated selections to space-separated for pacman
|
||||
echo "$pkg_names" | tr '\n' ' ' | xargs sudo pacman -S --noconfirm
|
||||
omarchy-show-done
|
||||
omarchy-show-done $?
|
||||
fi
|
||||
@@ -20,5 +20,5 @@ pkg_names=$(yay -Qqe | fzf "${fzf_args[@]}")
|
||||
if [[ -n $pkg_names ]]; then
|
||||
# Convert newline-separated selections to space-separated for yay
|
||||
echo "$pkg_names" | tr '\n' ' ' | xargs sudo pacman -Rns --noconfirm
|
||||
omarchy-show-done
|
||||
omarchy-show-done $?
|
||||
fi
|
||||
@@ -36,7 +36,8 @@ if [[ $hook_mode != "run-deferred" ]]; then
|
||||
echo "Setting channel to $channel"
|
||||
sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf
|
||||
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist
|
||||
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syyuu --noconfirm
|
||||
# Reset all package DBs and then update
|
||||
omarchy-update-pacman -Syyuu --noconfirm
|
||||
fi
|
||||
|
||||
# Keep the historical hook name, but finish every privileged refresh operation
|
||||
|
||||
@@ -11,8 +11,8 @@ set -e
|
||||
omarchy-refresh-pacman
|
||||
|
||||
# Downgrade any packages to the stable setup
|
||||
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Suu --noconfirm
|
||||
omarchy-update-pacman -Suu --noconfirm
|
||||
|
||||
# Ensure all packages are installed
|
||||
mapfile -t packages < <(grep -v '^#' "$OMARCHY_PATH/install/omarchy-base.packages" | grep -v '^$')
|
||||
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm --needed "${packages[@]}"
|
||||
omarchy-update-pacman -Syu --noconfirm --needed "${packages[@]}"
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Remove the Claude desktop app along with its configuration and caches.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
|
||||
set -euo pipefail
|
||||
|
||||
# The app rewrites ~/.config/Claude for as long as it runs, so a removal that
|
||||
# leaves it running deletes the directory and watches it come straight back.
|
||||
pkill -x claude-desktop || true
|
||||
|
||||
omarchy-pkg-drop claude-desktop
|
||||
|
||||
# Not ~/.claude, ~/.claude.json, or ~/.cache/claude-cli-nodejs: those belong
|
||||
# to the Claude Code CLI, which ships in a different package that survives
|
||||
# this one. The desktop app keeps its state in its own Electron directories.
|
||||
rm -rf \
|
||||
"$HOME/.config/Claude" \
|
||||
"$HOME/.cache/Claude"
|
||||
|
||||
echo ""
|
||||
echo "Claude has been removed."
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Display a "Done!" message and wait for user to press any key.
|
||||
# omarchy:summary=Display a "Done!" or "Failed!" message and wait for user to press any key.
|
||||
# omarchy:args=[exit-code]
|
||||
|
||||
code="${1:-0}"
|
||||
|
||||
# The device node is there whether or not a terminal is behind it, so opening
|
||||
# it is the only test that means anything.
|
||||
@@ -13,6 +16,10 @@ while read -rsn 1 -t 0.1 _ </dev/tty; do :; done
|
||||
|
||||
# Prompt on the terminal rather than stdout, or a caller that redirects us
|
||||
# leaves the user waiting on a prompt they were never shown.
|
||||
printf '\n\033[32m● \033[0mDone! Press any key to close...' >/dev/tty
|
||||
if (( code == 0 )); then
|
||||
printf '\n\033[32m● \033[0mDone! Press any key to close...' >/dev/tty
|
||||
else
|
||||
printf '\n\033[31m● \033[0mFailed (exit code %d)! Press any key to close...' "$code" >/dev/tty
|
||||
fi
|
||||
read -rsn 1 </dev/tty
|
||||
echo >/dev/tty
|
||||
+14
-1
@@ -21,6 +21,19 @@ if [[ -z $STATE_NAME ]]; then
|
||||
fi
|
||||
|
||||
case "$COMMAND" in
|
||||
set) touch "$STATE_DIR/$STATE_NAME" ;;
|
||||
set)
|
||||
# State names are fixed labels (reboot-required, restart-*-required). The
|
||||
# name becomes a filename under the state directory. A slash would turn it
|
||||
# into directory levels, and a bare `.` or `..` would touch the directory
|
||||
# itself or its parent. Refuse those. Dots inside a name (a..b) are fine;
|
||||
# once slashes are out, only the whole name being `.` or `..` can leave the
|
||||
# directory. clear needs no such guard: find -name matches basenames only,
|
||||
# so a pattern can never walk out of the directory.
|
||||
if [[ $STATE_NAME == */* || $STATE_NAME == "." || $STATE_NAME == ".." ]]; then
|
||||
echo "Invalid state name: $STATE_NAME" >&2
|
||||
exit 2
|
||||
fi
|
||||
touch "$STATE_DIR/$STATE_NAME"
|
||||
;;
|
||||
clear) find "$STATE_DIR" -maxdepth 1 -type f -name "$STATE_NAME" -delete ;;
|
||||
esac
|
||||
@@ -297,19 +297,34 @@ rebuild_next_boot() {
|
||||
umount "$next$esp_mount"
|
||||
}
|
||||
|
||||
# Both the staged system and the retained baseline must lose the old hashes.
|
||||
scrub_factory_accounts() {
|
||||
local root="$1" user users
|
||||
|
||||
users=$(awk -F: '$3 >= 1000 && $3 < 60000 { print $1 }' "$root/etc/passwd") || return 1
|
||||
for user in $users; do
|
||||
userdel --root "$root" "$user" 2>>"$LOG_FILE" || return 1
|
||||
rm -rf "${root:?}/home/$user" || return 1
|
||||
done
|
||||
|
||||
# passwd --lock preserves the hash. Replace it, then remove the backups
|
||||
# that userdel and usermod leave behind.
|
||||
usermod --root "$root" --password '!' root >>"$LOG_FILE" 2>&1 || return 1
|
||||
rm -f "$root/etc/"{shadow-,gshadow-,passwd-,group-,subuid-,subgid-}
|
||||
}
|
||||
|
||||
# Remove the seller's account material and machine identity from the retained
|
||||
# @factory baseline so it can neither be mounted for recovery nor restore the
|
||||
# seller's account on a future reset. Idempotent (a scrubbed baseline has no
|
||||
# uid>=1000 accounts left to remove).
|
||||
sanitize_factory_baseline() {
|
||||
local factory="$1" user
|
||||
local factory="$1"
|
||||
btrfs property set -ts "$factory" ro false
|
||||
|
||||
for user in $(awk -F: '$3 >= 1000 && $3 < 60000 { print $1 }' "$factory/etc/passwd"); do
|
||||
userdel --root "$factory" "$user" 2>>"$LOG_FILE" || true
|
||||
rm -rf "${factory:?}/home/$user"
|
||||
done
|
||||
passwd --root "$factory" --lock root >>"$LOG_FILE" 2>&1 || true
|
||||
if ! scrub_factory_accounts "$factory"; then
|
||||
btrfs property set -ts "$factory" ro true
|
||||
fail "could not remove account credentials from the factory baseline (see $LOG_FILE)"
|
||||
fi
|
||||
rm -f "$factory"/etc/ssh/ssh_host_*
|
||||
rm -f "$factory"/etc/NetworkManager/system-connections/*
|
||||
rm -rf "$factory"/var/lib/NetworkManager/* "$factory/var/lib/tailscale" "$factory/var/lib/iwd"
|
||||
@@ -337,17 +352,13 @@ stage_full_reset() {
|
||||
rm -rf "$next"/var/lib/NetworkManager/* "$next/var/lib/tailscale" "$next/var/lib/iwd"
|
||||
rm -f "$next/var/lib/sddm/state.conf" "$next/etc/sddm.conf.d/autologin.conf"
|
||||
|
||||
# A factory snapshot from a normal (normal) install contains the original
|
||||
# A factory snapshot from a normal install contains the original
|
||||
# user account; first-boot setup must start from none. A leftover account
|
||||
# would keep its password hash and group memberships (including wheel), so
|
||||
# failure here has to abort the reset, not be shrugged off.
|
||||
local user
|
||||
for user in $(awk -F: '$3 >= 1000 && $3 < 60000 { print $1 }' "$next/etc/passwd"); do
|
||||
log "Removing user $user from the factory system"
|
||||
userdel --root "$next" "$user" 2>>"$LOG_FILE" ||
|
||||
fail "could not remove user $user from the factory system (see $LOG_FILE)"
|
||||
done
|
||||
passwd --root "$next" --lock root >>"$LOG_FILE" 2>&1 || true
|
||||
log "Removing account credentials from the factory system"
|
||||
scrub_factory_accounts "$next" ||
|
||||
fail "could not remove account credentials from the factory system (see $LOG_FILE)"
|
||||
|
||||
# @factory itself survives the wipe as the baseline for future resets. If it
|
||||
# came from a normal install it still holds the seller's account and
|
||||
|
||||
@@ -8,7 +8,7 @@ gum style --border normal --padding "1 2" \
|
||||
"• You cannot stop the update once you start!" \
|
||||
"• Make sure you're connected to power or have a full battery" \
|
||||
"" \
|
||||
"What's new: https://github.com/basecamp/omarchy/releases/latest"
|
||||
"What's new: https://github.com/omacom/omarchy/releases/latest"
|
||||
|
||||
echo
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
# omarchy:summary=Ensure the Omarchy and Arch keyring packages are installed and populated
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# omarchy-update runs this under set -e as a trusted pre-step, so a failed recv
|
||||
# or a broken keyring has to stop this script here, not surface later as
|
||||
# signature errors in the middle of the main transaction.
|
||||
set -euo pipefail
|
||||
|
||||
if omarchy-pkg-missing omarchy-keyring || ! sudo pacman-key --list-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 &>/dev/null; then
|
||||
sudo pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org
|
||||
sudo pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571
|
||||
@@ -19,4 +24,8 @@ fi
|
||||
# Always reinstall, as the keyring can be updated without a package version bump.
|
||||
echo -e "\e[32m\nUpdate Arch signing keys\e[0m"
|
||||
sudo pacman -Sy --noconfirm archlinux-keyring >/dev/null 2> >(grep -vE '^warning: archlinux-keyring-[^ ]+ is up to date -- reinstalling$' >&2)
|
||||
|
||||
# Say "correct" only once the key verifiably is: before the failure checks
|
||||
# above, a failed recv or reinstall still ended here with exit 0.
|
||||
sudo pacman-key --list-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 >/dev/null
|
||||
echo "Keys are correct"
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Run a pacman transaction for the Omarchy update flow, shielded from desktop session teardown.
|
||||
# omarchy:args=<pacman-args>
|
||||
# omarchy:hidden=true
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# Upgrading systemd runs its post_upgrade scriptlet mid-transaction, which
|
||||
# reexecs both the system manager and every user manager. A pacman running
|
||||
# inside a user-session scope can be SIGKILLed by that reexec, abandoning the
|
||||
# transaction halfway. Registering the transaction as a PID 1 scope keeps it
|
||||
# out of the user manager's cgroups entirely, and system scopes survive the
|
||||
# system manager's own reexec.
|
||||
scope=()
|
||||
if [[ -d /run/systemd/system && ! -L /run/systemd/system ]]; then
|
||||
scope=(systemd-run --scope --quiet --collect)
|
||||
fi
|
||||
|
||||
# LC_ALL passes through so callers that parse pacman's stderr can pin the locale.
|
||||
env_args=(OMARCHY_UPDATE_PACMAN=1)
|
||||
if [[ -n ${LC_ALL:-} ]]; then
|
||||
env_args+=(LC_ALL="$LC_ALL")
|
||||
fi
|
||||
|
||||
exec sudo env "${env_args[@]}" "${scope[@]}" pacman "$@"
|
||||
@@ -10,7 +10,7 @@ set -e
|
||||
# a stream, because an upgrade without --noconfirm prompts on stderr. The
|
||||
# handler has already said why, so no heading here either.
|
||||
if [[ ${OMARCHY_UPDATE_CONFLICT:-} == 1 && ${OMARCHY_UPDATE_INTERACTIVE:-} == 1 ]]; then
|
||||
exec sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --overwrite '/usr/share/omarchy/*'
|
||||
exec omarchy-update-pacman -Syu --overwrite '/usr/share/omarchy/*'
|
||||
fi
|
||||
|
||||
echo -e "\e[32m\nUpdate system packages\e[0m"
|
||||
@@ -23,7 +23,7 @@ trap 'rm -f "$errors"' EXIT
|
||||
#
|
||||
# Progress bars stay on stdout. Errors are on stderr, kept for the conflict
|
||||
# handler below; LC_ALL=C is what keeps them parseable in any locale.
|
||||
if sudo env LC_ALL=C OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm \
|
||||
if LC_ALL=C omarchy-update-pacman -Syu --noconfirm \
|
||||
--overwrite '/usr/share/omarchy/*' 2>"$errors"; then
|
||||
cat "$errors" >&2
|
||||
exit 0
|
||||
|
||||
@@ -965,7 +965,7 @@ populate_legacy_hypr_defaults() {
|
||||
tmp_dir=$(mktemp -d)
|
||||
archive_dir="$tmp_dir/archive"
|
||||
mkdir -p "$archive_dir"
|
||||
if curl -fsSL https://github.com/basecamp/omarchy/archive/refs/heads/master.tar.gz | tar -xz -C "$archive_dir" &&
|
||||
if curl -fsSL https://github.com/omacom/omarchy/archive/refs/heads/master.tar.gz | tar -xz -C "$archive_dir" &&
|
||||
[[ -d $archive_dir/omarchy-master/default/hypr ]]; then
|
||||
cp -a "$archive_dir/omarchy-master/default/hypr/." "$shim_hypr_dir/"
|
||||
fi
|
||||
@@ -1569,7 +1569,6 @@ refresh xournalpp/settings.xml 1c1a9efbf1b6dc7813bf3440fd5bf8409fc283e8d0192ca29
|
||||
# fastfetch/config.jsonc -> /etc/fastfetch/config.jsonc
|
||||
# fontconfig/fonts.conf -> /usr/share/fontconfig/conf.avail/50-omarchy.conf
|
||||
# mimeapps.list -> /usr/share/applications/mimeapps.list
|
||||
# omarchy.ttf -> /usr/share/fonts/omarchy/omarchy.ttf
|
||||
# systemd/user/bt-agent.service -> /usr/lib/systemd/user/bt-agent.service
|
||||
# systemd/user/omarchy-recover-internal-monitor.service -> /usr/lib/systemd/user/omarchy-recover-internal-monitor.service
|
||||
# systemd/user/omarchy-sleep-lock.service -> /usr/lib/systemd/user/omarchy-sleep-lock.service
|
||||
@@ -1602,7 +1601,6 @@ retire fontconfig/fonts.conf 6dec98b539388b95ecfdb3c7ab951001c712820eb0581002832
|
||||
retire fontconfig/fonts.conf 3545f6c5a8c1465df7a4e251b3c2047d4ba03654c86636794f64cb0d8ea8ef63
|
||||
retire fontconfig/fonts.conf 0f085b449f1cbe8eda3b59a6235bf2a3ddb6e982ab5d22fa642248916e63bafc
|
||||
retire mimeapps.list 3b574cef135b5deb7a8a0c7e17139037cf1fe155300e3809f60bed0e04120975
|
||||
retire omarchy.ttf e55e67119e82f56f92d90cbf54b7ccc1b2946b32c535a29370439d7ef5215966
|
||||
retire systemd/user/bt-agent.service 0406b577a1225dc2a9f86638d3c346eb3635168576f04050be50ebcc0be6be12
|
||||
retire systemd/user/omarchy-recover-internal-monitor.service b9b92cedc44cf3cb6216948629be55b53d16746e31896dc6469fd49ba55e82f4
|
||||
retire systemd/user/omarchy-recover-internal-monitor.service e1483079b9f2aefcd43b4722c75a31643e5f3bb5a2eada5f52f1d7d201e8c289
|
||||
@@ -2381,6 +2379,8 @@ cleanup_retired_services
|
||||
ensure_sleep_lock_service
|
||||
remove_retired_default_packages
|
||||
run_final_system_package_upgrade
|
||||
# This also retires the stock ~/.local/share/fonts/omarchy.ttf missed by the
|
||||
# former ~/.config retirement entry, through migration 1788848726.
|
||||
run_post_upgrade_migrations
|
||||
run_post_upgrade_update_steps
|
||||
refresh_current_theme_after_upgrade
|
||||
|
||||
+25
-2
@@ -1457,8 +1457,31 @@ To stop: omarchy-windows-vm stop"
|
||||
fi
|
||||
# If scale is less than 130%, don't set any scale (use default 100)
|
||||
|
||||
# Connect with RDP in fullscreen (auto-detects resolution)
|
||||
xfreerdp3 /u:"$WIN_USER" /p:"$WIN_PASS" /v:127.0.0.1:3389 -grab-keyboard /sound /microphone /clipboard /cert:ignore /title:"Windows VM - Omarchy" /dynamic-resolution /gfx:AVC444 /floatbar:sticky:off,default:visible,show:fullscreen $RDP_SCALE
|
||||
RDP_ARGS=(
|
||||
"/u:$WIN_USER"
|
||||
"/p:$WIN_PASS"
|
||||
/v:127.0.0.1:3389
|
||||
-grab-keyboard
|
||||
/sound
|
||||
/microphone
|
||||
/clipboard
|
||||
/cert:ignore
|
||||
"/title:Windows VM - Omarchy"
|
||||
/dynamic-resolution
|
||||
/gfx:AVC444
|
||||
/floatbar:sticky:off,default:visible,show:fullscreen
|
||||
)
|
||||
if [[ -n $RDP_SCALE ]]; then
|
||||
RDP_ARGS+=("$RDP_SCALE")
|
||||
fi
|
||||
|
||||
# Connect with RDP in fullscreen (auto-detects resolution). The arguments go
|
||||
# in over stdin rather than on the command line: /proc/<pid>/cmdline is
|
||||
# world-readable, so passing the VM password as /p:"$WIN_PASS" would show it
|
||||
# to every other user on the machine for as long as the session is open.
|
||||
# /args-from must stay the only argument here — FreeRDP rejects it outright
|
||||
# when it is combined with any other, so new flags belong in RDP_ARGS above.
|
||||
printf '%s\n' "${RDP_ARGS[@]}" | xfreerdp3 /args-from:stdin
|
||||
|
||||
# After RDP closes, stop the container unless --keep-alive was specified
|
||||
if [[ $KEEP_ALIVE = "false" ]]; then
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
## Never suspend the KEF LSX II LT USB sink.
|
||||
## The speaker's USB firmware stops answering control requests when the host
|
||||
## stops the stream after idle (usb_set_interface -110), and only a replug
|
||||
## recovers it. Keeping the stream open avoids the trigger.
|
||||
|
||||
monitor.alsa.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
node.name = "~alsa_output.usb-KEF_LSX_II_LT.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
session.suspend-timeout-seconds = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -33,7 +33,7 @@ Recordings land in the configured Videos directory (override with
|
||||
`omarchy capture webcam resize <smaller|larger|reset|small|medium|large>`.
|
||||
|
||||
If recording fails to start, rerun with `OMARCHY_SCREENRECORD_DEBUG=true` to
|
||||
collect a log at `/tmp/omarchy-screenrecord.log` worth attaching to a bug
|
||||
collect a log at `$XDG_RUNTIME_DIR/omarchy-screenrecord.log` (or `${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/omarchy-screenrecord.log` without a session runtime directory) worth attaching to a bug
|
||||
report.
|
||||
|
||||
## Text Capture (OCR)
|
||||
|
||||
@@ -38,7 +38,7 @@ drag-and-drop in the web form, so save the capture and hand the user the file
|
||||
path to attach (`gh` cannot upload media).
|
||||
|
||||
For screen-recording failures specifically, rerun with
|
||||
`OMARCHY_SCREENRECORD_DEBUG=true` and attach `/tmp/omarchy-screenrecord.log`.
|
||||
`OMARCHY_SCREENRECORD_DEBUG=true` and attach `$XDG_RUNTIME_DIR/omarchy-screenrecord.log` (or `${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/omarchy-screenrecord.log` without a session runtime directory).
|
||||
|
||||
File the issue with `gh` when available:
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ The private-use glyphs in `omarchy.ttf` are:
|
||||
- `U+E907` — Ollama, from <https://simpleicons.org/icons/ollama.svg>
|
||||
- `U+E908` — T3 Code, traced from the app icon in <https://aur.archlinux.org/cgit/aur.git/plain/t3code-icon.png?h=t3code-bin>, since upstream publishes no monochrome SVG
|
||||
- `U+E909` — Ori, from <https://openrouter.ai/brand/v2/openrouter-glyph-dark.svg>, OpenRouter's own mark: Ori ships no separate logo and its product page uses this one
|
||||
- `U+E90A` — Hermes, Font Awesome's staff-snake (CC BY 4.0) from <https://fontawesome.com/icons/staff-snake>, the mark Hermes serves as its favicon: their app icon is a portrait that reads as a smudge at menu size
|
||||
- `U+E90A` — Hermes, traced from the [official desktop app portrait](https://raw.githubusercontent.com/NousResearch/hermes-agent/2237be355906fbe6065ce1815711eee52b2d646e/apps/desktop/assets/icon.png), the same artwork shipped by `hermes-desktop`. The monochrome trace is kept in [hermes.svg](hermes.svg).
|
||||
- `U+E90B` — Perplexity, from <https://simpleicons.org/icons/perplexity.svg>
|
||||
- `U+E90C` — OpenClaw, traced from the lobster mascot the openclaw package ships as `dist/control-ui/favicon.svg`, since upstream publishes no monochrome SVG
|
||||
- `U+E90D` — Cursor, from <https://simpleicons.org/icons/cursor.svg>
|
||||
- `U+E90E` — Claude, from <https://simpleicons.org/icons/claude.svg>
|
||||
|
||||
The agent marks are monochrome so the menu can render them using the active
|
||||
theme's foreground and selection colors.
|
||||
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
Hermes portrait traced from apps/desktop/assets/icon.png at
|
||||
NousResearch/hermes-agent commit 2237be355906fbe6065ce1815711eee52b2d646e.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Nous Research
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="13.951734482614546 14.280839829258241 100.90844360300241 98.9137748534075"><path d="M34.2 15C26.5 16.6 19.6 22.2 16.3 29.5C14.1 34.6 13 84.1 15 93.5C16.6 101.1 21 106.8 28.4 110.5C34.1 113.3 34.4 113.3 35.4 111.5C36 110.5 38.1 107.8 40.1 105.6C43.4 101.9 46 97.9 46 96.6C46 96.3 44.8 96.7 43.4 97.5C40.2 99.1 36.9 98.2 35.5 95.2C35 94 33.7 92.7 32.7 92.4C30.3 91.6 27 88 27 86.1C27.1 84.8 27.3 84.8 28.1 86.3C30.6 90.6 37.7 90.4 40.1 85.9L41.3 83.7L38.5 85.8C35 88.6 31.6 88.6 29 85.8C26.9 83.5 26.4 78.5 28 76C28.7 74.9 29 75.2 29 77.6C29 83.6 34.2 85.1 36.5 79.7C39.1 73.5 40.3 57.9 38.4 56.1C37.5 55.2 37 55.1 37.4 55.7C38.7 58 36 57.1 33.5 54.4C31.6 52.3 31 50.6 31 47.1C31.1 43.2 33.7 34.5 33.9 37.5C34 38.1 35.7 38.4 37.8 38.3C43.4 37.9 44 37.7 44 37C44 36.7 45.6 36.6 47.5 36.8C49.8 37.1 51 36.8 51 36C51 35.1 51.4 35.1 52.3 35.7C52.9 36.3 55.1 36.8 57.1 36.8C59.2 36.9 61.4 37.1 62.1 37.2C63 37.4 63.5 36.7 63.4 35.4C63.4 33.8 63.9 33.4 65.7 33.8C67 34 68 33.8 68 33.2C68 32.6 68.3 31.4 68.6 30.6C69.4 28.5 65.3 29 64 31C63.5 31.8 62.8 32.1 62.4 31.7C62 31.3 61.2 31.8 60.5 32.7C59.6 33.9 57.4 34.4 51.4 34.5C47.1 34.7 43.4 34.9 43.3 35C42.5 35.8 40.2 35.4 38.4 34.3C37.3 33.6 35.9 33.3 35.4 33.7C34.9 34.1 36 32.8 37.9 30.8C42.1 26.2 51.7 21 56 21C60.5 21 65.6 23.7 69 27.9C72.2 31.7 73 34.1 70.5 32C69.2 30.9 69.1 31 69.6 32.4C69.9 33.3 70.8 34 71.6 34C72.3 34 73.2 35.6 73.6 37.5C73.9 39.4 74.6 41.3 75.1 41.6C75.6 41.9 75.9 41.6 75.8 40.9C75.6 40.2 76.4 39.4 77.4 39.2C78.9 38.9 79.1 39.2 78.6 40.6C78.1 42 78.2 42.2 79 41.4C79.8 40.6 79.8 39.5 79 37.5C77.7 34.1 78.2 33.5 81 35C82.8 36 83.1 35.9 82.5 34.9C81.9 34 82.6 33.8 85.3 34.3C88.5 34.8 89 34.6 89.8 32.2C90.7 29.5 90.8 29.6 93 34.5C94.2 37.2 96.7 46.2 98.6 54.4C100.4 62.5 102.4 70.8 103 72.9C104.5 78.3 104.2 87.7 102.4 91.1C100.1 95.6 101.4 95.7 104.1 91.3C105.8 88.7 106.5 86 106.5 82.7V77.9L107.8 81.1C110.1 86.6 108.6 90.7 101.9 97.2C98.7 100.5 96 102.6 96 102.1C96 101.5 94.6 99.5 92.9 97.5C89.5 93.6 90 92.7 93.7 96.2C95.6 98 96 98.1 95.4 96.7C95.1 95.7 93.4 94.2 91.8 93.4C90.1 92.5 89.1 92.3 89.4 92.9C89.9 93.6 89.1 93.8 87.1 93.4C80.7 92.1 74.1 97.7 71.4 106.8C70.6 109.3 70 111.7 70 112.2C70 112.6 76 113 83.3 113C98 113 102 111.8 107.6 106.2C114.3 99.6 114.5 98.5 114.8 67.1C115.2 29.3 113.9 24.1 101.8 17.5L96.5 14.5L67.5 14.3C51.6 14.2 36.6 14.5 34.2 15ZM80.5 20.7C82.7 21.9 85.4 23.8 86.4 24.9C88.8 27.5 89.4 30.4 87.5 29.6C86.7 29.3 85.8 29.9 85.5 31C84.5 34.3 77.5 33.5 76 29.9C73.8 24.7 67.6 20 62.7 20C59.5 20 58.9 19.7 60 18.9C62.5 17.2 76 18.4 80.5 20.7ZM53 43.8C53 44.5 53.5 46.1 54.1 47.2C56 50.8 50.3 51.9 46.2 48.7C45.3 48 45.1 48.1 45.5 49.2C45.9 50.3 45.3 51 43.6 51.4C40.3 52.2 40.3 53.6 43.5 54.4C46.6 55.2 46.6 56 43.9 59.4C41.1 63 41.7 69.1 45.5 75.6L48.2 80.2L53.6 79.6C58.7 79 58.9 79.1 60.7 82C63.3 86.5 64.4 85.2 65.2 76.4C65.5 72.1 66.3 66.1 66.9 63.1C69.2 51.6 68.5 50 60.9 50C57.5 50 56.8 49.6 55.1 46.2C54 44.2 53.1 43.1 53 43.8ZM59.8 52.6C62 54.1 62 54.3 60.4 56.1C58.2 58.6 57.6 58.5 54.5 55.4C50.4 51.3 54.7 49 59.8 52.6ZM46.7 68.7C47.3 68.3 48.6 68.7 49.6 69.5C51.4 70.8 51.3 70.9 48.5 70.7C45.5 70.5 44 69.4 44 67.5C44 66.9 44.4 67.1 44.9 67.9C45.4 68.6 46.2 69 46.7 68.7ZM57.6 55.6C57.2 56.6 57.5 57 58.2 56.8C59.7 56.3 60.5 54 59.2 54C58.7 54 57.9 54.7 57.6 55.6ZM77.6 53.9C77.3 54.4 78.1 54.7 79.5 54.7C80.9 54.7 81.7 54.4 81.4 53.9C81.1 53.4 80.3 53 79.5 53C78.7 53 77.9 53.4 77.6 53.9ZM42 56.9C42 57.5 42.4 58.2 43 58.5C43.6 58.8 43.7 58.4 43.4 57.6C42.7 55.8 42 55.5 42 56.9ZM72.4 74.4C71.4 77.6 71.5 77.8 73 77C74.2 76.4 74.6 75.3 74.3 73.5C73.6 70.4 73.7 70.3 72.4 74.4ZM98.1 76.8C98 80.3 97.5 84.1 96.9 85.3C95.9 87.4 96 87.4 97.9 85.1C100.1 82.6 100.6 76.3 99 72.5C98.4 71.1 98.1 72.4 98.1 76.8ZM72 80.5C71 81.7 71 82.4 71.8 83.2C73.3 84.7 76.1 82.4 75.3 80.4C74.6 78.6 73.6 78.6 72 80.5ZM62.8 87.9C62 88.9 60.7 89.9 60 90.2C59 90.5 59.1 91.4 60.4 93.8C62.3 97.4 63 97.6 66.8 95.5C72.5 92.4 72.5 92.4 68.9 89.1C64.9 85.5 64.9 85.5 62.8 87.9ZM67 93.2C67 93.6 66.3 93.6 65.3 93.3C64.2 92.9 63.9 93 64.4 93.8C64.8 94.5 64.7 95 64.3 95C63.8 95 62.9 94 62.1 92.9C60.9 90.9 61 90.8 62.8 91.2C64.4 91.6 64.7 91.4 64.3 90.1C64 88.9 64.2 89 65.4 90.4C66.3 91.5 67 92.7 67 93.2ZM42.2 91C41.5 92.1 39.9 93 38.4 93C37 93 36.1 93.4 36.5 94C38.1 96.5 44.8 92.9 43.8 90C43.7 89.5 42.9 89.9 42.2 91ZM74 94C67.7 95.9 61.6 100.3 56.9 106.1C51 113.5 51.1 113 55.8 113C59.2 112.9 59.8 112.5 63.9 106.4C66.3 102.9 68.4 100.1 68.6 100.2C68.7 100.4 68 103.2 66.9 106.4C65.9 109.7 65.2 112.5 65.5 112.8C66.9 114.3 68.9 111.4 70 106.2C70.9 101.9 72.2 99.1 74.5 96.6C78 92.8 77.9 92.7 74 94ZM50.4 104.5C47.8 107.1 47.3 112 49.6 112.7C50.5 113 55 104.6 55 102.6C55 101.2 52.7 102.2 50.4 104.5ZM51.5 109C50.6 110.8 49.6 112 49.4 111.7C49.1 111.5 49.7 109.9 50.7 108.2C53 104.2 53.6 104.9 51.5 109ZM39.5 111C38.5 112.1 38.2 113 38.8 113C39.4 113 40.4 112.1 41 111C41.6 109.9 41.9 109 41.7 109C41.5 109 40.5 109.9 39.5 111Z"/></svg>
|
||||
Binary file not shown.
@@ -1 +1,3 @@
|
||||
o.window("^(1[p|P]assword)$", { no_screen_share = true, tag = "+floating-window" })
|
||||
-- 1Password 8.12 renamed its app id from "1Password" to the reverse-DNS form,
|
||||
-- so match both to keep older installs floating too.
|
||||
o.window("^(1[pP]assword|com\\.onepassword\\.OnePassword)$", { no_screen_share = true, tag = "+floating-window" })
|
||||
@@ -1,2 +1,2 @@
|
||||
-- Disable mouse focus (see https://github.com/basecamp/omarchy/pull/5183#issuecomment-4189299971).
|
||||
-- Disable mouse focus (see https://github.com/omacom/omarchy/pull/5183#issuecomment-4189299971).
|
||||
o.window("^(jetbrains-.*)$", { no_follow_mouse = true })
|
||||
+130
-22
@@ -5,6 +5,14 @@
|
||||
-- How much of the usable screen the console covers, measured from the top.
|
||||
local share = 0.5
|
||||
|
||||
-- A console holding a single window is boxed into a centered panel this many
|
||||
-- times wider than it is tall, rather than stretched the width of the screen.
|
||||
-- A second app on the scratchpad gets the full width back: two windows splitting
|
||||
-- a half-width column is worse than the band this replaced.
|
||||
local box = 2
|
||||
|
||||
local SCRATCHPAD = "special:scratchpad"
|
||||
|
||||
-- Seed the console with the default agent the first time it opens, rather than
|
||||
-- at boot, so nothing is running until it is wanted. The exec rule has to pin
|
||||
-- the workspace itself: Hyprland only tags a spawn with the workspace it came
|
||||
@@ -23,22 +31,25 @@ hl.config({
|
||||
},
|
||||
})
|
||||
|
||||
-- The panel is always flush with the top and always centered, so two numbers
|
||||
-- describe it: the gap down each side and the gap underneath.
|
||||
--
|
||||
-- Refitting replaces the rule in place rather than stacking a new one, but it
|
||||
-- still schedules a monitor and window state refresh, and monitor.focused fires
|
||||
-- on every hop between screens. Most of those hops do not change the number, so
|
||||
-- on every hop between screens. Most of those hops do not change the gaps, so
|
||||
-- only write the rule when it actually moves.
|
||||
local covering = nil
|
||||
local beside, below = nil, nil
|
||||
|
||||
local function cover(bottom)
|
||||
if covering == bottom then
|
||||
return
|
||||
local function cover(side, bottom)
|
||||
if beside == side and below == bottom then
|
||||
return false
|
||||
end
|
||||
covering = bottom
|
||||
beside, below = side, bottom
|
||||
|
||||
hl.workspace_rule({
|
||||
workspace = "special:scratchpad",
|
||||
workspace = SCRATCHPAD,
|
||||
gaps_in = 0,
|
||||
gaps_out = { top = 0, right = 0, bottom = bottom, left = 0 },
|
||||
gaps_out = { top = 0, right = side, bottom = bottom, left = side },
|
||||
|
||||
-- Nothing to highlight in a console that is only ever focused when it is
|
||||
-- open, and the active border reads as a stray frame around a panel that
|
||||
@@ -47,39 +58,136 @@ local function cover(bottom)
|
||||
|
||||
on_created_empty = seed,
|
||||
})
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- One window reads as a console and gets the panel. A second app has turned the
|
||||
-- scratchpad into a workspace, and a workspace wants the whole width. Only tiled
|
||||
-- windows count: the gaps are what size the panel, and a floating window on top
|
||||
-- of the console is not laid out by them.
|
||||
local function alone()
|
||||
local tiled = 0
|
||||
for _, window in ipairs(hl.get_workspace_windows(SCRATCHPAD)) do
|
||||
if not window.floating then
|
||||
tiled = tiled + 1
|
||||
end
|
||||
end
|
||||
return tiled <= 1
|
||||
end
|
||||
|
||||
-- Sizing the console with a window rule would freeze it at whatever the screen
|
||||
-- measured when it first opened, because Hyprland resolves those expressions
|
||||
-- once, as the window maps. Rescaling the monitor afterwards would leave a
|
||||
-- console that is no longer half of anything. Gaps are re-applied by the layout
|
||||
-- instead, so the console is sized by the gap left underneath it and that gap
|
||||
-- is recomputed whenever the monitor layout changes.
|
||||
local function fit()
|
||||
local monitor = hl.get_active_monitor()
|
||||
|
||||
-- instead, so the console is sized by the area left around it, and that area is
|
||||
-- recomputed whenever the monitor it opens on changes.
|
||||
local function fit(monitor)
|
||||
-- A monitor handle whose output has gone away answers nil to every field, and
|
||||
-- layout changes are exactly when that happens, so this also covers reading
|
||||
-- height and reserved below.
|
||||
-- width, height and reserved below.
|
||||
if not monitor or not monitor.scale or monitor.scale <= 0 then
|
||||
return
|
||||
return false
|
||||
end
|
||||
|
||||
-- Width and height are the panel's own pixels, so a monitor turned on its
|
||||
-- side still reports them the way the panel is built. The odd transforms are
|
||||
-- the quarter turns, and those are the ones that swap the work area.
|
||||
local width, height = monitor.width, monitor.height
|
||||
if monitor.transform % 2 == 1 then
|
||||
width, height = height, width
|
||||
end
|
||||
|
||||
-- Monitor dimensions are in physical pixels; gaps are logical, so the scale
|
||||
-- has to come out before the reserved area (already logical) comes off.
|
||||
local reserved = monitor.reserved
|
||||
local usable = monitor.height / monitor.scale - reserved.top - reserved.bottom
|
||||
height = height / monitor.scale - reserved.top - reserved.bottom
|
||||
width = width / monitor.scale - reserved.left - reserved.right
|
||||
|
||||
cover(math.max(0, math.floor(usable * (1 - share))))
|
||||
local tall = math.floor(height * share)
|
||||
local wide = width
|
||||
if alone() then
|
||||
wide = math.min(width, tall * box)
|
||||
end
|
||||
|
||||
return cover(math.floor((width - wide) / 2), math.floor(height - tall))
|
||||
end
|
||||
|
||||
-- The console keeps the geometry of the output it is open on: a follow_mouse hop
|
||||
-- onto another screen must not resize a console that is already showing. While
|
||||
-- it is hidden there is nothing to size but the output that will show it next.
|
||||
local function console_monitor()
|
||||
local ws = hl.get_workspace(SCRATCHPAD)
|
||||
local mon = ws and ws.visible and ws.monitor
|
||||
|
||||
if mon and mon.scale and mon.scale > 0 then
|
||||
return mon
|
||||
end
|
||||
|
||||
return hl.get_active_monitor()
|
||||
end
|
||||
|
||||
local function refit(monitor)
|
||||
if fit(monitor or console_monitor()) then
|
||||
-- Land the new gaps in this pass rather than a frame later, so the console
|
||||
-- does not visibly resize itself once it has already dropped down.
|
||||
hl.exec_scheduled_prop_refresh_immediately()
|
||||
end
|
||||
end
|
||||
|
||||
-- Until a monitor can be read, cover the whole work area rather than leaving
|
||||
-- the console unruled, so it is never seeded without its placement.
|
||||
cover(0)
|
||||
fit()
|
||||
-- the console unruled, so it is never seeded without its placement. A reload
|
||||
-- runs this again with the console already on screen, so it starts from the
|
||||
-- output the console is on rather than whichever one the pointer is over.
|
||||
cover(0, 0)
|
||||
fit(console_monitor())
|
||||
|
||||
hl.on("monitor.layout_changed", fit)
|
||||
hl.on("monitor.focused", fit)
|
||||
hl.on("monitor.layout_changed", function()
|
||||
refit()
|
||||
end)
|
||||
|
||||
hl.on("monitor.focused", function()
|
||||
refit()
|
||||
end)
|
||||
|
||||
-- Special workspaces open on the monitor they are toggled on, not on whichever
|
||||
-- output last happened to be focused when the rule was written, so these two
|
||||
-- take the monitor they are handed rather than looking one up.
|
||||
hl.on("workspace.special_active", function(ws, mon)
|
||||
if ws and ws.name == SCRATCHPAD then
|
||||
refit(mon)
|
||||
end
|
||||
end)
|
||||
|
||||
hl.on("workspace.move_to_monitor", function(ws, mon)
|
||||
if ws and ws.name == SCRATCHPAD then
|
||||
refit(mon)
|
||||
end
|
||||
end)
|
||||
|
||||
-- The panel is only centered while the console holds one tiled window, so the
|
||||
-- count has to be rechecked as apps come and go: opened and closed, moved on or
|
||||
-- off (Super+Alt+S, Super+Shift+1), and floated or tiled (Super+T, Super+O).
|
||||
-- window.close is left out, since it still counts the window on its way out and
|
||||
-- window.destroy follows it anyway. Measured on Hyprland 0.56.2, a move is
|
||||
-- trailed by several window.update_rules, as is a float toggle, and the last of
|
||||
-- those always reads the settled count; the earlier ones refit to what the rule
|
||||
-- already is, which cover() skips.
|
||||
--
|
||||
-- Only while it is on screen, though. A hidden console is refitted on its way in
|
||||
-- by workspace.special_active, and every window opened anywhere on the desktop
|
||||
-- would otherwise rewrite the rule.
|
||||
local function recount()
|
||||
local ws = hl.get_workspace(SCRATCHPAD)
|
||||
if ws and ws.visible then
|
||||
refit()
|
||||
end
|
||||
end
|
||||
|
||||
hl.on("window.open", recount)
|
||||
hl.on("window.destroy", recount)
|
||||
hl.on("window.move_to_workspace", recount)
|
||||
hl.on("window.update_rules", recount)
|
||||
|
||||
-- The direction names the edge the offset is measured from, not where the
|
||||
-- workspace goes: "slide top" drops it down into view, and "slide bottom"
|
||||
|
||||
@@ -241,6 +241,7 @@
|
||||
"install.terminal.ghostty": {"icon":"","label":"Ghostty","disabled":"omarchy-pkg-present ghostty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal ghostty'"},
|
||||
"install.terminal.kitty": {"icon":"","label":"Kitty","disabled":"omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"},
|
||||
"install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","disabled":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"},
|
||||
"install.ai.claude": {"icon":"","iconFont":"omarchy","label":"Claude Desktop","disabled":"omarchy-pkg-present claude-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-claude"},
|
||||
"install.ai.dictation": {"icon":"","label":"Dictation","disabled":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
|
||||
"install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","disabled":"omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"},
|
||||
"install.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","disabled":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-hermes"},
|
||||
@@ -309,6 +310,7 @@
|
||||
"remove.service.dropbox": {"icon":"","label":"Dropbox","when":"omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-service-dropbox"},
|
||||
"remove.service.tailscale": {"icon":"","label":"Tailscale","when":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-service-tailscale"},
|
||||
"remove.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","when":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-chatgpt"},
|
||||
"remove.ai.claude": {"icon":"","iconFont":"omarchy","label":"Claude Desktop","when":"omarchy-pkg-present claude-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-claude"},
|
||||
"remove.ai.dictation": {"icon":"","label":"Dictation","when":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-remove"},
|
||||
"remove.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"omarchy-pkg-present grok-bot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-grok-bot"},
|
||||
"remove.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"omarchy-pkg-present lmstudio-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-lm-studio"},
|
||||
|
||||
@@ -5,8 +5,6 @@ Window.SetBackgroundBottomColor(0.101, 0.105, 0.149);
|
||||
|
||||
logo.image = Image("logo.png");
|
||||
logo.sprite = Sprite(logo.image);
|
||||
logo.sprite.SetX(Window.GetWidth() / 2 - logo.image.GetWidth() / 2);
|
||||
logo.sprite.SetY(Window.GetHeight() / 2 - logo.image.GetHeight() / 2);
|
||||
logo.sprite.SetOpacity(1);
|
||||
|
||||
# Use these to adjust the progress bar timing
|
||||
@@ -22,7 +20,18 @@ global.fake_progress_start_time = 0.0; # Track when fake progress started
|
||||
global.password_shown = 0; # Track if password dialog has been shown
|
||||
global.max_progress = 0.0; # Track the maximum progress reached to prevent backwards movement
|
||||
|
||||
# Window size the sprites were last laid out for
|
||||
global.layout_width = 0;
|
||||
global.layout_height = 0;
|
||||
|
||||
fun refresh_callback() {
|
||||
# Displays can show up after this script ran, e.g. a Thunderbolt monitor
|
||||
# whose DisplayPort tunnel comes up after plymouthd started. The window
|
||||
# grows to the largest display, so re-center everything when it changes.
|
||||
if (Window.GetWidth() != global.layout_width || Window.GetHeight() != global.layout_height) {
|
||||
layout();
|
||||
}
|
||||
|
||||
global.animation_frame++;
|
||||
|
||||
# Animate fake progress to limit over time with easing
|
||||
@@ -108,27 +117,23 @@ entry.image = Image("entry.png");
|
||||
bullet.image = Image("bullet.png");
|
||||
|
||||
entry.sprite = Sprite(entry.image);
|
||||
entry.x = Window.GetWidth() / 2 - entry.image.GetWidth() / 2;
|
||||
entry.y = logo.sprite.GetY() + logo.image.GetHeight() + 40;
|
||||
entry.sprite.SetPosition(entry.x, entry.y, 10001);
|
||||
entry.sprite.SetOpacity(0);
|
||||
|
||||
# Scale lock to be slightly shorter than entry field height
|
||||
# Original lock is 84x96, entry height determines scale
|
||||
lock_height = entry.image.GetHeight() * 0.8;
|
||||
lock_scale = lock_height / 96;
|
||||
lock_width = 84 * lock_scale;
|
||||
lock.height = entry.image.GetHeight() * 0.8;
|
||||
lock.width = 84 * (lock.height / 96);
|
||||
|
||||
scaled_lock = lock.image.Scale(lock_width, lock_height);
|
||||
lock.sprite = Sprite(scaled_lock);
|
||||
lock.x = entry.x - lock_width - 15;
|
||||
lock.y = entry.y + entry.image.GetHeight() / 2 - lock_height / 2;
|
||||
lock.sprite.SetPosition(lock.x, lock.y, 10001);
|
||||
lock.sprite = Sprite(lock.image.Scale(lock.width, lock.height));
|
||||
lock.sprite.SetOpacity(0);
|
||||
|
||||
# Bullet array
|
||||
bullet.sprites = [];
|
||||
|
||||
fun position_bullet(index) {
|
||||
bullet.sprites[index].SetPosition(entry.x + 20 + index * (7 + 5), entry.y + entry.image.GetHeight() / 2 - 3.5, 10002);
|
||||
}
|
||||
|
||||
fun display_normal_callback() {
|
||||
hide_password_dialog();
|
||||
|
||||
@@ -165,11 +170,8 @@ fun display_password_callback(prompt, bullets) {
|
||||
for (index = 0; index < bullets_to_show; index++) {
|
||||
if (!bullet.sprites[index]) {
|
||||
# Scale bullet image to 7x7 pixels
|
||||
scaled_bullet = bullet.image.Scale(7, 7);
|
||||
bullet.sprites[index] = Sprite(scaled_bullet);
|
||||
bullet.x = entry.x + 20 + index * (7 + 5);
|
||||
bullet.y = entry.y + entry.image.GetHeight() / 2 - 3.5;
|
||||
bullet.sprites[index].SetPosition(bullet.x, bullet.y, 10002);
|
||||
bullet.sprites[index] = Sprite(bullet.image.Scale(7, 7));
|
||||
position_bullet(index);
|
||||
}
|
||||
|
||||
bullet.sprites[index].SetOpacity(1);
|
||||
@@ -183,19 +185,11 @@ Plymouth.SetDisplayPasswordFunction(display_password_callback);
|
||||
|
||||
progress_box.image = Image("progress_box.png");
|
||||
progress_box.sprite = Sprite(progress_box.image);
|
||||
|
||||
progress_box.x = Window.GetWidth() / 2 - progress_box.image.GetWidth() / 2;
|
||||
progress_box.y = entry.y + entry.image.GetHeight() / 2 - progress_box.image.GetHeight() / 2;
|
||||
progress_box.sprite.SetPosition(progress_box.x, progress_box.y, 0);
|
||||
progress_box.sprite.SetOpacity(0);
|
||||
|
||||
progress_bar.original_image = Image("progress_bar.png");
|
||||
progress_bar.sprite = Sprite();
|
||||
progress_bar.image = progress_bar.original_image.Scale(1, progress_bar.original_image.GetHeight());
|
||||
|
||||
progress_bar.x = Window.GetWidth() / 2 - progress_bar.original_image.GetWidth() / 2;
|
||||
progress_bar.y = progress_box.y + (progress_box.image.GetHeight() - progress_bar.original_image.GetHeight()) / 2;
|
||||
progress_bar.sprite.SetPosition(progress_bar.x, progress_bar.y, 1);
|
||||
progress_bar.sprite.SetOpacity(0);
|
||||
|
||||
fun progress_callback(duration, progress) {
|
||||
@@ -216,6 +210,39 @@ fun progress_callback(duration, progress) {
|
||||
|
||||
Plymouth.SetBootProgressFunction(progress_callback);
|
||||
|
||||
#----------------------------------------- Layout --------------------------------
|
||||
|
||||
# Center the logo, dialogue and progress bar for the current window size
|
||||
fun layout() {
|
||||
global.layout_width = Window.GetWidth();
|
||||
global.layout_height = Window.GetHeight();
|
||||
|
||||
logo.sprite.SetX(global.layout_width / 2 - logo.image.GetWidth() / 2);
|
||||
logo.sprite.SetY(global.layout_height / 2 - logo.image.GetHeight() / 2);
|
||||
|
||||
entry.x = global.layout_width / 2 - entry.image.GetWidth() / 2;
|
||||
entry.y = logo.sprite.GetY() + logo.image.GetHeight() + 40;
|
||||
entry.sprite.SetPosition(entry.x, entry.y, 10001);
|
||||
|
||||
lock.x = entry.x - lock.width - 15;
|
||||
lock.y = entry.y + entry.image.GetHeight() / 2 - lock.height / 2;
|
||||
lock.sprite.SetPosition(lock.x, lock.y, 10001);
|
||||
|
||||
for (index = 0; bullet.sprites[index]; index++) {
|
||||
position_bullet(index);
|
||||
}
|
||||
|
||||
progress_box.x = global.layout_width / 2 - progress_box.image.GetWidth() / 2;
|
||||
progress_box.y = entry.y + entry.image.GetHeight() / 2 - progress_box.image.GetHeight() / 2;
|
||||
progress_box.sprite.SetPosition(progress_box.x, progress_box.y, 0);
|
||||
|
||||
progress_bar.x = global.layout_width / 2 - progress_bar.original_image.GetWidth() / 2;
|
||||
progress_bar.y = progress_box.y + (progress_box.image.GetHeight() - progress_bar.original_image.GetHeight()) / 2;
|
||||
progress_bar.sprite.SetPosition(progress_bar.x, progress_bar.y, 1);
|
||||
}
|
||||
|
||||
layout();
|
||||
|
||||
#----------------------------------------- Message --------------------------------
|
||||
|
||||
message_sprite = Sprite();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Create the Cam Link 4K virtual camera
|
||||
After=systemd-modules-load.service
|
||||
|
||||
[Service]
|
||||
# Reruns on every relay start, so a deleted or unloaded device comes back.
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/modprobe v4l2loopback
|
||||
ExecStart=/bin/sh -c 'grep -qsx "Cam Link 4K" /sys/devices/virtual/video4linux/*/name || exec v4l2loopback-ctl add -n "Cam Link 4K" -x 1'
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
# udev starts this when the Cam Link 4K appears. Only the device's stop is
|
||||
# tied in: a start dependency on it would leave a job waiting whenever the
|
||||
# base v4l2-relayd.service is started with no Cam Link attached.
|
||||
ConditionPathExists=/dev/camlink4k
|
||||
StopPropagatedFrom=dev-camlink4k.device
|
||||
Requires=camlink-4k-loopback.service
|
||||
After=camlink-4k-loopback.service
|
||||
|
||||
[Service]
|
||||
RestartSec=2
|
||||
# The packaged command line, plus sync=false on the sink. v4l2src stamps each
|
||||
# frame with its capture time, so by the time it reaches the sink it is already
|
||||
# past due and a synced sink drops every one of them.
|
||||
ExecStart=
|
||||
ExecStart=/bin/sh -c 'DEVICE=$(grep -l -m1 -E "^${CARD_LABEL}$" /sys/devices/virtual/video4linux/*/name | cut -d/ -f6); exec /usr/bin/v4l2-relayd -i "${VIDEOSRC}" $${SPLASHSRC:+-s "$${SPLASHSRC}"} -o "appsrc name=appsrc caps=video/x-raw,format=${FORMAT},width=${WIDTH},height=${HEIGHT},framerate=${FRAMERATE} ! videoconvert ! v4l2sink name=v4l2sink sync=false device=/dev/$${DEVICE}" $EXTRA_OPTS'
|
||||
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=Omarchy speaker tuning filter-chain
|
||||
Documentation=https://github.com/basecamp/omarchy/blob/master/docs/audio-tuning.md
|
||||
Documentation=https://github.com/omacom/omarchy/blob/master/docs/audio-tuning.md
|
||||
# WirePlumber does the linking, so starting before it is up risks the output being
|
||||
# linked before the speaker device has been discovered.
|
||||
After=pipewire.service wireplumber.service
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Keep the Elgato Cam Link 4K's raw capture node away from users and hand it to
|
||||
# the v4l2-relayd instance that re-exposes it as a 16:9 virtual camera.
|
||||
# Sorted after 70-uaccess adds the tag and before 73-seat-late applies its ACL.
|
||||
SUBSYSTEM=="video4linux", ENV{ID_VENDOR_ID}=="0fd9", ENV{ID_MODEL}=="Cam_Link_4K", ENV{ID_V4L_CAPABILITIES}==":capture:", \
|
||||
TAG-="uaccess", OWNER="root", GROUP="root", MODE="0600", SYMLINK+="camlink4k", \
|
||||
TAG+="systemd", ENV{SYSTEMD_ALIAS}="/dev/camlink4k", ENV{SYSTEMD_WANTS}="v4l2-relayd@camlink.service"
|
||||
@@ -0,0 +1,9 @@
|
||||
# Elgato Cam Link 4K relayed as a fixed 16:9 virtual camera. Browser meeting
|
||||
# apps send 720p at most; raise WIDTH/HEIGHT (up to 3840x2160) for apps that
|
||||
# can use more.
|
||||
VIDEOSRC="v4l2src device=/dev/camlink4k"
|
||||
FORMAT=NV12
|
||||
WIDTH=1280
|
||||
HEIGHT=720
|
||||
FRAMERATE=30/1
|
||||
CARD_LABEL="Cam Link 4K"
|
||||
+10
-4
@@ -84,13 +84,18 @@ Omarchy update command, the hook exits non-zero with `AbortOnFail`, which stops
|
||||
the transaction before packages are changed.
|
||||
|
||||
`omarchy-update-system-pkgs`, `omarchy-refresh-pacman`, `omarchy-reinstall-pkgs`,
|
||||
`omarchy-channel-set`, and the v4 upgrader run pacman through:
|
||||
and `omarchy-channel-set` run pacman through the hidden `omarchy-update-pacman`
|
||||
helper (the v4 upgrader sets `OMARCHY_UPDATE_PACMAN=1` directly):
|
||||
|
||||
```bash
|
||||
env OMARCHY_UPDATE_PACMAN=1 pacman ...
|
||||
sudo env OMARCHY_UPDATE_PACMAN=1 systemd-run --scope --quiet --collect pacman ...
|
||||
```
|
||||
|
||||
so the guard allows Omarchy-owned update flows. A user can intentionally bypass
|
||||
so the guard allows Omarchy-owned update flows. The `systemd-run --scope`
|
||||
wrapper registers the transaction as a PID 1 scope: upgrading systemd reexecs
|
||||
the system and user managers mid-transaction, and a pacman left inside a
|
||||
user-session scope can be SIGKILLed by that reexec. On unbooted systems (such
|
||||
as the installer chroot) the helper runs pacman directly. A user can intentionally bypass
|
||||
the guard with:
|
||||
|
||||
```bash
|
||||
@@ -287,12 +292,13 @@ scripts.
|
||||
| `omarchy-update-confirm` | Gum confirmation copy for `omarchy update`. | **Question.** Could be inlined into `omarchy-update`; separate file only helps keep copy isolated. |
|
||||
| `omarchy-update-dev` | Fast-forwards the active dev-linked checkout from its configured upstream; no-ops for package-backed installs. | **Keep.** Runs before package updates so a checkout conflict stops the update before system mutation. |
|
||||
| `omarchy-update-keyring` | Ensures Omarchy keyring and Arch keyring are current before the main transaction. | **Keep, but review.** It uses targeted `pacman -Sy` for keyring bootstrapping; acceptable for this special case but should remain tightly scoped. |
|
||||
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with `--overwrite '/usr/share/omarchy/*'`, capturing stderr to a report file; on failure it execs `omarchy-update-system-pkgs-when-conflicted`. | **Keep for now.** Small leaf command, clear/testable. |
|
||||
| `omarchy-update-system-pkgs` | Runs `omarchy-update-pacman -Syu --noconfirm` with `--overwrite '/usr/share/omarchy/*'`, capturing stderr to a report file; on failure it execs `omarchy-update-system-pkgs-when-conflicted`. | **Keep for now.** Small leaf command, clear/testable. |
|
||||
| `omarchy-update-system-pkgs-when-conflicted` | Hidden conflict handler: quarantines unowned conflicting files under `/var/lib/omarchy/replaced`, retries the upgrade once, restores files the upgrade didn't claim, and hands package-vs-package conflicts to an interactive pacman run (never under `-y`). | **Keep internal/hidden.** Keeps conflict recovery out of the happy path. |
|
||||
| `omarchy-update-pkg-prune` | Trims the pacman cache to two versions per package (`paccache -rk2`) before the snapshot, keeping the offline downgrade path while capping snapshot growth. | **Keep internal/hidden.** |
|
||||
| `omarchy-update-requires-free-space` | Aborts the update below a 10 GiB free-space threshold on `/`; silently skipped when free space cannot be determined; `OMARCHY_UPDATE_FORCE=1` bypasses. | **Keep internal/hidden.** |
|
||||
| `omarchy-migrate` | Public migration command. Waits for pacman, then runs all pending migrations for the current user. Supports `--pending`. | **Keep.** This replaces the discarded `omarchy-update-user-finalize` name and no longer needs `--force`. |
|
||||
| `omarchy-update-pacman-guard` | ALPM pre-transaction guard that aborts direct `pacman -Syu` style upgrades unless Omarchy set `OMARCHY_UPDATE_PACMAN=1` or the user explicitly set `OMARCHY_ALLOW_DIRECT_PACMAN=1`. | **Keep internal/hidden.** This is what nudges users back to `omarchy update`. |
|
||||
| `omarchy-update-pacman` | Hidden helper that runs a guard-approved pacman transaction as a PID 1 scope (`systemd-run --scope`) so a mid-transaction systemd reexec cannot kill it; runs pacman directly when not booted under systemd. | **Keep internal/hidden.** Single place that owns how Omarchy invokes pacman for system mutation. |
|
||||
| `omarchy-migrate-notify` | Internal login-time notification helper. Uses `omarchy-migrate --pending` and shows a notification only when this user has pending migrations. | **Keep internal/hidden.** Clear name now that the public command is `omarchy-migrate`. |
|
||||
| `omarchy-update-user-notify` | Hidden compatibility wrapper for `omarchy-migrate-notify`. | **Temporary.** Keep only for old callers. |
|
||||
| `omarchy-update-available` | Update checker for shell widget and post-update refresh. | **Keep.** Could eventually be renamed `omarchy-update-check`, but current name matches widget semantics. |
|
||||
|
||||
@@ -15,7 +15,8 @@ ENABLE_LIMINE_FALLBACK=yes
|
||||
# Find and add other bootloaders
|
||||
FIND_BOOTLOADERS=yes
|
||||
|
||||
BOOT_ORDER="*, *fallback, Snapshots"
|
||||
# Keep T2 Macs on their specialized kernel; prefer linux-omarchy elsewhere.
|
||||
BOOT_ORDER="linux-t2, linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"
|
||||
|
||||
# Snapper is configured with NUMBER_LIMIT="5" (see default/snapper/root), but
|
||||
# limine-snapper-sync can see the newly created sixth snapshot before cleanup
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Keep Cursor optional while applying these options whenever mise installs it.
|
||||
# Expose only its launcher so the bundled Node cannot shadow the user's Node.
|
||||
[tool_alias]
|
||||
cursor-agent = 'http:cursor-agent[bin_path=bin,postinstall=mkdir -p "$MISE_TOOL_INSTALL_PATH/bin" && ln -sfn ../dist-package/cursor-agent "$MISE_TOOL_INSTALL_PATH/bin/cursor-agent"]'
|
||||
@@ -1,6 +1,12 @@
|
||||
# Solve common flakiness with SSH (MTU discovery on flaky links).
|
||||
net.ipv4.tcp_mtu_probing=1
|
||||
|
||||
# BBR estimates bottleneck bandwidth and minimum RTT and paces to them, where
|
||||
# cubic keeps pushing until packets drop. That cuts queueing latency
|
||||
# (bufferbloat) on fast links. fq is the qdisc BBR is built to pace through.
|
||||
net.core.default_qdisc=fq
|
||||
net.ipv4.tcp_congestion_control=bbr
|
||||
|
||||
# Tune reclaim for swap on zram, which is orders of magnitude faster than the
|
||||
# disk swapfile these defaults assume.
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Kyber keeps reads in their own queue and throttles the depth it submits to
|
||||
# hold a 2ms read latency target, so interactive reads keep flowing while a
|
||||
# large build, copy, or package upgrade floods the disk with writes. The
|
||||
# kernel's own pick (none or mq-deadline, depending on the device) does not
|
||||
# regulate latency once the queue fills. Whole disks only: partitions have no
|
||||
# scheduler of their own, and zram is memory with nothing to schedule. Zoned
|
||||
# btrfs disks are moved back to mq-deadline by 64-btrfs-zoned.rules.
|
||||
ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="nvme*|sd*|mmcblk*|vd*", ATTR{queue/scheduler}="kyber"
|
||||
@@ -14,18 +14,14 @@ run_logged "$OMARCHY_INSTALL/hardware/vulkan.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/video-acceleration.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/lpmd.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/thermald.sh"
|
||||
# Swap in the Panther Lake kernel before anything pulls DKMS modules in.
|
||||
# intel-ipu7-camera drags in ipu7-drivers, vision-drivers and v4l2loopback,
|
||||
# and building all three against the stock kernel only to rebuild them against
|
||||
# linux-ptl and tear the first set down again cost ~25s of the install.
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/ptl-kernel.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/ipu7-camera.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/fred.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/fix-wifi7-eht.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/sof-firmware.sh"
|
||||
|
||||
# Rebuilds the boot image, so it has to follow the Panther Lake kernel swap
|
||||
# above rather than sit with the other Dell leaf at the top of this file.
|
||||
run_logged "$OMARCHY_INSTALL/hardware/fix-elgato-camlink-4k.sh"
|
||||
|
||||
# Rebuilds the boot image, so it follows camera module setup.
|
||||
run_logged "$OMARCHY_INSTALL/hardware/dell-xps13-sidecar-amps.sh"
|
||||
|
||||
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-display-backlight.sh"
|
||||
|
||||
@@ -6,5 +6,5 @@ pci_info=$(lspci -nn)
|
||||
|
||||
if (echo "$pci_info" | grep -q "14e4:43a0" || echo "$pci_info" | grep -q "14e4:4331"); then
|
||||
echo "BCM4360 / BCM4331 detected"
|
||||
omarchy-pkg-add broadcom-wl dkms linux-headers
|
||||
omarchy-pkg-add broadcom-wl-dkms
|
||||
fi
|
||||
@@ -0,0 +1,17 @@
|
||||
# Expose the Elgato Cam Link 4K as a 16:9-only virtual camera.
|
||||
# Browsers ask it for 640x480, which it fills by cropping, and the 4:3 frame
|
||||
# then gets stretched into a 16:9 tile. The raw node is hidden from users and
|
||||
# v4l2-relayd re-exposes it at 1280x720 under the same name.
|
||||
|
||||
if omarchy-hw-elgato-camlink-4k; then
|
||||
omarchy-pkg-add v4l2loopback-dkms v4l2loopback-utils v4l2-relayd
|
||||
|
||||
sudo install -Dm644 "$OMARCHY_PATH/default/udev/elgato-camlink-4k.rules" /etc/udev/rules.d/71-elgato-camlink-4k.rules
|
||||
sudo install -Dm644 "$OMARCHY_PATH/default/v4l2-relayd/camlink.conf" /etc/v4l2-relayd.d/camlink.conf
|
||||
sudo install -Dm644 "$OMARCHY_PATH/default/systemd/system/camlink-4k-loopback.service" /etc/systemd/system/camlink-4k-loopback.service
|
||||
sudo install -Dm644 "$OMARCHY_PATH/default/systemd/system/v4l2-relayd@camlink.service.d/camlink.conf" "/etc/systemd/system/v4l2-relayd@camlink.service.d/camlink.conf"
|
||||
|
||||
# The module's own default device would otherwise show up in browsers as
|
||||
# "Dummy video device" on machines without another relay.
|
||||
echo "options v4l2loopback exclusive_caps=1" | sudo tee /etc/modprobe.d/v4l2loopback-exclusive-caps.conf >/dev/null
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
# Install Tuxedo drivers for keyboard backlighting on Tuxedo laptops and
|
||||
# compatible devices like the Slimbook Executive (Clevo/Tuxedo chassis).
|
||||
if cat /sys/class/dmi/id/sys_vendor 2>/dev/null | grep -qi "TUXEDO\|Slimbook"; then
|
||||
omarchy-pkg-add linux-headers tuxedo-drivers-nocompatcheck-dkms
|
||||
omarchy-pkg-add tuxedo-drivers-nocompatcheck-dkms
|
||||
|
||||
# Blacklist the legacy clevo_xsm_wmi module which conflicts with the tuxedo-drivers
|
||||
# clevo_wmi module. When clevo_xsm_wmi loads first, it grabs the Clevo WMI GUIDs,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Install drivers for Motorcomm YT6801 ethernet adapter used by the Slimbook Executive
|
||||
if lspci | grep -i "YT6801\|Motorcomm.*Ethernet"; then
|
||||
omarchy-pkg-add linux-headers yt6801-dkms
|
||||
omarchy-pkg-add yt6801-dkms
|
||||
fi
|
||||
@@ -1,25 +0,0 @@
|
||||
# Install Panther Lake kernel for Dell XPS Panther Lake systems
|
||||
# The linux-ptl kernel includes audio driver patches not yet in mainline.
|
||||
|
||||
if omarchy-hw-match "XPS" && omarchy-hw-intel-ptl; then
|
||||
echo "Detected Dell XPS Panther Lake, installing PTL kernel..."
|
||||
|
||||
omarchy-pkg-add linux-ptl linux-ptl-headers
|
||||
pacman -Rdd --noconfirm linux linux-headers || true
|
||||
|
||||
# linux-ptl doesn't provide=linux, so anything depending on linux drags the
|
||||
# stock kernel back in and the boot menu grows a second, slower entry.
|
||||
if pacman -Qq linux &>/dev/null; then
|
||||
echo "WARNING: stock linux kernel still installed alongside linux-ptl:"
|
||||
pacman -Qi linux | grep -i "required by"
|
||||
fi
|
||||
|
||||
mkdir -p /etc/limine-entry-tool.d
|
||||
# Named to sort after omarchy-defaults.conf: drop-ins are read in order and
|
||||
# the last BOOT_ORDER wins, so an earlier-sorting name is a silent no-op.
|
||||
rm -f /etc/limine-entry-tool.d/dell-xps-panther-lake.conf
|
||||
cat > /etc/limine-entry-tool.d/zz-dell-xps-panther-lake.conf <<'EOF'
|
||||
# Only show Panther Lake kernel in boot menu on Dell XPS Panther Lake
|
||||
BOOT_ORDER="linux-ptl*, *fallback, Snapshots"
|
||||
EOF
|
||||
fi
|
||||
@@ -1,8 +1,4 @@
|
||||
if lspci | grep -qi 'nvidia'; then
|
||||
# Check which kernel is installed and set appropriate headers package
|
||||
KERNEL_PACKAGE=$(pacman -Qqs '^linux(-zen|-lts|-hardened|-t2|-ptl)?$' | head -1 || true)
|
||||
[[ -n $KERNEL_PACKAGE ]] && omarchy-pkg-add "$KERNEL_PACKAGE-headers"
|
||||
|
||||
if omarchy-hw-nvidia-gsp; then
|
||||
PACKAGES=(nvidia-open-dkms nvidia-utils lib32-nvidia-utils libva-nvidia-driver)
|
||||
elif omarchy-hw-nvidia-without-gsp; then
|
||||
|
||||
@@ -5,7 +5,7 @@ autoconf-archive
|
||||
asusctl
|
||||
base
|
||||
base-devel
|
||||
broadcom-wl
|
||||
broadcom-wl-dkms
|
||||
btrfs-progs
|
||||
dkms
|
||||
egl-wayland
|
||||
@@ -20,11 +20,9 @@ libva-nvidia-driver
|
||||
limine
|
||||
limine-mkinitcpio-hook
|
||||
limine-snapper-sync
|
||||
linux
|
||||
linux-firmware
|
||||
linux-headers
|
||||
linux-ptl
|
||||
linux-ptl-headers
|
||||
linux-omarchy
|
||||
linux-omarchy-headers
|
||||
macbook12-spi-driver-dkms
|
||||
nvidia-580xx-dkms
|
||||
nvidia-dkms
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
mkdir -p "$HOME/Work"
|
||||
mkdir -p "$HOME/Work/tries"
|
||||
|
||||
cat >"$HOME/Work/.mise.toml" <<'EOF'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
EOF
|
||||
|
||||
mise trust ~/Work/.mise.toml
|
||||
|
||||
# Offline installs unpack the Node tarball bundled by the ISO: from
|
||||
# /opt/packages in the ISO chroot, or from the copy staged in provisioning state when
|
||||
# omarchy-provision-owner finalizes the user at first boot.
|
||||
|
||||
@@ -18,6 +18,8 @@ omarchy-cmd-missing cursor-agent && omarchy-mise-install cursor-agent
|
||||
omarchy-mise-install npm:@kitlangton/ghui ghui
|
||||
omarchy-mise-install aqua:modem-dev/hunk hunk
|
||||
omarchy-mise-install github:basecamp/hey-cli hey
|
||||
omarchy-mise-install github:basecamp/basecamp-cli basecamp
|
||||
omarchy-mise-install npm:cf cf
|
||||
omarchy-mise-install github:OpenRouterLabs/ori-releases ori
|
||||
# Every line above writes a stub and cannot fail. This one can: it exits
|
||||
# non-zero when Hermes Desktop owns Hermes but has not finished setting it up,
|
||||
|
||||
@@ -66,6 +66,8 @@ Finally, there's a special scratchpad workspace that drops down over whatever wo
|
||||
|
||||
It works especially well for a terminal running an agent, or for controls you want to interact with quickly without leaving the current workspace. To move a window off the scratchpad, send it directly to another workspace with something like `Super + Shift + 1`.
|
||||
|
||||
While the scratchpad holds a single window, it drops down as a centered panel rather than spanning the screen. Put a second app on it and it goes back to the full width, so the two have room to sit side by side.
|
||||
|
||||
### It takes some getting used to!
|
||||
|
||||
It takes a little while to get used to navigating your desktop like this, but once you do, it'll be hard to go back to a traditional mouse-driven desktop experience!
|
||||
+3
-1
@@ -26,6 +26,8 @@ To wrap an additional CLI the same way, run `omarchy-mise-install <package> [com
|
||||
|
||||
Pick your default agent with `omarchy default agent <name>` or under _Setup > Defaults > Agent_ in the Omarchy Menu (`Super + Space`). If the agent isn't installed yet, picking it installs it first. A fresh Omarchy will invite you to make this choice with a one-time notification.
|
||||
|
||||
Choosing Claude also attempts to set up its browser extension for Chromium, Chrome, Brave, Brave Origin, and Edge. The extension setup applies to all users and may ask for your system password; cancelling or a failed extension install still selects and launches Claude. Restart your browser, enable the extension if prompted, and sign in to Claude to finish connecting it. Run `/chrome` in Claude to check the connection. Firefox and Zen do not support this extension. If Claude was already your default before this setup was added, select it again to install the extension.
|
||||
|
||||
[Muse Code](https://dev.meta.ai) — Meta's `muse` — uses a preinstalled mise stub like the other agents. Picking it as the default installs Meta's official launcher through mise's HTTP backend. The launcher verifies and updates the native binary for your machine.
|
||||
|
||||
Once you've chosen, `Super + Shift + Ctrl + A` launches the default agent in a dedicated terminal window (or brings up the picker if you haven't chosen yet). You can also launch it straight into a task with `omarchy agent prompt "Review this project"`. Agents launched this way run unattended in their respective don't-stop-to-ask modes, so be ready for them to actually do things! And since agents refuse to remember trust for your home directory, launches from `$HOME` start in `~/Work` instead.
|
||||
@@ -48,7 +50,7 @@ Crashes can also be silenced one program at a time, which is what the diagnosis
|
||||
|
||||
### Desktop apps
|
||||
|
||||
The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, Hermes Desktop, OpenClaw, and the Perplexity desktop app.
|
||||
The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, the Claude desktop app (Anthropic's Linux beta, with Chat, Cowork, and Claude Code tabs), Grok Bot for chatting with xAI's models, Hermes Desktop, OpenClaw, and the Perplexity desktop app.
|
||||
|
||||
Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Installing it also hands Hermes the Omarchy theme as a skin named `omarchy`, which every Hermes surface follows as you switch themes; pick another under Hermes' Appearance settings or with `/skin` if you'd rather it didn't, and Omarchy leaves that choice alone. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself unless you tell it otherwise: it asks, defaulting to no, whether that data and your connection settings should go too.
|
||||
|
||||
|
||||
@@ -57,3 +57,5 @@ The full manual can be found via `man yt-dlp`.
|
||||
## try
|
||||
|
||||
[try](https://github.com/tobi/try) makes it easy to manage programming experiments with date-stamped directories. All experiments live in `~/Work/tries` and you can access them via `try`.
|
||||
|
||||
Omarchy does not add a project's `bin/` directory to `PATH` automatically. Run trusted project-local tools with an explicit relative path, such as `bin/rails` or `./bin/dev`.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Omarchy and your packages are kept up to date via _Update > Omarchy_ in the Omarchy menu (`Super + Space`).
|
||||
|
||||
Omarchy itself is installed as regular pacman packages from the [Omarchy Package Repository](https://github.com/omacom-io/omarchy-pkgs), so an update installs [the latest Omarchy release](https://github.com/basecamp/omarchy/releases), runs any pending migrations to get your system in sync with the latest, and updates all system packages from the [Omarchy Arch Mirror](https://github.com/omacom-io/omarchy-mirror) and [AUR](https://aur.archlinux.org/) (if you have installed any AUR packages).
|
||||
Omarchy itself is installed as regular pacman packages from the [Omarchy Package Repository](https://github.com/omacom-io/omarchy-pkgs), so an update installs [the latest Omarchy release](https://github.com/omacom/omarchy/releases), runs any pending migrations to get your system in sync with the latest, and updates all system packages from the [Omarchy Arch Mirror](https://github.com/omacom-io/omarchy-mirror) and [AUR](https://aur.archlinux.org/) (if you have installed any AUR packages).
|
||||
|
||||
When new releases are made, a circle arrow icon will appear to the right of your clock. Click it and the update process will start.
|
||||
|
||||
@@ -10,7 +10,7 @@ When new releases are made, a circle arrow icon will appear to the right of your
|
||||
|
||||
### Four channels
|
||||
|
||||
Omarchy is updated along four channels: stable, RC, edge, and dev. New installations start on the stable channel, which tracks the [official releases](https://github.com/basecamp/omarchy/releases/), as well as the [stable Omarchy Arch mirror](https://github.com/omacom-io/omarchy-mirror) that's running one month behind the latest, so we can catch any new incompatibilities that require config changes before they cause problems for people.
|
||||
Omarchy is updated along four channels: stable, RC, edge, and dev. New installations start on the stable channel, which tracks the [official releases](https://github.com/omacom/omarchy/releases/), as well as the [stable Omarchy Arch mirror](https://github.com/omacom-io/omarchy-mirror) that's running one month behind the latest, so we can catch any new incompatibilities that require config changes before they cause problems for people.
|
||||
|
||||
But if you'd like to help spot those potential issues, you can run on the edge channel. That'll keep your Omarchy packages tracking the latest development builds, and lets you update to the latest Arch packages as soon as they're available. You should only do this if you're experienced with Linux, and know how to recover a system that has problems.
|
||||
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export OMARCHY_SCREENSHOT_DIR="$HOME/Pictures/Screenshots"
|
||||
|
||||
You can do the same for screenrecordings using `OMARCHY_SCREENRECORD_DIR`.
|
||||
|
||||
Just remember to create the directoy you want to save to and restart Omarchy for this to take effect.
|
||||
Just remember to create the directory you want to save to and restart Omarchy for this to take effect.
|
||||
|
||||
### How do I get the speakers + webcam working on my Apple Studio Display?
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
|
||||
### Apple Virtual Machine
|
||||
|
||||
You can also install Omarchy inside a Parallels VM. Quite the cumbersome process, but there's [a user-driven guide](https://github.com/basecamp/omarchy/discussions/452) for that too.
|
||||
You can also install Omarchy inside a Parallels VM. Quite the cumbersome process, but there's [a user-driven guide](https://github.com/omacom/omarchy/discussions/452) for that too.
|
||||
|
||||
### VirtualBox
|
||||
|
||||
VirtualBox is a popular VM runner. [You can run Omarchy inside that too](https://github.com/basecamp/omarchy/discussions/176). But performance probably won't be great.
|
||||
VirtualBox is a popular VM runner. [You can run Omarchy inside that too](https://github.com/omacom/omarchy/discussions/176). But performance probably won't be great.
|
||||
|
||||
### VMware Workstation on Windows 11
|
||||
|
||||
Another popular VM runner for Windows. [Omarchy has been setup inside of that as well](https://github.com/basecamp/omarchy/discussions/572).
|
||||
Another popular VM runner for Windows. [Omarchy has been setup inside of that as well](https://github.com/omacom/omarchy/discussions/572).
|
||||
|
||||
### Steam Deck
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
echo "Retire the stock user icon font missed by the Quattro upgrade"
|
||||
|
||||
legacy_font="$HOME/.local/share/fonts/omarchy.ttf"
|
||||
|
||||
# The upgrader treated this as ~/.config/omarchy.ttf and left the old family
|
||||
# registered alongside the packaged font. Preserve custom fonts and symlinks.
|
||||
if [[ -f $legacy_font && ! -L $legacy_font ]]; then
|
||||
legacy_hash=$(sha256sum "$legacy_font")
|
||||
if [[ ${legacy_hash%% *} == "e55e67119e82f56f92d90cbf54b7ccc1b2946b32c535a29370439d7ef5215966" ]]; then
|
||||
if [[ ! -f /usr/share/fonts/omarchy/omarchy.ttf ]]; then
|
||||
echo "Packaged Omarchy icon font is missing; keeping the legacy font." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "$legacy_font"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Also refresh on retries after removal succeeded but the cache refresh failed.
|
||||
# The normal update restarts the shell, which reloads Qt's font database.
|
||||
fc-cache -f
|
||||
@@ -0,0 +1,20 @@
|
||||
echo "Expose the Elgato Cam Link 4K as a 16:9 virtual camera"
|
||||
|
||||
if omarchy-hw-elgato-camlink-4k; then
|
||||
source "$OMARCHY_PATH/install/hardware/fix-elgato-camlink-4k.sh"
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo udevadm control --reload
|
||||
|
||||
# Re-run the rules for the Cam Link that is plugged in now so it gets hidden
|
||||
# and relayed right away. The user ACL from its first plug survives a
|
||||
# re-trigger, so drop it here; a replug or reboot does the same on its own.
|
||||
sudo udevadm trigger --action=add --subsystem-match=video4linux
|
||||
sudo udevadm settle
|
||||
[[ -e /dev/camlink4k ]] && sudo setfacl -b /dev/camlink4k
|
||||
|
||||
# The trigger only starts the relay when the rule is new to the device.
|
||||
# Start it outright so a failed module build or loopback surfaces here,
|
||||
# with the raw camera hidden, rather than passing silently.
|
||||
sudo systemctl start v4l2-relayd@camlink.service
|
||||
fi
|
||||
@@ -0,0 +1,5 @@
|
||||
echo "Install basecamp (basecamp-cli) via mise wrapper"
|
||||
|
||||
if [[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]; then
|
||||
omarchy-mise-install github:basecamp/basecamp-cli basecamp
|
||||
fi
|
||||
@@ -0,0 +1,4 @@
|
||||
echo "Activate the Omarchy theme for existing T3 Code installs"
|
||||
|
||||
omarchy-pkg-present t3code-bin || exit 0
|
||||
omarchy-install-ai-t3-code
|
||||
@@ -0,0 +1,99 @@
|
||||
echo "Remove automatic project bin directories from PATH"
|
||||
|
||||
work_dir="$HOME/Work"
|
||||
mise_config="$work_dir/.mise.toml"
|
||||
# install/user/mise-work.sh as shipped in Omarchy 4.0.3.
|
||||
stock_sha="bd04f191d63bbde86920f44f76f0989fad980afc84e268e8474c201ec7149245"
|
||||
cwd_bin='\{\{[[:space:]]*cwd[[:space:]]*\}\}/bin'
|
||||
unsafe_path="^[[:space:]]*_[.]path[[:space:]]*=[[:space:]]*(\"$cwd_bin\"|'$cwd_bin')[[:space:]]*(#.*)?$"
|
||||
env_section='^[[:space:]]*\[[[:space:]]*env[[:space:]]*\][[:space:]]*(#.*)?$'
|
||||
any_section='^[[:space:]]*\[\[?.*\]\]?[[:space:]]*(#.*)?$'
|
||||
|
||||
remove_empty_work_dir=false
|
||||
if [[ ! -e $work_dir ]]; then
|
||||
mkdir -p "$work_dir"
|
||||
remove_empty_work_dir=true
|
||||
fi
|
||||
|
||||
was_ignored=false
|
||||
if [[ -d $work_dir ]]; then
|
||||
mise_state_dir=${MISE_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/mise}
|
||||
ignored_configs_dir="$mise_state_dir/ignored-configs"
|
||||
work_target=$(readlink -m "$work_dir")
|
||||
config_path_target="$work_target/.mise.toml"
|
||||
config_target=$(readlink -m "$mise_config")
|
||||
|
||||
if [[ -d $ignored_configs_dir ]]; then
|
||||
for ignored_entry in "$ignored_configs_dir"/*; do
|
||||
[[ -L $ignored_entry ]] || continue
|
||||
ignored_target=$(readlink "$ignored_entry")
|
||||
if [[ $ignored_target == $work_target || $ignored_target == $config_path_target || $ignored_target == $config_target ]]; then
|
||||
was_ignored=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $was_ignored == "false" ]]; then
|
||||
# Normal Mise trust is recorded against the config-root directory, while
|
||||
# paranoid trust is recorded against the file and its contents. Stage an
|
||||
# empty, inert config when the legacy file is gone so either trust mode can
|
||||
# resolve and revoke the original grant.
|
||||
remove_empty_mise_config=false
|
||||
if [[ ! -e $mise_config && ! -L $mise_config ]]; then
|
||||
if (set -o noclobber; : >"$mise_config") 2>/dev/null; then
|
||||
remove_empty_mise_config=true
|
||||
fi
|
||||
fi
|
||||
|
||||
untrust_target="$work_dir"
|
||||
if [[ -f $mise_config ]]; then
|
||||
untrust_target="$mise_config"
|
||||
fi
|
||||
|
||||
if mise trust --untrust "$untrust_target"; then
|
||||
:
|
||||
else
|
||||
if [[ $remove_empty_mise_config == "true" ]]; then
|
||||
rm -f -- "$mise_config"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $remove_empty_mise_config == "true" ]]; then
|
||||
rm -f -- "$mise_config"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f $mise_config ]]; then
|
||||
if [[ ! -L $mise_config && $(sha256sum "$mise_config" | cut -d ' ' -f 1) == $stock_sha ]]; then
|
||||
rm -f -- "$mise_config"
|
||||
else
|
||||
unsafe_env_paths=$(sed -n -E "\\%$env_section%,\\%$any_section% { \\%$unsafe_path%p; }" "$mise_config")
|
||||
if [[ -n $unsafe_env_paths ]]; then
|
||||
backup=$(mktemp "$mise_config.bak.XXXXXX")
|
||||
cp -p -- "$mise_config" "$backup"
|
||||
sed --follow-symlinks -i -E "\\%$env_section%,\\%$any_section% { \\%$unsafe_path%d; }" "$mise_config"
|
||||
|
||||
printf '\n%s\n' \
|
||||
"Automatic project bin directories were removed from your Mise PATH." \
|
||||
"Your other Mise settings were preserved."
|
||||
printf '\nBackup saved to:\n %s\n' "$backup"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f $mise_config ]]; then
|
||||
if [[ $was_ignored == "true" ]]; then
|
||||
printf '\n%s\n' "This custom config remains ignored by Mise."
|
||||
else
|
||||
printf '\n%s\n %s\n' \
|
||||
"Mise trust for this custom config was revoked. Review it before trusting it again:" \
|
||||
"mise trust $mise_config"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $remove_empty_work_dir == "true" ]]; then
|
||||
rmdir "$work_dir" 2>/dev/null || true
|
||||
fi
|
||||
@@ -0,0 +1,9 @@
|
||||
echo "Keep the KEF LSX II LT USB sink from suspending"
|
||||
|
||||
conf="wireplumber/wireplumber.conf.d/kef-lsx-no-suspend.conf"
|
||||
|
||||
if [[ ! -f "$HOME/.config/$conf" ]]; then
|
||||
omarchy-refresh-config "$conf"
|
||||
# WirePlumber only reads conf.d at startup; restart it if it is running.
|
||||
systemctl --user try-restart wireplumber.service 2>/dev/null || true
|
||||
fi
|
||||
@@ -0,0 +1,11 @@
|
||||
echo "Switch TCP congestion control to BBR with fq pacing"
|
||||
|
||||
# Boot applies the shipped file regardless; this only makes new connections
|
||||
# use BBR now. Setting the sysctls autoloads tcp_bbr and sch_fq. Qdiscs
|
||||
# already attached to interfaces stay until they are recreated, normally at
|
||||
# reboot.
|
||||
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" && $(sysctl -n net.core.default_qdisc) == "fq" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
sudo sysctl -p /etc/sysctl.d/99-omarchy-sysctl.conf >/dev/null || omarchy-state set reboot-required
|
||||
@@ -0,0 +1,5 @@
|
||||
echo "Install cf (Cloudflare CLI) via mise wrapper"
|
||||
|
||||
if [[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]; then
|
||||
omarchy-mise-install npm:cf cf
|
||||
fi
|
||||
@@ -0,0 +1,44 @@
|
||||
echo "Install the Omarchy kernel and make it the first Limine boot entry"
|
||||
|
||||
# linux-omarchy is an x86_64 kernel. T2 Macs must keep their specialized kernel,
|
||||
# including when other kernels are installed or the running T2 package is gone.
|
||||
[[ $(uname -m) == "x86_64" ]] || exit 0
|
||||
running_kernel=$(uname -r)
|
||||
if omarchy-pkg-present linux-t2 || [[ ${running_kernel,,} == *-t2* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
limine_conf="${OMARCHY_KERNEL_LIMINE_CONF:-/etc/default/limine}"
|
||||
rebuild_marker="${OMARCHY_KERNEL_REBUILD_MARKER:-/var/lib/omarchy/migrations/1789325478}"
|
||||
kernel="linux-omarchy"
|
||||
|
||||
# Completion is machine-wide even though migrations run once per user. Leave
|
||||
# the old kernel installed so it remains available if the new one cannot boot.
|
||||
# The new filename and marker also reach users who completed 1789095456.
|
||||
[[ ! -e $rebuild_marker ]] || exit 0
|
||||
omarchy-pkg-add "$kernel" "$kernel-headers"
|
||||
|
||||
# /etc/default/limine has priority over every drop-in, including old Dell
|
||||
# settings and customized package files whose updates landed in a .pacnew.
|
||||
# Set the exact kernel first: linux-omarchy-* only matches its older variants.
|
||||
# Preserve unrelated settings, especially the root filesystem's kernel cmdline.
|
||||
sudo mkdir -p "$(dirname "$limine_conf")"
|
||||
sudo touch "$limine_conf"
|
||||
sudo sed -i -E '/^[[:space:]]*BOOT_ORDER[[:space:]]*=/d' "$limine_conf"
|
||||
printf '\n%s\n' 'BOOT_ORDER="linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"' | sudo tee -a "$limine_conf" >/dev/null
|
||||
|
||||
# Package hooks ran before the config repair. Rebuild the new kernel's image
|
||||
# and boot entry explicitly, including on retries after a failed rebuild.
|
||||
sudo limine-mkinitcpio "$kernel"
|
||||
|
||||
# limine-mkinitcpio can return success after skipping a failed kernel build.
|
||||
# Do not mark the migration complete unless the new kernel is in the menu.
|
||||
if ! sudo limine-entry-tool --tree | grep -E "(^|[^[:alnum:]_-])$kernel([^[:alnum:]_-]|$)" >/dev/null; then
|
||||
echo "The Omarchy kernel has no Limine boot entry; rerun omarchy-migrate after fixing the boot image build." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Keeping the running kernel installed prevents the updater from detecting a
|
||||
# kernel replacement, so request the reboot explicitly.
|
||||
omarchy-state set reboot-required
|
||||
sudo install -Dm644 /dev/null "$rebuild_marker"
|
||||
@@ -0,0 +1,10 @@
|
||||
echo "Install missing headers for the Omarchy or T2 kernel"
|
||||
|
||||
# Fresh ISO installs mark earlier migrations complete, so the kernel migration
|
||||
# cannot repair headers omitted by those installers. Package installation is
|
||||
# idempotent when another user has already applied this repair.
|
||||
for kernel in linux-omarchy linux-t2; do
|
||||
if omarchy-pkg-present "$kernel"; then
|
||||
omarchy-pkg-add "$kernel-headers"
|
||||
fi
|
||||
done
|
||||
-150
@@ -1,150 +0,0 @@
|
||||
# Plan: Nix — replace Arch with a sovereign Nix foundation
|
||||
|
||||
Revision 2. Rev 2 incorporates adversarial review by codex (xhigh): atomicity restated as atomic selection rather than transactional activation, staged switches for major updates, password hashes kept out of the store, a legal-redistribution gate for unfree packages, precise sovereignty boundaries (mise, fwupd, Steam, Cloudflare), a signed release manifest with anti-rollback, source-rebuild proof in the continuity gate, garbage-collection policy, and a substantially hardened migration: supported-layout gating, live-probed hardware config, an explicit boot transaction with user blessing, state-divergence policy for the shared home, and two-stage rollback.
|
||||
|
||||
## Problem
|
||||
|
||||
Omarchy spends a remarkable amount of its code protecting users from its own package manager. The scars are all pacman-shaped:
|
||||
|
||||
- `omarchy-update-system-pkgs-when-conflicted` is ~150 lines of quarantine choreography — stash unowned conflicting files under `/var/lib/omarchy/replaced`, retry, restore what the upgrade didn't claim — because pacman refuses to own file conflicts.
|
||||
- The `etc-overrides/` mechanism (`docs/file-layout.md`) exists solely because pacman won't let two packages touch the same `/etc` file, so we ship copies to `/usr/share/omarchy/etc-overrides/` and `cp -f` them into place from scriptlets.
|
||||
- An ALPM hook (`00-omarchy-update-guard.hook`) aborts direct `pacman -Syu` because updates that bypass `omarchy update` skip the coordination around them — snapshots, migrations, hooks, restart checks; we built a guard to keep users away from the distribution's own tooling.
|
||||
- The keyring dance in `omarchy-update-keyring` bootstraps trust through `keys.openpgp.org`, and `etc/gnupg/dirmngr.conf` lists five more external keyservers — our signature chain roots outside our infrastructure.
|
||||
- Updates are not atomic, so we bolted atomicity on: snapper snapshots plus `limine-snapper-sync` approximate what the package manager can't promise, and `docs/update-process.md` still lists pacnew/pacsave handling as an open wound.
|
||||
- `omarchy-upgrade-to-quattro` is 2,389 lines. That is what it costs to move a fleet of mutable, individually-drifted Arch installs through one package-layout transition.
|
||||
|
||||
And sovereignty is only half-won. We already run the hosting — `mirror.omarchy.org` serves core/extra/multilib, `pkgs.omarchy.org` serves the `[omarchy]` repo, stable deliberately trails upstream Arch by a month (`manual/30-updates.md`) — but we don't own the substance. Arch decides what a "system upgrade" contains and when soname bumps land; we inherit every decision a day later and can only delay it. The AUR path (`omarchy-pkg-aur-*`, the Install menu) executes unsigned build scripts fetched live from `aur.archlinux.org`. T2 Macs add a GitHub-hosted third-party repo with `SigLevel = Never` (`install/hardware/pacman.sh`). And because every install mutates independently, no two Omarchy machines run the same bytes — "we tested this update" is a statement about our machine, not yours.
|
||||
|
||||
Nix fixes the category, not the symptoms. A NixOS system is a closure: one immutable tree of store paths containing every package, config file, and service definition, built once, signed once, and selected atomically — the running system is a symlink flip to a complete generation, and the old one stays bootable. Rollback is booting the previous generation; file conflicts and pacnew files are structurally impossible; and every machine's packages are the byte-identical store paths we built and tested (the thin top-level closure that composes them — hostname, disk UUIDs, the user's package manifest — is assembled per machine; the payload is not). To be precise about what is and isn't atomic: *selecting* a generation is atomic, *activating* one is a sequence — services stop, activation scripts run, services start — and a step in that sequence can fail. The design below stages risky switches across a reboot for exactly that reason. The catch is that the Nix ecosystem assumes nixos.org: `cache.nixos.org` as substituter, nixpkgs from GitHub, channels from `channels.nixos.org`, an install script piped from their web server. This plan takes the technology and none of the hosting.
|
||||
|
||||
## Shape
|
||||
|
||||
- Omarchy becomes a NixOS-based system whose entire supply chain runs on omarchy.org infrastructure: a pinned nixpkgs fork on our git hosting, closures built on our build farm, binaries served from our signed cache. A user's machine never contacts nixos.org, cache.nixos.org, or GitHub for OS concerns — the same posture `pkgs.omarchy.org` and the mirrors have today, extended until it covers everything.
|
||||
- Users don't learn Nix. The `omarchy` CLI keeps its verbs (`omarchy-pkg-add`, `omarchy update`, `omarchy-channel-set`), `~/.config` stays your mutable files, themes and the refresh pattern are untouched. Nix is plumbing, exactly as pacman was plumbing — it just leaks less.
|
||||
- An update is: fetch prebuilt, signed store paths from our cache, compose the new generation, activate it — across a reboot when the jump is big — and keep the old generation bootable. What we ship is what we tested, store path for store path.
|
||||
|
||||
## Sovereignty, precisely
|
||||
|
||||
"Sovereign" means two different things at two different times, and the plan should be honest about which is which:
|
||||
|
||||
- **Runtime sovereignty (absolute, for OS delivery)**: an installed machine resolves every OS need — binaries, sources, expressions, signatures, update metadata — against omarchy.org hosts only. No fallback substituters, no keyservers, no GitHub fetches, no upstream flake registry. If nixos.org vanished tomorrow, no user would notice.
|
||||
- **Build-time sovereignty (continuity)**: our infrastructure ingests from upstream nixpkgs at development time, then archives everything — the nixpkgs tree in our git mirror, every source tarball in our archive, every build product *and its build closure* (sources, patches, derivations, the compilers that made it) in our cache. If upstream vanished, we could keep building, patching, and releasing from what we hold, indefinitely. What we do not claim: re-deriving the world from a bootstrap seed. Nixpkgs' standard binary bootstrap tarballs are part of what we mirror and trust; full source-bootstrap purity is out of scope.
|
||||
|
||||
The boundary is OS delivery, and the plan names what sits outside it rather than letting "absolute" quietly overclaim. `mise`-managed tools pull from GitHub and language registries; fwupd firmware comes from LVFS; Steam downloads Valve's content; browsers update their own components. Those are application-content channels the user chose, not OS delivery, and they keep working — but each gets an explicit decision (mirror it, repoint it, or declare it outside the promise) instead of an assumption. The dev channel's GitHub clone in `omarchy-channel-set` repoints to our git hosting. And Cloudflare stays as the DDoS shield and CDN (`manual/48-security.md`), but the cache origin is storage we control, with a documented path to serve it from elsewhere — a CDN in front of sovereign infrastructure, never the only copy of it.
|
||||
|
||||
The release gate makes this testable, in two parts. Delivery: a release is publishable only if a clean machine, with outbound network restricted to omarchy.org, can install the ISO, update, and install every curated extra. Continuity: from an empty store, with binary substitution disabled and only our source archive reachable, the release closure must rebuild — proving we archived the build inputs, not just the outputs. Sovereignty becomes a CI assertion instead of an aspiration.
|
||||
|
||||
## Rejected approaches
|
||||
|
||||
- **Nix on top of Arch** (Nix as a secondary package manager, Arch stays the base): two package managers, two update pipelines, two failure modes, and the worst properties of both — pacman still owns the system, so none of the atomicity or reproducibility arrives where it matters. The halfway house costs most of the migration and delivers little of the payoff.
|
||||
- **Guix**: the same functional model with a nicer language, but its FSDG-purist stance on proprietary firmware, microcode, and NVIDIA drivers means fighting the distribution on exactly the hardware enablement (`install/hardware/` is 51 leaves deep) that Omarchy considers table stakes. Nonguix exists; building a product on an unofficial channel the project disowns is not a foundation.
|
||||
- **cache.nixos.org as fallback substituter**: the tempting hedge — use our cache first, theirs when we miss. It silently converts every gap in our build coverage into an external runtime dependency, which is precisely the failure mode this plan exists to eliminate. Misses should fail loudly and get fixed in our farm, not papered over by someone else's CDN.
|
||||
- **Hydra for the build farm**: the canonical Nix CI is a sprawling Perl application that is its own operational project. Our release matrix is a known, finite list of targets; plain `nix build` over that list in ordinary CI, followed by `nix copy` to the cache, does the job with tooling we already understand.
|
||||
- **A live binary-cache daemon** (Attic, Harmonia): a Nix binary cache is narinfo and nar files — static content. Object storage behind Cloudflare is the same shape as the pacman repo we serve today, has no attack surface, and scales for free. A daemon earns its keep only if we later want deduplicating storage across many releases; start dumb.
|
||||
- **home-manager for user configs**: it would make `~/.config` a farm of read-only symlinks into the store, which is the opposite of Omarchy's "your files" philosophy (`plans/dots.md` exists because those files are yours to edit). The declarative boundary stops at the system layer; the user layer stays mutable plain files.
|
||||
- **Image-based atomicity instead** (ostree/Silverblue-style, or A/B partitions): atomic, but at image granularity — you get our image or you get nothing, and local package additions become a bolted-on overlay mechanism. Nix gives the same atomicity at package granularity, so `omarchy-pkg-add` keeps meaning something.
|
||||
- **Staying on Arch and hardening further**: the baseline. Every mitigation above can be polished, but they remain mitigations for structural properties — mutability, non-atomicity, conflict-prone file ownership — that pacman cannot shed. We would be signing up to maintain the workaround museum forever.
|
||||
|
||||
## Design
|
||||
|
||||
### Supply chain
|
||||
|
||||
- **nixpkgs fork**: a mirror of nixpkgs on our git hosting, plus an `omarchy` branch carrying our patches (the successor to `omarchy-pkgs`' PKGBUILD patches). Each release pins an exact revision. Flake inputs reference our tarball endpoint (`https://mirror.omarchy.org/src/nixpkgs-<rev>.tar.gz`) with the lockfile's `narHash` pinning content, so even the expression source is fetched from us and integrity-checked.
|
||||
- **Source archive**: builders fetch upstream sources once, at ingestion; every fixed-output derivation's output is then held in our cache and our source mirror. `hashedMirrors` pointed at omarchy.org covers `fetchurl`, but it is a hint, not a boundary — `fetchgit`, flake fetchers, and language-ecosystem fetchers each need their own mirroring, and a cache miss makes Nix try a local build whose fetcher will happily call GitHub. So the boundary is enforced where it can't be forgotten: builder and client network policy allows omarchy.org only, and a miss *fails loudly* — a hole in our archive is a bug to fix in the farm, never a silent fallback to upstream. Rebuilds never need the original upstream URL to still exist.
|
||||
- **The omarchy flake**: lives where `omarchy-pkgs` lives today — same repo split as now (this repo is the runtime; the packaging repo owns pins, the overlay of packages nixpkgs lacks, and the NixOS modules; `omarchy-iso` owns the installer). The T2 Mac kernel and the `linux-ptl` kernel move from third-party repos and AUR-adjacent sources into our overlay, built and signed on our farm — which closes today's `SigLevel = Never` hole outright.
|
||||
|
||||
### Binary cache and trust
|
||||
|
||||
- `cache.omarchy.org`: narinfo + nar objects on object storage behind Cloudflare, populated by `nix copy` from the farm, signed with an Omarchy ed25519 cache key. Released objects are write-once (object-locked): a nondeterministic rebuild must never silently replace a narinfo the fleet already trusts.
|
||||
- Cache signatures authenticate store paths; they do not say "this is the current stable release." That job belongs to a **release manifest**: a small document per channel naming the release version, the exact top-level closure hashes per hardware variant, and an expiry — signed offline with a release key that is *separate* from the cache key, monotonically versioned so a compromised CDN cannot replay last month's release, and re-signed on a cadence so a frozen mirror goes stale loudly. `omarchy-update-available` and the update flow trust the manifest first, paths second. Key hygiene — build key, cache key, release key, rotation, and revocation — is a Phase 0 deliverable with a rehearsed compromise-recovery runbook, not an appendix.
|
||||
- Client `nix.conf` (owned by our NixOS module, not user-editable state): `substituters = https://cache.omarchy.org` — nothing else, replacing the default cache.nixos.org entirely; `trusted-public-keys` lists only our key; the flake registry is pinned to our own registry file so bare flake references cannot reach GitHub.
|
||||
- Trust roots: the cache and release public keys ship inside the ISO and the installed closure. `keys.openpgp.org`, `archlinux-keyring`, `omarchy-update-keyring`, and the five keyservers in `etc/gnupg/dirmngr.conf` all leave the OS trust path (gnupg remains for the user's own purposes).
|
||||
|
||||
### Build farm
|
||||
|
||||
Our own builders run `nix build` over the release matrix: the base system closure per hardware variant (NVIDIA open/legacy, T2, `linux-ptl`, plain), every optional package behind the Install menu and `omarchy-install-*`, and the ISO. A release job then verifies the gate: every store path in every target closure must be substitutable from `cache.omarchy.org` before the release tag is signed. Nothing a user can reach through blessed UI may miss the cache.
|
||||
|
||||
One gate is legal, not technical: nixpkgs distinguishes redistributable-unfree from unfree-you-may-not-redistribute, and serving a package from our cache *is* redistribution. NVIDIA userspace drivers (nixpkgs patches them), VS Code, Chrome, vendor firmware, and printer blobs each need a per-package answer in Phase 0: confirmed redistribution rights, a redistributable substitute (VSCodium-shaped choices), or a blessed vendor-fetch exception — which is a named, per-package hole in the runtime-sovereignty claim, recorded as such rather than discovered later. No package enters the curated set without landing in one of those three buckets.
|
||||
|
||||
### The system layer
|
||||
|
||||
- Everything under `install/config/`, `install/hardware/`, and the `etc/` tree becomes NixOS module code: `services.displayManager.sddm`, `boot.plymouth`, snapper, docker, cups hardening, the sysctl/sudoers/tmpfiles drop-ins, the NVIDIA modprobe and initrd logic that today lives as conditional bash inside `etc/mkinitcpio.conf.d/omarchy_hooks.conf`. `omarchy-apply-system` and `omarchy-apply-hardware` become module imports plus hardware-variant selection instead of sourced shell leaves — and the entire `etc-overrides/` mechanism is deleted, because composing `/etc` from multiple sources is what the module system is.
|
||||
- **Bootloader**: limine stays — NixOS ships a `boot.loader.limine` module — but its job changes: boot entries are system generations, not snapper snapshots, so `limine-snapper-sync` and `limine-mkinitcpio` retire. The UKI and fallback-entry behavior configured in `etc/limine-entry-tool.d/` and the direct-boot path (`omarchy-setup-direct-boot`) must be reproduced deliberately — upstream's limine/UKI story is still settling — and boot security is its own workstream: Secure Boot stays explicitly unsupported (as `manual/02-getting-started.md` says today) unless that workstream designs key enrollment, measurement, and recovery properly; it does not sneak in as a module default.
|
||||
- **Per-machine composition, budgeted**: the cache delivers every package prebuilt, but each machine still evaluates and assembles its thin top-level closure — `/etc`, initrd, activation scripts — locally on every switch. That cost is real on low-end hardware and gets a measured budget (time and memory, on the weakest supported machines) in the acceptance suite, not an assumption that "everything substitutes, so it's fast."
|
||||
- **A supported customization layer**: `/etc` becoming module-owned cannot mean "hope nobody needed to change it." Mounts, sudo rules, kernel parameters, and service tweaks are system concerns with no home-directory equivalent, so the machine gets a blessed local-override file the modules import — real Nix options, documented, surviving updates — and every managed `/etc` file has a named owner. Coordination that today hides behind the pacman guard (migrations, hooks, restart markers) moves into activation-time logic keyed by release version, so even a user running `nixos-rebuild` directly cannot skip it: `omarchy update` stays the pleasant path, but correctness no longer depends on being the only path.
|
||||
- **Store hygiene**: closures don't orphan, but unreferenced store paths accumulate and old generations are what rollback is made of — so garbage collection is policy, not an afterthought: automatic GC with a generation-retention window, a cap on boot-menu generations, and a free-space floor, sized so the store's steady state on a user disk compares honestly with today's pruned pacman cache.
|
||||
- **Filesystem**: btrfs stays for `/home` (snapper's remaining job: user-file snapshots, until `plans/backup.md` and `plans/dots.md` cover that ground) and for `omarchy-system-factory-reset`'s subvolume mechanics — though the reset workflow itself (the `@factory` baseline, UKI rebuild, LUKS re-key) must be ported, and the restore guarantee narrows honestly: booting an old generation restores the OS, not mutable `/var` state the old root snapshots used to carry. `omarchy-snapshot restore` for the OS becomes "boot the previous generation."
|
||||
|
||||
### The user layer stays mutable
|
||||
|
||||
Non-negotiable: `~/.config` remains plain files the user owns and edits. `/etc/skel` seeding, `omarchy-refresh-config`, themes, and the entire `default/` → `~/.config` pipeline work unchanged. The declarative world ends at the system/user boundary; crossing it (home-manager) is rejected above. This is the line that keeps Omarchy feeling like Omarchy rather than like NixOS.
|
||||
|
||||
### Package UX
|
||||
|
||||
- The machine grows a package manifest — a plain text list in the spirit of `install/omarchy-base.packages`, owned by the machine, listing what this user added. `omarchy-pkg-add <name>` resolves the name (an alias table maps established Arch names to nixpkgs attributes, so muscle memory and the menu's package names keep working), appends to the manifest, and rebuilds against our cache — prebuilt, so "rebuild" means download, a local re-evaluation, and a switch: never a compile, and held to the per-machine composition budget above rather than assumed fast. `omarchy-pkg-drop` removes and rebuilds. `pkg-present`/`pkg-missing` query the running closure.
|
||||
- The Quickshell menu's guard prelude (`shell/plugins/menu/MenuModel.js` snapshots `pacman -Qq` plus a Provides parse because forking per guard "spends over a second") gets simpler and faster: one listing of the current closure's package set, computed at activation time and cached, replaces the pacman queries.
|
||||
- **The AUR is gone, replaced by the curated extras set**: everything the Install menu offers today (Chrome, Brave, Zen, VS Code, Steam and the lib32 Vulkan stack via nixpkgs' 32-bit support, and friends) comes from nixpkgs or our overlay, built and signed on our farm — the first time Omarchy's optional software carries the same signature chain as its core. Arbitrary AUR browsing (`omarchy-pkg-aur-install`) has no sovereign equivalent and is not replaced. The escape hatch for power users — adding their own flakes or substituters — is real Nix, documented as leaving the supported, sovereign envelope, and never wired into blessed UI.
|
||||
|
||||
### Updates, channels, migrations
|
||||
|
||||
- `omarchy-update` keeps its skeleton — transcript, lock, free-space check, confirm, stay-awake, migrations, hooks, `omarchy-update-restart` — and swaps its heart: the pacman transaction becomes "download the release closure from the cache, then switch." Failure before activation leaves the running system untouched, and the failed download costs nothing. Activation itself is the sequence that can still hurt — services stop, scripts run, services start — so routine updates switch live, while kernel and other big jumps stage as the *next boot's* generation and activate through the reboot `omarchy-update-restart` already prompts for. `omarchy-update-analyze-logs` survives with a shorter beat: activation and service-restart failures still deserve forensics; package transactions no longer do. And rolling back a generation rolls back the OS, not `/var` — a service that migrated its database forward needs its own story, which is what snapshots-before-update remain for.
|
||||
- Deleted outright, with the failure modes they existed for: `omarchy-update-keyring`, `omarchy-update-pkg-prune`, `omarchy-update-system-pkgs-when-conflicted`, `omarchy-update-pacman-guard` and the ALPM hooks, `omarchy-update-orphan-pkgs` (replaced by the GC policy above), `omarchy-update-aur-pkgs`, and the pacnew concern. The guard's job — "don't update behind Omarchy's back" — is covered by the activation-time coordination described above, which runs no matter who triggers the switch.
|
||||
- **Channels**: `stable`/`rc`/`edge` become branches of the omarchy flake with their own nixpkgs pins and their own cache prefixes, mirroring today's three pacman.conf templates. `omarchy-channel-set` flips the flake reference and switches. `dev` keeps its meaning: a local checkout via `omarchy-dev-link`, with `omarchy update` fast-forwarding it as now.
|
||||
- **Version**: real at last. `omarchy-version` reports the release tag of the running closure instead of deriving it from `pacman -Q`; `omarchy-update-available` compares that against a small release-manifest JSON on the cache host instead of running `checkupdates`.
|
||||
- **Migrations** (`migrations/`, 94 files) shrink to their legitimate residue: user-space state under `$HOME`. The 14 that touch pacman/limine/mkinitcpio have no successors — system-state transitions become module code that is simply part of the next closure. The per-user marker mechanism and `omarchy-migrate-notify` survive for what remains.
|
||||
|
||||
### ISO and installer
|
||||
|
||||
`omarchy-iso` rebuilds around a NixOS ISO carrying the full release closure in its store. Offline installation becomes `nix copy` from the ISO's store to the target plus writing the hardware module selection and the machine manifest — structurally the same "offline mirror" trick the ISO does today with pacman packages, minus the post-install `pacman.conf` restore dance (`install/post-install/pacman.sh`). The ISO signature chain (`iso.omarchy.org`, `.sig`) is unchanged.
|
||||
|
||||
## Migrating from Quattro to Cinque
|
||||
|
||||
`omarchy-upgrade-to-quattro`'s 2,389 lines are the cautionary tale for what in-place transitions cost — and that one didn't change the package manager. But Quattro's standard disk layout is the opportunity: root on a btrfs subvolume (`@`) with `/home` on its own (`@home`), inside one LUKS container, under a bootloader that already knows how to offer multiple roots. That layout lets Cinque move in *beside* Quattro instead of on top of it.
|
||||
|
||||
### The parallel-root migration
|
||||
|
||||
`omarchy-upgrade-to-cinque` ships as an ordinary Quattro package update, the same delivery path the v3→v4 upgrader used. It never runs unprompted — migration is an explicit user action, announced through the usual channels, never something `omarchy update` springs on anyone.
|
||||
|
||||
1. **Preflight, running system untouched**: the migrator supports the standard layout — btrfs root on `@`, `/home` on `@home`, one LUKS container, limine — and *refuses* everything else (LVM, RAID, exotic mount graphs, hand-built boot chains) toward the reinstall path; `omarchy-system-factory-reset` already gates on the same layout for the same reason, and for boot and storage, "I don't recognize this" is a blocker, not a warning. Then: a hardware gate — the machine's variant (NVIDIA generation, T2, `linux-ptl`) must have a built Cinque closure in the cache, or the migrator refuses with "not yet" rather than "hope so"; a space gate computed from the actual NAR sizes the cache reports plus the retained Quattro root, btrfs metadata headroom, and ESP room for both systems' boot artifacts (the fixed 10 GiB check in `omarchy-update-requires-free-space` is not an estimator); hibernation detection — a suspended image or the swap-subvolume setup from `omarchy-hibernation-setup` is invalidated and its resume configuration carried or rebuilt, because resuming one OS's hibernation image from the other corrupts the filesystem; and the inventory that feeds the *won't-survive report* (see below), which the user reads before consenting.
|
||||
2. **Fetch**: the release closure downloads from `cache.omarchy.org` into a fresh `@cinque` subvolume's `/nix` store — resumable, verifiable against signatures, and entirely inert while Quattro keeps running. The sovereignty gate applies here too: the whole migration touches only omarchy.org hosts.
|
||||
3. **Carry state**: the partition table, LUKS container, and `@home` are untouched — Cinque mounts the same `/home`. The machine's module configuration is generated from the *live* system — current mounts, `fstab`, `crypttab`, `lsblk`, `/proc/cmdline` — not from a replay of historical hardware detection, and the generated initrd is validated before anything is asked to boot from it. Accounts carry as the full database, not a hash import: `/etc/passwd`, `/etc/shadow`, groups, and NixOS's ID-stability state move as root-only files with `users.mutableUsers` on — password hashes must never be interpolated into the world-readable store. `machine-id`, SSH host keys, and NetworkManager connections come along; `/var/lib` payloads that are data rather than OS (docker volumes chief among them) are copied with their services stopped — a reflink copy of a live database is cheap and worthless.
|
||||
4. **First boot, Quattro still the default**: the migrator adds a Cinque boot entry inside a deliberate boot transaction — the ESP contents and firmware boot variables are inventoried and backed up first, foreign entries (Windows, other distros, the fallback loader) are preserved, and machines using `omarchy-setup-direct-boot`'s NVRAM path get that path handled explicitly. The user boots Cinque by choosing it; limine has no proven boot-once/boot-counting mechanism today, so *blessing is a human act*: first-boot verification (graphical session reached, network up, closure healthy) presents its results and asks before Cinque becomes the default. A failed boot needs no cleverness — the default was still Quattro, and a diagnostic bundle waits for `omarchy-upload-log`.
|
||||
5. **Rollback window, then reclaim**: at cutover the migrator snapshots `@home` — the two systems share a live home from here on, and applications will migrate profiles and state forward in formats the old side may not read, so a real return to Quattro needs that anchor to offer. `omarchy-upgrade-to-cinque --rollback` is two-stage by construction: it makes Quattro the default and reboots into it; only then, from the running Quattro, does it offer to restore the home snapshot (with post-cutover writes preserved alongside, never silently discarded) and remove `@cinque` — a system never deletes the root it is running on. In the other direction, `--reclaim` (or the update pipeline, after enough clean boots — open question) deletes the Quattro root and returns the space.
|
||||
|
||||
Rollback is a reboot plus a decision about state, and the plan says so — the OS comes back untouched by menu choice; the shared home's forward drift is what the cutover snapshot exists to answer. That is still a property no in-place mechanism can offer, and it is what makes offering the migration to a fleet responsible rather than reckless.
|
||||
|
||||
One wrinkle owned explicitly: during the window, exactly one side owns the bootloader — Cinque, from the moment its entry is blessed. The Quattro root is kept bootable but frozen — the migrator's only writes into it are disabling `limine-snapper-sync` and the update timers, because two operating systems regenerating one boot configuration is how both stop booting. Booting Quattro during the window is for rescue and rollback, not for continued dual life; the way back to a *living* Quattro is `--rollback`, which returns bootloader ownership along with the default.
|
||||
|
||||
### The won't-survive report
|
||||
|
||||
Some of what a Quattro machine accumulated has no Cinque equivalent, and the preflight says so per-machine, before anything changes:
|
||||
|
||||
- **Packages the user added**: the delta of `pacman -Qqe` against the Quattro release baseline (the raw list would drown the signal in the base system), run through the alias table into the manifest; AUR packages without an overlay equivalent (`pacman -Qem` minus the curated set) are listed by name with the escape-hatch documentation linked. Not a blocker — the user decides.
|
||||
- **Custom pacman repos**: both the `pre-refresh-pacman.d` hook layer and repos hand-added to `pacman.conf`, named as unsupported since the mechanism itself retires.
|
||||
- **System-level drift**: `pacman -Qii` backup-file diffs are the start, not the whole story — the scan also covers unowned files in `/etc` (`omarchy-update-system-pkgs-when-conflicted`'s quarantine logic proves we can tell), package-file divergence via `pacman -Qkk`, locally enabled or masked systemd units and drop-ins, DKMS modules, printer configuration, and firewall rules. Everything found is listed so the user can carry the *intent* forward — into `~/.config`, an Omarchy setting, or Cinque's local-override module — instead of silently losing edits. Drift in boot or storage configuration is a blocker, per the preflight.
|
||||
|
||||
Per-user state needs no migration at all: migration markers, themes, and everything else under `/home` ride along on `@home`. Dev-link users get their checkout fast-forwarded onto the Cinque branch by the migrator rather than a package swap.
|
||||
|
||||
### Rejected migration paths
|
||||
|
||||
- **`NIXOS_LUSTRATE` in place**: the historical takeover mechanism mutates the only root the machine has — a failure mid-lustrate is an unbootable machine and a restore from backup — and upstream is deprecating it (it doesn't work with the now-default systemd initrd, and NixOS's own guidance points at install-to-another-root instead, which is exactly what the parallel subvolume is). The parallel root delivers everything lustrate promised, plus a rollback that is just a boot-menu choice. Machines without room for two roots get "free up space first," not a reason to lose the rollback.
|
||||
- **Reinstall as the only path**: always supported, documented, and cheap once `plans/backup.md` and `plans/dots.md` land (which this plan therefore treats as prerequisites, not nice-to-haves) — but a migration path only matters if the fleet actually takes it, and "back up, reflash, restore" is where fleets quietly decide to stay behind. Reinstall is the fallback, not the offer.
|
||||
- **Automatic migration through `omarchy update`**: never. Changing a user's operating system's foundation is a decision, not an update.
|
||||
|
||||
## Rollout
|
||||
|
||||
- **Phase 0 — infrastructure, zero user impact**: nixpkgs mirror and tarball endpoint, source archive, `cache.omarchy.org`, the key hierarchy (build/cache/release, offline signing workflow, compromise runbook), the signed release-manifest format, the legal-redistribution inventory for the curated set, the build farm, and a CI job that builds the current desktop's equivalent closure and proves both halves of the sovereignty gate (delivery with outbound network restricted to omarchy.org; rebuild from the source archive with substitution disabled).
|
||||
- **Phase 1 — system parity** (packaging repo, with changes here): NixOS modules covering every `install/config/`, `install/hardware/`, and `etc/` entry; the flake with per-channel pins; boots and passes the graphical acceptance suite in the VM (`agents/skills/acceptance-tests.md`).
|
||||
- **Phase 2 — CLI port** (this repo): `pkg-*`, `update-*`, `channel-*`, `version-*`, snapshot/restore semantics, menu guards; delete the pacman-only organs; port the 18 pacman/yay-mocking test files in `test/shell.d/` to the new seams.
|
||||
- **Phase 3 — ISO and installer** (`omarchy-iso`): the offline NixOS ISO, installer flow, hardware detection wiring into module selection.
|
||||
- **Phase 4 — release and overlap**: ship as the next major; maintain the Quattro channels in parallel through the overlap window; deliver `omarchy-upgrade-to-cinque` as a Quattro package update, with the reinstall-with-restore path documented as the fallback.
|
||||
- **Docs and tests**: `docs/update-process.md` rewritten around the switch model; a new `docs/` reference for the sovereignty gate and cache/mirror topology; manual chapters for updating, rollback-by-generation, and the extras set; shell tests for manifest editing, alias resolution, channel flips, and guard-free update flow; switch-time and evaluation budgets measured on the weakest supported hardware in the acceptance suite; the release-gate CI assertion is itself the sovereignty test.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Which Nix**: upstream CppNix is the safe default; Lix is an argument about governance and pace we don't strictly need to have while we're rehosting everything anyway. Whichever we pick, users get it from our ISO and our cache — never from an install script on someone else's domain.
|
||||
2. **Flakes or stable evaluation**: flakes are the ecosystem's lingua franca but formally still experimental upstream. Since we pin our own Nix, we can adopt flakes and own the flag — or use plain evaluation with explicit pins and lose some tooling. Leaning flakes; deserves a deliberate decision.
|
||||
3. **How far the curated extras set reaches**: nixpkgs holds ~100k packages; we will build and cache hundreds, not all of it. What is the story when a user wants a package outside the set — a request pipeline into the overlay, the documented unsupported escape hatch, or both?
|
||||
4. **Reclaim policy** for the migration's rollback window: does the retained Quattro root get deleted only by explicit `--reclaim`, or automatically after N clean Cinque boots — and how long is a responsible default window on space-constrained disks?
|
||||
5. **Btrfs by default, still**: with system rollback moved to generations, btrfs earns its place only through `/home` snapshots and factory reset. Keep it, or simplify the default filesystem story?
|
||||
6. **Naming and posture**: "powered by Nix" is a fact; "a NixOS derivative" is a relationship with trademark and community expectations attached. How loudly do we say which — and does sovereign rehosting change what we ought to call it?
|
||||
@@ -59,6 +59,16 @@ Item {
|
||||
if (!powerProfileReadProcess.running) powerProfileReadProcess.running = true
|
||||
}
|
||||
|
||||
function parseActiveProfile(text) {
|
||||
// busctl --json=short prints {"type":"s","data":"balanced"}; an empty or
|
||||
// malformed reply (daemon not running) reads as no active profile.
|
||||
try {
|
||||
return String(JSON.parse(text).data || "").trim()
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: warningProcess }
|
||||
|
||||
Process {
|
||||
@@ -71,17 +81,23 @@ Item {
|
||||
|
||||
Process {
|
||||
id: powerProfileReadProcess
|
||||
command: ["powerprofilesctl", "get"]
|
||||
// Read the property straight from the daemon rather than via
|
||||
// `powerprofilesctl get`. That is a PyGObject script, and spawning a Python
|
||||
// interpreter for it every two seconds trips a CPython 3.14 shutdown race
|
||||
// (python/cpython#124619): the GLib D-Bus worker thread re-enters the
|
||||
// interpreter after finalization and the process dies with SIGSEGV,
|
||||
// leaving a core dump and a crash notification behind roughly daily.
|
||||
command: ["busctl", "--json=short", "get-property", "net.hadess.PowerProfiles", "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", "ActiveProfile"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.activePowerProfile = String(text || "").trim()
|
||||
onStreamFinished: root.activePowerProfile = root.parseActiveProfile(text)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
// powerprofilesctl has no portable monitor subcommand; keep profile changes
|
||||
// visible to consumers such as the wallpaper service without requiring the
|
||||
// power panel to be open.
|
||||
// There is no portable way to subscribe to profile changes from QML; keep
|
||||
// them visible to consumers such as the wallpaper service without requiring
|
||||
// the power panel to be open.
|
||||
interval: 2000
|
||||
running: true
|
||||
repeat: true
|
||||
|
||||
@@ -24,6 +24,20 @@ verify_core_packages() {
|
||||
pass "all Omarchy core packages are installed (${#missing[@]} missing)"
|
||||
}
|
||||
|
||||
verify_kernel_headers() {
|
||||
local kernel=linux-omarchy
|
||||
local release
|
||||
release=$(uname -r)
|
||||
omarchy-pkg-present linux-t2 && kernel=linux-t2
|
||||
|
||||
[[ $(cat "/usr/lib/modules/$release/pkgbase") == "$kernel" ]] ||
|
||||
fail "the installed system boots the supported kernel" "$release is not $kernel"
|
||||
omarchy-pkg-present "$kernel-headers" || fail "kernel headers are installed" "$kernel-headers is missing"
|
||||
[[ $(cat "/usr/lib/modules/$release/build/include/config/kernel.release") == "$release" ]] ||
|
||||
fail "headers match the running kernel" "$release has missing or mismatched headers"
|
||||
pass "the running $kernel kernel has matching headers ($release)"
|
||||
}
|
||||
|
||||
verify_defaults() {
|
||||
[[ $(omarchy-default-browser) == "chromium" ]] || fail "Chromium is the default browser"
|
||||
pass "Chromium is the default browser"
|
||||
@@ -112,7 +126,7 @@ verify_user_setup() {
|
||||
pass "Omarchy user state and shell configuration exist"
|
||||
}
|
||||
|
||||
for check in verify_core_packages verify_defaults verify_services verify_runtime_tools verify_user_setup; do
|
||||
for check in verify_core_packages verify_kernel_headers verify_defaults verify_services verify_runtime_tools verify_user_setup; do
|
||||
if ! ("$check"); then
|
||||
status=1
|
||||
fi
|
||||
|
||||
@@ -46,6 +46,12 @@ printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
|
||||
cp "$stub_bin/sudo" "$SUDO_TEST_ROOT/mock/sudo"
|
||||
|
||||
write_stub omarchy-update-pacman '#!/bin/bash
|
||||
printf "update-pacman" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
'
|
||||
|
||||
write_stub omarchy-dev-unlink '#!/bin/bash
|
||||
printf "unlink" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
@@ -122,7 +128,7 @@ assert_log_line() {
|
||||
|
||||
run_channel stable
|
||||
assert_log_line $'refresh\tstable\tdefer-hook' "stable refreshes the stable pacman channel"
|
||||
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
|
||||
assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
|
||||
assert_log_line $'unlink\t--no-reboot' "stable restores the package-backed Omarchy path without an early reboot prompt"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "stable runs the normal update pipeline from the package-backed path"
|
||||
if grep -q $'^state\tset\treboot-required$' "$log_file"; then
|
||||
@@ -132,7 +138,7 @@ pass "stable does not require reboot when already package-backed"
|
||||
|
||||
run_channel rc
|
||||
assert_log_line $'refresh\trc\tdefer-hook' "rc refreshes the rc pacman channel"
|
||||
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
|
||||
assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
|
||||
assert_log_line $'unlink\t--no-reboot' "rc restores the package-backed Omarchy path without an early reboot prompt"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "rc runs the normal update pipeline from the package-backed path"
|
||||
|
||||
@@ -140,11 +146,11 @@ active_checkout="$test_tmp/active-checkout"
|
||||
cp -a "$package_root" "$active_checkout"
|
||||
OMARCHY_TEST_PATH="$active_checkout" run_channel edge
|
||||
assert_log_line $'refresh\tedge\tdefer-hook' "edge refreshes the edge pacman channel"
|
||||
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
|
||||
assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
|
||||
assert_log_line $'unlink\t--no-reboot' "edge unlinks dev without an early reboot prompt"
|
||||
assert_log_line $'state\tset\treboot-required' "edge marks reboot required when leaving dev"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "edge runs the normal update pipeline from the package-backed path"
|
||||
[[ $(grep -E '^(unlink|state|update)' "$log_file") == $'unlink\t--no-reboot\nstate\tset\treboot-required\nupdate\t-y\tOMARCHY_PATH='"$package_root" ]] ||
|
||||
[[ $(grep -E $'^(unlink|state|update)\t' "$log_file") == $'unlink\t--no-reboot\nstate\tset\treboot-required\nupdate\t-y\tOMARCHY_PATH='"$package_root" ]] ||
|
||||
fail "edge defers the reboot prompt until the update restart stage" "$(cat "$log_file")"
|
||||
pass "edge defers the reboot prompt until the update restart stage"
|
||||
|
||||
@@ -164,12 +170,12 @@ rmdir "$checkout"
|
||||
run_channel dev
|
||||
assert_log_line $'gum\tconfirm\t--default=false\tSwitch to dev channel?' "dev asks for confirmation"
|
||||
assert_log_line $'refresh\tedge\tdefer-hook' "dev refreshes the edge pacman channel"
|
||||
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
|
||||
assert_log_line $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy"
|
||||
assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
|
||||
assert_log_line $'git\tclone\thttps://github.com/omacom/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy"
|
||||
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "dev links ~/omarchy without an early reboot prompt"
|
||||
assert_log_line $'state\tset\treboot-required' "dev defers the reboot prompt to the update pipeline"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$checkout" "dev runs the normal update pipeline from the source checkout"
|
||||
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file" | sed '/run-deferred/d') == $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\tdefer-hook\nsudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
|
||||
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file" | sed '/run-deferred/d') == $'git\tclone\thttps://github.com/omacom/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\tdefer-hook\nupdate-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
|
||||
fail "dev activates the checkout before changing or updating packages" "$(cat "$log_file")"
|
||||
pass "dev activates the checkout before changing or updating packages"
|
||||
[[ $(tail -1 "$log_file") == $'refresh\tedge\trun-deferred' ]] || fail "channel refresh hook must run after the complete update"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
# Exercise the privileged installer without writing to the host's /usr/share.
|
||||
if ! command -v bwrap >/dev/null || ! bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 true 2>/dev/null; then
|
||||
pass "user namespaces unavailable; skipping isolated Claude extension installation"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
mkdir -p "$test_tmp/share" "$test_tmp/bin"
|
||||
installer="$ROOT/bin/omarchy-install-chromium-claude"
|
||||
extension_id=fcoeoabgfenejglbffodgkkbkcdhcgfn
|
||||
sandbox=(bwrap --ro-bind / / --bind "$test_tmp" "$test_tmp" --dev /dev --proc /proc --unshare-user)
|
||||
|
||||
"${sandbox[@]}" --uid 0 --gid 0 --bind "$test_tmp/share" /usr/share bash "$installer"
|
||||
for browser in chromium google-chrome microsoft-edge; do
|
||||
file="$test_tmp/share/$browser/extensions/$extension_id.json"
|
||||
jq -e '.external_update_url == "https://clients2.google.com/service/update2/crx"' "$file" >/dev/null ||
|
||||
fail "$browser registers the official Claude Web Store extension"
|
||||
[[ $(stat -c '%a' "$file") == "644" ]] || fail "$browser extension registration is readable"
|
||||
done
|
||||
pass "Claude extension installer registers all supported browser families"
|
||||
|
||||
cat >"$test_tmp/bin/pkexec" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$@" >"$AUTH_LOG"
|
||||
exit 42
|
||||
SH
|
||||
chmod +x "$test_tmp/bin/pkexec"
|
||||
export AUTH_LOG="$test_tmp/auth-log"
|
||||
export PATH="$test_tmp/bin:$PATH"
|
||||
|
||||
# A read-only /usr/share and a failing auth stub prove that a repeated run
|
||||
# neither rewrites the files nor requests authentication.
|
||||
"${sandbox[@]}" --uid 1000 --gid 1000 --ro-bind "$test_tmp/share" /usr/share bash "$installer" </dev/null
|
||||
[[ ! -e $AUTH_LOG ]] || fail "configured Claude extensions need no authentication"
|
||||
pass "configured Claude extensions need neither writes nor authentication"
|
||||
|
||||
rm "$test_tmp/share/google-chrome/extensions/$extension_id.json"
|
||||
status=0
|
||||
"${sandbox[@]}" --uid 1000 --gid 1000 --ro-bind "$test_tmp/share" /usr/share bash "$installer" </dev/null || status=$?
|
||||
[[ $status == 42 ]] || fail "Claude extension installer propagates authentication failure"
|
||||
[[ $(cat "$AUTH_LOG") == "/usr/bin/omarchy-install-chromium-claude" ]] ||
|
||||
fail "menu installation elevates only the packaged installer"
|
||||
pass "missing registration uses pkexec and propagates authentication failure"
|
||||
@@ -22,6 +22,15 @@ menu_log="$test_tmp/menu"
|
||||
muse_login_log="$test_tmp/muse-login"
|
||||
mkdir -p "$mock_bin" "$test_home"
|
||||
|
||||
cat >"$mock_bin/omarchy-install-chromium-claude" <<'SH'
|
||||
#!/bin/bash
|
||||
echo claude-extension >>"$OMARCHY_TEST_STUB_LOG"
|
||||
if [[ ${OMARCHY_TEST_EXTENSION_FAIL:-false} == "true" ]]; then
|
||||
echo "Extension installation failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/omarchy-notification-send" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' "$@" >>"$OMARCHY_TEST_NOTIFICATION_HISTORY"
|
||||
@@ -406,9 +415,16 @@ declare -A expected_packages=(
|
||||
for selection in "${!expected_agents[@]}"; do
|
||||
expected=${expected_agents[$selection]}
|
||||
: >"$agent_open_log"
|
||||
: >"$stub_log"
|
||||
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent "$selection"
|
||||
[[ $(omarchy-default-agent) == $expected ]] || fail "default agent canonicalizes $selection"
|
||||
|
||||
if [[ $expected == "claude" ]]; then
|
||||
grep -qx claude-extension "$stub_log" || fail "Claude selection installs the browser extension"
|
||||
else
|
||||
[[ ! -s $stub_log ]] || fail "other agents do not install the Claude extension"
|
||||
fi
|
||||
|
||||
mapfile -d '' -t mise_args <"$mise_log"
|
||||
[[ ${mise_args[0]} == "use" && ${mise_args[1]} == "-g" ]] ||
|
||||
fail "default agent installs $selection globally through mise"
|
||||
@@ -426,6 +442,14 @@ pass "default agent selects and opens every supported provider and alias"
|
||||
fail "default agent stores its selection in Omarchy user config"
|
||||
pass "default agent stores its selection in Omarchy user config"
|
||||
|
||||
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent pi
|
||||
: >"$agent_open_log"
|
||||
OMARCHY_TEST_AGENT_INSTALLED=true OMARCHY_TEST_EXTENSION_FAIL=true omarchy-default-agent claude >"$test_tmp/extension-failure" 2>&1
|
||||
[[ $(omarchy-default-agent) == "claude" ]] || fail "extension installation failure still selects Claude"
|
||||
mapfile -d '' -t agent_open_args <"$agent_open_log"
|
||||
[[ ${agent_open_args[*]} == "omarchy-agent" ]] || fail "extension installation failure still launches Claude"
|
||||
[[ ! -s $test_tmp/extension-failure ]] || fail "extension installation failure is silent"
|
||||
pass "extension installation failure silently continues selecting and launching Claude"
|
||||
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent pi
|
||||
: >"$notification_history"
|
||||
: >"$agent_open_log"
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
if (( EUID != 0 )); then
|
||||
if unshare --user --map-root-user true 2>/dev/null; then
|
||||
exec unshare --user --map-root-user bash "$0"
|
||||
fi
|
||||
pass "no unprivileged user namespace; skipping factory account cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
# Load the production functions without self-elevation or the reset entrypoint.
|
||||
awk '
|
||||
/^[a-z_]+\(\) \{/ { copying = 1 }
|
||||
copying { print }
|
||||
/^}/ { copying = 0 }
|
||||
' "$ROOT/bin/omarchy-system-factory-reset" >"$test_tmp/functions"
|
||||
|
||||
cat >"$test_tmp/reset" <<'SH'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
source "$1/functions"
|
||||
TOP_MNT="$2"
|
||||
NEXT_NAME=@omarchy-reset-next
|
||||
PROVISIONING_DIR=/var/lib/omarchy/provisioning
|
||||
LOG_FILE="$TOP_MNT/reset.log"
|
||||
|
||||
log() { printf '%s\n' "$1" >>"$LOG_FILE"; }
|
||||
fail() { log "$1"; exit 1; }
|
||||
|
||||
# Account tools are real. Only snapshots, boot rebuilding, and system services
|
||||
# are replaced: all writes stay inside this test's disposable directory.
|
||||
btrfs() {
|
||||
if [[ $1 == "subvolume" && $2 == "snapshot" ]]; then
|
||||
mkdir -p "$4"
|
||||
cp -a "$3/." "$4/"
|
||||
elif [[ $1 == "property" ]]; then
|
||||
printf '%s\n' "$6" >"$4/read-only"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
systemd-id128() { printf '%032d\n' 1; }
|
||||
install_provisioning_units() { :; }
|
||||
encrypted_install() { return 1; }
|
||||
rebuild_next_boot() { touch "$TOP_MNT/rebuilt"; }
|
||||
sync() { :; }
|
||||
|
||||
userdel() {
|
||||
[[ ${FAIL_COMMAND:-} == "userdel" && $2 == "$FAIL_ROOT" ]] && return 42
|
||||
command userdel "$@"
|
||||
}
|
||||
usermod() {
|
||||
[[ ${FAIL_COMMAND:-} == "usermod" && $2 == "$FAIL_ROOT" ]] && return 42
|
||||
command usermod "$@"
|
||||
}
|
||||
rm() {
|
||||
[[ ${FAIL_COMMAND:-} == "rm" && $* == *"$FAIL_ROOT/etc/shadow-"* ]] && return 42
|
||||
command rm "$@"
|
||||
}
|
||||
|
||||
stage_full_reset
|
||||
SH
|
||||
|
||||
make_fixture() {
|
||||
local top="$1" root_hash="${2:-original-root-hash}"
|
||||
local factory="$top/@factory"
|
||||
mkdir -p "$top/@" "$factory/etc" "$factory/home/seller" \
|
||||
"$factory/usr/bin" "$factory/usr/share/omarchy/install/provisioning" \
|
||||
"$factory/var/lib/omarchy/provisioning/packages"
|
||||
touch "$top/@/old-system" "$factory/home/seller/private-file" \
|
||||
"$factory/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service" \
|
||||
"$factory/var/lib/omarchy/provisioning/packages/node-v0.tar.gz"
|
||||
printf '#!/bin/bash\n' >"$factory/usr/bin/omarchy-provision-owner"
|
||||
chmod +x "$factory/usr/bin/omarchy-provision-owner"
|
||||
printf 'true\n' >"$factory/read-only"
|
||||
cat >"$factory/etc/passwd" <<'EOF'
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/:/usr/bin/nologin
|
||||
seller:x:1000:1000:Seller:/home/seller:/bin/bash
|
||||
EOF
|
||||
printf 'root:%s:20000:0:99999:7:::\ndaemon:*:20000:0:99999:7:::\nseller:original-user-hash:20000:0:99999:7:::\n' \
|
||||
"$root_hash" >"$factory/etc/shadow"
|
||||
printf 'root:x:0:\ndaemon:x:1:\nseller:x:1000:\nwheel:x:998:seller\n' >"$factory/etc/group"
|
||||
printf 'root:!::\ndaemon:!::\nseller:!::\nwheel:!::seller\n' >"$factory/etc/gshadow"
|
||||
printf 'USERGROUPS_ENAB yes\n' >"$factory/etc/login.defs"
|
||||
printf 'seller:100000:65536\n' >"$factory/etc/subuid"
|
||||
printf 'seller:100000:65536\n' >"$factory/etc/subgid"
|
||||
chmod 600 "$factory/etc/"{shadow,gshadow}
|
||||
for file in passwd shadow group gshadow subuid subgid; do
|
||||
cp "$factory/etc/$file" "$factory/etc/$file-"
|
||||
done
|
||||
}
|
||||
|
||||
assert_scrubbed() {
|
||||
local root="$1" file
|
||||
[[ $(awk -F: '$1 == "root" { print $2 }' "$root/etc/shadow") == "!" ]] ||
|
||||
fail "reset erases the root hash while keeping the account locked"
|
||||
! grep -q 'original-.*-hash\|seller' "$root/etc/"{passwd,shadow,group,gshadow} ||
|
||||
fail "reset removes seller account credentials and group membership"
|
||||
[[ ! -e $root/home/seller ]] || fail "reset removes the seller's baseline home"
|
||||
grep -q '^daemon:\*:' "$root/etc/shadow" || fail "reset preserves service accounts"
|
||||
[[ $(stat -c '%a' "$root/etc/shadow") == "600" ]] || fail "shadow stays private"
|
||||
for file in passwd shadow group gshadow subuid subgid; do
|
||||
[[ ! -e $root/etc/$file- ]] || fail "reset removes the $file backup"
|
||||
done
|
||||
}
|
||||
|
||||
for scenario in normal locked; do
|
||||
top="$test_tmp/$scenario"
|
||||
if [[ $scenario == "locked" ]]; then
|
||||
make_fixture "$top" '!'
|
||||
else
|
||||
make_fixture "$top"
|
||||
fi
|
||||
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset stages successfully"
|
||||
assert_scrubbed "$top/@factory"
|
||||
assert_scrubbed "$top/@"
|
||||
[[ $(cat "$top/@factory/read-only") == "true" ]] || fail "baseline returns to read-only"
|
||||
[[ -f $top/@/var/lib/omarchy/provisioning/pending && -f $top/rebuilt ]] ||
|
||||
fail "reset reaches provisioning after cleanup"
|
||||
|
||||
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset can be repeated"
|
||||
assert_scrubbed "$top/@factory"
|
||||
assert_scrubbed "$top/@"
|
||||
pass "$scenario reset scrubs both roots, preserves service accounts, and can be repeated"
|
||||
done
|
||||
|
||||
for target in @omarchy-reset-next @factory; do
|
||||
for command in userdel usermod rm; do
|
||||
top="$test_tmp/fail-$target-$command"
|
||||
make_fixture "$top"
|
||||
if FAIL_COMMAND="$command" FAIL_ROOT="$top/$target" bash "$test_tmp/reset" "$test_tmp" "$top"; then
|
||||
fail "reset accepted failed $command in $target"
|
||||
fi
|
||||
[[ -f $top/@/old-system && ! -e $top/rebuilt ]] ||
|
||||
fail "failed cleanup must not activate or rebuild the reset system"
|
||||
[[ $(cat "$top/@factory/read-only") == "true" ]] ||
|
||||
fail "failed cleanup must leave the baseline read-only"
|
||||
pass "failed $command in $target aborts reset before activation"
|
||||
done
|
||||
done
|
||||
@@ -0,0 +1,3 @@
|
||||
Stock Omarchy icon font from `config/omarchy.ttf` at `babfafa5^`, before fonts moved into the settings package. SHA-256: `e55e67119e82f56f92d90cbf54b7ccc1b2946b32c535a29370439d7ef5215966`.
|
||||
|
||||
The migration test uses the real font so it exercises the exact-content guard without mocking `sha256sum`.
|
||||
Binary file not shown.
@@ -30,6 +30,7 @@ PY
|
||||
}
|
||||
|
||||
copy_boundary_file bin/omarchy-security-functions
|
||||
copy_boundary_file bin/omarchy-update-pacman
|
||||
copy_boundary_file default/omarchy/sudo-no-update/sudo
|
||||
|
||||
cat >"$SUDO_TEST_ROOT/mock/sudo" <<'STUB'
|
||||
@@ -82,6 +83,12 @@ cat >"$SUDO_TEST_ROOT/bin/test-step" <<'STUB'
|
||||
set -euo pipefail
|
||||
step=${0##*/}
|
||||
printf 'step:%s %s\n' "$step" "$*" >>"$SUDO_TEST_LOG"
|
||||
if [[ $step == "systemd-run" ]]; then
|
||||
# omarchy-update-pacman registers the transaction as a PID 1 scope on booted
|
||||
# hosts. Run the wrapped command in place so the pacman step still executes.
|
||||
while (( $# )) && [[ $1 == -* ]]; do shift; done
|
||||
exec "$@"
|
||||
fi
|
||||
if [[ $step == "omarchy-hook" || $step == "omarchy-update-mise" ]]; then
|
||||
[[ ! -e $SUDO_TEST_CACHE ]] || exit 91
|
||||
fi
|
||||
@@ -108,7 +115,7 @@ case "$step" in
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$SUDO_TEST_ROOT/bin/test-step"
|
||||
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman cp yay; do
|
||||
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman systemd-run cp yay; do
|
||||
ln -s test-step "$SUDO_TEST_ROOT/bin/$step"
|
||||
done
|
||||
ln -s ../bin/test-step "$SUDO_TEST_ROOT/mock/pacman"
|
||||
|
||||
@@ -357,3 +357,18 @@ OMARCHY_TEST_HOME="$hermes_home/PrOfIlEs/coder/../coder/" run_installer || fail
|
||||
[[ -x $runtime/apps/desktop/release/linux-unpacked/Hermes ]] || fail "profile uses the canonical root runtime"
|
||||
grep -qxF "$hermes_home" "$test_tmp/install-args" || fail "canonical custom home reaches upstream installer"
|
||||
pass "custom profile paths normalize to the shared Hermes home"
|
||||
|
||||
# New releases moved the stamp writer out of main. Keep the earlier cases on
|
||||
# the old layout and exercise a fresh installation with the relocated helper.
|
||||
git -C "$test_tmp/seed" mv hermes_cli/main.py hermes_cli/main_desktop.py
|
||||
printf 'raise AssertionError("legacy module imported after desktop split")\n' >"$test_tmp/seed/hermes_cli/main.py"
|
||||
git -C "$test_tmp/seed" add hermes_cli/main.py
|
||||
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm split-desktop
|
||||
release_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
|
||||
export OMARCHY_TEST_RELEASE_COMMIT="$release_commit"
|
||||
printf '{"branch":"main","commit":"%s"}\n' "$release_commit" >"$test_tmp/package/resources/install-stamp.json"
|
||||
new_home split-desktop
|
||||
run_installer || fail "setup supports the relocated desktop helper" "$(cat "$test_tmp/output")"
|
||||
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'upstream build stamp' ]] || fail "relocated helper writes the build stamp"
|
||||
grep -qx launch "$test_tmp/events" || fail "setup launches after the relocated helper writes the stamp"
|
||||
pass "new releases use the relocated desktop stamp writer"
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
work_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$work_dir"' EXIT
|
||||
|
||||
fake_home="$work_dir/home"
|
||||
mkdir -p "$fake_home/.config/omarchy/hooks" "$fake_home/.local/state/omarchy"
|
||||
|
||||
# --- omarchy-hook --------------------------------------------------------------
|
||||
|
||||
# A hook name is a label, not a path. One carrying a slash, or one that is a
|
||||
# bare `.` or `..`, would run a script from outside the hooks directory.
|
||||
|
||||
cat >"$fake_home/.config/omarchy/hooks/test-hook" <<'SH'
|
||||
touch "$HOME/hook-ran"
|
||||
SH
|
||||
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook" test-hook
|
||||
[[ -f $fake_home/hook-ran ]] ||
|
||||
fail "omarchy hook runs a named hook from the hooks directory"
|
||||
pass "omarchy hook runs a named hook from the hooks directory"
|
||||
|
||||
# Dots inside a name are not a path. a..b stays inside the hooks directory.
|
||||
cat >"$fake_home/.config/omarchy/hooks/a..b" <<'SH'
|
||||
touch "$HOME/dotted-hook-ran"
|
||||
SH
|
||||
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook" a..b
|
||||
[[ -f $fake_home/dotted-hook-ran ]] ||
|
||||
fail "omarchy hook accepts a hook name with dots in the middle"
|
||||
pass "omarchy hook accepts a hook name with dots in the middle"
|
||||
|
||||
for name in . ..; do
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "$name" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy hook refuses a hook name of $name" "exit: $status"
|
||||
pass "omarchy hook refuses a hook name of $name"
|
||||
done
|
||||
|
||||
# This file sits where a name of ../../evil would resolve: hooks/../.. is
|
||||
# ~/.config.
|
||||
cat >"$fake_home/.config/evil" <<'SH'
|
||||
touch "$HOME/escape-ran"
|
||||
SH
|
||||
chmod +x "$fake_home/.config/evil"
|
||||
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "../../evil" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy hook refuses a hook name with a dot-dot" "exit: $status"
|
||||
[[ ! -e $fake_home/escape-ran ]] ||
|
||||
fail "omarchy hook runs nothing when it refuses the name"
|
||||
pass "omarchy hook refuses a hook name with a dot-dot"
|
||||
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "sub/dir" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy hook refuses a hook name with a slash" "exit: $status"
|
||||
pass "omarchy hook refuses a hook name with a slash"
|
||||
|
||||
# --- omarchy-hook-install ------------------------------------------------------
|
||||
|
||||
# The installer joins the type into ~/.config/omarchy/hooks/<type>.d before
|
||||
# mkdir/cp. The runner already refuses a slashed type; install must too, or a
|
||||
# name the runner will not run still lands on disk.
|
||||
|
||||
source_hook="$work_dir/source-hook"
|
||||
cat >"$source_hook" <<'SH'
|
||||
#!/bin/bash
|
||||
true
|
||||
SH
|
||||
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" post-update "$source_hook" >/dev/null
|
||||
[[ -f $fake_home/.config/omarchy/hooks/post-update.d/source-hook ]] ||
|
||||
fail "omarchy hook install still installs a named hook"
|
||||
pass "omarchy hook install still installs a named hook"
|
||||
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" a..b "$source_hook" >/dev/null
|
||||
[[ -f $fake_home/.config/omarchy/hooks/a..b.d/source-hook ]] ||
|
||||
fail "omarchy hook install accepts a hook name with dots in the middle"
|
||||
pass "omarchy hook install accepts a hook name with dots in the middle"
|
||||
|
||||
for name in . ..; do
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "$name" "$source_hook" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy hook install refuses a hook name of $name" "exit: $status"
|
||||
[[ ! -e $fake_home/.config/omarchy/hooks/${name}.d ]] ||
|
||||
fail "omarchy hook install creates no directory for a hook name of $name"
|
||||
pass "omarchy hook install refuses a hook name of $name"
|
||||
done
|
||||
|
||||
# hooks/../../evil.d is ~/.config/evil.d. The guard must fire before mkdir.
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "../../evil" "$source_hook" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy hook install refuses a hook name with a dot-dot" "exit: $status"
|
||||
[[ ! -e $fake_home/.config/evil.d ]] ||
|
||||
fail "omarchy hook install creates nothing outside the hooks directory"
|
||||
pass "omarchy hook install refuses a hook name with a dot-dot"
|
||||
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "sub/dir" "$source_hook" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy hook install refuses a hook name with a slash" "exit: $status"
|
||||
[[ ! -e $fake_home/.config/omarchy/hooks/sub ]] ||
|
||||
fail "omarchy hook install creates no nested directory from a slashed name"
|
||||
pass "omarchy hook install refuses a hook name with a slash"
|
||||
|
||||
# --- omarchy-state -------------------------------------------------------------
|
||||
|
||||
state_dir="$fake_home/.local/state/omarchy"
|
||||
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-state" set reboot-required
|
||||
[[ -f $state_dir/reboot-required ]] ||
|
||||
fail "omarchy state set still creates a plain state file"
|
||||
pass "omarchy state set still creates a plain state file"
|
||||
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-state" set v1..2
|
||||
[[ -f $state_dir/v1..2 ]] ||
|
||||
fail "omarchy state set accepts a state name with dots in the middle"
|
||||
pass "omarchy state set accepts a state name with dots in the middle"
|
||||
|
||||
for name in . ..; do
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "$name" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy state set refuses a state name of $name" "exit: $status"
|
||||
pass "omarchy state set refuses a state name of $name"
|
||||
done
|
||||
|
||||
# state/../.. is ~/.local. The guard must fire before touch gets there.
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "../../escape" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy state set refuses a state name with a dot-dot" "exit: $status"
|
||||
[[ ! -e $fake_home/.local/escape ]] ||
|
||||
fail "omarchy state set creates nothing outside the state directory"
|
||||
pass "omarchy state set refuses a state name with a dot-dot"
|
||||
|
||||
status=0
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "sub/dir" >/dev/null 2>&1 || status=$?
|
||||
(( status == 2 )) ||
|
||||
fail "omarchy state set refuses a state name with a slash" "exit: $status"
|
||||
pass "omarchy state set refuses a state name with a slash"
|
||||
|
||||
# clear takes patterns by design ("state-name-or-pattern") and matches
|
||||
# basenames through find -name, so it can never walk out of the directory.
|
||||
touch "$state_dir/restart-a-required" "$state_dir/restart-b-required" "$state_dir/keep-me"
|
||||
HOME="$fake_home" "$ROOT/bin/omarchy-state" clear "restart-*-required"
|
||||
[[ ! -e $state_dir/restart-a-required && ! -e $state_dir/restart-b-required && -f $state_dir/keep-me ]] ||
|
||||
fail "omarchy state clear still clears matching patterns only"
|
||||
pass "omarchy state clear still clears matching patterns only"
|
||||
@@ -4,15 +4,16 @@ source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command lua
|
||||
|
||||
# The console is sized by the gap underneath it, recomputed from the monitor,
|
||||
# The console is sized by the gaps around it, recomputed from the monitor,
|
||||
# because a window rule's size would freeze at whatever the screen measured when
|
||||
# the console first opened. The arithmetic is what keeps it half a screen on a
|
||||
# scaled display, so it is worth pinning down.
|
||||
# the console first opened. The arithmetic is what keeps it a half-height panel
|
||||
# on a scaled display, so it is worth pinning down.
|
||||
# base-test.sh does not set -e, so the assertions have to fail the file
|
||||
# themselves rather than leaving the pass below to run regardless.
|
||||
OMARCHY_PATH="$ROOT" lua - <<'LUA' || fail "the console covers half the work area at any monitor scale"
|
||||
OMARCHY_PATH="$ROOT" lua - <<'LUA' || fail "the console is a centered panel until a second app joins it"
|
||||
local rules, handlers = {}, {}
|
||||
local monitor = nil
|
||||
local workspace = nil
|
||||
|
||||
hl = {
|
||||
config = function() end,
|
||||
@@ -20,6 +21,16 @@ hl = {
|
||||
workspace_rule = function(rule) table.insert(rules, rule) end,
|
||||
on = function(event, callback) handlers[event] = callback end,
|
||||
get_active_monitor = function() return monitor end,
|
||||
get_workspace = function() return workspace end,
|
||||
-- workspace.windows is the tiled count here and workspace.floats the floating
|
||||
-- one, so the fixtures can say which kind of window is on the console.
|
||||
get_workspace_windows = function()
|
||||
local list = {}
|
||||
for _ = 1, workspace and workspace.windows or 0 do table.insert(list, { floating = false }) end
|
||||
for _ = 1, workspace and workspace.floats or 0 do table.insert(list, { floating = true }) end
|
||||
return list
|
||||
end,
|
||||
exec_scheduled_prop_refresh_immediately = function() end,
|
||||
}
|
||||
|
||||
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
|
||||
@@ -29,6 +40,11 @@ local function current()
|
||||
return rules[#rules]
|
||||
end
|
||||
|
||||
local function gaps()
|
||||
local g = current().gaps_out
|
||||
return g.top, g.right, g.bottom, g.left
|
||||
end
|
||||
|
||||
-- Config loads before the outputs are up, so the first pass has no monitor to
|
||||
-- read. It still has to leave a rule behind, or the console would open unseeded.
|
||||
assert(#rules > 0, "console is ruled even before a monitor can be read")
|
||||
@@ -38,7 +54,7 @@ assert(current().on_created_empty:find("^%[workspace special:scratchpad silent%]
|
||||
assert(current().workspace == "special:scratchpad")
|
||||
|
||||
local function rescale(height, scale, bar)
|
||||
monitor = { height = height, scale = scale, reserved = { top = bar, bottom = 0, left = 0, right = 0 } }
|
||||
monitor = { width = 1920, height = height, scale = scale, transform = 0, reserved = { top = bar, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
return current().gaps_out.bottom
|
||||
end
|
||||
@@ -52,11 +68,9 @@ assert(rescale(2160, 1.5, 40) == 700, "and at a fractional scale")
|
||||
-- console short.
|
||||
assert(rescale(1440, 1, 0) == 720, "a monitor with nothing reserved")
|
||||
|
||||
-- The console stays flush with the top and the sides, the way a Quake console
|
||||
-- drops in, and keeps its seed across every refit.
|
||||
local final = current()
|
||||
assert(final.gaps_out.top == 0 and final.gaps_out.left == 0 and final.gaps_out.right == 0,
|
||||
"the console is flush to the top and sides")
|
||||
assert(final.gaps_out.top == 0, "the console stays flush with the top, the way a drop-down arrives")
|
||||
assert(final.gaps_out.left == final.gaps_out.right, "the panel is centered")
|
||||
assert(final.on_created_empty:find("omarchy%-agent"), "refitting keeps the console seeded")
|
||||
assert(final.no_border == true, "the console drops the active window border")
|
||||
|
||||
@@ -69,22 +83,197 @@ assert(current().gaps_out.bottom == before, "an absent monitor leaves the consol
|
||||
-- A monitor handle outliving its output answers nil to everything, which is
|
||||
-- what a layout change looks like mid-flight. Reading height or reserved off
|
||||
-- that would throw, so the scale guard has to catch it first.
|
||||
monitor = setmetatable({}, { __index = function() return nil end })
|
||||
local expired = setmetatable({}, { __index = function() return nil end })
|
||||
monitor = expired
|
||||
handlers["monitor.layout_changed"]()
|
||||
assert(current().gaps_out.bottom == before, "an expired monitor handle is not read to pieces")
|
||||
|
||||
-- Refitting to the size it already is would still cost a state refresh, and
|
||||
-- monitor.focused fires on every hop between screens.
|
||||
monitor = { height = 1440, scale = 1, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
||||
monitor = { width = 2560, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
local written = #rules
|
||||
handlers["monitor.focused"]()
|
||||
handlers["monitor.layout_changed"]()
|
||||
assert(#rules == written, "refitting to the same size does not rewrite the rule")
|
||||
|
||||
monitor.scale = 2
|
||||
-- A centered 2:1 panel, not a full-width drop-down.
|
||||
monitor = { width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
assert(#rules == written + 1, "a real change still rewrites it")
|
||||
assert(current().gaps_out.bottom == 360, "and lands on half the rescaled screen")
|
||||
local top, right, bottom, left = gaps()
|
||||
assert(top == 0 and left == 420 and right == 420 and bottom == 540, "16:9 leaves a 1080x540 panel")
|
||||
|
||||
local dell = { name = "DP-1", width = 6144, height = 2560, scale = 1, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
||||
monitor = dell
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 1807 and right == 1807 and bottom == 1265, "the same 2:1 panel on 6K")
|
||||
|
||||
-- Same logical box at scale 2x (physical 12288x5120): scale does not change the
|
||||
-- panel's logical size.
|
||||
monitor = { name = "DP-1", width = 12288, height = 5120, scale = 2, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 1807 and right == 1807 and bottom == 1265, "the 6K box is in logical pixels")
|
||||
|
||||
-- Same height, different width: the sides have to move even though the bottom
|
||||
-- gap is identical, so the cache cannot key on height alone.
|
||||
monitor = { width = 2560, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
written = #rules
|
||||
monitor = { width = 3440, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
assert(#rules == written + 1, "a same-height ultrawide hop still rewrites the sides")
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 1000 and right == 1000 and bottom == 720, "3440x1440 leaves a 1440x720 box")
|
||||
|
||||
-- A panel wider than the screen is just the screen: a portrait monitor has no
|
||||
-- room for a 2:1 box and falls back to the full width rather than a negative gap.
|
||||
monitor = { width = 1080, height = 1920, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 0 and right == 0 and bottom == 960, "a portrait monitor keeps the full width")
|
||||
assert(left >= 0 and right >= 0 and bottom >= 0, "gaps are never negative")
|
||||
|
||||
-- A monitor turned on its side still reports the panel's own pixels, so the
|
||||
-- work area has to be turned with it. Measured against Hyprland 0.56.2: a
|
||||
-- rotated 1920x1080 lays its windows out in 1080x1920 while width and height
|
||||
-- still read 1920 and 1080. Quarter turns are the odd transforms; a half turn
|
||||
-- leaves the shape alone.
|
||||
monitor = { width = 1920, height = 1080, scale = 1, transform = 1, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 0 and right == 0 and bottom == 945, "a quarter-turned monitor is sized portrait")
|
||||
|
||||
monitor = { width = 1920, height = 1080, scale = 1, transform = 3, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 0 and right == 0 and bottom == 945, "and so is the other quarter turn")
|
||||
|
||||
monitor = { width = 1920, height = 1080, scale = 1, transform = 2, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435 and bottom == 525, "a half turn is still landscape")
|
||||
|
||||
-- Special workspaces open on the monitor they are toggled on, not on whichever
|
||||
-- output was focused when the rule was last written. Opening on 1080p after a
|
||||
-- 6K fit has to resize the box.
|
||||
local acer = { name = "HDMI-A-1", width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
||||
monitor = dell
|
||||
handlers["monitor.layout_changed"]()
|
||||
handlers["workspace.special_active"]({ name = "special:scratchpad" }, acer)
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435 and bottom == 525, "opening on 1080p after a 6K fit resizes the box")
|
||||
assert(1920 - left - right > 0 and 1080 - 30 - bottom > 0, "1080p leftover is never negative")
|
||||
|
||||
-- follow_mouse onto the 6K while the console is already showing on 1080p must
|
||||
-- not steal the rule; that is what oversized the Dell after a hop.
|
||||
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
|
||||
monitor = dell
|
||||
written = #rules
|
||||
handlers["monitor.focused"](dell)
|
||||
assert(#rules == written, "focus on another output does not rewrite an open console")
|
||||
|
||||
-- The output the console is showing on can go away mid-layout-change. Its
|
||||
-- handle then answers nil to everything, and preferring it blindly would leave
|
||||
-- the console stranded at the gaps of the monitor that is gone. The rule is
|
||||
-- still the 1080p one here, so only refitting on the Dell can satisfy this.
|
||||
workspace = { name = "special:scratchpad", visible = true, monitor = expired, windows = 1 }
|
||||
monitor = dell
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 1807 and right == 1807 and bottom == 1265,
|
||||
"a console whose output vanished refits on the monitor that is still there")
|
||||
|
||||
-- Back onto the 1080p panel for the window-count checks below.
|
||||
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
|
||||
monitor = acer
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435 and bottom == 525, "and refits again once it is back on a live output")
|
||||
|
||||
-- One window reads as a console and keeps the panel. A second app has turned
|
||||
-- the scratchpad into a workspace, and a workspace wants the whole width.
|
||||
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 2 }
|
||||
handlers["window.open"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 0 and right == 0, "a second app on the scratchpad restores the full width")
|
||||
assert(bottom == 525, "and the console keeps its half-height drop")
|
||||
|
||||
workspace.windows = 3
|
||||
written = #rules
|
||||
handlers["window.open"]()
|
||||
assert(#rules == written, "a third app changes nothing that is already full width")
|
||||
|
||||
workspace.windows = 1
|
||||
handlers["window.destroy"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435, "closing back down to one window recenters the panel")
|
||||
|
||||
-- An empty scratchpad is about to be seeded with a single agent, so it is sized
|
||||
-- as a console rather than as a workspace.
|
||||
workspace.windows = 0
|
||||
handlers["window.destroy"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435, "an empty console is still a console")
|
||||
|
||||
-- A hidden console is refitted on its way back in, so the count does not have to
|
||||
-- be chased while it is off screen; every window on the desktop would otherwise
|
||||
-- rewrite the rule.
|
||||
workspace = { name = "special:scratchpad", visible = false, monitor = acer, windows = 4 }
|
||||
monitor = dell
|
||||
written = #rules
|
||||
handlers["window.open"]()
|
||||
assert(#rules == written, "a window opening elsewhere does not rewrite a hidden console")
|
||||
|
||||
-- A scratchpad nothing has opened yet has no workspace to read at all, and is
|
||||
-- sized as the console the seed is about to put a single agent into.
|
||||
workspace = nil
|
||||
monitor = { width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 420 and right == 420 and bottom == 540, "a scratchpad that does not exist yet is sized as a console")
|
||||
|
||||
-- Back to a console on screen for the move and float checks.
|
||||
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
|
||||
monitor = acer
|
||||
handlers["monitor.layout_changed"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435, "a single tiled window is a console")
|
||||
|
||||
-- A floating window on top of the console is not laid out by the gaps, so it
|
||||
-- must not stretch the panel out from under the agent.
|
||||
workspace.floats = 1
|
||||
handlers["window.open"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435, "a floating window on the console keeps the panel")
|
||||
|
||||
-- Tiling that float (Super+T) makes it a second app, and floating it again
|
||||
-- gives the panel back. Both arrive as window.update_rules.
|
||||
workspace.floats, workspace.windows = 0, 2
|
||||
handlers["window.update_rules"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 0 and right == 0, "tiling a float on the console restores the full width")
|
||||
|
||||
workspace.floats, workspace.windows = 1, 1
|
||||
handlers["window.update_rules"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435, "floating it again recenters the panel")
|
||||
|
||||
-- Sending an app onto the scratchpad (Super+Alt+S) or off it (Super+Shift+1)
|
||||
-- does not open or destroy anything, so the move itself has to refit.
|
||||
workspace.floats, workspace.windows = 0, 2
|
||||
handlers["window.move_to_workspace"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 0 and right == 0, "moving a second app onto the console restores the full width")
|
||||
|
||||
workspace.windows = 1
|
||||
handlers["window.move_to_workspace"]()
|
||||
top, right, bottom, left = gaps()
|
||||
assert(left == 435 and right == 435, "moving it back off recenters the panel")
|
||||
|
||||
-- window.close still counts the window on its way out, and window.destroy
|
||||
-- follows it with the settled count, so only destroy is hooked.
|
||||
assert(handlers["window.close"] == nil, "window.close counts the window on its way out")
|
||||
LUA
|
||||
pass "the console covers half the work area at any monitor scale"
|
||||
pass "the console is a centered panel until a second app joins it"
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
tmp_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
mkdir -p "$tmp_dir/bin"
|
||||
export INSTALLED_PACKAGES="$tmp_dir/installed" CALL_LOG="$tmp_dir/calls"
|
||||
export PATH="$tmp_dir/bin:$ROOT/bin:$PATH"
|
||||
|
||||
# Keep the real package helpers, but contain every pacman transaction here.
|
||||
cat > "$tmp_dir/bin/pacman" <<'SH'
|
||||
#!/bin/bash
|
||||
case "$1" in
|
||||
-Q) grep -Fxq -- "$2" "$INSTALLED_PACKAGES" ;;
|
||||
-S)
|
||||
[[ ${FAIL_INSTALL:-0} == 0 ]] || exit 1
|
||||
shift 3 # -S --noconfirm --needed
|
||||
printf '%s\n' "$@" >> "$INSTALLED_PACKAGES"
|
||||
printf '%s\n' "$@" >> "$CALL_LOG"
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
SH
|
||||
cat > "$tmp_dir/bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $1 == "pacman" ]] || exit 1
|
||||
"$@"
|
||||
SH
|
||||
chmod +x "$tmp_dir/bin/"*
|
||||
|
||||
migration="$ROOT/migrations/1789444024.sh"
|
||||
for kernels in linux-omarchy linux-t2 'linux-omarchy linux-t2'; do
|
||||
read -ra installed <<< "$kernels"
|
||||
printf '%s\n' linux linux-headers "${installed[@]}" > "$INSTALLED_PACKAGES"
|
||||
: > "$CALL_LOG"
|
||||
bash -euo pipefail "$migration" >/dev/null
|
||||
for kernel in "${installed[@]}"; do
|
||||
grep -Fxq "$kernel-headers" "$INSTALLED_PACKAGES" || fail "$kernel gets its headers"
|
||||
done
|
||||
: > "$CALL_LOG"
|
||||
bash -euo pipefail "$migration" >/dev/null
|
||||
[[ ! -s $CALL_LOG ]] || fail "header repair is idempotent"
|
||||
pass "missing headers are repaired once for $kernels"
|
||||
done
|
||||
|
||||
printf '%s\n' linux linux-aarch64 > "$INSTALLED_PACKAGES"
|
||||
: > "$CALL_LOG"
|
||||
bash -euo pipefail "$migration" >/dev/null
|
||||
[[ ! -s $CALL_LOG ]] || fail "header repair skips unrelated kernels"
|
||||
pass "header repair skips unrelated kernels"
|
||||
|
||||
echo linux-omarchy > "$INSTALLED_PACKAGES"
|
||||
if FAIL_INSTALL=1 bash -euo pipefail "$migration" >/dev/null; then
|
||||
fail "a failed header installation must leave the migration pending"
|
||||
fi
|
||||
pass "header installation failure is propagated"
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1788848726.sh"
|
||||
fixture="$ROOT/test/shell.d/fixtures/legacy-icon-font/omarchy.ttf"
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
export FONT_TEST_HOME="$test_dir/home with spaces"
|
||||
export FONT_TEST_PACKAGE="$test_dir/packaged-font.ttf"
|
||||
export FONT_TEST_CACHE_LOG="$test_dir/cache.log"
|
||||
legacy_font="$FONT_TEST_HOME/.local/share/fonts/omarchy.ttf"
|
||||
|
||||
# Redirect only the filesystem roots; run the real hash check and removal.
|
||||
# Never change the developer's HOME or refresh their real font cache.
|
||||
python3 - "$migration" "$test_dir/migration.sh" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
source = pathlib.Path(sys.argv[1]).read_text()
|
||||
source = source.replace('$HOME', '$FONT_TEST_HOME')
|
||||
source = source.replace('/usr/share/fonts/omarchy/omarchy.ttf', '$FONT_TEST_PACKAGE')
|
||||
pathlib.Path(sys.argv[2]).write_text(source)
|
||||
PY
|
||||
|
||||
mkdir -p "$test_dir/bin"
|
||||
cat > "$test_dir/bin/fc-cache" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >> "$FONT_TEST_CACHE_LOG"
|
||||
exit "${FONT_TEST_CACHE_STATUS:-0}"
|
||||
SH
|
||||
chmod +x "$test_dir/bin/fc-cache"
|
||||
|
||||
reset_fonts() {
|
||||
rm -rf "$FONT_TEST_HOME"
|
||||
mkdir -p "$FONT_TEST_HOME/.local/share/fonts" "$FONT_TEST_HOME/.config"
|
||||
cp "$ROOT/default/fonts/omarchy/omarchy.ttf" "$FONT_TEST_PACKAGE"
|
||||
: > "$FONT_TEST_CACHE_LOG"
|
||||
}
|
||||
|
||||
run_migration() {
|
||||
PATH="$test_dir/bin:$PATH" bash -euo pipefail "$test_dir/migration.sh" > "$test_dir/output" 2>&1
|
||||
}
|
||||
|
||||
reset_fonts
|
||||
cp "$fixture" "$legacy_font"
|
||||
cp "$fixture" "$FONT_TEST_HOME/.config/omarchy.ttf"
|
||||
run_migration
|
||||
[[ ! -e $legacy_font ]] || fail "stock font is removed from the actual user font directory"
|
||||
cmp "$fixture" "$FONT_TEST_HOME/.config/omarchy.ttf" || fail "unrelated config path is untouched"
|
||||
cmp "$ROOT/default/fonts/omarchy/omarchy.ttf" "$FONT_TEST_PACKAGE" || fail "packaged font is untouched"
|
||||
[[ $(cat "$FONT_TEST_CACHE_LOG") == "-f" ]] || fail "font cache is refreshed after retirement"
|
||||
pass "retire the known stock font at its real path and refresh the cache"
|
||||
|
||||
run_migration
|
||||
[[ ! -e $legacy_font ]] || fail "a second run leaves the stock font retired"
|
||||
pass "font retirement is idempotent"
|
||||
|
||||
reset_fonts
|
||||
cp "$fixture" "$legacy_font"
|
||||
printf 'custom modification\n' >> "$legacy_font"
|
||||
cp "$legacy_font" "$test_dir/custom-font.ttf"
|
||||
run_migration
|
||||
cmp "$test_dir/custom-font.ttf" "$legacy_font" || fail "custom font is preserved"
|
||||
pass "preserve a modified font with the legacy filename"
|
||||
|
||||
reset_fonts
|
||||
cp "$fixture" "$test_dir/symlink-target.ttf"
|
||||
ln -s "$test_dir/symlink-target.ttf" "$legacy_font"
|
||||
run_migration
|
||||
[[ -L $legacy_font ]] || fail "user font symlink is preserved"
|
||||
cmp "$fixture" "$test_dir/symlink-target.ttf" || fail "symlink target is untouched"
|
||||
pass "preserve user font symlinks even when they point to the stock font"
|
||||
|
||||
reset_fonts
|
||||
run_migration
|
||||
[[ ! -e $legacy_font ]] || fail "an absent user font is left absent"
|
||||
pass "handle installs without a legacy user font"
|
||||
|
||||
reset_fonts
|
||||
cp "$fixture" "$legacy_font"
|
||||
rm "$FONT_TEST_PACKAGE"
|
||||
if run_migration; then
|
||||
fail "missing packaged font keeps the repair pending"
|
||||
fi
|
||||
cmp "$fixture" "$legacy_font" || fail "keep the stock font until its replacement is present"
|
||||
[[ ! -s $FONT_TEST_CACHE_LOG ]] || fail "missing replacement stops before cache refresh"
|
||||
pass "preserve the stock font and fail when the packaged replacement is missing"
|
||||
|
||||
reset_fonts
|
||||
cp "$fixture" "$legacy_font"
|
||||
if FONT_TEST_CACHE_STATUS=17 run_migration; then
|
||||
fail "font cache failure keeps the repair pending"
|
||||
fi
|
||||
[[ ! -e $legacy_font ]] || fail "cache failure follows successful retirement"
|
||||
run_migration
|
||||
(( $(wc -l < "$FONT_TEST_CACHE_LOG") == 2 )) || fail "retry refreshes the cache after the file was removed"
|
||||
pass "retry a failed cache refresh after successful font retirement"
|
||||
|
||||
if grep -q $'^retire\tomarchy.ttf\t' "$ROOT/bin/omarchy-upgrade-to-quattro"; then
|
||||
fail "upgrader no longer treats the user font as a config file"
|
||||
fi
|
||||
pass "upgrader leaves font retirement to its post-upgrade migrations"
|
||||
@@ -9,3 +9,7 @@ packaged_defaults="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"
|
||||
grep -Fq 'KERNEL_CMDLINE[default]+=" initramfs_async=0"' "$packaged_defaults" ||
|
||||
fail "the packaged Limine defaults still unpack the initramfs synchronously"
|
||||
pass "packaged Limine defaults keep Plymouth alive at the LUKS prompt"
|
||||
|
||||
grep -Fxq 'BOOT_ORDER="linux-t2, linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"' "$packaged_defaults" ||
|
||||
fail "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere"
|
||||
pass "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere"
|
||||
@@ -641,5 +641,5 @@ assert(
|
||||
JS
|
||||
|
||||
font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf")
|
||||
[[ $font_charset == *"e900-e90d"* ]] || fail "Omarchy icon font includes every custom menu glyph"
|
||||
[[ $font_charset == *"e900-e90e"* ]] || fail "Omarchy icon font includes every custom menu glyph"
|
||||
pass "Omarchy icon font includes the official agent marks"
|
||||
@@ -0,0 +1,270 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command mise
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
migration="$ROOT/migrations/1789095456.sh"
|
||||
|
||||
run_migration() {
|
||||
local test_home="$1"
|
||||
|
||||
env -i \
|
||||
HOME="$test_home" \
|
||||
XDG_CACHE_HOME="$test_home/.cache" \
|
||||
XDG_CONFIG_HOME="$test_home/.config" \
|
||||
XDG_DATA_HOME="$test_home/.local/share" \
|
||||
XDG_STATE_HOME="$test_home/.local/state" \
|
||||
MISE_PARANOID="${OMARCHY_TEST_MISE_PARANOID:-false}" \
|
||||
PATH=/usr/bin \
|
||||
bash -euo pipefail "$migration"
|
||||
}
|
||||
|
||||
run_mise() {
|
||||
local test_home="$1"
|
||||
shift
|
||||
|
||||
env -i \
|
||||
HOME="$test_home" \
|
||||
XDG_CACHE_HOME="$test_home/.cache" \
|
||||
XDG_CONFIG_HOME="$test_home/.config" \
|
||||
XDG_DATA_HOME="$test_home/.local/share" \
|
||||
XDG_STATE_HOME="$test_home/.local/state" \
|
||||
MISE_PARANOID="${OMARCHY_TEST_MISE_PARANOID:-false}" \
|
||||
PATH=/usr/bin \
|
||||
mise "$@"
|
||||
}
|
||||
|
||||
mise_environment() {
|
||||
local test_home="$1"
|
||||
local project="$2"
|
||||
|
||||
(
|
||||
cd "$project"
|
||||
run_mise "$test_home" env -s bash
|
||||
)
|
||||
}
|
||||
|
||||
mise_path_active() {
|
||||
local test_home="$1"
|
||||
local project="$2"
|
||||
local output
|
||||
|
||||
if ! output=$(mise_environment "$test_home" "$project" 2>/dev/null); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
grep -F "$project/bin" <<<"$output" >/dev/null
|
||||
}
|
||||
|
||||
assert_unsafe_variant_removed() {
|
||||
local variant="$1"
|
||||
local assignment="$2"
|
||||
local variant_home="$test_dir/$variant-home"
|
||||
local variant_config="$variant_home/Work/.mise.toml"
|
||||
local variant_project="$variant_home/Work/tries/untrusted-repository"
|
||||
|
||||
mkdir -p "$variant_project/bin"
|
||||
printf '[env]\n%s\n' "$assignment" >"$variant_config"
|
||||
run_mise "$variant_home" trust "$variant_config" >/dev/null
|
||||
|
||||
mise_path_active "$variant_home" "$variant_project" || fail "$variant legacy config prepends the repository bin directory"
|
||||
|
||||
run_migration "$variant_home" >/dev/null
|
||||
if mise_path_active "$variant_home" "$variant_project"; then
|
||||
fail "$variant repository bin directory remains in PATH after migration"
|
||||
fi
|
||||
}
|
||||
|
||||
install_home="$test_dir/install-home"
|
||||
install_log="$test_dir/install-mise.log"
|
||||
mkdir -p "$install_home" "$test_dir/bin"
|
||||
cat >"$test_dir/bin/mise" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$MISE_TEST_LOG"
|
||||
SH
|
||||
chmod +x "$test_dir/bin/mise"
|
||||
|
||||
env \
|
||||
HOME="$install_home" \
|
||||
MISE_TEST_LOG="$install_log" \
|
||||
OMARCHY_SETUP_CONTEXT=runtime \
|
||||
PATH="$test_dir/bin:/usr/bin" \
|
||||
bash -euo pipefail -c 'source "$1"' bash "$ROOT/install/user/mise-work.sh"
|
||||
|
||||
[[ -d $install_home/Work/tries ]] || fail "installer creates the work and tries directories"
|
||||
[[ ! -e $install_home/Work/.mise.toml ]] || fail "installer does not create a trusted Work Mise config"
|
||||
[[ $(<"$install_log") == "use -g node@latest" ]] || fail "installer only invokes Mise for the global Node setup"
|
||||
pass "new installs do not add project bin directories to PATH"
|
||||
|
||||
stock_home="$test_dir/stock-home"
|
||||
stock_config="$stock_home/Work/.mise.toml"
|
||||
stock_project="$stock_home/Work/tries/untrusted-repository"
|
||||
mkdir -p "$stock_project/bin"
|
||||
cat >"$stock_config" <<'TOML'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
TOML
|
||||
run_mise "$stock_home" trust "$stock_config" >/dev/null
|
||||
|
||||
mise_path_active "$stock_home" "$stock_project" || fail "legacy config prepends the repository bin directory"
|
||||
|
||||
run_migration "$stock_home" >/dev/null
|
||||
[[ ! -e $stock_config ]] || fail "migration removes the stock Work Mise config"
|
||||
cat >"$stock_config" <<'TOML'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
TOML
|
||||
if mise_path_active "$stock_home" "$stock_project"; then
|
||||
fail "recreated Work config remains trusted after migration"
|
||||
fi
|
||||
run_migration "$stock_home" >/dev/null
|
||||
[[ ! -e $stock_config ]] || fail "stock migration is idempotent"
|
||||
pass "migration removes the repository bin directory and revokes the Work trust root"
|
||||
|
||||
assert_unsafe_variant_removed inline-comment '_.path = "{{ cwd }}/bin" # Omarchy default'
|
||||
assert_unsafe_variant_removed single-quoted "_.path = '{{ cwd }}/bin'"
|
||||
pass "migration removes annotated and single-quoted project bin paths"
|
||||
|
||||
custom_home="$test_dir/custom-home"
|
||||
custom_config="$custom_home/Work/.mise.toml"
|
||||
mkdir -p "$(dirname "$custom_config")"
|
||||
cat >"$custom_config" <<'TOML'
|
||||
[env]
|
||||
KEEP = "yes"
|
||||
_.path = "{{ cwd }}/bin"
|
||||
# _.path = "{{ cwd }}/bin"
|
||||
|
||||
[tools]
|
||||
ruby = "latest"
|
||||
|
||||
[other]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
TOML
|
||||
cp "$custom_config" "$test_dir/custom-original"
|
||||
cat >"$test_dir/custom-expected" <<'TOML'
|
||||
[env]
|
||||
KEEP = "yes"
|
||||
# _.path = "{{ cwd }}/bin"
|
||||
|
||||
[tools]
|
||||
ruby = "latest"
|
||||
|
||||
[other]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
TOML
|
||||
chmod 600 "$custom_config"
|
||||
run_mise "$custom_home" trust "$custom_config" >/dev/null
|
||||
|
||||
custom_output=$(run_migration "$custom_home")
|
||||
cmp -s "$test_dir/custom-expected" "$custom_config" || fail "migration preserves unrelated custom Mise settings"
|
||||
[[ $(stat -c %a "$custom_config") == "600" ]] || fail "migration preserves custom config permissions"
|
||||
grep -F "mise trust $custom_config" <<<"$custom_output" >/dev/null || fail "migration explains how to review and re-trust a custom config"
|
||||
custom_backups=("$custom_config".bak.*)
|
||||
[[ -f ${custom_backups[0]} ]] || fail "migration backs up a customized Mise config"
|
||||
(( ${#custom_backups[@]} == 1 )) || fail "migration creates one custom config backup"
|
||||
cmp -s "$test_dir/custom-original" "${custom_backups[0]}" || fail "custom config backup preserves the original"
|
||||
|
||||
run_migration "$custom_home" >/dev/null
|
||||
custom_backups=("$custom_config".bak.*)
|
||||
(( ${#custom_backups[@]} == 1 )) || fail "custom migration does not create another backup on rerun"
|
||||
cmp -s "$test_dir/custom-expected" "$custom_config" || fail "custom migration is idempotent"
|
||||
pass "custom Mise settings, permissions, and original backup survive the repair"
|
||||
|
||||
unrelated_home="$test_dir/unrelated-home"
|
||||
unrelated_config="$unrelated_home/Work/.mise.toml"
|
||||
unrelated_project="$unrelated_home/Work/tries/untrusted-repository"
|
||||
mkdir -p "$unrelated_project/bin"
|
||||
printf '[env]\nKEEP = "yes"\n' >"$unrelated_config"
|
||||
cp "$unrelated_config" "$test_dir/unrelated-original"
|
||||
run_mise "$unrelated_home" trust "$unrelated_config" >/dev/null
|
||||
run_migration "$unrelated_home" >/dev/null
|
||||
cmp -s "$test_dir/unrelated-original" "$unrelated_config" || fail "unrelated Mise config remains unchanged"
|
||||
unrelated_backups=("$unrelated_config".bak.*)
|
||||
[[ ! -e ${unrelated_backups[0]} ]] || fail "unchanged Mise config is not backed up"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$unrelated_config"
|
||||
if mise_path_active "$unrelated_home" "$unrelated_project"; then
|
||||
fail "safe Work config retains its old trust grant"
|
||||
fi
|
||||
|
||||
absent_home="$test_dir/absent-home"
|
||||
absent_config="$absent_home/Work/.mise.toml"
|
||||
absent_project="$absent_home/Work/tries/untrusted-repository"
|
||||
mkdir -p "$(dirname "$absent_config")"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$absent_config"
|
||||
run_mise "$absent_home" trust "$absent_config" >/dev/null
|
||||
rm "$absent_config"
|
||||
rmdir "$absent_home/Work"
|
||||
run_migration "$absent_home" >/dev/null
|
||||
[[ ! -e $absent_home/Work ]] || fail "migration does not retain a temporary Work directory"
|
||||
mkdir -p "$absent_project/bin"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$absent_config"
|
||||
if mise_path_active "$absent_home" "$absent_project"; then
|
||||
fail "deleted Work directory retains its stale trust grant"
|
||||
fi
|
||||
pass "migration leaves unrelated configs alone and revokes dangling Work trust"
|
||||
|
||||
paranoid_home="$test_dir/paranoid-home"
|
||||
paranoid_work="$paranoid_home/Work"
|
||||
paranoid_config="$paranoid_work/.mise.toml"
|
||||
paranoid_project="$paranoid_work/tries/untrusted-repository"
|
||||
mkdir -p "$paranoid_project/bin"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$paranoid_config"
|
||||
OMARCHY_TEST_MISE_PARANOID=true run_mise "$paranoid_home" trust "$paranoid_config" >/dev/null
|
||||
OMARCHY_TEST_MISE_PARANOID=true mise_path_active "$paranoid_home" "$paranoid_project" || fail "paranoid legacy config prepends the repository bin directory"
|
||||
rm -r "$paranoid_work"
|
||||
|
||||
OMARCHY_TEST_MISE_PARANOID=true run_migration "$paranoid_home" >/dev/null
|
||||
[[ ! -e $paranoid_work ]] || fail "paranoid migration removes its temporary Work directory"
|
||||
mkdir -p "$paranoid_project/bin"
|
||||
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$paranoid_config"
|
||||
if OMARCHY_TEST_MISE_PARANOID=true mise_path_active "$paranoid_home" "$paranoid_project"; then
|
||||
fail "paranoid migration retains content-bound trust for the deleted legacy config"
|
||||
fi
|
||||
pass "migration revokes stale content-bound trust in Mise paranoid mode"
|
||||
|
||||
ignored_home="$test_dir/ignored-home"
|
||||
ignored_config="$ignored_home/Work/.mise.toml"
|
||||
ignored_project="$ignored_home/Work/tries/untrusted-repository"
|
||||
mkdir -p "$ignored_project/bin"
|
||||
cat >"$ignored_config" <<'TOML'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
KEEP = "yes"
|
||||
TOML
|
||||
run_mise "$ignored_home" trust "$ignored_config" >/dev/null
|
||||
run_mise "$ignored_home" trust --ignore "$ignored_config" >/dev/null
|
||||
|
||||
ignored_output=$(run_migration "$ignored_home")
|
||||
grep -F '{{ cwd }}/bin' "$ignored_config" >/dev/null && fail "ignored config retains the unsafe path"
|
||||
grep -Fx 'KEEP = "yes"' "$ignored_config" >/dev/null || fail "ignored config keeps unrelated settings"
|
||||
ignored_entries=("$ignored_home/.local/state/mise/ignored-configs/"*)
|
||||
[[ -L ${ignored_entries[0]} ]] || fail "migration preserves the explicit Mise ignore marker"
|
||||
(( ${#ignored_entries[@]} == 1 )) || fail "migration preserves exactly one Mise ignore marker"
|
||||
ignored_target=$(readlink "${ignored_entries[0]}")
|
||||
[[ $ignored_target == $ignored_home/Work || $ignored_target == $ignored_config ]] || fail "preserved Mise ignore marker still targets the Work config"
|
||||
grep -F "remains ignored by Mise" <<<"$ignored_output" >/dev/null || fail "migration reports that the custom config remains ignored"
|
||||
if mise_path_active "$ignored_home" "$ignored_project"; then
|
||||
fail "ignored config becomes active after migration"
|
||||
fi
|
||||
pass "migration preserves an explicit decision to ignore the Work config"
|
||||
|
||||
symlink_home="$test_dir/symlink-home"
|
||||
symlink_config="$symlink_home/Work/.mise.toml"
|
||||
symlink_target="$test_dir/dotfiles-mise.toml"
|
||||
mkdir -p "$(dirname "$symlink_config")"
|
||||
cat >"$symlink_target" <<'TOML'
|
||||
[env]
|
||||
_.path = "{{ cwd }}/bin"
|
||||
KEEP = "yes"
|
||||
TOML
|
||||
ln -s "$symlink_target" "$symlink_config"
|
||||
|
||||
run_migration "$symlink_home" >/dev/null
|
||||
[[ -L $symlink_config ]] || fail "migration preserves a dotfile symlink"
|
||||
grep -F '{{ cwd }}/bin' "$symlink_target" >/dev/null && fail "symlink target retains the unsafe path"
|
||||
grep -Fx 'KEEP = "yes"' "$symlink_target" >/dev/null || fail "symlink target keeps unrelated settings"
|
||||
pass "custom dotfile symlinks survive the repair"
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1789325478.sh"
|
||||
scratch=$(mktemp -d)
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
mkdir -p "$scratch/bin" "$scratch/drop-ins"
|
||||
|
||||
export PATH="$scratch/bin:$ROOT/bin:$PATH"
|
||||
export CALL_LOG="$scratch/calls"
|
||||
export INSTALLED_PACKAGES="$scratch/packages"
|
||||
export OMARCHY_KERNEL_LIMINE_CONF="$scratch/limine"
|
||||
export OMARCHY_KERNEL_LIMINE_DROP_INS="$scratch/drop-ins"
|
||||
export OMARCHY_KERNEL_REBUILD_MARKER="$scratch/state/1789325478"
|
||||
kernel="linux-omarchy"
|
||||
boot_order='BOOT_ORDER="linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"'
|
||||
|
||||
# Exercise the real package helpers, including their post-install queries.
|
||||
cat > "$scratch/bin/pacman" <<'SH'
|
||||
#!/bin/bash
|
||||
case "$1" in
|
||||
-Q) grep -Fxq "$2" "$INSTALLED_PACKAGES" ;;
|
||||
-S)
|
||||
printf 'pacman %s\n' "$*" >> "$CALL_LOG"
|
||||
[[ ${INSTALL_FAIL:-0} == "0" ]] || exit 1
|
||||
for arg in "$@"; do
|
||||
[[ $arg == -* ]] || printf '%s\n' "$arg" >> "$INSTALLED_PACKAGES"
|
||||
done
|
||||
;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
SH
|
||||
|
||||
cat > "$scratch/bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'sudo %s\n' "$*" >> "$CALL_LOG"
|
||||
case "$1" in
|
||||
pacman | mkdir | touch | sed | tee | install | limine-mkinitcpio | limine-entry-tool) exec "$@" ;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
SH
|
||||
|
||||
cat > "$scratch/bin/limine-mkinitcpio" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ ${REBUILD_FAIL:-0} == "0" ]]
|
||||
SH
|
||||
|
||||
cat > "$scratch/bin/limine-entry-tool" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $* == "--tree" ]] || exit 99
|
||||
printf '%s\n' 'Omarchy' ' linux-ptl' ' linux-omarchy-ptl-novrr-mm' ' linux-omarchy-bore' ' linux-omarchy-fallback' ' Snapshots'
|
||||
if [[ ${MISSING_ENTRY:-0} == "0" ]]; then
|
||||
printf '%s\n' ' linux-omarchy'
|
||||
fi
|
||||
SH
|
||||
|
||||
cat > "$scratch/bin/omarchy-state" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $* == "set reboot-required" ]] || exit 99
|
||||
printf 'state %s\n' "$*" >> "$CALL_LOG"
|
||||
SH
|
||||
|
||||
cat > "$scratch/bin/uname" <<'SH'
|
||||
#!/bin/bash
|
||||
case "$1" in
|
||||
-m) printf '%s\n' "${TEST_ARCH:-x86_64}" ;;
|
||||
-r) printf '%s\n' "${TEST_KERNEL_RELEASE:-7.2.5-arch1-1}" ;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
SH
|
||||
|
||||
cat > "$scratch/bin/omarchy-notification-dismiss" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$scratch/bin/"*
|
||||
|
||||
reset_fixture() {
|
||||
: > "$CALL_LOG"
|
||||
printf '%s\n' linux-ptl linux-ptl-headers > "$INSTALLED_PACKAGES"
|
||||
rm -f "$OMARCHY_KERNEL_REBUILD_MARKER" "$OMARCHY_KERNEL_LIMINE_DROP_INS/"*
|
||||
cat > "$OMARCHY_KERNEL_LIMINE_CONF" <<'CONF'
|
||||
KERNEL_CMDLINE[default]="root=UUID=keep-me rw cryptdevice=UUID=keep-me:root"
|
||||
BOOT_ORDER="*, *fallback, Snapshots"
|
||||
ENABLE_UKI=yes
|
||||
CONF
|
||||
cp "$OMARCHY_KERNEL_LIMINE_CONF" "$scratch/original-limine"
|
||||
}
|
||||
|
||||
run_migration() {
|
||||
bash -euo pipefail "$migration" > "$scratch/output" 2>&1
|
||||
}
|
||||
|
||||
assert_preferred() {
|
||||
grep -Fxq "$boot_order" "$1" || fail "Omarchy kernels are preferred in $1"
|
||||
}
|
||||
|
||||
assert_skipped() {
|
||||
[[ ! -s $CALL_LOG ]] || fail "excluded systems do not change" "$(<"$CALL_LOG")"
|
||||
cmp -s "$OMARCHY_KERNEL_LIMINE_CONF" "$scratch/original-limine" || fail "excluded systems keep their boot settings"
|
||||
[[ ! -e $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "excluded systems do not get a completion marker"
|
||||
}
|
||||
|
||||
for old_kernel in linux linux-lts linux-zen linux-ptl linux-omarchy-ptl-novrr-mm; do
|
||||
reset_fixture
|
||||
printf '%s\n' "$old_kernel" "$old_kernel-headers" > "$INSTALLED_PACKAGES"
|
||||
run_migration
|
||||
grep -Fxq "$kernel" "$INSTALLED_PACKAGES" || fail "the generic kernel is installed"
|
||||
grep -Fxq "$kernel-headers" "$INSTALLED_PACKAGES" || fail "its headers are installed"
|
||||
grep -Fxq "$old_kernel" "$INSTALLED_PACKAGES" || fail "the previous kernel remains available"
|
||||
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
|
||||
pass "$old_kernel systems receive the generic Omarchy kernel and retain their recovery kernel"
|
||||
done
|
||||
|
||||
for installed in linux-t2 $'linux-t2\nlinux\nlinux-ptl\nlinux-omarchy'; do
|
||||
reset_fixture
|
||||
printf '%s\n' "$installed" > "$INSTALLED_PACKAGES"
|
||||
run_migration
|
||||
assert_skipped
|
||||
done
|
||||
pass "linux-t2 systems are skipped even with other kernels installed"
|
||||
|
||||
reset_fixture
|
||||
TEST_KERNEL_RELEASE=7.2.5-arch1-T2 run_migration
|
||||
assert_skipped
|
||||
pass "a running T2 kernel is excluded even if its package is no longer installed"
|
||||
|
||||
reset_fixture
|
||||
TEST_ARCH=aarch64 run_migration
|
||||
assert_skipped
|
||||
pass "ARM systems cannot receive an x86_64 kernel"
|
||||
|
||||
reset_fixture
|
||||
printf '%s\n' linux-omarchy-ptl-novrr-mm > "$INSTALLED_PACKAGES"
|
||||
mkdir -p "$scratch/state" "$scratch/user-markers" "$scratch/omarchy/migrations"
|
||||
export OMARCHY_PTL_REBUILD_MARKER="$scratch/state/1789095456"
|
||||
export OMARCHY_MIGRATION_STATE="$scratch/user-markers"
|
||||
touch "$OMARCHY_PTL_REBUILD_MARKER" "$OMARCHY_MIGRATION_STATE/1789095456.sh"
|
||||
cp "$migration" "$scratch/omarchy/migrations/"
|
||||
[[ ! -e $ROOT/migrations/1789095456.sh ]] || fail "the superseded migration must not install the PTL variant first"
|
||||
pending=$(OMARCHY_PATH="$scratch/omarchy" "$ROOT/bin/omarchy-migrate" --pending)
|
||||
[[ $pending == "1789325478.sh" ]] || fail "the renamed migration is pending after completing the old migration"
|
||||
OMARCHY_PATH="$scratch/omarchy" "$ROOT/bin/omarchy-migrate" > "$scratch/output" 2>&1
|
||||
grep -Fxq "$kernel" "$INSTALLED_PACKAGES" || fail "the old completion markers cannot skip the generic kernel"
|
||||
[[ -f $OMARCHY_KERNEL_REBUILD_MARKER && -f $OMARCHY_MIGRATION_STATE/1789325478.sh ]] || fail "new machine and user completion markers are recorded"
|
||||
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
|
||||
pass "users who completed the PTL migration run the renamed migration with fresh completion markers"
|
||||
|
||||
reset_fixture
|
||||
for name in dell-xps-panther-lake zz-dell-xps-panther-lake; do
|
||||
printf '%s\n' 'BOOT_ORDER="linux-ptl*, *fallback, Snapshots"' > "$OMARCHY_KERNEL_LIMINE_DROP_INS/$name.conf"
|
||||
done
|
||||
cp "$OMARCHY_KERNEL_LIMINE_CONF" "$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf"
|
||||
run_migration
|
||||
grep -Fxq "pacman -S --noconfirm --needed $kernel $kernel-headers" "$CALL_LOG" || fail "both new packages are installed"
|
||||
grep -Fxq linux-ptl "$INSTALLED_PACKAGES" || fail "the old kernel is kept for recovery"
|
||||
grep -Fxq linux-ptl-headers "$INSTALLED_PACKAGES" || fail "the old kernel headers are kept"
|
||||
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
|
||||
diff -u <(sed '/^BOOT_ORDER=/d; /^$/d' "$scratch/original-limine") \
|
||||
<(sed '/^BOOT_ORDER=/d; /^$/d' "$OMARCHY_KERNEL_LIMINE_CONF") || fail "kernel command line and unrelated settings are preserved"
|
||||
grep -Fxq "sudo limine-mkinitcpio $kernel" "$CALL_LOG" || fail "the new kernel's boot image is rebuilt"
|
||||
[[ -f $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "successful completion is recorded"
|
||||
grep -Fxq 'state set reboot-required' "$CALL_LOG" || fail "the updater must offer a reboot when retaining the old kernel"
|
||||
pass "new packages install, the central boot order is set, and the old kernel remains available"
|
||||
|
||||
: > "$CALL_LOG"
|
||||
run_migration
|
||||
[[ ! -s $CALL_LOG ]] || fail "another user's run does not repeat the machine-wide migration"
|
||||
pass "repeat runs are a no-op after successful completion"
|
||||
|
||||
reset_fixture
|
||||
printf '%s\n' "$kernel" >> "$INSTALLED_PACKAGES"
|
||||
run_migration
|
||||
grep -Fxq "$kernel-headers" "$INSTALLED_PACKAGES" || fail "missing headers install when the kernel is already present"
|
||||
pass "a partially installed kernel gets its missing headers"
|
||||
|
||||
reset_fixture
|
||||
printf '%s\n' "$kernel" "$kernel-headers" >> "$INSTALLED_PACKAGES"
|
||||
run_migration
|
||||
! grep -q '^pacman -S' "$CALL_LOG" || fail "already installed packages are not reinstalled"
|
||||
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
|
||||
pass "existing new packages still receive the config repair and boot rebuild"
|
||||
|
||||
reset_fixture
|
||||
if INSTALL_FAIL=1 run_migration; then
|
||||
fail "package installation failure must fail the migration"
|
||||
fi
|
||||
cmp -s "$OMARCHY_KERNEL_LIMINE_CONF" "$scratch/original-limine" || fail "install failure leaves the config untouched"
|
||||
! grep -q 'limine-mkinitcpio' "$CALL_LOG" || fail "install failure does not rebuild"
|
||||
[[ ! -e $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "install failure stays pending"
|
||||
pass "package failures leave the old boot setup intact and the migration pending"
|
||||
|
||||
reset_fixture
|
||||
if REBUILD_FAIL=1 run_migration; then
|
||||
fail "boot rebuild failure must fail the migration"
|
||||
fi
|
||||
[[ ! -e $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "rebuild failure stays pending"
|
||||
! grep -q '^state ' "$CALL_LOG" || fail "rebuild failure must not request a reboot"
|
||||
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
|
||||
: > "$CALL_LOG"
|
||||
run_migration
|
||||
grep -Fxq "sudo limine-mkinitcpio $kernel" "$CALL_LOG" || fail "retry must rebuild even after config and packages are repaired"
|
||||
[[ -f $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "retry records successful completion"
|
||||
pass "a failed rebuild is retried even after config and package changes succeeded"
|
||||
|
||||
reset_fixture
|
||||
if MISSING_ENTRY=1 run_migration; then
|
||||
fail "a silently skipped kernel build must fail the migration"
|
||||
fi
|
||||
[[ ! -e $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "a missing boot entry stays pending"
|
||||
! grep -q '^state ' "$CALL_LOG" || fail "a missing boot entry must not request a reboot"
|
||||
run_migration
|
||||
[[ -f $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "a missing boot entry can be repaired on retry"
|
||||
pass "older Omarchy variants and fallback entries cannot satisfy generic kernel verification"
|
||||
|
||||
reset_fixture
|
||||
cat >> "$OMARCHY_KERNEL_LIMINE_CONF" <<'CONF'
|
||||
BOOT_ORDER="linux-omarchy-*, *, *fallback, Snapshots"
|
||||
BOOT_ORDER = 'linux-lts, *, *fallback, Snapshots'
|
||||
CONF
|
||||
run_migration
|
||||
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
|
||||
[[ $(grep -c '^BOOT_ORDER=' "$OMARCHY_KERNEL_LIMINE_CONF") == "1" ]] || fail "only one boot order is written"
|
||||
! grep -Eq '^[[:space:]]+BOOT_ORDER' "$OMARCHY_KERNEL_LIMINE_CONF" || fail "a later custom assignment cannot override first position"
|
||||
pass "the exact generic kernel takes first position over previous PTL and custom orders"
|
||||
|
||||
reset_fixture
|
||||
for name in omarchy-defaults dell-xps-panther-lake zz-dell-xps-panther-lake; do
|
||||
printf '%s\n' 'BOOT_ORDER="linux-ptl*, *fallback, Snapshots"' > "$OMARCHY_KERNEL_LIMINE_DROP_INS/$name.conf"
|
||||
done
|
||||
printf '%s\n' 'ENABLE_SORT=yes' >> "$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf"
|
||||
cp "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" "$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf.pacnew"
|
||||
run_migration
|
||||
effective_order=$(bash -c 'declare -A KERNEL_CMDLINE; for conf in "$1/"*.conf "$2"; do source "$conf"; done; printf "%s" "$BOOT_ORDER"' -- "$OMARCHY_KERNEL_LIMINE_DROP_INS" "$OMARCHY_KERNEL_LIMINE_CONF")
|
||||
[[ $effective_order == "linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots" ]] || fail "the central config wins over old and customized drop-ins"
|
||||
grep -Fxq 'ENABLE_SORT=yes' "$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf" || fail "custom packaged settings survive"
|
||||
cmp -s "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" \
|
||||
"$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf.pacnew" || fail "the pacnew is left for the administrator to merge"
|
||||
pass "the central boot order overrides legacy drop-ins and pacnew files without rewriting them"
|
||||
|
||||
reset_fixture
|
||||
rm "$OMARCHY_KERNEL_LIMINE_CONF"
|
||||
run_migration
|
||||
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
|
||||
pass "a missing central config is created with the exact kernel first"
|
||||
@@ -15,6 +15,14 @@ printf 'drop:%s\n' "$*" >>"$TEST_LOG"
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/omarchy-pkg-drop"
|
||||
|
||||
# omarchy-remove-ai-claude quits the running app before deleting its state;
|
||||
# a real pkill here would take the developer's own Claude with it.
|
||||
cat >"$tmp_dir/bin/pkill" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'pkill:%s\n' "$*" >>"$TEST_LOG"
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/pkill"
|
||||
|
||||
# omarchy-remove-ai-perplexity asks through gum whether the user's data goes
|
||||
# too. The stub answers "no" unless a test says otherwise and logs the call: a
|
||||
# real gum would hang the run, and one that answered "yes" on its own would be
|
||||
@@ -55,6 +63,27 @@ pass "ChatGPT removal keeps the Codex CLI's runtime cache"
|
||||
[[ -d $HOME/.codex ]] || fail "ChatGPT removal keeps the Codex CLI's config"
|
||||
pass "ChatGPT removal keeps the Codex CLI's config"
|
||||
|
||||
# The Claude Code CLI ships in its own package and keeps its state in
|
||||
# ~/.claude, ~/.claude.json, and ~/.cache/claude-cli-nodejs, so removing the
|
||||
# desktop app must not take it.
|
||||
fresh_home
|
||||
mkdir -p "$HOME/.config/Claude" "$HOME/.cache/Claude" "$HOME/.cache/claude-cli-nodejs" "$HOME/.claude"
|
||||
touch "$HOME/.claude.json"
|
||||
"$ROOT/bin/omarchy-remove-ai-claude" >/dev/null
|
||||
|
||||
for gone in .config/Claude .cache/Claude; do
|
||||
[[ ! -e $HOME/$gone ]] || fail "Claude removal deletes the desktop app's config and caches" "$gone"
|
||||
done
|
||||
pass "Claude removal deletes the desktop app's config and caches"
|
||||
|
||||
for kept in .claude .claude.json .cache/claude-cli-nodejs; do
|
||||
[[ -e $HOME/$kept ]] || fail "Claude removal keeps the Claude Code CLI's state" "$kept"
|
||||
done
|
||||
pass "Claude removal keeps the Claude Code CLI's state"
|
||||
|
||||
grep -qx 'pkill:-x claude-desktop' "$TEST_LOG" || fail "Claude removal quits the running app before deleting its state"
|
||||
pass "Claude removal quits the running app before deleting its state"
|
||||
|
||||
# LM Studio's models follow a relocatable home, named only by the pointer file.
|
||||
fresh_home
|
||||
mkdir -p "$tmp_dir/relocated-models/models"
|
||||
|
||||
@@ -299,3 +299,114 @@ grep -F 'move = { "(monitor_w-monitor_h*2/9-40)", "(monitor_h-monitor_h/4-40)" }
|
||||
grep -F 'move = { "(monitor_w-monitor_h*3/10-40)", "(monitor_h-monitor_h*27/80-40)" }' "$webcam_rules" >/dev/null || \
|
||||
fail "large webcam starts at its final corner position"
|
||||
pass "webcam size rules place the initial window in its final corner"
|
||||
|
||||
# The stop path reads the recording state file back and uses its contents as a
|
||||
# path -- ffmpeg writes beside it, `mv` replaces it, `rm -f` deletes its
|
||||
# preview -- so it has to live in the per-user runtime directory rather than
|
||||
# under a name in world-writable /tmp that another account can create first.
|
||||
recording_dir="$tmp_dir/recordings"
|
||||
mkdir -p "$recording_dir"
|
||||
|
||||
cat >"$stub_bin/pgrep" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 1
|
||||
SH
|
||||
|
||||
cat >"$stub_bin/omarchy-hyprland-monitor-focused" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'DP-1\n'
|
||||
SH
|
||||
|
||||
cat >"$stub_bin/gpu-screen-recorder" <<'SH'
|
||||
#!/bin/bash
|
||||
for i in "$@"; do
|
||||
[[ -n ${take_next:-} ]] && { : >"$i"; break; }
|
||||
[[ $i == "-o" ]] && take_next=1
|
||||
done
|
||||
sleep 5
|
||||
SH
|
||||
|
||||
cat >"$stub_bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
|
||||
chmod +x "$stub_bin"/pgrep "$stub_bin"/omarchy-hyprland-monitor-focused \
|
||||
"$stub_bin"/gpu-screen-recorder "$stub_bin"/omarchy-shell
|
||||
|
||||
# Compare that name across the run rather than demanding it be absent: the
|
||||
# whole point of the finding is that anyone can own it already, and a leftover
|
||||
# from a pre-fix recording would red-light the fixed script.
|
||||
tmp_state="/tmp/omarchy-screenrecord-filename"
|
||||
tmp_state_before=$(stat -c '%y %s' "$tmp_state" 2>/dev/null || true)
|
||||
|
||||
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
|
||||
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1
|
||||
|
||||
pkill -f "$stub_bin/gpu-screen-recorder" 2>/dev/null || true
|
||||
|
||||
[[ $(stat -c '%y %s' "$tmp_state" 2>/dev/null) == "$tmp_state_before" ]] ||
|
||||
fail "screen recording keeps no state under a fixed /tmp name"
|
||||
pass "screen recording keeps no state under a fixed /tmp name"
|
||||
|
||||
[[ -s $XDG_RUNTIME_DIR/omarchy-screenrecord-filename ]] ||
|
||||
fail "the recording state file lives in the per-user runtime directory" \
|
||||
"$(ls -a "$XDG_RUNTIME_DIR")"
|
||||
pass "the recording state file lives in the per-user runtime directory"
|
||||
|
||||
[[ $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-filename") == "$recording_dir"/* ]] ||
|
||||
fail "the recording state file names the recording that was started" \
|
||||
"$(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-filename")"
|
||||
pass "the recording state file names the recording that was started"
|
||||
|
||||
# The :-/tmp fallback would reopen the hole this PR closes. A recording
|
||||
# started without a session runtime dir has to land under the state directory.
|
||||
state_home="$tmp_dir/state-home"
|
||||
mkdir -p "$state_home/omarchy" "$tmp_dir/home-fallback"
|
||||
chmod 755 "$state_home/omarchy"
|
||||
tmp_state_before=$(stat -c '%y %s' "$tmp_state" 2>/dev/null || true)
|
||||
|
||||
env -u XDG_RUNTIME_DIR \
|
||||
HOME="$tmp_dir/home-fallback" \
|
||||
XDG_STATE_HOME="$state_home" \
|
||||
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
|
||||
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1
|
||||
|
||||
pkill -f "$stub_bin/gpu-screen-recorder" 2>/dev/null || true
|
||||
|
||||
[[ $(stat -c '%y %s' "$tmp_state" 2>/dev/null) == "$tmp_state_before" ]] ||
|
||||
fail "without a runtime dir, screen recording still keeps no state under a fixed /tmp name"
|
||||
pass "without a runtime dir, screen recording still keeps no state under a fixed /tmp name"
|
||||
|
||||
fallback_file="$state_home/omarchy/omarchy-screenrecord-filename"
|
||||
[[ -s $fallback_file ]] ||
|
||||
fail "without a runtime dir the recording state file lives in the state directory" \
|
||||
"$(ls -la "$state_home/omarchy" 2>/dev/null || true)"
|
||||
pass "without a runtime dir the recording state file lives in the state directory"
|
||||
|
||||
[[ $(<"$fallback_file") == "$recording_dir"/* ]] ||
|
||||
fail "the fallback state file names the recording that was started" \
|
||||
"$(<"$fallback_file")"
|
||||
pass "the fallback state file names the recording that was started"
|
||||
|
||||
# The overlay resizer reads the region file the recorder writes, so the two
|
||||
# have to resolve the same fallback as well as the same runtime dir.
|
||||
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
|
||||
echo "800x600+100+100" >"$state_home/omarchy/omarchy-screenrecord-region"
|
||||
env -u XDG_RUNTIME_DIR \
|
||||
HOME="$tmp_dir/home-fallback" \
|
||||
XDG_STATE_HOME="$state_home" \
|
||||
"$ROOT/bin/omarchy-capture-webcam-resize" reset
|
||||
|
||||
printf '%s\n' \
|
||||
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 133, y = 150 })' \
|
||||
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 727, y = 510 })' >"$expected_hyprctl_args"
|
||||
|
||||
if ! cmp -s "$OMARCHY_TEST_HYPRCTL_ARGS" "$expected_hyprctl_args"; then
|
||||
fail "without a runtime dir the webcam anchors to the recorded region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
|
||||
fi
|
||||
pass "without a runtime dir the webcam anchors to the recorded region"
|
||||
|
||||
mode=$(stat -c '%a' "$state_home/omarchy" 2>/dev/null || stat -f '%Lp' "$state_home/omarchy")
|
||||
[[ $mode == "700" ]] || fail "fallback directory is private even when it already existed" "mode: $mode"
|
||||
pass "fallback directory is private even when it already existed"
|
||||
@@ -31,10 +31,13 @@ allowed = {
|
||||
# them so the hook does not exist where it does not apply.
|
||||
"/usr/lib/systemd/system-sleep",
|
||||
# Written through a variable, so the scan below cannot see them at the point
|
||||
# they are written. Both drop configuration into another project's tree rather
|
||||
# than Omarchy's, which is why neither is a candidate for omarchy-settings.
|
||||
# they are written. These drop configuration into another project's tree
|
||||
# rather than Omarchy's and are not candidates for omarchy-settings.
|
||||
"/usr/share/chromium/extensions",
|
||||
"/usr/lib/firefox/distribution",
|
||||
# Claude's extension is registered only when the user selects Claude.
|
||||
"/usr/share/google-chrome/extensions",
|
||||
"/usr/share/microsoft-edge/extensions",
|
||||
# Static content that belongs in omarchy-settings. It cannot move there in the
|
||||
# same release that first ships omarchy-update-system-pkgs-when-conflicted: the
|
||||
# upgrade carrying the handler is the one that would hit the conflict, and the
|
||||
|
||||
@@ -15,6 +15,14 @@ cat >"$stub_bin/sudo" <<'STUB'
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
# omarchy-update-pacman wraps the transaction in a real PID 1 scope; the tests
|
||||
# must stay inside the fixture, so drop the wrapper's options and run the command.
|
||||
cat >"$stub_bin/systemd-run" <<'STUB'
|
||||
#!/bin/bash
|
||||
while [[ $1 == -* ]]; do shift; done
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
# Fails the first -Syu with the report under test, then succeeds unless the case
|
||||
# asked for the retry to fail too.
|
||||
cat >"$stub_bin/pacman" <<'STUB'
|
||||
@@ -40,7 +48,7 @@ fi
|
||||
echo "upgrade complete"
|
||||
STUB
|
||||
|
||||
chmod +x "$stub_bin/sudo" "$stub_bin/pacman"
|
||||
chmod +x "$stub_bin/sudo" "$stub_bin/systemd-run" "$stub_bin/pacman"
|
||||
|
||||
replaced="$test_tmp/replaced"
|
||||
|
||||
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user